Skip to content
正常

SQLite Browser 成品导览

sourceapp/10-database-tools/sqlite-browser/ related:app 栏数据库工具类整机成品

SQLite Browser 是 app 栏「数据库工具」这一类的整机成品。前面 widget 栏讲究单控件、image-viewer 把「看一张图」做到底;这件换一条线——把一个 SQLite 文件吃进来,浏览、改、随便跑 SQL。它的价值不在某个算法多巧,而在把 Qt 的 SQL 模块(QSqlDatabase / QSqlTableModel / QSqlQueryModel)和 Model/View 那套(QTableView)织成一台能用的机器,而且把数据库应用里最容易翻车的几个雷——连接还没释放就去 removeDatabase、切表残留旧数据、可编辑表与只读查询混用——都防住了。

本篇是「成品导览」

想直接用成品 → 看这里(架构 / 决策 / 踩坑 / 怎么读)。 想自己从零搓出来 → 转 手搓手册

1. 它做什么

一个能用的 SQLite 数据库浏览器:

  • 打开:菜单 / 工具栏 Open Database…(Ctrl+O),挑一个 .db/.sqlite/.sqlite3 文件,自动列出里面所有表
  • 浏览表:左边 QListWidget 列表点一张表,右边 QTableView 显示该表全部行;可以直接在单元格里改数据,失焦即提交(OnFieldChange)
  • 翻页:表内容走 Model/View,数据量再大也是按需取,状态栏实时显示行数
  • 跑 SQL:底部 QPlainTextEdit 写任意 SQL,按 F5 或 Execute 执行——有结果集(SELECT)就在下面 query_view 出表格(只读),没结果集(INSERT/UPDATE/DELETE/DDL)就在状态栏报影响行数
  • 关库:菜单 / 工具栏 Close Database,安全释放所有 model 再 removeDatabase,不留「connection still in use」的雷

跑起来看一眼:

bash
cmake -B build -S app && cmake --build build
./build/10-database-tools/sqlite-browser/demo/sqlite-browser_demo

2. 架构总览

类关系

整机就一个核心类 SqliteBrowserWindow(QMainWindow),它持有两套「表 → view」的链:可编辑表QSqlTableModel 填进 table_view_任意 SQL 结果走只读 QSqlQueryModel 填进 query_view_。左边 QListWidget 列表驱动表选择,底部 QPlainTextEdit 驱动 SQL 执行。所有数据库访问都挂在命名连接connection_name_)上,从不碰默认连接。

文件职责

文件职责
demo/sqlite_browser_window.h主窗口接口:菜单/工具栏/状态栏装配 + 两套 model + 命名连接状态;头注释讲清三条关键设计
demo/sqlite_browser_window.cpp主窗口实现:命名连接开/关、sqlite_master 列表、OnFieldChange 可编辑表、任意 SQL 走只读 model、安全释放链
demo/main.cpp入口:QApplication + 主窗口 show
demo/CMakeLists.txt工程配置——qt_add_executable + 链接 Qt6::Sql(关键:不链 Sql 整个编译都过不了)

点一张表怎么把数据填进表格

重点:切表前必先 clearTableModel()——先把 view 的 model 设 nullptr,再 delete 旧 model。顺序反了(先 delete 再 setModel 不会触发,但 view 还握着已删 model 指针)Qt 会打 warning,更糟的是旧 model 还引用着旧 db 连接,关库时 removeDatabaseconnection still in use。这条「先松手再 delete」是整份代码的第一条命脉(第二条是「可编辑表 vs 只读查询」用两个不同 model 隔离)。

3. 关键设计决策

① 每个 db 用「命名连接」,不碰默认连接。QSqlDatabase::addDatabase("QSQLITE")(不传第二参)会挂到默认连接上,多个 db 同时开、或默认连接被别处误用就乱套。这里一律传一个连接名——makeConnectionName 用「文件名 + 最后修改时间戳 + 自增序号」拼(QSqlDatabase 连接注册表是进程级,basename 相同且 mtime 同秒的多窗口也不撞名),同名 db 反复打开也不撞名。关库时按这个名精确 removeDatabase,干净。(sqlite_browser_window.cpp:139-146)

