☰
PyQt5数据库操作小工具实战:从SQLite连接到增删改查与排错
2026/10/2 11:03:49 网站建设 项目流程

简介:这是一份基于Python PyQt5实现的数据库操作小工具源码,配套SQLite数据库文件,适合学习Python GUI编程或进行轻量级数据库管理的开发者参考。工具通过PyQt5搭建交互界面,结合sqlite3模块完成连接、查询、插入、更新、删除等操作,源码中封装了DatabaseManager类及connect_db()、execute_sql()、fetch_data()等典型函数,并涉及异常处理与事务控制,具有清晰的学习路径。压缩包共171个文件,以104个BMP图片资源为主,同时包含19个Python源码、10个Qt界面文件(.ui)、10个C++源文件及11个头文件,另有4个qrc资源文件、4个bat批处理脚本和1个db3数据库文件,整包约12.48MB。已有118人学习下载。通过研读源码及运行界面,读者可直观理解PyQt5控件布局、信号槽机制与SQL执行逻辑,为扩展更复杂的数据库管理应用打下基础。

1. 先把“数据库操作小工具”该做成什么样,说清楚

一个做课设的朋友给我看他自己写的 PyQt5 数据库操作小工具,界面还凑合,一打开 SQLite 文件就白屏,报错也不弹。我接手后十分钟就定位了:他把数据库连接写在了全局,窗口关了连接还占着文件句柄,再打开同一个文件自然失败。这类工具听起来简单,实际翻车点全藏在连接生命周期和数据刷新策略里。

这个标题下的项目,本质是拿 Python 的 PyQt5 做一层 GUI 壳,把“连接数据库、看表、增删改、跑一句 SQL”这几件琐事变成按钮和表格。它不是要你仿一个 Navicat,而是做一个够用、可改、能打包的小工具,给业务同事免装客户端、免写 SQL 地用。适合的人是这几类:正在找 Python 课程设计源码的学生、想把公司内部某个数据库查询解放出来的开发,以及想从 PyQt5 界面设计入门但不想只做一个计算器的学习者。

2. 为什么偏要用 PyQt5 的 Qt SQL 模块:选型逻辑与工程结构拆解

2.1 Qt SQL 模块和原生 Python 驱动,到底该选哪个

做数据库小工具,PyQt5 本身自带两条路:一是 Qt SQL 模块里的 QSqlDatabase、QSqlQuery、QSqlTableModel 这一套,二是用 pymysql、psycopg2、sqlite3 这类原生驱动查完后手动刷界面。很多人第一次写的时候会本能地用原生驱动,因为文档眼熟、网上 Python 数据库教程都这么讲,但放在 PyQt5 里并不顺手。

对比项Qt SQL 模块原生 Python 驱动
界面绑定QSqlTableModel 直接喂给 QTableView,改动自动同步查到结果后手动按行列填表格,刷新逻辑要自己写
事务控制有 transaction() / commit() / rollback() 封装每个驱动都有自己的 API,写法不统一
复杂 SQL一个 QSqlQuery 对象搞定,参数绑定比较简单cursor 写起来也不复杂,但结果要转成模型
驱动安装SQLite 内置;MySQL 依赖客户端原生库,容易加载失败pip 装完就能连,依赖管理更省心
多线程连接不能跨线程共享,规则较死主流驱动在连接串上就能指定连接池,多线程体验更好

我的通常做法是:把 Qt SQL 模块作为主力,因为 QSqlTableModel 跟 QTableView 的联动能把大量刷新代码省掉;只有在执行某些数据库特有的复杂语句、或者要调连接池时,才单独开一条原生连接。混合使用是允许的,但两条连接指向同一个库时要小心写锁,尤其是 SQLite。

2.2 支持的数据库与驱动类型:从 SQLite 到达梦怎么选

PyQt5 的 QSqlDatabase 在驱动这块比很多人印象里要宽,但每个驱动背后都有原生依赖,这才是最大的坑。常见搭配如下:

数据库Qt 驱动类型实际部署条件
SQLiteQSQLITE随 PyQt5 自带,零依赖,单机小工具首选
MySQL / MariaDBQMYSQL需要本机有 MySQL 客户端库,Windows 上经常 QSqlDatabase: MySQL driver not loaded
PostgreSQLQPSQL需要 libpq,打包后还要带一堆 DLL
SQL ServerQODBC走 ODBC 数据源,注意 32/64 位要一致
达梦 / 人大金仓QODBC用官方 ODBC 驱动,连接串写法跟 SQL Server 接近

