Qt从入门到完整交付:个人记账本项目全流程拆解
2026/9/20 11:12:21 网站建设 项目流程

简介:一份面向 Qt 初学者的个人记账本教学案例,使用 Qt5.9.9 和 MinGW32 构建,可在 Windows 10/11 与 Ubuntu 18.04 等系统下直接运行。案例围绕记账本的新增、删除、查询、统计等核心功能展开,完整演示了 Qt 界面布局、信号与槽、事件处理、模型/视图、数据持久化及样式表等关键知识点,并对本地文件存储与数据库存储的不同选择进行了说明,适合课堂演示、课后练习,也适合有 C++ 基础的学习者作为课程设计参考。资源包体积仅 17KB,共 8 个文件,包括 3 个 cpp 源文件、2 个头文件、Qt 工程文件和 README 说明文档;代码量不大但结构清晰、模块划分明确,便于逐行阅读和二次扩展,能直观看到数据管理模块与主窗口的协作方式。目前已有 275 人学习下载。通过该项目可重点理解记账数据如何存储、检索与展示,并学习跨平台 Qt 工程在 Windows/Linux 下的目录组织与编译配置,是一份轻量实用的 GUI 开发入门资料。 很多学 Qt 的朋友做完几个控件小 Demo 之后就卡住了——Hello World 会写,信号槽会用,但真要独立交付一个完整的软件,总觉得哪哪都不对。我这些年带过不少 Qt 新手,也做过企业内部的技术培训,发现最适合拿来当"从入门到完整交付"过渡项目的,就是个人记账本。界面、数据库、图表、事件流转、打包发布全都能在一套代码里串起来,而且业务逻辑足够简单,不需要你边写代码边补业务知识。这篇文章就把这个教学案例的完整拆解写出来,从环境选型到数据层设计,再到界面交互、图表可视化和发布避坑,一套链路走完。

1. 为什么记账本能把 Qt 从未入门带到能交付

1.1 一个项目覆盖 Qt 八成核心知识点

计算器项目练的是控件摆放,画图板项目练的是绘图事件,播放器项目又一下跳到音视频解码,难度曲线直接起飞。记账本的难度曲线非常平滑,它天然包含了一套桌面软件最常见的完整结构:界面录入、数据存储、列表展示、统计汇总。你在学习过程中真正要面对的,恰好就是 Qt 日常开发里最常用的那部分能力,而不是某个偏门模块。

这个项目里你至少会遇到这些核心知识点:窗口和布局管理(QMainWindow、QWidget、QVBoxLayout)、常用控件(QTableWidget、QLineEdit、QComboBox、QDateEdit、QLabel)、信号槽的完整链路、SQLite 数据库操作(QSqlDatabase、QSqlQuery)、列表数据刷新、用 QSS 做界面美化,以及最后用 windeployqt 做发布打包。如果加上图表统计,还会用到 QCustomPlot 或者 Qt Charts 这一层可视化方案。

一个项目把界面、数据、事件、可视化、部署全走一遍,这是"能交付"和"会写控件"之间最重要的分水岭。很多人在面试时说做过 Qt 项目,结果一问数据是怎么存的、程序在别人电脑上能不能跑起来,就答不上来,就是因为之前的练习项目根本没走到这一步。

1.2 难度分级设计,教学节奏更好控

记账本还特别适合分阶段教学,不同基础的学员都能有自己的目标。基础版只要做到账目新增、删除、修改、列表展示和简单的金额统计,纯粹练控件和数据库操作。标准版加上分类筛选、日期范围查询,再用 QCustomPlot 画出支出趋势和分类占比图,这一档已经是一个可以自用的记账工具了。进阶版可以加预算提醒、CSV 导入导出、多账本切换,甚至用 Model/View 架构重写列表部分,往商业项目靠拢。

