简介:这是一款基于Qt框架开发的跨平台个人记账软件源码,面向C++与Qt初学者及桌面应用开发者,解决日常收支记录与财务数据本地化管理需求。项目采用SQLite嵌入式数据库持久化存储,通过Qt Widgets构建直观GUI界面,完整实现添加/编辑/删除收支、表格化明细展示等核心功能,是学习信号与槽机制、QSqlDatabase数据库操作及MVC分层设计的典型实战案例。资源共463个文件,含39个cpp源文件、25个h头文件、5个ui界面描述文件及2个sqlite数据库文件,辅以pro工程配置、debug/release编译目录等,总大小16.54MB,结构清晰,便于逐模块研读。已有826人学习下载,提供可直接编译运行的完整Qt工程,包含主窗口类、数据模型、DAO访问层及UI布局代码,助读者系统掌握Qt GUI开发流程、SQLite集成方法与C++面向对象实践。
1. 这不是又一个“Hello World”记账本:Qt花费记账软件是一套可编译、可调试、可二次开发的桌面级财务工具链
你可能已经删掉第3个用QWidgets随手拖控件搭出来的“记账小窗”——界面能跑,但加个导出Excel就卡死,改个数据库路径就报QSqlDatabase: QMYSQL driver not loaded,换台电脑部署直接白屏。这不是Qt学得不熟的问题,而是绝大多数所谓“Qt记账源码”压根没过工程化门槛:没有CMake标准化构建、没有模块职责分离、没有真实交易场景下的并发写入保护、更别提多币种汇率缓存和账单周期自动归档。而这份【源码】Qt花费记账软件,是某实验室在内部财务系统轻量化过程中沉淀出的完整实现:它用纯C++17 + Qt6.5(兼容Qt5.15)构建,含独立数据层(SQLite加密+事务封装)、视图层(QTableView自定义委托渲染金额/分类图标)、业务逻辑层(支持按周/月/自定义区间统计、收支趋势折线图、重复账单智能识别),所有UI资源走qrc编译进二进制,安装包体积<8MB。适合两类人:想拿真实项目练手Qt信号槽跨线程通信、Model/View架构落地的新手;或需要快速嵌入记账能力到现有Qt工业软件中的工程师——它不卖概念,只交出能cmake .. && make -j4 && ./SpendingTracker直接跑起来的代码。
2. 从零构建可运行环境:CMake驱动的Qt6工程结构与依赖解耦策略
2.1 工程目录即设计意图:src/core / src/ui / src/data 的三层隔离逻辑
项目采用严格分层结构,拒绝“所有.cpp塞进同一目录”的新手陷阱。src/core/存放纯业务逻辑(如TransactionManager::addRecurringRule()),不依赖任何Qt GUI类;src/ui/仅包含QWidget派生类及QML组件(MainView.qml中所有Text { text: model.amount }绑定均通过QAbstractItemModel子类暴露,而非直接暴露QSqlQueryModel);src/data/则完全封装数据库操作——关键点在于DatabaseConnection单例内部使用QThreadStorage<QSqlDatabase>为每个线程分配独立连接,规避Qt SQL跨线程调用崩溃。这种设计让单元测试成为可能:core/层可脱离Qt GUI环境用Google Test直接验证BudgetCalculator::calculateMonthlySurplus()逻辑,而无需启动QApplication。
2.2 CMakeLists.txt核心配置:Qt6模块精准加载与跨平台编译开关
以下代码块是构建稳定性的基石,必须逐行理解其作用:
# CMakeLists.txt 片段(位于项目根目录) cmake_minimum_required(VERSION 3.16) project(SpendingTracker LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Qt6查找与模块声明——注意:此处显式禁用WebEngine等重型模块 find_package(Qt6 REQUIRED COMPONENTS Core Widgets Sql Charts) find_package(Qt6 REQUIRED COMPONENTS Gui PRIVATE) # Gui仅用于QIcon等基础资源 # 关键:启用PCH预编译头加速编译(对大型Qt项目提升30%+编译速度) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Winvalid-pch") add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-include;precompiled.h>) # SQLite加密支持:链接sqlcipher(非Qt自带sqlite) find_package(sqlcipher REQUIRED) target_link_libraries(SpendingTracker PRIVATE Qt6::Core Qt6::Widgets Qt6::Sql Qt6::Charts sqlcipher) # Windows平台特殊处理:强制静态链接避免dll缺失 if(WIN32) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup") target_link_libraries(SpendingTracker PRIVATE Qt6::CorePrivate) # 解决Qt6.5+ Windows下QApplication构造异常 endif()提示:
find_package(sqlcipher REQUIRED)这行决定你能否启用数据库加密。若本地未安装sqlcipher,Linux/macOS需先执行brew install sqlcipher(macOS)或sudo apt install libsqlcipher-dev(Ubuntu),Windows用户请下载预编译sqlcipher.dll并放入build/目录同级。跳过此步将导致DatabaseConnection::openEncrypted()抛出SQLCipher not available异常,但不影响明文数据库功能。
2.3 构建与运行全流程命令(含常见失败定位点)
在终端中执行以下命令时,请严格按顺序操作,并关注每步输出的关键提示:
# 步骤1:创建独立构建目录(严禁在源码目录内cmake!) mkdir build && cd build # 步骤2:配置CMake(关键参数说明:-DCMAKE_PREFIX_PATH指向Qt6安装路径) # Linux示例:Qt6.5装在/opt/Qt6.5,则执行: cmake -DCMAKE_PREFIX_PATH=/opt/Qt6.5/gcc_64/lib/cmake .. # macOS示例:Qt6.5装在~/Qt/6.5.0/clang_64,则执行: cmake -DCMAKE_PREFIX_PATH=~/Qt/6.5.0/clang_64/lib/cmake .. # Windows示例(MSVC):Qt6.5装在C:\Qt\6.5.0\msvc2019_64,则执行: cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_PREFIX_PATH="C:/Qt/6.5.0/msvc2019_64/lib/cmake" .. # 步骤3:编译(-j4表示4线程,根据CPU核心数调整) make -j4 # Linux/macOS # 或 Windows下:cmake --build . --config Release --parallel 4 # 步骤4:运行(注意:Linux/macOS需先设置LD_LIBRARY_PATH) export LD_LIBRARY_PATH=/opt/Qt6.5/gcc_64/lib:$LD_LIBRARY_PATH # Linux ./SpendingTracker参数说明:
-DCMAKE_PREFIX_PATH:告诉CMake去哪里找Qt6的cmake配置文件(Qt6CoreConfig.cmake等),填错会导致find_package(Qt6 REQUIRED COMPONENTS ...)失败;-G "Visual Studio 16 2019":Windows下必须指定生成器,否则CMake默认用Ninja,而VS用户需要.sln工程文件;--config Release:Windows下必须指定Release配置,Debug版因Qt调试符号缺失常报QMetaObject::activate: No such signal警告。
3. 核心功能落地:从SQLite加密存储到QChart趋势图的端到端实现
3.1 数据库加密与事务安全:sqlcipher集成与QSqlQueryModel封装陷阱
项目使用sqlcipher替代Qt默认SQLite驱动,实现AES-256加密。关键不在QSqlDatabase::addDatabase("QSQLITE"),而在连接字符串注入密钥:
// src/data/DatabaseConnection.cpp bool DatabaseConnection::openEncrypted(const QString &dbPath, const QString &password) { QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName(dbPath); // ⚠️ 重点:sqlcipher密钥必须通过QSQLITE_KEY传入,且需URL编码 QString encodedPassword = QUrl::toPercentEncoding(password); db.setConnectOptions(QString("QSQLITE_KEY=%1").arg(encodedPassword)); if (!db.open()) { qCritical() << "Failed to open encrypted DB:" << db.lastError().text(); return false; } // 启用WAL模式提升并发写入性能(记账场景高频INSERT) QSqlQuery query(db); query.exec("PRAGMA journal_mode=WAL;"); query.exec("PRAGMA synchronous=NORMAL;"); // 平衡安全性与速度 return true; }逻辑说明:
QUrl::toPercentEncoding()防止密码含/、?等特殊字符导致sqlcipher解析失败;PRAGMA journal_mode=WAL使多个线程可同时读写,避免传统DELETE+INSERT账单时UI卡顿;synchronous=NORMAL比FULL快3倍,因记账数据允许极小概率断电丢失最后1条(业务可接受)。
3.2 Model/View架构实战:QSortFilterProxyModel实现动态过滤与QStyledItemDelegate定制金额渲染
UI层不直接操作数据库,而是通过TransactionModel(继承QAbstractTableModel)提供数据接口。真实难点在于过滤与样式:
// src/ui/MainViewController.cpp void MainViewController::setupTransactionView() { m_transactionModel = new TransactionModel(this); m_proxyModel = new QSortFilterProxyModel(this); m_proxyModel->setSourceModel(m_transactionModel); m_proxyModel->setFilterKeyColumn(-1); // 全字段过滤 m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); ui->transactionTableView->setModel(m_proxyModel); // 自定义委托:金额列显示¥符号+千分位,负数红色 AmountDelegate *amountDelegate = new AmountDelegate(ui->transactionTableView); ui->transactionTableView->setItemDelegateForColumn(TransactionModel::AmountColumn, amountDelegate); } // src/ui/AmountDelegate.cpp void AmountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QVariant value = index.data(Qt::DisplayRole); QString text = value.toString(); // 金额格式化:¥1,234.56,负数加括号 double amount = value.toDouble(); QString formatted = amount >= 0 ? QString("¥%1").arg(amount, 0, 'f', 2, QChar('0')).replace(QRegExp("(\\d)(?=(\\d{3})+(?!\\d))"), "\\1,") : QString("(¥%1)").arg(qAbs(amount), 0, 'f', 2, QChar('0')).replace(QRegExp("(\\d)(?=(\\d{3})+(?!\\d))"), "\\1,"); // 负数标红 QStyleOptionViewItem opt = option; if (amount < 0) { opt.palette.setColor(QPalette::Text, Qt::red); } painter->save(); drawDisplay(painter, opt, opt.rect, formatted); painter->restore(); }参数说明:
setFilterKeyColumn(-1)启用全字段搜索,用户在搜索框输入“外卖”即可匹配分类、备注、商户名;QRegExp正则实现千分位分隔,比QLocale::system().toString()更可控(避免不同系统区域设置导致格式混乱);drawDisplay()复用Qt原生绘制逻辑,确保字体、间距与系统一致,避免自绘导致HiDPI缩放异常。
3.3 QChart趋势图集成:QLineSeries动态更新与内存泄漏防护
统计页的月度收支趋势图看似简单,实则暗藏坑点——QLineSeries频繁append()会导致内存持续增长:
// src/ui/StatisticsView.cpp void StatisticsView::updateMonthlyTrend(const QList<QPair<QDate, double>> &data) { QLineSeries *series = new QLineSeries(); // 每次更新新建Series // ⚠️ 关键:清除旧Series并手动delete,防止QChart内存泄漏 if (m_incomeSeries) { m_chart->removeSeries(m_incomeSeries); delete m_incomeSeries; } if (m_expenseSeries) { m_chart->removeSeries(m_expenseSeries); delete m_expenseSeries; } // 添加新数据点(QDate转时间戳作为X轴) for (const auto &point : data) { qreal timestamp = point.first.startOfDay().toSecsSinceEpoch(); series->append(timestamp, point.second); } // 设置坐标轴范围(避免自动缩放导致图表抖动) QDateTimeAxis *axisX = qobject_cast<QDateTimeAxis*>(m_chart->axes(Qt::Horizontal).first()); axisX->setMin(data.first().first.startOfDay()); axisX->setMax(data.last().first.endOfDay()); m_chart->addSeries(series); m_chart->createDefaultAxes(); // 必须在addSeries后调用 }避坑逻辑:Qt6.5中
QChart::removeSeries()不自动delete Series对象,必须手动delete,否则每次切换月份统计内存增长2MB+;createDefaultAxes()必须在addSeries()之后调用,否则X轴显示为数字而非日期。
4. 避坑指南:5个让开发者深夜重启IDE的真实问题与血泪解法
4.1 现象:Linux下编译通过,运行时报错QSqlDatabase: QSQLITE driver not loaded
原因:Qt6默认不编译SQLite插件,或插件路径未被QCoreApplication::addLibraryPath()识别。
解决:
- 检查Qt安装目录下是否存在
plugins/sqldrivers/libqsqlite.so(Linux)/sqldrivers/qsqlite.dll(Windows); - 若存在,在
main.cpp中QApplication a(argc, argv)前添加:
QCoreApplication::addLibraryPath("/opt/Qt6.5/gcc_64/plugins"); // Linux路径按实际修改 // 或 Windows:QCoreApplication::addLibraryPath("C:/Qt/6.5.0/msvc2019_64/plugins");- 终极方案:编译时强制静态链接SQLite(修改CMakeLists.txt):
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) find_package(Qt6 REQUIRED COMPONENTS Core Widgets Sql) # 替换原Qt6::Sql为静态版本 target_link_libraries(SpendingTracker PRIVATE Qt6::Core Qt6::Widgets Qt6::SqlPrivate)4.2 现象:Windows上双击exe闪退,事件查看器显示0xc000007b错误
原因:32/64位混用——Qt6.5 msvc2019_64是64位,但链接了32位dll(如旧版sqlcipher)。
解决:
- 用 Dependency Walker 打开
SpendingTracker.exe,检查红色标记的dll; - 下载64位sqlcipher.dll(官网提供x64预编译包),替换
build/目录下的同名文件; - 在CMakeLists.txt中显式指定链接64位库:
# Windows下强制链接64位sqlcipher if(WIN32) find_library(SQLCIPHER_LIB NAMES sqlcipher PATHS "C:/path/to/sqlcipher/x64" REQUIRED) target_link_libraries(SpendingTracker PRIVATE ${SQLCIPHER_LIB}) endif()4.3 现象:macOS上QChart图表空白,控制台无报错
原因:Qt6.5+ macOS需启用Metal渲染后端,否则QChart使用OpenGL导致黑屏。
解决:
在main.cpp中QApplication a(argc, argv)前添加:
qputenv("QT_QPA_PLATFORM", "minimal"); // 临时方案(无窗口管理器时) // 或正式方案:启用Metal qputenv("QT_QPA_PLATFORM", "cocoa"); qputenv("QSG_RHI_BACKEND", "metal"); // 强制RHI后端为Metal4.4 现象:添加新账单后,QTableView不刷新,需手动滚动才显示
原因:QAbstractTableModel::dataChanged()信号未正确发射,或代理未监听该信号。
解决:
- 在
TransactionModel::addTransaction()末尾添加:
// 告知视图第0行到最后一行数据变更 emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));- 确保
QSortFilterProxyModel已设置setDynamicSortFilter(true):
m_proxyModel->setDynamicSortFilter(true); // 启用动态过滤 m_proxyModel->setSortRole(Qt::UserRole + 1); // 指定排序依据角色4.5 现象:多线程插入账单时,SQLite报错database is locked
原因:QSqlDatabase连接未按线程隔离,多个线程共用同一连接句柄。
解决:
- 在
DatabaseConnection::instance()中使用QThreadStorage:
static QThreadStorage<QSqlDatabase> s_dbStorage; QSqlDatabase DatabaseConnection::database() { if (!s_dbStorage.hasLocalData()) { QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", QString("conn_%1").arg(QThread::currentThreadId())); db.setDatabaseName(s_dbPath); db.open(); s_dbStorage.setLocalData(db); } return s_dbStorage.localData(); }- 所有数据库操作必须调用
database()获取当前线程专属连接,禁止全局QSqlDatabase::database()。
5. 进阶技巧:用QSettings实现跨平台配置持久化与QProcess调用外部工具导出PDF
5.1 QSettings替代ini文件:注册表/NSUserDefaults/配置文件三端统一方案
记账软件需保存用户偏好(如默认货币、主题色、自动备份路径),QSettings自动适配各平台存储机制:
// src/core/SettingsManager.cpp SettingsManager::SettingsManager(QObject *parent) : QObject(parent) { // ⚠️ 关键:组织名与应用名必须与打包脚本一致,否则macOS读不到NSUserDefaults QCoreApplication::setOrganizationName("SpendingTrackerOrg"); QCoreApplication::setApplicationName("SpendingTracker"); m_settings = new QSettings(QSettings::NativeFormat, QSettings::UserScope, "SpendingTrackerOrg", "SpendingTracker", this); } void SettingsManager::saveCurrency(const QString &code) { m_settings->setValue("currency/code", code); // 自动存入Windows注册表/HKEY_CURRENT_USER/... m_settings->setValue("currency/symbol", currencySymbol(code)); // macOS存入NSUserDefaults m_settings->sync(); // 立即写入磁盘,避免程序崩溃丢失 } QString SettingsManager::currencySymbol(const QString &code) { static QHash<QString, QString> symbols{{"CNY", "¥"}, {"USD", "$"}, {"EUR", "€"}}; return symbols.value(code, "¥"); }参数说明:
QSettings::NativeFormat让Windows走注册表(HKEY_CURRENT_USER\Software\SpendingTrackerOrg\SpendingTracker),macOS走NSUserDefaults,Linux走~/.config/SpendingTrackerOrg/SpendingTracker.conf,开发者无需判断平台。
5.2 QProcess调用wkhtmltopdf生成PDF报表:规避Qt WebEngine内存爆炸
统计页需导出PDF,但Qt6 WebEngine在Linux/macOS上占用内存超500MB。改用轻量方案:
// src/ui/ExportController.cpp bool ExportController::exportToPdf(const QString &htmlPath, const QString &pdfPath) { QProcess process; QStringList args; // wkhtmltopdf命令行参数(需提前安装wkhtmltopdf) args << "--page-size" << "A4" << "--margin-top" << "10" << "--margin-bottom" << "10" << htmlPath << pdfPath; // ⚠️ 关键:Linux/macOS需指定绝对路径,Windows可直接用"wkhtmltopdf" QString program = "wkhtmltopdf"; #ifdef Q_OS_LINUX program = "/usr/local/bin/wkhtmltopdf"; // Ubuntu默认路径 #elif defined(Q_OS_MACOS) program = "/usr/local/bin/wkhtmltopdf"; // Homebrew安装路径 #endif process.start(program, args); if (!process.waitForStarted()) { qWarning() << "wkhtmltopdf not found. Please install it."; return false; } if (!process.waitForFinished(30000)) { // 30秒超时 qWarning() << "wkhtmltopdf timeout"; return false; } return process.exitCode() == 0; }验证方法:
- 终端执行
wkhtmltopdf --version确认已安装; - 生成HTML报表时,用
QFile::copy(":/templates/statistics.html", tempHtmlPath)复制模板,再用QTextStream注入动态数据; - 导出后用
QDesktopServices::openUrl(QUrl::fromLocalFile(pdfPath))直接打开PDF。
5.3 一个硬核习惯:每次修改CMakeLists.txt后,强制清理构建缓存
这是我在某跨平台医疗设备项目中踩出的血泪经验——Qt6.5升级后,旧CMake缓存导致find_package(Qt6 REQUIRED COMPONENTS Charts)始终失败,但错误信息指向QSqlDatabase。最终发现是CMakeCache.txt中残留Qt5.15路径。从此我养成了雷打不动的习惯:
# 删除整个build目录(不是只删CMakeCache.txt!) rm -rf build/ mkdir build && cd build # 重新cmake,且必须带完整路径参数 cmake -DCMAKE_PREFIX_PATH=/opt/Qt6.5/gcc_64/lib/cmake .. make -j4从那以后我每次修改CMakeLists.txt或切换Qt版本,都强制走一遍
rm -rf build/ && mkdir build && cd build && cmake ...。省下的3小时debug时间,够我喝两杯咖啡。希望帮到你。
本文还有配套的精品资源,点击获取