简介面向Qt初学者的个人日程管理桌面应用完整项目基于C/Qt Widgets实现事件记录、任务分类、日期选择、定时提醒等核心功能。项目采用模型视图架构与本地数据存储包含登录注册、日程增删改查、日历联动、待办列表、今日/明日/已完成视图等模块适合学习Qt界面开发与业务逻辑整合的开发者参考。资源共89个文件以cpp源文件、h头文件、ui界面文件为主另含pro工程配置、sln解决方案、exe可执行程序、pdb调试符号及dat数据文件压缩包约10.83MB可在Windows环境中直接打开工程并运行编译结果。目前已有468人学习下载。通过完整源码可掌握QCalendarWidget日历集成、QTableView列表展示、QTimer定时提醒、QSS美化等关键技巧并借助内置数据文件与清晰目录结构快速理解个人事务处理应用的实现思路便于二次开发、课程设计或直接部署使用。 老实说“个人日程安排处理事务”这个项目我一开始并不打算自己写。市面上的 Todo 软件不少但要么数据在云端要么对提醒粒度、展示方式限制太多。恰好 Qt 一直是我做桌面工具的首选框架于是我用它做了个小日程管理工具按日历查看、录入待办、到点提醒、数据本地保存。功能不算多但解决了我的实际问题——不用联网也能用数据文件就在自己手边。这个项目也很适合刚开始接触 Qt 桌面开发的人控件、信号槽、SQLite、界面设计、打包发布这些关键点全过了一遍做完收获很大。接下来说说完整的设计和实现过程包括踩过的坑。1. 项目整体设计先别急着写代码1.1 核心需求拆解个人日程工具听起来简单但需求一旦不收敛很容易做成一个“什么都有但什么都不好用”的怪物。我一开始列了十几个功能最终砍到四个核心模块日程的增删改查、按时提醒、数据持久化、按日期查看与检索。剩下的“月视图统计、云同步、多设备共享”全部放到二期再说。这四个模块其实对应着 Qt 里几块非常典型的技术增删改查对应界面控件与数据模型的交互提醒对应 QTimer 和系统托盘数据持久化对应 SQLite 或文件读写按日期查看对应 QCalendarWidget 与模型过滤。把这几个点吃透不只这个项目后面做任何 Qt 工具类应用都能复用。边界划定以后我还给自己定了一条原则所有操作都应该在 2 次鼠标点击内完成。比如双击日程直接弹出编辑框右键菜单直接提供删除和完成选项顶部按钮只放“新增”和“搜索”。越小的工具交互路径越短越好用。1.2 技术选型Qt Widgets 不是老古董而是最稳的方案很多新手会在 Qt Widgets 和 QML 之间纠结网上也总有人说 QML 是未来、Widgets 要淘汰。对这个“本地日程管理”场景我反而建议用 Qt Widgets。原因是它的控件体系非常成熟QCalendarWidget、QTableView、QDialog 都是现成的模型视图框架配合 QStandardItemModel 写起来很顺手做增删改查几乎不需要额外封装。QML 的优势在复杂动画、触摸交互和嵌入式界面做日程工具属于杀鸡用牛刀还得额外维护 QML 文件和 JavaScript 逻辑项目结构反而更重。如果后面想做成手机 App再考虑 Qt Quick 也不迟。版本选择上我用了 Qt 5.15.2 LTS这个版本稳定网上的资料也最全。Qt 6 当然也可以用但如果你要继承很多旧代码或者考虑某些老 Linux 系统的兼容性5.15.2 依然是稳妥的选择。构建工具我推荐 CMake而不是 qmake虽然 Qt Creator 对 qmake 支持得很好但 CMake 是当前跨平台 C 项目的主流后续想接 CI 或转到 VS Code 开发也更方便。1.3 为什么不用现成 Todo 应用非要自己写这个问题几乎每个朋友都问过我。现成的日程管理软件确实多但“个人日程安排处理事务”这个需求里真正难的是符合自己的使用习惯。我要的是本地离线数据、自由的字段扩展、双击就能改、不强制登录这些在商业软件里要么订阅付费要么被数据绑定。用 Qt 自己写数据格式完全可控还能顺便把 SQLite、信号槽、线程处理这些桌面开发基本功练扎实。另外还有一层考虑个人工具最大的成本不是开发而是“改需求”。自己写代码今天想加一个“提前两小时提醒”的字段改几行就完事用别人的软件可能等一年都等不到这个功能。这个项目做完后我自己最深的体会是小工具的关键不是功能多少而是能不能被快速改造。2. 核心实现界面布局与交互流程2.1 项目骨架与 CMake 配置搭建项目时我采用了清晰的分层结构把界面逻辑、数据库操作、数据模型分开这样不会在后续加功能时把整个 main.cpp 堆成几百行。QtSchedule/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── MainWindow.h │ ├── MainWindow.cpp │ ├── ScheduleItem.h │ ├── ScheduleDatabase.h │ └── ScheduleDatabase.cpp └── resources/ └── icons/CMake 配置里我特意开启了 UTF-8 编译选项否则在 Windows 上 MSVC 编译器会把中文字符串按本地代码页解析导致界面乱码。代码是这样写的cmake_minimum_required(VERSION 3.16) project(QtSchedule VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt5 COMPONENTS Widgets Sql REQUIRED) add_executable(QtSchedule src/main.cpp src/MainWindow.cpp src/ScheduleDatabase.cpp ) target_link_libraries(QtSchedule PRIVATE Qt5::Widgets Qt5::Sql) if(MSVC) target_compile_options(QtSchedule PRIVATE /utf-8) endif()如果你用的是 Qt 6把 find_package 和 target_link_libraries 里的 Qt5 改成 Qt6 就行其余逻辑一致。main.cpp 里只需要创建 QApplication、设置应用名称和图标、打开主窗口这里不在开头做太多初始化避免启动一闪而过。2.2 主窗口布局日历、列表、详情三区联动主界面我采用了经典的“左日历、右列表、下方详情”的结构。左侧是一个 QCalendarWidget用户选择日期后会触发 selectionChanged 信号右侧是一个 QTableView展示选中日期的所有日程最下方用一个 QPlainTextEdit 显示当前选中日程的备注。三者联动是典型的 Qt 信号槽用法。| QCalendarWidget | QTableView | | | (日程列表) | | | 双击 - 编辑对话框 | --------------------------------------- | QPlainTextEdit (日程详情预览) | ---------------------------------------列表和日历联动时过滤逻辑放在 QSortFilterProxyModel 里是最省事的。我最初直接在刷新函数里遍历所有数据然后重新构建 QStandardItemModel数据量小的时候没问题但到了上百条日程以后每次切换日期都会卡一下。后来改成 QSortFilterProxyModel 的子类重写 filterAcceptsRow按 QDate 匹配界面切换就顺畅多了。bool ScheduleFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex sourceParent) const { QModelIndex index sourceModel()-index(sourceRow, 0, sourceParent); QDateTime startTime index.data(Qt::UserRole 1).toDateTime(); return startTime.date() m_currentDate; }这里的重点是不要在数据行里直接塞显示字符串而是把完整的 QDateTime 对象放到 Qt::UserRole 里。显示和排序都可以用同一套数据后面做统计也方便。这是我做这个项目时最开始忽略的一点后来加提醒功能时被迫改了一遍结构幸好模型封装得不算太烂。2.3 双击编辑、右键删除的交互细节日程列表的交互我设计得很直白双击任意一行弹出 QDialog 编辑界面右键弹出菜单支持“标记完成”和“删除”。两个操作都会触发数据库更新然后刷新列表。这个环节最容易出现的问题是和 QTableView 自带的编辑功能冲突。如果不想让用户直接在单元格里改数据记得设置table-setEditTriggers(QAbstractItemView::NoEditTriggers)。否则双击会先进入编辑态你的编辑对话框反而弹不出来。另一个小技巧是把“完成”状态用复选框列展示用户勾选后通过 itemChanged 信号写回数据库比右键菜单多一步点击但视觉上更直观。2.4 提醒机制QTimer 加上系统托盘提醒是这个项目的灵魂。实现思路不复杂一个 QTimer 每秒触发一次遍历当天日程比较当前时间是否落在“开始时间前 N 分钟”的提醒窗口内。触发后调用 QSystemTrayIcon::showMessage 弹出系统通知。代码大致如下QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, []() { QDateTime now QDateTime::currentDateTime(); const QListScheduleItem items db-schedulesForDate(now.date()); for (const ScheduleItem item : items) { QDateTime remindTime item.startTime.addSecs(-item.remindMinutes * 60); if (now remindTime now item.startTime !notifiedIds.contains(item.id)) { trayIcon-showMessage(item.title, item.detail, QSystemTrayIcon::Information, 10000); notifiedIds.insert(item.id); } } }); timer-start(1000);这个方案有几个前提要注意ScheduleItem 必须能快速从数据库按日期拉取notifiedIds 是 QSet 程序启动后要清空否则重启前提醒过的日程不会再提醒如果日程跨越午夜按日期拉取就会出现遗漏我自己后来改成了按结束时间跨天匹配这才算处理完整。3. 数据持久化SQLite 让重启不丢数据3.1 为什么选 SQLite 而不是 JSON 文件个人日程的数据量不大几十到几百条记录用 JSON 文件确实够用。但你会发现一旦要“按日期筛选”“按时间排序”“标记完成状态”用文件读写就得自己写排序和过滤逻辑很容易出 bug。SQLite 作为嵌入式数据库一个文件搞定全部数据Qt 还提供了现成的 QSqlDatabase、QSqlQuery 封装不需要额外安装服务端所以选它做存储层是最合理的。有人担心 SQLite 并发性能差但这是单用户桌面工具不存在多进程频繁写库的场景。唯一要注意的是 UI 线程不要直接执行慢 SQL如果后面数据能到几万条再把数据库操作丢到 QThread 或 QtConcurrent 里。3.2 表结构设计数据库里我建了一张 schedules 表字段含义很直观字段名类型说明idINTEGER PRIMARY KEY AUTOINCREMENT主键titleTEXT NOT NULL日程标题detailTEXT详细备注start_timeTEXT NOT NULL开始时间ISO 8601 格式end_timeTEXT结束时间ISO 8601 格式remind_minutesINTEGER DEFAULT 0提前多少分钟提醒is_doneINTEGER DEFAULT 0是否已完成created_atTEXT创建时间时间字段我用 QDateTime::toString(Qt::ISODate) 存成字符串看起来可读还能直接用字符串排序。虽然用 epoch 毫秒数存也行但调试时不方便而且本地工具不存在时区同步问题。建表时给 start_time 加一个普通索引等数据量上来以后按日期查询会快很多。3.3 SQL 封装与防注入我曾经见过不少 Qt 项目把 SQL 语句直接拼字符串比如QString(SELECT * FROM schedules WHERE title%1).arg(title)一旦 title 里带单引号程序就崩。正确做法是用 QSqlQuery 的 prepare bindValuebool ScheduleDatabase::addSchedule(const ScheduleItem item) { QSqlDatabase db QSqlDatabase::database(schedule_conn); QSqlQuery query(db); query.prepare(INSERT INTO schedules(title, detail, start_time, end_time, remind_minutes) VALUES(:title, :detail, :start_time, :end_time, :remind_minutes)); query.bindValue(:title, item.title); query.bindValue(:detail, item.detail); query.bindValue(:start_time, item.startTime.toString(Qt::ISODate)); query.bindValue(:end_time, item.endTime.toString(Qt::ISODate)); query.bindValue(:remind_minutes, item.remindMinutes); return query.exec(); }数据库连接名我固定为 schedule_conn整个程序统一用这一个连接避免多连接导致锁库。封装成一个 ScheduleDatabase 单例类提供 addSchedule、updateSchedule、deleteSchedule、schedulesForDate 四个接口界面层只和这个类打交道不要到处直接写 SQL。3.4 提醒时间判断中的小细节提醒判断里最容易踩坑的是“提前几分钟”和“已提醒过”这两个状态。如果提醒时间恰好在程序启动之前或者用户把系统时间改动了判断逻辑就会出问题。我后来加了一个字段 last_notified_id启动时从数据库加载当天的提醒记录避免同一个日程被反复提醒。这个做法算不上复杂但能让提醒功能的可靠度提高很多。4. 常见问题与排查技巧实录4.1 启动崩溃qt platform plugin could not be initialized这大概是 Qt 新手遇到最多的报错热词里也反复出现。这个报错的意思是程序在启动时没有找到平台插件Windows 下通常是 platforms/qwindows.dll 缺失Linux 下是 libqxcb.so 缺失。出问题的时机一般在“Qt Creator 里运行正常双击 exe 报错”的时候。解决办法很简单用 windeployqt 自动拷贝依赖或者在程序目录手动建一个 platforms 文件夹从 Qt 安装目录的 plugins/platforms 拷贝对应 dll。还有一种情况是系统环境变量 QT_QPA_PLATFORM_PLUGIN_PATH 被设置成了不存在的路径排查时先打印 qgetenv(QT_QPA_PLATFORM_PLUGIN_PATH) 看看配置是否正确。Linux 下还要注意 libxcb 系列库是否齐全缺少 libxcb-xinerama0 也会报类似错误。4.2 Qt Creator 还是 VS Code我的实际选择在 Qt 开发 IDE 的选择上我两个都用过一年以上。Qt Creator 的优势是开箱即用Qt Designer 拖拽界面、调试器集成、qmake/CMake 支持、信号槽自动补全都做得很好对新手尤其友好。VS Code 的优势是轻量和通用配合 C/C 扩展、CMake Tools 插件、Qt 官方插件也能跑起来但需要自己配置编译套件和调试器中间的小问题非常多。如果你只做 Qt 桌面工具我建议直接用 Qt Creator省下的时间足够多做几个功能。如果你同时要维护多个语言项目或者习惯了 Nvim/VSCode 的操作习惯再用 VS Code。这个选择没有标准答案关键是不要在本该写代码的时候折腾编辑器。4.3 打包发布windeployqt 的标准流程打包 Qt 程序最常用的工具是 windeployqt。命令行格式是C:/Qt/5.15.2/msvc2019/bin/windeployqt.exe --release --compiler-runtime build/QtSchedule.exe执行后会自动把 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll、platforms/qwindows.dll 等一系列依赖复制到 exe 同级目录。但这里有一个坑windeployqt 不会自动处理 SQLite 数据库驱动。如果程序里用了 QSqlDatabase 连接 SQLite必须手动把 plugins/sqldrivers/qsqlite.dll 复制到发布目录的 sqldrivers 文件夹下否则程序能启动但数据库打不开报“driver not loaded”。发布完以后我习惯用一个精简的绿色软件目录结构大概长这样发布目录/ ├── QtSchedule.exe ├── Qt5Core.dll ├── Qt5Gui.dll ├── Qt5Widgets.dll ├── Qt5Sql.dll ├── platforms/ │ └── qwindows.dll ├── sqldrivers/ │ └── qsqlite.dll └── styles/ └── qwindowsvistastyle.dll用 NSIS 或 Inno Setup 把整个目录打包成安装程序用户运行安装后就能直接使用。如果想要更小的体积可以尝试编译时开启 release 模式、剔除不用的 Qt 模块但这一步优化收益有限没有必要为了体积牺牲稳定性。4.4 一段速查表我在项目里遇到的高频问题现象可能原因解决思路启动报 no qt platform plugin平台插件缺失或环境变量错误windeployqt 拷贝依赖检查 QT_QPA_PLATFORM_PLUGIN_PATH中文乱码MSVC 源码编码不匹配源码存 UTF-8编译加 /utf-8数据库打开失败driver not loadedqsqlite.dll 未复制到发布目录手动拷贝 sqldrivers 插件按钮点击没反应connect 返回 false 或对象提前销毁qDebug 检查 connection确认 lambda 捕获生命周期程序启动后卡顿在 UI 线程执行耗时任务用 QThread 或 QtConcurrentLinux 托盘图标不显示桌面环境不支持 QSystemTrayIcon检查 /usr/lib/x86_64-linux-gnu 下依赖或改用 AppIndicator5. 进阶扩展把这个小工具做成完整产品5.1 网络同步用 POST 请求把日程送到服务器如果你不满足于本地单机使用想实现手机和电脑同步就需要用 Qt Network。核心代码不复杂用 QNetworkAccessManager 发一个 POST 请求把日程数据转成 JSON 传到后端接口QNetworkAccessManager *manager new QNetworkAccessManager(this); QNetworkRequest request(QUrl(https://example.com/api/schedules)); request.setHeader(QNetworkRequest::ContentTypeHeader, application/json); QJsonObject obj; obj[title] item.title; obj[start_time] item.startTime.toString(Qt::ISODate); QNetworkReply *reply manager-post(request, QJsonDocument(obj).toJson()); connect(reply, QNetworkReply::finished, this, []() { if (reply-error() QNetworkReply::NoError) { qDebug() sync ok; } else { qDebug() reply-errorString(); } reply-deleteLater(); });这个功能我后来实测过最常见的报错不是 Qt 代码问题而是后端接口不支持 POST或者 Content-Type 设置不对。如果你在调试时遇到“request method post not supported”先检查服务器路由是否真的接收 POST再在抓包工具里看请求头是否带了 application/json。5.2 数据统计把日程记录变成时域波形日程管理做到后期我加了一个“完成率统计”的小功能用 QCustomPlot 把每天完成的日程数量画成折线图。如果你对热词里的“qt 时域图转换为频域图”感兴趣可以把“每天完成数量”看成一个时间序列按日期采样然后用 kissfft 这类开源 FFT 库做频域分析观察自己的日程是否具有周周期或月周期。这个方向已经超出普通日程工具的范畴更多的是数据处理实验但确实能让项目显得更有深度。QCustomPlot 接入 Qt 项目很简单把 qcustomplot.h 和 qcustomplot.cpp 加入工程然后实例化一个 QCustomPlot 控件添加 QCPGraph 即可。画折线图时记得调用 replot()它会主动重绘一遍否则新数据不会显示在界面上。这个库对新手非常友好是我在图表示场景里最常用的一把锤子。5.3 命令行快速添加与单实例如果你每天经常用快捷键唤起日程可以在程序里加命令行参数解析用 QCommandLineParser 支持--add 标题然后通过 QLocalServer 和 QLocalSocket 把参数传给已经运行的实例实现“已在运行就追加日程没有运行就启动主界面”。这个功能不太起眼但是我实际使用频率最高的一个入口。QCommandLineParser parser; parser.setApplicationDescription(QtSchedule); parser.addHelpOption(); QCommandLineOption addOption(add, Add a schedule, title); parser.addOption(addOption); parser.process(app); if (parser.isSet(addOption)) { // 调用 ScheduleDatabase 插入一条日程 }最后说点实在的把日程工具从“能跑”变成“好用”差的往往不是大功能而是那些不起眼的小交互——双击编辑、右键删除、托盘提醒、命令行入口。每一个小功能背后都有一套完整的 Qt 技术链。我个人体会最深的是做个人项目时不要一开始就追求架构完美先把主流程跑通再针对真正影响到自己使用效率的点去迭代。这样才能保持开发动力也让工具真正长成自己想要的样子。本文还有配套的精品资源点击获取