② 关库 / 切表前,先让所有 model「松手」再 delete,最后才 removeDatabaseremoveDatabase 的前提是所有引用该连接的 QSqlDatabase 副本都已销毁——而 QSqlTableModel/QSqlQueryModel 内部就握着一份副本。直接 removeDatabase 会触发 Qt 经典警告 QSqlDatabasePrivate::removeDatabase: connection 'xxx' is still in use,连接也不会真释放。这里的 closeDatabase 严格排序:先 clearTableModel() + 清 query_model → 再在嵌套作用域db.close()(让 QSqlDatabase 副本随作用域析构,引用数回落到 1)→ 最后 removeDatabase。(sqlite_browser_window.cpp:196-219)

③ 可编辑表用 QSqlTableModel + OnFieldChange,任意 SQL 结果用只读 QSqlQueryModel,物理隔离。 「双击单元格改数据」需要可写 model——QSqlTableModel 干这个,配 OnFieldChange 失焦即提交;但任意 SQL(尤其多表 JOIN、聚合)没法映射成单表读写,强行塞进 QSqlTableModel 会丢编辑能力还容易报错。于是把两类结果分到两个 view + 两个 model:表内容填 table_view_(可编辑),查询结果填 query_view_(只读 QSqlQueryModel)。互不污染、各走各的。(sqlite_browser_window.cpp:282-295 vs 339-341)

④ 任意 SQL 先 QSqlQuery::exec 探路,用 isSelect() 区分「有结果集」与「DML/DDL」,分别处理。 用户在框里写的可能是 SELECT(要出表格)、也可能是 INSERT/UPDATE/DELETE/CREATE(不出表格、只报影响行数)。直接拿 QSqlQueryModel::setQuery(sql) 会把后者当成无结果查询、表格空着、行数也读不到。这里先用 QSqlQuery::exec(sql) 执行,靠 query.isSelect() 分流:无结果集读 numRowsAffected() 报状态栏、有结果集再把已 exec 的 query std::moveQSqlQueryModel 渲染。一次执行、零浪费。(sqlite_browser_window.cpp:324-348)

refreshTableList 全程 blockSignals(true),避免 clear() 误触发 onTableSelected(-1)QListWidget::clear() 会把当前选中项清掉,连带发 currentRowChanged(-1)——如果正连着 onTableSelected,切库的瞬间就会去 clearTableModel + updateStatus,逻辑错乱。这里在 clear / addItem 整段区间 blockSignals(true),填完再放开,保证「填列表」和「用户选中」两件事不串台。(sqlite_browser_window.cpp:235-260)

4. 怎么读这份 code

按这个顺序读,最快建立心智:

  1. demo/sqlite_browser_window.h 头注释 + 成员——先看「窗口握着什么」(两套 model、命名连接、两个 view、列表/SQL 输入),三条关键设计写在头注释里
  2. setupCentralsqlite_browser_window.cpp:54)——QSplitter 怎么切出「左列表|右表格」上区 + 「查询结果|SQL 输入」下区
  3. openDatabasesqlite_browser_window.cpp:158)——命名连接的开法、open 失败的回滚(removeDatabase)
  4. refreshTableListsqlite_browser_window.cpp:234)——查 sqlite_master 取表名 + blockSignals 防串台
  5. loadTablesqlite_browser_window.cpp:274)——QSqlTableModel 装配:setTable → OnFieldChange → select → fetchMore → setModel
  6. clearTableModelsqlite_browser_window.cpp:221)——「先 submitAll 再 setModel(nullptr) 再 delete」的微操作,整份代码的防雷核心
  7. closeDatabasesqlite_browser_window.cpp:196)——释放顺序:model → 嵌套作用域 close → removeDatabase
  8. onExecuteSqlsqlite_browser_window.cpp:301)——isSelect 分流 + move query 进只读 model

