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

Qt实战教程:用Widgets+SQLite从零开发个人日程管理工具

简介面向有C基础、希望掌握Qt桌面应用开发的开发者这份资源围绕“个人日程安排处理事务”提供了一整套基于Qt的工程实践。资源以timemanager项目为核心覆盖日历选择、待办事项管理、定时提醒等功能贯穿Widgets布局、模型视图架构、QSql数据库存取、QTimer信号槽等关键知识点适合课程设计或项目参考。压缩包共89个文件容量10.83MB主要包含cpp/h源码、ui界面文件、工程配置、dat数据文件以及exe可执行程序可直接用Qt Creator或Visual Studio打开编译边看边跑。包内采用清晰的目录组织按时间与完成状态区分多个数据文件便于快速理解日程数据的持久化设计。已有468人浏览学习对正在学习Qt的开发者而言这是一份结构清晰、代码可复用的实战型案例。 有不少朋友问我Qt学完基础之后能做点什么拿得出手的东西我通常会建议做一个小而完整的工具类应用。今天分享的这个项目——“qt 个人日程安排处理事务”很适合拿来练手它能覆盖Qt桌面开发的主干流程界面设计、数据存取、定时任务、图表展示、应用打包。更重要的是它解决的是一个真实需求一个本地的、不依赖云端的个人时间管理工具。这个项目适合正在学习Qt的C开发者、需要做课程设计的同学以及打算把Qt作为主力桌面开发框架、想走一遍完整项目流程的工程师。我用Qt 5.15.2 Qt Widgets SQLite实现整体思路和代码都可以直接改造成待办清单、备忘录、甚至小型项目管理系统。下面把整个实现过程拆开详细讲。1. 整体设计与技术选型思路1.1 先理清需求日程管理到底要做什么个人日程安排工具核心功能听起来简单无非是记录某天某时要做什么。但实际落地时你会发现要处理的需求比想象中多日程项的增、删、改、查这是最基本的CRUD。按日期维度查看日程比如只看今天、本周、某一天的安排。到点提醒这是日程工具和普通记事本的本质区别。数据必须持久化程序关了再打开数据不能丢。界面要直观一眼能看到某天是否有安排。在动手之前把这些需求拆清楚非常关键。我见过不少初学者打开Qt Creator就开始拖控件拖到一半发现逻辑和数据模型没想清楚代码越写越乱。正确顺序应该先定数据结构再定界面布局最后才是写交互代码。1.2 技术选型为什么选Qt 5.15.2 Widgets SQLite技术选型我纠结过一阵子最终敲定三个核心组件Qt版本选5.15.2而不是6.x。5.15.2是Qt 5系列的最后一个长期支持版本稳定、资料多网上遇到的问题基本都有现成解决方案。Qt 6虽然性能更好但有些模块的接口有变化对新手不友好而且很多老教程都是针对Qt 5写的。如果你是企业项目上新可以从Qt 6起步但做个人项目学习5.15.2是性价比之王。界面框架选Widgets而不是QML。QML适合做界面效果华丽的应用比如动画、触摸交互但开发效率和对C开发者的友好程度不如Widgets。日程管理这类工具型应用界面以信息展示和表单操作为主用Widgets写起来更直接调试也方便。而且Widgets的知识是积累性的换到其他项目依然适用。数据存储选SQLite。Qt自带的QSQLITE驱动无需单独安装数据库服务整个数据库就是一个文件备份、迁移都很方便。个人工具这个量级SQLite的性能完全够用比用JSON或TXT自己管理数据要正规得多也为后面扩展统计功能埋下伏笔。1.3 整体架构界面层、逻辑层、数据层分离我用最简单也最实用的三层结构组织代码数据层封装一个DatabaseManager类负责数据库的打开、建表、增删改查。逻辑层封装日程数据模型ScheduleItem以及提醒检查逻辑。界面层主窗口和各个对话框只负责交互和展示不直接写SQL。这个分层的好处在于界面改动不影响数据逻辑换数据库也不影响界面代码。项目规模小不需要引入复杂的MVVM模式三层已经足够清爽。你别说这是小题大做等你需要给数据库加字段、修改提醒逻辑的时候就明白分层的重要性了。2. 界面布局与交互设计细节2.1 主窗口拆解日历 列表 详情三栏主窗口我采用的是左右布局左侧是QCalendarWidget日历右侧是日程列表和编辑按钮。右侧用垂直布局上方放QListWidget显示选中日期的日程下方放“新增”“编辑”“删除”三个按钮和一条日程状态标签。// 主窗口构造函数中的核心布局代码 QCalendarWidget *calendar new QCalendarWidget(this); QListWidget *scheduleList new QListWidget(this); QPushButton *btnAdd new QPushButton(新增日程, this); QPushButton *btnEdit new QPushButton(编辑日程, this); QPushButton *btnDelete new QPushButton(删除日程, this); QSplitter *splitter new QSplitter(Qt::Horizontal, this); splitter-addWidget(calendar); splitter-addWidget(scheduleList); splitter-setStretchFactor(0, 1); splitter-setStretchFactor(1, 2); setCentralWidget(splitter);用QSplitter的好处是运行时用户可以直接拖动分隔条有人习惯日历大一点有人习惯列表大一点交给用户自己调整。日历部件有个非常实用的信号selectionChanged()用户点击日期时通过在槽函数里重新查询数据库刷新右侧列表。有一个细节特别容易忽略日历默认选择的日期是程序启动当天的日期如果日程数据跨越多年建议设置日历的年份范围避免用户手动翻很多年才能找到某一天。我习惯把范围设为当前年份前后7年。2.2 新增和编辑日程对话框的实现新增和编辑共用一个对话框界面包含标题输入框、日期选择框、时间选择框、优先级下拉框、备注多行输入框。这里用QDialogButtonBox放置“确定”“取消”按钮配合accept()和reject()处理返回值。// 日程数据保存时的关键代码 void ScheduleDialog::onButtonBoxAccepted() { if (ui-titleEdit-text().trimmed().isEmpty()) { QMessageBox::warning(this, 提示, 日程标题不能为空); return; } QDateTime startTime(ui-dateEdit-date(), ui-timeEdit-time()); QDialog::accept(); }标题为空时拦截保存操作是每个表单类应用都该有的基础校验。至于日期和时间为什么要合并成一个QDateTime因为后面做提醒功能时需要用一个时间点去判断“是否到期”拆成两个字段存储反而增加复杂度。优先级的实现可以简单点一个QComboBox下拉框里面放“高、中、低”三档存数据库时用整数0/1/2表示。2.3 右键菜单与双击编辑交互细节决定体验除了按钮操作我加了右键菜单和双击编辑两个功能。列表项itemDoubleClicked信号直接打开编辑对话框右键菜单里放“编辑”“删除”两个动作删除时弹确认框。这些操作看起来不起眼但实际使用中你会发现高频操作全部依赖它们——鼠标移动到列表上双击就能改根本不需要移到下方按钮位置。右键菜单用QMenu实现在列表的customContextMenuRequested信号中弹出// 启用右键菜单需要设置这个属性 scheduleList-setContextMenuPolicy(Qt::CustomContextMenu); // 信号槽连接 connect(scheduleList, QListWidget::customContextMenuRequested, this, MainWindow::onScheduleListContextMenu); // 槽函数实现 void MainWindow::onScheduleListContextMenu(const QPoint pos) { QMenu menu(this); menu.addAction(编辑, this, MainWindow::onEditSchedule); menu.addAction(删除, this, MainWindow::onDeleteSchedule); menu.exec(scheduleList-mapToGlobal(pos)); }注意QListWidget默认的右键事件不会触发customContextMenuRequested必须在代码里显式调用setContextMenuPolicy(Qt::CustomContextMenu)。这个坑我踩过当时找了好久才发现是策略没设置对。3. 数据存储与CRUD实操3.1 数据库设计一张表搞定所有需求日程管理的数据结构不复杂我只建了一张表。但字段设计还是花了些心思核心表结构如下CREATE TABLE IF NOT EXISTS schedules ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, start_datetime DATETIME NOT NULL, priority INTEGER DEFAULT 1, note TEXT DEFAULT , status INTEGER DEFAULT 0 ); CREATE INDEX idx_start_datetime ON schedules(start_datetime);关键在于id自增主键同时作为QListWidgetItem的Qt::UserRole值。与列表项对应便于查找。start_datetime存ISO格式的日期时间字符串形如“2024-12-20T09:30:00”。为什么不存Unix时间戳因为SQLite的DATETIME函数可以直接处理这种字符串调试查看时也直观。status0表示未完成1表示已完成。给后续勾选完成、统计遗留任务留了扩展空间。索引idx_start_datetime一定要建虽然现在数据量小感觉不到差别但当日程积累到上千条按日期范围查询时索引带来的性能提升非常明显。3.2 数据库操作封装DatabaseManager类我封装了一个单例DatabaseManager在构造时打开数据库文件。数据库文件默认放在应用程序当前目录你也可以用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)放到用户数据目录。class DatabaseManager { public: static DatabaseManager instance(); bool addSchedule(const ScheduleItem item); bool updateSchedule(const ScheduleItem item); bool deleteSchedule(int id); QListScheduleItem getSchedulesByDate(const QDate date); QListScheduleItem getTodaySchedules(); private: explicit DatabaseManager(); QSqlDatabase m_db; };获取某一天的日程SQL语句这么写QListScheduleItem DatabaseManager::getSchedulesByDate(const QDate date) { QListScheduleItem result; QString dateStr date.toString(yyyy-MM-dd); QSqlQuery query(m_db); query.prepare(SELECT * FROM schedules WHERE substr(start_datetime, 1, 10) ? ORDER BY start_datetime); query.addBindValue(dateStr); if (query.exec()) { while (query.next()) { ScheduleItem item; item.id query.value(id).toInt(); item.title query.value(title).toString(); item.startDateTime QDateTime::fromString(query.value(start_datetime).toString(), Qt::ISODate); item.priority query.value(priority).toInt(); item.note query.value(note).toString(); item.status query.value(status).toInt(); result.append(item); } } return result; }用substr(start_datetime, 1, 10)截取日期部分和传入的日期字符串比较逻辑简单直观。所有查询都用prepareaddBindValue这是一种好习惯。第一个好处是防止SQL注入第二个是SQLite会缓存查询计划多次执行时性能更好。虽然个人工具不存在被攻击的问题但好习惯是从小项目养成的。3.3 列表刷新与数据模型同步日历选中日期改变时主窗口需要重新查库并刷新列表。这里的核心是把数据库里的id绑定到列表项上void MainWindow::refreshScheduleList(const QDate date) { scheduleList-clear(); QListScheduleItem items db.getSchedulesByDate(date); for (const ScheduleItem item : items) { QListWidgetItem *listItem new QListWidgetItem( QString(%1 %2).arg(item.startDateTime.toString(HH:mm), item.title), scheduleList ); listItem-setData(Qt::UserRole, item.id); } }编辑、删除时通过scheduleList-currentItem()-data(Qt::UserRole).toInt()拿到id再调用数据库接口。这里有一个新手容易犯的错误直接用列表项的文本去比对数据库。如果两个日程标题相同就会出现改错、删错的情况。用id关联才是稳妥做法。4. 提醒功能与可视化扩展4.1 到点提醒QTimer轮询方案提醒功能的实现方案我对比过两种一种是用QTimer每30秒轮询数据库检查是否有日程时间匹配当前时间窗口另一种是计算距下一个日程的秒数用单次定时器等待。前者实现简单且可靠性高后者看似优雅但一旦日程变更需要重置定时器逻辑反而复杂。个人工具我选了轮询方案。// 提醒检查的核心逻辑 void MainWindow::checkReminder() { QDateTime now QDateTime::currentDateTime(); QDateTime windowStart now.addSecs(-60); // 检查过去1分钟到未来1分钟 QDateTime windowEnd now.addSecs(60); QSqlQuery query(db.database()); query.prepare(SELECT title, start_datetime FROM schedules WHERE start_datetime BETWEEN ? AND ? AND status 0); query.addBindValue(windowStart.toString(Qt::ISODate)); query.addBindValue(windowEnd.toString(Qt::ISODate)); // ... 执行查询并弹出提醒 }实操心得轮询时间窗口取“前后各1分钟”而不是“正好等于当前时间”。因为程序不可能精确到秒触发而且如果程序一直在运行等于当前时间的查询只会在那一瞬间命中一次很容易漏掉。有了1分钟的缓冲窗口即使某次检查稍有延迟依然能捕获到期日程。弹出提醒时我用的是QMessageBox加提示音。如果希望不打扰当前操作可以换成右下角气泡通知但气泡通知在Windows上需要额外处理个人工具用QMessageBox最省心。4.2 未完成日程补提醒跨程序重启的处理程序启动时必须检查“已经过期但还没完成的日程”这是日程工具最容易漏掉的一个场景——比如昨晚设了今天早上9点的提醒但程序没开。启动时补提醒逻辑如下void MainWindow::checkMissedSchedules() { QDateTime now QDateTime::currentDateTime(); QSqlQuery query(db.database()); query.prepare(SELECT title, start_datetime FROM schedules WHERE start_datetime ? AND status 0); query.addBindValue(now.toString(Qt::ISODate)); // ... 逐个弹出“您有N条未完成日程”的提示 }补提醒的信息要聚合不能一条弹一个框否则有20条过期日程就弹20个框用户会崩溃。我是在启动时把过期日程汇总成一条QMessageBox显示“您有N条未完成的日程点击查看”点击后跳转到过期日程列表。4.3 统计可视化用QCustomPlot画每周任务量日程数据攒多了之后列表展示已经不够直观。我加了一个统计页用第三方图表库QCustomPlot画柱状图展示最近7天每天的任务数量。搜索词里提到的“qcustomplot kissfft时域到频域波形”虽然主要用在信号处理场景但QCustomPlot这套封装好的图形库画柱状图、折线图同样非常顺手。// 每周任务量柱状图绘制核心代码 void StatsWidget::updateWeeklyChart() { QCustomPlot *plot new QCustomPlot(this); QVectordouble xData, yData; // 循环7天统计每天任务数并填充xData/yData QSharedPointerQCPAxisTickerText textTicker(new QCPAxisTickerText); // 设置x轴刻度标签为周几 plot-addGraph(); plot-graph(0)-setData(xData, yData); plot-xAxis-setTicker(textTicker); plot-rescaleAxes(); plot-replot(); }QCustomPlot的使用要点每次更新数据后必须调用replot()否则界面不会刷新。这个库默认是GPL协议个人使用没问题商用则要注意开源协议限制。如果你在意协议可以用Qt自带的QChart模块代替但QChart的实时刷新性能稍差对日程统计这种低频刷新场景倒是够用。4.4 数据备份给用户多一点安全感本地数据最怕文件损坏。我在“工具”菜单里加了“导出数据库备份”选项实际上就是把SQLite文件复制到用户选择的路径。这个功能实现只需要QFile::copy()但价值很大——我自己的习惯是每周导一次备份到U盘出问题能随时找回。5. 打包发布与常见问题排查实录5.1 Windows打包windeployqt的正确使用姿势开发调试没问题打包发布是另一个坎。Qt程序在开发机上正常运行换到别的电脑就跑不起来的现象太常见了。原因很简单开发机装了完整的Qt运行库目标电脑没有。Windows下我习惯用官方提供的windeployqt工具# 先将程序编译为Release版本然后执行 cd D:\build\ScheduleApp\release D:\Qt\5.15.2\msvc2019\bin\windeployqt.exe ScheduleApp.exe这条命令会自动把Qt的DLL、插件、资源文件复制到exe所在目录。执行完后把整个目录打包成zip发给别人就能直接运行了。有个关键点必须注意mingw版本和msvc版本的Qt库不能混用。如果你用Qt的MinGW编译器就要找到对应的windeployqt.exe同时确保目标电脑安装了对应的Microsoft Visual C Redistributable。MSVC编译的程序依赖VC运行库MinGW则静态链接了相关库。5.2 经典报错no qt platform plugin could be initialized这是搜索词里出现频率非常高的问题也是在未安装Qt的电脑上运行Qt程序最常见的错误。报错信息包含“could not find or load the Qt platform plugin windows”之类的文字根本原因是程序找不到platforms目录下的qwindows.dll。排查步骤按顺序做检查exe所在目录有没有platforms文件夹里面有没有qwindows.dll。如果从其他电脑复制过来的程序大概率少了这个。检查platforms文件夹是否和exe在同一级目录不是的话Qt按照相对路径找不到。检查DLL位数是否匹配64位程序要配64位的qwindows.dll混用必然报错。确认环境变量QT_QPA_PLATFORM_PLUGIN_PATH没有指向错误路径。正常情况下执行完windeployqt后platforms文件夹会自动生成不需要手动处理。如果你是自己手工整理的依赖这个报错就很容易出现。5.3 中文字体、乱码和编码问题Qt 5的QString默认使用UTF-16编码但在Windows上控制台和文件系统的编码习惯和Linux不同乱码问题几乎每个新手都会遇到。我的建议是源码文件统一用UTF-8编码并在顶部声明字符串硬编码时使用QString::fromUtf8()或直接用QStringLiteral宏。写代码时避免使用tr(中文)而不配备翻译文件否则lupdate和lrelease工具会把这些中文字符串移除。个人工具可以直接用QStringLiteral。读取外部文本文件时明确指定编码QTextStream::setCodec(UTF-8)不要依赖系统默认编码。5.4 Linux下部署和交叉编译补充搜索词里提到了“linux找qt路径”“qt离线安装 麒麟x86”等场景。在Linux平台打包相对复杂因为不同的发行版依赖库版本不一样。个人工具的临时解决办法是使用linuxdeployqt工具但它只对基于Ubuntu/Deepin的体系支持较好。如果是国产麒麟等系统本身自带Qt库的情况下直接用ldd命令查看程序依赖把缺失的Qt库复制到程序目录配合启动脚本设置LD_LIBRARY_PATH。这个方案不完美但胜在快速可用。关于“qt命令行”——Qt开发除了图形界面命令行工具也很有用。lupdate、lrelease、windeployqt、moc等都是命令行工具学会在终端调用它们比在IDE里点来点去效率更高。6. 后续扩展方向与开发心得这个日程工具做好之后我持续迭代了几轮加了几个效果不错的功能按优先级显示不同颜色标签、支持拖拽调整日程顺序、导出为CSV文件到Excel。技术层面也做了增强把数据库访问改成异步执行避免数据量大时界面卡顿用QSystemTrayIcon做系统托盘程序最小化到托盘后依然能收到提醒。回想整个开发过程我最大的体会是Qt项目做得顺不顺一半取决于你对Qt框架的熟悉程度另一半取决于你对业务需求的分析是否透彻。日程管理这个项目看着简单但真正处理起“跨天提醒”“过期补提醒”“数据备份恢复”这些场景时每一项都需要认真设计。我在做“过期补提醒”功能时一开始只想着启动时查一次数据库后来发现程序运行中也可能出现休眠恢复后错过提醒的情况最后改成在QTimer的每次触发里都检查过期时间逻辑才真正闭环。如果你也想试试这个项目我建议你按这样的顺序走先把CRUD做通再做日历联动接着加提醒最后考虑可视化。每一步加上测试和重构整个项目下来你对Qt的掌握会从“会写代码”提升到“能设计程序”的层次。最后分享一个小经验日程数据是你的“日记”开发时可以多造一些测试数据最好覆盖“今天早上已过期”“明天下午有空闲”“下周三有重要会议”等不同场景这样测试提醒和统计功能时才不至于无数据可用。我就是靠这个方法在联调阶段快速发现了好几个隐藏的逻辑问题。本文还有配套的精品资源点击获取
分享:

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

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