拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用Qt打造个人日程提醒工具:从数据存储到置顶弹窗的完整实践

简介面向Qt开发者的个人日程管理示例工程演示如何基于Qt Widgets搭建完整的日程安排与事务处理应用。工程覆盖日历浏览、日期时间选择、待办事项列表、数据持久化、定时提醒、信号槽交互与多窗口协作等关键环节适合正在学习Qt桌面开发或需要快速上手日程管理类项目的读者参考。压缩包共89个文件包含15个C源文件、15个头文件、6个Qt Designer界面文件以及工程配置、数据文件、可执行程序和调试符号等完整保留了从源码到编译产物的全过程便于直接运行与二次修改。资源包整体约10.83MB目前已有468人学习。通过这份代码可直观理解QCalendarWidget、QDateTimeEdit、QTimer等组件的实际用法以及模型视图架构和SQLite数据存储如何应用于真实场景。工程还包含登录、注册、任务管理、任务盒等多个功能模块目录清晰适合对照学习Qt项目的分层组织与模块拆分思路。1. 为什么用Qt做个人日程工具从需求倒推技术选型1.1 需求边界先想清楚不要一上来就搞全套先说说我为什么要写这个工具。手机自带的日历应用功能很全但我一直有个痛点很多日程是在电脑前工作时产生的比如下午两点记得给服务器续费、周三之前把季度报表发给财务这时候切换到手机新建日程再设提醒路径太长了。我真正需要的是一个常驻桌面、可以随时双击添加事项、到点能明确弹窗提醒的轻量工具。在动手之前我给自己划了个边界。第一版不碰云同步不碰多人协作不做复杂的重复规则就做三类核心能力按日期维护个人日程、快速查看某一天/某一个月的安排、到点弹出提醒。这个边界很重要很多个人项目烂尾就是因为一开始想要的太多最后卡在某个非核心功能上。确认了需求边界技术选型就顺理成章了。C和Qt对我来说是最熟悉的技术栈而且这个场景有一个其他框架比拟不了的优势Qt自带成熟的日期时间处理库QDate、QTime、QDateTime自带跨平台的托盘、定时器、置顶窗口能力这些东西做日程工具全是刚需。1.2 Widgets与QML之争这个场景选哪个Qt的新手经常纠结用Qt Widgets还是QML。我的结论很直接纯桌面工具、以表单和列表为主、没有复杂动效需求就老老实实用Widgets。我的判断依据有三条。第一Widgets的QCalendarWidget和QTableView这类现成控件做日历网格和管理列表几乎是开箱即用而QML里这些基础控件反而要自己拼。第二Widgets的调试心智负担低很多一个个人项目你不可能花大量精力去搞QML的上下文属性和信号绑定。第三Widgets在Windows和Linux下的中文渲染、DPI适配成熟稳定日常使用不会有看起来不错但用起来别扭的问题。注意如果你以后想做触屏设备或界面需要大量自定义动画那时候再考虑QML不迟。个人桌面工具选Widgets能把精力省下来去打磨业务逻辑。2. 数据模型设计按天分片的JSON存储方案2.1 两种存储方案的对比日程数据怎么存我对比了两个方案SQLite和JSON文件分片。这里直接把我当时的对比表放出来。对比维度SQLite按天分片JSON单日读取速度需要走SQL查询索引命中后毫秒级直接加载当天文件最快数据备份与迁移单文件搞定但对普通用户来说不太直观每个日期一个文件方便手动查看和编辑并发与事务强支持完整事务弱但个人单机场景完全够用实现复杂度需要引入Qt SQL模块还要管理表结构和迁移用QJsonDocument加QSaveFile三十行代码搞定扩展性强以后要做搜索和统计很方便弱跨天查询要遍历文件看到这个对比你可能觉得SQLite才是正统选择。但我的判断是这个工具90%的操作都是打开某一天——增删改当天事项这种访问模式天然就是按天聚集的。SQLite在这种场景下没有显著优势反而因为表结构设计、日期的字符串/时间戳转换等问题增加了代码量。JSON文件分片方案最直观的收益是某个文件坏了只影响那一天的数据而且我随时可以用文本编辑器直接改数据排查问题。2.2 核心数据结构定义我定义的日程数据结构是这样的单个文件对应一天{ date: 2025-06-21, items: [ { id: uuid-xxx, time: 09:30, title: 项目周会, note: 带上上周的进度统计表, remindMinutes: 10, done: false } ] }字段说明一下id是我用QUuid::createUuid()生成的字符串删除和修改事项都靠它定位不要用数组下标——排序变化之后下标就乱了。time字段用HH:mm格式的字符串而不是QTime序列化因为字符串在JSON里可读性最好解析也最省事。remindMinutes表示提前几分钟提醒0表示准时提醒-1表示不提醒。存储路径我用的是QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)这是Qt提供的标准跨平台路径方案在Windows上是C:/Users/用户名/AppData/Roaming/应用名在Linux上是~/.local/share/应用名。不要自己硬编码一个C:/MyApp/Data这种路径不同系统权限规则不一样迟早要踩坑。写文件时用QSaveFile而不是QFile这个细节值得提一下。QSaveFile是先写临时文件再原子替换的机制即使写入过程中程序崩溃原文件也不会损坏。日程数据丢了是很痛苦的事情这点保障很重要。配合QDir().mkpath()先确保目录存在这一套下来数据层就很扎实了。3. 界面布局日历网格与事项列表联动3.1 主窗口拆成三块界面是整个工具的脸面我的布局方案是一个典型的左右分栏结构。左侧是一个QCalendarWidget日历组件占屏幕大约三分之一右侧是当天事项列表用一个QListWidget展示菜单栏或底部工具栏放新建事项、删除选中、设为已完成这几个操作按钮。在动手前我没有先做整体布局而是先做了一件关键事情写一个最简单的原型把QCalendarWidget的selectionChanged信号连到一个空槽函数点几个日期试试反应速度。为什么要先做这一步因为QCalendarWidget是Qt自带组件里比较重的一个某些Qt版本在部分平台的绘制效率并不理想。如果基础交互都有迟滞感后面的功能全白搭。实测下来我用的Qt 5.15.2在Windows 10上没有明显卡顿这才放心继续往下做。3.2 日历刷新事项数标记日程工具和普通日历最大的区别在于用户扫一眼日历需要立刻知道哪些天有事、事情多不多。QCalendarWidget自带的能力是setDateTextFormat可以给指定日期设置文字颜色和背景色我就用这个接口做了个事项数量热力标记。核心思路是每次切换到某个月时扫描当前显示月份所有日期的items文件统计事项数量数量为0的不动1-2件的用浅橙色标记3件以上的用深橙色标记。月份切换时扫描整个月比每天单独检查更高效因为只需要获取一次月份天数然后循环30或31次文件是否存在。具体实现上我重写了QCalendarWidget的子类拦截currentPageChanged信号在这个信号里做日期标记刷新。void CalendarWidget::onCurrentPageChanged(int year, int month) { // 先清空所有日期格式 setDateTextFormat(QDate(), QTextCharFormat()); int days QDate(year, month, 1).daysInMonth(); for (int day 1; day days; day) { QDate d(year, month, day); QString filePath dataDir / d.toString(yyyy-MM-dd) .json; QFileInfo fi(filePath); if (!fi.exists()) continue; QTextCharFormat fmt; int count loadItemCount(filePath); if (count 0) { fmt.setBackground(count 3 ? QColor(255, 200, 150) : QColor(255, 230, 200)); fmt.setForeground(Qt::black); setDateTextFormat(d, fmt); } } }3.3 事项列表与编辑弹窗右侧的QListWidget我用的是setItemWidget的方式填充自定义行控件而不是直接用QListWidgetItem的文本。自定义行控件可以显示事项时间、标题、完成状态勾选框和删除按钮交互更直观。这里有一个细节经验自定义行控件的高度要在创建时显式设置否则QListWidget的行高会错乱。我的做法是给每个行控件设置固定高度比如56像素然后动态计算每个item的sizeHint。另外当通过日历切换日期时要先把QListWidget的updatesEnabled置为false一次性清空再填充所有行结束后恢复为true。这个操作能显著减少界面闪烁。编辑弹窗我用的是一个模态QDialog里面放四个控件标题QLineEdit、时间QTimeEdit、提前提醒QSpinBox范围从0到120分钟加一个不提醒的复选项、备注QTextEdit。因为是模态弹窗确认和取消用QDialogButtonBox的标准按钮就可以不需要自己手写信号逻辑。4. 日期与时间处理三个必踩的坑4.1 周起始日周一还是周日QCalendarWidget默认周日是一周的第一天但中国人习惯周一为一周的开始。这个坑倒不难解决用setFirstDayOfWeek(Qt::Monday)一行代码搞定。但这个设置必须在窗口初始化时调用放到后面调用会导致周几的列标题和日期网格对不上看起来非常诡异。真正麻烦的是在自定义周视图逻辑里如果我做本周日程总览必须自己处理QDate和本周周一的日期之间的换算。这个换算我自己写过几次每次都要翻资料后来直接封装成了一个静态函数static QDate mondayOfWeek(const QDate date) { int dayOfWeek date.dayOfWeek(); // Qt: 1Monday, 7Sunday return date.addDays(1 - dayOfWeek); }注意dayOfWeek()的返回规则周一返回1周日返回7。这跟中国习惯一致但跟很多C语言库的tm_wday周日为0不一样混用的时候一定要留个心眼。4.2 月份天数别自己算闰年获取一个月有多少天新手最常犯的错误是自己写闰年判断逻辑。Qt提供了现成的QDate(year, month, 1).daysInMonth()这个接口已经完整处理了闰年规则能被4整除但不能被100整除或者能被400整除完全不需要自己造轮子。我之所以强调这个是因为日程工具的月份遍历逻辑几乎无处不在日历网格要算出这个月第一天是周几、总共多少天每个月要做事项数量热力标记月底要做下月待办预提醒。这些逻辑如果自己手写日期计算每写一处就有一次出错的机会。用Qt的QDate方法一次性把日期规范化、加减天数、月份比较这些坑全填平了。4.3 全天事项与日期存储的时区陷阱另一个容易出错的是全天事项的存储。如果你把全天事项的时间存成00:00然后拿QDateTime去比较很可能会因为时区问题偏移一天。个人单机工具虽然不涉及国际时区但Windows的时区设置如果用了UTC8以外的区域或者系统启用了自动调整夏令时就可能在凌晨边界出问题。我的规避方法是全天事项不存具体时间戳而是单独用一个allDay: true字段标记显示时只渲染日期和标题不渲染时间。凡是涉及日期是否重合的判断都只用日期字符串做比较绝不转成时间戳比较——日期字符串的格式是yyyy-MM-dd字典序就是时间顺序直接operator比较即可。5. 提醒机制QTimer轮询置顶弹窗5.1 为什么不用系统级通知在做提醒功能时我面临一个选择用Qt自带的QSystemTrayIcon::showMessage做系统托盘通知还是自己做置顶弹窗。系统通知的好处是省事但对我这个需求来说有两个致命问题。第一Windows的系统托盘通知是短时显示的用户离开电脑几分钟再回来很可能没看见第二系统通知无法强制用户确认也就是说你无法知道用户到底看没看到这条日程提醒。日程提醒的特点是可以不到但绝不能漏。所以我选择了一条更稳妥的路提醒弹窗做成置顶窗口持续显示直到用户点击我知道了。这个交互虽然粗暴但信息传达率是100%。5.2 提醒检查的核心逻辑提醒的触发逻辑用单个QTimer实现策略是每30秒检查一次所有今天的日程。为什么是30秒因为日程时间的精度本身是分钟级30秒的检查频率对提前10分钟提醒这类需求完全够用而且几乎不占CPU。每次检查时遍历当天事项列表对每个尚未提醒且有remindMinutes设置的事项计算当前时间与事项时间的差值。用秒级时间戳做差值计算qint64 nowSecs QDateTime::currentDateTime().toSecsSinceEpoch(); qint64 targetSecs QDateTime::currentDateTime().currentDateTime(); // 用当天的日期 item的time字符串构造提醒时间 QDateTime remindTime QDateTime::fromString( QDate::currentDate().toString(yyyy-MM-dd) item.time, yyyy-MM-dd HH:mm); if (item.remindMinutes 0) { remindTime remindTime.addSecs(-item.remindMinutes * 60); } if (nowSecs remindTime.toSecsSinceEpoch()) { triggerReminder(item); }有了触发条件还不够还需要一个是否已提醒的标记机制。我是在Item结构体里加了一个bool reminded字段但要注意这个字段不能直接写回原JSON文件因为用户可能在提醒前编辑过这条日程。我采用的办法是单独维护一个QSetQString里面存已提醒事项的id程序启动时清空运行期间记录。这样同一日程无论检查多少次只弹一次窗。5.3 弹窗的特殊处理置顶闪烁提醒弹窗我用的是一个无边框的Qt::WindowStaysOnTopHint | Qt::Tool窗口为什么不直接用一个普通QDialog因为你点了其他窗口后普通QDialog会躲到后面如果用户在忙别的事提醒就看不到了。WindowStaysOnTopHint确保弹窗始终在z序最上面Qt::Tool让弹窗不出现在任务栏避免任务栏按钮堆积。弹窗内容设计上也动了些脑筋标题加粗显示中间是一个大号的事标题下面用红色显示事项时间底部只有一个我知道了按钮。为了让用户快速感知我还在弹窗显示时配合一个窗口闪烁效果——通过QPropertyAnimation让窗口透明度在400毫秒内从0.3变化到1.0循环三次。注意置顶窗口不要滥用普通操作窗口如果也置顶会严重影响用户操作。提醒弹窗只在显示期间置顶用户点击我知道了关闭时立刻释放置顶状态我是在closeEvent里显式调用了setWindowFlag(Qt::WindowStaysOnTopHint, false)并重新show()确保释放生效。6. 发布与分发windeployqt和那些坑6.1 打包三步走工具开发完了最后一个关键环节是打包发布。Qt的发布比很多框架要讲究因为程序在开发环境能跑不代表换一台机器能跑——缺DLL、缺插件、缺编译运行库每一种情况都够你排查半天。用windeployqt工具的完整流程我总结为三步。第一步用Release模式编译不要在Debug模式下打包Debug版本依赖的调试运行库体积大且目标机器没有。第二步将生成的exe放到一个空目录然后在命令行执行windeployqt 你的程序名.exe这个工具会自动把需要的Qt DLL、插件、翻译文件复制到exe同目录。第三步手动补上编译器运行库如果用的是MSVC编译需要把vcruntime140.dll、msvcp140.dll等拷贝进去通常可以在系统目录找到如果用的是MinGW则需要拷贝对应的libgcc_s_seh-1.dll等。6.2 中文路径和Qt平台插件的坑打包发布中我有一个印象深刻的教训Qt程序在路径含中文的目录下经常报Windows no Qt platform plugin could be initialized这个错误。这个错误表面上是找不到platforms/qwindows.dll但实际上很可能是路径解析异常导致插件加载失败。我后来做了两个防御性措施来解决这个问题一是在程序中显式设置插件目录QApplication app(argc, argv); QApplication::addLibraryPath(QApplication::applicationDirPath() /plugins);二是打包时保证exe所在路径的全英文。虽然这样限制了一部分用户的使用习惯但稳定性优先。另外一个打包时容易忽略的点是如果程序用了中文字体和翻译文件windeployqt默认可能不会拷贝所有翻译模块你需要手动确认目录里有没有translations/qt_zh_CN.qm。如果没有这个文件程序在非中文系统上会显示乱码方块。对于单机个人工具我建议把Qt自带的字体策略改掉在main函数里设置QApplication::setFont时用QFont(Microsoft YaHei)来显式指定字体而不是依赖系统默认字体。发布后的验证方式也很重要不要只在开发机上测试把整个目录拷贝到一台没有安装Qt的开发环境虚拟机里跑一遍重点测新建日程、弹窗提醒、数据保存这三个核心链路。我当时的测试机是一台Windows 10 x64的干净虚拟机实测下来一次通过这才敢把工具正式纳入日常工作流。做这样一个工具前前后后花了两三个周末的时间。回头看的最大体会是个人工具不需要追求大而全把添加日程、查看日程、提醒到位这三件事做扎实它带来的效率提升远比功能堆砌更可感。如果你也想折腾一个类似的桌面工具建议也从这三个核心链路开始一点点把细节磨到位。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门