资讯动态

用PyQt5打造轻量级数据库操作工具:从QSqlTableModel到SQLite实战

发布时间:2026/10/9 21:41:49 来源:尧图企业网站定制
简介一份基于Python PyQt5开发的数据库操作小工具源码同时附带了SQLite数据库文件面向正在学习PyQt5界面编程与sqlite3数据库交互的开发者尤其适合需要轻量级桌面数据库管理场景的动手实践。包内共171个文件压缩包大小12.48MB主体包含19个Python源码、10个Qt Designer界面ui文件及10个cpp工程源文件另有大量bmp图标资源用于完善界面展示整体目录结构清晰便于按模块阅读与二次开发。已有119人学习下载。源码中实现了数据库连接、增删改查等基础操作并通过DatabaseManager类封装核心方法配合try-except异常处理与事务提交逻辑能够直观展示PyQt5界面层与数据库逻辑层的分工方式包内db3数据库文件也可直接用于测试省去建库步骤。对于想要快速上手PyQt5表单设计、信号槽绑定或多表操作的人来说这套工程提供了可运行、可改造的完整参考适合作为课设或内部小工具的基础模板。1. 用PyQt5写数据库操作小工具解决的不只是“命令看不清”的问题用Python PyQt5写一个数据库操作小工具几乎每个做后端或者运维的开发者都在某个阶段冒出过这个念头。最直接的触发场景一般是测试环境有个临时库要改几百行数据项目里没有现成的管理后台重量级的数据库客户端对非专业同事又太沉而命令行敲SQL确实不直观。于是干脆自己动手做一个带界面的数据库操作小工具把连接、查询、增删改查和结果展示都框在一个窗口里。这类项目通常还会附带一个SQLite数据库文件拿到就能直接跑源码结构清楚核心代码三五十行就能点亮主窗口。适合谁写过一点Python、想学PyQt5的初级开发者以及需要一个轻量数据库操作界面替自己省事的全栈工程师都能从这套方案里拿到自己想要的东西。2. 先吃透PyQt5的SQL模块为什么用QTableView QSqlTableModel而不是手绘表格2.1 三层结构一次说清QSqlDatabase管连接、QSqlQuery管SQL、Model管数据Qt的SQL模块设计得很规整拆开看就是三个角色QSqlDatabase负责建立和维护连接QSqlQuery负责执行SQL语句QSqlTableModel这类Model负责把数据表映射到界面组件上。新手最容易犯的错是一上来就操作QTableWidget把数据一行一行塞进去数据库一变化就得手动同步工具稍微复杂一点就维护不动了。QSqlDatabase.addDatabase(QSQLITE)是一个全局工厂方法它按驱动名创建连接对象默认连接可以直接通过无参的QSqlDatabase.database()拿回来。为什么用工厂而不是直接QSqlDatabase()因为同一个进程里可能要同时开SQLite和MySQL两组连接工厂模式允许给每个连接起名字后续查询和释放都靠这个名字定位。看一个最小连接片段from PyQt5.QtSql import QSqlDatabase db QSqlDatabase.addDatabase(QSQLITE) # 注册驱动并拿到连接实例 db.setDatabaseName(demo.db) # SQLite 对应文件路径 if not db.open(): print(连接失败:, db.lastError().text())这段代码里有个细节值得养成习惯open()失败一定要读db.lastError().text()。否则驱动版本不对、路径写错、文件被占用你只能看到一个冷冰冰的False排查效率极低。lastError()返回的是QSqlError对象包含数据库驱动给出的原始错误文本几乎所有QtSQL调用失败都能靠它定位问题。2.2 QSqlTableModel的两种打开方式绑定整表和自定义SQL填模型QSqlTableModel是QtSQL模块里最实用的一个类它把“表”这个概念直接映射成Model。只需要告诉它“连接哪张表”它自己会生成SELECT语句、维护缓存、处理分页界面绑上QTableView就能显示。这里有一个最常见的对比QTableWidget是“手动填格子”数据量小且固定时无所谓QSqlTableModel是“表驱动”修改完数据可以批量提交也支持回滚。import sys from PyQt5.QtWidgets import QApplication, QTableView from PyQt5.QtSql import QSqlDatabase, QSqlTableModel app QApplication(sys.argv) db QSqlDatabase.addDatabase(QSQLITE) db.setDatabaseName(demo.db) if not db.open(): sys.exit(数据库打开失败: db.lastError().text()) view QTableView() model QSqlTableModel() model.setTable(users) # 绑定 users 表 model.setEditStrategy(QSqlTableModel.OnManualSubmit) # 手动提交 model.select() # 真正执行 SELECT view.setModel(model) view.resize(800, 500) view.show() sys.exit(app.exec_())setEditStrategy三个取值按使用场景选OnFieldChange是任意单元格改动立刻写库适合单机小工具OnRowChange是当前行切换时提交上一行适合表格连续录入OnManualSubmit是全部改动攒着点保存按钮才提交适合带确认界面的工具。我的习惯是工具类默认OnManualSubmit用户误改还有后悔药提交前能统一校验。2.3 初始化数据库文件DDL、种子数据和一个自动检查连接的小函数配套的数据库文件不是凭空来的工具在启动时最好自己检查表是否存在不存在就建表、插种子数据。这样做的好处是任何人拿到源码和空的数据库文件都能一键跑起来不用再手动执行建表脚本。下面是一个通用的初始化函数from PyQt5.QtSql import QSqlDatabase, QSqlQuery def init_database(db_path: str): db QSqlDatabase.addDatabase(QSQLITE) db.setDatabaseName(db_path) if not db.open(): raise RuntimeError(db.lastError().text()) query QSqlQuery(db) # 先查 sqlite_master避免每次启动都重复建表 query.exec_(SELECT name FROM sqlite_master WHERE typetable AND nameusers) if not query.next(): query.exec_( CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE, created_at TEXT DEFAULT (datetime(now, localtime)) ) ) query.prepare(INSERT INTO users (name, email) VALUES (?, ?)) for name, email in [(A同学, aexample.com), (开发者B, bexample.com)]: query.addBindValue(name) query.addBindValue(email) query.exec_() return db注意exec_()末尾的下划线PyQt5为了和Python关键字exec区分给所有Qt方法加了下划线这是PyQt5的正常写法。?占位符是SQLite的参数风格配addBindValue按顺序绑定。为什么不直接拼字符串种子数据和用户输入一样都要防注入习惯养成后写增删改查时不会踩拼接SQL的坑。datetime(now,localtime)取的是本地时区时间不是UTC国内工具用着更直观。3. 把工具真正跑起来主窗口布局、SQL查询面板与增删改查的完整代码3.1 主窗口骨架工具栏、状态栏和可拖拽的左右分栏工具的主窗口我用QMainWindow左侧放一个QTreeWidget列数据库里的表名右侧放QTableView显示结果中间用QSplitter隔开用户可以自由拖动比例。顶部留一行查询面板输入SQL后回车直接执行这比把所有操作都埋在菜单里顺手得多。from PyQt5.QtCore import Qt from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTableView, QTreeWidget, QSplitter, QLineEdit, QPushButton, QToolBar, QStatusBar) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(数据库操作小工具) self.resize(1024, 680) # 查询面板 query_panel QWidget() q_layout QHBoxLayout(query_panel) self.sql_edit QLineEdit() self.sql_edit.setPlaceholderText(输入 SQL 后回车执行) self.run_btn QPushButton(执行) q_layout.addWidget(self.sql_edit, 1) q_layout.addWidget(self.run_btn) # 左侧表树 右侧结果表 splitter QSplitter(Qt.Horizontal) self.table_tree QTreeWidget() self.table_tree.setHeaderLabel(表) self.result_view QTableView() splitter.addWidget(self.table_tree) splitter.addWidget(self.result_view) splitter.setStretchFactor(1, 1) # 右侧给更多空间 container QWidget() outer QVBoxLayout(container) outer.addWidget(query_panel) outer.addWidget(splitter, 1) self.setCentralWidget(container) toolbar QToolBar(主工具栏) self.addToolBar(toolbar) self.statusBar().showMessage(就绪)setStretchFactor(1, 1)是让结果表吃掉多出来的空间左侧树保持默认宽度。工具栏在简化代码里没放按钮实际使用时把“新增”“删除”“导出”塞进去工具类软件最忌讳功能藏在菜单深层。3.2 原生SQL查询QLineEdit输入、QTableView输出结果动态生成模型查询面板是工具的核心入口。用户输入的可能是一条SELECT也可能是UPDATE所以不能像QSqlTableModel那样只做单选而是用QSqlQuery执行原生SQL动态生成一个QStandardItemModel来展示结果。这样无论查询语句多复杂结果列数、列名都能跟着SQL走。from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtSql import QSqlQuery from PyQt5.QtWidgets import QMessageBox def run_sql(self): sql self.sql_edit.text().strip() if not sql: return query QSqlQuery(self.db) if not query.exec_(sql): QMessageBox.critical(self, SQL错误, query.lastError().text()) return model QStandardItemModel() if query.isActive() and query.record().count() 0: cols query.record().count() # 动态取列名不能用固定表头 headers [query.record().fieldName(i) for i in range(cols)] model.setHorizontalHeaderLabels(headers) while query.next(): row [QStandardItem(str(query.value(i) or )) for i in range(cols)] model.appendRow(row) self.result_view.setModel(model) self.statusBar().showMessage(f返回行数 / 影响行数: {query.numRowsAffected()})query.isActive()判断SQL是否产生了结果集SELECT语句会返回TrueUPDATE/DELETE虽然执行成功但record().count()为0此时表格清空即可。query.value(i)返回的是QVariant做一次str()包裹再塞给QStandardItem能避免None值和类型转换导致显示异常。这个函数是工具里使用频率最高的路径值得认真打磨。3.3 增删改查走表单弹窗prepared statement绑定参数别拼字符串在表格里直接编辑固然方便但生产数据操作还是要有个确认动作。我用一个QDialog做表单弹窗新增、编辑、删除都从这里走减少误操作的概率。下面这段是新增用户的完整写法from PyQt5.QtWidgets import QDialog, QFormLayout, QLineEdit, QPushButton, QHBoxLayout class UserDialog(QDialog): def __init__(self, parentNone): super().__init__(parent) self.setWindowTitle(新增用户) form QFormLayout(self) self.name_edit QLineEdit() self.email_edit QLineEdit() form.addRow(姓名, self.name_edit) form.addRow(邮箱, self.email_edit) buttons QHBoxLayout() save_btn QPushButton(保存) cancel_btn QPushButton(取消) buttons.addWidget(save_btn) buttons.addWidget(cancel_btn) form.addRow(buttons) save_btn.clicked.connect(self.accept) cancel_btn.clicked.connect(self.reject) def add_user(self): dlg UserDialog(self) if dlg.exec_() ! QDialog.Accepted: return query QSqlQuery(self.db) query.prepare(INSERT INTO users(name, email) VALUES (?, ?)) query.addBindValue(dlg.name_edit.text().strip()) query.addBindValue(dlg.email_edit.text().strip()) if not query.exec_(): QMessageBox.warning(self, 插入失败, query.lastError().text()) self.refresh_model()prepare()会让数据库提前编译SQLaddBindValue()按顺序把用户输入填进占位符全程没有字符串拼接SQL注入在根源上被堵死。更新操作只是把SQL换成UPDATE users SET name?, email? WHERE id?再补一个addBindValue(id)逻辑完全一致。3.4 刷新模型和状态栏同步每次操作后让界面说真话工具做过增删改之后界面上要有即时反馈。我写了一个refresh_model()每次提交成功后调用它重刷表格和状态栏。这里有个关键细节QTableView绑定的Model如果被替换旧Model会被释放所以不能只调select()要看当前到底绑的是哪个Model再决定刷新逻辑。def refresh_model(self, table_name: str users): self.model QSqlTableModel(self, self.db) self.model.setTable(table_name) self.model.setEditStrategy(QSqlTableModel.OnManualSubmit) self.model.select() self.result_view.setModel(self.model) self.statusBar().showMessage(f{table_name} 表共 {self.model.rowCount()} 行)如果SQL查询面板刚执行过一条自定义SQL此时结果区绑的是QStandardItemModel再调refresh_model就会把它换成表模型。这是设计取舍原生SQL查询和表模型操作共用一块显示区必然有一个覆盖的关系只要每次操作后都主动刷新用户看到的就是最新状态。状态栏同步很关键它让用户确认“刚才那条更新到底影响了几行”。4. 数据库连接参数与事务策略一份连接配置既跑SQLite也接MySQL4.1 各数据库驱动的连接参数对照与什么时候用SQLitePyQt5的SQL模块抽象做得比较统一换数据库基本就是换驱动名和几个参数。我整理过一份对照表工具里直接用代码判断数据库驱动名关键参数注意点SQLiteQSQLITEsetDatabaseName(文件路径)不需要主机和端口文件即库MySQL / MariaDBQMYSQLsetHostName / setPort / setDatabaseName / setUserName / setPasswordQt安装包可能没带mysql插件PostgreSQLQPSQL同上驱动通常需要单独编译ODBC数据源QODBCsetDatabaseName(DSN名)适合接SQL Server等工具默认SQLite理由很实际零配置一个文件拷走就能跑适合“源码数据库”这种交付形态。但代码不能写死所以我封装了一个连接函数靠驱动名切换def connect_db(driver: str, params: dict): conn_name params.get(conn_name, main_conn) db QSqlDatabase.addDatabase(driver, conn_name) if driver QSQLITE: db.setDatabaseName(params[db_file]) else: db.setHostName(params[host]) db.setPort(params[port]) db.setDatabaseName(params[dbname]) db.setUserName(params[user]) db.setPassword(params[password]) if not db.open(): raise RuntimeError(db.lastError().text()) return db给连接起名字的习惯值得养成。addDatabase不传连接名时用的是默认连接一旦窗口或线程里开了第二个连接默认连接容易张冠李戴。带名字的连接在释放时也更明确QSqlDatabase.removeDatabase(conn_name)。4.2 批量写入一万行事务提交节奏为什么是“500条一提交”默认情况下每一条SQL执行完就自动提交了这在批量写入时是灾难。一万行INSERT如果每条都独立事务磁盘同步的损耗会让写入慢得不可接受。解决办法是用数据库事务包住分批提交。我这里用500条一提交兼顾内存占用和失败回滚粒度def batch_insert(db, total10000, batch500): db.transaction() # 显式开启事务 query QSqlQuery(db) query.prepare(INSERT INTO logs(msg, created_at) VALUES (?, datetime(now))) for i in range(total): query.addBindValue(flog-{i}) if not query.exec_(): db.rollback() # 出错回滚整批 raise RuntimeError(query.lastError().text()) if i 0 and i % batch 0: db.commit() # 每500条落一次盘 db.transaction() # 开启下一批事务 db.commit()为什么不是每条提交或一万条一次性提交每条提交慢一万条一次性提交会占住事务很久SQLite可能出现database is lockedMySQL等大库也可能锁竞争变激烈。500条是一个折中回滚时最多损失500条执行时间也稳定。事务中间一旦出现错误rollback()把当前批次全部撤销避免半截数据入库。4.3 连接生命周期不要随手removeDatabase连接生命周期管理是工具里最容易被忽视的部分。主窗口单线程跑着一条长连接程序退出时先close()再removeDatabase()看似简单但顺序错了会看到一个奇怪的警告QSqlDatabasePrivate::removeDatabase: connection ... is still in use。原因通常是连接对象还有其他引用活着比如QSqlQuery或QSqlTableModel还在用它。def teardown(self): self.model None # 先释放 Model 对连接的引用 self.db.close() # 再关闭连接 conn_name self.db.connectionName() del self.db # 解除 Python 侧引用 QSqlDatabase.removeDatabase(conn_name)顺序不能反。QSqlQuery如果不传连接参数会自动从默认连接取如果那个连接恰好被移除后续查询就会在运行时崩。工具里我坚持所有QSqlQuery(db)都显式传入连接对象一是线程安全二是代码清楚查问题时不至于猜数据从哪个连接来的。5. PyQt5开发数据库工具避坑记录驱动加载失败、表格不刷新、SQLite锁都在这里5.1 QSQLITE driver not loaded打包后最容易踩的雷现象源码环境跑得好好的用PyInstaller打包成exe后双击运行报QSqlDatabase: QSQLITE driver not loaded。原因PyInstaller默认不会把PyQt5的sqldrivers插件目录完整收进包运行时Qt找不到驱动。解决打包时显式加上插件目录Windows下用分号分隔源和目标pyinstaller --windowed --nameDBTool \ --add-data PyQt5/Qt/plugins/sqldrivers;PyQt5/Qt/plugins/sqldrivers main.pyLinux/macOS要把分号换成冒号。还有一种情况是本地开发环境本身没装全PyQt5只装了PyQt5-Qt5的子集重装完整PyQt5即可。5.2 编辑完表格点保存数据却“消失”了现象表格里改了内容点保存按钮后表格变成了原来那几行改动全部不见。原因setEditStrategy(QSqlTableModel.OnManualSubmit)模式下改动只存在Model的内存缓存里没调submitAll()就直接调了select()select()用数据库最新数据把缓存覆盖了。解决保存时先提交再刷新if not self.model.submitAll(): QMessageBox.warning(self, 保存失败, self.model.lastError().text()) else: self.model.select()注意lastError()要从Model上取不要从QSqlQuery上取两者错误状态不是同一个。5.3 database is locked谁把SQLite文件锁住了现象工具里执行批量更新时报database is locked表现时好时坏。原因同一时间有另一个连接正在写同一个SQLite文件或者自己的事务开着没提交就继续执行下一条写操作。SQLite的写锁粒度是整个文件长事务最危险。解决事务包小包短及时commit()连接建立后执行PRAGMA busy_timeout3000让SQLite等锁最多3秒query QSqlQuery(db) query.exec_(PRAGMA busy_timeout3000) query.exec_(PRAGMA journal_modeWAL)journal_modeWAL把写并发能力提一档工具场景几乎必开。5.4 select()之后表格不刷新新数据看不见现象Insert成功后调了model.select()界面还是旧数据。原因select()执行失败被忽略了或者setTable()之后只执行过一次select()后续插入的数据没触发模型的数据变化信号。解决先检查model.lastError()SQLite在AUTOINCREMENT表上执行批量插入时如果事务没提交外面查不到是正常的优先怀疑事务没结束。另外setTable()后必须重新select()光是setTable不会加载数据。5.5 中文路径和中文表名引发的玄学问题现象数据库文件放在D:\数据\demo.db时工具时好时坏打包后经常打不开库表名带中文也偶发查询失败。原因大部分是Python 3的字符串编码和Windows控制台或文件系统编码不一致还有少量是SQL拼接时中文没加引号。解决文件路径全部用pathlib.Path处理不要手动拼字符串表名和字段名在SQL里用反引号包起来如SELECT * FROM用户表。中文乱码一旦出现先确认db.setDatabaseName()传的是str而不是bytesencode()操作在这里只会添乱。6. 从能用变得好用PyInstaller打包、定时巡检和导出CSV的三种能力6.1 PyInstaller打包参数windowed隐藏控制台并带上SQL驱动工具给自己用可以跑源码给别人用就得打包。我常用的打包命令是pyinstaller --windowed --nameDBTool --add-data PyQt5/Qt/plugins/sqldrivers;PyQt5/Qt/plugins/sqldrivers main.py--windowed隐藏控制台窗口避免U盘拷走的工具在别人桌面上弹黑色命令框吓人。--add-data把sqldrivers目录打进包解决5.1里的驱动缺失。打包时间会有点长几十秒到几分钟都正常体积在几十MB上下PyQt5的应用这个体积是正常水平。6.2 定时巡检让工具自动查一次表并弹窗提醒工具能定时跑SQL就比普通数据库客户端多了一层“巡检”能力。用QTimer在后台轮询发现异常数据弹窗提醒。注意QTimer的回调跑在主线程SQL查询要快别在槽函数里做重型统计。from PyQt5.QtCore import QTimer self.timer QTimer(self) self.timer.timeout.connect(self.check_alert) self.timer.start(60000) # 每 60000 毫秒即每分钟跑一次 def check_alert(self): query QSqlQuery(self.db) query.exec_(SELECT COUNT(*) FROM alerts WHERE statuspending) if query.next() and query.value(0) 0: QMessageBox.warning(self, 巡检提醒, f有 {query.value(0)} 条待处理提醒)凌晨跑批后看看有没有失败记录或者盯着一张业务表的数据量有没有突增这种小功能在运维场景里相当实用代码量却只有几行。6.3 导出CSV数据不引第三方库也能把结果留下导出功能是工具最后的体面。用Python标准库csv就能完成不引入openpyxl这样的大依赖。重点是编码用utf-8-sigExcel打开才不乱码import csv def export_csv(self, file_path: str): query QSqlQuery(self.db) query.exec_(SELECT * FROM users) with open(file_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) cols query.record().count() headers [query.record().fieldName(i) for i in range(cols)] writer.writerow(headers) while query.next(): writer.writerow([query.value(i) for i in range(cols)])如果哪天需要多sheet、单元格合并或者自定义样式再换openpyxl不迟CSV足够对付90%的交付场景。我的习惯是每次导出前先弹一个文件选择框让用户确认路径而不是默认存到程序目录导出完了顺手在状态栏显示“已导出N行”。这个工具做到这里已经能覆盖日常需求了。回看整个方案核心就一句话QSqlTableModel管表、QSqlQuery管SQL、QTimer管巡检三层各司其职。踩过的坑集中在驱动打包、事务提交时机和SQLite锁这三块每次遇到我就先往这三个方向排查省掉一大半瞎折腾的时间。有人问为什么不直接用现成的数据库管理软件对这个场景来说自己写的工具能按自己的操作习惯定制还能把业务校验逻辑融进去这本身就是价值。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