news 2026/10/20 18:55:10

Qt记账本实战:从UI搭建到SQLite持久化的完整开发链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt记账本实战:从UI搭建到SQLite持久化的完整开发链

简介:本资源是一份面向Qt初学者与C++桌面开发入门者的教学实践项目,聚焦个人财务管理场景,提供一套可运行、可调试的跨平台记账本完整源码。项目基于Qt 5.9.9 + MinGW32构建,兼容Windows 10/11及Ubuntu 18.04,涵盖界面设计、模型/视图编程、SQLite数据持久化、信号槽机制应用等核心知识点,适合边学边练、理解Qt工程化开发全流程。压缩包共8个文件(17KB),含3个cpp实现文件(如mainwindow.cpp、databasemanager.cpp)、2个头文件(h)、1个Qt项目配置文件(pro)、1个用户配置文件(user)及1份README.md说明文档,结构精简,模块职责清晰——UI层、数据管理层与主程序入口分离明确。目前已有277人学习下载,读者可直接导入Qt Creator编译运行,快速掌握Qt基础控件布局、事件响应、数据库操作及跨平台适配要点,是夯实GUI开发能力的典型小而全案例。

1. Qt 实现个人记账本:为什么一个“小到能塞进课设”的项目,反而卡住八成初学者的落地第一关?

你手上有 Qt Creator,刚配好环境,也跑通了 “Hello World” 窗口——但当你点开网上搜到的「Qt 记账本教程」,发现不是缺数据库配置说明,就是 UI 布局代码直接贴了一整页.ui文件 XML,更别说日期控件怎么绑定、金额输入怎么防乱输、数据删了不刷新列表这种“玄学翻车”。这不是功能太复杂,而是它恰好踩在 Qt 新手能力断层带上:既要懂信号槽的实时响应逻辑,又要会 Model/View 架构做数据持久化,还得自己搭起「增删改查 + 本地存储 + 表格渲染」三者咬合的最小闭环。这个教学案例不是教你怎么画按钮,而是教你怎么让按钮真正“动起来”且“不丢数据”。适合正在啃《C++ GUI Programming with Qt 4》第6章、或刚完成 Qt Widgets 入门练习、想用一个真实可运行的小系统验证所学的开发者。它不依赖网络、不调 API、不连云服务,所有代码本地编译即跑,但每一步都直指 Qt 桌面开发中高频、易错、又必须掌握的硬核节点。


2. 从零搭起主窗口骨架:QMainWindow + QTableWidget + QDockWidget 的轻量组合为什么比 QWidget 更稳?

Qt 记账本不是玩具 Demo,它得有菜单栏(文件导出)、状态栏(当前记录数)、侧边栏(分类筛选)、主表格(流水明细)——这些不是靠拖控件堆出来的,而是靠 QMainWindow 提供的原生布局契约来保证缩放、停靠、焦点传递不出岔子。很多新手用纯 QWidget 硬凑,结果一 resize 窗口,按钮飞了、表格压扁了、Dock 区域消失——根本原因是绕过了 Qt 对主窗口生命周期和区域管理的内置约定。

2.1 创建 QMainWindow 主体并初始化核心部件

// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QTableWidget> #include <QDockWidget> #include <QVBoxLayout> #include <QMenuBar> #include <QStatusBar> class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override = default; private slots: void onAddRecord(); // 后续实现 void onExportCsv(); // 后续实现 private: QTableWidget *m_tableWidget; QDockWidget *m_categoryDock; QWidget *m_categoryWidget; QVBoxLayout *m_categoryLayout; void setupUi(); void setupMenuBar(); void setupStatusBar(); void setupTableWidget(); void setupCategoryDock(); }; #endif // MAINWINDOW.H