我带培训的时候一般按四次课来排:第一次搭工程和主界面,第二次接 SQLite 做增删改查,第三次做图表统计和界面美化,第四次讲打包发布。每次课都有肉眼可见的产出,学员的反馈比前面讲控件语法时要好得多。

2. 环境选型:5.15.2 + Creator + MSVC,稳字当头

2.1 版本和编译器的选择逻辑

Qt 的版本选择是个老生常谈的问题。教学场景下我强烈建议用 Qt 5.15.2,它是 5.x 系列里相当稳定的一个 LTS 版本,网上的资料、博客、论坛问答数量最多。遇到问题你基本搜得到答案,这一点在教学场景里比"最新版本"重要得多。Qt 6 当然值得学,但作为从入门到交付的第一门课,稳定和资料丰富才是优先考虑的。

编译器方面,Windows 上 MainWindow 会遇到 MSVC 和 MinGW 两个选择。我推荐 MSVC2019 64-bit。原因很简单:后续如果项目要接入第三方库、做 Windows 原生功能,或者需要调试更完整的调用栈,MSVC 兼容性明显更好,而且 Qt 官方对 MSVC 版本的预编译包更新也最积极。MinGW 的好处是安装体积小、不需要额外装 Visual Studio,但发布时常常要多带 libgcc 和 libstdc++ 运行库,新手在这上面踩的坑反而多。

不少学员装完 Qt 之后发现 Qt Creator 里对应的 Kit 显示红色感叹号,编译按钮是灰的,多半就是只装了 Qt 的 MSVC 组件,但机器上并没有安装 Visual Studio 的 Build Tools。MSVC 组件编译时需要调用微软的 C++ 编译器和 Windows SDK,这些不是 Qt 安装包自带的。解决方式就是装 Visual Studio 或者单独装 VS Build Tools,然后重启 Qt Creator。

2.2 下载安装和组件勾选建议

Qt 官方安装包下载速度经常让人怀疑人生,国内直接用镜像源就好,中科大、清华的 Qt 镜像都挺稳定。下载安装器之后,在线安装时选择组件,教学场景下组件不用贪多,勾这些就够:

组件是否建议说明
Qt 5.15.2 > MSVC 2019 64-bit必选核心模块,包含 Qt Widgets、SQL、Core、GUI
Qt Debug Symbols可选调试时定位更精准,就是占磁盘空间
Qt Sources可选想深入看源码再装
Qt Creator必选官方 IDE
Qt Charts可选如果用 Qt Charts 做图表,需要勾选
MinGW 相关组件可不选使用 MSVC 方案时用不到

2.3 Creator 还是 VS Code

关于 Qt Creator 和 VS Code 的争论一直都有。VS Code 配 Qt 插件之后确实能写代码,但教学场景下我更偏 Qt Creator,因为它的 Designer 可视化编辑、Qt 帮助文档集成、断点调试、qrc 资源管理都是开箱即用的。学员不用折腾 launch.json、tasks.json 这些环境配置,把精力集中在项目本身的逻辑上。性能实测方面,Creator 在大项目上启动确实比 VS Code 稍慢,但记账本这种体量的工程完全感觉不到差别。

你如果已经习惯 VS Code,当然也可以继续用,装好 Qt VS Code 扩展之后配合 CMake 工程,体验也不差。但对第一次接触 Qt 的学员来说,Creator 的容错率更高,省下来的配置时间干点正事不好吗。

3. 先把数据层立住:SQLite 表结构与 DbManager 封装

3.1 为什么一上来就要谈数据层

很多新手写代码的习惯是先画界面,界面觉得差不多了再回头想数据怎么存。这个顺序在记账本这种小项目里勉强能跑,但你会发现界面代码和 SQL 语句搅在一起,函数越写越长,改一个查询条件要翻半天。正确做法是先定数据层,再把界面接到数据层上。

数据层在主程序里一般体现为一个 DbManager 类,负责数据库连接、建表、增删改查的 SQL 封装。界面层只需要调用addBilldeleteBillloadBills这些方法,不需要知道 SQLite 底层怎么执行。这样每次界面调整时,只要接口不变,数据层完全不用动。

