简介基于Python PyQt5开发的数据库操作小工具面向希望将图形界面与数据库编程相结合的开发者适用于课程设计、轻量级数据管理或PyQt5入门实践。资源内含完整源码与配套数据库文件共171个文件压缩包约12.48MB。文件类型以19个Python脚本为核心配合10个.ui界面文件和4个.qrc资源配置另有10个.cpp、11个.h辅助代码以及104个.bmp图标用于按钮与状态美化并附赠1个.db3示例数据库。目前已有118人学习下载。通过研究源码可系统了解PyQt5窗口布局、信号槽交互与sqlite3模块的配合方式覆盖数据库连接、增删改查、事务处理及异常捕获等关键环节代码结构清晰便于在此基础上扩展为更完整的数据库管理工具。1. PyQt5 写数据库操作小工具比 Navicat 轻比手写 SQL 稳日常做数据分析或者维护内部系统时最烦的事不是不会写 SQL而是频繁在 IDE、命令行和数据库客户端之间来回切换。改一条数据要打开 Navicat看一个表结构又要切回命令行费时费神。用 PyQt5 写一个属于自己的数据库操作小工具能把连接、浏览、增删改查压缩到一个窗口里打开即用不改动现有数据库结构也无需部署 Web 服务。这个方向的源码并不复杂核心是 PyQt5 的模型视图架构配合数据库驱动层。本文会带着你从界面拆分到 SQL 封装完整走一遍给你一套能复现、能改造成自己业务工具的项目骨架顺带把驱动加载失败、中文乱码、界面卡死这些高频坑全部说清楚。2. 界面在前面、SQL 在后面原理和选型为什么是这样2.1 为什么是 PyQt5 而不是 Tkinter、Web 页面或 C#数据库工具类软件的界面特点是表单密集、表格密集、操作反馈要求即时。Tkinter 上手快但表格控件弱想要实现点击排序、按列筛选、单元格编辑后自动提交这些能力得自己写一堆事件绑定写出来代码量不比 PyQt5 少维护起来还更麻烦。Web 方案Flask Bootstrap确实漂亮但你得启动服务、处理跨域、考虑浏览器兼容给一个单机用户用实在是大炮打蚊子。C# WinForms 在 Windows 上体验很好可一旦换到 macOS 或 Linux 环境就卡住了很多内部工具恰恰需要在多平台分发。PyQt5 在 Qt 的模型视图框架加持下一个 QTableView 就能接管数据展示和编辑的大部分逻辑。它自带的 QSqlDatabase、QSqlQuery、QSqlTableModel 构成了完整的 SQL 访问链路这意味着从界面控件到数据库驱动之间是有官方封装层的不需要自己去拼连接串、管理事务、轮询结果集。对于“单机给团队用”这类小工具场景PyQt5 的授权模式GPL和发布方式PyInstaller 打包也是团队里大家最能接受的。工具的价值在于把你常用的操作逻辑固化下来而不是每次都要现写一遍。2.2 数据访问直接跑 SQL 还是用 QSqlTableModel数据访问有两条路线很多初学者一开始会混淆。第一条是直接用 QSqlQuery 执行 SQL 语句把结果手动填进 QTableWidget。这种写法的优点是逻辑直白每条 SQL 都是你亲自写的出了问题一眼能找到。缺点是你要自己维护表头映射、行类型转换和刷新逻辑当字段从 5 个变成 15 个时代码里到处是 setItem 和 column 下标改起来很痛苦。第二条是 QSqlTableModel 配合 QTableView。你需要做的只是设置表名和字段排序模型自动把数据库表的每一行映射成视图的一行单元格双击即可编辑setData 后调用 submitAll 就完成更新。代码量直接砍掉一大半。我的方案是两套并存。主数据浏览页用 QSqlTableModel适合快速查看和编辑单表复杂查询、跨表 join、存储过程调用则单独走 QSqlQuery结果输出到 QTableWidget。这样既享受模型视图的高效又保留 SQL 的灵活性。选型不是二选一而是看操作场景浏览用模型统计用查询这是后期维护舒服的关键。2.3 项目结构怎么拆才能让源码不变成黑匣子拿到一个 PyQt5 数据库工具源码时最怕的是所有代码堆在几个 500 行以上的文件里改一个按钮都要滚动半天。合理的结构应该把界面、连接和业务逻辑分开哪怕代码量只有一千行也值得这样拆。db_tool/ ├── main.py # 入口启动事件循环 ├── connection.py # 数据库连接管理单一连接文件 ├── main_window.py # 主窗口界面布局 ├── table_view.py # 表格模型与视图封装 ├── query_panel.py # 自定义 SQL 查询面板 └── requirements.txtmain.py 只做两件事创建 QApplication实例化主窗口然后进入事件循环。connection.py 保存当前数据库连接对象所有模块都从这里拿 connection 和 database 名不允许多处重复编写连接代码。main_window.py 负责把菜单、工具栏和中心控件组合起来。table_view.py 是核心文件封装 QSqlTableModel 的设置、刷新、提交和事务回滚。query_panel.py 是独立输入区执行任意 SQL用来弥补模型视图处理复杂查询时的不足。这个结构的好处是当你想加一个“导出当前表为 CSV”的功能时只需要在 table_view.py 里加一个方法主窗口加一个按钮不需要碰其他文件。当数据库从 MySQL 切换为 PostgreSQL 时只需修改 connection.py 里的驱动名称和连接参数界面和业务代码不受影响。新手拿到这套源码后能从结构上看出每个模块的职责边界上手成本很低。3. 把可复现的骨架跑起来连接、表格映射和增删改查3.1 最小可运行例子QTableWidget 手查 SQLite先感受一下最朴素的流程。建立一个 SQLite 数据库用 QSqlQuery 查询所有记录再逐行填入 QTableWidget 的单元格里。这个代码是整个工具里最基础的一段逻辑先把这条链路跑通再往上加东西。import sys import sqlite3 from PyQt5.QtWidgets import QApplication, QTableWidget, QTableWidgetItem # 先用标准库 sqlite3 建库造一点测试数据避免依赖 Qt 的数据库驱动 conn sqlite3.connect(demo.db) cursor conn.cursor() cursor.execute(CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)) cursor.executemany(INSERT INTO users (name, age) VALUES (?, ?), [(张三, 23), (李四, 31), (王五, 28)]) conn.commit() conn.close() app QApplication(sys.argv) table QTableWidget() table.setWindowTitle(最简单的数据浏览) # 用标准库查询一次拿到行数和列数 db sqlite3.connect(demo.db) cursor db.cursor() cursor.execute(SELECT id, name, age FROM users) rows cursor.fetchall() db.close() table.setRowCount(len(rows)) table.setColumnCount(3) table.setHorizontalHeaderLabels([ID, 姓名, 年龄]) for row_idx, row_data in enumerate(rows): for col_idx, value in enumerate(row_data): table.setItem(row_idx, col_idx, QTableWidgetItem(str(value))) table.show() sys.exit(app.exec_())这段代码的意图是验证 PyQt5 的表格控件能正常接收 Python 的查询结果。注意查询用的是标准库 sqlite3因为这段是验证 UI 层避免 Qt 驱动问题干扰判断。表头 label 是手动指定的因为原始结果集没有列名。这里有个关键参数QTableWidgetItem 接收的是字符串不写 str(value) 的话数字类型会出现类型错误这个转换在实际代码里非常容易漏。后面改用 QSqlTableModel 时就不存在这个问题模型会按数据库字段类型自动处理。这段基础代码跑通之后就可以确认 PyQt5 安装无误、事件循环正常再往 Qt 的 SQL 模块迁移。3.2 QSqlTableModel 把数据表变成一张活表格QTableWidget 适合一次性加载结果的场景但真实工具里数据可能会变。每次重新查询、重新填充不仅慢而且视图滚动位置、选中状态都会丢失。换成 QSqlTableModel 后数据表与数据库表之间保持映射关系数据库值变了调用 select 就能刷新视图滚动位置和列宽会保留这一点在操作体验上提升非常明显。import sys from PyQt5.QtWidgets import QApplication, QTableView from PyQt5.QtSql import QSqlDatabase, QSqlTableModel # 建立数据库连接一个 QSqlDatabase 实例就够不能再多 db QSqlDatabase.addDatabase(QSQLITE) db.setDatabaseName(demo.db) if not db.open(): print(数据库打开失败, db.lastError().text()) sys.exit(1) app QApplication(sys.argv) # 模型绑定到 users 表 model QSqlTableModel() model.setTable(users) model.setEditStrategy(QSqlTableModel.OnManualSubmit) # 指定显示的列顺序第 0 列是 ID 不参与编辑 model.setHeaderData(0, __import__(PyQt5.QtCore, fromlist[Qt]).Qt.Horizontal, ID) model.setHeaderData(1, __import__(PyQt5.QtCore, fromlist[Qt]).Qt.Horizontal, 姓名) model.setHeaderData(2, __import__(PyQt5.QtCore, fromlist[Qt]).Qt.Horizontal, 年龄) # select() 执行真正的 SELECT数据拉到本地 model.select() view QTableView() view.setModel(model) view.resize(600, 400) view.show() sys.exit(app.exec_())这段代码是工具的数据展示基座。核心动作是 setTable 绑定表名setEditStrategy 决定编辑策略。OnManualSubmit 表示所有修改先缓存在模型里等你调用 submitAll 才一次性写入数据库。这个策略适合工具类软件因为误操作后还能用 revertAll 回滚有后悔药吃。注意 QSqlDatabase.addDatabase 是全局单例注册机制同一个连接名只能注册一次。进程内只需要一个数据库连接反复调用 addDatabase 同一驱动实例会导致警告甚至连接被重置。setHeaderData 里用了 import 的一种别扭写法实际项目里直接用 from PyQt5.QtCore import Qt 放顶层即可这里是为了展示独立运行时也能拿到枚举。3.3 增删改查的按钮背后提交、回滚和事务边界有界面的工具就离不开按钮。把三个常用操作绑定到视图模型上提交修改、撤销修改、删除选中行。关键是对事务边界的理解每一批操作必须是一个完整提交单元不能让修改、删除、插入混在同一个 commit 里。from PyQt5.QtWidgets import QAction, QMessageBox from PyQt5.QtSql import QSqlTableModel def submit_changes(model, parent_widget): # 未改动直接跳过避免无意义的提交触发行锁 if not model.isDirty(): return if model.submitAll(): # 提交成功后重新拉取数据确保视图和数据库完全一致 model.select() else: # 提交失败时输出数据库层错误方便定位主键约束等问题 QMessageBox.warning(parent_widget, 提交失败, model.lastError().text()) model.revertAll() def revert_changes(model): # 所有未提交的修改全部丢弃恢复到上一次 select 的副本 model.revertAll() def delete_selected(model, view): # 获取选中行对应的模型行号按行删除所有选中项 selected view.selectionModel().selectedRows() if not selected: return for index in sorted(selected, reverseTrue): model.removeRow(index.row()) # 删除是修改操作提交方式与 submit_changes 保持一致 if model.submitAll(): model.select() else: model.revertAll()submitAll 不是只做一次 UPDATE它会把模型里所有 pending 的修改整合成一条事务执行。isDirty 是用来判断是否有未提交修改的开关批量场景下可以省掉多余的网络往返。删除时必须按行号逆序处理否则先删掉前面行会让后面行的索引整体错位。revertAll 在提交失败时调用它只回滚模型内存中的修改不会影响数据库里已提交的数据。需要理解的是“提交失败时为什么 revert”这条逻辑。数据库报错比如重复主键、外键约束时模型内部已经缓存了修改后的数据。如果你不 revert 再改下一次 submitAll 可能还会以同样的错误数据重试用户改了半天也存不上。直接 revert 回滚到初始状态让用户重新编辑是最安全的方案。3.4 从单表到多表下拉框切换和动态条件查询真正的工具不会只操作一张表。常见做法是提供一个表选择下拉框和一个条件输入框。切换表时销毁旧模型、按新表名创建新模型条件输入则翻译成 WHERE 子句传给模型过滤。from PyQt5.QtSql import QSqlTableModel from PyQt5.QtWidgets import QComboBox, QLineEdit def apply_table_filter(combo: QComboBox, model, filter_input: QLineEdit): # 获取当前选中的表名 table_name combo.currentText() model.setTable(table_name) # 将条件输入框里的内容原样拆成 WHERE 子句 # 例如输入 id 10 会直接拼进 SELECT 语句 if filter_input.text().strip(): model.setFilter(filter_input.text().strip()) else: model.setFilter() model.select() def fill_table_list(combo: QComboBox, database): # 通过数据库连接读取所有用户表的表名 tables database.tables() filtered [t for t in tables if not t.startswith(sqlite_)] combo.clear() combo.addItems(filtered)这段实现里有安全边界问题setFilter 只能接受可信输入不要把用户输入直接作为 filter 拼接。内部工具可以容忍点的就是必须跟使用者明确不要在条件框里放分号后面跟 DELETE 语句。QSqlTableModel 的 filter 是合法 SQL 的 WHERE 部分它不会阻止恶意输入所以这个功能的正确姿态是面向团队内部并且输入框做一侧过滤提示。多表切换时还有列头恢复问题setTable 会重置列头。你每切一次表需要重新走一遍 setHeaderData。实际项目里可以做一个表名字段元数据字典切换表时一次恢复所有表头。这里不展开但这是一个常见的再次翻车点。4. PyQt5 数据库的 5 个高频踩坑点现象、原因、解决4.1 Driver not loaded 崩溃现象程序执行到 addDatabase(QMYSQL) 时lastError 返回 “Driver not loaded” 或 “unable to open database file”但命令行里 mysql 客户端却能用。原因Qt 的 SQL 驱动插件是分离编译的PyQt5 主包默认自带 SQLite 驱动但 MySQL、PostgreSQL 驱动需要额外的 QtSQL 插件库插件没被找到自然加载失败。解决安装 PyQt5 的完整依赖或者改用 QPSQL 前确认插件路径。常见做法是先用 QSQLITE 开发调试等到环境固定了再切具体数据库。如果你对驱动插件检查没把握最快的方式是把安装好的 PyQt5 目录下 Qt\plugins\sqldrivers 这个文件夹内容打印出来逐个确认有没有你需要的驱动文件。# 列出当前环境能用的 SQL 驱动先看清楚再写连接代码 python -c from PyQt5.QtSql import QSqlDatabase; print(QSqlDatabase.drivers()) # 输出的列表里必须包含你需要的 QSQLITE 或 QMYSQL出现 “QSQLITE database is locked” 则是另一个方向的问题多半是因为同一个数据库文件被多个连接同时写或者上次连接没 close。检查所有分支里是否有 open 了但没 close 的连接。还有一个隐蔽点在 PyQt5 里直接删除 QSqlDatabase 对象不会立刻关闭底层句柄需要显式调用 db.close() 并设置为空。4.2 中文乱码或保存后变成问号现象界面显示正常写入 MySQL 后变成 “???”或从 SQLite 读出来直接乱码。原因连接参数里没有显式指定字符集驱动默认拿到的是数据库会话级别编码。解决MySQL 连接时在连接字符串末尾加上 charset 参数SQLite 本身不涉及这个层面但 SQLite 从旧版本库迁移到新库时容易出现 UTF-8 与 UTF-16 混用的问题。# 注意连接参数拼写务必放在数据库名之后很多乱码案例都是少拼了这一项 db.setDatabaseName(testdb?characteretutf8useUnicodetrue)如果你是直接用 QSqlTableModelsetData 时不用额外转码因为用 QString 内部就是 unicode但如果直接用 QSqlQuery 且结果是 strPython 3 下也不会有编码问题。真正的乱码源头是 MySQL 数据库层面的字符集不一致。检查表字段 collation 是否与连接参数一致或者执行SET NAMES utf8mb4让会话频率一致。这个问题最常见的场景是数据库是 latin1代码里全是 utf-8不管怎么改界面都白搭。4.3 删除行后视图行号跳变现象删除表格当前选中行之后下一次点击删除按钮发现删掉的不是当前选中的行。原因删除一个行后模型行号已经变化视图 selectionModel 的索引还停留在旧位置。解决每次删除完成后强制重新 select并且清空选中状态防止残留索引影响下一次操作。model.select() view.clearSelection()另一个衍生问题是批量删除时一次性触发多次 submitAll。把多行删除放进同一个事务里不要删一行提交一次。尤其 MySQL 下每提交一次就多一次网络往返行数多时这不只是慢的问题还有可能半途失败让数据处于不一致状态。4.4 界面假死执行大查询时不转圈现象点查询按钮后整个窗体卡住鼠标转圈标题栏显示“未响应”。原因耗时查询直接跑在主线程里SQL 执行期间事件循环被阻塞窗口重绘、按钮点击全部停摆。解决数据库操作放进 QThread或者用 QtConcurrent 的异步方式跑任务执行完成后通过信号把结果传回主线程。from PyQt5.QtCore import QThread, pyqtSignal class QueryThread(QThread): # 自定义信号查询完成后把二维列表发回界面线程 result_ready pyqtSignal(list) def __init__(self, sql, db_name): super().__init__() self.sql sql self.db_name db_name def run(self): # 在线程里重新打开连接不要在多个线程共享同一个 QSqlDatabase 连接 from PyQt5.QtSql import QSqlDatabase, QSqlQuery db QSqlDatabase.addDatabase(QSQLITE, thread_db_conn) db.setDatabaseName(self.db_name) db.open() query QSqlQuery(db) query.exec(self.sql) rows [] while query.next(): record [query.value(i) for i in range(query.record().count())] rows.append(record) db.close() QSqlDatabase.removeDatabase(thread_db_conn) self.result_ready.emit(rows)注意线程里 QSqlDatabase.addDatabase 要带第二个参数 connectionName不能使用默认连接名原因是默认连接在主线程已经注册过了。一个 QSqlDatabase 连接实例不能跨线程使用跨线程共享十分容易崩溃严重的直接段错误。Qt 官方文档对此写得很保守实际经验是“每个线程按需创建独立连接用完销毁”这样最稳。4.5 模型提交失败后 quietly 丢失修改还有一个隐藏点现象双击表格修改数据后点击保存界面好像没反应数据其实是成功写进数据库了但表格显示的还是旧值。原因QSqlTableModel 的 setData 触发后 Qt 的视图更新有延迟在你没有主动刷新模型的时候视图可能没有立即同步已完成提交的新值。解决submitAll 成功之后紧接着调用 select 刷新让模型重新拉一套数据视图自然同步。这个问题和上面提到的删除跳行、提交失败回滚其实是同一个底层习惯每次和数据库交互后一定要重新 select 一次视图。一个小小的刷新动作可以避免各种“改了没变化”的玄学反馈。5. 再进一步把它从个人脚本变成能交付的小工具骨架跑通之后真正的价值在于把它打磨成别人愿意用的东西。个人脚本只需要自己看得懂但工具分发给团队之后交互细节要求立刻变高。最值得先做的三件事是第一件事把查询结果导出为 CSV 或 Excel 文件第二件事给所有写操作加确认对话框第三件事记录操作日志。导出功能看起来简单但有一个索引错位的易错点直接从选中行读数据时视图的行号必须经过view.selectionModel().selectedRows()先做一次映射不能直接用当前选中行号去模型取数据因为在排序之后视图行号和模型行号是不一致的。这个细节是导出功能最常见的翻车原因。验证一个数据库工具是否合格比较实用的手段是对照 SQL 执行日志。手动在数据库端开 general log然后用工具进行增删改查操作最后比对日志和界面操作是否完全一致。这个验证方式能捕捉到“提交了两次”“条件过滤没生效”“删除时误删其他行”这类问题。我自己的习惯是每个小版本发布前用一条生产环境的只读副本库跑一遍全流程操作全程开着慢查询日志确认没有多余的全表扫描和重复提交再交给团队用。还有一点是关于发布部署的PyInstaller 打包 PyQt5 工具时的坑不在代码而在资源文件。如果你的工具里有图标、数据库文件或额外的驱动库默认的--onefile模式可能会出现资源路径找不到的问题程序双击能打开但图标全丢。处理方式是优先采用--onedir模式打包然后把资源路径换算成相对执行文件的路径而不是相对当前工作目录的路径。这个路径一旦弄错换个目录启动就会翻车。这次把选型、结构和核心代码分析完落地方向也算清楚先跑通 SQLite 骨架再换 MySQL 驱动然后根据实际业务表结构调整界面字段最后打包分发。只要选定一个真实在用的业务表花一个下午把这个流程走一遍后面再写第二个数据库工具时基本不用查资料。希望帮到你。本文还有配套的精品资源点击获取