简介:本资源面向希望快速上手二维码个性化生成与桌面工具打包的Python学习者与开发者,提供一套带图形界面的二维码生成器源码及可执行文件打包方案。核心逻辑基于qrcode与PIL库,支持自定义前景色、背景色、边框宽度,并可通过paste方法嵌入logo,实现视觉风格独特的二维码输出;同时借助PyInstaller将脚本打包为exe,使无Python环境的用户也能直接运行。压缩包整体约241.46MB,内含源码脚本、PyInstaller配置文件及打包产物等,spec文件可用于定制入口脚本、图标与窗口参数,dist目录存放可直接分发的可执行程序。目前已有598人学习下载,适合想掌握GUI工具开发、图像处理与程序打包流程的读者参考,可据此快速搭建自己的二维码生成工具并完成独立分发。
1. 从一张被扫码枪拒识的二维码说起:这个带 GUI 的 Python 工具到底能干什么
上周帮朋友改一张活动海报,他发来的二维码是渐变紫底加白色圆点,手机扫得挺顺,结果现场那台老扫码枪连扫三次都报错。拆开一看,前景色和背景色对比度只有 1.8:1,纠错等级还是 L。这事让我重新翻出自己压箱底的一个小工具——一个用 Python 写的酷炫二维码生成器,带图形界面,能直接打包成 exe 丢给不懂代码的同事用。它解决的不是"生成二维码"这种烂大街的需求,而是"生成一张既好看、又能被各种设备稳定识别的二维码"这个真实痛点。核心依赖就两个:qrcode负责编码矩阵,PIL(Pillow)负责像素级美化,最后用PyInstaller把整个 GUI 程序封成单文件 exe。适合谁?做运营物料的设计师、需要批量出码的活动执行、以及想拿一个完整 Python GUI + 打包案例练手的开发者。下面我按"能跑起来 → 能改参数 → 能避坑 → 能进阶"的顺序,把这个包拆开讲透。
2. 环境搭建与核心生成逻辑:qrcode 和 PIL 到底怎么配合
2.1 依赖安装与版本选择
这个工具的运行环境不复杂,但版本有讲究。qrcode在 7.x 之后对PIL的依赖方式有调整,PyInstaller对 Python 3.8 到 3.11 的支持最稳,再新的版本偶尔会在打包时漏掉PIL._tkinter_finder这类隐藏导入。我一般会建一个干净虚拟环境,避免和系统里已有的numpy、cv2打架。
# 创建虚拟环境,Python 3.8~3.11 均可 python -m venv qr_env # Windows 激活 qr_env\Scripts\activate # macOS / Linux 激活 source qr_env/bin/activate # 安装核心依赖,指定版本区间避免 API 变动 pip install "qrcode[pil]>=7.3,<8.0" "Pillow>=9.0" pyinstaller这里qrcode[pil]的方括号写法会自动把Pillow作为可选依赖装进来,省得你手动对齐版本。Pillow低于 9.0 时,Image.Resampling.LANCZOS这个重采样常量还不存在,代码里如果用了新写法会直接报AttributeError。PyInstaller不锁版本问题不大,但建议用 5.x 以上,它对tkinter的钩子处理更完整。
2.2 基础二维码生成:从数据到矩阵
qrcode的核心对象是QRCode,它把输入字符串编码成布尔矩阵。很多人直接调qrcode.make()就完事,但要做酷炫效果,必须拿到矩阵自己控制每个模块的绘制。下面这段是工具里最底层的生成函数:
import qrcode from qrcode.constants import ERROR_CORRECT_H def build_qr_matrix(data, version=None, box_size=10, border=4, ec_level=ERROR_CORRECT_H): """ 生成二维码矩阵,返回 QRCode 对象供后续 PIL 绘制 :param data: 要编码的字符串,支持中文(qrcode 内部会做 UTF-8 编码) :param version: 二维码版本 1-40,None 表示自动根据数据长度选择 :param box_size: 每个模块的像素边长,最终图像尺寸 = (模块数 + 2*border) * box_size :param border: 静默区宽度,单位是模块数,标准要求至少 4 :param ec_level: 纠错等级,H 约 30% 容错,适合加 logo """ qr = qrcode.QRCode( version=version, error_correction=ec_level, box_size=box_size, border=border, ) qr.add_data(data) qr.make(fit=True) # fit=True 时 version 参数被忽略,自动选最小可用版本 return qr逻辑说明:add_data只是把数据塞进缓冲区,make(fit=True)才真正执行编码并确定版本号。box_size和border在这里不影响矩阵内容,只影响后续qr.get_matrix()返回的二维列表尺寸——实际上get_matrix()返回的列表已经包含了 border 的空白行和列。参数上,ec_level选 H 是酷炫二维码的默认操作,因为你要往中间贴 logo,logo 会遮挡部分模块,没有 30% 容错基本必翻车。version留 None 让库自动算,除非你要固定尺寸做批量排版。
2.3 用 PIL 做视觉定制:颜色、圆点、logo 粘贴
拿到矩阵后,真正的"酷炫"发生在 PIL 这一层。工具里没有直接用qr.make_image(),而是自己遍历矩阵,用ImageDraw逐模块绘制。这样做的好处是每个模块可以画成圆角矩形、圆形,甚至根据位置渐变。
from PIL import Image, ImageDraw def render_styled_qr(qr, fg_color="#1a1a2e", bg_color="#ffffff", dot_style="rounded", logo_path=None, logo_ratio=0.22): """ 根据矩阵渲染带样式的二维码图像 :param qr: build_qr_matrix 返回的 QRCode 对象 :param fg_color: 前景色,深色,需与 bg_color 保持高对比 :param bg_color: 背景色,浅色 :param dot_style: "square" 或 "rounded",圆点风格更柔和但识别率略降 :param logo_path: 中心 logo 图片路径,None 则不贴 :param logo_ratio: logo 边长占二维码边长的比例,建议不超过 0.25 """ matrix = qr.get_matrix() box = qr.box_size size = len(matrix) * box img = Image.new("RGB", (size, size), bg_color) draw = ImageDraw.Draw(img) for r, row in enumerate(matrix): for c, val in enumerate(row): if not val: continue x0, y0 = c * box, r * box x1, y1 = x0 + box, y0 + box if dot_style == "rounded": radius = box // 3 draw.rounded_rectangle([x0, y0, x1, y1], radius=radius, fill=fg_color) else: draw.rectangle([x0, y0, x1, y1], fill=fg_color) if logo_path: logo = Image.open(logo_path).convert("RGBA") logo_size = int(size * logo_ratio) logo = logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS) # 给 logo 加白色圆角底,避免直接压在模块上导致对比度不足 mask = Image.new("L", (logo_size, logo_size), 0) ImageDraw.Draw(mask).rounded_rectangle( [0, 0, logo_size, logo_size], radius=logo_size // 6, fill=255 ) pos = ((size - logo_size) // 2, (size - logo_size) // 2) img.paste(logo, pos, mask) return img这段代码里几个关键参数直接决定成品能不能扫:fg_color和bg_color的亮度差要足够,我一般要求对比度至少 4.5:1,深蓝配白、黑配浅灰都行,但别用红配绿。dot_style="rounded"时圆角半径取box//3,再大模块之间会断开,扫码器可能把相邻模块误判为独立点。logo_ratio超过 0.25 时,即使 H 级纠错也救不回来,这是血泪经验——曾经有个客户非要 logo 占三分之一,结果微信长按识别直接没反应。paste的第三个参数是遮罩,用圆角矩形遮罩能让 logo 边缘不突兀,同时白色底也充当了 logo 区域的静默区。
2.4 GUI 层:tkinter 输入框与实时预览
工具带界面,用的是 Python 自带的tkinter,不需要额外装 Qt 那套重家伙。界面逻辑很直白:一个Entry收内容,两个ColorChooser选前景背景色,一个Checkbutton切换圆点风格,一个Button触发保存。实时预览用ImageTk.PhotoImage把 PIL 图像贴到Label上。
import tkinter as tk from tkinter import colorchooser, filedialog from PIL import ImageTk class QRApp: def __init__(self, root): self.root = root self.root.title("酷炫二维码生成器") self.fg = "#1a1a2e" self.bg = "#ffffff" tk.Label(root, text="内容:").grid(row=0, column=0, sticky="e") self.entry = tk.Entry(root, width=40) self.entry.grid(row=0, column=1, columnspan=2, pady=6) tk.Button(root, text="前景色", command=self.pick_fg).grid(row=1, column=0) tk.Button(root, text="背景色", command=self.pick_bg).grid(row=1, column=1) tk.Button(root, text="生成预览", command=self.preview).grid(row=1, column=2) self.preview_label = tk.Label(root) self.preview_label.grid(row=2, column=0, columnspan=3, pady=10) tk.Button(root, text="保存 PNG", command=self.save).grid(row=3, column=1) def pick_fg(self): _, self.fg = colorchooser.askcolor(color=self.fg) def pick_bg(self): _, self.bg = colorchooser.askcolor(color=self.bg) def preview(self): qr = build_qr_matrix(self.entry.get() or "https://example.com") img = render_styled_qr(qr, fg_color=self.fg, bg_color=self.bg) self.tk_img = ImageTk.PhotoImage(img) self.preview_label.config(image=self.tk_img) def save(self): path = filedialog.asksaveasfilename(defaultextension=".png") if path: qr = build_qr_matrix(self.entry.get() or "https://example.com") render_styled_qr(qr, fg_color=self.fg, bg_color=self.bg).save(path)ImageTk.PhotoImage必须保持一个引用(这里赋给self.tk_img),否则 Python 垃圾回收会把图像对象清掉,预览区就是一片空白——这是 tkinter 最经典的坑之一。colorchooser.askcolor返回(rgb_tuple, hex_string),我们只用 hex。保存时重新生成一次而不是复用预览图,是为了保证保存的图不带 GUI 缩放带来的插值失真。
3. PyInstaller 打包 exe:spec 文件里每个参数都值得抠
3.1 为什么不用命令行直接打包
pyinstaller -F -w 二维码生成器.py确实能出 exe,但这个项目带了图标、可能还要嵌入字体或默认 logo,命令行参数会越堆越长。项目里直接给了二维码生成器.spec,这是 PyInstaller 第一次运行后生成的配置文件,改它比每次敲一长串命令靠谱。spec 文件本质是一个 Python 脚本,PyInstaller 执行它来收集分析结果。
3.2 spec 文件关键字段逐项说明
# 二维码生成器.spec a = Analysis( ['二维码生成器.py'], # 入口脚本,路径相对于 spec 所在目录 pathex=[], binaries=[], datas=[('assets/logo.png', 'assets')], # 非代码资源,元组是 (源路径, 打包内目标目录) hiddenimports=['PIL._tkinter_finder'], # 显式声明 PyInstaller 分析不到的隐藏导入 hookspath=[], runtime_hooks=[], excludes=['numpy', 'cv2', 'matplotlib'], # 排除用不到的重型库,能砍掉几十 MB noarchive=False, ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, a.binaries, a.datas, name='酷炫二维码生成器', debug=False, strip=False, upx=True, # 用 UPX 压缩,杀软误报率会上升,见避坑章 console=False, # GUI 程序必须 False,否则带黑框 icon='assets/app.ico', # exe 图标,必须是 .ico 格式 )datas里如果写相对路径,PyInstaller 在运行时会把资源解压到sys._MEIPASS临时目录,代码里读 logo 要用os.path.join(sys._MEIPASS, 'assets', 'logo.png')做兼容,否则打包后一贴 logo 就报文件找不到。hiddenimports里PIL._tkinter_finder是 Pillow 和 tkinter 配合时的动态导入,静态分析扫不到,不写进去打包后预览功能直接崩。excludes是体积优化的关键,numpy和cv2一旦被误收集,exe 轻松破 200MB。console=False和icon是 GUI 程序的门面,upx=True能压到原来一半大小,但代价是部分杀毒软件会报启发式威胁。
3.3 打包命令与产物目录
# 在 spec 文件所在目录执行 pyinstaller 二维码生成器.spec --clean --noconfirm--clean清掉上次打包的缓存,避免旧字节码混进来;--noconfirm跳过覆盖确认,适合脚本化。执行后dist/目录下就是最终 exe,build/是中间产物,里面是分析日志和临时文件,分发时只拿dist里的东西。如果 exe 启动闪退,第一件事是看build/二维码生成器/warn-二维码生成器.txt,里面列了所有没找到的模块和导入警告,比盲目猜快得多。
3.4 单文件模式与目录模式的取舍
spec 里EXE只传了a.binaries和a.datas,没有COLLECT,这是单文件模式。单文件 exe 启动时要把所有内容解压到临时目录,冷启动比目录模式慢 1 到 3 秒,但分发时只有一个文件,适合发给外部人员。如果程序要频繁启动,或者资源文件很大,改成目录模式更合适:在 spec 末尾加coll = COLLECT(exe, a.binaries, a.datas, name='酷炫二维码生成器'),产物是一个文件夹,启动快,但得整个文件夹一起发。
4. 避坑与排查:扫码失败、打包报错、杀软误报的现场记录
4.1 现象:手机能扫,专业扫码枪报错
原因:扫码枪对对比度和静默区比手机摄像头苛刻得多。手机 App 有图像增强算法,扫码枪基本是硬阈值二值化。前景背景亮度差不足、或者border小于 4 模块时,扫码枪直接拒识。
解决:把fg_color和bg_color的亮度差拉到 4.5:1 以上,border保持默认 4 不要动。如果非要浅色前景,至少保证前景是深灰而不是中灰。测试时别只用微信扫,找个手持扫码枪或者用zxing命令行工具验一遍。
4.2 现象:贴了 logo 后微信长按识别失败
原因:logo 遮挡面积超过纠错容量,或者 logo 本身颜色和二维码模块混在一起,导致定位图案(三个角上的回字形)被破坏。
解决:logo_ratio压到 0.22 以内,纠错等级锁死 H。logo 不要贴到三个定位角上,居中放置。如果 logo 是透明底 PNG,先合成到白色圆角底上再贴,避免透明区域露出下面的模块造成视觉噪声。
4.3 现象:打包后运行提示No module named 'PIL._tkinter_finder'
原因:PyInstaller 的静态分析没有追踪到 Pillow 在 tkinter 场景下的动态导入,这个模块在打包时被漏掉了。
解决:在 spec 的hiddenimports列表里显式加上'PIL._tkinter_finder',重新打包。同类问题还可能出现在qrcode的某些编码后端上,如果报No module named 'qrcode.image.pil',同样加进 hiddenimports。
4.4 现象:exe 被 Windows Defender 或某数字杀软直接删除
原因:PyInstaller 生成的引导程序(bootloader)是很多恶意软件常用的打包方式,加上upx=True压缩后特征更明显,启发式引擎容易误判。
解决:先试upx=False重新打包,体积大一点但误报率明显下降。如果还报,给 exe 加数字签名(个人开发者可以用自签名证书),或者把单文件模式改成目录模式,目录模式的误报率低于单文件。分发时附一句"若被拦截请添加信任",比让用户自己猜强。
4.5 现象:GUI 预览区空白,但保存的 PNG 正常
原因:ImageTk.PhotoImage对象被局部变量引用,函数返回后引用计数归零,图像被回收,Label 上只剩一个空壳。
解决:把PhotoImage对象挂到实例属性上(如self.tk_img = ImageTk.PhotoImage(img)),或者挂到 Label 自身的属性上。这是 tkinter 图像显示最经典的坑,和二维码本身无关,但在这个工具里必踩。
5. 进阶技巧:批量出码、动态样式与识别率自检
5.1 批量生成带序号的二维码
活动物料经常要一次出几百张码,每张内容不同。把生成逻辑从 GUI 里抽出来,写个批处理脚本,读 CSV 或 Excel 列,循环调build_qr_matrix和render_styled_qr,文件名用序号或内容哈希。注意qrcode对象不要复用,每次add_data前重新构造,否则数据会叠加。
import csv, os from build_qr_matrix import build_qr_matrix, render_styled_qr os.makedirs("output", exist_ok=True) with open("codes.csv", encoding="utf-8") as f: for i, row in enumerate(csv.DictReader(f)): qr = build_qr_matrix(row["url"]) img = render_styled_qr(qr, fg_color="#0f3460", bg_color="#f5f5f5") img.save(f"output/qr_{i:04d}.png")csv.DictReader按表头取列,i:04d保证文件名排序和生成顺序一致。批量场景下box_size建议固定 10,version固定一个够用的值(比如 5),这样所有图尺寸一致,排版时不用再对齐。
5.2 用 zxing 做识别率自检
生成完不验证等于没生成。我习惯在批量脚本末尾加一道自检:用pyzbar或zxing解码刚生成的图,比对内容是否一致。pyzbar在 Windows 上需要 Visual C++ 运行库,zxing的 Python 绑定更轻。
from PIL import Image from pyzbar.pyzbar import decode def verify(path, expected): result = decode(Image.open(path)) if not result or result[0].data.decode("utf-8") != expected: print(f"[FAIL] {path} 识别失败或内容不符") return False return True这一步能拦住 90% 的"看着好看但扫不出来"的翻车。自检不通过就调对比度或缩小 logo,别靠肉眼判断。
5.3 动态样式:根据内容长度自动调 box_size
内容越长,二维码版本越高,模块数越多。如果box_size固定,长内容的图会很大,短内容的图又太小。可以在build_qr_matrix之后读qr.version,按版本反推box_size,让最终图像边长控制在一个区间内。
| 版本范围 | 模块数(含 border) | 建议 box_size | 最终边长约 |
|---|---|---|---|
| 1-5 | 25-41 | 12 | 300-492 |
| 6-10 | 45-61 | 10 | 450-610 |
| 11-20 | 65-101 | 8 | 520-808 |
| 21-40 | 105-181 | 6 | 630-1086 |
这张表是我自己批量出码时总结的,目标是让边长落在 400 到 800 像素之间,既够印刷精度,又不至于文件过大。qr.version在make()之后才有效,fit=True时它是自动算出来的,直接读就行。
从那以后我每次生成完二维码,不管多急,都强制走一遍pyzbar自检再交付。希望帮到你。
本文还有配套的精品资源,点击获取