只要你把工具做成了“新建连接”的形式,切换底下驱动只需要改连接串,界面代码不用动。这一点在给国产数据库做配套工具时特别占便宜,很多人习惯在 Navicat 里配达梦数据源,本质也是走 ODBC,到了 Qt 这边是一个思路。

2.3 拿到“源码 + 数据库”资源包后第一步先看什么

这类资源包到手后,别急着跑 main.py,先看三个地方:数据库连接参数、数据库文件在哪个路径、有没有 qt.conf 或打包配置。绝大多数资源包里内嵌的是 SQLite 文件,连接串写的是相对路径。

demo/ ├── app/ │ ├── main.py # 程序入口,负责 QApplication 和主窗口 │ ├── db_helper.py # 数据库连接和 SQL 封装 │ ├── ui_main.py # PyQt5 界面文件转出来的 Python 脚本 │ └── resources/ │ └── icon.png ├── data/ │ └── demo.db # SQLite 数据库文件 └── requirements.txt

requirements.txt 里一般锁定这几个包,别装成 PyQt6,API 名字不一致,代码直接报错在 import 这行。

PyQt5>=5.15 PyQt5-Qt5>=5.15 PyQt5-sip>=12.11 pymysql>=1.0

打开 db_helper.py 看有没有 db.close() 和 QSqlDatabase.removeDatabase() 的成对出现,没成对就是启动后第二次连不上同文件的首要嫌疑。另外一个高频问题是 data 目录和 exe 不在同一级,双击打包后的 exe 会提示路径找不到,这类问题统一放在排错章节里说。

2.4 数据库文件路径的三种坑:相对路径、绝对路径、打包路径

在 PyCharm 里跑,相对路径指向项目根目录没问题;打包成 exe 后,当前目录变成 exe 所在目录,data/demo.db 可能就丢了。给源码配数据库文件时,我一般会在 db_helper 里加一层探测:

import os import sys BASE_DIR = getattr(sys, "_MEIPASS", os.path.dirname(os.path.dirname(__file__))) DB_PATH = os.path.join(BASE_DIR, "data", "demo.db")

参数说明:sys._MEIPASS 是 PyInstaller 打包后解压临时资源的目录,存在时优先用它,普通 Python 解释器里没有这个属性就走回项目根目录。这样同一份代码在源码调试和打包后都能找到数据库文件。

3. 把数据库表显示到界面上:连接 SQLite 到 QTableView 的最小可跑代码

3.1 打开 SQLite 数据库并列出所有表

先写一个 db_helper.py,把连接动作独立出来,便于主窗口重用它。QSqlDatabase.addDatabase 要求连接名唯一,如果不指定连接名,默认的名字是空字符串 “qt_sql_default_connection”,重复 addDatabase 会告警。

from PyQt5.QtSql import QSqlDatabase, QSqlQuery DB_TYPE = "QSQLITE" DB_PATH = "./data/demo.db" def create_connection(db_path=DB_PATH, db_type=DB_TYPE): """建立全局唯一的 Qt 数据库连接,返回连接名。""" conn_name = "qt_sql_default_connection" db = QSqlDatabase.addDatabase(db_type, conn_name) db.setDatabaseName(db_path) if not db.open(): print("连接失败:", db.lastError().text()) return None return conn_name def fetch_all_tables(conn_name="qt_sql_default_connection"): """列出库里所有用户表,排除系统表,可用在下拉框里。""" query = QSqlQuery("SELECT name FROM sqlite_master " "WHERE type='table' ORDER BY name", db=QSqlDatabase.database(conn_name)) tables = [] while query.next(): tables.append(query.value(0)) return tables

逻辑说明:Sqlite 的系统表 sqlite_sequence、sqlite_stat1 也在 sqlite_master 里,WHERE type='table' 已经把它们过滤掉,绝大多数表名不会出现。QSqlQuery 构造函数的第二个参数可以指定用哪个连接,这个参数在多连接并存时非常关键,省略的话会默认走 “qt_sql_default_connection”。

3.2 用 QSqlTableModel 把表视图塞进 QTableView

连接建立之后,主窗口用一个下拉框选表名,一个按钮触发加载,QTableView 负责展示。QSqlTableModel 只做一件小事:把一张表映射成可编辑的表格模型,增删改查都通过模型方法做,避免了手写堆表格控件的重复劳动。