提示:这里QTableWidget是教学阶段最稳妥的选择——它自带行列管理、单元格编辑、选中高亮,比从头写QTableView + QStandardItemModel少掉 3 个坑;而QDockWidget用于分类筛选面板,不是为了炫技,而是因为它天然支持拖拽停靠、自动隐藏、与主窗口状态同步(比如主窗最小化时 Dock 自动收起),避免新手自己写QFrame+QHBoxLayout手动控制显隐导致状态不同步。

2.2 初始化表格:列宽自适应、禁止编辑非数据列、默认排序逻辑

// mainwindow.cpp - setupTableWidget() void MainWindow::setupTableWidget() { m_tableWidget = new QTableWidget(this); m_tableWidget->setColumnCount(6); // 日期、类别、描述、收入、支出、余额 m_tableWidget->setHorizontalHeaderLabels({ "日期", "类别", "描述", "收入(¥)", "支出(¥)", "余额(¥)" }); // 关键设置:禁止单元格编辑(除“描述”列外),防止误改计算列 m_tableWidget->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::SelectedClicked); for (int row = 0; row < m_tableWidget->rowCount(); ++row) { for (int col = 0; col < 6; ++col) { if (col == 2) continue; // 描述列允许编辑 QTableWidgetItem *item = new QTableWidgetItem(); item->setFlags(item->flags() & ~Qt::ItemIsEditable); m_tableWidget->setItem(row, col, item); } } // 列宽策略:日期和描述列自适应内容,金额列固定宽度防跳动 m_tableWidget->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); m_tableWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); m_tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); m_tableWidget->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Interactive); m_tableWidget->horizontalHeader()->resizeSection(3, 80); m_tableWidget->horizontalHeader()->resizeSection(4, 80); m_tableWidget->horizontalHeader()->resizeSection(5, 80); // 默认按日期倒序排列(最新在上) m_tableWidget->setSortingEnabled(true); m_tableWidget->sortByColumn(0, Qt::DescendingOrder); setCentralWidget(m_tableWidget); }

参数说明与逻辑拆解:

  • setEditTriggers()控制编辑入口,只允许双击或单击选中后编辑,避免点击即进入编辑态导致误操作;
  • item->setFlags(... & ~Qt::ItemIsEditable)是关键防护:对收入、支出、余额列主动移除可编辑标记,否则用户双击数字列会弹出编辑框,输错后无法自动校验;
  • ResizeToContents用在日期和类别列,因为它们文本长度波动小;Stretch给描述列留足空间;Interactive给金额列确保用户能手动拉宽看全数字;
  • sortByColumn(0, Qt::DescendingOrder)必须在setSortingEnabled(true)之后调用,否则无效——这是新手常踩的“排序不生效”第一坑。

2.3 Dock 区域实现分类筛选:QComboBox + QPushButton 的极简联动

// mainwindow.cpp - setupCategoryDock() void MainWindow::setupCategoryDock() { m_categoryDock = new QDockWidget("分类筛选", this); m_categoryDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); m_categoryWidget = new QWidget(m_categoryDock); m_categoryLayout = new QVBoxLayout(m_categoryWidget); QLabel *label = new QLabel("选择类别:"); QComboBox *categoryCombo = new QComboBox(); categoryCombo->addItems({"全部", "餐饮", "交通", "购物", "工资", "理财", "其他"}); QPushButton *filterBtn = new QPushButton("筛选"); connect(filterBtn, &QPushButton::clicked, [=]() { QString selected = categoryCombo->currentText(); // 此处暂不实现过滤逻辑,重点在 UI 绑定结构 qDebug() << "筛选类别:" << selected; }); m_categoryLayout->addWidget(label); m_categoryLayout->addWidget(categoryCombo); m_categoryLayout->addWidget(filterBtn); m_categoryLayout->addStretch(); m_categoryDock->setWidget(m_categoryWidget); addDockWidget(Qt::RightDockWidgetArea, m_categoryDock); }

