资讯动态

C#开发个人日程管理系统:WinForms+SQLite从入门到发布全流程

发布时间:2026/10/1 2:09:38 来源:尧图企业网站定制
简介这是一套基于C#开发的桌面版个人日程管理系统源码包主要面向C#课程设计、毕业设计或希望学习WinForm与SQLite整合开发的读者。系统除了实现待办事项的增删改查还内置番茄钟计时器、兴趣偏好爬虫抓取、任务完成统计及提醒铃声设置等模块界面采用古风主题兼顾实用性与新颖体验。压缩包共402个文件容量约53.92MB包含40个.cs源码文件、62个.dll依赖库、89个.wav音效资源以及docx课程报告、开题报告、pptx演示文稿和数据库文件可完整覆盖从设计、编码到演示汇报的全过程。当前已有1169人学习下载适合需要快速搭建个人日程管理项目、掌握SQLite数据操作、了解定时提醒与爬虫程序写法的开发者参考。配套文档齐全源码结构清晰既可直接运行调试也可作为二次开发或项目文档撰写的范本。1. 个人日程管理系统C#入门到成品交付的典型样本每年毕业季最不缺的就是“日程管理”“备忘录”这类选题被选烂了但恰恰因为小它特别适合把 C# 从入门到交付的流程完整走一遍。基于 C# 实现的个人日程管理系统常见形态是 WinForms 桌面程序配 SQLite 单文件数据库覆盖日程的增删改查、分类管理、到期提醒三件事。它的价值不在功能多而在于一套代码同时触及了界面布局、数据访问、多线程、定时任务和发布部署——这是 C# 桌面开发最小也最完整的闭环。如果你正在做实训、准备毕业设计或者刚学完 C# 基础想从教程跳到独立项目这个方向能让你一个月内走完从建库到发布的全程。2. 先定技术栈与数据模型为什么是 WinForms SQLite2.1 WinForms 与 WPF 怎么选先说结论这个项目用 WinForms不要犹豫。WPF 拥有更好的样式系统和数据绑定但它引入的 MVVM、依赖属性、模板化样式会把你的精力从“日程管理”本身拉到界面框架上。日程管理需要的交互场景无非是点日期看列表、双击编辑、右键删除WinForms 用事件处理器半小时就能写完。反观 WPF光是搞清楚如何用绑定驱动一个 DataGrid 的选中项就足够消耗掉一个晚上。从答辩和演示的角度看WinForms 的稳定性也是优势。程序跑起来不出幺蛾子比界面惊艳更重要。C# 高级编程里的泛型、委托、事件这些特性在这个项目里照样用得上——数据访问层的通用方法、提醒扫描的回调、按钮点击事件背后都是这些机制。换一个花哨的框架反而让评审抓不住重点问到底层原理时你还要额外解释一堆框架概念。2.2 数据库选型SQLite 的免运维优势数据库在 Access、SQL Server、SQLite 之间选。Access 已经边缘化SQL Server Express 虽然功能强大但要求目标机器装了实例或者你得在安装包里捆绑几百 MB 的运行时——对个人日程这种单机应用纯属负担。SQLite 的优势是零安装、零配置整个数据库就是一个 .db 文件。C# 里使用 SQLite 的常见做法是引入 System.Data.SQLite 或 Microsoft.Data.Sqlite 这个 NuGet 包前者老牌稳定后者轻量且在 .NET Core / .NET 5 上维护活跃。日程数据量级在万条以内SQLite 的读写性能绰绰有余备份也简单把 .db 文件复制走就是备份。并发写入虽然不占优势但单机应用根本不会遇到两个进程同时写的场景。需要注意的是不要在 UI 线程做批量数据操作否则界面会卡顿这条在发布章节会一起说。提示如果目标是学习公司里常见的 C/S 架构可以在数据访问层留好接口后续把 SQLite 换成 SQL Server。2.3 表结构设计分类表、日程表、提醒状态功能上需要分类和日程两张核心表提醒状态作为日程字段内嵌。建议直接按这份 DDL 建表语句可以反复执行CREATE TABLE IF NOT EXISTS Categories ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL, Color TEXT DEFAULT #4FC3F7 ); CREATE TABLE IF NOT EXISTS Schedules ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Title TEXT NOT NULL, Content TEXT, CategoryId INTEGER, StartTime TEXT NOT NULL, EndTime TEXT NOT NULL, RemindMinutes INTEGER DEFAULT 0, IsNotified INTEGER DEFAULT 0, CreateTime TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (CategoryId) REFERENCES Categories(Id) ); CREATE INDEX IF NOT EXISTS idx_schedules_start ON Schedules(StartTime); CREATE INDEX IF NOT EXISTS idx_schedules_notified ON Schedules(IsNotified);字段说明标题与正文用 Title、ContentStartTime 和 EndTime 用 TEXT 存储统一格式化为 yyyy-MM-dd HH:mm:ss避免区域设置导致的解析问题RemindMinutes 表示提前多少分钟提醒0 表示不提醒IsNotified 是关键字段标记这条日程的提醒是否已触发防止重复弹窗。这里有三个细节值得注意。第一Id 用 INTEGER PRIMARY KEY AUTOINCREMENTSQLite 会自动生成自增主键不需要在 C# 里赋值。第二SQLite 的外键约束默认关闭程序启动时要执行 PRAGMA foreign_keys ON否则 CategoryId 只是普通整数删除分类后日程会留下悬空引用。第三StartTime 和 IsNotified 都建了索引否则按日期范围查询会全表扫描数据量大了以后界面能明显感觉到延迟。项目分层按经典的 UI / BLL / DAL / Models 四层走。Models 放实体类DAL 放 SQLiteHelper 和 ScheduleDALBLL 放 ScheduleServiceUI 是 WinForms 窗体。这个结构对这个规模的项目看似偏重但它换来的是换数据库、加功能、写单元测试时都不需要动界面代码。有同学问为什么不用 EF CoreEF Core 确实能省掉手写 SQL但这个项目总共就几张表、十几个方法手写参数化 SQL 反而更直观答辩时也更容易讲清楚。实体类可以手写也可以后面接 Dapper 映射。手写的写法与表一一对应public class Schedule { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public int CategoryId { get; set; } public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } public int RemindMinutes { get; set; } public int IsNotified { get; set; } }DateTime 类型与 TEXT 存储之间的转换全部收敛在数据访问层UI 层拿到的永远是 DateTime不关心底层怎么存的。程序首次启动时还要做基础数据初始化检测 Categories 表为空就插入“默认分类”和几个常用分类再塞两条示例日程否则界面打开一片空白演示效果很差。3. 把数据访问层做成工具SQLiteHelper 与日程 CRUD3.1 引用包与连接字符串在 Visual Studio 里通过 NuGet 安装 Microsoft.Data.Sqlite然后在 App.config 中配置数据库路径configuration appSettings add keyDbPath valuedata\schedule.db / /appSettings /configuration运行时不要直接用相对路径因为调试和发布后的工作目录不一样。标准做法是用 AppDomain.CurrentDomain.BaseDirectory 拼出绝对路径顺便创建目录string dbDir Path.Combine(AppDomain.CurrentDomain.BaseDirectory, data); Directory.CreateDirectory(dbDir); string dbPath Path.Combine(dbDir, schedule.db); string connStr $Data Source{dbPath};逻辑说明启动时先创建 data 目录数据库文件也随之生成整个程序拷给别人就是一个文件夹不需要任何安装步骤——这是 SQLite 最让人省心的地方。连接字符串里的路径必须指向这个绝对路径调试器和发布版读的是同一份配置。参数说明如果数据库文件放到项目根目录再用 ../../ 去定位发布后十有八九报错“unable to open database file”。另外SQLite 连接字符串里还可以追加 Poolingtrue 开启连接池对多窗体频繁访问的场景有帮助。3.2 SQLiteHelper通用查询与执行封装数据访问层最值得封装的是一个静态 SQLiteHelper 类核心方法就四个Query 返回 DataTableExecute 执行非查询加上连接字符串的初始化入口public static class SQLiteHelper { public static string ConnStr { get; private set; } public static void Init(string connectionString) { ConnStr connectionString; } public static DataTable Query(string sql, params SqliteParameter[] parameters) { using var conn new SqliteConnection(ConnStr); using var cmd new SqliteCommand(sql, conn); if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); var dt new DataTable(); using (var reader cmd.ExecuteReader()) { dt.Load(reader); } return dt; } public static int Execute(string sql, params SqliteParameter[] parameters) { using var conn new SqliteConnection(ConnStr); using var cmd new SqliteCommand(sql, conn); if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } }逻辑说明每个方法内部自己创建连接、执行完由 using 释放避免连接泄漏。返回 DataTable 是为了让 WinForms 的 DataGridView 直接绑定UI 层不用关心 SqliteDataReader 的生命周期。所有外部输入都通过 SqliteParameter 传入不从字符串拼接 SQL这是防止注入的唯一可靠方式。参数说明参数名带 前缀与 SQL 语句里的占位符一一对应。这样做还有一个容易被忽略的好处——日期时间参数由驱动自动处理格式转换不会因为你是中文区域设置、DateTime.ToString() 输出斜杠格式而导致查不到数据。3.3 日程服务增删改查与按日期查询有了 SQLiteHelper日程的增删改查就是一组很机械的方法。插入方法如下public int Insert(Schedule s) { string sql INSERT INTO Schedules (Title, Content, CategoryId, StartTime, EndTime, RemindMinutes, IsNotified) VALUES (title, content, categoryId, start, end, remind, notified); SELECT last_insert_rowid();; using var conn new SqliteConnection(SQLiteHelper.ConnStr); conn.Open(); using var cmd new SqliteCommand(sql, conn); cmd.Parameters.AddRange(new[] { new SqliteParameter(title, s.Title), new SqliteParameter(content, (object)s.Content ?? DBNull.Value), new SqliteParameter(categoryId, s.CategoryId), new SqliteParameter(start, s.StartTime.ToString(yyyy-MM-dd HH:mm:ss)), new SqliteParameter(end, s.EndTime.ToString(yyyy-MM-dd HH:mm:ss)), new SqliteParameter(remind, s.RemindMinutes), new SqliteParameter(notified, s.IsNotified) }); return Convert.ToInt32(cmd.ExecuteScalar()); }逻辑说明末尾的 SELECT last_insert_rowid() 让插入后直接拿到新记录的自增 Id省掉一次重新查询。Content 允许为空所以判断后传入 DBNull.Value这是 ADO.NET 处理空值列的规范写法直接传 null 会导致数据库收到字符串 null。删除和更新相对直白public void Delete(int id) { SQLiteHelper.Execute(DELETE FROM Schedules WHERE Id id, new SqliteParameter(id, id)); } public int Update(Schedule s) { string sql UPDATE Schedules SET Title title, Content content, CategoryId categoryId, StartTime start, EndTime end, RemindMinutes remind, IsNotified notified WHERE Id id; return SQLiteHelper.Execute(sql, new SqliteParameter(title, s.Title), new SqliteParameter(content, (object)s.Content ?? DBNull.Value), new SqliteParameter(categoryId, s.CategoryId), new SqliteParameter(start, s.StartTime.ToString(yyyy-MM-dd HH:mm:ss)), new SqliteParameter(end, s.EndTime.ToString(yyyy-MM-dd HH:mm:ss)), new SqliteParameter(remind, s.RemindMinutes), new SqliteParameter(notified, s.IsNotified), new SqliteParameter(id, s.Id)); }按日期范围查询是日历视图的数据基础采用左闭右开区间public ListSchedule QueryByDateRange(DateTime begin, DateTime end) { string sql SELECT * FROM Schedules WHERE StartTime begin AND StartTime end ORDER BY StartTime; DataTable dt SQLiteHelper.Query(sql, new SqliteParameter(begin, begin.ToString(yyyy-MM-dd HH:mm:ss)), new SqliteParameter(end, end.ToString(yyyy-MM-dd HH:mm:ss))); var list new ListSchedule(); foreach (DataRow row in dt.Rows) { list.Add(new Schedule { Id Convert.ToInt32(row[Id]), Title row[Title].ToString(), Content row[Content] is DBNull ? : row[Content].ToString(), CategoryId Convert.ToInt32(row[CategoryId]), StartTime DateTime.Parse(row[StartTime].ToString()), EndTime DateTime.Parse(row[EndTime].ToString()), RemindMinutes Convert.ToInt32(row[RemindMinutes]), IsNotified Convert.ToInt32(row[IsNotified]) }); } return list; }参数说明日历视图展示某月时传本月 1 号和下月 1 号半开区间 [begin, end) 保证不会漏掉整点边界的数据也不会重复。字符串格式统一 yyyy-MM-dd HH:mm:ss与存储规则一致DateTime.Parse 解析时不受区域设置影响。到这一层为止增删改查已经齐了下一步接提醒机制。4. 提醒机制定时扫描、多线程与跨线程刷新界面4.1 两种 Timer 怎么选WinForms 里做定时任务最常见的两个选择是 System.Windows.Forms.Timer 和 System.Timers.Timer。前者跑在 UI 线程Tick 事件里可以安全操作控件但精度低受消息循环影响而且 UI 卡死时它也跟着卡。后者跑在后台线程池精度高但 Elapsed 事件里直接改控件会触发跨线程异常。这个项目的提醒场景是分钟级的不需要毫秒精度但数据库查询不适合放 UI 线程——数据量大时界面会掉帧。我一般这样分配主界面刷新用 Forms.Timer 就够了提醒扫描单独用 System.Timers.Timer 在后台跑两边各司其职。4.2 后台轮询与 Invoke 回 UI 线程提醒扫描的做法每 30 秒查一次数据库找出提醒时间落在当前窗口内、且 IsNotified 0 的日程弹窗提示后立刻把 IsNotified 置 1。核心代码在 MainForm 里private System.Timers.Timer _remindTimer; private void InitRemindTimer() { _remindTimer new System.Timers.Timer(30000); _remindTimer.Elapsed OnRemindScan; _remindTimer.Start(); } private void OnRemindScan(object sender, System.Timers.ElapsedEventArgs e) { try { DateTime now DateTime.Now; DateTime upper now.AddMinutes(1); var dueList _service.GetDueSchedules(now, upper); foreach (var item in dueList) { _service.MarkNotified(item.Id); this.Invoke(new Action(() { MessageBox.Show($日程「{item.Title}」将在 {item.StartTime:HH:mm} 开始, 日程提醒, MessageBoxButtons.OK, MessageBoxIcon.Information); })); } } catch (Exception ex) { LogHelper.Write(ex.ToString()); } }逻辑说明查询条件是 [now, now1min) 这个窗口配合 RemindMinutes 算出的实际提醒时间覆盖“提前 N 分钟提醒”的场景。MarkNotified 先于弹窗执行避免弹窗还没关、下一轮扫描又查出同一条记录。LogHelper 是一个静态日志类用 File.AppendAllText 写本地日志文件后台线程的异常不能直接抛到 UI否则程序会无声崩溃。跨线程说明System.Timers.Timer 的 Elapsed 在后台线程触发直接 MessageBox.Show 不一定立即报错但操作文本框、DataGridView 一定会抛 InvalidOperationException。上面用 this.Invoke 把弹窗动作切回 UI 线程Invoke 的参数是一个 Action 委托——这正是 C# 委托机制在这个项目里的实际应用比单纯背概念更直观。GetDueSchedules 对应的 SQL 把提前量放在数据库里计算SELECT * FROM Schedules WHERE IsNotified 0 AND datetime(StartTime, - || RemindMinutes || minutes) BETWEEN now AND uppersqlite 的 datetime 函数支持 modifier这里把 StartTime 往前推 RemindMinutes 分钟落在当前窗口内的就是该提醒的。这样比在 C# 里先算出提醒时间再比较更直观而且 StartTime 上的索引依然能派上用场。4.3 状态字段防止重复提醒和漏提醒的关键IsNotified 这个字段是整个提醒模块的地基。扫描时只查 IsNotified 0 的记录弹窗后立刻置 1。如果漏了这一步程序每次扫描都会把同一条日程弹一遍用户会连续点掉几十个一模一样的提示框——这是整个项目最容易翻车的细节之一。另一个细节用户修改日程时间后编辑窗体的保存逻辑要把 IsNotified 重置为 0。如果这个字段藏在实体里忘了赋值Update 方法会把旧值 1 原样写回用户改完时间永远等不到提醒。建议在编辑窗体 Load 时把这个字段显式读出保存时显式写回不让它走默认值。日程实际开始时间已经过去但还没提醒的记录永远不会再提醒但它们会一直躺在 IsNotified 0 里徒增扫描开销。常见做法是定期执行一条兜底清理UPDATE Schedules SET IsNotified 1 WHERE StartTime datetime(now) AND IsNotified 0;配合界面刷新的约定日历视图和列表视图共用同一个数据源任何增删改操作完成后统一调用 RefreshFromDb() 重新查询当前月份并重新绑定这样界面状态只有一个事实来源不会出现列表和日历各显示一套数据。5. 常见问题与排查这个项目最容易踩的 5 个坑5.1 数据库文件“找不到”发布后一打开就报错现象程序在本机调试正常打包后拷到别人的电脑一运行就抛异常“unable to open database file”。原因连接字符串里用了相对路径或者路径指向的 data 目录在发布机上不存在。调试时工作目录正好是项目输出目录发布后工作目录变了相对路径自然失效。解决用 AppDomain.CurrentDomain.BaseDirectory 拼接绝对路径并在打开连接前执行 Directory.CreateDirectory。排查时可以临时在启动代码里输出 dbPath 到日志文件看实际路径和文件是否存在这个检查方式比看报错文本更直接。5.2 按日期查询始终查不到数据现象DateTimePicker 选了一个日期列表返回 0 条直接用 SQLite 工具打开库能看到数据。原因TEXT 存储的时间格式不统一。中文区域设置下 DateTime.ToString() 默认输出类似 2024/5/6 14:30:00库里存的是 2024-05-06 14:30:00。字符串比较按字符逐位比对/ 的 ASCII 码大于 -两个字符串永久不相等索引和条件都没用错就是格式对不上。解决写入时统一 ToString(yyyy-MM-dd HH:mm:ss)查询参数也走同一个格式绝不在 SQL 里手拼 DateTime。这条规则要在所有数据访问方法里贯彻漏一个就会出这种“看起来没问题但就是查不到”的玄学问题。5.3 跨线程操作控件抛 InvalidOperationException现象提醒触发时程序崩溃报“线程间操作无效: 从不是创建控件‘XXX’的线程访问它”。原因System.Timers.Timer 的 Elapsed 回调运行在线程池线程直接操作窗体控件违反 WinForms 的线程亲和性。MessageBox.Show 有时不报错让你误以为没事等哪天操作一个 DataGridView 就立刻炸。解决用 this.Invoke 或 BeginInvoke 把 UI 操作切回主线程。注意 Invoke 是同步的UI 线程忙时后台扫描会等所以扫描逻辑里不要做重活弹窗前先把 MarkNotified 落地。网上有人建议设置 Control.CheckForIllegalCrossThreadCalls false 来屏蔽异常强烈不建议屏蔽只是不让异常抛出来后台线程直接改 UI 内存对象轻则显示错乱重则崩溃得更莫名其妙。5.4 重复提醒同一日程弹了七八次现象日程到期后提醒框反复弹出用户要连续关掉很多次才能停。原因扫描逻辑没有先更新 IsNotified 就弹窗或者弹窗是模态的用户没来得及点确定下一个 30 秒周期又查出同一条记录。还有一种隐蔽情况用户编辑日程时 IsNotified 没有被重置修改后原状态 0 或 1 被错误保留。解决先执行 UPDATE 把 IsNotified 置 1再弹窗即使弹窗挂着下一轮也查不到这条。编辑保存时显式重置 IsNotified 为 0并把这个规则写进代码注释里。重复提醒的根因永远是状态没有按顺序流转把“查未提醒→标记已提醒→弹窗”这三步的顺序固定下来问题就消失了。5.5 删除分类时报外键冲突或留下脏数据现象分类下还有日程时删除分类程序报错或者强制删了日程列表里出现一堆 CategoryId 不存在的记录。原因SQLite 外键约束默认关闭如果没在连接时执行 PRAGMA foreign_keys ON删除操作不会报错但会造成悬空引用。如果开启了又会在删除时抛出外键约束异常界面直接崩。解决删除分类前先把该分类下的日程转移到一个“默认分类”再执行删除。顺序是 UPDATE Schedules SET CategoryId 1 WHERE CategoryId 要删的分类Id然后 DELETE FROM Categories WHERE Id 要删的Id。连接字符串打开后的第一件事就是执行 PRAGMA foreign_keys ON让数据库自己挡住违规操作。6. 发布、备份与下一步改造把项目变成能长期用的工具6.1 发布配置.NET 6 以上的 WinForms 项目在 Visual Studio 的发布面板里选择 win-x64 目标运行时部署模式选“框架依赖”勾选“生成单个文件”交付物就是一个干净的 exe。发布前检查项目属性里 SQLite 相关原生库是否跟随程序集输出——Microsoft.Data.Sqlite 依赖 SQLitePCLRaw 的 bundle漏掉的话目标机器会报 System.DllNotFoundException这类错误在开发机上永远复现不出来。6.2 数据的备份与迁移SQLite 的备份就是文件复制但要在程序退出后复制否则可能拷到未落盘的脏页。简单做法在设置菜单里加一个“备份数据”按钮把 data/schedule.db 另存为带时间戳的文件数据库体积不大保留最近 7 份就够。所有写操作收敛在 ScheduleService 的十几个方法里以后想迁移到 SQL Server只需要换连接字符串、把 TEXT 日期列改成 datetime2其余代码基本不动。6.3 可选的下一步改造功能上可以加“今日待办”每日汇总窗或者对每周、每月的重复日程做规则展开。技术上可以引入 Dapper 替换手写的 DataRow 映射也可以把主窗体改成左侧分类树、右侧日历加列表的经典布局。做 C# 上位机或企业应用时常用的日志、全局异常捕获、配置文件这套习惯在这个小项目上提前养成后面接复杂项目会省很多事。我的习惯是把这个项目留作自己的模板任何需要 WinForms SQLite 的组合直接复制 SQLiteHelper 和分层骨架半天就能搭出一个能跑的原型。个人日程管理系统听起来不“高大上”但它把 C# 桌面开发里最容易出问题的几个环节全部覆盖了一遍照着做完收获比看十遍教程都大。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