news 2026/9/9 16:38:48

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用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展示;菜单栏或底部工具栏放"新建事项"、"删除选中"、"设为已完成"这几个操作按钮。

在动手前我没有先做整体布局,而是先做了一件关键事情:写一个最简单的原型,把QCalendarWidgetselectionChanged信号连到一个空槽函数,点几个日期试试反应速度。为什么要先做这一步?因为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。另外,当通过日历切换日期时,要先把QListWidgetupdatesEnabled置为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: 1=Monday, 7=Sunday 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的时区设置如果用了UTC+8以外的区域,或者系统启用了"自动调整夏令时",就可能在凌晨边界出问题。

我的规避方法是全天事项不存具体时间戳,而是单独用一个"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文件,因为用户可能在提醒前编辑过这条日程。我采用的办法是单独维护一个QSet<QString>,里面存已提醒事项的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.dllmsvcp140.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的干净虚拟机,实测下来一次通过,这才敢把工具正式纳入日常工作流。

做这样一个工具,前前后后花了两三个周末的时间。回头看的最大体会是:个人工具不需要追求大而全,把"添加日程、查看日程、提醒到位"这三件事做扎实,它带来的效率提升远比功能堆砌更可感。如果你也想折腾一个类似的桌面工具,建议也从这三个核心链路开始,一点点把细节磨到位。

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

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

WrenAI 搭配 K8s HPA 弹性伸缩,把低谷期账单省掉 40-60%

WrenAI 搭配 K8s HPA 弹性伸缩&#xff0c;把低谷期账单省掉 40-60% 【免费下载链接】WrenAI GenBI (Generative BI) for AI agents, an open-source, governed text-to-SQL through an open context layer that turns natural-language questions into trusted dashboards, ch…

作者头像 李华
网站建设 2026/9/9 16:37:53

你的产品会被AI看见吗?本地视觉识别与API接入实战

同一个产品&#xff0c;人看一眼能认出来&#xff0c;AI 认不认得出&#xff0c;是另一回事。在电商搜索、广告投放、内容审核、多模态搜索这些场景里&#xff0c;产品主图、详情页、短视频素材每天会被 AI 系统扫描成千上万次。AI 识别不准&#xff0c;轻则曝光不准、搜索流量…

作者头像 李华
网站建设 2026/9/9 16:31:04

COMSOL激光烧蚀、熔覆与选区激光熔化仿真建模实战与避坑指南

做激光仿真的朋友应该都有同感&#xff1a;COMSOL 里面“激光烧蚀、激光熔覆、选区激光熔化”这三个方向&#xff0c;名字像三兄弟&#xff0c;网上案例包也不少&#xff0c;但真自己动手复现的时候&#xff0c;每一步都在踩坑。我最早从激光烧蚀开始&#xff0c;后来把手伸到熔…

作者头像 李华
网站建设 2026/9/9 16:30:56

三端柔性直流输电VSC-HVDC的MATLAB/Simulink建模与仿真

做柔性直流输电仿真这几年&#xff0c;说实话&#xff0c;能完整跑通一套三端VSC-HVDC模型并不容易。网上能下到的MATLAB模型大多是两端背靠背的&#xff0c;真正把三端分布式接入、各换流站控制策略都搭出来的资料&#xff0c;掰着手指头都能数过来。我最近正好用MATLAB/Simul…

作者头像 李华
网站建设 2026/9/9 16:29:46

Python多线程:从GIL原理到IO密集任务的高效处理

在 Python 生态里聊多线程&#xff0c;几乎每次都会被人拿 GIL 怼一遍。这话没毛病&#xff0c;CPU 密集任务拿多线程去跑&#xff0c;确实可能越跑越慢&#xff1b;但如果你是写爬虫、报表生成、批量接口调用、文件处理这类脚本&#xff0c;多线程在大部分情况下就是性价比最高…

作者头像 李华