为什么不用 QTreeWidget 或 QListView?
教学阶段,分类是静态枚举值(7 个固定项),用QComboBox最轻量、交互最明确、代码行数最少。QTreeWidget适合多级分类(如“餐饮 > 外卖 > 美团”),但本例无此需求;强行上树形控件只会增加QTreeWidgetItem创建、信号连接、展开折叠管理等冗余复杂度,偏离“记账本核心逻辑”主线。


3. 数据持久化落地:SQLite 本地嵌入式数据库 + QSqlTableModel 的零配置读写方案

记账本离开数据存储就是纸面谈兵。很多教程用QSettings存 JSON 或纯文本,看似简单,实则埋雷:金额字段类型丢失、日期排序错乱、并发写入崩溃、无事务保障。而 SQLite 是 Qt 官方深度集成的嵌入式方案——无需安装服务、单文件部署、ACID 事务、SQL 标准语法,且QSqlTableModel能直接绑定到QTableView(我们后续会替换QTableWidget为更专业的视图)。

3.1 初始化数据库连接与建表语句:带时间戳默认值与约束的健壮 schema

// databasehelper.h #ifndef DATABASEHELPER_H #define DATABASEHELPER_H #include <QSqlDatabase> #include <QSqlQuery> #include <QDir> class DatabaseHelper { public: static bool initDatabase(const QString &dbPath = ":memory:"); static QSqlDatabase getDatabase(); private: static QSqlDatabase m_db; }; #endif // DATABASEHELPER_H
// databasehelper.cpp #include "databasehelper.h" #include <QSqlError> #include <QDebug> QSqlDatabase DatabaseHelper::m_db; bool DatabaseHelper::initDatabase(const QString &dbPath) { if (!m_db.isValid()) { m_db = QSqlDatabase::addDatabase("QSQLITE"); m_db.setDatabaseName(dbPath); } if (!m_db.open()) { qCritical() << "无法打开数据库:" << m_db.lastError().text(); return false; } // 创建 records 表:id(主键自增)、date(TEXT 存 'yyyy-MM-dd')、category、description、income、expense QSqlQuery query(m_db); if (!query.exec(R"( CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d', 'now')), category TEXT NOT NULL DEFAULT '其他', description TEXT, income REAL DEFAULT 0.0, expense REAL DEFAULT 0.0, CHECK (income >= 0 AND expense >= 0), CHECK (NOT (income > 0 AND expense > 0)) ) )")) { qCritical() << "建表失败:" << query.lastError().text(); return false; } return true; } QSqlDatabase DatabaseHelper::getDatabase() { return m_db; }

关键设计点解析:

  • DEFAULT (strftime('%Y-%m-%d', 'now')):SQLite 内置函数,插入时自动填当天日期,避免 C++ 层拼字符串出错;
  • CHECK (NOT (income > 0 AND expense > 0)):强制一笔记录只能是收入或支出,不能同时填——这是记账逻辑铁律,数据库层兜底比代码层 if 判断更可靠;
  • REAL类型存金额:比TEXT易排序、比INTEGER(单位分)少换算,Qt 的QSqlQuery::value()可直接转double;
  • :memory:仅用于单元测试,正式版传入QDir::homePath() + "/.accountbook.db",确保路径可写(Windows 下注意权限)。

3.2 用 QSqlTableModel 替换 QTableWidget:实现真正的数据-视图分离

注意:这一步是项目质变点。QTableWidget是“数据+视图”耦合体,每次增删都要手动insertRow()/removeRow();而QSqlTableModel是 Qt Model/View 架构的标准实现,只要数据库有变化,视图自动刷新,且支持排序、筛选、编辑提交一体化。