from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QComboBox, QPushButton, QTableView, QMessageBox) from PyQt5.QtSql import QSqlTableModel class TableWindow(QMainWindow): def __init__(self): super().__init__() self.combo = QComboBox() self.load_btn = QPushButton("加载表") self.table_view = QTableView() self.model = None central = QWidget() layout = QVBoxLayout(central) layout.addWidget(self.combo) layout.addWidget(self.load_btn) layout.addWidget(self.table_view) self.setCentralWidget(central) self.combo.addItems(fetch_all_tables()) self.load_btn.clicked.connect(self.load_table) def load_table(self): table_name = self.combo.currentText() if not table_name: return self.model = QSqlTableModel(self) self.model.setTable(table_name) self.model.setEditStrategy(QSqlTableModel.OnManualSubmit) self.model.select() self.table_view.setModel(self.model) self.setWindowTitle(f"当前表:{table_name}")

参数说明:setTable 只是设置表名,真正执行 SELECT 的是 select(),忘写 select() 时界面会白白一片,这是新手最容易踩的空状态。setEditStrategy 定义了修改什么时候提交,OnManualSubmit 表示手动点保存才写库,后面增删改章节会展开讲。

3.3 中文字段名与表头显示

SQLite 里的字段名会原样显示在列头,中文表头没问题,但如果你在代码里给列重命名,要记得 setHeaderData 放在 select() 之前或之后都行,只要在界面刷新前设置即可。更稳妥的做法是单独维护一个“字段中文名”字典,按字段明查:

field_alias = { "id": "编号", "name": "姓名", "created_at": "创建时间" } for col in range(self.model.columnCount()): field_name = self.model.headerData(col, Qt.Horizontal) if field_name in field_alias: self.model.setHeaderData(col, Qt.Horizontal, field_alias[field_name])

这段代码适合接进 load_table 末尾,它不影响数据内容,只影响表头显示。遇到中文表名本身查询正常、显示乱码的情况,问题出在连接字符集,Qt 的 QSQLITE 驱动用 UTF-8 处理,一般不会乱码,真乱码需要检查操作系统区域设置。

3.4 表名拼接的边界:参数绑定帮不上忙的环节

SQLite 不支持在绑定参数的位置写表名,所以从下拉框拿到的表名必须直接拼进 setTable()。这里要防的不是 SQL 注入,而是非法字符,比如表名里带双引号会让 Qt 生成错误的查询语句。我给 fetch_all_tables 的下拉框加过滤条件,只允许字母、数字、下划线开头的表名,其他名字不进列表,比正则校验更省心。不用这套限制也可以,但用户在数据库文件里手工建一个带空格的表时,加载就会翻车。

4. 增删改查的界面化实现:从模型策略到执行任意 SQL

4.1 三种编辑策略,选了 OnManualSubmit 就等于上了保险

QSqlTableModel 默认的策略是 OnRowChange,改成 OnManualSubmit 后,所有对表格的修改都停留在内存里,点“保存修改”才调用 submitAll() 真正写库,点“撤销修改”调用 revertAll() 把改动清掉。这是整个小工具里最有用的设计取舍。

编辑策略触发时机实际体验
OnFieldChange单元格一改就提交误改数据立刻入库,连后悔药都没有
OnRowChange光标离开当前行就提交改一半切走就落库,比上面好一点但还不够
OnManualSubmit手动调用 submitAll()所有改动收在一起,统一保存或回滚,适合业务工具

表格上的单元格双击进入编辑,InputDialog 确认后写库。代码上主窗口加两个按钮“保存修改”“撤销修改”即可:

def save_changes(self): if self.model is None: return if self.model.submitAll(): self.statusBar().showMessage("保存成功") else: QMessageBox.critical(self, "错误", self.model.lastError().text()) self.model.revertAll() def revert_changes(self): if self.model is None: return self.model.revertAll()

submitAll() 返回 False 时 lastError() 才有意义,不是数据库错误也可能返回 False,比如有字段违反非空约束。先把 lastError().text() 弹出来,再 revertAll 是稳定顺序。别反过来先回滚再取错误,错误信息已经丢了。

4.2 新增一行和删除选中行:代码上怎么写最稳

新增行用 model.insertRecord(行号, record),删除行用 model.removeRow(行号)。这两个动作只影响内存,直到 submitAll() 才写库,所以新增和删除可以混在一起用。