入口:demo/main.cppSqliteBrowserWindow 跑起来,对照读。

5. 踩坑

#现象原因后果解法
关库时控制台刷 connection 'xxx' is still in use,连接没真释放removeDatabase 时还有该连接的 QSqlDatabase 副本没销毁(model 内部握一份;取回的 db 副本也没出作用域)——这是 Qt 文档明令禁止的反模式连接泄漏、反复开同名 db 可能撞名或资源不回收关库前先 clearTableModel + 清 query_model,再在嵌套作用域db.close()(副本随作用域析构、引用数回落到 1),最后 removeDatabasesqlite_browser_window.cpp:196-219
切表后表格显示旧表数据 / setTable 报错撞旧引用切表没先清旧 model,新 model 还没建、旧 model 残留表格内容错乱、select 撞到已失效的表/连接loadTable 第一行就 clearTableModel()sqlite_browser_window.cpp:275
delete model 后 Qt 打 QAbstractItemView: ... warningview 还指着已 delete 的 model,没先 setModel(nullptr) 松手控制台噪音、view 可能访问野指针clearTableModel view->setModel(nullptr) delete modelsqlite_browser_window.cpp:225-227
同一个 db 反复打开报 connection already exists,或多窗口打开 basename 相同且 mtime 同秒的文件互相 invalidate用默认连接、或固定连接名、或仅靠文件名+mtime——QSqlDatabase 连接注册表是进程级,同名就撞第二次开库失败、或静默串到旧连接导致数据错乱makeConnectionName 用「文件名 + mtime + QAtomicInt 自增序号」拼唯一名,进程内永不撞名(sqlite_browser_window.cpp:139-146
任意 SQL 跑 INSERT 表格空着、状态栏不报行数直接 QSqlQueryModel::setQuery(sql) 把 DML 当无结果查询,读不到 numRowsAffected用户不知道到底改了几行QSqlQuery::exec 探路,isSelect() 分流:DML 报 numRowsAffected、SELECT 才进 QueryModel(sqlite_browser_window.cpp:324-336
切库瞬间状态栏乱跳 / 表格被误清refreshTableListclear() 触发 currentRowChanged(-1) 串到 onTableSelected切库时序错乱、表格闪烁填列表全程 blockSignals(true),填完放开(sqlite_browser_window.cpp:235-260
表 >255 行时状态栏报假行数(只显示 256),翻不到底SQLite 驱动 QuerySize=falseQSqlTableModel/QSqlQueryModel 只 prefetch 255 行,rowCount() 此时只反映已取部分状态栏对大表报错行数、用户以为数据不全、分页判断失效select()/setQuery() 后循环 while (canFetchMore()) fetchMore(); 拉全量,rowCount() 才准(sqlite_browser_window.cpp:291-294342-345
OnFieldChange 编辑器还持焦时切表/关库,最后一条改动丢了失焦即提交,但切表/关库动作抢在失焦之前,最后一条未提交用户改的数据没落库、静默丢失clearTableModeltable_model_->submitAll() 兜底提交未落库编辑,再松手 delete(sqlite_browser_window.cpp:222-227
错误信息只显示一半(要么数据库层、要么驱动层)手动只取 databaseText()driverText(),漏了另一层用户看到不完整错误、排查困难错误文本统一用 err.text()(= databaseText() + driverText() 标准拼接),见 onExecuteSqlerr.text()sqlite_browser_window.cpp:326-327

6. 官方文档


这套「命名连接 + 两套 model 隔离可编辑/只读 + 安全释放链」是数据库类整机应用的通用骨架——任何「打开库 → 浏览表 → 改数据 → 跑 SQL」的工具(MySQL/Postgres 客户端、配置库编辑器、日志查看器)都能换皮复用。想自己搓?手搓手册带你从一个空 QMainWindow 一行行搓到这个成品。

AwesomeQt v0.2.0-58-gde7eeb4-dirty · de7eeb4 · 2026-07-03