简介:基于Qt框架的C++个人日程管理程序源码与编译包,面向需要学习Qt桌面应用开发的入门与中级开发者,帮助解决日程记录、提醒设置、待办管理等常见事务处理需求。包内含完整工程文件与可执行程序,共89个文件,核心代码部分包含15个cpp源文件、15个h头文件、6个ui界面设计文件,另有pro/sln工程配置、pdb调试信息、exe可执行程序及多个dat数据文件,压缩包约10.83MB。已有469人学习下载。通过该资源可系统掌握QCalendarWidget日历展示、QTimer定时提醒、模型视图架构与SQLite数据持久化等关键实现;工程内登录注册、任务添加、日程列表、数据存储等模块划分清晰,辅以可直接运行的exe与测试数据文件,便于对照源码理解Qt信号槽机制、布局管理与数据库交互,适合作为课程设计或个人项目二次开发的参考基底。
1. 从日程爆炸到桌面提醒:这个 Qt 项目把「处理事务」做成了一套闭环
工作日的上午,手机里的日程提醒 App 和电脑上的日历各自为政,会议、报销、周报、取快递散落在三个工具里,漏掉一件就足以让半天节奏乱掉。这个「Qt 个人日程安排处理事务」项目,解决的就是这个最具体的痛点——用 Qt Widgets 做一个桌面端的日程管理工具,把「记录事项 → 设定时间 → 到期提醒 → 标记完成 → 查看历史」串成一条完整链路。它不是又一个待办清单 Demo,而是把日程当作带状态机的事务对象来处理:新建、编辑、删除、提醒、完成、归档,每一件事都有明确的流转路径,数据落在本地 SQLite 里,重启不丢。适合想学 Qt 界面与业务逻辑怎么分层、想把 C++ 桌面应用真正做成能日常使用工具的开发者,也适合拿它当骨架去接自己的提醒逻辑或数据同步。
2. 项目定位与技术选型:为什么是 Qt Widgets 而不是 QML,数据为什么落 SQLite
2.1 技术栈的取舍逻辑
这个项目选的是 Qt Widgets + C++17,而不是 QML/Qt Quick。原因很现实:日程管理这种工具型软件,界面元素无非是表格、日历、树形列表、按钮、输入框,Widgets 的 QTableView、QCalendarWidget、QSystemTrayIcon 全都是现成且成熟的控件,配合 QSS 改样式比 QML 里调布局省事得多。如果换 QML,动画和触屏交互会更顺,但这种「处理事务」类应用并不需要花哨的转场,稳定性和可维护性优先级更高。
数据层选了 SQLite,通过 Qt 的 QSqlDatabase 模块访问。不选 JSON 文件或纯内存存模型的原因是:日程数据天然有「按时间查询」「按状态过滤」「修改历史」需求,SQL 的 WHERE 和 ORDER BY 直接解决,不需要在代码里手写遍历。SQLite 又不需要独立安装服务端,打包时带一个数据库文件即可,个人桌面应用非常合适。项目里默认把数据库文件放在用户目录的 AppData 路径下,这比丢在 exe 同目录更符合桌面软件习惯,因为程序更新时不会误删用户数据。
2.2 整体架构:界面与业务分离的三层结构
这个项目不是把所有逻辑塞进 MainWindow,而是拆成了三层:界面层(MainWindow、各种 Dialog)、业务层(ScheduleManager 服务类)、数据层(SqliteStorage 封装)。界面层只负责用户交互和数据展示,不直接写 SQL;业务层处理日程的增删改查、提醒触发、状态流转;数据层负责连接数据库和执行 SQL。这样拆的好处是,未来如果要把数据换成 MySQL,或者把界面从 Widgets 换成 Web,改动可以局限在某一层内。
目录结构大致是:
qt-schedule-manager/ ├── src/ │ ├── main.cpp │ ├── MainWindow.h / MainWindow.cpp │ ├── ScheduleManager.h / ScheduleManager.cpp │ ├── SqliteStorage.h / SqliteStorage.cpp │ ├── ScheduleItem.h │ ├── RemindDialog.h / RemindDialog.cpp │ ├── AddScheduleDialog.h / AddScheduleDialog.cpp │ └── models/ │ ├── ScheduleTableModel.h / ScheduleTableModel.cpp ├── resources/ │ ├── app.qrc │ └── icons/ ├── schedule.db (运行时生成) ├── CMakeLists.txt └── README.mdScheduleItem 是核心数据结构,用 struct 而非 class,因为它的职责只是承载字段:id、title、description、startTime、endTime、remindTime、priority、status、category。status 用枚举定义,包含 Pending、Done、Expired、Cancelled 四种,这样在 SQL 里存整数而不是字符串,避免拼写错误,也方便 ORM 化。
2.3 ScheduleManager:业务核心怎么设计
ScheduleManager 是唯一被 MainWindow 依赖的业务类,它内部持有 SqliteStorage 的实例。对外暴露的接口大致如下:
class ScheduleManager : public QObject { Q_OBJECT public: explicit ScheduleManager(QObject *parent = nullptr); ~ScheduleManager(); bool initialize(); // 创建数据库连接和表结构 QVector<ScheduleItem> getAllSchedules(); // 返回全部日程,按时间排序 QVector<ScheduleItem> getSchedulesByDate(const QDate &date); QVector<ScheduleItem> getSchedulesByStatus(int status); bool addSchedule(const ScheduleItem &item); // 新增 / 更新 bool updateSchedule(const ScheduleItem &item); bool deleteSchedule(int id); bool markAsDone(int id); signals: void scheduleAdded(int id); void scheduleUpdated(int id); void scheduleDeleted(int id); void scheduleReminded(const ScheduleItem &item); private: void checkReminders(); // 供定时器调用,检查到期提醒 SqliteStorage *m_storage; QTimer *m_remindTimer; };这里有个设计细节值得注意:所有写操作返回 bool 而不是 void。原因是 SQLite 的写入有可能因磁盘满、数据库锁等原因失败,把结果返回给界面层,便于在捕获到失败时弹出 QMessageBox 提示。信号 scheduleReminded 是给 RemindDialog 用的,它监听这个信号,有日程到期就弹出提醒窗口,同时系统托盘也会闪烁图标。
3. 核心模块拆解:一个日程从录入到提醒再到归档的完整链路
3.1 录入流程:AddScheduleDialog 的校验与数据组装
用户点击「新增日程」后,MainWindow 会 new 一个 AddScheduleDialog,这是个模态对话框。对话框里主要控件包括:标题输入框、描述文本框、开始时间/结束时间(QDateTimeEdit)、提前提醒时间(QComboBox,选项为 0/5/10/30/60 分钟)、优先级(QComboBox:高/中/低)、分类(QComboBox:工作/生活/学习/其他)、状态(默认为待处理)。
用户点击确定时,对话框做三件事:校验、组项、返回结果。校验逻辑写在一个独立的 validate() 方法里:
bool AddScheduleDialog::validate() { if (ui->titleEdit->text().trimmed().isEmpty()) { QMessageBox::warning(this, "校验失败", "标题不能为空"); return false; } if (ui->startEdit->dateTime() >= ui->endEdit->dateTime()) { QMessageBox::warning(this, "校验失败", "结束时间必须晚于开始时间"); return false; } if (ui->remindCombo->currentIndex() > 0) { QDateTime remindTime = ui->startEdit->dateTime().addSecs( -ui->remindCombo->currentData().toInt() * 60); if (remindTime < QDateTime::currentDateTime()) { QMessageBox::warning(this, "校验失败", "提醒时间不能早于当前时间"); return false; } } return true; }这里的核心是「数据有效性优先于用户体验」:哪怕界面允许用户随便填,业务层也不该接脏数据。提醒时间的计算方式是「开始时间减去提前量」,所以如果用户设定的是一个即将开始的事务,提前量过大就会导致提醒时间已经过去,这种情况必须拦截。
校验通过后,成员变量 m_item 会被填充,对话框的 exec() 返回 QDialog::Accepted,MainWindow 拿到 m_item 后调用 manager->addSchedule()。这里没有让 Dialog 直接操作 manager,而是通过返回值让 MainWindow 去调用,保持界面互相解耦。
3.2 存储实现:SQLite 建表与参数化写入
SqliteStorage 的 initialize() 方法负责打开数据库和建表。表结构设计如下:
CREATE TABLE IF NOT EXISTS schedules ( id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(200) NOT NULL, description TEXT, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, remind_time DATETIME, priority INTEGER DEFAULT 1, status INTEGER DEFAULT 0, category VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP );时间字段用 DATETIME 存 ISO 字符串,比如 "2026-05-18T09:30:00"。不用 UNIX 时间戳的原因是这个项目的查询经常涉及「某一天的所有日程」,ISO 字符串前缀比较可以直接用 LIKE '2026-05-18%',可读性比时间戳强。当然如果数据量到百万级,前缀 LIKE 会有性能问题,但对个人日程来说完全够用。
写入时用 QSqlQuery 的 prepare+bindValue 方式,杜绝字符串拼接 SQL:
bool SqliteStorage::insertSchedule(const ScheduleItem &item) { QSqlQuery query(m_db); query.prepare("INSERT INTO schedules " "(title, description, start_time, end_time, remind_time, priority, status, category) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); query.addBindValue(item.title); query.addBindValue(item.description); query.addBindValue(item.startTime.toString(Qt::ISODate)); query.addBindValue(item.endTime.toString(Qt::ISODate)); query.addBindValue(item.remindTime.toString(Qt::ISODate)); query.addBindValue(static_cast<int>(item.priority)); query.addBindValue(static_cast<int>(item.status)); query.addBindValue(item.category); if (!query.exec()) { qWarning() << "插入日程失败:" << query.lastError().text(); return false; } return true; }参数化为什么不只是好习惯而是必须:如果直接拼 SQL 字符串,用户输入内容里带单引号就会破坏语句结构,甚至产生 SQL 注入风险。这个项目里所有用户可控文本都走了 bindValue,这是写 Qt 数据库程序的最低底线。
3.3 列表展示:ScheduleTableModel 与 QTableView 如何绑定
MainWindow 主界面左侧是日期列表或日历,右侧是 QTableView 展示当日或全部日程。表格的数据模型继承 QAbstractTableModel,而不是直接用 QSqlTableModel。原因有二:一是 QSqlTableModel 虽然少写代码,但排序、状态过滤、自定义显示文本(把 status 的整数映射成「待处理/已完成/已过期」)都要绕很多弯子;二是自定义 Model 能更好地配合 QVector 做缓存。
核心的 data() 重写方法如下:
QVariant ScheduleTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= m_schedules.size()) return QVariant(); const ScheduleItem &item = m_schedules.at(index.row()); if (role == Qt::DisplayRole) { switch (index.column()) { case 0: return item.title; case 1: return item.startTime.toString("yyyy-MM-dd HH:mm"); case 2: return item.endTime.toString("yyyy-MM-dd HH:mm"); case 3: return priorityText(item.priority); case 4: return statusText(item.status); case 5: return item.category; } } else if (role == Qt::BackgroundRole) { if (item.status == ScheduleItem::Expired) return QColor(255, 235, 235); if (item.status == ScheduleItem::Done) return QColor(235, 255, 235); if (item.priority == ScheduleItem::High) return QColor(255, 250, 230); } else if (role == Qt::TextAlignmentRole) { return Qt::AlignCenter; } return QVariant(); }这段代码把数据和显示逻辑抽离:Model 里不出现「已完成」这种中文字符串,而是通过 statusText() 映射。这样如果以后要做多语言,只需要改 statusText() 一个函数。背景色这里用的是「偏低饱和的颜色」,避免高饱和色导致文字看不清。所有颜色判断都放在 BackgroundRole 分支,你可以随时关闭或调整。
3.4 提醒触发机制:QTimer 轮询的节奏控制
提醒功能是这个项目最容易被忽视又最容易翻车的地方。常见做法是「进入程序启动一个每秒触发的 QTimer,每次遍历所有日程,发现到了提醒时间就弹窗」。但这样做有两个问题:线程阻塞导致 UI 卡顿,以及同一条提醒被触发多次。
这个项目的处理方式是先按分钟粒度轮询,而不是按秒。在 ScheduleManager 里,m_remindTimer 的 interval 设为 60000 毫秒,也就是每分钟走一次检查:
void ScheduleManager::checkReminders() { QDateTime now = QDateTime::currentDateTime(); QVector<ScheduleItem> pending = m_storage->fetchPendingReminders(now); for (const ScheduleItem &item : pending) { QSet<int> ¬ified = m_notifiedIds; if (notified.contains(item.id)) continue; notified.insert(item.id); emit scheduleReminded(item); m_storage->markRemindAsNotified(item.id); } }代码中 markRemindAsNotified 是「幂等触发」的关键:它把 remind_time 标记为已提醒。边界场景分析:用户把日程从下午 3 点改到下午 5 点,此时旧的提醒已经触发过且已标记,改时间后 remind_time 变化,应重新允许提醒——所以字段设计时需要在 updateSchedule 里把 notified 标记清零;另一种边界是软件没启动,日程时间点错过了,那启动后的第一次轮询要不要补一条记录?这里不做补提醒,因为个人日程管理里「已错过」靠状态显示为已过期来兜底,而不是靠事后弹窗,弹窗反而是一种骚扰。
4. 运行与配置:从拿到源码到系统托盘出现提醒
4.1 环境准备与构建指令
项目是基于 CMake 构建的 Qt Widgets 工程。需要的前提环境如下表:
| 依赖 | 版本建议 | 说明 |
|---|---|---|
| Qt | 5.15 或 6.x | 推荐用 Qt 6.2 以上,范例使用 C++17 |
| CMake | 3.16+ | Qt6 要求相对高一些 |
| 编译器 | MSVC 2019+ / MinGW / Clang | Windows 推荐 MSVC,Linux 用 GCC 9+ |
| SQLite | Qt 自带驱动 | 确认 Qt 安装时勾选 SQL 模块 |
拿到源码后,先检查 CMakeLists 里的 Qt 模块列表。以 Qt6 为例,核心部分长这样:
cmake_minimum_required(VERSION 3.16) project(ScheduleManager VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets Sql ) add_executable(ScheduleManager src/main.cpp src/MainWindow.cpp src/ScheduleManager.cpp src/SqliteStorage.cpp src/AddScheduleDialog.cpp src/RemindDialog.cpp src/models/ScheduleTableModel.cpp resources/app.qrc ) target_link_libraries(ScheduleManager PRIVATE Qt6::Widgets Qt6::Sql)在 Windows 上,最简单的构建路径是打开 Qt 的命令行环境(如「Qt 6.5.0 (MSVC 2019 64-bit)」),然后依次执行:
mkdir build cd build cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release cmake --build . --config Release注意这里的 -G "Ninja" 不是必须的,如果使用 Visual Studio 生成器,cmake 会自动生成 .sln 解决方案,用 VS 打开直接点本地 Windows 调试器也能跑。构建完成后会生成 ScheduleManager.exe,首次运行会在用户目录自动创建 schedule.db,无需手动建数据文件。
4.2 数据库初始化失败时的排查路径
SqliteStorage 初始化失败是跑起来时最容易遇到的问题,表现为程序启动后弹数据库连接错误,或所有界面加载正常但增删改查无反应。排查路径按以下顺序走:
第一,确认 Qt 安装时勾选了 SQL 模块。在 Qt 安装目录下找 sqldrivers 文件夹,看里面有没有 qsqlite.dll(Windows)或 libqsqlite 相关文件。没有这个 DLL,QSqlDatabase::addDatabase 会提示 driver not loaded。我一般在 CMake 里临时加一行 message 打印 QSqlDatabase::drivers(),看输出列表里有没有 QSQLITE。
第二,检查数据库文件是否可写。项目里初始化路径在 QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) 下,如果这个路径不存在且程序没有提前创建,会返回 false。核实一下 QDir().mkpath(dir) 是否执行了,以及应用程序是否有该目录的写权限。第三,排查数据库锁问题:如果之前崩溃导致 journal 文件残留,SQLite 会尝试恢复,但可能因为权限问题恢复失败。最简单粗暴的办法是退出程序后删掉同目录下所有 .journal 文件再启动,项目不会出现脏数据丢失问题,因为日程数据本身不频繁写入。
4.3 系统托盘和桌面提醒的配置注意点
主窗口关闭时默认会退出程序,但日程管理工具的理想行为是「关窗口缩到托盘,后台继续跑提醒」。实现上需要 override MainWindow 的 closeEvent:
void MainWindow::closeEvent(QCloseEvent *event) { if (m_trayIcon->isVisible() && !m_isQuitting) { hide(); event->ignore(); } else { event->accept(); } }因为窗口 hide() 后 QApplication 不会自动退,QSystemTrayIcon 会留在系统托盘。真正退出需要加一个「退出程序」的托盘菜单项,里面调用 qApp->quit()。这一个环节如果漏掉,用户勾掉任务栏窗口后以为程序退了,结果托盘图标还在,而且每周数据库文件还在增长,这是桌面向导软件最常见的「假退出」问题。
5. 避坑与常见问题排查:日程不提醒、时间错乱、界面卡顿这些坑我都踩过
5.1 提醒弹窗反复出现,像复读机一样停不下来
现象:某条日程的提醒时间到了以后,弹出提醒对话框,点击关闭后一分钟内又弹,无限循环。原因:ScheduleManager 的 notifiedIds 是存在内存的 QSet,如果程序重启或者 notifierId 没有持久化到数据库,每次轮询都会重新触发。解决:在 SqliteStorage 里加一个字段 notify_flag,每次触发提醒后 UPDATE 该字段为 1,查询可提醒日程时只查 notify_flag = 0 的。我的习惯是把「是否已提醒」作为表字段而不是运行时变量,因为程序随时可能被系统强制杀掉。
5.2 日期时间显示比系统时间早 8 小时
现象:数据库里存的时间是 14:00,显示出来变成 06:00。原因:QDateTime::toString(Qt::ISODate) 默认用本地时间,但某些 Qt 版本或系统区域设置下 toString 走的是 UTC。更常见的是从数据库读出来时没指定时区,QDateTime::fromString 默认认为是本地时间。解决:统一在写入时用 item.startTime.toLocalTime().toString(Qt::ISODate),读取时用 QDateTime::fromString(str, Qt::ISODate) 后不额外设置时区。设计规格:数据库里存「逝去世界的绝对时刻」还是「本地钟表读数」这里选了后者,因为个人日程没有跨时区开会场景。
5.3 日程列表数据量大时上下滚动卡顿
现象:日程数量到 500 条以上,滚动表格有明显迟滞。原因:数据模型没有做懒加载,每次用户滚动到新区域,data() 方法都会去 QVector 里取数据,这本身还好,但如果 MainWindow 在 model 的 dataChanged 信号里又触发了一次全表刷新,就会出现 O(n) 滚动性能。解决:第一,只在数据库增删改后发送 beginInsertRows / endInsertRows 或 dataChanged,不要每次刷新都 resets。第二,对已过滤的 QVector 做局部排序,QTableView 的 setSortingEnabled 打开后,sortByColumn 会自行整理,不需要每次都从 SQL 层重查。
5.4 在 Linux 上中文输入法无法输入标题
现象:AddScheduleDialog 的 QLineEdit 里敲不了中文,英文能输入。原因:大多数情况是 Qt 的输入法插件没加载,或者程序用了非 ascii 编码的字体对话框。解决:在 main.cpp 里设置 QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);检查 Qt 平台插件 libqinputcontextplugin.so 是否在 plugins/platforminputcontexts 目录;如果还不行,在 CMake 里强制链接 Qt6::Gui 的 inputcontexts 插件。这只影响输入法,不影响显示,所以之前有需求的中文用户反馈「能显示中文日程但输入中文就翻车」。
5.5 CMake 构建时找不到 Qt 模块
现象:提示找不到 Qt6Widgets / Qt6::Widgets 或 include 路径不对。原因:CMAKE_PREFIX_PATH 没设,或者 find_package 里写的 Qt6 但系统装的是 Qt5。解决:在 CMakeLists 最前面加 set(CMAKE_PREFIX_PATH "C:/Qt/6.5.0/msvc2019_64"),或从「Qt 的命令行」打开 CMake,那样环境变量自动带好。不要一边用 Qt5 的安装目录一边用 find_package(Qt6) 这样对不上的配置,报错会非常隐晦。
6. 二次开发与改造思路:把这套骨架扩展成你的定制提醒引擎
到了二次开发阶段,建议按三步走:第一步做日志,第二步做导入导出,第三步换提醒方式。先说日志——这个项目现在最缺的是操作留痕,加一个简单的 SqliteTable 命名为 schedule_log,记录 id、action、time、description,在 ScheduleManager 的增删改函数里各插一行日志。理由很现实:你会想知道为什么某条日程被误删了,以及数据从哪一天开始乱的。
第二步做 iCalendar(.ics)导出是最常见的需求。实现思路也很直白:遍历当前列表的 ScheduleItem,按 RFC 5545 拼成文本,写到 .ics 文件,这样就能和手机日历互通。小技巧是 ICS 时间格式为「UTC 时间 + Z 后缀」,比直接写本地时钟更通用,但若是仅在本机导入导出,保持一致即可。你可以给 MainWindow 加一个「导出」的 QAction,弹出 QFileDialog::getSaveFileName,用户选路径后走 ExportTask 函数。导入则反过来,读 .ics 并按字段映射回 ScheduleItem,注意处理打卡类重复日程。
第三步是提醒方式的改造。现在的弹窗提醒只停留在界面层,如果你想接到企业微信机器人、邮箱或系统通知,核心改动集中在 ScheduleManager 的 remindTriggered 信号之后。我一般会用一个 ReminderDispatcher 类,接收 scheduleReminded,然后根据 item.priority 分流:高优先级弹窗 + 发邮件,低优先级只弹托盘气泡。邮件发送用 QNetworkAccessManager 走 SendGrid API,不用 SMTP 是因为 smtp 库要引入额外依赖,API 方式代码量更小。改造完后的验证方式也简单:把系统时间往后调 2 分钟,看提醒是否在预期时间点触发,日志表里 record 是否新增。
从那以后我每次改提醒逻辑都会强制走一遍「改时间验证 + 查日志 + 重启应用再验证」三个动作,把遗漏的触发状态问题扼杀在测试环境里,而不是等用户来反馈「为什么又没提醒」。这个项目的价值不在于它多复杂,而在于每一个环节都给了你改的空间:想换数据层、换交互、加推送,都是从一个小口子切入,不伤筋动骨。希望帮到你。
本文还有配套的精品资源,点击获取