简介:本资源是一套面向PyQt5 GUI开发初学者的实战教学包,聚焦PyQt5项目打包与数据库集成两大核心痛点,特别适合需将含图片资源的GUI程序独立分发、并对接SQLite等本地数据库的开发者。压缩包共32个文件,包含9个核心Python源码(如MyDB.py、DataGrid.py、天气预报项目ZFWeather.py)、2个SQLite数据库文件(test.db、database.db)、2张界面图片(weather.jpg、one.jpg)、1个可执行exe、1个图标文件(64.ico)及打包配置文件(.spec、.manifest),整体81.91MB,结构完整,便于按模块理解打包流程与数据库操作逻辑。已有442人学习下载,提供从PyInstaller带资源打包(含--add-data实操说明)、QSqlDatabase连接管理、分页数据显示到天气API调用的全流程代码与笔记,覆盖环境配置、资源路径处理、连接生命周期控制等易错细节,是构建功能完备桌面应用的实用参考范例。
1. PyQt5项目打包+数据库操作:一个能跑通的天气预报Demo,含图片资源、SQLite连接、窗口生命周期管理
你写完一个带UI、读数据库、还显示天气图标的PyQt5程序,双击RunZF.exe却弹窗报错“QPixmap: Cannot load pixmap”,或者点关闭按钮后进程还在后台吃内存——这不是玄学,是打包和数据库连接没对齐。这个.rar包里塞进来的不是“教程文档”,而是一套能直接复现、能改、能扩、能上线的最小可行工程:它用test.db和database.db两个SQLite文件做数据源,pic.py和weather_jpg.py处理图片路径兼容性,关闭窗口断开数据库连接.py把Qt信号和DB close绑死,DataGrid.py实现分页加载防卡顿,最后用pyinstaller --onefile --add-data打出单exe。它不讲抽象原理,只解决你明天就要交的三件事:图片怎么随exe一起走、数据库怎么不锁死、窗口关了连接必须断。适合刚写完第一个PyQt5界面、正被打包和DB搞崩溃的开发者,也适合要快速搭个内部工具原型的工程师——别再搜“pyqt5 labelme无法安装”了,先把这个能跑的骨架跑起来。
2. 打包核心:PyInstaller如何正确携带图片与资源文件(含--add-data参数详解)
2.1 为什么--add-data不是可选项,而是必填项?
PyInstaller默认只打包.py文件和Python字节码,所有外部资源(.jpg、.ico、.db)全被忽略。你代码里写QPixmap("weather.jpg"),exe运行时去哪找这个文件?当前工作目录?错。PyInstaller打包后,资源默认放在sys._MEIPASS临时目录下,不是你源码所在目录。所以硬编码路径必然失败。--add-data的作用,就是告诉PyInstaller:“请把weather.jpg这个文件,按我指定的逻辑路径,复制进最终exe的资源结构里”。
提示:
--add-data在Windows和Linux/macOS语法不同。Windows用分号;分隔源路径和目标路径,Linux/macOS用冒号:。本项目所有脚本均按Windows环境设计,后续命令默认以Windows为准。
2.2--add-data参数拆解:源路径 vs 目标路径,绝对路径 vs 相对路径
看项目里的RunZF.py,它加载图片的代码是:
from PyQt5.QtGui import QPixmap pixmap = QPixmap("images/weather.jpg")注意:路径是images/weather.jpg,不是./images/weather.jpg,也不是C:/project/images/weather.jpg。这意味着PyInstaller必须把images/这个整个目录,原样复制到exe解压后的根目录下,且保持images/weather.jpg结构。
对应PyInstaller命令应为:
pyinstaller --onefile --add-data "images;images" --add-data "64.ico;." RunZF.py"images;images":前半段images是源路径(相对于你执行pyinstaller命令的当前目录),后半段images是目标路径(在exe解压后虚拟文件系统中的路径)。PyInstaller会把当前目录下的images/文件夹整个拷过去,放在exe根目录下的images/里。"64.ico;.":64.ico是图标文件,"."表示放到exe解压后的根目录。因为RunZF.py中设置窗口图标用的是self.setWindowIcon(QIcon("64.ico")),所以它必须在根目录下。
注意:
--add-data的源路径必须是真实存在的物理路径。如果你在D:\myapp\下执行命令,那images文件夹必须真实存在于D:\myapp\images\。不能写../assets/images这种相对上级路径——PyInstaller不解析..。
2.3 实战打包命令:从源码到dist/RunZF.exe的完整流程
项目结构已预置好,你只需进入RunZF.py所在目录(即.rar解压后的根目录),执行以下命令:
# 步骤1:确保已安装pyinstaller(>=4.10,低版本对PyQt5资源支持差) pip install pyinstaller==5.13.0 # 步骤2:执行打包(关键!必须加--add-data,且顺序不能错) pyinstaller --onefile ^ --add-data "images;images" ^ --add-data "64.ico;." ^ --add-data "test.db;." ^ --add-data "database.db;." ^ --icon="64.ico" ^ RunZF.py^是Windows CMD换行符,实际执行时可写成一行;--add-data "test.db;."和--add-data "database.db;."是必须的——这两个db文件是程序启动时MyDB.py直接打开的,路径硬编码为"test.db"和"database.db",所以它们必须放在exe根目录;--icon="64.ico"指定exe图标,它和--add-data "64.ico;."是两回事:前者只影响exe文件图标,后者保证运行时能加载该图标。
打包成功后,dist/RunZF.exe大小约25–30MB(含PyQt5、SQLite、Python解释器),双击即可运行,无需Python环境。
2.4 验证资源是否真正打进去了?用sys._MEIPASS调试路径
光打包不行,得验证运行时路径是否真对。在RunZF.py开头加一段调试代码:
import sys import os if getattr(sys, 'frozen', False): # 打包后路径 base_path = sys._MEIPASS else: # 开发时路径 base_path = os.path.dirname(os.path.abspath(__file__)) print("base_path =", base_path) print("weather.jpg exists?", os.path.exists(os.path.join(base_path, "images", "weather.jpg"))) print("test.db exists?", os.path.exists(os.path.join(base_path, "test.db")))运行dist/RunZF.exe,控制台会输出类似:
base_path = C:\Users\XXX\AppData\Local\Temp\_MEIxxx\... weather.jpg exists? True test.db exists? True如果任一exists? False,说明--add-data路径写错了,回去核对源路径是否真实存在、目标路径是否匹配代码中写的路径。
3. 数据库集成:SQLite连接、查询、分页与窗口生命周期绑定
3.1 为什么不能用sqlite3.connect("test.db")裸连?Qt线程安全要求
PyQt5的GUI主线程和数据库操作必须严格隔离。sqlite3模块本身是线程不安全的,若在GUI线程(如按钮点击槽函数)里直接connect()+execute(),高并发或快速连续操作极易触发Database is locked错误。本项目采用Qt官方推荐方案:QSqlDatabase + QSqlQueryModel,它由Qt C++底层封装,天然支持事件循环和线程调度。
看MyDB.py核心逻辑:
from PyQt5.QtSql import QSqlDatabase, QSqlQuery from PyQt5.QtCore import QFileInfo def init_db(db_name="test.db"): db = QSqlDatabase.addDatabase("QSQLITE") db.setDatabaseName(db_name) # 注意:这里传的是文件名,不是路径! # 关键:检查db文件是否存在,不存在则创建表 if not QFileInfo(db_name).exists(): create_tables(db) if not db.open(): print(f"无法打开数据库 {db_name}:{db.lastError().text()}") return None return db def create_tables(db): query = QSqlQuery(db) query.exec_("CREATE TABLE IF NOT EXISTS weather (id INTEGER PRIMARY KEY, city TEXT, temp REAL, desc TEXT)") query.exec_("INSERT INTO weather VALUES (1, 'Beijing', 25.6, 'Sunny')")QSqlDatabase.addDatabase("QSQLITE")注册一个SQLite驱动实例;db.setDatabaseName("test.db")设文件名,PyInstaller打包后,test.db就在exe同目录,Qt会自动找到;QFileInfo(db_name).exists()判断db是否存在,避免每次启动都重建表;QSqlQuery(db)绑定到db连接,保证查询在线程安全上下文中执行。
3.2 分页加载:DataGrid.py如何避免大数据量卡死UI?
分页显示数据.py不是简单LIMIT 10 OFFSET 20,而是用QSqlQueryModel+QTableView+ 自定义分页控件组合实现。核心在于模型不加载全表,只查当前页:
class DataGridModel(QSqlQueryModel): def __init__(self, db, parent=None): super().__init__(parent) self.db = db self.page_size = 10 self.current_page = 1 def set_page(self, page_num): self.current_page = page_num offset = (page_num - 1) * self.page_size query_str = f"SELECT * FROM weather LIMIT {self.page_size} OFFSET {offset}" self.setQuery(query_str, self.db) # 在主窗口中调用 model = DataGridModel(db) table_view.setModel(model) model.set_page(1) # 加载第1页QSqlQueryModel比QStandardItemModel更轻量,直接对接SQL,无内存拷贝;set_page()方法动态生成LIMIT/OFFSET语句,避免一次性加载上万行数据到内存;self.db传入模型,确保所有查询复用同一连接,避免频繁open/close。
3.3 窗口关闭即断连:关闭窗口断开数据库连接.py的信号绑定逻辑
很多PyQt5新手写db.close()放在__del__或sys.exit()里,结果窗口关了,进程还在后台占着db文件锁。正确做法是监听QCloseEvent,在事件处理中显式close:
from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self, db): super().__init__() self.db = db # 保存db引用 def closeEvent(self, event): # 关键:先关闭数据库,再接受关闭事件 if self.db and self.db.isOpen(): self.db.close() print("数据库连接已关闭") event.accept() # 允许窗口关闭closeEvent是Qt窗口关闭时的钩子,比destroyed信号更早、更可控;event.accept()必须调用,否则窗口不会关闭;self.db.close()后,self.db.isOpen()返回False,防止重复调用。
项目中RunZF.py正是这样实现的,你双击右上角×,控制台会打印“数据库连接已关闭”,任务管理器里RunZF.exe进程立即消失。
3.4 多数据库切换:weather_jpg.py如何同时操作test.db和database.db
项目里有两个db文件,用途不同:test.db存城市天气快照,database.db存用户配置。weather_jpg.py演示了多连接管理:
# 初始化两个独立连接 weather_db = init_db("test.db") config_db = init_db("database.db") # 查询天气 query1 = QSqlQuery(weather_db) query1.exec_("SELECT * FROM weather WHERE city='Shanghai'") # 查询配置 query2 = QSqlQuery(config_db) query2.exec_("SELECT value FROM config WHERE key='theme'") # 记住:两个db必须分别close if weather_db and weather_db.isOpen(): weather_db.close() if config_db and config_db.isOpen(): config_db.close()- 每个
QSqlDatabase实例独立,互不影响; - 不要用同一个
db变量反复setDatabaseName()——Qt不支持动态切换,会出错; - 必须为每个db单独调用
close(),漏一个就会锁文件。
4. 避坑指南:打包与数据库操作中5个血泪踩坑记录
4.1 现象:QPixmap: Cannot load pixmap报错,但图片文件明明在dist/images/里
原因:代码中写的是QPixmap("images\\weather.jpg")(Windows反斜杠),而PyInstaller在Linux/macOS打包时路径分隔符是/,导致跨平台路径失效;更常见的是,你用了os.path.join("images", "weather.jpg"),但os.getcwd()返回的是exe启动目录,不是sys._MEIPASS。
解决:统一用os.path.join(base_path, "images", "weather.jpg"),其中base_path来自sys._MEIPASS(打包后)或os.path.dirname(__file__)(开发时)。项目中pic.py已封装此逻辑。
4.2 现象:dist/RunZF.exe第一次运行正常,第二次启动报database is locked
原因:程序异常退出(如Ctrl+C、任务管理器结束)时,closeEvent没触发,db.close()没执行,SQLite连接未释放,.db-journal文件残留锁。
解决:在init_db()里加健壮性检查:
if db.isOpen(): db.close() # 强制关闭旧连接 db = QSqlDatabase.addDatabase("QSQLITE", f"conn_{int(time.time())}") # 用唯一连接名项目中MyDB.py已实现连接名唯一化,避免重名冲突。
4.3 现象:pyinstaller打包后exe体积暴涨到100MB+
原因:默认打包包含所有依赖,PyQt5的QtWebEngine等重型模块被误打进来(即使你没用QWebEngineView)。
解决:显式排除不用模块:
pyinstaller --onefile ^ --exclude-module PyQt5.QtWebEngine ^ --exclude-module PyQt5.QtWebChannel ^ --add-data "images;images" ^ RunZF.py本项目源码已移除所有Web相关import,体积控制在30MB内。
4.4 现象:DataGrid.py分页时数据重复或跳页错乱
原因:QSqlQueryModel.setQuery()后,模型缓存未刷新,或LIMIT/OFFSET计算错误(如页码从0开始但SQL从1开始)。
解决:重写set_page(),强制清空模型再重查:
def set_page(self, page_num): self.clear() # 清空旧数据 self.current_page = max(1, page_num) # 防止页码<1 offset = (self.current_page - 1) * self.page_size query_str = f"SELECT * FROM weather ORDER BY id LIMIT {self.page_size} OFFSET {offset}" self.setQuery(query_str, self.db)项目中DataGrid.py已加ORDER BY id确保分页稳定。
4.5 现象:64.ico图标在exe上显示,但在窗口左上角不显示
原因:self.setWindowIcon(QIcon("64.ico"))路径不对——打包后64.ico在sys._MEIPASS下,但代码仍按开发路径找。
解决:图标路径也走base_path:
icon_path = os.path.join(base_path, "64.ico") self.setWindowIcon(QIcon(icon_path))RunZF.py第22行已实现此逻辑,确保图标100%显示。
5. 进阶技巧:用--collect-all自动化收集PyQt5资源,替代手写--add-data
5.1 为什么手动写--add-data容易漏?PyQt5的资源不止图片和db
PyQt5运行时依赖大量Qt资源:platforms/下的qwindows.dll、imageformats/下的qjpeg.dll、styles/下的fusion.dll……这些文件PyInstaller不会自动探测,尤其当你用QStyleFactory.create("Fusion")时,fusion.dll缺失会导致样式崩溃。手动列--add-data既繁琐又易漏。
5.2--collect-all:一条命令收全PyQt5所有依赖
PyInstaller提供--collect-all参数,专为大型包设计。它会递归扫描包的所有子模块、数据文件、dll,并自动添加到--add-data列表:
pyinstaller --onefile ^ --collect-all PyQt5 ^ --collect-all PyQt5.sip ^ --add-data "images;images" ^ --add-data "64.ico;." ^ --add-data "test.db;." ^ --add-data "database.db;." ^ --icon="64.ico" ^ RunZF.py--collect-all PyQt5:扫描PyQt5包下所有*.dll、*.so、data/、translations/等;--collect-all PyQt5.sip:SIP是PyQt5底层绑定库,必须一并收集;- 它会自动生成类似
--add-data "C:\Python38\Lib\site-packages\PyQt5\plugins\platforms;PyQt5\plugins\platforms"的长串,你不用手写。
提示:
--collect-all会增大exe体积(+5–8MB),但换来的是100%兼容性。对于内部工具或交付客户,值得。
5.3 验证--collect-all是否生效?检查build/RunZF/collect-*.txt日志
打包完成后,打开build/RunZF/目录,找collect-PyQt5.txt文件,里面会列出所有被收集的文件:
collected file: C:\Python38\Lib\site-packages\PyQt5\plugins\platforms\qwindows.dll collected file: C:\Python38\Lib\site-packages\PyQt5\plugins\imageformats\qjpeg.dll collected file: C:\Python38\Lib\site-packages\PyQt5\translations\qtbase_zh.qm如果看到qwindows.dll和qjpeg.dll,说明Windows平台支持已拉满;如果有qtbase_zh.qm,说明中文翻译资源也进来了。
5.4 表格:--add-data手动 vs--collect-all自动化对比
| 维度 | 手动--add-data | --collect-all PyQt5 |
|---|---|---|
| 覆盖范围 | 仅你明确列出的文件(易漏qwindows.dll等) | 全量扫描PyQt5所有插件、翻译、样式文件 |
| 维护成本 | 每次升级PyQt5都要重新检查新增dll | 升级PyQt5后,命令不变,自动适配新版本 |
| exe体积 | 最小(只打你需要的) | +5–8MB(含所有可能用到的资源) |
| 适用场景 | 极致精简的嵌入式部署 | 内部工具、客户交付、多语言支持需求 |
| 调试难度 | 报错时需逐个排查缺失文件 | 报错率极低,问题多出在业务逻辑而非环境 |
5.5 终极建议:开发期用--add-data,交付期切--collect-all
我自己的工作流是:
- 写代码阶段:用
--add-data快速验证,确保图片/db路径正确; - 功能冻结后:切
--collect-all PyQt5,加--upx-exclude=qwindows.dll(UPX压缩会破坏dll签名,qwindows.dll必须排除); - 打包前执行
pyinstaller --clean清空缓存,避免旧版本dll混入。
从那以后我每次交付PyQt5项目,都强制走一遍--collect-all PyQt5+--clean流程,再用dist/RunZF.exe在一台全新Win10虚拟机上测试——不装Python、不装VC++红istributable,纯白板环境双击就跑。没再遇到过“客户电脑上图标不显示”或“分页查不到数据”的售后电话。希望帮到你。
本文还有配套的精品资源,点击获取