ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Qt从零开发个人记账本:数据库、模型视图与图表全搞定

用Qt从零开发个人记账本:数据库、模型视图与图表全搞定 简介一个基于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里写完代码后按一下CtrlR直接运行然后顺手按一下CtrlB编译快捷键——让这两个动作形成肌肉记忆。看起来微不足道但“多编译、多运行、多调试”就是高手和新手之间最开始拉开差距的地方。祝你顺利通关这个项目有问题欢迎在评论区交流。本文还有配套的精品资源点击获取
返回列表