3.2 账目表结构设计

SQLite 里建一张 bills 表就够了,字段这么设计:

CREATE TABLE IF NOT EXISTS bills ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, category TEXT NOT NULL, amount REAL NOT NULL, date TEXT NOT NULL, note TEXT ); CREATE INDEX IF NOT EXISTS idx_bills_date ON bills(date);

type 字段表示"支出"还是"收入",category 存分类名,比如餐饮、交通、购物、工资这些。amount 用 REAL 存浮点金额。date 我用 TEXT 存,而不像有些人想的用 DATE 类型。SQLite 本身是动态类型,用 ISO 格式的字符串yyyy-MM-dd存日期,天然支持字典序排列,范围查询BETWEEN '2024-01-01' AND '2024-01-31'完全没问题,而且显示的时候直接就是字符串,不用再做格式转换。

按 date 建索引是因为后续最常见的查询是"某时间范围内有多少笔支出",这个索引能帮上忙。记账本数据量小,这个索引效果未必明显,但是让学员养成建索引的意识很有价值。

3.3 DbManager 的核心实现

DbManager 的初始化和建表逻辑长这样:

class DbManager { public: bool init(const QString& dbPath); bool addBill(const QString& type, const QString& category, double amount, const QString& date, const QString& note); bool deleteBill(int id); bool updateBill(int id, const QString& category, double amount, const QString& note); QVector<BillInfo> loadBills(const QString& dateBegin, const QString& dateEnd, const QString& type, const QString& category); private: QSqlDatabase m_db; }; bool DbManager::init(const QString& dbPath) { m_db = QSqlDatabase::addDatabase("QSQLITE"); m_db.setDatabaseName(dbPath); if (!m_db.open()) { qWarning() << "Open database failed:" << m_db.lastError().text(); return false; } QSqlQuery query; return query.exec("CREATE TABLE IF NOT EXISTS bills (...)"); }

新增一条账目记录时,务必用 prepared statement 而不是拼字符串:

bool DbManager::addBill(const QString& type, const QString& category, double amount, const QString& date, const QString& note) { QSqlQuery query; query.prepare("INSERT INTO bills(type, category, amount, date, note) " "VALUES (:type, :category, :amount, :date, :note)"); query.bindValue(":type", type); query.bindValue(":category", category); query.bindValue(":amount", amount); query.bindValue(":date", date); query.bindValue(":note", note); return query.exec(); }

这样除了防止 SQL 注入外,还顺带免去了字符串里引号、特殊字符的转义噩梦。你想象一下如果备注里写了I'm fine,拼 SQL 字符串很可能会拼出语法错误,而 prepared statement 完全没这个问题。

数据库文件放哪也值得说一下。直接放程序当前工作目录最简单,但发布后往往没有写权限,或者路径里有中文引发各种奇怪问题。更好的位置用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation),这是系统为应用准备的用户数据目录,各平台都能正确解析。教学阶段可以两套逻辑都留口子:路径不存在就QDir().mkpath()创建。

4. 主界面搭建与增删改查的完整信号槽链路

4.1 主窗口控件组成

主窗口用 QMainWindow,中心组件放一个 QWidget 作为容器。整个界面从上到下分三个区域:顶部是筛选条件区,一行排开起始日期、结束日期、类型下拉框、分类下拉框、查询按钮。中间是账目列表区,用 QTableWidget 展示,列设置为日期、类型、分类、金额、备注,最后一列放操作按钮或者用行选中后点击删除。底部是统计区,三个 QLabel 分别显示筛选范围内的总支出、总收入、结余。

筛选条件区用 QHBoxLayout 横向排列,主布局用 QVBoxLayout,左右留白不要太夸张,整体看起来像正经桌面软件的样子就行。表格初始化的时候要设置列宽:日期列 110 像素、类型和分类列 80 像素、金额列 100 像素、备注列拉伸占满剩余空间。horizontalHeader()->setStretchLastSection(true)可以让最后一列自动分配剩余宽度。