def add_row(self): if self.model is None: return record = self.model.record() record.setValue("name", "新用户") record.setValue("created_at", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) self.model.insertRecord(self.model.rowCount(), record) def delete_selected_row(self): index = self.table_view.currentIndex() if not index.isValid(): return reply = QMessageBox.question(self, "确认", "删除这一行?") if reply == QMessageBox.Yes: self.model.removeRow(index.row()) self.model.submitAll()

逻辑说明:record() 拿到的是一条空结构的记录,包含所有字段名,setValue 按字段名写值,推荐用字段名而不是列号,字段名在表结构变化时更容易排查。删除前加弹窗确认是必要的,因为 removeRow 后如果直接 submitAll,用户误删了连 revoke 的机会都没有。

4.3 通用 SQL 执行器:把任意语句放进黑匣子之前先想清楚

除了表格模式,这个工具还应该有一个输入 SQL 的入口,用 QSqlQueryModel 承载查询结果。它和 QSqlTableModel 的区别是:只读,不提供编辑能力,适合执行分析用 SQL。

from PyQt5.QtSql import QSqlQueryModel def run_sql(self): sql_text = self.sql_edit.toPlainText().strip() if not sql_text: return self.query_model = QSqlQueryModel(self) self.query_model.setQuery(sql_text) self.result_table.setModel(self.query_model) self.statusBar().showMessage(f"返回 {self.query_model.rowCount()} 行")

注意事项:QSqlQueryModel.setQuery 执行 INSERT / UPDATE / DELETE 时不会报错,但也不会返回影响行数,界面看起来像是没反应。给这个执行器加一个简单判断,取语句前六个字母判断是不是 select,不是的话自动换成 QSqlQuery 执行并打印影响行数。

def run_sql_detect(self): sql_text = self.sql_edit.toPlainText().strip() first_word = sql_text.split(" ", 1)[0].lower() if first_word in ("insert", "update", "delete"): query = QSqlQuery() query.exec_(sql_text) self.statusBar().showMessage(f"影响行数:{query.numRowsAffected()}") else: self.run_sql()

4.4 参数绑定:别再拿 f-string 拼值

演示代码里我用 f-string 直接构造 SQL 是为了让新手看清执行过程,但真正做工具时,值部分必须用 bindValue。表名没法绑定,字段值完全可以绑定,而且绑定之后 SQLite 会自动帮你处理引号转义,避免中文、单引号问题。

query = QSqlQuery() query.prepare("INSERT INTO projects(name, owner, created_at) " "VALUES (:name, :owner, :created_at)") query.bindValue(":name", project_name) query.bindValue(":owner", current_user) query.bindValue(":created_at", datetime.now().isoformat()) if not query.exec_(): print("写入失败:", query.lastError().text())

参数说明:prepare 的 SQL 里用冒号普通参数,bindValue 绑定时参数名不带冒号也能匹配,但建议带,带错了提示明显。这个写法解决的坑是“中文里带单引号导致 SQL 报错”,网上很多老教程还是 % 格式化拼 SQL,搬到 PyQt5 里写小工具非常容易被骂不专业。

4.5 主窗口把所有功能排布在一起:一个够用的布局

三个区域横向排布最常用:左侧放“表选择 + 加载 + 保存/撤销”,中间主表区,底部放 SQL 执行器。这个布局的好处是项目源码给到别人时,不需要任何说明就能看懂这是干什么的,后期接达梦、接 MySQL 也只是改连接串的事。

5. 调试避坑清单:五个高频翻车现场的现象、原因、解决

5.1 SQLite 文件被锁死:程序没退出,下一次却打不开

现象:第一次运行正常,关闭窗口后再点打开就报 database is locked,或者直接把数据库文件锁到其他程序无法读取。

原因:QSqlDatabase 的底层连接没有随窗口关闭而释放,旧连接还占着文件句柄。PyQt5 窗口 close 事件默认不会自动断开数据库连接,更麻烦的是如果反复 addDatabase 同一个连接名,Qt 会打印连接已存在的警告。

解决:在窗口关闭事件里显式清理连接,并记得调用 QSqlDatabase.removeDatabase。

def closeEvent(self, event): db = QSqlDatabase.database() if db.isOpen(): db.close() del db QSqlDatabase.removeDatabase("qt_sql_default_connection") super().closeEvent(event)