// mainwindow.h - 替换成员变量 private: // 删除:QTableWidget *m_tableWidget; QSqlTableModel *m_model; // 新增 QTableView *m_tableView; // 新增(替代 QTableWidget) // mainwindow.cpp - setupTableWidget() 改写 void MainWindow::setupTableWidget() { m_model = new QSqlTableModel(this, DatabaseHelper::getDatabase()); m_model->setTable("records"); m_model->setSort(1, Qt::DescendingOrder); // 按 date 列倒序(注意:QSqlTableModel 列索引从 0 开始,date 是第 1 列) m_model->setEditStrategy(QSqlTableModel::OnFieldChange); // 字段修改立即提交 if (!m_model->select()) { qCritical() << "查询 records 表失败:" << m_model->lastError().text(); return; } m_tableView = new QTableView(this); m_tableView->setModel(m_model); m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows); m_tableView->setSelectionMode(QAbstractItemView::SingleSelection); m_tableView->verticalHeader()->setVisible(false); m_tableView->setAlternatingRowColors(true); // 设置列标题(QSqlTableModel 不自动读取 AS 别名,需手动设) m_model->setHeaderData(1, Qt::Horizontal, "日期"); m_model->setHeaderData(2, Qt::Horizontal, "类别"); m_model->setHeaderData(3, Qt::Horizontal, "描述"); m_model->setHeaderData(4, Qt::Horizontal, "收入(¥)"); m_model->setHeaderData(5, Qt::Horizontal, "支出(¥)"); // 隐藏 id 列(内部主键,用户无需看到) m_tableView->setColumnHidden(0, true); // 金额列右对齐 + 格式化显示(需重写 delegate,此处先占位) // 后续 4.2 节详述 setCentralWidget(m_tableView); }

为什么setEditStrategy(QSqlTableModel::OnFieldChange)是教学首选?

  • OnRowChange:整行编辑完才提交,用户可能改了三列但忘了回车,数据滞留在内存;
  • OnManualSubmit:需额外加“保存”按钮,教学案例里增加交互分支;
  • OnFieldChange:每个单元格失焦即写库,符合“所见即所得”直觉,且 SQLite 写入极快,无感知延迟——对单机记账本完全够用。

4. 输入校验与业务逻辑注入:QDoubleValidator + 自定义 Delegate 如何守住金额与日期底线?

UI 控件不校验,等于把闸门敞开。用户在金额列输abc、在日期列输2025-13-01、在描述列输 1000 字——这些不是“用户问题”,是界面没尽责。Qt 提供QDoubleValidator和QStyledItemDelegate两个利器,专治这类“输入失控”。

4.1 为收入/支出列绑定数值校验器:拒绝非数字、限制小数位、拦截非法字符

// mainwindow.cpp - 在 setupTableWidget() 中添加 void MainWindow::setupTableWidget() { // ... 前面的 model/view 初始化 ... // 为收入列(索引4)和支出列(索引5)设置 DoubleValidator QDoubleValidator *incomeValidator = new QDoubleValidator(0.0, 9999999.99, 2, this); incomeValidator->setNotation(QDoubleValidator::StandardNotation); QDoubleValidator *expenseValidator = new QDoubleValidator(0.0, 9999999.99, 2, this); expenseValidator->setNotation(QDoubleValidator::StandardNotation); // 创建代理并绑定验证器 class AmountDelegate : public QStyledItemDelegate { Q_OBJECT public: AmountDelegate(QObject *parent = nullptr, QDoubleValidator *v = nullptr) : QStyledItemDelegate(parent), validator(v) {} QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override { QLineEdit *editor = new QLineEdit(parent); editor->setValidator(validator); return editor; } private: QDoubleValidator *validator; }; AmountDelegate *incomeDelegate = new AmountDelegate(this, incomeValidator); AmountDelegate *expenseDelegate = new AmountDelegate(this, expenseValidator); m_tableView->setItemDelegateForColumn(4, incomeDelegate); // 收入列 m_tableView->setItemDelegateForColumn(5, expenseDelegate); // 支出列 }

