多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于QT与SQLite的桌面数据库工具开发实战:从环境搭建到CRUD实现

基于QT与SQLite的桌面数据库工具开发实战:从环境搭建到CRUD实现 1. 项目概述为什么选择QTSQLite做桌面数据库工具最近在整理一些本地数据发现用Excel或者记事本管理起来太零散了想找个轻量级的工具来可视化操作和查询。市面上现成的数据库管理工具功能是强大但要么太重要么不够灵活不能按我的需求定制界面和功能。于是我就琢磨着自己动手写一个。对于这种桌面端的小工具QT框架加上SQLite数据库几乎就是“黄金搭档”。QT是一套成熟的C跨平台应用框架它的GUI组件库非常丰富做出来的界面既好看又专业。更重要的是QT对数据库操作有原生的、封装得很好的支持通过QSql模块我们可以用几乎统一的接口去操作SQLite、MySQL、PostgreSQL等多种数据库这大大降低了开发门槛。而SQLite它是一个进程内的、零配置的、服务器端的数据库引擎。简单说它就是一个文件。你把一个.db或.sqlite文件放在那里程序就能直接读写不需要安装任何数据库服务特别适合这种单机、轻量级的应用场景。我这次用的环境是QT 5.12.3 LTS长期支持版这个版本非常稳定社区资料也丰富。C作为实现语言能保证程序的运行效率。最终目标是实现一个具备基础增删改查CRUD功能并且带有表格可视化界面的小工具。它不仅能帮我管理数据整个开发过程本身也是对QT数据库编程和模型/视图架构的一次深度实践。2. 环境搭建与项目初始化2.1 QT 5.12.3 与 SQLite 驱动确认首先确保你的开发环境已经正确安装了QT 5.12.3。你可以通过QT官方安装器或者自己编译源码来安装。安装时务必勾选上Sources和Qt Script某些旧组件可能依赖当然最重要的是确保你安装的套件包含了Qt SQL模块。安装完成后创建一个简单的控制台项目来测试SQLite驱动是否可用。在main.cpp里写下如下代码#include QCoreApplication #include QSqlDatabase #include QDebug #include QStringList int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() Available SQL drivers:; QStringList drivers QSqlDatabase::drivers(); foreach(const QString driver, drivers) { qDebug() driver; } // 特别检查SQLite if (drivers.contains(QSQLITE)) { qDebug() \nQSQLITE driver is available. Good to go!; } else { qDebug() \nERROR: QSQLITE driver NOT found!; qDebug() Your QT installation might lack the SQL module or SQLite support.; return -1; } return 0; }运行这个程序如果输出列表中包含“QSQLITE”那么恭喜你基础环境没问题。QT通常默认就编译了SQLite驱动因为SQLite本身就是一个轻量的头文件库。2.2 新建QT Widgets项目与基础配置打开Qt Creator新建一个项目选择Qt Widgets Application。给项目起个名字比如DatabaseViewer。在Kit Selection那一步选择你安装的QT 5.12.3套件比如Desktop Qt 5.12.3 MSVC2017 64bit。项目创建好后第一件事就是修改项目配置文件.pro文件添加SQL模块。打开DatabaseViewer.pro你会看到类似这样的内容QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets我们需要在其中加上sql变成QT core gui sql greaterThan(QT_MAJOR_VERSION, 4): QT widgets保存.pro文件Qt Creator会提示你重新解析项目点确定即可。这一步至关重要它告诉构建系统我们的项目需要链接到QtSql库。接下来设计主界面。打开mainwindow.ui文件从左侧的Widget Box里拖拽控件进行布局。一个基础的数据管理界面通常包含以下区域连接/数据库控制区放置按钮如“连接数据库”、“新建表”。数据展示区一个QTableView控件这是显示数据库表内容的核心。数据操作区放置“新增”、“删除”、“修改”、“查询”按钮。SQL命令执行区一个QTextEdit或QPlainTextEdit用于输入SQL语句一个“执行”按钮一个QTableView或QTextEdit用于显示执行结果。你可以参考下面的简单布局使用水平布局QHBoxLayout和垂直布局QVBoxLayout组合左侧面板垂直放置数据库操作按钮。中间主区域垂直上方放QTableView下方放SQL命令输入框和执行按钮。右侧面板垂直放置针对当前选中记录的数据操作按钮增删改。注意在UI设计阶段建议为所有后续需要在代码中操作的控件起一个清晰的objectName例如将QTableView命名为tableView将执行SQL的按钮命名为pushButtonExecute。这能让你在代码中通过ui-tableView的方式快速访问它们避免混淆。3. 核心模块实现连接、模型与视图3.1 数据库连接管理数据库操作的第一步是建立连接。我们在MainWindow类中实现。首先在mainwindow.h中添加必要的头文件和成员变量/函数声明#ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include QSqlDatabase #include QSqlTableModel namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: void on_actionOpenDatabase_triggered(); // 通过菜单或按钮触发的槽函数 void onCurrentTableChanged(const QString tableName); // 响应表选择变化 private: Ui::MainWindow *ui; QSqlDatabase m_db; // 数据库连接对象 QSqlTableModel *m_model; // 用于在TableView中显示数据的模型 void setupDatabaseModel(); // 初始化模型和视图的连接 }; #endif // MAINWINDOW_H在mainwindow.cpp中实现打开数据库的逻辑#include “mainwindow.h” #include “ui_mainwindow.h” #include QFileDialog #include QMessageBox #include QSqlError #include QSqlQuery MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), m_model(nullptr) { ui-setupUi(this); // 初始化时数据库是未连接状态 // 可以在这里连接一些信号槽比如一个用来选择数据库文件的按钮 connect(ui-actionOpenDatabase, QAction::triggered, this, MainWindow::on_actionOpenDatabase_triggered); } MainWindow::~MainWindow() { // 程序退出前关闭数据库连接 if (m_db.isOpen()) { m_db.close(); } delete m_model; delete ui; } void MainWindow::on_actionOpenDatabase_triggered() { // 弹出一个文件选择对话框让用户选择SQLite数据库文件 QString dbFilePath QFileDialog::getOpenFileName(this, tr(“打开数据库文件”), QDir::homePath(), tr(“SQLite数据库文件 (*.db *.sqlite *.sqlite3);;所有文件 (*.*)”)); if (dbFilePath.isEmpty()) { return; // 用户取消了选择 } // 如果之前已经打开了一个数据库先关闭它 if (m_db.isOpen()) { m_db.close(); if (m_model) { m_model-clear(); } } // 使用QSQLITE驱动创建数据库连接连接名可以自定义这里用“default_connection” m_db QSqlDatabase::addDatabase(“QSQLITE”, “default_connection”); m_db.setDatabaseName(dbFilePath); // SQLite数据库名就是文件路径 if (!m_db.open()) { QMessageBox::critical(this, tr(“数据库错误”), tr(“无法打开数据库\n%1”).arg(m_db.lastError().text())); return; } // 数据库打开成功更新UI状态比如窗口标题 setWindowTitle(tr(“Database Viewer - [%1]”).arg(QFileInfo(dbFilePath).fileName())); // 获取数据库中的所有表名并填充到一个QComboBox中供用户选择 QStringList tables m_db.tables(QSql::Tables); ui-comboBoxTableList-clear(); ui-comboBoxTableList-addItems(tables); // 连接表名选择变化的信号 connect(ui-comboBoxTableList, QComboBox::currentTextChanged, this, MainWindow::onCurrentTableChanged); // 如果存在表默认选中第一个并显示 if (!tables.isEmpty()) { onCurrentTableChanged(tables.first()); } }实操心得QSqlDatabase::addDatabase()的第二个参数是连接名。如果你在整个应用中只操作一个数据库可以省略它QT会使用一个默认连接名。但如果你需要同时连接多个数据库比如连接一个本地的SQLite和一个远程的MySQL就必须为每个连接指定唯一的名称并在后续操作如创建QSqlQuery对象时指明使用哪个连接。这是一个容易忽略但很重要的细节。3.2 QSqlTableModel与QTableView的绑定QT的模型/视图Model/View架构是其核心优势之一。QSqlTableModel是一个为数据库表操作量身定制的模型类它可以直接映射一张数据库表。QTableView则是用来显示表格数据的视图。将它们绑定后对模型的数据修改在满足一定策略下可以自动同步到数据库。在MainWindow类中添加setupDatabaseModel函数的实现并在onCurrentTableChanged槽函数中调用它void MainWindow::setupDatabaseModel() { if (!m_db.isOpen()) { return; } // 如果已有模型先删除旧的 if (m_model) { delete m_model; m_model nullptr; } // 创建一个新的QSqlTableModel并指定其使用的数据库连接 m_model new QSqlTableModel(this, m_db); // 设置编辑策略。OnManualSubmit表示需要手动调用submitAll()或revertAll()来提交或回滚更改。 // OnFieldChange则是在字段编辑后立即提交。这里使用OnManualSubmit给我们更大的控制权。 m_model-setEditStrategy(QSqlTableModel::OnManualSubmit); // 将模型设置给TableView ui-tableView-setModel(m_model); // 一些TableView的显示优化 ui-tableView-setSelectionMode(QAbstractItemView::SingleSelection); // 单选 ui-tableView-setSelectionBehavior(QAbstractItemView::SelectRows); // 选中整行 ui-tableView-horizontalHeader()-setStretchLastSection(true); // 最后一列填充空白 ui-tableView-setAlternatingRowColors(true); // 行背景交替颜色更易读 } void MainWindow::onCurrentTableChanged(const QString tableName) { if (tableName.isEmpty() || !m_model) { return; } // 设置模型要操作的表名 m_model-setTable(tableName); // 选择数据相当于执行 SELECT * FROM tableName if (!m_model-select()) { QMessageBox::warning(this, tr(“查询错误”), tr(“无法从表 ‘%1’ 中获取数据\n%2”) .arg(tableName) .arg(m_model-lastError().text())); return; } // 可选设置列标题默认使用数据库字段名可以自定义为更友好的名字 // m_model-setHeaderData(0, Qt::Horizontal, tr(“ID”)); // m_model-setHeaderData(1, Qt::Horizontal, tr(“姓名”)); // ... // 刷新TableView的显示 ui-tableView-resizeColumnsToContents(); // 根据内容调整列宽 }现在运行程序打开一个SQLite数据库文件如果还没有可以用下一节的方法创建一个然后在表下拉框中选择一个表你应该能看到表中的数据整齐地显示在QTableView里了。注意事项QSqlTableModel::select()会拉取整个表的数据到内存中。对于数据量非常大的表几十万、上百万行这会导致界面卡顿甚至内存不足。在生产环境中对于大数据集应该考虑使用QSqlQueryModel配合自定义的分页逻辑或者使用QTableView的setIndexWidget等高级特性进行懒加载。QSqlTableModel更适合中小型数据集的全量编辑场景。4. 实现增删改查CRUD功能有了模型和视图的基础实现CRUD就相对直观了。我们将通过按钮触发这些操作。4.1 新增记录在UI上添加一个“新增”按钮并在MainWindow类中为其创建槽函数可以通过Qt Creator右键按钮-“转到槽”快速创建。void MainWindow::on_pushButtonAdd_clicked() { if (!m_model) { return; } // 在模型的末尾插入一行空记录 int row m_model-rowCount(); if (!m_model-insertRow(row)) { QMessageBox::warning(this, tr(“插入失败”), tr(“无法插入新行\n%1”).arg(m_model-lastError().text())); return; } // 获取新插入行的模型索引并让TableView滚动到并编辑这一行 QModelIndex index m_model-index(row, 0); // 定位到新行的第一列 ui-tableView-setCurrentIndex(index); ui-tableView-edit(index); // 开始编辑 // 注意此时数据只是在模型的缓存中并未提交到数据库。 // 用户可以在TableView中直接编辑单元格。 }4.2 删除记录“删除”按钮的槽函数void MainWindow::on_pushButtonDelete_clicked() { if (!m_model) { return; } // 获取当前TableView中选中的行 QModelIndexList selectedIndexes ui-tableView-selectionModel()-selectedRows(); if (selectedIndexes.isEmpty()) { QMessageBox::information(this, tr(“提示”), tr(“请先选择要删除的行。”)); return; } // 由于我们设置了SelectRows行为selectedRows()返回的是每行第一个单元格的索引。 // 我们需要获取行号。注意索引可能不是按顺序的所以先排序。 QListint rowsToDelete; for (const QModelIndex index : selectedIndexes) { rowsToDelete.append(index.row()); } // 从大到小排序这样从后往前删除不会影响前面行的索引 std::sort(rowsToDelete.begin(), rowsToDelete.end(), std::greaterint()); // 确认删除 if (QMessageBox::question(this, tr(“确认删除”), tr(“确定要删除选中的 %1 行吗此操作不可撤销。”).arg(rowsToDelete.count()), QMessageBox::Yes | QMessageBox::No) QMessageBox::No) { return; } // 遍历删除 for (int row : rowsToDelete) { if (!m_model-removeRow(row)) { QMessageBox::critical(this, tr(“删除失败”), tr(“删除第 %1 行时出错\n%2”) .arg(row 1) .arg(m_model-lastError().text())); // 如果某一行删除失败可以break或者尝试继续删除其他行 // 这里选择break并回滚所有未提交的更改 m_model-revertAll(); return; } } // 删除操作成功但同样只是标记删除需要提交。 }4.3 修改记录与提交/回滚修改记录是最简单的因为用户可以直接在QTableView的单元格里双击进行编辑。我们真正需要实现的是“提交”和“撤销”按钮来将内存中的更改包括新增、删除、修改一次性写入数据库或全部放弃。void MainWindow::on_pushButtonSubmit_clicked() { if (!m_model) { return; } // 开始一个数据库事务保证一系列操作的原子性 m_db.transaction(); if (m_model-submitAll()) { // 所有更改成功提交到数据库 if (m_db.commit()) { QMessageBox::information(this, tr(“成功”), tr(“所有更改已保存到数据库。”)); // 重新查询以刷新视图特别是处理了自增ID后 m_model-select(); } else { // 提交事务失败 QMessageBox::critical(this, tr(“提交失败”), tr(“数据库事务提交失败\n%1”).arg(m_db.lastError().text())); m_db.rollback(); m_model-revertAll(); // 模型也回滚 } } else { // 模型提交失败如数据校验错误、唯一约束冲突等 QMessageBox::critical(this, tr(“提交失败”), tr(“无法提交更改到数据库\n%1”).arg(m_model-lastError().text())); m_db.rollback(); // 模型会自动回滚到未提交状态吗对于OnManualSubmit策略submitAll失败后模型状态是未定义的 // 安全起见显式调用revertAll()。 m_model-revertAll(); } } void MainWindow::on_pushButtonRevert_clicked() { if (!m_model) { return; } if (QMessageBox::question(this, tr(“确认撤销”), tr(“确定要放弃所有未保存的更改吗”), QMessageBox::Yes | QMessageBox::No) QMessageBox::Yes) { m_model-revertAll(); } }核心原理QSqlTableModel的setEditStrategy决定了数据同步的时机。我们选择了OnManualSubmit这给了我们最大的控制权。所有的编辑操作首先发生在模型的缓存里。只有调用submitAll()时模型才会生成相应的INSERT、UPDATE、DELETE语句并执行。同时我们手动管理数据库事务transaction(),commit(),rollback()这是确保数据一致性的关键。例如在删除多行时如果中间某一行失败事务回滚可以保证之前已经删除的行也恢复数据库状态保持完整。4.4 查询与过滤除了操作当前整个表我们经常需要按条件查询。QSqlTableModel提供了setFilter()函数。在UI上添加一个QLineEdit用于输入过滤条件和一个“过滤”按钮。void MainWindow::on_pushButtonFilter_clicked() { if (!m_model) { return; } QString filterText ui-lineEditFilter-text().trimmed(); if (filterText.isEmpty()) { // 如果过滤条件为空则清除过滤显示全部数据 m_model-setFilter(“”); } else { // 构建WHERE子句。这里是一个简单示例假设我们有一个name字段。 // !!! 警告直接拼接字符串有SQL注入风险对于用户输入应使用参数化查询。 // 但QSqlTableModel的setFilter本身期望一个SQL WHERE子句字符串。 // 在简单内部工具中如果信任用户输入或做严格校验可以这样用。 // 更安全的做法是使用QSqlQuery进行参数化查询然后手动设置模型数据但这更复杂。 QString filter QString(“name LIKE ‘%%1%’”).arg(filterText); // 模糊查询 m_model-setFilter(filter); } if (!m_model-select()) { QMessageBox::warning(this, tr(“过滤错误”), tr(“应用过滤条件失败\n%1”).arg(m_model-lastError().text())); } ui-tableView-resizeColumnsToContents(); }重要安全提示上面的示例为了简单直接将用户输入拼接到了SQL语句中这存在严重的SQL注入风险。在实际项目中如果过滤条件复杂或来自不可信源绝对不应该这样做。对于QSqlTableModel一个相对安全的做法是使用QSqlQuery进行参数化查询获取到结果集QSqlRecord后手动创建一个QStandardItemModel来展示数据。或者对用户输入进行严格的校验和转义。对于简单的、受控的内部工具且字段类型已知如整数、日期可以构建更安全的过滤字符串例如QString(“id %1”).arg(userInput.toInt())。5. 执行自定义SQL与高级功能5.1 实现SQL命令执行器一个完整的数据库工具怎么能少得了直接运行SQL语句的功能呢我们在UI上放置一个QPlainTextEdit命名为textEditSql用于输入SQL一个QPushButton命名为pushButtonExecSql来执行另一个QTableView命名为tableViewSqlResult来显示结果。void MainWindow::on_pushButtonExecSql_clicked() { QString sql ui-textEditSql-toPlainText().trimmed(); if (sql.isEmpty()) { return; } QSqlQuery query(m_db); // 使用当前数据库连接创建查询对象 if (!query.exec(sql)) { QMessageBox::critical(this, tr(“SQL执行错误”), tr(“执行SQL语句时出错\n%1\n\nSQL: %2”) .arg(query.lastError().text()) .arg(sql)); return; } // 判断查询类型 if (query.isSelect()) { // 是SELECT查询有结果集 // 使用QSqlQueryModel来展示结果很方便 QSqlQueryModel *sqlModel new QSqlQueryModel(this); sqlModel-setQuery(std::move(query)); // 将查询对象转移给模型 ui-tableViewSqlResult-setModel(sqlModel); ui-tableViewSqlResult-resizeColumnsToContents(); // 注意sqlModel的生命周期。这里每次执行都new一个新的旧的需要删除。 // 一个更优的做法是复用同一个模型并在设置新查询前删除旧数据。 // 这里简单起见每次创建新的。需要处理旧模型的删除避免内存泄漏。 QAbstractItemModel *oldModel ui-tableViewSqlResult-model(); if (oldModel oldModel ! m_model) { // 注意不要误删了主表的模型 delete oldModel; } } else { // 是INSERT, UPDATE, DELETE, CREATE TABLE等非SELECT语句 int numRowsAffected query.numRowsAffected(); QMessageBox::information(this, tr(“执行成功”), tr(“SQL语句执行成功。\n影响的行数%1”) .arg(numRowsAffected -1 ? QString::number(numRowsAffected) : tr(“未知”))); // 如果执行的语句可能改变了当前主表的数据刷新主表视图 if (m_model sql.contains(m_model-tableName(), Qt::CaseInsensitive)) { // 简单判断SQL中是否包含了当前表名如果是则刷新。这是一个粗略的判断。 m_model-select(); } } }5.2 数据库与表的创建我们可以扩展功能让用户能直接创建新的数据库文件或新表。创建新数据库其实就是创建一个新的SQLite文件。void MainWindow::on_actionNewDatabase_triggered() { QString savePath QFileDialog::getSaveFileName(this, tr(“新建数据库文件”), QDir::homePath(), tr(“SQLite数据库文件 (*.db *.sqlite);;所有文件 (*.*)”)); if (savePath.isEmpty()) { return; } // 如果文件已存在QSqlDatabase会打开它。这里我们先检查避免覆盖。 if (QFile::exists(savePath)) { if (QMessageBox::question(this, tr(“文件已存在”), tr(“文件 ‘%1’ 已存在是否打开它”).arg(savePath), QMessageBox::Yes | QMessageBox::No) QMessageBox::No) { return; } } // 关闭当前连接如果有 if (m_db.isOpen()) { m_db.close(); } // 创建并打开新数据库如果文件不存在SQLite会自动创建 m_db QSqlDatabase::addDatabase(“QSQLITE”, “default_connection”); m_db.setDatabaseName(savePath); if (!m_db.open()) { QMessageBox::critical(this, tr(“错误”), tr(“无法创建或打开数据库文件。”)); return; } setWindowTitle(tr(“Database Viewer - [%1]”).arg(QFileInfo(savePath).fileName())); // 新数据库是空的清空表列表和主视图 ui-comboBoxTableList-clear(); if (m_model) { m_model-clear(); } QMessageBox::information(this, tr(“成功”), tr(“新的数据库文件已创建并打开。现在你可以创建表了。”)); }创建新表可以通过执行CREATE TABLE语句来实现。我们可以设计一个简单的对话框让用户输入表名和字段定义然后拼接成SQL执行。这里为了演示我们提供一个简化版的通过SQL命令创建的方式引导用户在SQL执行器中输入CREATE TABLE语句。6. 常见问题、调试技巧与优化6.1 中文乱码问题这是一个非常常见的问题尤其是在Windows环境下。乱码可能出现在路径或数据库文件名包含中文QT默认使用UTF-8编码而Windows系统本地编码可能是GBK。使用QFileDialog获取的路径是Unicode内部是UTF-16直接传给setDatabaseName没问题。但如果你手动拼接了包含中文的路径字符串要确保字符串字面量是UTF-8编码。在.pro文件中添加CONFIG utf8_source是个好习惯。数据库内数据的中文乱码SQLite默认以UTF-8编码存储文本。确保你在插入数据时字符串是正确的UTF-8格式。在C代码中QString内部是UTF-16当它被转换为QByteArray例如通过toUtf8()传递给SQLite驱动时驱动会将其作为UTF-8处理。关键点在于统一使用UTF-8。在代码中所有字符串字面量用QStringLiteral或隐式构造。从文件或网络读取数据时明确指定编码。在UI设计器中控件显示的文本在Qt Creator里设置一般没问题。SQL语句中包含中文同上确保SQL语句字符串本身是UTF-8编码。如果从数据库读出的中文显示为乱码而写入是正确的很可能是显示环节的问题。QTableView、QLineEdit等控件默认都能正确显示UTF-8编码的QString。问题可能出在数据库连接本身。有一种古老的做法是在打开数据库后执行一条PRAGMA encoding “UTF-8”;语句但对于SQLite 3.x这通常不是必须的。6.2 数据库被锁与并发访问SQLite在写入INSERT, UPDATE, DELETE时会锁定整个数据库文件。这意味着如果你的程序正在写入其他进程或线程尝试写入就会失败报错database is locked。解决方案与最佳实践写操作序列化确保在你的应用内同一时间只有一个写操作在进行。使用事务如我们之前做的可以将多个写操作包装成一个原子操作减少锁的持有时间。超时设置QSqlDatabase可以设置连接选项。在打开数据库前可以尝试设置忙等待超时。m_db QSqlDatabase::addDatabase(“QSQLITE”); m_db.setDatabaseName(dbFilePath); m_db.setConnectOptions(“QSQLITE_BUSY_TIMEOUT5000”); // 设置忙等待超时为5秒 if (!m_db.open()) { ... }对于真正的多线程访问SQLite支持多线程的“串行化”模式但需要编译时开启相应选项。更常见的QT多线程数据库做法是每个线程使用独立的数据库连接但连接同一个文件并且写操作必须严格序列化。通常我们会将数据库操作封装在一个单独的类中并使用信号槽或事件队列来接收其他线程的请求在这个类的单线程中进行实际的数据库调用。6.3 程序崩溃与调试崩溃在QSqlDatabase或QSqlQuery析构时这通常是因为对象的生命周期管理问题。确保QSqlQuery对象在使用它的QSqlDatabase连接关闭之前被销毁。并且在QSqlQuery对象销毁前不要销毁它所属的QSqlDatabase对象。在我们的设计中m_db和m_model内部使用m_db都是MainWindow的成员随着主窗口一起创建和销毁顺序是正确的。QSqlTableModel提交失败仔细检查lastError()返回的信息。常见原因有字段非空约束、唯一约束、外键约束违反数据类型不匹配等。在调用submitAll()之前可以通过m_model-data(m_model-index(row, col), Qt::EditRole)来获取用户输入的数据并进行校验。使用qDebug()输出调试信息在关键步骤如打开数据库、执行SQL前后输出状态信息和错误信息能极大帮助定位问题。qDebug() “Attempting to open database at:” dbFilePath; if (!m_db.open()) { qDebug() “Open failed:” m_db.lastError(); } else { qDebug() “Open successful. Tables:” m_db.tables(); }6.4 性能优化与小技巧批量操作使用事务如前所述将大量的INSERT/UPDATE操作放在一个事务中比每条语句一个事务要快几个数量级。适时调用QSqlTableModel::select()select()会重新查询整个表。在数据变化后如果确定需要刷新再调用它。避免在频繁触发的事件如单元格编辑中调用。为QTableView启用代理Delegate进行数据验证和格式化你可以继承QItemDelegate或QStyledItemDelegate重写createEditor,setEditorData,setModelData等方法在用户编辑时提供下拉框、日期选择器等友好控件并进行输入验证。显示大量数据时考虑实现分页。QSqlTableModel本身不支持分页。你可以用QSqlQuery执行SELECT * FROM table LIMIT pageSize OFFSET (pageNum-1)*pageSize然后用QStandardItemModel来装载当前页的数据。保存窗口状态与列宽使用QSettings来保存主窗口的几何状态、表格各列的宽度、最近打开的数据库文件路径等提升用户体验。// 在MainWindow构造函数中读取设置 QSettings settings(“MyCompany”, “DatabaseViewer”); restoreGeometry(settings.value(“mainWindowGeometry”).toByteArray()); restoreState(settings.value(“mainWindowState”).toByteArray()); // 在closeEvent中保存设置 void MainWindow::closeEvent(QCloseEvent *event) { QSettings settings(“MyCompany”, “DatabaseViewer”); settings.setValue(“mainWindowGeometry”, saveGeometry()); settings.setValue(“mainWindowState”, saveState()); QMainWindow::closeEvent(event); }这个项目从环境搭建到核心功能实现涵盖了QT数据库编程的大部分基础知识点。它不仅仅是一个工具更是一个理解QT模型/视图架构、SQLite操作以及桌面应用开发的绝佳练习。你可以在此基础上继续扩展比如增加数据导入导出CSV、Excel、图表展示使用Qt Charts、用户权限管理、更复杂的多表关联查询视图等功能让它真正成为一个得心应手的个人数据管理瑞士军刀。
返回列表