ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Delphi打造本地日程管理工具:JSON存储与提醒机制实现全解析

用Delphi打造本地日程管理工具:JSON存储与提醒机制实现全解析 简介一套基于Delphi FMX框架的Android日程管理APP示例工程面向移动开发入门者及跨平台实践者覆盖日程数据管理场景下的界面交互与存储实现。压缩包共27个文件类型包含pas源代码、fmx窗体、vlb可视布局、SQLite数据库文件.db及配套SQL脚本同时包括项目配置、部署文件与自定义ListView外观包整体仅46KB轻量紧凑、模块清晰。目前已有474人学习/下载。工程完整演示了SQLite数据库建表与增删改查、FMX自定义ListView样式与滑动/长按手势、调用Android系统分享组件、半透明背景提示框设计等关键技术附带vkbdhelper虚拟键盘辅助单元和ListView外观增强包目录按窗体、数据层与工具单元划分可直接导入Delphi IDE运行。借助完整的工程文件与SQL初始化脚本读者能快速掌握Delphi移动开发中数据持久化、控件定制及系统能力调用的常见方法适合作为课程设计或入职练手项目参考。 事情是这样的我手头有一堆零散日程——会议、截稿日期、给客户回电话、周末去保养车之前一直用手机日历但坐在电脑前写代码的时候切到手机去记一笔总觉得别扭一来打断思路二来手机日历的重复提醒设置得越细越麻烦。网上找了几款桌面日历要么捆绑安装要么强制登录要么日历视图花里胡哨但连基础提醒都做不好。一怒之下我决定用 DELPHI 自己写一个日程管理 APP目标是双击就能跑、数据本地存、提醒不漏、改起来顺手。这篇文章就把整个开发过程、关键代码和踩过的坑完整记录下来。这个工具适合谁如果你也是 Delphi 开发者或者你手里有个旧工具需要维护又或者你只是想找一个可以完全掌控数据的桌面日程方案那这篇笔记应该能帮你省不少时间。我不会只贴成品代码而是把每一处设计决策的“为什么”也讲清楚这样你拿到之后可以按自己的习惯改。1. 为什么这个年头我还在用Delphi写日程管理1.1 一个老Delphi开发者的真实场景我平时的主要工作还是围绕 Windows 桌面端的业务系统开发Delphi 对我来说不是“上古传说”而是每天都在用的生产力工具。项目里有大量现成的 VCL 组件、公司内部封装好的公共库、还有跑了好多年的业务逻辑——这些东西用其他语言重写一遍成本极高也不划算。日程管理这个需求本质上就是一个“轻量工具类”应用界面要简单、启动要快、数据要可控、提醒要稳定。用 Delphi 写这种工具编译出来的 exe 通常只有几 MB不需要依赖几十 MB 的运行时环境双击就跑发给同事也能直接用。这个优势在工具类软件开发里非常实际尤其当你需要在多台电脑上快速部署的时候。另外还有一层原因我想把日程数据完全握在自己手里不依赖某个云服务的账号体系。我的日程就是我的本地文件格式自己定备份直接拷文件换电脑把文件带过去就行不存在“服务商哪天调整策略导致数据迁移麻烦”的问题。1.2 用桌面版而非手机APP的三个理由这个项目标题里虽然有“APP”三个字但我的第一落点选择了 Windows 桌面版而不是手机端。原因有三个。第一我的绝大多数日程都产生在办公场景人坐在电脑前的时间最长桌面端常驻右下角托盘比掏手机快得多。日程提醒弹出时我正在写代码一个气泡提示就够了不需要拿起手机解锁、查看、再放下。第二桌面端的数据管理更透明。手机 APP 的沙箱机制导致数据文件不好直接访问而桌面应用的数据就是一个 JSON 文件放在 exe 同目录或者%APPDATA%下面我自己能随时查看、修改、备份。对于“简单日程管理”这个量级的需求这已经是天花板级别的可控性。第三Delphi 在桌面开发上的效率仍然很高。VCL 组件成熟稳定SQLite、JSON、网络通信都有现成库我一个晚上就能把核心功能搓出来。如果后续真要做移动端FireMonkeyFMX可以复用大部分数据层代码这个后面再展开。2. 数据怎么存我选JSON文件而不是SQLite2.1 日程数据模型设计够用就好第一版的时候我犹豫过要不要上 SQLite后来想明白了日程管理这种量级的数据一个 JSON 文件完全足够。SQLite 的查询、索引、事务这些能力在这个场景里用不上反而引入了一个额外的 DLL 依赖。如果你的日程数量到几万条再考虑 SQLite 也不迟但“简单 APP”的核心价值就是简单。我设计的日程字段如下刻意保持最小集合type TScheduleItem record ID: Integer; Title: string; // 日程标题 StartTime: TDateTime; // 开始时间 Duration: Integer; // 持续分钟数 Category: string; // 分类工作、生活、其他 Priority: Integer; // 优先级1高、2中、3低 Remark: string; // 备注 Done: Boolean; // 是否完成 Remind: Boolean; // 是否开启提醒 end;StartTime用了TDateTime而不是字符串存储这是一个很重要的决定。Delphi 的TDateTime本质上是 double整数部分是日期小数部分是时间计算“距离提醒还有多少分钟”只需要做减法不需要解析字符串效率高而且不容易出错。JSON 文件的结构也很直白{ items: [ { id: 1, title: 项目周会, startTime: 2025-06-16T10:00:00, duration: 60, category: 工作, priority: 1, remark: , done: false, remind: true } ] }文件我放在 exe 同目录下的schedule.json简单粗暴。启动时如果文件不存在就自动创建空数据退出时保存。为了防止写坏文件我用了“先写临时文件再覆盖原文件”的策略这个习惯能从根上避免断电或程序崩溃导致数据丢失。2.2 基于TJSONObject的增删改查代码Delphi 从 XE 系列开始就内置了System.JSON单元读取和生成 JSON 非常方便不需要引第三方库。我的加载函数核心逻辑是这样的function LoadScheduleFromFile(const AFileName: string): TArrayTScheduleItem; var LJson: TJSONObject; LArr: TJSONArray; LObj: TJSONObject; I: Integer; begin SetLength(Result, 0); if not FileExists(AFileName) then Exit; LJson : TJSONObject.Create; try LJson.Parse(TFile.ReadAllText(AFileName, TEncoding.UTF8), False); if not LJson.TryGetValueTJSONArray(items, LArr) then Exit; SetLength(Result, LArr.Count); for I : 0 to LArr.Count - 1 do begin LObj : LArr.Items[I] as TJSONObject; Result[I].ID : LObj.GetValueInteger(id); Result[I].Title : LObj.GetValuestring(title); Result[I].StartTime : ISO8601ToDate(LObj.GetValuestring(startTime)); Result[I].Duration : LObj.GetValueInteger(duration); Result[I].Category : LObj.GetValuestring(category); Result[I].Priority : LObj.GetValueInteger(priority); Result[I].Remark : LObj.GetValuestring(remark); Result[I].Done : LObj.GetValueBoolean(done); Result[I].Remind : LObj.GetValueBoolean(remind); end; finally LJson.Free; end; end;保存的代码反过来遍历数组然后生成 JSON。这里有两个容易踩的坑。第一个坑是TJsonObject.Parse的第二个参数传False表示“不严格检查”遇到未知字段直接跳过。这样以后我要在 JSON 里加新字段旧程序读新文件也不会崩兼容性会好很多。第二个坑是编码。TFile.ReadAllText一定要指定TEncoding.UTF8否则在中文 Windows 上默认会按 ANSI 解析中文日程标题就乱码了。写入的时候用TFile.WriteAllText(AFileName, LJson.ToJSON, TEncoding.UTF8)两头都锁死 UTF-8就再也不会出现“在我电脑上正常拷到同事电脑上乱码”的问题。新增和删除就没什么好说的了。新增就是SetLength扩容然后赋值 ID用TStopwatch.GetTimestamp或者简单的递增计数器生成。删除我用的是“标记删除”——不真正从数组里移除而是把Done置为True。一个是保留历史记录另一个是避免删除操作引发的数组元素整体搬移数据量小的时候其实无所谓但习惯养好了将来数据量上来了也不会手忙脚乱。3. 核心交互与提醒机制的实现细节3.1 日期处理三件套今天、本周、周六日判断日程管理离不开日期判断尤其是“今天有哪些日程”“本周有哪些日程”以及热词里有人问过的“Delphi如何判断是周六日”。这三个场景我拆成了三个独立函数各有各的注意点。判断“今天”不能用FormatDateTime(yyyy-mm-dd, ADate) FormatDateTime(yyyy-mm-dd, Now)这种字符串比较虽然能用但每次比较都要格式化循环几万次就能感觉到慢。更优雅的方式是直接用YearOf、MonthOf、DayOf三个函数function IsToday(const ADateTime: TDateTime): Boolean; begin Result : (YearOf(ADateTime) YearOf(Now)) and (MonthOf(ADateTime) MonthOf(Now)) and (DayOf(ADateTime) DayOf(Now)); end;判断“本周”稍微有点讲究。一个自然周是从周一开始还是周日开始在不同的业务场景里结论不同。Delphi 的DayOfWeek返回 1 到 7其中 1 代表周日7 代表周六这一点很容易搞错。如果你要让周一作为一周的开始判断代码要这样写function IsThisWeek(const ADateTime: TDateTime): Boolean; var LToday, LTarget: TDateTime; LOffset: Integer; begin LToday : DateOf(Now); LOffset : DayOfWeek(LToday) - 2; // 周一偏移为0 if LOffset 0 then LOffset : 6; LTarget : DateOf(ADateTime); Result : (LTarget LToday - LOffset) and (LTarget LToday - LOffset 7); end;这段代码的原理是先算出“本周一”是哪一天然后判断目标日期是否落在[本周一, 下周一)的区间内。写成和而不是和是为了避免跨周时的边界错误。判断周六日则是热词“delphi如何判断是周六日”的直接答案function IsWeekend(const ADateTime: TDateTime): Boolean; var LDay: Integer; begin LDay : DayOfWeek(ADateTime); Result : (LDay 1) or (LDay 7); // 1周日, 7周六 end;3.2 提醒轮询TTimer的间隔、去重和误触发处理提醒是日程管理最核心的功能没有之一。我的方案是主窗体放一个TTimer每秒或者每 30 秒触发一次扫描所有开启提醒且未完成的日程如果当前时间落在“提醒时间窗口”内就弹一个系统托盘气泡。procedure TMainForm.Timer1Timer(Sender: TObject); var LNow: TDateTime; begin LNow : Now; for var LItem in FItems do begin if LItem.Remind and (not LItem.Done) and (LItem.StartTime LNow) and (LItem.StartTime - LNow 30 / 1440) and (not FReminded.ContainsKey(LItem.ID)) then begin FReminded.Add(LItem.ID, True); ShowNotification(LItem.Title 将于30分钟内开始); end; end; end;这里有几个设计细节值得展开说。第一30 / 1440是把 30 分钟转换成TDateTime的小数形式。因为一天是 1.0所以 1 小时就是1/241 分钟就是1/1440。这个换算容易错我一开始直接写成0.5结果程序把“提前 12 小时提醒”当成了“提前 30 分钟提醒”调试了半天才发现问题。第二FReminded是一个TDictionaryInteger, Boolean用来记录哪些日程已经提醒过了。这个去重设计很重要否则每隔 30 秒就会重复弹一次能把人烦死。但要注意程序重启后FReminded会被清空如果日程还没过期再次启动时又会提醒一次。我的处理方式是在加载日程数据时把所有StartTime Now的提醒日程重新加入待提醒集合这样重启后也只会提醒一次不会反复轰炸。第三TTimer 的触发精度问题。Windows 的TTimer默认基于WM_TIMER消息优先级比较低如果 CPU 忙可能延迟几百毫秒甚至几秒。对于日程提醒这种场景这个误差完全可以接受没必要上高精度的多媒体定时器。但如果你的系统经常休眠或睡眠TTimer在唤醒后可能会连续触发多次。我的处理是在Application.OnRestore事件里重置FReminded并刷新一次列表确保睡眠期间错过的提醒不会在唤醒后集中爆发。4. 界面布局与用户体验的小心思4.1 主界面三栏布局日历缩略、日程列表、详情编辑界面我用的是 VCL 经典三栏布局左侧一个TMonthCalendar日历缩略组件中间一个TListView显示日程列表右侧一个TPanel放详情和编辑控件。这个布局参考了 Outlook 的经典样式用户上手零成本。TMonthCalendar选日期后中间的TListView就刷新成当天的日程列表。每次切换日期时我用前面写的IsToday逻辑判断一下如果是今天列表标题前加一个蓝色的“今天”标记这个小小的视觉提示非常实用。详情编辑区的内容很直接TEdit输入标题TDateTimePicker选择开始日期和时间TEdit输入持续分钟数TComboBox选择分类TComboBox选择优先级TMemo输入备注两个TCheckBox分别控制“已完成”和“开启提醒”这个编辑区一次只对应一个选中日程不搞复杂的多行编辑表格。简单工具就是要让用户一眼看懂当前在编辑什么数据绑定我用的是最原始的控件赋值和读取完全没用 LiveBindings因为在这个场景里 LiveBindings 的模板配置成本比手写赋值高不少。4.2 列表排序、颜色标识与双击编辑的交互闭环TListView在 VCL 里的表现力比TStringGrid强很多我设置了ViewStyle : vsReport列依次是时间、标题、分类、优先级、状态。排序逻辑只有一条规则未完成的排前面完成的排后面同组内按开始时间升序。这个排序不在界面里做而是在刷新列表前对FItems数组做一次排序TArray.SortTScheduleItem(FItems, TComparerTScheduleItem.Construct( function(const A, B: TScheduleItem): Integer begin if A.Done B.Done then begin if A.Done then Result : 1 else Result : -1; Exit; end; Result : CompareValue(A.StartTime, B.StartTime, 0.000001); end));颜色标识也很重要。我在TListView.OnAdvancedCustomDrawItem事件里根据优先级上色高优先级红色、中优先级橙色、低优先级默认色。这个视觉层级能让你在一堆日程里迅速找到最紧急的事。交互闭环我做得比较完整单击列表项右侧详情面板加载数据可以就地编辑双击列表项弹出独立的编辑窗口右键弹出菜单新增日程、编辑、标记完成、删除这里有一个容易忽略的细节标记完成和删除一定要分开。我见过很多工具把“删除”放在“完成”旁边用户本想勾选完成结果手滑点成删除数据就没了。我的做法是删除操作必须弹确认框并且要输入“确认删除”四个字才能删。这个门槛虽然看起来有点反人性但实际用下来它真的把误删的概率降到了零。5. 编译发布与踩坑实录5.1 发布配置静态编译、图标和版本信息Delphi 开发工具类应用最爽的一点就是发布简单但简单不等于不用配置。我每次发布前都要检查三个地方。第一Project Options - Runtime Packages里的“Build with runtime packages”一定要取消勾选。如果不取消生成的 exe 会依赖一堆rtl260.bpl、vcl260.bpl之类的动态库换台电脑就报“找不到 bpl”。取消勾选后默认静态链接exe 体积会从几百 KB 涨到几 MB但换来的是双击即用、拷贝即用。第二Project Options - Application - Icon设置应用图标。默认的 Delphi 图标一看就是没用心日程工具属于办公软件建议自己做一个简单的日历图标。用 16x16 和 32x32 两套尺寸保存为.ico文件再在项目设置里指定。第三Project Options - Version Info里填写版本号和产品名称。没有版本信息的 exe 在 Windows 的 UAC 弹窗里会显示“未知发布者”虽然不影响使用但看起来不专业而且后续做自动更新时版本号是判断是否需要更新的基础。5.2 我踩过的三个坑字体模糊、DPI缩放和JSON解析乱码第一个坑是高 DPI 缩放导致的字体模糊。现在很多笔记本默认缩放是 150% 甚至 200%如果程序没有声明支持 DPI 感知Windows 会做位图拉伸界面字体会发虚。解决办法是在项目源文件.dpr最开头加上一行procedure SetDPIAware; begin SetProcessDPIAware; end;或者在项目设置里把DPI Awareness设为Per Monitor V2。Delphi 10.4 之后的版本默认配置已经处理得不错但如果你还在维护老项目这个坑几乎必踩。第二个坑是 JSON 解析乱码前面提到过编码问题这里再说一个具体现象在 IDE 里运行程序一切正常发布后到别的电脑上中文变成一堆问号。原因是我在保存 JSON 时使用了TFile.WriteAllText的默认编码ANSI而读取时却按 UTF-8 读。现在我的代码里所有文件读写都显式指定TEncoding.UTF8再也没有出过乱码。第三个坑是TTimer的Interval设置成了 1000毫秒也就是 1 秒触发一次。看起来没问题但每次触发都要遍历全部日程数组如果数组里有一万条数据每秒遍历一次也不是什么大事问题不大。可如果在这个事件里又做了界面刷新性能就急剧下降。我的经验是提醒逻辑和界面刷新逻辑一定要拆开提醒轮询只做数据判断发现需要提醒了再调TThread.Queue去更新界面避免在 Timer 事件里直接操作控件。6. 后续扩展方向别小看这个小工具6.1 用SendMessage把日程提醒推到局域网内其他电脑日程管理做到这个程度已经能覆盖日常使用了。但作为一个爱折腾的人我还想让它和团队协作场景打通。热词里有人搜过“delphi 局域网 从一个程序 发消息给 另一个程序 sendmessage”这其实是一个很实用的扩展方向。Windows 原生提供了一个SendMessageAPI通过窗口消息机制在两个进程之间传递数据。我计划给日程工具加一个“局域网提醒广播”功能当本机日程提醒触发时同时对局域网内其他同款工具发送一条自定义窗口消息。接收方收到消息后在托盘区弹一条“来自XX电脑的日程提醒”。这样一个小团队在办公室各用各的电脑也能实现轻量级的日程互通不需要搭服务器不需要数据库同步纯消息通知一条WM_COPYDATA就搞定了。具体做法是定义一个自定义消息 ID比如WM_APP 100发送方用SendMessage(FindWindow(TScheduleMainForm, nil), WM_APP 100, 0, LPARAM(DataStruct))发送接收方在WndProc里拦截并处理。注意WM_COPYDATA传字符串是最稳的因为它的内存由系统管理不会出现指针失效问题。6.2 从Excel批量导入排班表/课表的思路另一个高频需求是批量导入。比如老师拿到一张课表 Excel想一次性导入日程工具手工录入 30 条记录容易出错。Delphi 里读 Excel 的成熟方案是 TMS FlexCel 或者老牌的TExcelApplication但如果你只是导入xlsx文件还有一个轻量思路用zip解压xl/worksheets/sheet1.xml然后用IXMLDocument解析。这个方法听起来很绕但好处是不依赖 Excel 组件不需要安装 Office也不用引入大的第三方库。xlsx本质上就是一个 zip 包里面的 XML 文件存储了单元格数据。只要你知道每个单元格的行列坐标读出来并不难。当然如果你不想折腾 XML最省事的方式是让用户先另存为 CSV然后用TStringList逐行解析5 分钟就能搞定导入功能。这个扩展方向的价值在于它把一个“个人日程工具”升级成了“能衔接已有工作流”的入口。排班表、课程表、项目里程碑这些数据往往已经在 Excel 里了能自动导入工具的使用意愿会大幅提升。我在实际使用这个日程管理 APP 时最大的体会是不要一上来就想着功能大而全先把“新建日程”“准点提醒”“不会重复打扰”这三个核心体验做到位比堆砌任何花哨功能都管用。Delphi 开发这种轻量工具仍然非常高效我现在每天开机的第一件事就是打开它双击、看今天、处理日程整个过程不到十秒。如果你也想动手写一个建议直接从 JSON 存储 TTimer 轮询 ListView 展示这种最简架构开始跑通之后再按自己的习惯慢慢加功能。本文还有配套的精品资源点击获取
返回列表