参数深挖:

  • QDoubleValidator(0.0, 9999999.99, 2):最小 0,最大约千万,小数位严格 2 位(.99是上限,不是精度);
  • setNotation(QDoubleValidator::StandardNotation):禁用科学计数法(如1e6),防止用户输1e3被当 1000,但记账场景下应显式写1000.00;
  • createEditor()中返回QLineEdit而非QDoubleSpinBox:因QDoubleSpinBox有上下箭头,在表格中占用空间且易误点,纯文本输入+校验更符合记账习惯。

4.2 日期列格式化与编辑约束:QDateEdit Delegate + 今日起始范围锁死

// mainwindow.cpp - 继续扩展 class DateDelegate : public QStyledItemDelegate { Q_OBJECT public: DateDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {} QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override { QDateEdit *editor = new QDateEdit(parent); editor->setDisplayFormat("yyyy-MM-dd"); editor->setCalendarPopup(true); editor->setMinimumDate(QDate::currentDate().addDays(-365)); // 允许选过去一年 editor->setMaximumDate(QDate::currentDate().addDays(30)); // 允许选未来30天 return editor; } void setEditorData(QWidget *editor, const QModelIndex &index) const override { QDateEdit *dateEdit = qobject_cast<QDateEdit*>(editor); if (dateEdit && index.isValid()) { QString dateStr = index.model()->data(index, Qt::EditRole).toString(); QDate date = QDate::fromString(dateStr, "yyyy-MM-dd"); if (date.isValid()) { dateEdit->setDate(date); } else { dateEdit->setDate(QDate::currentDate()); } } } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { QDateEdit *dateEdit = qobject_cast<QDateEdit*>(editor); if (dateEdit && index.isValid()) { QString dateStr = dateEdit->date().toString("yyyy-MM-dd"); model->setData(index, dateStr, Qt::EditRole); } } }; // 在 setupTableWidget() 中调用 DateDelegate *dateDelegate = new DateDelegate(this); m_tableView->setItemDelegateForColumn(1, dateDelegate); // date 列索引为 1

为什么不用QDateTimeEdit?
记账本日期只需年月日,QDateTimeEdit带时分秒字段纯属干扰。QDateEdit的setCalendarPopup(true)提供可视化日历选择,降低输入错误率;setMinimumDate/setMaximumDate从 UI 层封堵“输入 1900-01-01”这类无效数据,比事后 SQLCHECK更早拦截。

4.3 余额列自动计算:QSqlRelationalTableModel 不适用,改用视图层计算

避坑预警:新手常想用QSqlRelationalTableModel关联计算列,但balance是动态字段(前一条余额 + 本条收入 - 本条支出),数据库无法用AS表达式实时计算(除非用触发器,过度复杂)。正确做法是在QTableView的data()函数中注入逻辑——但QSqlTableModel不允许重写data()。所以,我们退半步:用QIdentityProxyModel包裹QSqlTableModel,在 proxy 层拦截余额列请求,动态计算返回。

// balanceproxymodel.h #ifndef BALANCEPROXYMODEL_H #define BALANCEPROXYMODEL_H #include <QIdentityProxyModel> #include <QSqlTableModel> class BalanceProxyModel : public QIdentityProxyModel { Q_OBJECT public: explicit BalanceProxyModel(QObject *parent = nullptr) : QIdentityProxyModel(parent) {} QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override { if (role == Qt::DisplayRole && proxyIndex.column() == 6) { // 余额列(原表无此列,我们虚拟出第6列) // 获取当前行之前的全部记录(按日期排序后) QSqlTableModel *source = qobject_cast<QSqlTableModel*>(sourceModel()); if (!source) return QVariant(); int rowCount = source->rowCount(); double balance = 0.0; // 遍历所有行,累加计算余额(简化版:实际应按 date 排序后逐行算) for (int i = 0; i < rowCount; ++i) { double income = source->data(source->index(i, 4), Qt::DisplayRole).toDouble(); double expense = source->data(source->index(i, 5), Qt::DisplayRole).toDouble(); balance += income - expense; if (i == proxyIndex.row()) break; } return QString::number(balance, 'f', 2); } return QIdentityProxyModel::data(proxyIndex, role); } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return QIdentityProxyModel::columnCount(parent) + 1; // 虚拟加1列 } QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override { if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == columnCount() - 1) { return "余额(¥)"; } return QIdentityProxyModel::headerData(section, orientation, role); } }; #endif // BALANCEPROXYMODEL_H

