资讯详情

C# WinForms+SQLite 个人日程管理系统开发详解

📅 2026/10/3 14:43:24 | 华诺云谱 👁 阅读
C# WinForms+SQLite 个人日程管理系统开发详解
简介基于C#的桌面应用个人日程管理系统课程设计资源包面向计算机相关专业学生用于课程设计、综合实践或毕业设计参考。集成待办事项管理、番茄钟计时、网络兴趣内容爬取、任务统计和提醒设置等模块并通过古风UI带来差异性体验。压缩包约53.92MB共402个文件含40个.cs源码、62个dll依赖库、89个wav音频、2个docx报告和1个pptx演示等wav对应提醒音效dll为SQLite等组件依赖源码与文档分层存放便于按需查阅。目前已有1169人学习浏览适合作为课程设计项目的完整参考。除可直接运行的工程源码外还附有开题报告、课程报告和演示PPT覆盖从需求分析到成果展示的完整流程爬虫与SQLite等模块的代码也提供了网络数据获取和本地存储的落地参考系统采用Windows窗体架构数据库使用SQLite便于本地离线管理古风界面设计也为同类项目提供了风格参考。1. 基于C#实现的个人日程管理系统为什么 WinForms SQLite 是本地日程的稳妥起点一个基于C#实现的个人日程管理系统说白了就是跑在 Windows 桌面上的一个常驻小程序左边是日历右边是当天的日程列表到点自动弹提醒数据落在本地 SQLite 文件里不需要联网也不需要启动任何服务。我知道很多人习惯用 Excel、手机日历或者在线待办工具但桌面日程管理仍然有它不可替代的位置——开机就在、缩进托盘不占视野、数据文件拷走就是备份。对于正在学 C# 的开发者来说它又是一个很合适的练手项目数据库、界面绑定、定时器、委托回调全都沾得上。这类系统最怕两件事一是提醒做成“不提醒”二是数据存着存着就乱套。我见过不少自己写着玩的版本界面挺好看但提醒一弹就铺满屏幕或者日程跨天就找不到问题几乎都出在时间处理和提醒状态上。这篇文章按我自己的做法把落地路径整个拆开从表结构到轮询逻辑再到备份和发布照着搭一个能日常用起来的工具并不难核心代码量远比想象中少。2. 先搭数据库与数据访问层日程表设计、SQLiteHelper 封装与安装选择2.1 日程表字段设计为什么提醒时间要单独存个人日程管理系统最核心的实体就是一条日程。看起来只需要“标题 时间”两个字段但只要你后面要加提醒、要跨天显示、要按优先级排序、要标记完成字段就会迅速膨胀。我一般会在一开始就把表设计得宽一点省得后面频繁改表结构。建表 SQL 放在程序启动时执行一次用 IF NOT EXISTS 保证重复启动不会报错。CREATE TABLE IF NOT EXISTS Schedules ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Title TEXT NOT NULL, Description TEXT NOT NULL DEFAULT , StartTime TEXT NOT NULL, EndTime TEXT NOT NULL, Priority INTEGER NOT NULL DEFAULT 2, IsDone INTEGER NOT NULL DEFAULT 0, RemindTime TEXT NOT NULL DEFAULT , Reminded INTEGER NOT NULL DEFAULT 0, CreateTime TEXT NOT NULL DEFAULT (datetime(now,localtime)), UpdateTime TEXT NOT NULL DEFAULT (datetime(now,localtime)) );表建好后记得补两个索引。很多人建表不建索引数据少时感觉不到差别等日程积累到几千条每天按时间范围查询时会明显变慢更关键的是后面提醒轮询每 30 秒执行一次查询计划只要扫全表磁盘 IO 和锁等待都会上来。CREATE INDEX IF NOT EXISTS idx_schedules_start ON Schedules(StartTime); CREATE INDEX IF NOT EXISTS idx_schedules_remind ON Schedules(RemindTime);字段里最容易被忽略的是 RemindTime 和 Reminded。RemindTime 是“提醒时间”它可能跟 StartTime 相同也可能提前十分钟、半小时如果只存一个开始时间后面做“提前提醒”就得临时改开始时间绝对是个陷阱。Reminded 是布尔标记提醒弹窗后置 1避免每 30 秒反复弹同一个提醒具体机制在第 5 章里详细说。下表把每个字段的用途归纳一下方便做界面时对照着取数字段类型默认值用途IdINTEGER自增主键界面 Tag 里存它TitleTEXT必填日程标题DescriptionTEXT空字符串详细说明StartTimeTEXT—开始时间统一存字符串EndTimeTEXT—结束时间必须晚于开始时间PriorityINTEGER21高 2中 3低IsDoneINTEGER0是否完成RemindTimeTEXT空字符串提醒时间可早于开始时间RemindedINTEGER0是否弹过提醒CreateTimeTEXT当前时间创建时间2.2 SQLite 安装选择System.Data.SQLite 与 Microsoft.Data.Sqlite数据库驱动要在写代码之前定下来。C# 里操作 SQLite 最常见的两条路是 System.Data.SQLite 和 Microsoft.Data.Sqlite前者是独立维护的 ADO.NET 实现自带混合模式 DLL老项目里用得非常广后者是微软官方偏轻量的实现API 风格更接近现代 C#。个人日程管理系统没有复杂的并发需求两者都够用。我一般用 System.Data.SQLite因为它跟 SqlConnection、SqlCommand 的用法几乎一致C# 入门读者迁移成本最低网上搜“C# 安装和使用 SQLite 数据库”能出来一大把现成写法可以参考。提示用 NuGet 安装 System.Data.SQLite 时包会带 x86/x64 两套 native 文件后面第 5 章会专门说 AnyCPU 导致加载失败的问题。2.3 封装一个 SQLiteHelper连接、查询、写操作各留一个方法不建议在每个窗体里自己 new SQLiteConnection连接串散落得到处都是后期数据库文件路径一改就要全局替换。常见做法是写一个静态类把连接串和三个方法固定下来Query 返回 DataTable 用于界面绑定Execute 执行增删改ExecuteScalar 留着取单值比如统计数量。这样一个 Helper 能覆盖整个日程系统 95% 的数据访问场景。using System; using System.Data; using System.Data.SQLite; public static class SQLiteHelper { // 数据库文件放在程序运行目录便于直接拷贝备份 private static readonly string connectionString Data Source AppDomain.CurrentDomain.BaseDirectory schedule.db;Version3;; private static readonly object lockObj new object(); public static DataTable Query(string sql, SQLiteParameter[] parameters null) { using (var conn new SQLiteConnection(connectionString)) { conn.Open(); using (var cmd new SQLiteCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } using (var adapter new SQLiteDataAdapter(cmd)) { var dt new DataTable(); adapter.Fill(dt); return dt; } } } } public static int Execute(string sql, SQLiteParameter[] parameters null) { lock (lockObj) // 写操作串行化避免并发写锁冲突 { using (var conn new SQLiteConnection(connectionString)) { conn.Open(); using (var cmd new SQLiteCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } return cmd.ExecuteNonQuery(); } } } } public static object ExecuteScalar(string sql, SQLiteParameter[] parameters null) { using (var conn new SQLiteConnection(connectionString)) { conn.Open(); using (var cmd new SQLiteCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } return cmd.ExecuteScalar(); } } } }这段代码有三个值得说明的参数细节。connectionString 里的 Version3 是 SQLite 文件格式版本不是 .NET 版本漏了它会导致连接失败。Execute 里的 lock 是很多人会忽略的点。日程系统的提醒轮询会同时读数据库如果用户在编辑窗格里点保存读写并发就可能撞出 database is locked。把写操作串行化读操作不加锁能控制住绝大多数并发场景。所有操作都用 using 保证连接及时释放。SQLite 连接不关闭文件会被一直占用后面备份数据库时 File.Copy 就会报“另一个进程正在使用”。2.4 连接串路径与初始化入口处只做一次建表检查连接串里我特意用了 AppDomain.CurrentDomain.BaseDirectory 而不是 Environment.CurrentDirectory。很多人栽在这里用 Visual Studio 调试时后者指向 bin\Debug看起来没问题但如果你把程序设为开机启动或从托盘快捷方式拉起Environment.CurrentDirectory 可能会指向别的目录SQLite 就会在你意想不到的地方凭空生成一个空的 schedule.db。用 BaseDirectory 则始终指向 exe 所在目录行为可预期。Helper 写完后建议在 Main 函数里做初始化而不是在某个窗体事件里建表。原因有两个窗体加载可能因为某些异常被中断但数据库已经写了一半而在入口处初始化失败逻辑更清晰直接提示并退出即可。[STAThread] static void Main() { string sql CREATE TABLE IF NOT EXISTS Schedules ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Title TEXT NOT NULL, Description TEXT NOT NULL DEFAULT , StartTime TEXT NOT NULL, EndTime TEXT NOT NULL, Priority INTEGER NOT NULL DEFAULT 2, IsDone INTEGER NOT NULL DEFAULT 0, RemindTime TEXT NOT NULL DEFAULT , Reminded INTEGER NOT NULL DEFAULT 0, CreateTime TEXT NOT NULL DEFAULT (datetime(now,localtime)), UpdateTime TEXT NOT NULL DEFAULT (datetime(now,localtime)) ); CREATE INDEX IF NOT EXISTS idx_schedules_start ON Schedules(StartTime); CREATE INDEX IF NOT EXISTS idx_schedules_remind ON Schedules(RemindTime);; SQLiteHelper.Execute(sql); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); }提示SQLite 支持一次执行多条语句所以把建表和索引拼成一条传给 Execute 没问题。如果后续报“near ... syntax error”多半是分号位置错了仔细检查一下每条语句的结尾分号。3. 主窗体与日程列表日历选日期、列表看当天、增删改一条链3.1 窗体布局MonthCalendar 加 ListView 比自定义日历控件更稳主窗体我建议用最朴素的组合左边一个 MonthCalendar 选日期右边一个 ListView 显示当天日程底部一个编辑面板负责新增和修改。WinForms 里的第三方日历控件非常多但换机器、换 .NET 版本时容易出字体和渲染问题MonthCalendar 是系统控件样式稳定做单日选择足够。日程项的显示格式我这样设定第一列是时间第二列是标题加优先级标记第三列显示是否完成。ListView 的每个 Item 对应一条日程Tag 里塞进整条记录的 Id后面删除和修改直接从 Tag 取值。布局参数上日历控件高度一般设 180 到 220列表控件 Fill 填满剩余区域底部编辑面板固定高度约 140。这样一个窗体在 1366×768 的笔记本屏幕上就能完整放下不需要滚动。3.2 加载当天日程跨天日程要按区间重叠过滤点击日历后触发 DateChanged 事件调用 LoadSchedules 把当天日程拉出来。这里有一个所有新手都会踩的坑不能只查 StartTime 在当天范围内的记录而要把查询条件改成“区间重叠”否则前一天晚上 22 点开始、第二天凌晨 2 点结束的日程在第二天白天的列表里就神秘消失了。private ListScheduleItem LoadSchedules(DateTime selectedDate) { string dayStart selectedDate.ToString(yyyy-MM-dd) 00:00:00; string dayEnd selectedDate.ToString(yyyy-MM-dd) 23:59:59; string sql SELECT Id, Title, Description, StartTime, EndTime, Priority, IsDone FROM Schedules WHERE StartTime dayEnd AND EndTime dayStart ORDER BY StartTime ASC, Priority ASC; var dt SQLiteHelper.Query(sql, new[] { new SQLiteParameter(dayStart, dayStart), new SQLiteParameter(dayEnd, dayEnd) }); var list new ListScheduleItem(); foreach (DataRow row in dt.Rows) { list.Add(new ScheduleItem { Id Convert.ToInt32(row[Id]), Title row[Title].ToString(), Description row[Description].ToString(), StartTime DateTime.Parse(row[StartTime].ToString()), EndTime DateTime.Parse(row[EndTime].ToString()), Priority Convert.ToInt32(row[Priority]), IsDone Convert.ToInt32(row[IsDone]) 1 }); } return list; }这个查询里的 dayStart 和 dayEnd 都以“yyyy-MM-dd HH:mm:ss”的固定格式传入。SQLite 里 TEXT 类型对字符串排序就是按字符顺序排所以只要格式统一字符串比较结果就等同于时间先后ORDER BY StartTime ASC 不需要做任何转换也不用把字符串解析回 DateTime 再排序。3.3 新增与修改提交前把默认值补齐别让空值进数据库新增和修改共用一个编辑面板点击“新建”时清空输入框双击列表项时把当前值填入输入框。保存按钮统一走 SaveSchedule 方法通过窗体里的 editId 字段判断是插入还是更新。这里有个设计细节日期取自日历控件的 SelectionStart时分秒取自两个 DateTimePicker这样用户换日期时不会影响已经选好的时间。private void btnSave_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtTitle.Text)) { MessageBox.Show(标题不能为空, 校验失败, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var selected cldCalendar.SelectionStart; DateTime start selected.Date dtpStartTime.Value.TimeOfDay; DateTime end selected.Date dtpEndTime.Value.TimeOfDay; if (end start) { MessageBox.Show(结束时间必须晚于开始时间, 校验失败, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } // 提醒时间用户没填就给个兜底值默认开始前5分钟 DateTime remind dtpRemindTime.Checked ? selected.Date dtpRemindTime.Value.TimeOfDay : start.AddMinutes(-5); if (editId -1) { string sql INSERT INTO Schedules (Title, Description, StartTime, EndTime, Priority, RemindTime, IsDone) VALUES (title, desc, start, end, priority, remind, 0); SQLiteHelper.Execute(sql, new[] { new SQLiteParameter(title, txtTitle.Text.Trim()), new SQLiteParameter(desc, txtDesc.Text.Trim()), new SQLiteParameter(start, start.ToString(yyyy-MM-dd HH:mm:ss)), new SQLiteParameter(end, end.ToString(yyyy-MM-dd HH:mm:ss)), new SQLiteParameter(priority, numPriority.Value), new SQLiteParameter(remind, remind.ToString(yyyy-MM-dd HH:mm:ss)) }); } else { string sql UPDATE Schedules SET Title title, Description desc, StartTime start, EndTime end, Priority priority, RemindTime remind, Reminded 0, UpdateTime datetime(now,localtime) WHERE Id id; SQLiteHelper.Execute(sql, new[] { new SQLiteParameter(title, txtTitle.Text.Trim()), new SQLiteParameter(desc, txtDesc.Text.Trim()), new SQLiteParameter(start, start.ToString(yyyy-MM-dd HH:mm:ss)), new SQLiteParameter(end, end.ToString(yyyy-MM-dd HH:mm:ss)), new SQLiteParameter(priority, numPriority.Value), new SQLiteParameter(remind, remind.ToString(yyyy-MM-dd HH:mm:ss)), new SQLiteParameter(id, editId) }); } RefreshCurrentDay(); }保存逻辑里有三个值得注意的点。dtpStartTime 和 dtpEndTime 是 DateTimePicker我把它拆成“日期取日历控件、时分取时间控件”用户改日期时不用重新选时间。更新时把 Reminded 重置为 0这一步很关键。如果这条日程的提醒时间改到了未来旧状态还标着“已提醒”那它永远不会再弹第二次。所有写入都通过 SQLiteParameter 传参不要拼字符串。日程标题里出现单引号或者百分号是常有的事拼 SQL 会在这些字符上翻车。3.4 删除、完成勾选与状态刷新操作后立即重新加载删除前弹确认框确认后按 Id 删除完成勾选则把 IsDone 置 1并在列表里用灰色文字区分。这两个操作代码都不长但容易漏掉一件事操作完后一定要重新加载当天列表否则界面状态和数据库不一致用户会误以为删除没生效。private void btnDelete_Click(object sender, EventArgs e) { if (listView1.SelectedItems.Count 0) { return; } var item listView1.SelectedItems[0]; int id (int)item.Tag; var confirm MessageBox.Show(确定删除这条日程, 删除确认, MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (confirm DialogResult.Yes) { SQLiteHelper.Execute(DELETE FROM Schedules WHERE Id id, new[] { new SQLiteParameter(id, id) }); RefreshCurrentDay(); } }RefreshCurrentDay 内部就是把 3.2 节的 LoadSchedules 结果重新绑定到 ListView顺便更新状态栏的“共 X 条日程”。这个刷新动作放在所有写操作的末尾是保证界面不出现脏数据的底线。如果你在某个按钮事件里忘了调它排查时不要在数据库上浪费时间先看操作后有没有刷新。4. 提醒与常驻托盘轮询策略、防重复弹窗与托盘气泡4.1 为什么用 Timer 每 30 秒扫一次比任务计划程序更省心提醒机制有两条路一是用 Windows 任务计划程序额外配置系统服务二是程序自己在运行期间持续检查。对于个人日程管理系统这种便携小工具配置任务计划程序太笨重而且用户换了机器就要重新配置。桌面程序最常见的做法是 WinForms 自带的 System.Windows.Forms.Timer在 UI 线程里每 30 秒执行一次查询看当前有没有到点未提醒的日程。选 System.Windows.Forms.Timer 而不是 System.Threading.Timer原因只有一个前者的事件回调在 UI 线程执行可以直接弹 MessageBox 和更新控件后者回调在线程池线程弹窗时还要用 this.Invoke 转回 UI 线程处理不好就会出现“跨线程操作无效”的异常。个人日程管理系统不需要高并发触发频率 30 秒一次对 UI 线程的影响几乎为零。间隔参数不是越长越好。设 10 秒会频繁读数据库而且用户可能在输入内容时被打断设 5 分钟又容易错过只有几分钟提前量的提醒。30 秒是个比较均衡的折中值既能在一分钟内弹出提醒又不会让 SQLite 文件长时间被占用。为了进一步减少查询开销Tick 里只查表的主键、标题和提醒时间三个字段不取 Description 这种大文本。4.2 到点检查逻辑先标记已提醒再弹窗把检查逻辑写在 Timer 的 Tick 事件里每一步都要考虑重复触发。核心顺序是查出所有满足“到点未提醒”的记录逐条先 UPDATE 标记然后再弹窗。标记和弹窗必须分开而且标记在前这样即使弹窗被用户挂起下次 Tick 也不会再查出同一条记录。private void timerRemind_Tick(object sender, EventArgs e) { string now DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss); string sql SELECT Id, Title FROM Schedules WHERE Reminded 0 AND RemindTime ! AND RemindTime now ORDER BY RemindTime ASC; var dt SQLiteHelper.Query(sql, new[] { new SQLiteParameter(now, now) }); foreach (DataRow row in dt.Rows) { int id Convert.ToInt32(row[Id]); string title row[Title].ToString(); // 第一步先把这条记录标记为已提醒 SQLiteHelper.Execute( UPDATE Schedules SET Reminded 1 WHERE Id id, new[] { new SQLiteParameter(id, id) }); // 第二步标记成功后再弹窗 if (notifyIcon1.Visible) { notifyIcon1.ShowBalloonTip(5000, 日程提醒, title, ToolTipIcon.Info); } else { MessageBox.Show(title, 日程提醒, MessageBoxButtons.OK, MessageBoxIcon.Information); } } }这段代码的顺序值得反复强调先 UPDATE Reminded 1 再弹窗是防止重复提醒的最小成本方案。很多翻车项目都是先弹窗再标记又或者在循环里弹窗时阻塞了 UI导致 Timer 期间反复进入 Tick提醒像洪水一样涌出来。Reminded 字段持久化在数据库里即使用户重启程序已经弹过的提醒也不会再弹。4.3 托盘、最小化和重启后的状态恢复日程系统是典型的小时连续性工具如果只是普通窗体用户一旦点最小化提醒就看不见了。我一般把主窗体做成“关闭按钮最小化到托盘托盘双击恢复窗体”的形态。托盘图标用 NotifyIcon 组件右键菜单里放“打开主界面”和“退出程序”两项。NotifyIcon 的 DoubleClick 事件本质上就是一个委托回调这也是 C# 事件机制在这个项目里最自然的一个落点。private void MainForm_Resize(object sender, EventArgs e) { if (this.WindowState FormWindowState.Minimized) { // 最小化时隐藏任务栏窗口只保留托盘图标 this.ShowInTaskbar false; notifyIcon1.Visible true; this.Hide(); } } private void notifyIcon1_DoubleClick(object sender, EventArgs e) { this.Show(); this.WindowState FormWindowState.Normal; this.ShowInTaskbar true; }托盘模式下还有一件事容易被忽略重启程序后Reminded 为 1 的旧提醒应该保持不弹而今天未到时的提醒要正常生效。这个逻辑不需要额外编码因为 Reminded 是持久化在数据库里的字段上一次运行弹过提醒的记录重启后同一时间点不会再弹。但注意如果你把 Reminded 只放在内存里而不写库重启就失效所以字段持久化是必须的。4.4 提前量设计与重复日程的取舍提醒精度不需要做到秒级。常见做法是给 dtpRemindTime 提供几个快捷选项准时、提前 5 分钟、提前 15 分钟、提前 1 小时。我一般默认选“提前 5 分钟”因为多数会议和待办事件提前五分钟足够用户把手上事情收尾。重复日程每天、每周是这个项目里最容易把它拖成重工程的功能我不建议第一版做。用一条 RemindTime 加一条 Reminded 字段先保证单条日程的提醒可靠。如果确实需要每天提醒可以等把单条跑顺了再扩展成“生成多条子日程”的方案——在保存时检测到重复规则循环插入多条独立日程记录每条都有自己的 Reminded 状态实现简单也不容易出 bug。5. 避坑记录日期格式、跨天查询与重复提醒的 5 个典型问题5.1 提醒重复弹出现象、原因与解决现象到了提醒时间提示框每 30 秒弹一次关掉又弹用户只能把程序杀了。原因Tick 里的 SELECT 只查了时间条件没有把 Reminded 放进 WHERE或者查询语句包含了 Reminded但 UPDATE 在弹窗之后执行UI 线程被弹窗阻塞UPDATE 一直没跑。解决WHERE 里加 Reminded 0并且把标记更新挪到弹窗前。更稳的做法是先执行 UPDATE再检查受影响行数行数为 1 才弹窗。这样即使两条线程同时查出同一条日程也只有一条能执行成功。5.2 日期格式不统一导致当天列表查不到现象某天添加的日程当天能显示第二天打开就找不到了排序也乱早上 9 点的排在早上 8 点前面。原因添加时用了 DateTime.ToString() 的默认格式插入数据库的是“2025/2/14 9:05”而查询参数用的是“yyyy-MM-dd HH:mm:ss”。TEXT 比较按字符序“2025/2/14”排在“2025-02-14”之后区间判断全部失效。解决入库前统一执行 ToString(“yyyy-MM-dd HH:mm:ss”)查询参数构造时也用同一格式。不要相信用户在 DateTimePicker 里看到的样子一切以格式化后的字符串为准。5.3 跨天日程神秘消失现象今晚 22 点到明天凌晨 2 点的日程添加后第二天白天打开列表一条也看不到。原因加载当天日程的 SQL 写成了 StartTime BETWEEN 当天开始 AND 当天结束只挑“当天开始的日程”跨天日程的开始时间不在当天被过滤掉。解决改成重叠判断 StartTime 当天结束 AND EndTime 当天开始。这个写法同时覆盖单日日程、跨天日程和全天日程是个人日程管理系统里最值得记住的一个查询条件。5.4 System.Data.SQLite 在 64 位系统上启动即崩现象开发机运行正常部署到另一台电脑后一启动就报“未能加载文件或程序集 System.Data.SQLite”或者抛 BadImageFormatException。原因NuGet 包引入了 x86 和 x64 两套 native 文件项目平台是 AnyCPU 时运行时选错位数加载失败。解决项目属性里把目标平台固定成 x64现代 Win10/11 基本都是 64 位如果还在维护老旧 32 位机器再改成 x86。这是一个编译时没事、发布后炸的典型问题部署前先在干净的虚拟机或另一台电脑上跑一遍。5.5 备份时数据库文件被占用现象程序运行中点击备份File.Copy 弹“另一个进程正在使用”或者拷贝出来的 db 文件用数据库工具打不开。原因SQLite 连接没有在操作后释放可能是某个查询漏了 using也可能是备份时提醒 Timer 正在读数据库。解决两个方向一起做。一是确保 SQLiteHelper 的所有方法都用 using 包裹二是备份前先把提醒 Timer 停掉复制完再恢复。顺序是 timerRemind.Stop()然后 File.Copy最后 timerRemind.Start()。5.6 调试技巧把 SQL 和参数一起打进日志我排查日程系统问题时第一步永远是看实际执行的 SQL 和参数。做法是给 SQLiteHelper 加一个调试开关开启后在 Execute 前把格式化好的 SQL 输出。日期格式、参数类型、是否带了多余的引号一眼就能看出来。public static int Execute(string sql, SQLiteParameter[] parameters null) { if (debugMode) { string finalSql sql; if (parameters ! null) { foreach (var p in parameters) { finalSql finalSql.Replace( p.ParameterName, p.Value ); } } System.Diagnostics.Debug.WriteLine(finalSql); } // ... 原有执行逻辑 }提示Debug.WriteLine 只在 DEBUG 编译时生效。发布 Release 版时这段日志代码会被编译器移除不影响性能。想在正式环境保留日志就改用 File.AppendAllText 写文件但要注意别把日程内容里的敏感信息写进去。6. 进阶备份数据库、明日预告与一键发布6.1 给托盘菜单加一键备份备份功能可以做成托盘右键菜单里的一个选项目录固定在程序目录下的 backup 文件夹文件名带时间戳。备份前先停 Timer复制完再恢复避免并发读拷出损坏文件。private void menuBackup_Click(object sender, EventArgs e) { timerRemind.Stop(); try { string dir Path.Combine(Application.StartupPath, backup); Directory.CreateDirectory(dir); string target Path.Combine(dir, schedule_ DateTime.Now.ToString(yyyyMMdd_HHmmss) .db); File.Copy(Path.Combine(Application.StartupPath, schedule.db), target); MessageBox.Show(备份完成 target); } finally { timerRemind.Start(); } }6.2 明日日程预告在托盘气泡或主窗体状态栏加一行“明日共 X 条日程”SQL 与当天加载完全相同只是把边界换成明天的 00:00:00 和 23:59:59。这个功能能给出第二天一早的安排预览做起来很快查询里只取标题、开始时间、优先级三列在 NotifyIcon 的气泡里拼接成摘要。6.3 发布成单文件小工具如果用 .NET FrameworkSQLite 原生文件加 exe、DLL、db 一起拷到目标机器就能跑如果用 .NET 6 以上发布时勾选“生成单个文件”再加修剪输出就是一个独立 exe。数据库文件 schedule.db 会在程序第一次启动时自动生成所以分发时只需要给一个 exe用户拿到手就是零安装的小工具。这个发布形态比较适合个人工具类项目。最终验证建议按这个顺序自查一遍新装机器上能否直接启动、添加一条“一分钟后提醒”的日程、弹出后关掉、改日程时间、再确认旧提醒不重复弹、跨天日程能在次日列表出现、备份文件能拷到另一台电脑打开。跑通这六步这个系统就真的能交给自己日常用了。我自己做这类小工具时吃过太多次“写的时候觉得没问题、用的时候才发现提醒没响”的亏所以现在每一版都会先跑一遍上面六步再发布。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