Qt Widgets记账本开发实战:从SQLite到自定义模型的全流程解析
简介一份基于 Qt5.9.9 与 MinGW32 编译环境的个人记账本教学案例面向 Qt 初学者、计算机专业学生以及需要练习跨平台桌面应用开发的读者围绕「记账本」这一典型场景完整演示 Qt 常用控件、信号与槽机制、数据持久化以及增删改查逻辑的工程实现。压缩包共 8 个文件以 C 源文件和头文件为主配合 Qt 工程配置文件与说明文档整体仅 17KB目录结构轻量清晰便于逐模块阅读和二次练习。案例覆盖用户界面设计、本地数据存储、记账项添加/删除/查询/统计、跨平台适配等关键环节并兼顾 Windows 10/11 与 Ubuntu 18.04 的兼容处理可直接用于课程设计或 Qt 教学参考已有 275 人学习浏览。通过研读主窗口与数据库管理模块的实现读者能够掌握模型/视图、样式表、布局组织等实用技巧在完成个人记账功能的同时建立对完整 Qt 项目结构、工程配置与跨平台开发流程的整体认知。 很多学了Qt基础的朋友都会遇到同一个瓶颈信号与槽、布局、控件这些知识点单个拎出来都看得懂但真让自己独立做一个“有完整功能”的程序就不知道从哪里下手。如果你正处于这个阶段那我特别建议你把“个人记账本”当成练手项目。它不是一个花哨的应用但麻雀虽小五脏俱全SQLite存储、自定义表格模型、增删改查、统计图表、界面布局全都能串起来做完之后你对Qt的掌控感会完全不一样。这篇文章我会把我实现 Qt 个人记账本教学案例的整套思路、表结构设计、关键代码片段和踩过的坑完整记录下来内容按照实际开发顺序展开从工程规划一路写到打包发布。项目本身我是用 Qt Widgets C 做的数据库选的 SQLite表格用的 QTableView 自定义 QAbstractTableModel统计图用 QCustomPlot。每个模块我都会解释“为什么这么选”让大家不是只抄代码而是真的理解背后的设计逻辑。1. 项目规划与界面框架搭建1.1 为什么选 Qt Widgets 来做桌面记账本先解决一个最容易被纠结的问题用 Qt Widgets 还是 QML。记账本这种工具型软件界面以表格、表单、按钮为主数据密集、交互偏传统Qt Widgets 是天然合适的选择。它成熟稳定网上资料多出错时信息也好查。当然 QML 做动画效果确实漂亮但教学案例的重点是让学生把 C 的数据结构和业务逻辑理清楚Widgets 的模型视图架构本身就很有教学价值。而且 Qt Widgets 的学习成果是可以直接迁移到工作中的。很多工业软件、内部工具、医疗客户端都还在用 Widgets 维护和开发把这套东西吃透比只会在 QML 里摆控件更吃香。我自己在实际项目中Widgets 的占比仍然很高所以教学案例我坚决用 Widgets。1.2 主窗口布局规划这个记账本界面我不打算做得很复杂主窗口按照“左右结构 上下分区”的思路规划左侧是筛选区放一个“收支类型”下拉框和“分类”下拉框用来快速过滤记录右侧是记录表格用来展示账目明细底部是一个汇总栏实时显示总收入、总支出和结余顶部用 QToolBar 放置“新增”“编辑”“删除”“统计”这几个核心操作按钮。整体用 QSplitter 把左侧筛选区和右侧表格区分开用户可以拖动分隔条调整比例。主窗口中央部件我们用一个 QWidget 做容器最外层用 QVBoxLayout 把筛选区、表格、汇总栏纵向排布。布局时注意给表格设置合理的拉伸因子保证窗口拉大时表格能撑满空间。主窗口的代码骨架大致如下MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle(QStringLiteral(个人记账本)); resize(1000, 640); QWidget *central new QWidget(this); QVBoxLayout *mainLayout new QVBoxLayout(central); // 顶部按钮栏 QToolBar *toolBar new QToolBar(this); addToolBar(Qt::TopToolBarArea, toolBar); // 新增、编辑、删除、统计按钮 QAction *actAdd new QAction(QStringLiteral(新增), this); QAction *actEdit new QAction(QStringLiteral(编辑), this); QAction *actDelete new QAction(QStringLiteral(删除), this); QAction *actStat new QAction(QStringLiteral(统计), this); toolBar-addAction(actAdd); toolBar-addAction(actEdit); toolBar-addAction(actDelete); toolBar-addSeparator(); toolBar-addAction(actStat); setCentralWidget(central); }工具栏用 QAction 是推荐做法因为 QAction 可以同时绑定菜单、按钮和快捷键逻辑只需写一次。后续新增、编辑、删除的槽函数都连接到这几个 action 上就好了。1.3 工程文件组织教学案例最忌讳所有代码堆在 mainwindow.cpp 里文件又长又难维护。我建议按职责拆分成以下几类文件accountbook/ ├── accountbook.pro ├── main.cpp ├── mainwindow.h / mainwindow.cpp // 主窗口、菜单、操作入口 ├── databasehelper.h / databasehelper.cpp // 数据库初始化与通用查询 ├── recordmodel.h / recordmodel.cpp // 自定义表格模型 ├── addrecorddialog.h / addrecorddialog.cpp // 新增/编辑对话框 ├── statisticdialog.h / statisticdialog.cpp // 统计图对话框.pro 文件里面要注意加上 sql 模块以及 qcustomplot 的包含路径。如果用的是 Qt Charts 做图表还需要加 charts 模块QT core gui sql widgets greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 SOURCES \ main.cpp \ mainwindow.cpp \ databasehelper.cpp \ recordmodel.cpp \ addrecorddialog.cpp \ statisticdialog.cpp HEADERS \ mainwindow.h \ databasehelper.h \ recordmodel.h \ addrecorddialog.h \ statisticdialog.h # 如果使用 QCustomPlot请把 qcustomplot.h 和 qcustomplot.cpp 加入工程 SOURCES qcustomplot/qcustomplot.cpp HEADERS qcustomplot/qcustomplot.h工程结构清晰之后后续每一个新功能都只需要在对应模块里改别人接手代码也容易看懂。这一点在项目教学里比功能本身还重要因为“可维护性”才是工程能力的分水岭。2. 数据层设计用 SQLite 把账目存下来2.1 数据库表结构设计记账本最核心的就是账目表。我用一张 records 表加一张 category 表来存储数据。优先保证业务简单但又能体现关系型设计的基本思想。CREATE TABLE category ( id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER NOT NULL, -- 0 支出1 收入 name TEXT NOT NULL ); CREATE TABLE record ( id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER NOT NULL, -- 0 支出1 收入 category_id INTEGER NOT NULL, amount_cents INTEGER NOT NULL, -- 金额以“分”为单位 record_date TEXT NOT NULL, -- 日期格式 yyyy-MM-dd note TEXT );这里有一个很关键的设计细节金额字段用整数amount_cents而不是浮点数。单据金额如果直接存 double在做累加、比较时很容易出现 0.1 0.2 ! 0.3 这类浮点精度问题。存成分展示的时候再除以 100这样既精确又不需要引入额外库。另外日期用 TEXT 类型格式固定为yyyy-MM-dd这样既能直接字符串排序也方便在 SQL 里按月份聚合统计。category 表把收入和支出的分类分开例如收入有工资、红包、理财支出有餐饮、交通、购物。下拉框加载时按 type 过滤不会混在一起。2.2 数据库初始化与路径处理数据库文件路径不能写死在代码里不同系统的可写目录不一样。推荐用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)拿当前用户的 AppData 目录再创建我们自己的子目录QString DatabaseHelper::getDbPath() { QString dir QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir().mkpath(dir); // 目录可能不存在先创建 return dir QStringLiteral(/accountbook.db); }初始化数据库时要注意QSqlDatabase::addDatabase的 connectionName 很关键。如果你在多个类里反复调用addDatabase()且不指定连接名第二次就会报 “duplicate connection name” 错误。更安全的方式是使用全局唯一的连接名并在打开之后把这个连接传出去QSqlDatabase DatabaseHelper::getConnection() { QString connName QStringLiteral(accountbook_conn); if (QSqlDatabase::contains(connName)) { return QSqlDatabase::database(connName); } QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE, connName); db.setDatabaseName(getDbPath()); db.open(); return db; }建表语句统一放在初始化函数里用QSqlQuery::exec执行即可。如果表已经存在SQLite 不会重复创建所以我们每次启动都跑一遍建表语句问题不大。教学时可以把建表 SQL 放在一个单独的init.sql文件里从资源文件加载逻辑上更干净。2.3 数据访问接口封装我不建议在界面代码里直接拼 SQL那样后期维护是噩梦。更稳妥的做法是给数据库操作封装一个 DAO 层界面层只关心“插入一条记录”“查询记录列表”这种业务动作。教学案例里我们的 DatabaseHelper 只保留几个核心接口QVectorRecord queryRecords(int typeFilter, int categoryId, const QString dateStart, const QString dateEnd)bool insertRecord(const Record rec)bool updateRecord(int id, const Record rec)bool deleteRecord(int id)QVectorQPairQString, qint64 querySumByCategory(int type, const QString month)这里把 Record 定义成一个简单的结构体包含 id、type、categoryId、amountCents、date、note。界面层和模型层都操作这个结构体数据流转非常直观。这些接口内部统一用QSqlQuery的preparebindValue做参数绑定绝对不要用QString::arg直接把用户输入拼进 SQL 字符串。参数绑定一方面能避免 SQL 注入另一方面也自动处理了特殊字符转义这属于“少踩一个坑”的典型操作。3. 记账核心功能实现3.1 用 QTableView 自定义 Model 展示账目列表刚开始写 Qt 的人特别喜欢直接用 QTableWidget因为 SetItem 就能显示数据代码短。但一旦数据量大、需要频繁增删改或要与数据库联动QTableWidget 就会变得非常笨重。教学案例里我特意选了 QTableView 自定义 QAbstractTableModel这是模型视图架构里最重要的套路。自定义 Model 的核心是继承 QAbstractTableModel 并重写这几个虚函数class RecordModel : public QAbstractTableModel { Q_OBJECT public: int rowCount(const QModelIndex parent QModelIndex()) const override; int columnCount(const QModelIndex parent QModelIndex()) const override; QVariant data(const QModelIndex index, int role Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; bool removeRows(int row, int count, const QModelIndex parent QModelIndex()) override; void setRecords(const QVectorRecord records); Record recordAt(int row) const; private: QVectorRecord m_records; };data()函数中根据role Qt::DisplayRole返回对应列的内容注意“金额”列要把 amountCents 换算成元并带两位小数日期列返回字符串类型列返回“收入/支出”。常见的难点是表头的显示名称例如“日期”“分类”“金额”“备注”不要用英文这在headerData()里返回中文即可。数据刷新时有一个初学者必踩的坑直接清空m_records并调用beginResetModel()/endResetModel()。如果忘记调 reset界面不会更新甚至可能越界崩溃。正确写法如下void RecordModel::setRecords(const QVectorRecord records) { beginResetModel(); m_records records; endResetModel(); }使用自定义 Model 之后后续筛选、排序、联动都会顺手很多。还有一个好处删除记录时我们可以用selectionModel()-selectedRows()拿到选中行再通过recordAt(row)得到对应记录 id从数据库删除。3.2 新增与编辑记录的对话框实现新增和编辑记录的界面我复用一个 QDialog。顶部放收入/支出类型选择下面放分类下拉框、金额输入、日期选择、备注输入。复用逻辑很简单对话框构造时传入一个可选的 Record 指针有值就回填数据、标题显示“编辑记录”没值就按新增处理。这里有个交互细节值得展开收入和支出对应的分类不同。当用户在对话框里切换收支类型时分类下拉框的内容要跟着变。实现时我用了 QStandardItemModel 作为 QComboBox 的数据源切换时重新加载分类数据并清除当前选择。金额输入框我用 QDoubleSpinBox设置范围为 0.00 到 10000000.00小数位数为 2。校验时只要用户点保存就判断当前金额是否大于 0同时备注长度限制在 100 个字符内。对话框的按钮用了 QDialogButtonBox保存和取消的槽函数分别调accept()和reject()。保存时把界面控件的数据转换成一个 Record 对象再交给 DatabaseHelper 执行插入或更新。成功之后主窗口的通过信号通知模型刷新表格。整个流程就是“界面 - 业务对象 - 数据库 - 模型刷新”这样一条链思路清楚了代码就不会乱。3.3 删除、汇总和筛选删除操作要处理好“多选”场景。我支持按住 Ctrl 多选表格行然后一次删除。删除前记得弹确认框避免误操作QModelIndexList selected ui-tableView-selectionModel()-selectedRows(); if (selected.isEmpty()) { QMessageBox::information(this, QStringLiteral(提示), QStringLiteral(请先选择要删除的记录)); return; }批量删除时最好开事务SQLite 默认每条语句一个事务逐条删除多条记录会很慢。这里我用QSqlDatabase::transaction()把多条 delete 放在同一个事务里速度更快而且任何一条失败都可以回滚不会出现删了一半的脏数据。汇总栏的“总收入、总支出、结余”我选择在模型刷新后用 SQL 的 SUM 聚合函数重新统计而不是在界面上做减法。这样哪怕数据库被其他地方改了界面也不会出错。按条件筛选时我把类型、分类、日期范围三个条件组装成 SQL 的 WHERE 子句再走 prepare bindValue。这里有一个技巧日期范围的两个 QDateEdit 需要允许“未选中”状态才能表示不限日期否则就只能限定精确区间。实际项目中我加了两个复选框勾选后才启用日期过滤条件交互上更自然。4. 统计图表与体验细节优化4.1 用 QCustomPlot 画支出分类占比饼图图表部分是这个记账本最出彩的地方。统计对话框里放两个图“本月支出分类占比”饼图和“近半年收支趋势”折线图。饼图我用的 QCustomPlot这个第三方库直接把 qcustomplot.h 和 qcustomplot.cpp 加进工程就能用比 Qt Charts 更轻而且文档齐全。需要注意新版 QCustomPlot2.1.0 之后才内置 QCPPieChart老版本没有需要自己用 QCPBars 或者外部类来实现所以大家尽量用新版或者干脆选择 Qt Charts 的 QPieSeries效果一样好。我在项目里为了减少第三方依赖最终选了 Qt Charts 作为备选方案但如果你希望保留 QCustomPlot 的定制度那 2.x 版本完全够用。画饼图的核心是准备分类汇总数据。我先用 query 按分类分组统计本月的支出金额再把结果填充进饼图QPieSeries *series new QPieSeries(); for (auto item : sumList) { QPieSlice *slice series-append(item.categoryName, item.totalCents); slice-setLabel(QStringLiteral(%1 %2%).arg(item.categoryName) .arg(item.totalCents * 100 / totalCents)); }饼图的块颜色可以手动指定用一组预设的 QColor 循环分配避免相邻块颜色太接近。显示百分比时注意除零保护——如果本月没有支出直接提示“本月暂无支出数据”而不是让绘图代码去处理空数据。4.2 近半年收支趋势折线图折线图的横轴是月份纵轴是金额。为了让坐标轴显示“2025-03”这样的文字标签我用 QCPAxisTickerDateTime 设置时间格式QSharedPointerQCPAxisTickerDateTime ticker(new QCPAxisTickerDateTime); ticker-setDateTimeFormat(yyyy-MM); xAxis-setTicker(ticker);数据按自然月分组聚合从当前月份往回推 6 个月每个月分别算收入、支出 2 个点生成两条 QCPGraph。画线前把上个月的日期时间戳用QDateTime::currentSecsSinceEpoch()和月份计算函数转成秒数注意 QCustomPlot 的时间单位是 Unix 秒别把 QDate 直接塞进去。图表绘制完成后给折线加上范围拖拽和缩放功能这样用户可以用鼠标框选查看某一段时间的明细趋势。对教学项目来说这个交互细节会明显提升“作品感”。4.3 空状态、快捷键和其他体验细节一个软件好不好用细节决定体验。记账本最少要处理下面几个细节表格为空时不要显示一张白板配上一条“暂无记账记录点击上方新增开始记账”的提示标签。快捷键支持CtrlN 新增、CtrlE 编辑、Delete 删除、CtrlS 打开统计窗口。菜单栏里把快捷键文字标出来用户一看就会。状态栏实时刷新汇总金额操作完任何一笔账都能立刻看到结余变化。表格列宽需要根据内容自适应金额、日期列固定宽度备注列拉伸填满剩余空间。窗口关闭时如果存在未保存的编辑对话框要给出提示避免用户误关。状态栏汇总的计算不一定非要实时查库。我是把 3.3 节提到的汇总逻辑单独抽了一个函数在模型刷新后调用一次填充 QLabel 文本。Realtime 刷新不仅开销小还让界面交互更跟手。5. 常见问题与排查技巧实录5.1 平台插件启动报错运行程序时如果弹窗提示类似 “windows no qt platform plugin could be initialized” 的错误多半是插件拷贝不全。新手最容易犯的错误是直接从 Qt 安装目录把 exe 发给别人对方机器上没有 Qt 环境当然跑不起来。正确做法是用官方打包工具windeployqt accountbook.exe它会自动把 exe 依赖的 Qt 运行库和插件复制到 exe 当前目录。但注意 windeployqt 版本必须和编译器的位数一致。你用 MSVC2019 64 位编译就要用对应的 windeployqt千万别拿 MinGW 版去拷贝 MSVC 的程序那样会缺一堆运行库。如果打包之后还报同样的错误检查 exe 同级目录下有没有 platforms 文件夹里面有没有 qwindows.dll。没有的话手动把 Qt 安装目录plugins/platforms整个文件夹复制到 exe 目录。另外某些精简版 Qt 安装时没有勾选这一层的插件重装时把所有模块都勾上可以避免后续麻烦。5.2 表格数据刷新后显示错乱一个很典型的症状点击筛选后表格行数不对或者某列显示的内容和数据库里不一致。这通常是因为模型刷新时没有正确调用 reset。QAbstractTableModel 当中插入或删除行数据时必须先调用beginInsertRows/endInsertRows插入或beginResetModel/endResetModel全量刷新。我自己的经验是如果数据是整批重新查询并替换就用 reset 没问题如果只是局部修改则尽量改行数较少的接口。另一个常见问题是data()函数里对非法索引没有做保护。在模型里拿到一个空 index 或者行号越界会直接导致崩溃排查半天。统一在 data() 开头加一个 if (!index.isValid()) return QVariant 的防御判断能省下很多调试时间。5.3 日期和时间相关的坑记账本和日期打交道非常多日期也是容易出问题的地方。SQL 里的日期字符串要统一格式我在插入时强制用QDate::toString(yyyy-MM-dd)绝不在界面里直接显示“2025年3月15日”这种格式后存库否则排序和比较都会乱套。统计“本月”支出时SQL 语句里用strftime(%Y-%m, record_date) strftime(%Y-%m, now)来过滤能自动适配不同月份的边界不用在程序里算上个月是哪天。5.4 中文乱码与编码问题Qt 5 的时代中文字符串直接写在源码里编译器编码和源码编码不一致就会乱码。我给大家的建议是在项目中统一使用 UTF-8 编码MSVC 环境下在 .pro 文件或编译选项里加一句解决msvc { QMAKE_CXXFLAGS /utf-8 }如果你在用 Qt 6源码默认就是 UTF-8基本不用操心乱码问题。如果界面上某处中文还是显示成问号优先检查那行的字符串是不是误用了本地编码的字节流。数据库里读取的中文正常显示界面里手工输入的中文却乱码那基本就是上面这个编译选项的问题。写在最后的一个建议这个记账本项目我前前后后带过好几轮学生每次做完大家的最大收获并不是“学会了 Qt”而是真正理解了一个软件是怎么从空白界面一步步长成完整程序的。如果你照着本文的模块划分自己动手写我建议不要一次性全部照抄先搭好数据库和表格再逐步加编辑、删除、统计每加一个功能就编译运行一次这样每一步的成就感都能清晰感知。整个项目过程中最值得花时间深入研究的是自定义 Model 和信号槽之间的关系这不光是为了写记账本以后做任何数据密集型软件都会用上。如果你写完记账本之后还觉得不过瘾可以继续给它加导入导出 CSV、月预算提醒、多账本切换这些扩展功能。个人记账本这类项目的天花板很高而起点恰好又很友好非常适合作为第一个完整 Qt 项目认真打磨。本文还有配套的精品资源点击获取