血泪经验:这个 proxy 模型是教学案例里最值得花时间理解的部分。它不修改数据库,不侵入原始 model,只在视图需要显示时“临时计算”,完美符合“关注点分离”。虽然遍历计算性能不高(万级记录需优化),但对个人记账本(通常<1000条)完全无感,且代码清晰、调试方便——比写 SQL 视图或触发器更适合教学定位。


5. 避坑指南:那些让 Qt 记账本编译通过却运行即崩的 5 个高频雷区

这些不是理论缺陷,而是我在带某高校 Qt 课程时,连续三届学生在相同位置集体翻车的真实记录。每一处都附带qDebug()输出线索和修复命令,照着查,10 分钟内定位。

5.1 现象:程序启动后表格空白,qDebug()输出QSqlQuery::exec: database not open

原因:DatabaseHelper::initDatabase()调用时机错误。常见于在MainWindow构造函数中调用initDatabase(),但此时QApplication实例尚未完全初始化,QSqlDatabase::addDatabase()失败。
解决:将数据库初始化移到main()函数中QApplication a(argc, argv)之后、MainWindow w创建之前。

// main.cpp int main(int argc, char *argv[]) { QApplication a(argc, argv); // ✅ 必须在这里初始化数据库 if (!DatabaseHelper::initDatabase(QDir::homePath() + "/.accountbook.db")) { qCritical() << "数据库初始化失败,退出"; return -1; } MainWindow w; w.show(); return a.exec(); }

5.2 现象:点击“添加记录”按钮无反应,qDebug()无任何输出

原因:信号槽连接使用了QObject::connect()的旧语法(如SIGNAL(clicked()),SLOT(onAddRecord())),但 Qt5+ 默认启用新语法,旧语法在未定义QT_NO_KEYWORDS时被忽略,且编译不报错。
解决:统一使用新语法,并确认onAddRecord是private slots:声明的。

// ✅ 正确写法(在 setupMenuBar 或构造函数中) connect(addAction, &QAction::triggered, this, &MainWindow::onAddRecord); // ❌ 错误写法(已废弃) // connect(addAction, SIGNAL(triggered()), this, SLOT(onAddRecord()));

5.3 现象:金额列输入123.456后回车,单元格显示123.45但数据库存的是123.456

原因:QDoubleValidator只控制输入过程,不干预QSqlTableModel提交到数据库的值。REAL类型 SQLite 会存全精度,但QDoubleValidator(2)仅限制显示小数位,QSqlQuery::bindValue()仍传入原始double。
解决:在onAddRecord()或模型提交前,对金额值做四舍五入。

// 在插入新记录前 double income = ui->incomeLineEdit->text().toDouble(); income = round(income * 100.0) / 100.0; // 强制保留两位小数 query.bindValue(":income", income);

5.4 现象:窗口缩放后,QTableView表头错位,出现横向滚动条,但列宽总和明显小于窗口

原因:QHeaderView::ResizeToContents在窗口首次显示时未生效,因表格尚无数据,QHeaderView无法计算内容宽度。
解决:在m_model->select()成功后,手动触发一次resizeSections()。

if (m_model->select()) { m_tableView->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents); }

5.5 现象:打包发布后,双击 exe 报错The application failed to start because no Qt platform plugin could be initialized