4.2 新增一条账目的完整链路

当用户点击"新增账目"按钮时,完整的调用链路是这样的:按钮的 clicked 信号连接到 MainWindow 的槽函数,槽函数弹出录入对话框,对话框里选择类型、分类,填写金额、日期、备注,点击确定后通过一个自定义信号把数据传回主窗口,主窗口拿到数据先做校验,再调用 DbManager 添加记录,最后刷新表格和统计。

校验这块容易偷懒,但恰恰是教学重点。金额要校验是不是有效的正数,日期要校验是不是合理的日期范围,备注可以允许为空但长度做个限制。这些校验放在对话框的确定槽里,不合法就弹 QMessageBox::warning,不关闭对话框。很多初学者不写校验,结果数据库里存进一堆脏数据,后面图表统计的时候就会发现所有分类金额加起来对不上。

录入界面本身可以用 QDialog,也可以做得更轻量,在主界面下方内嵌一个录入表单区。内嵌形式少一次弹窗交互,操作更快,界面也更好看。我常用后者,学员普遍反馈这种"即填即存即刷新"的体验更有成就感。

4.3 表格刷新、删除和修改的联动处理

加载列表数据时,先执行查询,然后按结果数量设置表格行数。有个很常见的小坑:每次刷新前要tableWidget->setRowCount(0)清空旧数据,否则上一轮查询结果行会残留,造成显示错乱。逐行填充数据时,金额列可以用QTableWidgetItem设置右对齐,并格式化抹平浮点误差:

ui->tableWidget->setItem(row, 3, new QTableWidgetItem(QString::number(amount, 'f', 2)));

删除操作的正确姿势是:先currentRow()拿到当前选中行的行号,再从这一行数据里找出 id。所以我在加载数据时,把 id 存在 item 的Qt::UserRole数据里,而不是单独弄一列显示给用户看。

修改功能我一般不在表格里直接编辑,而是选中某一行之后点"修改"按钮,把这一行的数据回填到录入表单里。这里最容易出错的点是类型字段是下拉框,回填时要通过findText找到对应索引再setCurrentIndex,而不是直接把字符串塞进去。更新完数据后用同样的方式刷新表格,界面就能和数据库保持一致。

4.4 界面美化:QSS 让教学案例更像产品

功能全部跑通之后加 QSS,是提升项目完成度的关键一步。不需要设计得多花哨,干净清爽就行。一个浅色背景、圆角输入框、表格隔行换色的方案,就比默认风格的灰色方块好看太多。

#mainWidget { background-color: #f7f8fa; } QTableWidget { background-color: white; border: none; border-radius: 8px; alternate-background-color: #fafbfc; gridline-color: #ececec; selection-background-color: #e8f0fe; selection-color: #202124; } QPushButton#addBtn { background-color: #3b7cff; color: white; border: none; border-radius: 6px; padding: 8px 24px; } QPushButton#addBtn:hover { background-color: #2b6ce3; }

把 qss 文件放到资源文件 qrc 里,用QFile读取再qApp->setStyleSheet加载,比在代码里一行行 setStyleSheet 要清爽得多。教学时可以让学员自己配一套主题色,这个环节他们通常玩得最起劲。

5. 用 QCustomPlot 做出支出趋势图和分类占比图

5.1 QCustomPlot 还是 Qt Charts

做图表统计前先选工具。Qt Charts 是 Qt 官方模块,安装时勾选就能用,动画效果和交互反馈比较现代。但我个人更推荐教学项目用 QCustomPlot,原因很实际:它只有 qcustomplot.h 和 qcustomplot.cpp 两个文件,直接拖进工程就能编译,不需要额外模块依赖,随时随地都能跑起来。

