C++与Qt5实战:从零构建桌面待办事项应用

发布时间:2026/7/24 4:57:46
C++与Qt5实战:从零构建桌面待办事项应用 1. 项目概述与核心价值最近在整理硬盘翻到了一个几年前自己边学边做的《C 与 Qt5 GUI 编程实战》开源项目教程。这个项目最初源于我自己的学习困境当时想从纯后端C转向带界面的桌面应用开发市面上资料要么是纯理论要么是零散的“Hello World”示例很难串联成一个完整的、能上手的项目。于是我决定自己动手用做一个真实可用的“个人待办事项管理工具”作为主线把C、Qt5的核心知识点像串珍珠一样串起来并全程开源。没想到这个项目在GitHub上获得了不少关注很多初学者反馈说这种“以战代练”的方式让他们真正理解了如何用C和Qt5构建一个现代化的GUI应用。这个教程项目本质上是一个从零到一的完整开发指南。它不满足于教你弹出一个窗口、放几个按钮而是带你经历一个真实桌面应用开发的全生命周期从项目创建、界面设计、业务逻辑实现到数据持久化、打包发布乃至一些进阶的调试和优化技巧。它的核心价值在于实战性和系统性。对于有C基础但没接触过GUI或者用过其他GUI框架想转战Qt的开发者来说这个项目提供了一个绝佳的“脚手架”。你不仅可以照着做更能理解每一步背后的设计考量和最佳实践最终获得独立开发类似应用的能力。项目里用到的技术栈非常经典且不过时C11/14作为核心语言Qt5作为GUI框架SQLite作为本地数据库CMake进行项目构建。这些都是工业界开发桌面应用尤其是对性能和原生体验有要求的应用时经久不衰的选择。2. 项目整体架构与设计思路2.1 为什么选择“待办事项”作为实战项目在构思教程载体时我考虑过很多点子比如计算器、文本编辑器、简易绘图板等。最终选择“待办事项管理工具”主要基于以下几点考量复杂度适中覆盖知识点全面一个完整的待办事项应用需要前端界面列表、输入框、按钮、菜单、后端业务逻辑增删改查、状态切换、数据持久化本地存储。它比计算器复杂能引入更多Qt控件和设计模式又比一个完整的办公软件简单确保教程焦点集中。贴近实际需求有实用价值做出来的东西自己真的能用这是持续学习的强大动力。一个功能完善的待办工具本身就是生产力工具。易于扩展和想象完成基础功能后学员可以很容易地为其添加新功能如分类标签、优先级设置、到期提醒、数据统计图表等这为自主学习提供了无限空间。2.2 技术选型背后的逻辑C11/14这是现代C开发的一个“甜蜜点”。auto关键字、范围for循环、智能指针std::shared_ptr,std::unique_ptr、Lambda表达式等特性能极大提升开发效率和代码安全性同时又不至于像C17/20那样在某些环境存在兼容性问题。教程中会刻意展示如何用现代C特性写出更简洁、更安全的Qt代码。Qt5选择Qt5而非Qt6主要是出于稳定性和生态成熟度的考虑。Qt5 LTS版本如5.15拥有极其庞大的用户群和丰富的第三方资源网络上几乎你遇到的任何问题都能找到解决方案。对于初学者避开新版本可能存在的微小兼容性陷阱能更平滑地入门。教程重点使用的模块包括Qt Widgets用于构建传统的桌面窗口应用是学习Qt GUI编程的基石。Qt Core信号与槽、容器类、文件IO、多线程等核心非GUI功能。Qt SQL用于连接和操作SQLite数据库。SQLite作为单机桌面应用的数据存储方案SQLite几乎是唯一选择。它无需安装数据库服务器一个文件就是整个数据库完美契合“个人待办事项”这种轻量级、本地化的场景。通过Qt SQL模块操作它也能学习到数据库编程的基本方法。CMake现代C项目的标准构建工具。相比于Qt自带的.pro文件qmakeCMake更通用、更强大也更容易与CI/CD、跨平台编译等现代开发流程集成。教程会从零开始编写CMakeLists.txt让你理解如何组织一个Qt项目。2.3 应用架构设计整个应用采用经典的Model-View架构这是Qt框架的核心设计模式之一非常适合数据展示类应用。Model模型负责管理数据。我们创建一个TodoModel类它内部封装了对SQLite数据库的所有操作增、删、改、查。同时为了让这个模型能直接与Qt的视图组件如QListView、QTableView绑定我们会让它继承自QAbstractTableModel。这样当模型中的数据发生变化时视图会自动更新。View视图负责展示数据。主界面使用QMainWindow包含菜单栏、工具栏、状态栏和一个中心部件。中心部件采用QWidget配合布局管理器如QVBoxLayout,QHBoxLayout来组织各个控件一个用于显示待办列表的QTableView一个用于输入新任务的QLineEdit以及“添加”、“删除”、“标记完成”等QPushButton。Controller控制器在Qt的MV模式中控制器角色通常由“信号与槽”机制和主窗口的业务逻辑代码共同承担。例如当用户点击“添加”按钮发出clicked()信号对应的槽函数会从输入框获取文本然后调用TodoModel的接口将数据插入数据库模型数据更新后表格视图自动刷新。这种架构的优势是清晰地将数据、界面和逻辑解耦。修改界面样式不影响数据层更换数据库比如未来想支持网络同步也只需修改模型内部实现界面和业务逻辑几乎不用动。注意很多初学者喜欢把所有代码都写在主窗口类里导致后期难以维护。本教程从一开始就强制进行模块化设计虽然前期稍显繁琐但对培养良好的工程习惯至关重要。3. 开发环境搭建与项目初始化3.1 工具链选择与安装工欲善其事必先利其器。一个顺手的开发环境能事半功倍。Qt 安装推荐方式从Qt官网下载在线安装器Qt Maintenance Tool。不建议下载整个离线安装包体积太大。组件选择在安装时务必勾选以下组件Qt 5.15.2(或最新的LTS版本) 下的MSVC 2019 64-bit如果你用Visual Studio或MinGW 8.1.0 64-bit如果你用MinGW。Qt Creator这是Qt官方IDE对Qt开发的支持最完善特别是其集成的UI设计器Qt Designer和调试工具。CMake如果系统没有可以在这里一并安装。关于 Qt WebKit在搜索热词中看到“qt5如何安装webkit”。需要明确Qt WebKit模块在Qt5.6之后已被标记为废弃并在后续版本中移除。Qt推荐使用新的Qt WebEngine模块基于Chromium来替代。如果你的项目必须使用WebKit可能需要寻找非常老的Qt版本或第三方移植但这会带来巨大的安全和兼容性风险。对于新项目强烈建议使用Qt WebEngine或避免内嵌浏览器需求。IDE/编辑器Qt Creator开箱即用与Qt框架无缝集成是学习和开发Qt应用的首选。其内置的UI设计器可以拖拽生成界面代码非常适合初学者理解控件和布局。Visual Studio如果你更熟悉VS生态可以安装“使用C的桌面开发”工作负载并在安装Qt时选择MSVC套件。之后需要安装“Qt VS Tools”扩展以便在VS中管理Qt项目。VSCode轻量灵活通过安装C扩展、CMake Tools扩展和Qt相关的插件如qt-vscode也能获得很好的开发体验。适合喜欢高度自定义的开发者。编译器根据你选择的Qt套件对应使用MSVC或MinGW。确保系统环境变量配置正确。3.2 创建第一个CMake Qt项目我们不用Qt Creator的向导而是手动创建以彻底理解项目结构。创建项目目录例如TodoApp。编写 CMakeLists.txt在TodoApp根目录下创建CMakeLists.txt这是项目的总蓝图。cmake_minimum_required(VERSION 3.16) project(TodoApp VERSION 1.0.0 LANGUAGES CXX) # 设置C标准 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 自动处理Qt的MOC、UIC、RCC set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) # 查找所需的Qt组件 find_package(Qt5 REQUIRED COMPONENTS Core Widgets Sql) # 添加可执行文件目标 add_executable(TodoApp src/main.cpp src/mainwindow.cpp src/mainwindow.h src/todomodel.cpp src/todomodel.h resources/resources.qrc ) # 链接Qt库到目标 target_link_libraries(TodoApp Qt5::Core Qt5::Widgets Qt5::Sql) # 设置可执行文件的输出目录可选 set_target_properties(TodoApp PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin )这段CMake脚本做了几件关键事指定C11标准启用Qt的元对象编译器MOC等自动化工具查找并链接Qt Core, Widgets, Sql三个模块定义了最终生成的可执行文件TodoApp及其源文件。创建基础源文件src/main.cpp程序入口创建并显示主窗口。#include QApplication #include mainwindow.h int main(int argc, char *argv[]) { QApplication app(argc, argv); // 每个Qt Widgets应用都需要一个QApplication对象 MainWindow window; window.show(); // 显示窗口 return app.exec(); // 进入主事件循环 }src/mainwindow.h/src/mainwindow.cpp主窗口类的声明和定义暂时留空或写个简单构造函数。src/todomodel.h/src/todomodel.cpp数据模型类暂时留空。resources/resources.qrcQt资源文件用于将图标、翻译文件等打包进可执行程序。构建与运行# 在项目根目录下 mkdir build cd build cmake .. -G MinGW Makefiles # 如果你用MinGW指定生成器。Windows下用MSVC可省略-G参数。 cmake --build . # 运行生成的可执行文件通常在 ./bin 或 ./Debug/Release 目录下实操心得第一次手动配置CMake可能会遇到各种问题比如找不到Qt、链接错误等。关键检查点1)find_package是否成功2) 环境变量PATH是否包含Qt的bin目录和编译器的bin目录3) 生成器-G是否与你的编译器匹配。耐心解决第一次以后就是复制粘贴了。4. 核心模块实现详解4.1 数据模型 (TodoModel) 的实现模型是应用的心脏它直接与数据库交互并为视图提供数据。数据库初始化 在TodoModel的构造函数中我们需要建立与SQLite数据库的连接并创建必要的表。// todomodel.cpp #include QSqlDatabase #include QSqlError #include QSqlQuery #include QStandardPaths #include QDir TodoModel::TodoModel(QObject *parent) : QAbstractTableModel(parent) { // 确定数据库文件路径通常放在用户数据目录 QString dataDir QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir dir(dataDir); if (!dir.exists()) { dir.mkpath(dataDir); } QString dbPath dataDir /todos.db; // 打开或创建数据库 QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); db.setDatabaseName(dbPath); if (!db.open()) { qCritical() Cannot open database: db.lastError(); return; } // 创建表如果不存在 QSqlQuery query; QString createTableSql R( CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, is_done BOOLEAN NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ); if (!query.exec(createTableSql)) { qCritical() Create table failed: query.lastError(); } // 可以在这里触发一个信号通知界面模型已就绪或者直接调用一个加载数据的方法 loadTodos(); }QStandardPaths用于获取跨平台的标准路径AppDataLocation在Windows上是AppData/Local在macOS上是Library/Application Support在Linux上是.local/share。实现 QAbstractTableModel 的虚函数 要让模型能在QTableView中工作必须实现以下几个核心虚函数// todomodel.h class TodoModel : public QAbstractTableModel { Q_OBJECT public: // ... 构造函数等其他成员 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; private: struct TodoItem { int id; QString title; QString description; bool isDone; QDateTime createdAt; QDateTime updatedAt; }; QVectorTodoItem m_todos; // 内存中缓存所有待办项 void loadTodos(); // 从数据库加载数据到m_todos };rowCount/columnCount告诉视图有多少行/列数据。我们可能设计为3列完成状态、标题、创建时间。data这是最重要的函数根据index行、列和role角色如显示文本、字体颜色、背景色等返回对应的数据。例如当role Qt::DisplayRole且column 1时返回m_todos[row].title。headerData提供表头信息。setData当用户在视图上编辑单元格如勾选完成状态时此函数被调用用于更新模型内部数据m_todos和底层数据库。flags返回每一项的标志例如Qt::ItemIsEditable表示该项可编辑。实现增删改查方法 在TodoModel中提供公共接口供控制器主窗口调用。bool TodoModel::addTodo(const QString title, const QString desc) { // 插入数据库 QSqlQuery query; query.prepare(INSERT INTO todos (title, description) VALUES (?, ?)); query.addBindValue(title); query.addBindValue(desc); if (!query.exec()) { qWarning() Insert failed: query.lastError(); return false; } // 获取新插入的ID int newId query.lastInsertId().toInt(); // 更新内存缓存 beginInsertRows(QModelIndex(), m_todos.size(), m_todos.size()); // 通知视图即将插入行 m_todos.append({newId, title, desc, false, QDateTime::currentDateTime(), QDateTime::currentDateTime()}); endInsertRows(); // 通知视图插入完成 return true; } bool TodoModel::removeTodo(int row) { if (row 0 || row m_todos.size()) return false; int id m_todos.at(row).id; QSqlQuery query; query.prepare(DELETE FROM todos WHERE id ?); query.addBindValue(id); if (!query.exec()) { qWarning() Delete failed: query.lastError(); return false; } beginRemoveRows(QModelIndex(), row, row); // 通知视图即将移除行 m_todos.removeAt(row); endRemoveRows(); return true; }关键点任何修改m_todos作为模型内部数据的操作都必须包裹在beginInsertRows/endInsertRows、beginRemoveRows/endRemoveRows或beginResetModel/endResetModel等通知函数中。这是Qt模型/视图框架的强制要求只有这样视图才能知道数据发生了变化并自动更新UI。4.2 用户界面 (MainWindow) 的设计与实现界面是用户直接交互的部分良好的设计能提升用户体验。使用 Qt Designer 设计界面在Qt Creator中可以右键项目-添加新文件-Qt-Qt Designer Form Class选择MainWindow模板创建mainwindow.ui文件。在Designer中拖拽控件一个QTableView一个QLineEdit几个QPushButton可能还有一个QTextEdit用于输入描述。使用布局管理器Layouts来排列控件确保窗口缩放时界面不会混乱。例如将输入框和按钮放在一个水平布局QHBoxLayout中再将这个水平布局和表格视图放在一个垂直布局QVBoxLayout里最后将这个垂直布局设置为中心部件的布局。为重要的控件设置一个清晰的objectName如tableView,titleLineEdit,addButton以便在代码中引用。在代码中关联界面与模型Qt Creator会自动生成ui_mainwindow.h文件其中包含一个Ui::MainWindow类它定义了界面上的所有控件指针。在MainWindow的构造函数中我们需要设置UI并将模型与视图关联。// mainwindow.cpp #include ui_mainwindow.h #include todomodel.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_model(new TodoModel(this)) // 创建数据模型 { ui-setupUi(this); // 初始化UI这是关键一步 // 将模型设置给表格视图 ui-tableView-setModel(m_model); // 可以设置表格的列宽、选择行为等 ui-tableView-setSelectionBehavior(QAbstractItemView::SelectRows); ui-tableView-horizontalHeader()-setStretchLastSection(true); // 连接信号与槽 connect(ui-addButton, QPushButton::clicked, this, MainWindow::onAddButtonClicked); connect(ui-deleteButton, QPushButton::clicked, this, MainWindow::onDeleteButtonClicked); connect(ui-tableView, QTableView::doubleClicked, this, MainWindow::onTodoDoubleClicked); }实现业务逻辑槽函数void MainWindow::onAddButtonClicked() { QString title ui-titleLineEdit-text().trimmed(); if (title.isEmpty()) { QMessageBox::warning(this, tr(Warning), tr(Title cannot be empty!)); return; } QString desc ui-descTextEdit-toPlainText(); if (m_model-addTodo(title, desc)) { ui-titleLineEdit-clear(); ui-descTextEdit-clear(); ui-titleLineEdit-setFocus(); // 清空输入框并聚焦方便连续输入 } else { QMessageBox::critical(this, tr(Error), tr(Failed to add new todo.)); } } void MainWindow::onDeleteButtonClicked() { QModelIndexList selected ui-tableView-selectionModel()-selectedRows(); if (selected.isEmpty()) { QMessageBox::information(this, tr(Info), tr(Please select a row to delete.)); return; } // 从后往前删除避免索引变化导致的问题 for (int i selected.count() - 1; i 0; --i) { int row selected.at(i).row(); m_model-removeTodo(row); } }4.3 数据持久化与高级特性数据库事务对于批量删除或修改操作使用事务可以保证数据一致性并提升性能。bool TodoModel::removeTodos(const QVectorint rows) { QSqlDatabase db QSqlDatabase::database(); // 获取当前数据库连接 if (!db.transaction()) { // 开始事务 qWarning() Start transaction failed; return false; } bool allSuccess true; // ... 循环执行删除操作 if (allSuccess) { if (!db.commit()) { // 提交事务 qWarning() Commit transaction failed; allSuccess false; } } else { db.rollback(); // 回滚事务 } return allSuccess; }自定义委托 (Delegate)如果你想让“完成状态”这一列显示为复选框而不是文本“true/false”就需要为这一列设置一个QStyledItemDelegate的子类重写它的paint和editorEvent方法。这属于Qt模型/视图框架的进阶内容教程中会详细演示如何实现一个简单的复选框委托。样式表 (QSS)Qt支持使用类似CSS的样式表来美化界面。你可以为整个应用或单个控件设置样式。// 在主窗口构造函数中 this-setStyleSheet(QMainWindow { background-color: #f0f0f0; } QTableView { alternate-background-color: #e8e8e8; } QPushButton { background-color: #4CAF50; color: white; border-radius: 4px; padding: 5px; } QPushButton:hover { background-color: #45a049; });5. 项目构建、调试与发布5.1 使用CMake进行高效构建我们已经在CMakeLists.txt中配置了基础构建。随着项目变大你可能需要添加子目录将模型、视图、工具类等分到不同的CMakeLists.txt中使用add_subdirectory引入。管理资源文件确保resources.qrc被正确包含并使用qt5_add_resources命令如果使用CMake 3.16且find_package了Qt5CMAKE_AUTORCC已自动处理。处理翻译文件 (.ts)使用qt5_add_translation命令来生成和发布多语言支持。5.2 调试技巧与常见问题排查使用 qDebug() 和 qCritical()这是Qt中最简单的日志输出方式在调试时非常有用。记得在发布版本中可以通过定义QT_NO_DEBUG_OUTPUT来关闭它们。善用 Qt Creator 的调试器可以直观地查看Qt特有的对象如QString的内容、QList的元素等。内存管理虽然使用了现代C智能指针但Qt有自己的父子对象内存管理机制。将QObject派生类的子对象指定父对象通常在构造函数中传入this当父对象销毁时会自动销毁所有子对象。注意不要混合使用智能指针和Qt的父子机制管理同一个对象这会导致双重释放。信号与槽连接失败这是最常见的问题之一。检查点发送者和接收者是否都是QObject的派生类或其子类。槽函数的签名是否与信号匹配参数类型、数量、const修饰符。在C11及以上推荐使用新式语法connect(sender, Sender::signal, receiver, Receiver::slot)它能在编译时检查类型安全。如果使用QML连接方式不同但本教程不涉及QML。5.3 打包发布可执行文件开发完成后你需要将应用分发给没有开发环境的用户。Qt应用发布需要携带相关的运行时库DLL。找到依赖的DLLWindows (MinGW)在Qt安装目录的bin文件夹下如C:\Qt\5.15.2\mingw81_64\bin找到Qt5Core.dll,Qt5Widgets.dll,Qt5Gui.dll,Qt5Sql.dll等以及libgcc_s_seh-1.dll,libstdc-6.dll,libwinpthread-1.dll等编译器运行时库。Windows (MSVC)同样在Qt的bin目录下找对应的DLL此外还需要VC Redistributable运行时库msvcp140.dll,vcruntime140.dll等用户可能需要单独安装或你一并打包。使用 windeployqt 工具 (Windows)这是Qt官方提供的部署工具能自动扫描你的exe文件找出所有需要的Qt库并复制到目标文件夹。# 在构建目录下假设你的exe在 ./bin/TodoApp.exe cd /path/to/your/build/bin windeployqt TodoApp.exe执行后当前目录下会多出许多Qt的DLL和必要的文件夹如platforms。创建发布包将TodoApp.exe、windeployqt复制过来的所有文件、你自己的数据库文件或让程序在首次运行时创建以及可能需要的其他资源如图标、配置文件一起打包。可以使用Inno Setup或NSIS等工具制作安装程序。macOS 和 Linux过程类似macOS使用macdeployqtLinux通常需要指定库的安装路径或使用AppImage等打包格式。避坑指南发布后最常见的错误是“无法找到Qt平台插件”或程序启动后崩溃。99%的原因都是必要的DLL或插件文件夹特别是platforms文件夹下的qwindows.dll没有正确打包。务必确保发布目录的结构与开发环境中运行时的结构一致。6. 从Qt5到未来进阶学习路径完成这个基础项目后你已经掌握了使用C和Qt5开发桌面应用的核心技能。但Qt的世界远不止于此。你可以基于这个项目尝试以下方向进行深化和扩展界面美化与自定义控件深入学习QSS制作更精美的界面。继承QWidget或现有控件绘制完全自定义的控件重写paintEvent。多线程与异步如果待办事项需要处理网络请求如同步到云端或耗时计算必须使用多线程避免阻塞UI主线程导致界面卡顿。学习QThread、QtConcurrent以及信号槽的跨线程连接类型Qt::ConnectionType。插件化架构设想你的待办事项工具可以支持不同的存储后端本地SQLite、网络API。可以设计一个插件接口通过动态库DLL/dylib/so的方式加载不同的存储插件。探索 Qt Quick / QML对于需要更炫酷、更动态界面的应用Qt QuickQML是更好的选择。它是一种声明式语言类似于JSON专门用于构建流畅的现代UI。你可以考虑用QML重写前端而C作为后端逻辑。跨平台移动端Qt不仅支持桌面Windows, macOS, Linux也支持移动平台iOS, Android。虽然移动端开发更推荐使用Qt Quick但了解如何为移动设备调整UI布局和交互逻辑是很有价值的技能。现代化C特性融合在模型类中尝试使用std::optional、std::variant使用更安全的容器算法探索将部分逻辑用std::function和Lambda进行重构让代码更符合现代C风格。这个《C 与 Qt5 GUI 编程实战》项目就像一把钥匙为你打开了桌面应用开发的大门。它提供的不是一堆零散的知识点而是一个完整的、可运行的、可扩展的代码骨架。我始终认为编程最好的学习方式就是动手去做一个东西在解决一个个具体问题的过程中那些抽象的概念和API才会变得鲜活起来。希望你在跟随这个教程实践时不仅能复现出一个待办事项工具更能真正理解其背后的设计思想并最终有能力创造出属于自己的、更复杂的桌面应用。