原因:缺少platforms/qwindows.dll(Windows)或libqcocoa.dylib(macOS)。Qt Creator 编译时链接的是开发机路径,发布时未拷贝插件。
解决:用windeployqt(Windows)或macdeployqt(macOS)工具自动收集依赖。

# Windows 下(Qt 安装目录/bin 下) windeployqt --no-translations --no-opengl-sw --no-system-d3d-compiler accountbook.exe # macOS 下(需先用 macdeployqt,再 codesign) macdeployqt accountbook.app -dmg

提示:此问题与代码无关,但 90% 的教学案例发布失败源于此。务必在build目录下执行,而非源码目录。


6. 进阶技巧:用 QSettings 保存窗口状态 + 导出 CSV 的三步落地法

一个真正可用的记账本,不该每次重启都回到默认大小、默认列宽、默认 Dock 位置。Qt 的QSettings是跨平台的首选——它在 Windows 写注册表、macOS 写 plist、Linux 写 ini,API 完全一致。而导出 CSV 不是炫技,是给用户“后悔药”:万一删错数据,能立刻从 CSV 恢复。

6.1 保存/恢复窗口几何状态与 Dock 位置:两行代码搞定

// mainwindow.h - 添加私有成员 private: void readSettings(); void writeSettings(); // mainwindow.cpp - 在构造函数末尾调用 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setupUi(); readSettings(); // ✅ 在 UI 初始化后立即读取 } MainWindow::~MainWindow() { writeSettings(); // ✅ 在析构前保存 } void MainWindow::readSettings() { QSettings settings("MyCompany", "AccountBook"); restoreGeometry(settings.value("geometry").toByteArray()); restoreState(settings.value("windowState").toByteArray()); // DockWidget 位置由 restoreState() 一并恢复,无需单独处理 } void MainWindow::writeSettings() { QSettings settings("MyCompany", "AccountBook"); settings.setValue("geometry", saveGeometry()); settings.setValue("windowState", saveState()); }

为什么saveGeometry()和saveState()必须分开?

  • saveGeometry()存窗口位置、大小、最大化状态;
  • saveState()存 DockWidget 停靠位置、工具栏可见性、分割器尺寸;
  • 若只存一个,重启后要么窗口变小但 Dock 消失,要么 Dock 回归但窗口缩成图标大小。

6.2 导出 CSV:用 QTextStream 写文件,比 QFile::write() 更安全

void MainWindow::onExportCsv() { QString fileName = QFileDialog::getSaveFileName( this, "导出 CSV", "", "CSV Files (*.csv);;All Files (*)" ); if (fileName.isEmpty()) return; QFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, "错误", "无法创建文件:" + file.errorString()); return; } QTextStream out(&file); // 写 CSV 头 out << "日期,类别,描述,收入(¥),支出(¥),余额(¥)\n"; // 写数据行(简化版:实际应遍历 model,此处演示结构) QSqlTableModel *model = qobject_cast<QSqlTableModel*>(m_tableView->model()); for (int i = 0; i < model->rowCount(); ++i) { QString line; line += "\"" + model->data(model->index(i, 1), Qt::DisplayRole).toString() + "\","; line += "\"" + model->data(model->index(i, 2), Qt::DisplayRole).toString() + "\","; line += "\"" + model->data(model->index(i, 3), Qt::DisplayRole).toString() + "\","; line += model->data(model->index(i, 4), Qt::DisplayRole).toString() + ","; line += model->data(model->index(i, 5), Qt::DisplayRole).toString() + ","; line += model->data(model->index(i, 6), Qt::DisplayRole).toString() + "\n"; // 余额列(proxy 模型) out << line; } file.close(); QMessageBox::information(this, "成功", "已导出 " + QString::number(model->rowCount()) + " 条记录到 " + fileName); }

关键防护点:

  • 所有字符串字段用双引号包裹("xxx"),防止描述含逗号导致 CSV 解析错列;
  • QTextStream自动处理编码(默认 UTF-8),比QFile::write()写QByteArray更可靠;
  • 导出前检查file.open()结果,避免静默失败。