QCustomPlot 虽然经常被用来做信号波形可视化,比如时域波形转频域图那种场景,但它的统计图表能力同样很能打。折线图、柱状图、饼图都有现成的类,接口直白,文档和示例代码也全。对记账本这种"查询数据然后画出来"的需求,完全够用。加上它是开源的(GPL 协议,商用需要购买商业授权),对教学来说没有任何成本门槛。

我给的选型建议是:教学用 QCustomPlot,因为部署成本低、教育场景不存在协议风险;如果后续做商业产品再考虑 Qt Charts 或者升级成 QCustomPlot 商业版。

5.2 月度支出趋势折线图

趋势图展示的是某段时间内每个月的支出总额变化。数据用 SQL 按月汇总:

SELECT substr(date, 1, 7) AS month, SUM(amount) FROM bills WHERE type = '支出' AND date BETWEEN :begin AND :end GROUP BY month ORDER BY month;

拿到数据之后往 QCustomPlot 里塞,核心代码:

ui->trendPlot->clearGraphs(); QCPGraph *graph = ui->trendPlot->addGraph(); QVector<double> xs, ys; for (int i = 0; i < months.size(); ++i) { xs << i; ys << amounts[i]; } graph->setData(xs, ys); graph->setPen(QPen(QColor(0, 160, 233), 2)); graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, 6)); QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText); textTicker->addTicks(xs, months); ui->trendPlot->xAxis->setTicker(textTicker); ui->trendPlot->xAxis->setTickLabelRotation(30); ui->trendPlot->rescaleAxes(); ui->trendPlot->replot();

这里有个关键点:x 轴放的并不是日期字符串,而是 0、1、2 这样的索引,再用 QCPAxisTickerText 把索引对应的月份标签映射到轴上。如果直接把字符串当 x 轴坐标,QCustomPlot 处理不了类别轴,图表就会乱。这个坑不少学员都踩过。

rescaleAxes()会自动把坐标轴范围贴合数据范围,别忘了在数据变化之后调用。最后一定要replot(),QCustomPlot 不会自动重绘,不调用这行界面就看不到新图表。

5.3 分类支出占比饼图

饼图用 QCustomPlot 的 QCPPie 类实现。它的逻辑是先创建饼图对象,然后往里面一块块加数据:

QCPPie *pie = new QCPPie(ui->piePlot->xAxis->range().center(), ui->piePlot->yAxis->range().center()); pie->setPen(QPen(Qt::NoPen)); pie->setLabelMode(QCPPie::lmPercent); // 显示百分比 for (int i = 0; i < categories.size(); ++i) { pie->addSlice(categories[i], amounts[i]); } ui->piePlot->addPlottable(pie); ui->piePlot->replot();

默认情况下 QCPPie 会自动分配一组颜色,但同一批数据顺序每次一致的话,颜色也固定,所以不用担心刷新时配色乱跳。如果你希望能手动控制分类的颜色,可以拿第一个 slice 返回的QCPPieSlice指针,然后逐个设置setBrush

饼图刷新的时机建议和表格联动:当用户改了筛选条件并点击查询后,refreshChart()refreshTable()一起被调用。不要把查询逻辑写在窗口构造函数里只跑一次,否则新增一条账目后图表完全不会更新,初学者经常在这里困惑。

5.4 图表数据刷新的时机与性能

记账本的数据量一般不大,每次查询后直接重新生成图表数据即可,不用做增量更新。QCustomPlot 在几百个数据点范围内重绘性能绰绰有余。如果学员想扩展成"按周/按月/按年"多种统计粒度,只需要替换 SQL 的GROUP BY字段,代码框架完全不用动。

数据库连接池这种东西在这个项目里是没必要的,单用户本地应用单连接完全够用。这个话可以跟学员讲清楚,让他们理解不同场景下技术选型差异,不要一上来就过度设计。

6. 打包发布避坑:从"我电脑上能跑"到"别人电脑上也能跑"

