简介:基于Qt的个人日程管理程序资源包,面向希望掌握C++/Qt界面开发的初中级开发者,解决个人事务记录、日期选择与到期提醒等常见管理需求。压缩包共89个文件,大小10.83MB,包含C++源码与头文件各15个、Qt界面设计文件6个,以及工程配置、编译调试产物和日程数据文件,整体结构清晰,可用Qt Creator直接打开编译运行。已有469人学习浏览。借助该工程可熟悉日历控件、表格视图、定时器调用等核心组件,理解模型视图架构、信号槽交互以及数据持久化具体写法;任务类封装、界面布局与数据库表结构也都有可直接参考的示例。多平台编译配置一并提供,同时附带可执行程序与数据文件,便于直接运行观察效果,适合作为个人日程管理项目的起步框架和二次开发模板。
1. qt个人日程安排处理事务:源码包解决什么、适合谁、边界在哪
qt个人日程安排处理事务这类源码包,第一眼看上去像“课程作业全家桶”,但真正跑起来你会发现,它把桌面日历、事务记录、超期提醒这几件实际需求都串起来了。适合两类读者:一是要交Qt课程设计、需要完整可运行项目结构的人;二是想做个人日程工具、不想从零写数据库和界面的人。它解决的问题很具体——每天打开电脑就能看到今天什么事、哪些事超期了、提前多久提醒,所有数据存在本地,不依赖任何在线服务。我拆过的同类型包里,这套的代码组织算是比较规矩的。下面按“选型—编译—拆代码—排坑—进阶”的顺序讲透,照着敲就能跑。
2. 架构与选型:MVC分层、SQLite存储与轮询提醒为什么够用
2.1 三层架构:界面、数据与调度各管各的
拿到一份Qt个人日程源码,别急着编译,先看目录和类名。大多数可维护版本都遵循MVC思想:视图层是QCalendarWidget和QTableView的配合,负责日历翻页和列表展示;模型层是QStandardItemModel,负责把数据库结果映射成表格行;控制层是主窗口类里的槽函数,负责增删改查的入口分发。我一般会先找MainWindow这个类,看它构造了哪些成员。规范做法的分工是:窗口持有数据库连接对象、持有QTimer成员用于调度提醒、持有QStandardItemModel用于绑定表格。三个成员齐了,说明这个包的结构是干净的;如果看到在槽函数里new QTableView这种写法,趁早换一个版本。
拿到源码包后,可以先跑一句编译前的摸底命令,不用翻遍每个文件就能知道核心逻辑在哪:
grep -rn "QTimer\|QSqlDatabase\|QSystemTrayIcon\|QStandardItemModel" MainWindow.* | head -20逻辑说明:这条命令一次性把四个关键类在MainWindow相关文件里的出现位置列出来,哪几个类出现次数多,那个模块就是源码的核心骨架。参数说明:head -20限制输出前20行,避免列表刷新类方法里每行匹配都刷屏;通配符MainWindow.只检查主窗口两个文件,如果源码拆得更散,改成.cpp *.h就能覆盖全工程。
2.2 为什么是SQLite而不是JSON或文本文件
个人日程这个场景,数据量撑死几千条,用JSON文件存也“能跑”。但源码包里用SQLite明显更靠谱,核心原因是事务。一次日程修改涉及“更新事件表、刷新表格、写日志”三个动作,如果换成JSON,你得手动读写整个文件,中途断电就丢数据。SQLite一条UPDATE配合事务,要么全部生效要么全部回滚,配套QSqlQuery的prepare绑定,还能顺手防SQL注入。
SQLite另一个被忽略的实际优势是查询可以走索引。日程表一般会在date字段上建索引,按日期拉取当天列表时,走索引比遍历JSON数组快几个数量级。个人使用量级下体验不出差别,但源码如果按日期过滤的查询没建索引,后面数据多了会肉眼可见地卡顿。
2.3 提醒轮询:QTimer定时器为什么比单次定时更稳
提醒模块最常见的翻车点是“提前X分钟提醒”变成“只提醒一次然后失效”。用QTimer::singleShot一次性定时,跨天、休眠恢复、手动改系统时间都会让定时结果不可控。可靠做法是固定轮询周期,比如每10秒或每30秒扫一次事件表,凡是“提醒时间 <= 当前时间 <= 事件开始时间”的记录就弹一次提醒并立即标记已提醒。
轮询的代价是每周期执行一次SELECT查询,几千条记录的桌面包完全不是问题,别担心性能。代码里如果看到while循环加sleep做轮询,那是阻塞UI的写法,要改成QTimer配合槽函数。判断提醒模块是否合格,就看三点:定时器周期是否可配置、查询是否绑定参数、以及是否用status字段防止重复提醒。
3. 编译与首跑:CMake工程结构、数据库初始化和主窗口挂载
3.1 源码包的文件结构:先看这几个文件
下载解压后,根目录一般是这样的组织:CMakeLists.txt、main.cpp、MainWindow.h、MainWindow.cpp,外加一个qrc资源文件和resources目录用于放图标,数据库文件schedule.db首次运行自动生成,所以如果打开目录发现没有db文件,不用慌。课程设计类的包还会带一个docs目录放设计文档和ER图,那部分对答辩有用,对跑代码用处不大。先看CMakeLists.txt里的Qt版本要求,用Qt5还是Qt6决定了本地环境怎么准备。
如果CMakeLists里出现find_package(Qt6 COMPONENTS Widgets Sql),你需要Qt6对应版本;如果出现Qt5::Widgets,Qt5.15也能编译。两者在本场景用的API基本一致,唯一要留意的是高DPI缩放策略,Qt6默认开启,Qt5要手动设置AA_EnableHighDpiScaling,否则在2K屏上界面会糊。
3.2 CMake编译:一条命令跑通
我把这个包的编译配置整理成Qt5/Qt6都兼容的写法,避免你因为版本卡住第一步:
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) find_package(Qt6 COMPONENTS Widgets Sql QUIET) if(NOT Qt6_FOUND) find_package(Qt5 5.15 COMPONENTS Widgets Sql REQUIRED) endif() add_executable(schedule_app main.cpp MainWindow.cpp MainWindow.h) if(Qt6_FOUND) target_link_libraries(schedule_app PRIVATE Qt6::Widgets Qt6::Sql) else() target_link_libraries(schedule_app PRIVATE Qt5::Widgets Qt5::Sql) endif()逻辑说明:find_package先探测Qt6,找不到就自动降级到Qt5;add_executable把三个源文件一起编译,头文件写进去是为了让IDE识别类关系并生成moc;target_link_libraries决定链接哪个版本的Qt模块。CMAKE_AUTOMOC必须开,MainWindow里有Q_OBJECT宏,关闭它编译会报“未定义的vtable”一类错误。
参数说明:CMAKE_CXX_STANDARD 17是Qt6推荐标准,Qt5用C++11也能过,统一写17两个版本都兼容。如果Qt安装在非默认路径,需要在cmake命令里加 -DCMAKE_PREFIX_PATH=/path/to/qt安装目录,Windows下通常是C:/Qt/6.5.0/mingw_64。
编译运行命令:
cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j4 ./build/schedule_app逻辑说明:第一行指定构建目录并生成Release配置,第二行并行编译减少等待,第三行直接运行。如果编译时报“找不到Qt6Widgets”,多半是CMAKE_PREFIX_PATH没指对;报“fatal error: QSqlDatabase: No such file”,则是只装了Widgets没装Qt Sql模块,重装时勾上Sql组件即可。
3.3 数据库初始化:表结构设计与主窗口挂载
main.cpp是这个程序最先执行的地方,规范写法是先建表、再显示窗口,避免窗口弹出后查空表还要做一层容错。默认连接不传名称,后续所有QSqlQuery都从默认连接取数,这是最省事也最不容易写错的用法。
#include <QApplication> #include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError> #include <QDebug> #include "MainWindow.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setApplicationName("qtSchedule"); QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("schedule.db"); if (!db.open()) { qWarning() << "数据库打开失败: " << db.lastError().text(); return 1; } QSqlQuery query(db); query.exec( "CREATE TABLE IF NOT EXISTS events (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "title TEXT NOT NULL," "date TEXT NOT NULL," "time TEXT NOT NULL," "remind_minutes INTEGER DEFAULT 10," "note TEXT," "status INTEGER DEFAULT 0)"); if (query.lastError().type() != QSqlError::NoError) { qWarning() << "建表失败: " << query.lastError().text(); } MainWindow w; w.show(); return app.exec(); }逻辑说明:addDatabase("QSQLITE")注册一个默认连接,setDatabaseName指定库文件;open失败说明当前目录不可写或路径非法,返回1结束程序。create table用IF NOT EXISTS保证二次启动不报错。日期和时间分开存字符串,是为了查询时直接按date过滤,不需要再做时间戳换算。remind_minutes存提前提醒分钟数,status字段标记0未提醒、1已提醒,这是防止重复弹通知的关键。
参数说明:库名用相对路径schedule.db,程序启动时会生成在当前工作目录。要固定目录可以改成绝对路径,但注意先手动创建目录,否则sqlite在目录不存在时直接open失败。AUTOINCREMENT不是必须的,INTEGER PRIMARY KEY本身会自增;加上它只是避免重用已删除的id,对日程场景意义不大,但保留无妨。
提示:Windows下如果双击运行出现“数据库打开失败”,很可能是工作目录在系统保护目录,换到普通用户目录再跑就行。
4. 核心逻辑拆解:新增日程、列表刷新与超期提醒的代码实现
4.1 新增日程:表单校验与预处理
窗口上通常是一个“标题输入框+日期控件+时间控件+提前提醒分钟数+备注输入框”的表单。点击保存后如果直接把控件值拼进SQL字符串,那是最危险的写法,既容易被注入,也容易因为格式不统一出问题。规范的流程是先trim、再校验、最后走绑定参数入库。
#include <QMessageBox> bool MainWindow::handleAddEvent() { QString title = ui->titleEdit->text().trimmed(); QDate date = ui->dateEdit->date(); QTime time = ui->timeEdit->time(); int remindMin = ui->remindSpin->value(); QString note = ui->noteEdit->toPlainText().trimmed(); if (title.isEmpty()) { QMessageBox::warning(this, QStringLiteral("提示"), QStringLiteral("日程标题不能为空")); return false; } if (remindMin > 24 * 60) { remindMin = 24 * 60; } QSqlQuery query(m_db); query.prepare("INSERT INTO events (title, date, time, remind_minutes, note) " "VALUES (?, ?, ?, ?, ?)"); query.addBindValue(title); query.addBindValue(date.toString(Qt::ISODate)); query.addBindValue(time.toString(QStringLiteral("HH:mm"))); query.addBindValue(remindMin); query.addBindValue(note); if (!query.exec()) { qWarning() << "插入失败: " << query.lastError().text(); return false; } refreshTableView(date); return true; }逻辑说明:trim去掉首尾空格,避免“开会”和“开会 ”变成两条。标题为空直接拦截,这是入库前最基础的防线。提前提醒分钟数上限设为1440(一整天的分钟数),防误填“30000分钟”这种数据。prepare五参数绑定顺序和INSERT字段一一对应,避免字符串拼接出现的引号问题。日期格式用Qt::ISODate(yyyy-MM-dd),时间用HH:mm,这两个格式按字典序排序等价于按时间排序,后续ORDER BY不用做转换。
参数说明:dateEdit允许选过去日期,这个不用拦,有人就是为了补记几天前的事。真正的兜底放在提醒逻辑里:早于当前时间的事件永远不会命中提醒窗口,所以不会弹出过期通知。setValue的上限也可以在ui文件里直接配成1440,但代码里再兜一层更稳。
4.2 列表刷新:模型与视图的配合方式
刷新列表按“清空模型、重设表头、重查数据库、填充行数据”四步走。用QStandardItemModel是最轻量的方案,不用自己继承QAbstractTableModel,个人日程这种列数固定、行数几十条的场景完全够用。
void MainWindow::refreshTableView(const QDate &date) { m_model->clear(); m_model->setHorizontalHeaderLabels( {QStringLiteral("标题"), QStringLiteral("时间"), QStringLiteral("备注"), QStringLiteral("状态")}); QSqlQuery query(m_db); query.prepare("SELECT title, time, note, status FROM events " "WHERE date = ? ORDER BY time ASC"); query.addBindValue(date.toString(Qt::ISODate)); if (!query.exec()) { qWarning() << "查询失败: " << query.lastError().text(); return; } int row = 0; while (query.next()) { QString title = query.value(0).toString(); QString time = query.value(1).toString(); QString note = query.value(2).toString(); int status = query.value(3).toInt(); m_model->setItem(row, 0, new QStandardItem(title)); m_model->setItem(row, 1, new QStandardItem(time)); m_model->setItem(row, 2, new QStandardItem(note)); QStandardItem *statusItem = new QStandardItem( status == 0 ? QStringLiteral("未提醒") : QStringLiteral("已提醒")); statusItem->setTextAlignment(Qt::AlignCenter); m_model->setItem(row, 3, statusItem); if (status == 1) { for (int col = 0; col < 4; ++col) { m_model->item(row, col)->setForeground(QColor(160, 160, 160)); } } ++row; } ui->tableView->setModel(m_model); ui->tableView->resizeColumnsToContents(); }逻辑说明:clear把旧行全部移除并破坏选中状态,再重设表头,保证重新查询后没有脏数据残留。查询按date过滤并按time升序,表格里的顺序就是一天的时间轴。query.next()每调用一次往下走一行,读出四列数据后分别setItem放进模型。已提醒的行整行置灰,视觉上直接区分“已完成”和“未提醒”。resizeColumnsToContents让四列宽度自适应内容。
参数说明:setItem(row, col, item)里row从0开始对应表格第一行;状态列必须给值,否则会出现空列。如果某行状态为1,循环里用item(row,col)->setForeground改色,注意此时item一定存在,因为上一行刚set过。这里查询结果一般几十行,放在主线程毫无压力。
4.3 超期提醒:轮询与状态标记
提醒模块是源码包的核心重头戏。以下代码把提醒逻辑写成定时器加槽函数的形式,避开阻塞UI的陷阱。
void MainWindow::startReminderTimer() { m_timer = new QTimer(this); m_timer->setInterval(10000); // 每10秒扫描一次 connect(m_timer, &QTimer::timeout, this, &MainWindow::checkReminders); m_timer->start(); } void MainWindow::checkReminders() { QDateTime now = QDateTime::currentDateTime(); QSqlQuery query(m_db); query.prepare("SELECT id, title, date, time, remind_minutes " "FROM events WHERE status = 0"); if (!query.exec()) { qWarning() << "提醒查询失败: " << query.lastError().text(); return; } while (query.next()) { int id = query.value(0).toInt(); QString title = query.value(1).toString(); QDate date = QDate::fromString(query.value(2).toString(), Qt::ISODate); QTime time = QTime::fromString(query.value(3).toString(), "HH:mm"); int remindMin = query.value(4).toInt(); QDateTime eventStart(date, time); QDateTime remindTime = eventStart.addSecs(-remindMin * 60); if (!now.isValid() || !eventStart.isValid()) { continue; } if (now >= remindTime && now <= eventStart && now.date() == eventStart.date()) { showRemindNotification(title, eventStart); QSqlQuery update(m_db); update.prepare("UPDATE events SET status = 1 WHERE id = ?"); update.addBindValue(id); update.exec(); } } }逻辑说明:startReminderTimer只在窗口初始化时调用一次,10秒一个周期。checkReminders每次只捞status=0的记录,已提醒的不再参与查询,数据集逐渐变小。remindTime = eventStart - remindMin*60计算出提醒窗口的起点;只有当“当前时间落入窗口”且“当前日期等于事件日期”时,才弹通知。弹完立刻把status置1,就算下一次周期只有0.1秒后执行,也不会重复弹第二条。用新的QSqlQuery对象执行UPDATE,是为了避免覆盖SELECT的结果集。
参数说明:轮询周期10000毫秒等于10秒,这是个人日程的最佳折中。改成1秒会造成无意义的全表扫描加上数据库锁竞争;改成60秒,用户提前2分钟设置的提醒最长会晚58秒才弹。now在函数开头只取一次,整个循环共享同一个时间基准,避免循环期间跨秒导致同一条数据被判断两次。
提示:setInterval的单位是毫秒,10000是10秒。如果写成10,那就是每10毫秒扫一次,数据库会一直被唤醒,窗口卡顿到没法用。
5. 避坑排查:日期跨天、中文乱码、数据库锁与托盘驻留的四条记录
5.1 跨天提醒不弹或反复弹
现象:日程设在23:59、提前10分钟提醒,23:50没有任何通知弹出;另一种情况是昨天设置的日程没处理,今天打开程序后提醒一直弹。
原因:第一类是提醒计算时只拼接时间和当前日期,没有把事件本身日期纳入比较,导致提醒窗口落在“昨天加今天”的混合时间轴上。第二类是提醒命中后没有及时更新status,或者更新语句条件写错,导致已经提醒过的记录每次都命中同一个窗口。
解决:所有比较统一用QDateTime完成,窗口边界用eventStart.addSecs(-remindMin * 60)计算,命中条件中必须加now.date() == eventStart.date()。轮询函数里取一次now,循环过程中不要重新调currentDateTime,避免循环执行中途跨秒导致时间基准不统一。这些是我排过几次跨天bug之后固定下来的写法,能挡住绝大多数日期边界问题。
5.2 中文乱码:显示成“锟斤拷”或问号
现象:界面按钮、表格内容、弹窗标题里的中文全部变成乱码,显示成类似“锟斤拷”或问号方块,英文字符、数字和标点完全正常,整个程序看起来像用错误字符集渲染了一遍。
原因:源码文件实际编码与编译器默认解析编码不一致。最常见的组合是GBK编码源码被按UTF-8读取,或者UTF-8源码被Windows下的编译器按系统代码页GBK解析。Qt5和Qt6在Windows下对窄字符串的处理默认跟随系统区域,代码里直接写中文literal特别容易翻车。
解决:先把源码文件统一为UTF-8(不带BOM),并在main.cpp顶部加编译提示:
#if defined(_MSC_VER) #pragma execution_character_set("utf-8") #endif逻辑说明:这行pragma只对Windows下的编译器生效,告诉编译器把execution character set设置成UTF-8,解决源码是UTF-8但默认代码页是GBK的问题。参数说明:预定义宏_MSC_VER只在Windows编译器环境中存在,GCC和Clang环境不会进入这个分支,所以这段代码放在跨平台工程里也安全。
第二步,所有面向用户的中文字符串用QStringLiteral()包裹,不使用裸字面量。QStringLiteral在编译期按UTF-8转成QString,运行时不依赖系统代码页。从论坛下载的源码包大概率是GBK,用编辑器批量转码后重新编译即可。
5.3 数据库被锁:反复报“database is locked”
现象:程序运行一段时间后,新增日程或更新状态时偶发报错,错误文本是database is locked,重试几次又恢复正常,完全没有规律。
原因:SQLite写锁是全局的。当某个写事务没有及时提交或回滚,下一轮查询又以新的QSqlQuery对象尝试写库,就会碰到锁。个人日程程序没有多线程也能触发这个错误,最常见的原因是代码里手动BEGIN后没有COMMIT,或者程序里创建了多个QSqlDatabase实例各自管理连接。
解决:全程序只调用一次addDatabase,所有QSqlQuery通过同一个m_db成员构造;手动事务务必成对出现,open事务后要么commit要么rollback。写成prepare加addBindValue比直接exec短语句更稳,因为预编译语句占用的时间短。如果还锁,检查是否有外部程序正打开同一份schedule.db,比如数据库查看工具,关掉它再跑程序。
5.4 关掉窗口进程还在:托盘驻留导致“关不掉”
现象:点击窗口右上角关闭按钮,界面瞬间消失,但任务管理器里进程还在,再次启动程序提示数据库被占用,只有强制结束进程才能恢复。
原因:窗口重写了closeEvent或者调用了setQuitOnLastWindowClosed(false),把关闭动作改成了隐藏。如果代码里又创建了QSystemTrayIcon且没有提供明确退出动作,app.exec()就永远不会返回,进程自然留在后台。
解决:在closeEvent里区分“关闭即退出”和“隐藏到托盘”两种意图。简单方案是保留默认行为,关闭即退出;做托盘版本就一定要在托盘菜单里放“退出”动作,并调用qApp->quit(),退出前隐藏托盘图标和移除通知气泡。如果程序不需要常驻,最省事的做法是不要在代码里写setQuitOnLastWindowClosed(false),保持Qt默认。
6. 进阶技巧:最小化到托盘、开机自启与提醒时间配置的习惯
6.1 托盘与开机自启的补全写法
当基础增删改查和提醒跑通后,真正让个人日程工具“像样”的是三个体验细节:最小化到托盘、开机自启、提醒时间配置。源码包里如果没做,可以自己补,代码量不大。
托盘驻留的写法是在主窗口构造函数里创建QSystemTrayIcon,绑定菜单,菜单里放“显示主界面”和“退出”两个动作。注意退出动作必须调用qApp->quit(),不能只hide(),否则就是避坑章里说的“进程关不掉”。
#include <QSystemTrayIcon> #include <QMenu> #include <QStandardPaths> trayIcon = new QSystemTrayIcon(this); trayIcon->setIcon(QIcon(":/icons/app.png")); trayMenu = new QMenu(this); trayMenu->addAction(QStringLiteral("显示主界面"), this, &MainWindow::showNormal); trayMenu->addAction(QStringLiteral("退出"), qApp, &QApplication::quit); trayIcon->setContextMenu(trayMenu); trayIcon->show();逻辑说明:setContextMenu给托盘图标绑定右键菜单。addAction第一个参数是菜单文字,第二个是接收者,第三个是槽函数地址。显示主界面绑到showNormal,退出绑到qApp的quit,点击后整个事件循环结束。参数说明:托盘图标指向qrc资源文件,如果没做资源文件,运行时图标显示为空白,这是最常见的“托盘不可见”原因。
开机自启在Windows下往启动文件夹放快捷方式最稳,不依赖注册表权限:
QString lnkPath = QStandardPaths::writableLocation( QStandardPaths::ApplicationsLocation) + QStringLiteral("/schedule_app.lnk"); QFile::link(QCoreApplication::applicationFilePath(), lnkPath);逻辑说明:QFile::link在Windows创建快捷方式,目标指向当前exe;ApplicationsLocation对应启动文件夹,放进这里的快捷方式会在用户登录后自动启动。参数说明:绿色版程序不建议依赖原路径创建自启,因为exe目录变化会导致快捷方式失效;先拷到固定目录再创建。
6.2 提醒时间要能逐条配置,不要全局一刀切
提醒分钟数建议保留在表单里,让用户每条日程单独修改,而不是把所有日程的提醒时间固定成一个全局值。真实使用场景里,会议要提前10分钟,交材料要提前一天,两类提醒放同一套参数里必定有一边失效。
很多日程源码包把提醒时间写死在代码里,改起来要重新编译,这种我很不喜欢。拿到包第一件事就是检查这一条:remind字段是“全局唯一”还是“逐条可配”,逐条可配的版本才值得复用。我当年用过一个全局提醒的工具,所有日程固定提前10分钟,结果需要提前一天准备的材料当天完全没想起来。从那以后我每次拿到日程类源码包,第一件事就是检查提醒字段和提醒状态位的设计。多花两分钟看这两个字段,后面能省掉大量使用时的火气。希望帮到你。
本文还有配套的精品资源,点击获取