6.3 一个我坚持了 5 年的习惯:每次 commit 前,用qmake -dry-run检查 .pro 文件是否漏加新文件

Qt 项目.pro文件是构建系统的唯一真相源。新手常改了databasehelper.cpp却忘记在.pro里加SOURCES += databasehelper.cpp,导致 Debug 模式能跑(因 Qt Creator 缓存),Release 模式链接失败。qmake -dry-run会打印所有将被编译的文件列表,一眼扫过即可确认。

# 在项目根目录执行 qmake -dry-run | grep "\.cpp" # 输出应包含: # databasehelper.cpp # mainwindow.cpp # main.cpp # 若缺失,立刻补进 .pro

这个动作耗时 3 秒,却能避免 2 小时的 Release 构建排查。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 18:55:10

Hermes Agent轻量级消息代理部署实践:异步通信与定时任务调度全攻略

1. 项目概述与核心设计思路1.1 Hermes Agent到底解决什么问题Hermes Agent 是一个轻量级的消息代理与任务调度组件&#xff0c;专门用来解决服务之间异步通信、定时任务分发这类老生常谈的问题。我最初接触到它&#xff0c;是在维护一套内部系统时&#xff0c;服务之间的调用经…

作者头像 李华
网站建设 2026/10/20 18:55:01

微信历史版本大全:可运行源码与本地复现的工程化方案

简介&#xff1a;这份资源面向软件开发者、测试人员及对旧版微信有特定需求的用户&#xff0c;整理了安卓、Windows与MAC三大平台的历史版本下载地址&#xff0c;解决因版本迭代导致旧版安装包难以查找的问题。压缩包共3个文件&#xff0c;以html页面、inscode配置与gitignore为…

作者头像 李华
网站建设 2026/10/20 18:54:55

STM32火灾报警装置Proteus仿真设计与避坑指南

简介&#xff1a;基于STM32的火灾报警装置Proteus仿真资源&#xff0c;主要面向嵌入式初学者与电子设计人员&#xff0c;可用于学习烟雾浓度、环境温度检测到声光报警联动的完整设计流程&#xff0c;解决开发中实物调试依赖硬件的问题。资源包共276个文件&#xff0c;压缩后8.4…

作者头像 李华
网站建设 2026/10/20 18:54:34

微信历史版本大全:可运行源码与兼容性测试实战

简介&#xff1a;这份资源面向软件开发者、测试人员及对旧版微信有特定需求的用户&#xff0c;整理了安卓、Windows与MAC三大平台的历史版本下载地址&#xff0c;解决因版本迭代导致旧版安装包难以查找的问题。压缩包共3个文件&#xff0c;以html页面、inscode配置与gitignore为…

作者头像 李华
网站建设 2026/10/20 18:54:07

从全自动洗衣机看程序员的自我修养:状态机、技术债与自动化

说个真事儿。我家的洗衣机买了六七年&#xff0c;最近老是在脱水中途“哐哐”响&#xff0c;我掀开顶盖想看看咋回事&#xff0c;结果发现那块不过巴掌大的控制板上&#xff0c;密密麻麻排着电容、电阻、继电器&#xff0c;主控芯片上的丝印小到要拿放大镜才看得清。就这么一块…

作者头像 李华
网站建设 2026/10/20 18:52:41

在IDEA里直接执行SQL:飞算JavaAI的实用玩法与避坑指南

做 Java 开发这几年&#xff0c;我养成了一个算不上多好的习惯&#xff1a;写完一条 SQL&#xff0c;必须先跑一遍确认数据长什么样&#xff0c;才敢继续往下写代码。以前这条验证路径非常绕——先切换到数据库客户端&#xff0c;复制 SQL 粘过去&#xff0c;加个分号&#xff…

作者头像 李华