6.1 windeployqt 的正确打开方式

项目做完之后第一件事是切到 Release 模式重新编译一遍,确认没有 qDebug 输出干扰、没有依赖调试库。然后打开 Qt 提供的命令行工具,切到 exe 所在目录执行:

cd /d "D:\build-Ledger-Desktop_Qt_5_15_2_MSVC2019_64bit-Release\release" D:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe Ledger.exe

windeployqt 会扫描 exe 的依赖,自动把 Qt 的 DLL、platforms 插件目录、styles 插件、翻译文件等复制到 exe 旁边。执行完之后你会看到一个几十兆的文件夹,这就是最基础的发布形态。

6.2 platform plugin 初始化失败这类报错的排查链路

发布后在干净环境里双击 exe,最常见的一个报错就是windows no qt platform plugin could be initialized, reinstalling the application may fix this problem。你按正常的路径应该是:先检查 exe 同目录下有没有platforms文件夹,里面有没有qwindows.dllplatforms文件夹必须和 exe 同级,不能塞到子目录里,更不能漏掉。

如果 platforms 文件夹没问题,接下来看 PATH 环境变量。Windows 上如果之前装过其他版本的 Qt,或者 PATH 里残留了某个 Qt 的 bin 目录,exe 运行时优先加载的 DLL 可能就不是同目录下的那批,版本和编译器不一致就会触发 platform plugin 初始化失败。排查方法是把 exe 目录移到一台干净的机器或者一个隔离目录里再跑,排除 PATH 干扰。

另一个隐蔽的坑是 Release 和 Debug 混用。有的人平时一直用 Debug 构建,发布时直接把 Debug 的 exe 拖出来拿到 windeployqt 来打,结果把 debug 依赖的 Qt5Cored.dll 和 release 的 Qt5Core.dll 混在一起。Qt 的 debug 和 release 库不能混着用,发布前务必重新构建 Release 版本。最直接的办法是在 Qt Creator 左下角的构建配置里切换成 Release,然后重新编译。

6.3 运行库和目录结构

MSVC 编译的程序还需要 VC++ 运行库。目标机器上如果没有安装过 Visual C++ Redistributable,程序可能根本起不来。最稳妥的做法是发布时把vc_redist.x64.exe一起附带,写个安装脚本或者在压缩包里说明先安装运行库。windeployqt 默认不会自动带上 VC++ 运行库,这一步很容易被忽略。

发布目录里建议把数据库文件路径处理成首次启动自动创建,而不是把开发机器上已经塞满测试数据的 ledger.db 直接打包给用户。我一般会在发布说明里加一句:首次启动会在用户数据目录自动生成空白账本数据库。

如果目标环境是国产 Linux 发行版,要留意系统里自带的 Qt 版本和开发环境版本是否一致,不一致时建议把项目用到的 Qt 插件和依赖库一并打包发布,避免依赖系统库版本差异导致运行时崩溃。这类环境的坑比较深,教学阶段可以在虚拟机里跑通一个流程,给学员建立"跨平台发布需要单独适配"的意识就够了。

我自己带项目的经验是,打包发布这一关卡住的人最多。原因不是技术难,而是这个环节没有报错定位工具,全靠对 Qt 运行时结构有完整认知。所以课堂上我通常会让学员先把发布目录结构和每个文件夹的作用讲一遍,再动手打包,效果比直接扔给他们 windeployqt 命令好得多。

最后分享一个教学小技巧:让每个学员把自己的账本应用设计成不同主题色,然后互相拷贝发布会遇到各种奇奇怪怪的问题,大家一起排查一遍,这种真实环境里的"被迫动手"比看十遍教程都管用。记账本这个案例能扩展的方向还有很多,预算管理、账单提醒、CSV 导入导出、多账户支持,每一个都是把现有代码结构往更工程化方向推进的好题目。先把这个项目完整交付出去,你就算真正迈进 Qt 开发的门了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询