注意 del db 写在 removeDatabase 之前,removeDatabase 要求该连接对象没有其他引用,否则会告警 “connection still in use”。

5.2 表加载后界面一片空白

现象:QTableView 框都显示出来了,模型也 setTable 了,但表里一行数据都没有,也没有任何报错。

原因:只写了 setTable,忘了 select()。setTable 只是告诉模型“我要绑这张表”,实际查询动作发生在 select() 里,没执行就是空模型。QSqlTableModel 跟 QTableView 绑定后,如果模型没有提交第一次查询,表格自然白屏。

解决:在表名设置后立刻调用 select(),并在后面加一个 rowCount() 断言打日志。凡是发现空白第一件事不是查界面,而是先看 rowCount() 是不是 0。

5.3 多线程刷新界面卡死

现象:后台线程查询数据库后直接拿 QueryModel 刷新界面,程序假死或直接崩溃。

原因:Qt 的 UI 控件不允许在工作线程直接操作,数据库连接更不允许跨线程共享。PyQt5 里数据库连接是在主线程创建的,子线程一用就崩。

解决:线程里只做数据查询,拿到结果后用信号把数据发回主线程再刷新。如果想在子线程用数据库,必须在子线程里新建独立连接,不能用 QSqlDatabase.database() 拿主线程那个。

5.4 Anaconda 环境下 pip 装 PyQt5 装不上或 import 报错

现象:conda 环境里 pip install PyQt5 成功,但代码里 import PyQt5.QtSql 直接报 DLL load failed,或者 PyCharm 里明明装上了还是红波浪线。这个现场在运行 labelme 这类需要 pyqt5 的源码时遇到过很多次。

原因:conda 虚拟环境里的 Qt 库版本和 pip 装进来的 PyQt5 绑定的 Qt5 DLL 冲突,尤其是 Windows 上常见。

解决:先在环境中执行 conda install pyqt=5,再补 pip install PyQt5-sip。如果还不行,把环境删了重建一个纯 Python 环境。纯 Python 环境比 conda 环境跑 PyQt5 省心得多,这是血泪经验。

5.5 中文参数写入数据库变成乱码

现象:MySQL 里写入的中文正常,SQLite 写入的中文没乱码,但同样的代码连达梦或 SQL Server 就乱码。

原因:连接串没指定字符集,Qt ODBC 驱动按默认编码解析,碰上 GBK 环境的数据库自然错乱。

解决:给连接串显式加 charset=utf8 或用 ODBC 的配置项。达梦 ODBC 数据源在配置工具里把字符串编码选成 UTF-8,SQL Server 则看实例的排序规则。这个坑最隐蔽,因为它不是每次必现,只有数据源本机区域是非中文时才有规律。

6. 验证技巧与进阶:给 SQL 执行加上耗时打印,几行代码找到慢查询

工具写完不等于能交付给别人用,交付前先做三件验证:连接数据库、打开表、执行一句 SELECT,三步都通过才算能跑。我习惯给所有 SQL 操作包一层耗时统计,点一下按钮看状态栏就知道这条语句快还是慢。

import time from functools import wraps def log_sql_time(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = (time.perf_counter() - start) * 1000 self.statusBar().showMessage(f"{func.__name__} 耗时 {elapsed:.1f} ms") return result return wrapper

这个装饰器挂在 execute_sql、load_table、save_changes 三个方法上,能快速感知数据库响应。SQLite 本地文件正常耗时应在一两毫秒到几十毫秒,超过一百毫秒就该怀疑是不是没有索引或者做了全表扫描。

进阶验证方式是用 PyQt5 自带的 QTest 做冒烟测试,不需要引入 pytest,直接 assert 模型行数:

from PyQt5.QtTest import QTest from PyQt5.QtCore import Qt def test_load_table_smoke(): window = TableWindow() window.combo.setCurrentText("users") QTest.mouseClick(window.load_btn, Qt.LeftButton) assert window.model is not None assert window.model.rowCount() >= 0

这套测试放在主程序里用命令行参数触发,交付源码包时把触发命令写在 README 里,接手的人直接跑一遍心里就有底。我现在的习惯是:每次改完底层连接代码,优先跑这个冒烟用例再开界面,否则连线都断了自己还在点按钮,排查起来很绕。数据库小工具不怕功能少,就怕连接没管好、刷新时序不对,这些小细节整理顺了,工具才能真正甩给别人用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询