全解析)
簡介一套基于Delphi FMX框架的Android日程管理APP示例工程面向移動開發(fā)入門者及跨平臺實踐者覆蓋日程數(shù)據(jù)管理場景下的界面交互與存儲實現(xiàn)。壓縮包共27個文件類型包含pas源代碼、fmx窗體、vlb可視布局、SQLite數(shù)據(jù)庫文件.db及配套SQL腳本同時包括項目配置、部署文件與自定義ListView外觀包整體僅46KB輕量緊湊、模塊清晰。目前已有474人學(xué)習(xí)/下載。工程完整演示了SQLite數(shù)據(jù)庫建表與增刪改查、FMX自定義ListView樣式與滑動/長按手勢、調(diào)用Android系統(tǒng)分享組件、半透明背景提示框設(shè)計等關(guān)鍵技術(shù)附帶vkbdhelper虛擬鍵盤輔助單元和ListView外觀增強包目錄按窗體、數(shù)據(jù)層與工具單元劃分可直接導(dǎo)入Delphi IDE運行。借助完整的工程文件與SQL初始化腳本讀者能快速掌握Delphi移動開發(fā)中數(shù)據(jù)持久化、控件定制及系統(tǒng)能力調(diào)用的常見方法適合作為課程設(shè)計或入職練手項目參考。 事情是這樣的我手頭有一堆零散日程——會議、截稿日期、給客戶回電話、周末去保養(yǎng)車之前一直用手機日歷但坐在電腦前寫代碼的時候切到手機去記一筆總覺得別扭一來打斷思路二來手機日歷的重復(fù)提醒設(shè)置得越細(xì)越麻煩。網(wǎng)上找了幾款桌面日歷要么捆綁安裝要么強制登錄要么日歷視圖花里胡哨但連基礎(chǔ)提醒都做不好。一怒之下我決定用 DELPHI 自己寫一個日程管理 APP目標(biāo)是雙擊就能跑、數(shù)據(jù)本地存、提醒不漏、改起來順手。這篇文章就把整個開發(fā)過程、關(guān)鍵代碼和踩過的坑完整記錄下來。這個工具適合誰如果你也是 Delphi 開發(fā)者或者你手里有個舊工具需要維護又或者你只是想找一個可以完全掌控數(shù)據(jù)的桌面日程方案那這篇筆記應(yīng)該能幫你省不少時間。我不會只貼成品代碼而是把每一處設(shè)計決策的“為什么”也講清楚這樣你拿到之后可以按自己的習(xí)慣改。1. 為什么這個年頭我還在用Delphi寫日程管理1.1 一個老Delphi開發(fā)者的真實場景我平時的主要工作還是圍繞 Windows 桌面端的業(yè)務(wù)系統(tǒng)開發(fā)Delphi 對我來說不是“上古傳說”而是每天都在用的生產(chǎn)力工具。項目里有大量現(xiàn)成的 VCL 組件、公司內(nèi)部封裝好的公共庫、還有跑了好多年的業(yè)務(wù)邏輯——這些東西用其他語言重寫一遍成本極高也不劃算。日程管理這個需求本質(zhì)上就是一個“輕量工具類”應(yīng)用界面要簡單、啟動要快、數(shù)據(jù)要可控、提醒要穩(wěn)定。用 Delphi 寫這種工具編譯出來的 exe 通常只有幾 MB不需要依賴幾十 MB 的運行時環(huán)境雙擊就跑發(fā)給同事也能直接用。這個優(yōu)勢在工具類軟件開發(fā)里非常實際尤其當(dāng)你需要在多臺電腦上快速部署的時候。另外還有一層原因我想把日程數(shù)據(jù)完全握在自己手里不依賴某個云服務(wù)的賬號體系。我的日程就是我的本地文件格式自己定備份直接拷文件換電腦把文件帶過去就行不存在“服務(wù)商哪天調(diào)整策略導(dǎo)致數(shù)據(jù)遷移麻煩”的問題。1.2 用桌面版而非手機APP的三個理由這個項目標(biāo)題里雖然有“APP”三個字但我的第一落點選擇了 Windows 桌面版而不是手機端。原因有三個。第一我的絕大多數(shù)日程都產(chǎn)生在辦公場景人坐在電腦前的時間最長桌面端常駐右下角托盤比掏手機快得多。日程提醒彈出時我正在寫代碼一個氣泡提示就夠了不需要拿起手機解鎖、查看、再放下。第二桌面端的數(shù)據(jù)管理更透明。手機 APP 的沙箱機制導(dǎo)致數(shù)據(jù)文件不好直接訪問而桌面應(yīng)用的數(shù)據(jù)就是一個 JSON 文件放在 exe 同目錄或者%APPDATA%下面我自己能隨時查看、修改、備份。對于“簡單日程管理”這個量級的需求這已經(jīng)是天花板級別的可控性。第三Delphi 在桌面開發(fā)上的效率仍然很高。VCL 組件成熟穩(wěn)定SQLite、JSON、網(wǎng)絡(luò)通信都有現(xiàn)成庫我一個晚上就能把核心功能搓出來。如果后續(xù)真要做移動端FireMonkeyFMX可以復(fù)用大部分?jǐn)?shù)據(jù)層代碼這個后面再展開。2. 數(shù)據(jù)怎么存我選JSON文件而不是SQLite2.1 日程數(shù)據(jù)模型設(shè)計夠用就好第一版的時候我猶豫過要不要上 SQLite后來想明白了日程管理這種量級的數(shù)據(jù)一個 JSON 文件完全足夠。SQLite 的查詢、索引、事務(wù)這些能力在這個場景里用不上反而引入了一個額外的 DLL 依賴。如果你的日程數(shù)量到幾萬條再考慮 SQLite 也不遲但“簡單 APP”的核心價值就是簡單。我設(shè)計的日程字段如下刻意保持最小集合type TScheduleItem record ID: Integer; Title: string; // 日程標(biāo)題 StartTime: TDateTime; // 開始時間 Duration: Integer; // 持續(xù)分鐘數(shù) Category: string; // 分類工作、生活、其他 Priority: Integer; // 優(yōu)先級1高、2中、3低 Remark: string; // 備注 Done: Boolean; // 是否完成 Remind: Boolean; // 是否開啟提醒 end;StartTime用了TDateTime而不是字符串存儲這是一個很重要的決定。Delphi 的TDateTime本質(zhì)上是 double整數(shù)部分是日期小數(shù)部分是時間計算“距離提醒還有多少分鐘”只需要做減法不需要解析字符串效率高而且不容易出錯。JSON 文件的結(jié)構(gòu)也很直白{ items: [ { id: 1, title: 項目周會, startTime: 2025-06-16T10:00:00, duration: 60, category: 工作, priority: 1, remark: , done: false, remind: true } ] }文件我放在 exe 同目錄下的schedule.json簡單粗暴。啟動時如果文件不存在就自動創(chuàng)建空數(shù)據(jù)退出時保存。為了防止寫壞文件我用了“先寫臨時文件再覆蓋原文件”的策略這個習(xí)慣能從根上避免斷電或程序崩潰導(dǎo)致數(shù)據(jù)丟失。2.2 基于TJSONObject的增刪改查代碼Delphi 從 XE 系列開始就內(nèi)置了System.JSON單元讀取和生成 JSON 非常方便不需要引第三方庫。我的加載函數(shù)核心邏輯是這樣的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;保存的代碼反過來遍歷數(shù)組然后生成 JSON。這里有兩個容易踩的坑。第一個坑是TJsonObject.Parse的第二個參數(shù)傳False表示“不嚴(yán)格檢查”遇到未知字段直接跳過。這樣以后我要在 JSON 里加新字段舊程序讀新文件也不會崩兼容性會好很多。第二個坑是編碼。TFile.ReadAllText一定要指定TEncoding.UTF8否則在中文 Windows 上默認(rèn)會按 ANSI 解析中文日程標(biāo)題就亂碼了。寫入的時候用TFile.WriteAllText(AFileName, LJson.ToJSON, TEncoding.UTF8)兩頭都鎖死 UTF-8就再也不會出現(xiàn)“在我電腦上正??降酵码娔X上亂碼”的問題。新增和刪除就沒什么好說的了。新增就是SetLength擴容然后賦值 ID用TStopwatch.GetTimestamp或者簡單的遞增計數(shù)器生成。刪除我用的是“標(biāo)記刪除”——不真正從數(shù)組里移除而是把Done置為True。一個是保留歷史記錄另一個是避免刪除操作引發(fā)的數(shù)組元素整體搬移數(shù)據(jù)量小的時候其實無所謂但習(xí)慣養(yǎng)好了將來數(shù)據(jù)量上來了也不會手忙腳亂。3. 核心交互與提醒機制的實現(xiàn)細(xì)節(jié)3.1 日期處理三件套今天、本周、周六日判斷日程管理離不開日期判斷尤其是“今天有哪些日程”“本周有哪些日程”以及熱詞里有人問過的“Delphi如何判斷是周六日”。這三個場景我拆成了三個獨立函數(shù)各有各的注意點。判斷“今天”不能用FormatDateTime(yyyy-mm-dd, ADate) FormatDateTime(yyyy-mm-dd, Now)這種字符串比較雖然能用但每次比較都要格式化循環(huán)幾萬次就能感覺到慢。更優(yōu)雅的方式是直接用YearOf、MonthOf、DayOf三個函數(shù)function IsToday(const ADateTime: TDateTime): Boolean; begin Result : (YearOf(ADateTime) YearOf(Now)) and (MonthOf(ADateTime) MonthOf(Now)) and (DayOf(ADateTime) DayOf(Now)); end;判斷“本周”稍微有點講究。一個自然周是從周一開始還是周日開始在不同的業(yè)務(wù)場景里結(jié)論不同。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;這段代碼的原理是先算出“本周一”是哪一天然后判斷目標(biāo)日期是否落在[本周一, 下周一)的區(qū)間內(nèi)。寫成和而不是和是為了避免跨周時的邊界錯誤。判斷周六日則是熱詞“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的間隔、去重和誤觸發(fā)處理提醒是日程管理最核心的功能沒有之一。我的方案是主窗體放一個TTimer每秒或者每 30 秒觸發(fā)一次掃描所有開啟提醒且未完成的日程如果當(dāng)前時間落在“提醒時間窗口”內(nèi)就彈一個系統(tǒng)托盤氣泡。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分鐘內(nèi)開始); end; end; end;這里有幾個設(shè)計細(xì)節(jié)值得展開說。第一30 / 1440是把 30 分鐘轉(zhuǎn)換成TDateTime的小數(shù)形式。因為一天是 1.0所以 1 小時就是1/241 分鐘就是1/1440。這個換算容易錯我一開始直接寫成0.5結(jié)果程序把“提前 12 小時提醒”當(dāng)成了“提前 30 分鐘提醒”調(diào)試了半天才發(fā)現(xiàn)問題。第二FReminded是一個TDictionaryInteger, Boolean用來記錄哪些日程已經(jīng)提醒過了。這個去重設(shè)計很重要否則每隔 30 秒就會重復(fù)彈一次能把人煩死。但要注意程序重啟后FReminded會被清空如果日程還沒過期再次啟動時又會提醒一次。我的處理方式是在加載日程數(shù)據(jù)時把所有StartTime Now的提醒日程重新加入待提醒集合這樣重啟后也只會提醒一次不會反復(fù)轟炸。第三TTimer 的觸發(fā)精度問題。Windows 的TTimer默認(rèn)基于WM_TIMER消息優(yōu)先級比較低如果 CPU 忙可能延遲幾百毫秒甚至幾秒。對于日程提醒這種場景這個誤差完全可以接受沒必要上高精度的多媒體定時器。但如果你的系統(tǒng)經(jīng)常休眠或睡眠TTimer在喚醒后可能會連續(xù)觸發(fā)多次。我的處理是在Application.OnRestore事件里重置FReminded并刷新一次列表確保睡眠期間錯過的提醒不會在喚醒后集中爆發(fā)。4. 界面布局與用戶體驗的小心思4.1 主界面三欄布局日歷縮略、日程列表、詳情編輯界面我用的是 VCL 經(jīng)典三欄布局左側(cè)一個TMonthCalendar日歷縮略組件中間一個TListView顯示日程列表右側(cè)一個TPanel放詳情和編輯控件。這個布局參考了 Outlook 的經(jīng)典樣式用戶上手零成本。TMonthCalendar選日期后中間的TListView就刷新成當(dāng)天的日程列表。每次切換日期時我用前面寫的IsToday邏輯判斷一下如果是今天列表標(biāo)題前加一個藍(lán)色的“今天”標(biāo)記這個小小的視覺提示非常實用。詳情編輯區(qū)的內(nèi)容很直接TEdit輸入標(biāo)題TDateTimePicker選擇開始日期和時間TEdit輸入持續(xù)分鐘數(shù)TComboBox選擇分類TComboBox選擇優(yōu)先級TMemo輸入備注兩個TCheckBox分別控制“已完成”和“開啟提醒”這個編輯區(qū)一次只對應(yīng)一個選中日程不搞復(fù)雜的多行編輯表格。簡單工具就是要讓用戶一眼看懂當(dāng)前在編輯什么數(shù)據(jù)綁定我用的是最原始的控件賦值和讀取完全沒用 LiveBindings因為在這個場景里 LiveBindings 的模板配置成本比手寫賦值高不少。4.2 列表排序、顏色標(biāo)識與雙擊編輯的交互閉環(huán)TListView在 VCL 里的表現(xiàn)力比TStringGrid強很多我設(shè)置了ViewStyle : vsReport列依次是時間、標(biāo)題、分類、優(yōu)先級、狀態(tài)。排序邏輯只有一條規(guī)則未完成的排前面完成的排后面同組內(nèi)按開始時間升序。這個排序不在界面里做而是在刷新列表前對FItems數(shù)組做一次排序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));顏色標(biāo)識也很重要。我在TListView.OnAdvancedCustomDrawItem事件里根據(jù)優(yōu)先級上色高優(yōu)先級紅色、中優(yōu)先級橙色、低優(yōu)先級默認(rèn)色。這個視覺層級能讓你在一堆日程里迅速找到最緊急的事。交互閉環(huán)我做得比較完整單擊列表項右側(cè)詳情面板加載數(shù)據(jù)可以就地編輯雙擊列表項彈出獨立的編輯窗口右鍵彈出菜單新增日程、編輯、標(biāo)記完成、刪除這里有一個容易忽略的細(xì)節(jié)標(biāo)記完成和刪除一定要分開。我見過很多工具把“刪除”放在“完成”旁邊用戶本想勾選完成結(jié)果手滑點成刪除數(shù)據(jù)就沒了。我的做法是刪除操作必須彈確認(rèn)框并且要輸入“確認(rèn)刪除”四個字才能刪。這個門檻雖然看起來有點反人性但實際用下來它真的把誤刪的概率降到了零。5. 編譯發(fā)布與踩坑實錄5.1 發(fā)布配置靜態(tài)編譯、圖標(biāo)和版本信息Delphi 開發(fā)工具類應(yīng)用最爽的一點就是發(fā)布簡單但簡單不等于不用配置。我每次發(fā)布前都要檢查三個地方。第一Project Options - Runtime Packages里的“Build with runtime packages”一定要取消勾選。如果不取消生成的 exe 會依賴一堆rtl260.bpl、vcl260.bpl之類的動態(tài)庫換臺電腦就報“找不到 bpl”。取消勾選后默認(rèn)靜態(tài)鏈接exe 體積會從幾百 KB 漲到幾 MB但換來的是雙擊即用、拷貝即用。第二Project Options - Application - Icon設(shè)置應(yīng)用圖標(biāo)。默認(rèn)的 Delphi 圖標(biāo)一看就是沒用心日程工具屬于辦公軟件建議自己做一個簡單的日歷圖標(biāo)。用 16x16 和 32x32 兩套尺寸保存為.ico文件再在項目設(shè)置里指定。第三Project Options - Version Info里填寫版本號和產(chǎn)品名稱。沒有版本信息的 exe 在 Windows 的 UAC 彈窗里會顯示“未知發(fā)布者”雖然不影響使用但看起來不專業(yè)而且后續(xù)做自動更新時版本號是判斷是否需要更新的基礎(chǔ)。5.2 我踩過的三個坑字體模糊、DPI縮放和JSON解析亂碼第一個坑是高 DPI 縮放導(dǎo)致的字體模糊?,F(xiàn)在很多筆記本默認(rèn)縮放是 150% 甚至 200%如果程序沒有聲明支持 DPI 感知Windows 會做位圖拉伸界面字體會發(fā)虛。解決辦法是在項目源文件.dpr最開頭加上一行procedure SetDPIAware; begin SetProcessDPIAware; end;或者在項目設(shè)置里把DPI Awareness設(shè)為Per Monitor V2。Delphi 10.4 之后的版本默認(rèn)配置已經(jīng)處理得不錯但如果你還在維護老項目這個坑幾乎必踩。第二個坑是 JSON 解析亂碼前面提到過編碼問題這里再說一個具體現(xiàn)象在 IDE 里運行程序一切正常發(fā)布后到別的電腦上中文變成一堆問號。原因是我在保存 JSON 時使用了TFile.WriteAllText的默認(rèn)編碼ANSI而讀取時卻按 UTF-8 讀。現(xiàn)在我的代碼里所有文件讀寫都顯式指定TEncoding.UTF8再也沒有出過亂碼。第三個坑是TTimer的Interval設(shè)置成了 1000毫秒也就是 1 秒觸發(fā)一次??雌饋頉]問題但每次觸發(fā)都要遍歷全部日程數(shù)組如果數(shù)組里有一萬條數(shù)據(jù)每秒遍歷一次也不是什么大事問題不大。可如果在這個事件里又做了界面刷新性能就急劇下降。我的經(jīng)驗是提醒邏輯和界面刷新邏輯一定要拆開提醒輪詢只做數(shù)據(jù)判斷發(fā)現(xiàn)需要提醒了再調(diào)TThread.Queue去更新界面避免在 Timer 事件里直接操作控件。6. 后續(xù)擴展方向別小看這個小工具6.1 用SendMessage把日程提醒推到局域網(wǎng)內(nèi)其他電腦日程管理做到這個程度已經(jīng)能覆蓋日常使用了。但作為一個愛折騰的人我還想讓它和團隊協(xié)作場景打通。熱詞里有人搜過“delphi 局域網(wǎng) 從一個程序 發(fā)消息給 另一個程序 sendmessage”這其實是一個很實用的擴展方向。Windows 原生提供了一個SendMessageAPI通過窗口消息機制在兩個進(jìn)程之間傳遞數(shù)據(jù)。我計劃給日程工具加一個“局域網(wǎng)提醒廣播”功能當(dāng)本機日程提醒觸發(fā)時同時對局域網(wǎng)內(nèi)其他同款工具發(fā)送一條自定義窗口消息。接收方收到消息后在托盤區(qū)彈一條“來自XX電腦的日程提醒”。這樣一個小團隊在辦公室各用各的電腦也能實現(xiàn)輕量級的日程互通不需要搭服務(wù)器不需要數(shù)據(jù)庫同步純消息通知一條WM_COPYDATA就搞定了。具體做法是定義一個自定義消息 ID比如WM_APP 100發(fā)送方用SendMessage(FindWindow(TScheduleMainForm, nil), WM_APP 100, 0, LPARAM(DataStruct))發(fā)送接收方在WndProc里攔截并處理。注意WM_COPYDATA傳字符串是最穩(wěn)的因為它的內(nèi)存由系統(tǒng)管理不會出現(xiàn)指針失效問題。6.2 從Excel批量導(dǎo)入排班表/課表的思路另一個高頻需求是批量導(dǎo)入。比如老師拿到一張課表 Excel想一次性導(dǎo)入日程工具手工錄入 30 條記錄容易出錯。Delphi 里讀 Excel 的成熟方案是 TMS FlexCel 或者老牌的TExcelApplication但如果你只是導(dǎo)入xlsx文件還有一個輕量思路用zip解壓xl/worksheets/sheet1.xml然后用IXMLDocument解析。這個方法聽起來很繞但好處是不依賴 Excel 組件不需要安裝 Office也不用引入大的第三方庫。xlsx本質(zhì)上就是一個 zip 包里面的 XML 文件存儲了單元格數(shù)據(jù)。只要你知道每個單元格的行列坐標(biāo)讀出來并不難。當(dāng)然如果你不想折騰 XML最省事的方式是讓用戶先另存為 CSV然后用TStringList逐行解析5 分鐘就能搞定導(dǎo)入功能。這個擴展方向的價值在于它把一個“個人日程工具”升級成了“能銜接已有工作流”的入口。排班表、課程表、項目里程碑這些數(shù)據(jù)往往已經(jīng)在 Excel 里了能自動導(dǎo)入工具的使用意愿會大幅提升。我在實際使用這個日程管理 APP 時最大的體會是不要一上來就想著功能大而全先把“新建日程”“準(zhǔn)點提醒”“不會重復(fù)打擾”這三個核心體驗做到位比堆砌任何花哨功能都管用。Delphi 開發(fā)這種輕量工具仍然非常高效我現(xiàn)在每天開機的第一件事就是打開它雙擊、看今天、處理日程整個過程不到十秒。如果你也想動手寫一個建議直接從 JSON 存儲 TTimer 輪詢 ListView 展示這種最簡架構(gòu)開始跑通之后再按自己的習(xí)慣慢慢加功能。本文還有配套的精品資源點擊獲取