C++与Qt实战:从零开发桌面记账软件“海豚记账本”

发布时间:2026/7/30 10:56:08

C++与Qt实战:从零开发桌面记账软件“海豚记账本” 1. 项目概述从“海豚记账本”看C桌面应用开发的价值最近在技术社区里看到不少朋友在讨论C的“八股文”面试题或者纠结于如何配置VSCode的C环境。这让我想起自己多年前的一个老项目——一个用纯C和Qt框架开发的桌面记账软件我给它起名叫“海豚记账本”。今天我想把这个项目的完整实现思路和核心代码拿出来聊聊这或许比单纯刷题更能让你理解C在构建真实、可用桌面应用时的魅力。“海豚记账本”这个名字听起来可能有点“古早味”但它背后涉及的技术栈恰恰是C在图形用户界面GUI和本地数据管理领域的经典组合。这个项目本质上是一个单机版的个人财务管理工具核心功能包括账目的增删改查、按类别/时间统计、数据可视化比如简单的图表展示以及数据文件的导入导出。选择C和Qt来实现主要基于几个考量一是C的运行时效率极高对于频繁进行数据计算和筛选的记账操作响应迅速二是Qt框架提供了极其丰富且成熟的GUI组件能构建出专业、美观的桌面界面三是整个应用可以编译成独立的可执行文件无需依赖复杂的运行时环境如.NET Framework或Java虚拟机真正做到“开箱即用”。对于正在学习C的朋友来说这样一个项目比写一个控制台下的“学生管理系统”要有趣和实用得多。它能让你接触到事件驱动编程、模型-视图Model-View架构、文件I/O操作、SQLite数据库集成、简单的图表绘制等核心知识点。更重要的是你能看到一个完整的、有界面的应用程序是如何从零搭建起来的这对于理解软件工程的整体流程至关重要。接下来我会分模块拆解这个项目的设计与实现并提供可直接编译运行的代码片段。2. 技术选型与项目架构设计在动手写第一行代码之前明确技术选型和架构是成功的一半。对于“海豚记账本”这类数据管理型的桌面应用一个清晰的分层架构能极大提升代码的可维护性和可扩展性。2.1 为什么选择Qt框架市面上C的GUI库不少像MFC、wxWidgets等。我最终选择Qt原因很直接信号与槽Signals Slots机制这是Qt的核心特性它提供了一种类型安全、松耦合的对象间通信方式。比如当用户点击“添加”按钮时按钮会发出一个clicked()信号而处理添加业务的槽函数会自动被调用。这种机制比传统的回调函数清晰得多也避免了复杂的父子窗口消息传递。全面的工具链Qt不仅仅是一个库它提供了一整套工具。Qt Creator是一个优秀的跨平台IDE对Qt项目支持极佳qmake或CMake用于构建Qt Designer允许你通过拖拽的方式设计界面生成的.ui文件可以被直接集成到代码中实现界面与逻辑的分离。丰富的内置功能除了GUI控件Qt还提供了网络、数据库、XML/JSON解析、图表Qt Charts、多媒体等模块。这意味着我们不需要到处寻找第三方库在Qt的生态内就能完成绝大多数功能减少了依赖管理的麻烦。出色的跨平台能力一套代码稍作调整即可编译运行在Windows、macOS、Linux上。这对于希望作品被更多用户使用的开发者来说吸引力巨大。注意Qt有两个主要的许可版本开源版GPL/LGPL和商业版。对于个人学习和开源项目使用开源版完全没问题。在下载安装时务必从官网选择正确的版本。2.2 项目整体架构设计我采用了经典的三层架构并融入Qt特有的Model-View模式进行优化。海豚记账本架构 ├── 表示层 (Presentation Layer) │ ├── MainWindow (主窗口) │ ├── DialogAddRecord (添加记录对话框) │ ├── DialogChartView (图表查看对话框) │ └── ... (其他UI界面) │ ├── 业务逻辑层 (Business Logic Layer) │ ├── AccountBookManager (账本管理核心类) │ ├── Record (账目记录数据模型) │ ├── CategoryManager (类别管理) │ └── ... (其他业务逻辑) │ └── 数据访问层 (Data Access Layer) ├── DatabaseManager (数据库封装类) └── (文件导入/导出处理器)表示层由各个窗口QMainWindow和对话框QDialog构成负责接收用户输入和展示数据。这一层应该尽可能“薄”只处理界面交互不包含核心业务逻辑。业务逻辑层这是应用的大脑。AccountBookManager作为总控制器协调所有业务操作。Record类是一个简单的数据容器POD结构代表一条记账记录包含日期、类型收入/支出、类别、金额、备注等字段。CategoryManager负责管理用户自定义的收支类别。数据访问层负责与持久化存储打交道。我们使用SQLite作为本地数据库因为它轻量、无需服务器、单个文件即可管理。DatabaseManager类封装了所有SQL操作向上层提供简洁的API如addRecord(const Record record)、getRecordsByDateRange(...)等实现了数据层与业务层的解耦。模型-视图Model-View的应用在Qt中显示列表或表格数据最佳实践是使用Model-View模式。我们将创建一个继承自QAbstractTableModel的自定义模型类例如RecordTableModel。这个模型内部持有或连接到一个记录列表或直接与数据库交互。然后在主界面的QTableView控件上设置这个模型。这样当模型中的数据发生变化时比如新增了一条记录视图会自动更新我们无需手动去操作表格的每一行每一列。这种设计极大地简化了UI与数据的同步工作。3. 核心模块实现与代码解析有了架构蓝图我们就可以开始动手实现核心模块了。这里我会聚焦于几个关键部分并提供详细的代码说明。3.1 数据模型与数据库设计首先定义我们的核心数据结构Record。// record.h #ifndef RECORD_H #define RECORD_H #include QString #include QDate #include QMetaType // 用于使Record类型可用于Qt的信号槽 // 收支类型枚举 enum class RecordType { Income, Expense }; class Record { public: Record() default; Record(int id, const QDate date, RecordType type, const QString category, double amount, const QString note QString()); // Getter 和 Setter int id() const { return m_id; } QDate date() const { return m_date; } RecordType type() const { return m_type; } QString category() const { return m_category; } double amount() const { return m_amount; } QString note() const { return m_note; } // 为了方便也可以提供设置器如果业务需要修改记录 void setDate(const QDate date) { m_date date; } // ... 其他setter private: int m_id -1; // 数据库自增ID-1表示新记录 QDate m_date; RecordType m_type RecordType::Expense; QString m_category; double m_amount 0.0; QString m_note; }; // 注册Record类型到Qt元对象系统使其能用于信号槽传递如果需要 Q_DECLARE_METATYPE(Record) #endif // RECORD_H接下来是数据库管理类DatabaseManager。我们使用Qt自带的QSqlDatabase来操作SQLite。// databasemanager.h #pragma once #include QSqlDatabase #include QVector #include record.h class DatabaseManager { public: static DatabaseManager instance(); bool initialize(const QString databasePath accountbook.db); // 记录操作 bool addRecord(const Record record); bool deleteRecord(int recordId); bool updateRecord(const Record record); QVectorRecord getAllRecords() const; QVectorRecord getRecordsByDateRange(const QDate start, const QDate end) const; // 类别操作、统计查询等... private: DatabaseManager() default; // 单例模式 ~DatabaseManager(); DatabaseManager(const DatabaseManager) delete; DatabaseManager operator(const DatabaseManager) delete; bool createTables(); // 创建数据库表 QSqlDatabase m_database; };// databasemanager.cpp #include databasemanager.h #include QSqlQuery #include QSqlError #include QDebug #include QStandardPaths #include QDir DatabaseManager DatabaseManager::instance() { static DatabaseManager instance; return instance; } bool DatabaseManager::initialize(const QString databasePath) { // 构建数据库文件的完整路径通常放在用户数据目录 QString path QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir dir(path); if (!dir.exists()) { dir.mkpath(.); } path dir.filePath(databasePath); m_database QSqlDatabase::addDatabase(QSQLITE); m_database.setDatabaseName(path); if (!m_database.open()) { qCritical() 无法打开数据库: m_database.lastError().text(); return false; } return createTables(); } bool DatabaseManager::createTables() { QSqlQuery query; // 创建记录表 QString sql R( CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, type INTEGER NOT NULL, -- 0: Income, 1: Expense category TEXT NOT NULL, amount REAL NOT NULL, note TEXT ); ); if (!query.exec(sql)) { qCritical() 创建records表失败: query.lastError().text(); return false; } // 创建类别表示例 sql R( CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, type INTEGER NOT NULL -- 关联收支类型 ); ); if (!query.exec(sql)) { qCritical() 创建categories表失败: query.lastError().text(); return false; } // 可以在这里插入一些默认类别如“餐饮”、“交通”、“工资”等 return true; } bool DatabaseManager::addRecord(const Record record) { QSqlQuery query; query.prepare(INSERT INTO records (date, type, category, amount, note) VALUES (:date, :type, :category, :amount, :note)); query.bindValue(:date, record.date().toString(Qt::ISODate)); query.bindValue(:type, static_castint(record.type())); query.bindValue(:category, record.category()); query.bindValue(:amount, record.amount()); query.bindValue(:note, record.note()); if (!query.exec()) { qCritical() 添加记录失败: query.lastError().text(); return false; } return true; } // 其他函数实现类似使用预编译SQL语句防止注入并做好错误处理。实操心得数据库操作一定要做好错误处理qCritical()或qDebug()输出错误日志。SQLite的IF NOT EXISTS是个好习惯避免重复创建表。将数据库文件放在QStandardPaths::AppDataLocation目录下是跨平台保存用户数据的标准做法。3.2 自定义表格模型QAbstractTableModel这是连接数据和UI视图的桥梁。我们创建一个模型来管理Record列表并在表格中显示。// recordtablemodel.h #pragma once #include QAbstractTableModel #include QVector #include record.h class RecordTableModel : public QAbstractTableModel { Q_OBJECT public: explicit RecordTableModel(QObject* parent nullptr); // 必须重写的接口 int rowCount(const QModelIndex parent QModelIndex()) const override; int columnCount(const QModelIndex parent QModelIndex()) const override; QVariant data(const QModelIndex index, int role Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role Qt::DisplayRole) const override; // 可选支持编辑 bool setData(const QModelIndex index, const QVariant value, int role Qt::EditRole) override; Qt::ItemFlags flags(const QModelIndex index) const override; // 自定义方法加载数据、添加/删除记录 void loadRecords(const QVectorRecord records); bool addRecord(const Record record); bool removeRecord(int row); const Record recordAt(int row) const; private: QVectorRecord m_records; };// recordtablemodel.cpp #include recordtablemodel.h #include QBrush RecordTableModel::RecordTableModel(QObject* parent) : QAbstractTableModel(parent) {} int RecordTableModel::rowCount(const QModelIndex parent) const { if (parent.isValid()) return 0; // 对于平面列表父索引有效时返回0 return m_records.size(); } int RecordTableModel::columnCount(const QModelIndex parent) const { Q_UNUSED(parent); // 定义显示的列日期、类型、类别、金额、备注 return 5; } QVariant RecordTableModel::data(const QModelIndex index, int role) const { if (!index.isValid() || index.row() m_records.size()) return QVariant(); const Record record m_records.at(index.row()); if (role Qt::DisplayRole || role Qt::EditRole) { switch (index.column()) { case 0: return record.date().toString(yyyy-MM-dd); case 1: return (record.type() RecordType::Income) ? 收入 : 支出; case 2: return record.category(); case 3: return QString::number(record.amount(), f, 2); case 4: return record.note(); default: return QVariant(); } } // 可以添加其他角色如Qt::TextAlignmentRole设置对齐Qt::ForegroundRole设置颜色如支出红色收入绿色 else if (role Qt::ForegroundRole) { if (index.column() 3) { // 金额列 return (record.type() RecordType::Income) ? QBrush(Qt::darkGreen) : QBrush(Qt::red); } } return QVariant(); } QVariant RecordTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role ! Qt::DisplayRole) return QVariant(); if (orientation Qt::Horizontal) { static const QString headers[] { 日期, 类型, 类别, 金额, 备注 }; if (section 0 section 5) return headers[section]; } return QVariant(); } void RecordTableModel::loadRecords(const QVectorRecord records) { beginResetModel(); // 通知视图模型即将发生巨大变化 m_records records; endResetModel(); // 通知视图模型重置完成请刷新 } bool RecordTableModel::addRecord(const Record record) { // 在模型末尾插入 int row m_records.size(); beginInsertRows(QModelIndex(), row, row); // 通知视图即将插入行 m_records.append(record); endInsertRows(); // 通知视图插入完成 // 这里通常也会调用DatabaseManager::instance().addRecord(record)持久化到数据库 return true; } const Record RecordTableModel::recordAt(int row) const { Q_ASSERT(row 0 row m_records.size()); return m_records.at(row); }3.3 主界面与业务逻辑整合主窗口MainWindow使用Qt Designer设计界面生成mainwindow.ui文件包含菜单栏、工具栏、一个QTableView用于显示记录以及一些过滤控件如日期选择器、类别下拉框。在MainWindow的类中我们需要初始化数据库和模型。将模型设置给表格视图。连接按钮信号到对应的槽函数实现增删改查。// mainwindow.cpp 部分关键代码 #include mainwindow.h #include ui_mainwindow.h // 由uic工具从.ui文件生成 #include databasemanager.h #include recordtablemodel.h #include dialogaddrecord.h #include QMessageBox MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_model(new RecordTableModel(this)) { ui-setupUi(this); // 1. 初始化数据库 if (!DatabaseManager::instance().initialize()) { QMessageBox::critical(this, 错误, 数据库初始化失败应用无法启动。); QCoreApplication::quit(); return; } // 2. 设置模型到视图 ui-tableView-setModel(m_model); ui-tableView-horizontalHeader()-setStretchLastSection(true); // 最后一列填充 ui-tableView-setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选择 // 3. 加载初始数据 refreshTableView(); // 4. 连接信号与槽 connect(ui-actionAdd, QAction::triggered, this, MainWindow::onAddRecord); connect(ui-actionDelete, QAction::triggered, this, MainWindow::onDeleteRecord); connect(ui-btnRefresh, QPushButton::clicked, this, MainWindow::refreshTableView); // ... 连接其他控件 } void MainWindow::refreshTableView() { auto records DatabaseManager::instance().getAllRecords(); // 或根据过滤条件查询 m_model-loadRecords(records); // 更新统计信息如总收入、总支出、结余 updateStatistics(records); } void MainWindow::onAddRecord() { DialogAddRecord dialog(this); if (dialog.exec() QDialog::Accepted) { Record newRecord dialog.getRecord(); // 从对话框获取用户输入的记录 if (DatabaseManager::instance().addRecord(newRecord)) { m_model-addRecord(newRecord); // 更新模型和视图 refreshTableView(); // 或者直接调用模型的方法这里简单刷新 } else { QMessageBox::warning(this, 警告, 添加记录失败); } } } void MainWindow::onDeleteRecord() { QModelIndexList selected ui-tableView-selectionModel()-selectedRows(); if (selected.isEmpty()) { QMessageBox::information(this, 提示, 请先选择要删除的记录。); return; } // 获取选中的行号对于排序/过滤后的视图需要使用映射 int sourceRow m_model-mapToSource(selected.first()).row(); const Record recordToDelete m_model-recordAt(sourceRow); if (QMessageBox::question(this, 确认删除, QString(确定要删除【%1】这条记录吗).arg(recordToDelete.note().isEmpty()? QString::number(recordToDelete.amount()) : recordToDelete.note()), QMessageBox::Yes | QMessageBox::No) QMessageBox::Yes) { if (DatabaseManager::instance().deleteRecord(recordToDelete.id())) { m_model-removeRecord(sourceRow); refreshTableView(); } } }DialogAddRecord是一个自定义对话框里面放置了QDateEdit、QComboBox、QDoubleSpinBox、QLineEdit等控件供用户输入。点击“确定”后收集这些控件的值构造一个Record对象返回。4. 进阶功能实现数据统计与图表展示一个记账本如果只能记录和查看列表那就太基础了。数据可视化能直观反映消费趋势。我们可以利用Qt Charts模块来绘制简单的饼图显示各类别支出占比和柱状图显示月度收支对比。4.1 集成Qt Charts模块首先需要在项目文件.pro中添加charts模块QT core gui sql charts # 添加charts然后在统计对话框或主窗口的某个标签页中创建图表视图。4.2 绘制类别支出饼图// 假设在某个函数中我们获取了支出类别的统计数据 // QMapQString, double categoryAmountMap; // 类别 - 金额 #include QtCharts/QPieSeries #include QtCharts/QPieSlice #include QtCharts/QChartView QT_CHARTS_USE_NAMESPACE // 使用Qt Charts命名空间 void generateCategoryPieChart(const QMapQString, double data) { QPieSeries *series new QPieSeries(); for (auto it data.constBegin(); it ! data.constEnd(); it) { QString label QString(%1\n%2元).arg(it.key()).arg(it.value(), 0, f, 2); QPieSlice *slice series-append(label, it.value()); slice-setLabelVisible(); // 显示标签 // 可以自定义颜色等 } QChart *chart new QChart(); chart-addSeries(series); chart-setTitle(支出类别占比); chart-legend()-setAlignment(Qt::AlignRight); // 图例靠右 QChartView *chartView new QChartView(chart); chartView-setRenderHint(QPainter::Antialiasing); // 抗锯齿 // 将chartView添加到你的对话框布局中 // ui-chartLayout-addWidget(chartView); }4.3 绘制月度收支柱状图柱状图稍微复杂一点需要按月份聚合收入与支出数据。#include QtCharts/QBarSeries #include QtCharts/QBarSet #include QtCharts/QBarCategoryAxis #include QtCharts/QValueAxis void generateMonthlyBarChart(const QVectorRecord records) { // 假设我们已经将records按月份聚合成了两个列表months, incomeList, expenseList // QStringList months {2024-01, 2024-02, ...}; // QVectordouble incomePerMonth, expensePerMonth; QBarSet *setIncome new QBarSet(收入); QBarSet *setExpense new QBarSet(支出); for (int i 0; i months.size(); i) { *setIncome incomePerMonth[i]; *setExpense expensePerMonth[i]; } QBarSeries *series new QBarSeries(); series-append(setIncome); series-append(setExpense); QChart *chart new QChart(); chart-addSeries(series); chart-setTitle(月度收支对比); chart-setAnimationOptions(QChart::SeriesAnimations); QBarCategoryAxis *axisX new QBarCategoryAxis(); axisX-append(months); chart-addAxis(axisX, Qt::AlignBottom); series-attachAxis(axisX); QValueAxis *axisY new QValueAxis(); axisY-setLabelFormat(%.0f元); chart-addAxis(axisY, Qt::AlignLeft); series-attachAxis(axisY); chart-legend()-setVisible(true); chart-legend()-setAlignment(Qt::AlignBottom); QChartView *chartView new QChartView(chart); chartView-setRenderHint(QPainter::Antialiasing); // 添加到UI }注意事项Qt Charts模块在商业使用上需要注意许可协议。对于开源项目使用LGPL版本的Qt即可。另外图表的数据处理聚合、统计最好放在一个单独的StatisticsCalculator类中保持业务逻辑清晰。5. 项目构建、打包与部署代码写完了如何把它变成一个可以分发给别人使用的软件5.1 使用CMake构建项目现代Qt项目推荐使用CMake进行构建管理。一个基本的CMakeLists.txt如下cmake_minimum_required(VERSION 3.16) project(DolphinAccountBook VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) # 自动处理Qt的moc set(CMAKE_AUTOUIC ON) # 自动处理ui文件 set(CMAKE_AUTORCC ON) # 自动处理资源文件 find_package(Qt6 COMPONENTS Core Gui Widgets Sql Charts REQUIRED) # 查找Qt6组件 # 如果你的Qt是5则用 find_package(Qt5 ...) set(SOURCES src/main.cpp src/mainwindow.cpp src/databasemanager.cpp src/recordtablemodel.cpp src/record.cpp src/dialogaddrecord.cpp # ... 添加所有.cpp文件 ) set(HEADERS include/mainwindow.h include/databasemanager.h include/recordtablemodel.h include/record.h include/dialogaddrecord.h # ... 添加所有.h文件 ) set(UI_FORMS ui/mainwindow.ui ui/dialogaddrecord.ui # ... 添加所有.ui文件 ) # 如果有资源文件(.qrc) set(RESOURCES resources/resources.qrc ) add_executable(${PROJECT_NAME} WIN32 ${SOURCES} ${HEADERS} ${UI_FORMS} ${RESOURCES} ) target_include_directories(${PROJECT_NAME} PRIVATE include) target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Sql Qt6::Charts ) # 在Windows下可设置应用程序图标 if(WIN32) set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE TRUE # RC_ICONS ${CMAKE_CURRENT_SOURCE_DIR}/app.ico ) endif()在项目根目录下执行以下命令进行构建mkdir build cd build cmake .. -G MinGW Makefiles -DCMAKE_PREFIX_PATHC:/Qt/6.5.0/mingw_64 # 指定你的Qt路径 cmake --build .5.2 使用windeployqtWindows或macdeployqtmacOS打包Qt提供了一个便捷的工具可以自动将应用依赖的DLLWindows或框架macOS复制到可执行文件旁边。对于WindowsMinGW在Qt安装目录的bin文件夹下找到windeployqt.exe例如C:\Qt\6.5.0\mingw_64\bin\windeployqt.exe。在build目录或你的可执行文件所在目录打开命令行运行windeployqt.exe DolphinAccountBook.exe它会扫描exe文件将所需的Qt库、插件等全部拷贝到当前目录。你还需要手动拷贝mingw的运行时DLL如libgcc_s_seh-1.dlllibstdc-6.dlllibwinpthread-1.dll。它们通常在C:\Qt\Tools\mingw1120_64\bin或类似路径下。现在整个文件夹就可以压缩分发给其他Windows用户了他们无需安装Qt。对于macOS 使用macdeployqt它会将应用打包成.appbundle并处理好依赖。macdeployqt DolphinAccountBook.app踩坑记录windeployqt有时会漏掉某些插件尤其是数据库驱动qsqlite.dll和图像格式插件。如果运行时提示找不到驱动可以手动从Qt安装目录/plugins下的sqldrivers、imageformats等文件夹复制对应的dll到可执行文件目录下的plugins子文件夹中。一个更稳妥的办法是在代码中显式添加插件路径QCoreApplication::addLibraryPath(./plugins);。6. 开发中常见问题与调试技巧在开发“海豚记账本”这类项目时我遇到了不少典型问题这里总结一下希望能帮你避坑。6.1 数据库连接与驱动问题问题程序启动时报错“QSQLITE driver not loaded”。原因Qt默认可能没有编译SQLite驱动或者驱动文件没有被打包。解决确保项目文件.pro中包含了QT sql。打包时确认plugins/sqldrivers目录下的qsqlite.dllWindows或libqsqlite.dylibmacOS已存在。可以使用QCoreApplication::libraryPaths()打印查看插件搜索路径。在main函数中可以在创建QApplication对象后强制加载驱动仅用于调试#include QSqlDatabase qDebug() Available drivers: QSqlDatabase::drivers(); // 输出应该包含 QSQLITE6.2 模型/视图数据不同步问题在RecordTableModel中直接修改了m_records数据但表格视图没有刷新。原因没有按照Qt的规则通知视图。直接修改底层数据视图是不知道的。解决任何会改变模型所管理数据结构的操作增、删、改都必须放在beginInsertRows()/endInsertRows()、beginRemoveRows()/endRemoveRows()、beginResetModel()/endResetModel()或dataChanged()信号之间。对于单个单元格数据的修改应在setData()中发出dataChanged(index, index)信号。6.3 界面布局在调整大小时错乱问题窗口拉大后控件没有按预期拉伸或保持比例。原因没有正确使用布局管理器QLayout。解决绝对避免使用setGeometry硬编码控件位置和大小。在Qt Designer中熟练使用水平布局、垂直布局、网格布局和间隔器。为顶级窗口部件如QDialog设置一个总布局。利用大小策略sizePolicy和最小/最大大小属性来微调控件行为。复杂界面可以考虑使用QTabWidget、QSplitter或QStackedWidget进行组织。6.4 内存管理父对象机制Qt使用对象树来管理内存。当一个QObject派生类对象被创建时指定了父对象parent它会在父对象被销毁时自动销毁。最佳实践在堆上创建带有界面的部件如QWidget时通常指定父对象。例如在MainWindow的构造函数中创建DialogAddRecord对话框可以传this作为父对象。注意对于模型如RecordTableModel或一些工具类如果父对象是QObject派生类也可以利用此机制。否则需要自己管理其生命周期或在合适的时机delete。6.5 使用qDebug()进行高效调试在开发阶段不要吝啬使用qDebug()输出关键变量和流程信息。#include QDebug ... qDebug() 正在添加记录日期 record.date() 金额 record.amount(); qDebug() 查询SQL: query.lastQuery(); qDebug() 错误信息: query.lastError().text();在Qt Creator中这些信息会输出到“应用程序输出”面板非常方便。对于更复杂的调试可以使用Qt Creator内置的调试器设置断点单步执行。开发这样一个完整的C桌面应用从设计到实现再到打包是一个系统工程。它强迫你去思考架构、数据流、用户体验和错误处理这些经验远比写算法题来得全面。当你看到自己写的程序能稳定运行帮助管理财务时那种成就感是无可替代的。希望“海豚记账本”这个案例能为你打开一扇窗看到C在GUI应用开发领域的扎实与稳健。

相关新闻