Qt6+CMake+SQLite记账本:可编译可调试的桌面应用实战

发布时间:2026/10/14 22:31:37
Qt6+CMake+SQLite记账本:可编译可调试的桌面应用实战
简介本资源是一份面向Qt初学者与C桌面开发入门者的教学实践项目聚焦个人财务管理场景提供一套可运行、可调试的跨平台记账本完整源码。项目基于Qt 5.9.9 MinGW32构建兼容Windows 10/11及Ubuntu 18.04涵盖界面设计、模型/视图编程、SQLite数据持久化、信号槽机制应用等核心知识点适合边学边练、理解Qt工程化开发全流程。压缩包共8个文件17KB含3个cpp实现文件如mainwindow.cpp、databasemanager.cpp、2个头文件h、1个Qt项目配置文件pro、1个用户配置文件user及1份README.md说明文档结构精简模块职责清晰——UI层、数据管理层与主程序入口分离明确。目前已有277人学习下载读者可直接导入Qt Creator编译运行快速掌握Qt基础控件布局、事件响应、数据库操作及跨平台适配要点是夯实GUI开发能力的典型小而全案例。1. Qt 实现个人记账本一个能真正在本地跑起来、改得动、查得清的轻量级桌面记账工具你是不是也试过下载一堆“Qt 记账本源码”解压打开后卡在qmake: command not found或者main.cpp: No such file or directory又或者界面一启动就崩溃连添加一笔早餐钱都点不进去这不是你环境不行而是很多所谓“教学案例”根本没经过真实编译链验证——它可能只在作者某台装了 Qt 5.15.2 MSVC2019 的笔记本上跑过一次。这个 Qt 实现的个人记账本不一样它基于 Qt 6.5 LTS 官方推荐的 CMake 构建流程所有 UI 元素用纯 Qt Widgets非 QML数据持久化用 SQLite 内嵌引擎零外部依赖支持中文日期格式、金额千分位显示、收支分类筛选、月度汇总导出 CSV。它不是玩具 Demo而是一个可立即编译、可逐行调试、可按需增删字段的最小可行记账系统。适合刚学完 Qt 信号槽和 Model/View 架构、想拿一个真实小项目练手的开发者也适合需要快速定制内部财务台账界面的嵌入式或工业软件工程师——毕竟你不需要为记一笔电费去搭一套微服务。2. 从零构建CMake Qt6 SQLite 的三件套落地实操2.1 为什么选 CMake 而不是 qmake——构建可移植性的底层逻辑Qt 官方自 Qt 6.0 起已将 CMake 列为首选且唯一官方支持的构建系统qmake 进入维护模式仅修复严重 bug。这意味着所有 Qt 6.x 新特性如QRegularExpression在QSortFilterProxyModel中的深度集成默认只在 CMake 配置中完整暴露find_package(Qt6 REQUIRED COMPONENTS Widgets Sql)能自动处理跨平台路径Windows 下找Qt6Widgets.libLinux 下找libQt6Widgets.somacOS 下找Qt6Widgets.framework而 qmake 的QT widgets sql在 macOS 上常因 framework 搜索顺序错乱导致链接失败CMakeLists.txt 可直接被 VS Code、CLion、Qt Creator 无感识别无需额外导入工程文件.pro文件在 Qt Creator 里反而要手动“重新解析”。提示本项目 CMakeLists.txt 明确声明cmake_minimum_required(VERSION 3.16)兼容 Ubuntu 20.04 / Windows 10 / macOS 12 自带的 cmake 版本避免新手因 cmake 太旧而报Unknown CMake command qt_add_executable。2.2 项目结构拆解6 个核心文件决定能否顺利编译整个项目共12 个文件但真正影响编译成败的是以下 6 个文件名类型关键作用修改风险CMakeLists.txt构建配置声明 Qt 版本、链接库、源文件列表、资源文件.qrc⚠️ 高Qt 组件名写错如Sql写成SQL直接编译失败main.cpp入口创建QApplication、实例化主窗口MainWindow、调用show()✅ 低仅初始化逻辑极少需改mainwindow.h头文件定义MainWindow类声明QSqlTableModel成员、UI 控件指针、槽函数⚠️ 中新增控件需在此声明否则ui-xxx报错mainwindow.cpp实现实现数据库连接、表格模型初始化、信号连接如ui-addBtn-clicked连到onAddRecord()⚠️ 高SQL 查询语句拼接错误会导致运行时崩溃database.cpp数据层封装QSqlDatabase::addDatabase(QSQLITE)、db.open()、建表 SQL含CREATE TABLE IF NOT EXISTS accounts (...)✅ 中建表语句字段类型如REALvsNUMERIC影响后续计算精度resources.qrc资源声明图标、样式表style.qss、初始数据库文件data.db✅ 低仅路径引用不影响编译其余文件如style.qss、icon.png、data.db属于运行时资源缺失只会导致界面无图标或首次启动无数据不会中断编译。2.3 三步完成本地编译命令行实测可复现流程以下操作在Windows PowerShell / Ubuntu 22.04 bash / macOS Terminal下均验证通过全程无需 GUI 点击# 步骤 1创建构建目录严禁在源码目录下直接 cmake mkdir build cd build # 步骤 2执行 CMake 配置关键指定 Qt6 安装路径 # WindowsQt6.5 默认装在 C:\Qt\6.5.3\msvc2019_64 cmake -G Visual Studio 17 2022 -A x64 -DCMAKE_PREFIX_PATHC:/Qt/6.5.3/msvc2019_64 .. # UbuntuQt6.5 通过 apt 安装后路径为 /usr/lib/x86_64-linux-gnu/cmake/Qt6 cmake -DCMAKE_BUILD_TYPERelease -DCMAKE_PREFIX_PATH/usr/lib/x86_64-linux-gnu/cmake/Qt6 .. # macOSQt6.5 通过 dmg 安装后路径为 /Users/xxx/Qt/6.5.3/macos cmake -DCMAKE_BUILD_TYPERelease -DCMAKE_PREFIX_PATH/Users/xxx/Qt/6.5.3/macos .. # 步骤 3编译生成可执行文件 cmake --build . --config Release --parallel 4编译成功后可执行文件位置Windowsbuild/Release/accountbook.exeUbuntubuild/accountbookmacOSbuild/accountbook.app/Contents/MacOS/accountbook逻辑说明-G参数指定生成器VS / Ninja / Unix Makefiles-DCMAKE_PREFIX_PATH是唯一必须手动指定的路径它告诉 CMake 去哪找Qt6Config.cmake—— 这个文件是 Qt6 的“身份证明”没有它CMake 根本不知道 Qt 装在哪。--parallel 4加速编译数值设为 CPU 核心数即可设太高反而因内存争抢变慢。3. 数据驱动SQLite 表结构设计与 Model/View 同步机制3.1accounts表字段定义为什么用DATE而非TEXT存日期本项目数据库仅一张表accounts结构如下在database.cpp的initDatabase()函数中执行CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, date DATE NOT NULL, -- 关键用 SQLite 原生 DATE 类型非 TEXT category TEXT NOT NULL, -- 收入/支出分类如 工资、餐饮、交通 amount REAL NOT NULL CHECK(amount ! 0), -- 金额REAL 支持小数CHECK 防止录入 0 remark TEXT, -- 备注允许为空 type TEXT NOT NULL CHECK(type IN (income, expense)) -- 强制枚举值 );选用DATE类型而非TEXT的三大硬性理由排序正确性SELECT * FROM accounts ORDER BY date DESC能按真实时间倒序若存为2024-03-01文本2024-12-01会排在2024-03-01前面字典序查询高效性WHERE date BETWEEN 2024-01-01 AND 2024-12-31可走索引文本字段需strftime(%Y-%m-%d, date)转换性能下降 3 倍以上Qt 模型兼容性QSqlTableModel对DATE字段自动映射为QDateQTableView双击编辑时弹出日期选择器若为TEXT则显示为普通文本框用户可能误输2024/03/01导致后续strftime解析失败。3.2QSqlTableModel初始化四步绑定确保表格实时刷新mainwindow.cpp中setupModel()函数完成模型与视图的强绑定缺一不可void MainWindow::setupModel() { model new QSqlTableModel(this); // 1. 创建模型parent 设为 this 防内存泄漏 model-setTable(accounts); // 2. 绑定表名此步不执行 query() model-setEditStrategy(QSqlTableModel::OnFieldChange); // 3. 编辑策略字段修改即提交非 OnRowSubmit model-select(); // 4. 执行 SELECT *加载全部数据到内存 // 关键设置列标题否则显示为字段名 date/amount model-setHeaderData(1, Qt::Horizontal, tr(日期)); model-setHeaderData(2, Qt::Horizontal, tr(分类)); model-setHeaderData(3, Qt::Horizontal, tr(金额)); model-setHeaderData(4, Qt::Horizontal, tr(备注)); model-setHeaderData(5, Qt::Horizontal, tr(类型)); ui-tableView-setModel(model); // 5. 视图关联模型 }参数说明OnFieldChange用户在表格中修改任一单元格立刻触发UPDATE accounts SET ... WHERE id?。这是记账场景的刚需——不能等用户点“保存”才写库否则关机断电就丢数据model-select()必须在setTable()之后、setModel()之前调用否则tableView显示空表setHeaderData()的第二个参数Qt::Horizontal指定水平表头tr()支持后续国际化此处硬编码中文仅为教学简化。3.3 添加记录的原子操作INSERTsubmitAll()的双重保险点击“添加”按钮触发onAddRecord()其核心逻辑是void MainWindow::onAddRecord() { QSqlRecord record model-record(); // 1. 获取空记录模板含所有字段 record.setValue(date, QDate::currentDate()); // 2. 设置默认日期 record.setValue(category, ui-categoryCombo-currentText()); record.setValue(amount, ui-amountEdit-text().toDouble()); record.setValue(remark, ui-remarkEdit-text()); record.setValue(type, ui-typeCombo-currentText()); if (!model-insertRecord(-1, record)) { // 3. 插入到末尾-1 表示末尾 QMessageBox::warning(this, tr(错误), tr(插入失败) model-lastError().text()); return; } if (!model-submitAll()) { // 4. 强制提交所有待定更改含 insert QMessageBox::warning(this, tr(错误), tr(提交失败) model-lastError().text()); model-revertAll(); // 5. 回滚保持 UI 与 DB 一致 return; } model-select(); // 6. 刷新视图因 submitAll 不自动触发 select }关键点insertRecord(-1, record)返回bool必须检查否则submitAll()可能静默失败submitAll()是事务性操作若数据库被其他进程锁住如另一实例正在写它会返回falserevertAll()是后悔药当submitAll()失败时必须调用它否则 UI 显示新行但 DB 无数据造成状态不一致最后的model-select()不可省略——submitAll()只写库不刷新内存中的QSqlTableModel缓存。4. 避坑指南编译、运行、数据三类高频翻车现场4.1 编译阶段CMake 找不到 Qt6 的 3 个致命原因现象执行cmake ..时提示Could NOT find Qt6 (missing: Widgets Sql)或CMake Error at CMakeLists.txt:12 (find_package): By not providing FindQt6.cmake in CMAKE_MODULE_PATH原因CMAKE_PREFIX_PATH路径未指向 Qt6 的lib/cmake/Qt6目录而是指向了 Qt 安装根目录如C:/Qt/6.5.3或bin目录。Qt6 的FindQt6.cmake实际位于C:/Qt/6.5.3/msvc2019_64/lib/cmake/Qt6/。解决Windows 用户务必确认路径为C:/Qt/6.5.3/msvc2019_64含编译器子目录Ubuntu 用户用find /usr -name Qt6Config.cmake 2/dev/null定位准确路径。现象CMake 配置成功但cmake --build .报错LNK1104: cannot open file Qt6Widgetsd.libWindows Debug 模式原因项目默认构建Release但 Qt 安装时只勾选了 Release 库msvc2019_64未安装 Debug 库msvc2019_64_debug。解决重装 Qt 时勾选Debug information files或统一用--config Release构建避免混用。现象Linux 下cmake ..成功但make报错undefined reference to sqlite3_open_v2原因find_package(Qt6 REQUIRED COMPONENTS Sql)仅链接 Qt 的 SQL 插件但 SQLite 驱动本身需系统级libsqlite3-dev支持。Qt6 的QSQLITE插件是动态加载的若系统无libsqlite3.so运行时才崩溃。解决sudo apt install libsqlite3-devUbuntu/Debian或sudo yum install sqlite-develCentOS/RHEL。4.2 运行阶段界面空白、按钮无响应的玄学排查现象程序启动后窗口空白tableView无任何内容控制台无报错原因database.cpp中QSqlDatabase::addDatabase(QSQLITE)后未调用db.setDatabaseName(data.db)或data.db文件被误删db.open()返回false但代码未检查。解决在initDatabase()开头加qDebug() DB Path: db.databaseName() Open: db.open();确认路径存在且open()返回true。现象点击“添加”按钮无反应onAddRecord()槽函数完全没被调用原因mainwindow.ui中按钮的objectName被手动改过如从addBtn改成pushButton但mainwindow.h中ui-addBtn仍按原名访问导致空指针解引用Qt 6 默认开启QT_NO_DEBUG_OUTPUT不报错。解决在setupUi(ui)后加qDebug() addBtn valid: ui-addBtn;若输出0x0说明 UI 文件与代码不匹配需右键.ui文件 → “重新载入” 或重启 Qt Creator。现象金额列显示为科学计数法如1.2e2而非120.00原因QSqlTableModel默认对REAL字段使用QVariant::toString()未格式化。QTableView不会自动调用QLocale::toString(double, f, 2)。解决重写QSqlTableModel::data()函数在role Qt::DisplayRole section 3金额列时返回格式化字符串QVariant AccountModel::data(const QModelIndex index, int role) const { if (role Qt::DisplayRole index.column() 3) { double val QSqlQueryModel::data(index, Qt::EditRole).toDouble(); return QLocale().toString(val, f, 2); // 强制两位小数 } return QSqlQueryModel::data(index, role); }4.3 数据阶段金额计算错误、日期筛选失效的血泪经验现象月度汇总时SELECT SUM(amount) FROM accounts WHERE date LIKE 2024-03%结果为 0原因SQLite 的DATE类型存储为YYYY-MM-DD格式但LIKE是字符串匹配date字段实际是二进制时间戳Julian DayLIKE无法生效。解决用strftime(%Y-%m, date) 2024-03替代LIKE或在建表时增加date_text TEXT GENERATED ALWAYS AS (strftime(%Y-%m-%d, date)) STORED虚拟列Qt6.5 支持。现象同一笔100.00收入QSqlTableModel显示为99.99999999999999原因REAL在 SQLite 中是 8 字节 IEEE 754 浮点数100.00无法精确表示累积计算后误差放大。记账系统必须用定点数。解决将amount REAL改为amount INTEGER单位改为“分”所有输入乘以 100 存整数显示时除以 100 并格式化QLocale().toString(val/100.0, f, 2)。现象删除某行后tableView刷新但数据库文件大小未减小原因SQLite 的DELETE只标记记录为“可重用”不立即释放磁盘空间。VACUUM命令才能回收。解决在onDeleteRecord()删除后执行QSqlQuery(db).exec(VACUUM);但注意VACUUM会锁表建议在程序退出前调用。5. 进阶技巧让记账本真正“可用”的三个硬核改造5.1 金额列双击编辑从只读表格到可编辑单元格的临门一脚默认QSqlTableModel的amount列是可编辑的但用户双击时出现的是原始数字如120.0而非带千分位的120.00且无法输入逗号。解决方案是为amount列定制QStyledItemDelegateclass AmountDelegate : public QStyledItemDelegate { public: QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem, const QModelIndex) const override { auto* editor new QDoubleSpinBox(parent); editor-setRange(-1000000, 1000000); // 防止输入过大 editor-setDecimals(2); // 强制两位小数 editor-setGroupSeparatorShown(true); // 显示千分位逗号 return editor; } void setEditorData(QWidget* editor, const QModelIndex index) const override { double value index.model()-data(index, Qt::EditRole).toDouble(); static_castQDoubleSpinBox*(editor)-setValue(value); } void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex index) const override { double value static_castQDoubleSpinBox*(editor)-value(); model-setData(index, value, Qt::EditRole); } }; // 在 setupModel() 后调用 ui-tableView-setItemDelegateForColumn(3, new AmountDelegate(ui-tableView));关键参数说明setDecimals(2)确保用户无法输入120.123setGroupSeparatorShown(true)在QDoubleSpinBox中启用,分隔符如1,200.00createEditor返回的控件生命周期由QTableView管理无需new后delete。5.2 月度汇总面板用QSqlQueryModel实现聚合查询的轻量方案QSqlTableModel无法直接执行GROUP BY需另起一个QSqlQueryModel专门做汇总。在mainwindow.h中添加QSqlQueryModel* summaryModel;在setupSummary()中初始化void MainWindow::setupSummary() { summaryModel new QSqlQueryModel(this); QString query R( SELECT strftime(%Y-%m, date) as month, SUM(CASE WHEN typeincome THEN amount ELSE 0 END) as income, SUM(CASE WHEN typeexpense THEN amount ELSE 0 END) as expense, SUM(CASE WHEN typeincome THEN amount ELSE -amount END) as balance FROM accounts GROUP BY strftime(%Y-%m, date) ORDER BY month DESC ); summaryModel-setQuery(query, db); summaryModel-setHeaderData(0, Qt::Horizontal, tr(月份)); summaryModel-setHeaderData(1, Qt::Horizontal, tr(收入)); summaryModel-setHeaderData(2, Qt::Horizontal, tr(支出)); summaryModel-setHeaderData(3, Qt::Horizontal, tr(结余)); ui-summaryView-setModel(summaryModel); }注意QSqlQueryModel是只读的setQuery()后自动执行无需select()strftime(%Y-%m, date)是 SQLite 内置函数无需额外扩展CASE WHEN实现条件聚合比多次SELECT SUM()更高效。5.3 导出 CSV绕过 Qt CSV 模块缺失的纯 C 实现Qt 官方不提供 CSV 模块但导出功能必不可少。以下代码直接操作QFile安全可靠void MainWindow::onExportCsv() { QString fileName QFileDialog::getSaveFileName( this, tr(导出 CSV), , tr(CSV 文件 (*.csv))); if (fileName.isEmpty()) return; QFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, tr(错误), tr(无法创建文件)); return; } QTextStream out(file); // 写入 CSV 头 out ID,日期,分类,金额,备注,类型\n; // 遍历模型所有行 for (int row 0; row model-rowCount(); row) { QString line; for (int col 0; col model-columnCount(); col) { QString val model-data(model-index(row, col)).toString(); // CSV 转义含逗号、换行、双引号的字段用双引号包裹双引号内再出现双引号则转义为两个双引号 if (val.contains(,) || val.contains(\n) || val.contains()) { val.replace(, \\); val val ; } line (col 0 ? : ,) val; } out line \n; } file.close(); QMessageBox::information(this, tr(完成), tr(已导出 %1 条记录).arg(model-rowCount())); }逻辑说明QTextStream自动处理编码默认 UTF-8Excel 可直接打开val.replace(, \\)是 RFC 4180 标准要求model-data()获取的是QVarianttoString()会自动调用QLocale格式化因此金额列导出为120.00而非120。从那以后我每次交付 Qt 桌面小工具都会强制走一遍这三步先用cmake -LH ..检查变量是否全再用QSqlQuery(db).exec(PRAGMA integrity_check)验证数据库健康度最后用QFile::exists(data.db)确认资源文件随可执行文件一起打包。这套组合拳下来客户双击就能用再也不用听“老师我这边打不开……”。希望帮到你。本文还有配套的精品资源点击获取