简介:这是一份面向PyQt5初学者与GUI开发者的实战型学习资源,聚焦PyQt5桌面应用开发中的两大关键难点:含图片资源的可执行文件打包与SQLite数据库集成操作。资源提供完整项目源码、分步教程笔记及可直接运行的exe示例,覆盖PyInstaller多参数打包(如--add-data处理图片路径)、QSqlDatabase内存/文件数据库连接、数据增删查改、窗口关闭时安全断开连接等核心实践场景,并延伸至天气预报API调用与JSON解析,形成GUI+数据+网络的闭环能力训练。压缩包共32个文件,含9个Python源码(如MyDB.py、DataGrid.py、ZFWeather.py)、2个SQLite数据库文件、2张JPG图片、1个ICO图标、1个EXE可执行程序及spec/build/dist等打包产物,整体81.91MB,结构清晰便于对照学习。已有444人下载学习,适合希望快速掌握PyQt5工程化落地能力的开发者。
1. PyQT5项目打包+数据库操作:一个能跑通的天气预报Demo,含图片资源嵌入、SQLite连接管理、窗口关闭自动断连三件套
你写完一个PyQT5天气预报界面,本地运行丝滑流畅,图标漂亮、图片加载正常、数据库增删改查全对——结果用PyInstaller打包成exe双击就闪退?控制台一闪而过,连报错都看不到;或者图片全变空白,数据库提示“unable to open database file”;更玄学的是:程序关掉后后台还挂着sqlite3连接,下次启动直接报“database is locked”。这不是你代码写得差,是PyInstaller对GUI资源路径的处理机制和Qt数据库连接生命周期没对齐。这个压缩包里给的不是“理论教程”,而是一套已验证能落地的组合方案:RunZF.py主入口 +MyDB.py连接池封装 +关闭窗口断开数据库连接.py显式释放逻辑 +pic.py资源路径适配器 +dist/RunZF.exe可执行文件实测样本。它专治三类人:刚学完PyQT5控件但卡在部署环节的新人、手头有现成GUI项目却不敢交客户的中级开发者、被客户临时要求“打包发个exe我试试”的救火队员。所有源码基于Python 3.8 + PyQt5 5.15.9(兼容Windows 10/11),不依赖外部数据库服务,SQLite单文件即开即用。
2. PyInstaller打包实战:从命令行参数到资源路径硬编码的完整链路
2.1 为什么--add-data必须分号隔开,且路径方向不能反?
PyInstaller在Windows下使用分号;作为--add-data参数的路径分隔符,前半段是源路径(开发时你项目里的相对路径),后半段是目标路径(打包后exe解压到临时目录时的相对位置)。很多人写成--add-data 'images;images'或--add-data 'images\*.jpg;images',结果图片全丢——因为PyInstaller不支持通配符,也不识别反斜杠\在命令行中的转义歧义。正确写法必须用正斜杠/,且每个资源单独加:
pyinstaller --onefile --windowed --icon=64.ico ^ --add-data "images/weather.jpg;images" ^ --add-data "images/one.jpg;images" ^ --add-data "weather_jpg.py;." ^ --add-data "test.db;." ^ RunZF.py提示:
^是Windows CMD续行符,Linux/macOS用\。--windowed禁用黑框,--icon指定exe图标,test.db放在根目录而非./db/下,是为了让MyDB.py中os.path.join(os.path.dirname(sys.executable), "test.db")能稳定定位——这是后续所有数据库操作的前提。
2.2pic.py:用getattr(sys, '_MEIPASS', os.path.dirname(__file__))动态解析资源路径
PyInstaller打包后,所有资源文件(图片、db、py模块)会被解压到一个临时目录(如C:\Users\XXX\AppData\Local\Temp\_MEIxxx),此时__file__指向的是这个临时路径,而非原始开发目录。硬编码"./images/weather.jpg"必然失败。pic.py的核心逻辑就是兜底:
import sys import os def resource_path(relative_path): """获取资源绝对路径,兼容开发环境与PyInstaller打包环境""" try: # PyInstaller创建的临时文件夹路径 base_path = sys._MEIPASS except Exception: # 开发时直接用当前文件所在目录 base_path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base_path, relative_path) # 使用示例 weather_img = resource_path("images/weather.jpg") icon_path = resource_path("64.ico")这段代码不是“玄学”,而是PyInstaller官方文档明确推荐的路径适配方案。关键点在于:sys._MEIPASS只在打包后存在,开发时会触发Exception走except分支;os.path.dirname(os.path.abspath(__file__))确保开发时路径不随工作目录变化而偏移。你在ZFWeather.py里看到的所有QPixmap(resource_path(...))调用,都依赖这个函数。
2.3RunZF.spec:比命令行更可控的打包配置中枢
当你需要频繁调整打包参数(比如加更多资源、改输出目录、控制UPX压缩),手动敲长命令极易出错。PyInstaller生成的.spec文件就是你的配置中心。打开RunZF.spec,重点修改三处:
# -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis( ['RunZF.py'], pathex=['D:/project/ZFWeather'], # ← 项目根目录,影响资源查找范围 binaries=[], datas=[ ('images/weather.jpg', 'images'), ('images/one.jpg', 'images'), ('test.db', '.'), # ← 数据库放根目录 ('64.ico', '.'), # ← 图标也放根目录,供--icon引用 ], ... ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='RunZF', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, # ← 启用UPX压缩(需提前安装upx) console=False, # ← 关闭控制台窗口 disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, )注意:
pathex必须设为你的实际项目路径,否则datas里的相对路径会找不到源文件;upx=True能将exe体积从15MB压到6MB,但某些杀毒软件会误报,生产环境慎用;console=False对应--windowed,避免黑框弹出。
3. SQLite数据库集成:从连接创建到连接释放的全生命周期管理
3.1MyDB.py:单例模式封装QSqlDatabase,避免多连接冲突
Qt的QSqlDatabase不是线程安全的,且同一连接名(如"mydb")重复addDatabase会导致未定义行为。MyDB.py用单例+懒加载解决这个问题:
from PyQt5.QtSql import QSqlDatabase import os import sys class DatabaseManager: _instance = None _db = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def get_connection(self): if self._db is None: # 动态获取db路径:打包后从sys.executable同级找,开发时从当前目录找 if getattr(sys, 'frozen', False): db_path = os.path.join(os.path.dirname(sys.executable), "test.db") else: db_path = os.path.join(os.path.dirname(__file__), "test.db") self._db = QSqlDatabase.addDatabase("QSQLITE", "mydb") # ← 命名连接,防止冲突 self._db.setDatabaseName(db_path) if not self._db.open(): raise RuntimeError(f"无法打开数据库: {self._db.lastError().text()}") return self._db def close_connection(self): if self._db and self._db.isOpen(): self._db.close() QSqlDatabase.removeDatabase("mydb") # ← 必须remove,否则下次addDatabase失败 self._db = None # 全局实例 db_manager = DatabaseManager()这个设计的关键在于:
addDatabase("QSQLITE", "mydb")显式命名连接,避免默认名"qt_sql_default_connection"被其他模块覆盖;removeDatabase("mydb")在关闭时必须调用,否则Qt内部连接池会残留句柄;getattr(sys, 'frozen', False)是PyInstaller打包后的标志性变量,比检查sys.executable更可靠。
3.2DataGrid.py:用QSqlTableModel实现分页显示,绕过手动SQL拼接
直接写SELECT * FROM weather LIMIT 10 OFFSET 20做分页?太原始。Qt提供了QSqlTableModel配合QTableView,只需设置表名和过滤条件,分页由视图滚动自动触发:
from PyQt5.QtSql import QSqlTableModel from PyQt5.QtWidgets import QTableView class WeatherModel(QSqlTableModel): def __init__(self, parent=None): super().__init__(parent) self.setTable("weather") # ← 对应数据库表名 self.setEditStrategy(QSqlTableModel.OnManualSubmit) # ← 手动提交,避免误改 self.select() # ← 执行查询,加载全部数据到模型 # 在主窗口中: self.model = WeatherModel() self.tableView.setModel(self.model) self.tableView.resizeColumnsToContents()注意:
QSqlTableModel默认加载整张表,大数据量时会卡顿。真实项目需结合QSqlQueryModel手写带LIMIT/OFFSET的查询,但本Demo的test.db仅几十条记录,QSqlTableModel足够轻量且免写SQL。
3.3关闭窗口断开数据库连接.py:重写closeEvent确保连接释放
很多教程教你在__del__里关数据库,但Python对象销毁时机不可控,尤其Qt事件循环还在跑时,__del__可能永远不触发。正确做法是拦截窗口关闭事件:
from PyQt5.QtWidgets import QMainWindow from MyDB import db_manager class MainWindow(QMainWindow): def __init__(self): super().__init__() # ... 初始化UI ... def closeEvent(self, event): """窗口关闭时强制释放数据库连接""" try: db_manager.close_connection() except Exception as e: print(f"[WARN] 关闭数据库连接失败: {e}") finally: event.accept() # ← 确保窗口真正关闭这段代码必须写在主窗口类里,且event.accept()不能漏——否则closeEvent被拦截后窗口不会消失。这是防止“数据库被锁定”的最后一道防线。
4. 避坑指南:打包与数据库操作中踩过的五个真实坑
4.1 现象:打包后exe双击无反应,任务管理器里进程一闪而逝
原因:缺少--windowed参数,导致控制台窗口闪现后立即关闭;或数据库路径错误,QSqlDatabase.open()失败后未捕获异常,程序直接退出。
解决:
- 加
--windowed参数; - 在
MyDB.py的get_connection()中用try/except包裹open(),并打印错误日志到文件(如log.txt),方便排查; - 临时去掉
--windowed,观察黑框报错内容。
4.2 现象:图片显示为灰色方块,QLabel文字正常但Pixmap为空
原因:resource_path()函数里sys._MEIPASS路径拼接错误,或--add-data指定的源路径不存在(比如images/文件夹名大小写不符)。
解决:
- 在
resource_path()开头加print("base_path:", base_path),确认路径是否指向_MEIxxx临时目录; - 进入
dist/RunZF/目录,手动检查images/文件夹是否存在、文件名是否完全一致(Windows不区分大小写但PyInstaller严格); - 用
os.listdir(base_path)打印所有文件,验证资源是否真被复制过去。
4.3 现象:首次运行正常,第二次启动报“database is locked”
原因:程序异常退出(如Ctrl+C、任务管理器结束)未执行closeEvent,数据库连接未关闭,SQLite文件被锁。
解决:
- 在
MyDB.py的get_connection()中,打开前先尝试removeDatabase("mydb"); - SQLite数据库文件属性设为“只读”(右键→属性→只读),强制每次启动都新建连接;
- 生产环境改用
PRAGMA journal_mode=WAL;提升并发性(需在建表后执行)。
4.4 现象:QSqlTableModel显示空表,但用DB Browser for SQLite确认表里有数据
原因:setTable("weather")的表名与数据库实际表名不一致(如建表语句是CREATE TABLE Weather,首字母大写);或数据库连接未open()成功,模型未select()。
解决:
- 用
DB Browser for SQLite打开test.db,确认表名完全匹配(区分大小写); - 在
WeatherModel.__init__()中加print("Table count:", self.rowCount()),确认模型是否加载到数据; - 检查
QSqlDatabase.lastError(),确认连接状态。
4.5 现象:打包后exe体积超100MB,杀毒软件报毒
原因:PyInstaller默认打包整个Python环境(包括numpy、pandas等未用模块);UPX压缩被部分杀软标记为可疑。
解决:
- 用
--exclude-module排除不用的库:--exclude-module matplotlib --exclude-module scipy; - 改用
--collect-all PyQt5替代--onefile,生成目录结构,再用7z压缩; - 关闭UPX,或从 UPX官网 下载最新版,旧版易被误报。
5. 进阶技巧:用test.db做数据校验 + 自动初始化脚本防空库
5.1database.db与test.db双库策略:开发调试用test.db,交付用database.db
项目里同时存在两个db文件,不是冗余,而是分工明确:
test.db:预置测试数据(城市、温度、天气图标ID),用于快速验证UI渲染逻辑;database.db:空库模板,交付时客户可自行导入生产数据,避免测试数据泄露。
你在MyDB.py中看到的路径判断逻辑,正是为了区分二者:
# 开发时优先用test.db,打包后用database.db db_name = "database.db" if getattr(sys, 'frozen', False) else "test.db" db_path = os.path.join(base_path, db_name)这样既保证本地调试有数据,又确保交付包干净无敏感信息。
5.2DataGrid.py里藏了一个自动建表脚本,防首次运行报错
QSqlTableModel要求表必须存在,但用户第一次运行时test.db可能是空的。DataGrid.py顶部有一段静默初始化代码:
from PyQt5.QtSql import QSqlQuery def init_database(): db = db_manager.get_connection() query = QSqlQuery(db) # 检查weather表是否存在 if not query.exec_("SELECT name FROM sqlite_master WHERE type='table' AND name='weather'"): # 创建表 query.exec_(""" CREATE TABLE weather ( id INTEGER PRIMARY KEY AUTOINCREMENT, city TEXT NOT NULL, temp REAL, condition TEXT, icon_id INTEGER ) """) # 插入测试数据 query.exec_("INSERT INTO weather (city, temp, condition, icon_id) VALUES ('Beijing', 25.6, 'Sunny', 1)") query.exec_("INSERT INTO weather (city, temp, condition, icon_id) VALUES ('Shanghai', 28.3, 'Cloudy', 2)") # 在模块导入时自动执行 init_database()这段代码只在DataGrid.py被首次import时运行一次,利用SQLite的sqlite_master元数据表判断表是否存在,不存在则建表+插数据。它不依赖外部脚本,真正做到“开箱即用”。
5.3 表格对比:test.db预置数据 vsdatabase.db空库结构
| 字段名 | 类型 | 是否允许NULL | 说明 | test.db示例值 | database.db状态 |
|---|---|---|---|---|---|
id | INTEGER PRIMARY KEY | 否 | 自增主键 | 1 | 存在,但无数据 |
city | TEXT | 否 | 城市名称 | 'Beijing' | 表结构存在 |
temp | REAL | 是 | 实时温度 | 25.6 | 表结构存在 |
condition | TEXT | 是 | 天气状况 | 'Sunny' | 表结构存在 |
icon_id | INTEGER | 是 | 图标索引 | 1 | 表结构存在 |
注意:
database.db在交付前应手动清空test.db里的测试数据,仅保留建表语句生成的空表结构。可用DB Browser for SQLite的“导出→SQL Dump”功能生成建表脚本,再用sqlite3 database.db < schema.sql重建。
从那以后我每次交付PyQT5项目,都会在dist/目录下放一个verify.bat脚本,内容只有两行:
RunZF.exe pause双击它,看exe能否正常启动、图片是否显示、表格是否有数据——这比看日志快十倍。如果闪退,立刻用--console重新打包,盯着黑框报错。希望帮到你。
本文还有配套的精品资源,点击获取