简介:一个基于Qt的个人记账本教学案例,面向初学Qt或正在做课程设计的开发者,以Windows 10 + Qt5.9.9 + MinGW32为开发环境,展示了跨平台工程的结构与实现思路。案例重点覆盖用户界面搭建、信号与槽机制、数据存储与查询,以及不同操作系统下的兼容处理。代码注释清晰,适合作为记账类应用的起步模板。资源包共8个文件,包含3个C++源文件、2个头文件、工程文件(.pro)、Qt Creator用户配置(.user)和README说明文档,压缩包仅17KB,内容精炼,便于逐行阅读。目前已有275人学习,适合快速借鉴项目结构。通过阅读源码可掌握基于QTableView等控件的记账界面布局,理解databasemanager对账目记录的增删查改操作,还能参考其跨平台运行配置方法。对于需要完成Qt课程作业或了解桌面应用开发流程的读者,是一份可直接上手的轻量示例。
1. 项目概述:为什么拿记账本当Qt教学案例
做Qt教学这么多年,如果让我只挑一个项目带新手完整走一遍,我肯定选个人记账本。为什么?因为它麻雀虽小五脏俱全。界面布局、信号槽、数据库操作、模型视图、图表绘制、打包发布,Qt开发里你能叫得上名字的模块,它几乎全都能用上。而且记账本的需求每个读者都懂,不用花时间解释业务逻辑,可以把全部注意力放在技术实现上。
这次项目我带的是一群刚学完C++基础、准备转向Qt桌面开发的同学。我选了Qt 5.15.2 + Qt Creator这套组合,编译器用的MSVC2019 64位。项目最终实现的效果是:能记录收入和支出,按日期、分类筛选流水,用表格呈现所有记录,用图表直观展示每月的收支趋势和分类占比,数据全部持久化存储到本地SQLite数据库文件里。
说实话,第一次上课的时候我并没有指望这群同学能写出多惊艳的功能,我更看重的是整个代码结构是否清晰、模块划分是否合理。但从最终的提交作业来看,有几个同学确实把我教的套路吃透了,甚至自己主动加了年度预算提醒功能。这个项目的可扩展性就是这么强——核心架构搭好后,往上加功能就是水到渠成的事。
如果你是刚开始学Qt,或者学了一半感觉知识很散、不知道能做什么,我强烈建议你跟着这个项目走一遍。这套案例适合有三到六个月C++基础的人,完全不熟悉面向对象的读者可能需要先补一下类、继承、多态这些基础概念。项目整体工作量大约是一到两周的课余时间,做完之后你对Qt的理解会有一个质的飞跃。
2. 整体设计思路拆解:从需求到模块划分
2.1 记账本的核心需求到底有哪些
提需求这事看起来简单,但真让你把“记账”二字拆开,你会发现里面藏着一堆细节。我带着学生们花了一节课专门列需求清单,最后大家达成一致:一个能用的记账本,起码要有流水记录(金额、分类、日期、备注)、按时间范围查询、按分类统计、图表展示、数据存储这五块。
前排有个同学还提了一句:“老师,能不能加个导入导出Excel的功能?”我当时没让他做,但对于有余力的同学确实是个不错的加分项。Qt里用QAxObject操作Excel是现成的方案,不过需要Windows环境,而且代码量不小,基础阶段先不碰它。
我把需求拆成了几个核心模块:数据层负责SQLite表的创建和增删改查,业务层负责校验金额、格式化日期这类规则,界面层由主窗口、流水表格、筛选区、统计图四部分组成。这里面的核心思想是分层——界面代码不直接写SQL,数据层不关心按钮在哪里,各玩各的,互相通过接口通信。
2.2 技术选型:为什么是SQLite + QTableView + QChart
先说数据存储。记账本这类轻量级个人工具,数据量撑死几千条,用MySQL或PostgreSQL纯属杀鸡用牛刀。SQLite作为嵌入式数据库,不需要独立服务进程,Qt自带的QSQLITE驱动直接就能用,生成的文件就是一个.db文件,拷走就能迁移,做教学案例再合适不过。
表格显示方面,我见过不少教程直接教新手往QTableWidget里逐行insert数据。这个方案不是不行,但每次刷新都要手动清空再重填,代码又臭又长。更好的选择是QTableView + QSqlTableModel的组合,模型绑定数据库表之后,增删改操作会自动同步到界面,刷新逻辑全省了。这背后的思想是Qt的模型视图架构:数据(模型)和显示(视图)分离,模型改了,视图自动跟着变。
图表部分用的QChart。它从Qt 5.7开始就是官方模块,画折线图、饼图都挺顺手,而且和QWidget配合得很自然。网上也有很多人用QCustomPlot,但它毕竟是第三方库,需要额外集成源码,教学场景下我用官方的东西,省去环境配置的麻烦。关于QCustomPlot的性能优势,等以后做高频刷新数据的项目时再介绍。
2.3 这个方案能让学生学到什么
如果只是为了做一个记账工具,现成的开源项目随便下载一个都比我们写的强。教学案例的价值在于让学习者在这个过程中掌握一套通用的Qt开发方法论。
首先是信号槽机制。界面上的“添加记录”按钮点击后,如何触发数据层的插入操作,参数怎么传递,这条链路是整个Qt应用开发的核心。其次是模型视图架构,QSqlTableModel一个类就把数据库和界面串起来了,理解了它,以后再接触QStandardItemModel、QSortFilterProxyModel都会很轻松。再次是图表绘制,QChart的API设计得相当规整,学会画一个饼图,换个series类型就能画折线图、柱状图。
我一直跟学生强调一句话:学Qt不要背API,要背套路。就像做菜一样,同样是西红柿炒鸡蛋,老手知道先炒蛋还是先炒番茄、什么时候放糖什么时候放盐,这些套路才是经验所在。这个项目覆盖的套路足够多,做完一遍,你对Qt的整体认知框架也就建立起来了。
3. 核心细节解析与实操要点
3.1 数据库表设计:分类表与流水表的前世今生
数据库设计是一切的根基,表建得不好,后面写代码全是坑。我们的记账本需要两张表:分类表category和流水表record。数据库直接用sqlite的main schema,建表语句如下:
CREATE TABLE IF NOT EXISTS category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, type TEXT NOT NULL CHECK(type IN ('income', 'expense')) ); CREATE TABLE IF NOT EXISTS record ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, amount REAL NOT NULL CHECK(amount > 0), record_date TEXT NOT NULL, note TEXT, FOREIGN KEY(category_id) REFERENCES category(id) );这里我故意用了两个设计决策,需要解释一下为什么。
第一,金额为什么不存成整数分而是用REAL?如果涉及金融交易,浮点数的精度问题很致命,必须用整数存储最小单位。但个人记账本场景下,金额就是给人看的,一般也就两位小数,REAL类型够用了。我不想在教学中过早引入精度问题分散注意力。如果你的项目对接支付系统,请务必改成以分为单位的整数字段。
第二,日期为什么用TEXT不用DATE类型?SQLite本身就是弱类型数据库,DATE类型存进去本质上也是TEXT。我用ISO格式(YYYY-MM-DD)存日期,好处是字典序就是时间序,直接ORDER BY record_date就能得到正确排序,不用做任何转换。这个细节很多人不注意,到了做区间筛选的时候才后悔。
分类表的type字段加了CHECK约束,限定只能是income或expense,从数据库层面杜绝脏数据。record表通过category_id外键关联分类表,这也是一个相对规范的范式设计。虽然SQLite默认不强制外键约束(需要PRAGMA foreign_keys = ON),但表结构上有这层关系,做JOIN查询时就顺理成章了。
3.2 界面布局:QTableView + QChartView怎么放才顺手
界面我采用的是左右分栏布局:左侧是操作区和统计图表,右侧是流水表格区。整体用QSplitter承载,运行后用户可以自由拖动分隔条调整两侧比例,比固定布局多了点灵活性。
右侧核心控件是QTableView。我把它的selectionBehavior设置为QAbstractItemView::SelectRows,用户点击任意单元格都选中整行。列宽策略上,金额列设为ResizeToContents,备注列设为Stretch,这样长备注会自动撑开,而金额列不会因为显示数字长度不同而抖动。所有列默认都不能编辑,编辑入口统一放在“添加/编辑”弹窗里,避免用户在表格里随意改数据。
左侧上半部分是筛选区,包含开始日期、结束日期两个QDateEdit,一个收支类型下拉框QComboBox,以及“查询”和“重置”两个按钮。下半部分是一个带Tab的QTabWidget,第一个Tab放月度收支折线图(QChartView),第二个Tab放分类占比饼图。
顶部工具栏则放了“添加收入”“添加支出”“编辑”“删除”四个按钮。为什么把收入支出分开两个按钮而不是一个统一的“添加”按钮?因为实际使用中,用户在记录一笔收入时,他脑子里很清晰这是收钱的,不想再点一下下拉框选类型。直接分开两个入口,每个按钮预置好类型,操作路径短了一截,体验会好很多。这两个按钮复用一个添加对话框,只是传入的type参数不同。
整个主窗口用QSS做了一套浅色主题。我给学生展示了一套简化的QSS,重点演示了QPushButton的hover效果、QTableView表头的背景色设置、QDateEdit的圆角边框。不需要太复杂,但能让学生看到QSS的威力——同样的代码,加了样式之后观感完全不一样。
3.3 关键代码拆解:增删改查与图表刷新的完整链路
先说最核心的:数据库连接与模型初始化。我在main函数里只做一件事,设置应用名称和打开数据库文件,初始化工作全部放在MainWindow构造函数里,这样逻辑更内聚。
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName(QCoreApplication::applicationDirPath() + "/accounting.db"); if (!db.open()) { QMessageBox::critical(this, "错误", "数据库打开失败: " + db.lastError().text()); return; }打开数据库后,紧接着创建表和初始化分类数据。如果category表是空的,就插入默认的几个分类:工资、奖金(收入类),餐饮、交通、购物、居住、娱乐(支出类)。
流水表的模型初始化代码如下:
model = new QSqlTableModel(this); model->setTable("record"); model->setSort(3, Qt::DescendingOrder); // 按日期倒序 model->setHeaderData(1, Qt::Horizontal, "分类"); model->setHeaderData(2, Qt::Horizontal, "金额"); model->setHeaderData(3, Qt::Horizontal, "日期"); model->setHeaderData(4, Qt::Horizontal, "备注"); model->select(); ui->tableView->setModel(model);这里有个坑必须提一下:QSqlTableModel默认显示的列是表的全部列,但我不希望用户看到category_id这个外键字段,所以用setHeaderData改了表头名称还不够,还需要隐藏category_id列。正确做法是调用ui->tableView->setColumnHidden(1, true),但要注意列索引是从0开始的,第0列是id,第1列才是category_id,别隐藏错了。
添加记录时,我用了一个QDialog对话框,里面放了分类下拉框(选项根据当前是收入还是支出从category表拉取)、金额输入框(QDoubleSpinBox,范围0.01到9999999.99,步长0.01)、日期选择(QDateEdit,默认今天)和备注输入框(QLineEdit)。确定按钮绑定的槽函数如下:
void MainWindow::onAddRecord(const QString &type) { QSqlQuery query; query.prepare("INSERT INTO record (category_id, amount, record_date, note) " "VALUES (?, ?, ?, ?)"); query.addBindValue(dlg->categoryId()); query.addBindValue(dlg->amount()); query.addBindValue(dlg->date().toString("yyyy-MM-dd")); query.addBindValue(dlg->note().trimmed()); if (!query.exec()) { QMessageBox::warning(this, "错误", "添加失败: " + query.lastError().text()); return; } model->select(); refreshChart(); }这里用了prepared statement和addBindValue绑定参数,而不是直接把字符串拼进SQL。这不仅是防SQL注入的安全习惯,还能让代码逻辑更清晰。很多初学者图省事写字符串拼接,遇到值里带引号就炸了,预编译绑定完全规避了这类问题。
图表刷新是整个项目里最让学生眼前一亮的环节。我用QChart的折线图展示近30天每天的收入和支出总额,饼图展示本月各类支出占比。以饼图为例:
void MainWindow::refreshPieChart() { QSqlQuery query; query.prepare("SELECT c.name, SUM(r.amount) FROM record r " "JOIN category c ON r.category_id = c.id " "WHERE c.type = 'expense' AND r.record_date LIKE :prefix " "GROUP BY c.name"); query.bindValue(":prefix", QDate::currentDate().toString("yyyy-MM") + "%"); QPieSeries *series = new QPieSeries(); while (query.next()) { series->append(query.value(0).toString(), query.value(1).toDouble()); } QChart *chart = new QChart(); chart->addSeries(series); chart->setTitle("本月支出分类占比"); chart->legend()->setAlignment(Qt::AlignRight); chart->setAnimationOptions(QChart::SeriesAnimations); ui->pieChartView->setChart(chart); ui->pieChartView->setRenderHint(QPainter::Antialiasing); }有几个容易忽略的细节值得强调。QChartView::setRenderHint(QPainter::Antialiasing)这句能让图表边缘平滑,不设置的话折线和饼图边缘有锯齿,很影响观感。chart->legend()->setAlignment(Qt::AlignRight)把图例放在右边,对于分类多的场景比放底部省空间。setAnimationOptions(QChart::SeriesAnimations)给图表加了动画效果,刷新数据时过渡很流畅,花一行代码换来视觉效果大幅提升,这笔买卖很划算。
3.4 筛选查询与统计逻辑
筛选功能本质上就是动态修改QSqlTableModel的filter属性。比如用户选择了开始日期、结束日期和收入类型,我就拼接filter字符串:
QString filterConditions; // 条件集合 if (!startDate.isEmpty() && !endDate.isEmpty()) { filterConditions += "record_date BETWEEN '" + startDate + "' AND '" + endDate + "'"; } if (currentType != "all") { // 需要先join category表才能按类型过滤 model->setFilter(...); }不过这里有个隐藏的坑:QSqlTableModel默认只操作一张表,如果要在tableView中显示分类名称(来自category表)而不是category_id,就不能直接在filter里写category.type = 'income'。我的解决办法是在record表里冗余存储一个type字段,插入时由程序写入,虽然反范式,但省掉了JOIN的复杂度。这算是教学项目的务实折中,真实项目中我会考虑用QSqlQueryModel配合自定义SQL来实现联表查询。
关于筛选,我还引入了一个小技巧:金额出入校验。当用户输入的开始日期晚于结束日期时,直接提示并阻断查询,而不是拿一个空结果糊弄用户。这类防御性编程的思路,比功能本身更值得学生吸收。
4. 实操过程与常见问题排查
4.1 手把手:从新建工程到跑起来
我用的开发环境是Windows 10 + Qt 5.15.2 + Qt Creator 4.14。第一步是新建Qt Widgets Application工程,注意在Kit Selection页面勾选MSVC2019 64位,别选成MinGW。有些同学电脑上两套编译器都装了,结果编译时库不匹配报出一堆link错误。
在.pro文件中,需要追加这行配置:
QT += sql charts不加这一行,代码里#include 或#include 会直接报找不到头文件的错误。很多新手第一次遇到这个问题都是懵的,其实就是漏了模块声明。
接下来按顺序添加以下文件:
database.h / database.cpp:封装数据库初始化、建表、默认分类插入recorddialog.h / recorddialog.cpp:添加/编辑记录的对话框类mainwindow.h / mainwindow.cpp:主窗口,集成表格、筛选、图表main.cpp:程序入口
按这个顺序写的好处是依赖关系递减,先做底层,再做界面,每完成一块都可以编译跑一次验证。我反复强调学生不要写完2000行代码再编译,那样一旦报错根本不知道从哪里找起。分步编译调试的习惯从第一个Qt项目就要养成。
全部代码写完后,在Qt Creator左下角点击绿色三角箭头直接运行。成功后,会看到一个空白的表格和两个空白图表。此时别急着高兴,测试一下添加记录:点“添加支出”,分类选“餐饮”,金额输入25.5,日期保持今天,备注写“午餐”,确定后表格里多了一行,饼图上也出现了餐饮这一块。此时的成就感会特别真实。
4.2 高频率问题实录:你踩过的坑这里都有
问题一:运行报“no qt platform plugin could be initialized”
这个报错出现在双击exe直接运行时,典型的运行环境问题。在Qt Creator里跑没问题,但脱离Creator直接运行exe就报这个错。原因是exe找不到Qt的platforms插件目录(qwindows.dll所在文件夹)。解决方案是用windeployqt工具部署,在命令行执行:
windeployqt --release path\to\your\app.exe它会自动把exe依赖的Qt DLL和plugins复制到exe所在目录。我在项目结束后的打包发布环节专门演示了一遍:把C盘Qt目录下的windeployqt.exe路径加入系统PATH,然后在build目录里执行命令,瞬间多出几十个文件。再去bin目录里看一眼,确认qwindows.dll在platforms子目录下,运行就正常了。记住一个原则:发给别人的程序一定用windeployqt部署过,别直接发裸exe。
问题二:QChart图表不显示,界面上一片空白
我排查过好几次这个问题,最后发现是忘了写chart->createDefaultAxes()。折线图需要坐标轴,饼图则不需要。如果没创建默认轴,系列数据根本没有轴可依,图自然画不出来。另外还有一个坑是QChartView只绑定了一个新的QChart实例,但旧实例没释放,导致内存泄漏,表现就是切换Tab再切回来后图表消失了。我给学生强调:每次刷新图表时,先delete chart再新建,或者让QChartView的setChart接管新chart的所有权。
问题三:QSS样式不对,按钮没反应
初学者写好QSS后经常发现样式没生效。常见原因有三个:一是选择器写错了,比如.QPushButton误写成了QPushButton,注意带不带点前缀含义完全不同;二是没有用#objectName指定对象名;三是QSS文件没有被加载。我在项目里用的是程序内加载QSS的方式:
QFile file(":/style.qss"); file.open(QFile::ReadOnly); QString styleSheet = QString::fromUtf8(file.readAll()); qApp->setStyleSheet(styleSheet);注意资源文件一定要把style.qss加进去,不然readAll读到的是空字符串。更隐蔽的问题是文件编码,QSS文件里如果写了中文注释,必须用UTF-8编码保存,否则用QSplitter设置边界那些样式时中文注释会变成乱码并可能导致解析失败。
问题四:QTableView数据刷新后图表没同步
这是因为刷新图表函数没有在增删改记录的路径上被调用。我给学生的建议是:把所有会改动数据的操作集中到几个固定函数(addRecord、editRecord、deleteRecord),这三个函数末尾统一调用refreshChart()。不要让业务逻辑散落在各个按钮的槽函数里,不然你删了一条数据但忘了刷新图表,坑就来了。
问题五:分类下拉框重复出现相同项
原因是没有去重,初始化时多次向QComboBox里addItem了同一批分类。我建议在启动时只调用一次loadCategories(),并且在下一次加载前先comboExpense->clear()清空原有内容,然后再填充。这也是“先清理后填充”的通用思路,在任何下拉框数据源更新的场景都适用。
4.3 打包发布:一个让人头大又必须掌握的环节
项目做完后,学生最兴奋的就是把程序打包发给朋友炫一下。此时windeployqt导致的坑又来了:你用Release模式编译的exe,如果用Debug版的Qt库去部署,一样会报错。记住一定要用和编译模式一致的部署工具。
如果目标机器是另一台没有装过Qt的电脑,除了exe同目录的所有DLL和plugins目录之外,还要注意SQLite插件是否被自动包含。windeployqt默认会带上sqldrivers目录下的qsqlite.dll,但如果你在代码里用了QSQLITE驱动,请确认部署目录里确实有这个文件,很多“程序在我电脑上能跑,在别人电脑上就崩”的案例都是少拷了插件目录导致的。
打包成单个exe文件,一般要借助Enigma Virtual Box或打包成安装程序用Inno Setup。我用的是Inno Setup,脚本很简单,把exe和整个部署目录装进去,在开始菜单创建快捷方式。做完后双击安装包,全流程不到一分钟,很有仪式感。
5. 教学效果复盘:学生学完能迁移什么能力
每次带完这个项目,我都会让同学们写一份总结报告。从他们的反馈来看,收获最集中的不是某个具体函数,而是一种“拆解问题再组装解决方案”的思维。比如有学生后来自己想做一个宿舍水电费管理小程序,迁移了记账本的整套框架——数据库表设计、模型视图绑定、统计图表展示——只改了业务字段,架构完全复用。
从技术成长角度看,这个项目帮学生清晰建立起了几条知识链:
第一,数据库知识链。SQLite的连接、建表、增删改查、WHERE条件过滤、JOIN联表、聚合函数SUM与GROUP BY分组,这些SQL常用操作在真实场景里全部过了一遍。会写SQL和会用SQL解决实际问题,完全是两个层次。
第二,模型视图编程思想。QSqlTableModel只是个开始,后面学QSortFilterProxyModel做自定义排序筛选、学QStandardItemModel做树形结构,都是同一个套路。掌握了模型视图,Qt开发的视野一下子就开阔了。
第三,界面与数据的联动思维。选项变了、数据变了、点击按钮了,界面如何响应,数据如何流转,这是桌面应用开发的核心逻辑。很多学生第一次体会到“写代码让界面活起来”的成就感,这个项目功不可没。
关于扩展,我一般会留下三个课外任务让感兴趣的同学挑战:给表格加一个按月份折叠分组的功能;支持导出CSV文件用Excel打开;把单人记账变成多人家庭账本,加个“成员”字段。这些任务难度梯度合理,学有余力的同学可以按照项目已搭好的架构继续弄,不需要伤筋动骨。
还有一个我事后想起来觉得非常值得补充的点:版本管理。我要求所有学生从第一天就使用Git提交代码,每完成一个功能模块就commit一次。很多同学一开始觉得麻烦,但出现改坏代码需要回滚、或者实验新功能想对比效果的时候,Git的好处就体现得淋漓尽致。以后参加工作,代码托管和协作协作是基本技能,通过这个项目积累的commit习惯,会让学生在团队协作时少踩很多坑。
6. 写在最后的一点项目心得
这个记账本案例我一直用到现在,每年带新班都会稍作调整但总体框架不变,因为它实在太经典了。Qt就是那种“入门容易、精通难”的框架,而记账本恰好能在难度和趣味性之间找到一个平衡点。
如果你想拿这个项目练手,我建议你不要完全照抄我的代码。先自己画一遍界面草图,写一遍需求文档,然后动手去实现。遇到卡壳的地方再回来翻我写的这些解析。没有谁能一步到位写出完美的代码,第一版丑一点没关系,能跑起来就是胜利。我在写这个项目的第一版时,也是踩了无数个莫名其妙的坑才把它磨顺的。
最后再分享一个小技巧:每次在Qt Creator里写完代码后,按一下Ctrl+R直接运行,然后顺手按一下Ctrl+B编译快捷键——让这两个动作形成肌肉记忆。看起来微不足道,但“多编译、多运行、多调试”就是高手和新手之间最开始拉开差距的地方。祝你顺利通关这个项目,有问题欢迎在评论区交流。
本文还有配套的精品资源,点击获取