C#本地日程管理系统:SQLite+EF Core+后台提醒实战 📅 发布时间:2026/9/10 2:27:09 👁 浏览次数: 简介这是一份面向C#初学者与课程设计学生的个人日程管理桌面应用实战项目聚焦时间管理、任务规划与轻量级自动化需求适用于高校软件工程、计算机专业课程设计或毕业设计参考。资源包共402个文件含40个核心C#源码.cs、62个运行依赖DLL含SQLite数据库驱动、17个界面资源图.png、1个可执行程序.exe及1个演示PPT.pptx另有WAV铃声、XML配置、RESX本地化等配套文件整体53.92MB结构完整开箱即用。已有1168人学习下载涵盖课程报告、开题报告、源码与演示PPT四类交付物完整呈现从需求分析、番茄钟计时、兴趣爬虫联动如NBA赛事自动抓取并生成待办、多维统计到古风UI设计的全流程实现逻辑内容预览显示大量编译缓存与项目配置文件印证其为VS开发环境下的可构建工程具备调试、二次开发与功能拓展基础。1. 一个能真正用起来的 C# 个人日程管理系统不是 Demo而是你明天就能打开、添加会议、设置提醒、导出周报的本地工具很多人搜“C# 个人日程管理系统”点开 GitHub 或 CSDN 下载 ZIP 后发现界面能跑双击新增一条日程关掉程序再打开——数据没了。或者用 SQLite 却没建索引500 条日程加载要 3 秒又或者提醒功能只在窗体激活时弹窗后台运行时静默失效更常见的是日期逻辑写成DateTime.Now.ToString(yyyy-MM-dd)拼字符串比对跨月/跨年/夏令时全乱套。这不是系统是教学练习。真正的个人日程管理核心就三件事数据不丢、时间不错、提醒不漏。它不需要用户学 SQL也不该依赖网络或云服务——你的会议安排、待办事项、生日提醒理应完全掌控在本地硬盘上。本篇讲透如何用 C#.NET 6从零搭起一个可长期使用的轻量级日程系统用 SQLite 做持久化用 Windows Forms 做稳定 UI用System.Threading.Timer实现后台常驻提醒所有代码可直接编译运行所有配置项都明确标注作用与修改边界。适合刚学完 C# 类与对象、想落地第一个完整项目的开发者也适合需要快速定制内部办公小工具的 IT 运维或行政人员。2. 用 SQLite Entity Framework Core 实现可靠持久化为什么不用 XML/JSON 文件也不用 LocalDB2.1 选型依据文件 vs 数据库的硬性分界点在哪里新手常误以为“个人工具就该用文件存”但日程管理有三个不可回避的查询模式按日期范围查如“查下周所有会议”→ 需WHERE StartDate BETWEEN start AND end按状态查如“查所有未完成的待办”→ 需WHERE Status Pending按关键词模糊查如“找包含‘客户评审’的日程”→ 需WHERE Title LIKE %客户评审%XML/JSON 文件必须全量加载到内存再 LINQ 筛选1000 条日程就要遍历 1000 次而 SQLite 在磁盘上建索引后上述查询毫秒级返回。更重要的是——事务安全。当用户同时点击“删除”和“编辑”或程序崩溃在保存中途文件操作极易产生半截数据如 JSON 中}缺失而 SQLite 的 WAL 模式保证每次SaveChanges()要么全成功、要么全回滚。LocalDB 虽然也支持事务但需安装 SQL Server Express部署成本高且 Windows 10/11 家庭版默认不支持。SQLite 单个.dll引用零配置.db文件就是普通文件备份只需复制一个文件。提示不要用System.Data.SQLite原生驱动。它需手动管理连接生命周期易出现“数据库被锁定”异常。Entity Framework Core 7 内置 SQLite Provider 已优化并发访问推荐使用。2.2 创建日程实体与上下文关键字段设计与索引策略// Models/ScheduleItem.cs public class ScheduleItem { public int Id { get; set; } [Required, StringLength(100)] public string Title { get; set; } string.Empty; [Required] public DateTime StartDate { get; set; } // 存储 UTC 时间避免本地时区转换错误 [Required] public TimeSpan StartTime { get; set; } // 分离时间部分便于按“上午/下午”筛选 public DateTime? EndDate { get; set; } // 允许空值支持单日事件 public TimeSpan? EndTime { get; set; } [StringLength(500)] public string Description { get; set; } string.Empty; public bool IsAllDay { get; set; } // 全天事件忽略 StartTime/EndTime public string Category { get; set; } 默认; // 支持分类筛选如“工作”“家庭”“学习” public bool IsCompleted { get; set; } public DateTime CreatedAt { get; set; } DateTime.UtcNow; public DateTime? LastModifiedAt { get; set; } }// Data/ScheduleContext.cs public class ScheduleContext : DbContext { public DbSetScheduleItem Items { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) options.UseSqlite(Data Sourceschedule.db;CacheShared;); protected override void OnModelCreating(ModelBuilder modelBuilder) { // 主键索引自动创建无需额外声明 // 关键业务索引按日期范围查询最频繁必须建复合索引 modelBuilder.EntityScheduleItem() .HasIndex(e new { e.StartDate, e.EndDate, e.IsCompleted }); // 按分类完成状态筛选高频加速“工作类未完成事项”列表 modelBuilder.EntityScheduleItem() .HasIndex(e new { e.Category, e.IsCompleted }); // 标题模糊搜索加全文索引SQLite FTS5 modelBuilder.EntityScheduleItem() .ToTable(ScheduleItems) .HasIndex(e e.Title).IsDatabaseGenerated(false); // FTS5 需单独建表 } }2.2.1 索引参数说明与实测对比索引类型创建命令SQLite CLI1000 条数据查询耗时适用场景CREATE INDEX idx_date_status ON ScheduleItems(StartDate, EndDate, IsCompleted);手动执行平均 8ms“查本周未完成事件”CREATE INDEX idx_category_status ON ScheduleItems(Category, IsCompleted);手动执行平均 5ms切换分类标签页CREATE VIRTUAL TABLE ScheduleItems_fts USING fts5(Title, Description);需在 Migration 中执行全文检索 12ms搜索框输入“合同”注意EF Core 的HasIndex()不会自动创建 FTS5 虚拟表必须在首次迁移时用context.Database.ExecuteSqlRaw()执行建表语句。否则WHERE Title LIKE %xxx%会全表扫描。2.3 初始化数据库与首次迁移避免“找不到数据库文件”的启动失败// Program.cs - 应用启动时执行 static void InitializeDatabase() { using var context new ScheduleContext(); // 确保数据库文件存在且可写 if (!File.Exists(schedule.db)) { context.Database.EnsureCreated(); // 创建表结构 // 补充插入默认分类和示例数据 context.Items.AddRange( new ScheduleItem { Title 每日站会, StartDate DateTime.Today, StartTime TimeSpan.FromHours(9.5), Category 工作 }, new ScheduleItem { Title 健身, StartDate DateTime.Today.AddDays(1), StartTime TimeSpan.FromHours(19), Category 健康 } ); context.SaveChanges(); } else { // 检查是否需要迁移版本升级时 context.Database.Migrate(); // 执行挂起的迁移 } }2.3.1 迁移失败的典型排错路径错误SQLite Error 14: unable to open database file→ 检查当前工作目录Console.WriteLine(Directory.GetCurrentDirectory());确保schedule.db路径可写避免放在Program Files下。解决方案改用绝对路径Path.Combine(AppDomain.CurrentDomain.BaseDirectory, schedule.db)。错误The model backing the context has changed since the database was created→ 删除schedule.db文件重新运行程序触发EnsureCreated()。生产环境应改用 Code First Migrations 并生成.sql脚本。3. Windows Forms UI 实现解决“UI 卡顿”与“扫码枪触发事件”的真实痛点3.1 主窗体布局与数据绑定用 BindingSource 避免手动刷新// MainForm.cs private BindingSource _bindingSource new BindingSource(); private ScheduleContext _context; public MainForm() { InitializeComponent(); _context new ScheduleContext(); LoadData(); } private void LoadData() { var items _context.Items .Where(x x.StartDate DateTime.Today.AddDays(-7) x.StartDate DateTime.Today.AddDays(30)) .OrderBy(x x.StartDate) .ThenBy(x x.StartTime) .ToList(); _bindingSource.DataSource items; dataGridView1.DataSource _bindingSource; // 绑定列避免 DataGridView 自动生成列导致格式错乱 dataGridView1.Columns[Id].Visible false; dataGridView1.Columns[CreatedAt].Visible false; dataGridView1.Columns[LastModifiedAt].Visible false; dataGridView1.Columns[Description].Width 200; dataGridView1.Columns[StartDate].DefaultCellStyle.Format yyyy-MM-dd; dataGridView1.Columns[StartTime].DefaultCellStyle.Format t; }3.1.1 为什么BindingSource能解决 UI 卡顿当用户编辑某行标题后点击“保存”若直接调用dataGridView1.Refresh()整个网格会重绘所有单元格100 行数据卡顿明显。而BindingSource将数据源与视图解耦修改items[index].Title 新标题后调用_bindingSource.ResetItem(index)仅重绘第index行其余行保持原状ResetItem内部触发ListChanged事件DataGridView 自动更新对应单元格提示禁用AutoSizeMode。将dataGridView1.AutoSizeColumnsMode DataGridViewAutoSizeColumnsMode.None;手动设置各列宽度。否则每次数据量变化都会触发布局重算导致滚动卡顿。3.2 扫码枪触发事件把 USB 扫码器当键盘用捕获条码后自动聚焦到搜索框扫码枪本质是 HID 键盘设备输入条码后自动发送Enter。关键不是“监听扫码枪”而是拦截全局 Enter 键并判断焦点位置// MainForm.cs protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { // 当焦点不在文本框内且按下 Enter 时视为扫码完成 if (keyData Keys.Enter !this.ActiveControl?.GetType().IsAssignableFrom(typeof(TextBox)) true) { // 获取剪贴板内容扫码枪通常会先输入再回车内容已在剪贴板 if (Clipboard.ContainsText()) { string barcode Clipboard.GetText().Trim(); if (!string.IsNullOrEmpty(barcode)) { SearchByBarcode(barcode); return true; // 拦截 Enter防止触发其他按钮 } } } return base.ProcessCmdKey(ref msg, keyData); } private void SearchByBarcode(string barcode) { // 示例按条码匹配日程标题或描述 var result _context.Items .FirstOrDefault(x x.Title.Contains(barcode) || x.Description.Contains(barcode)); if (result ! null) { // 选中该行并滚动到可视区域 int rowIndex _bindingSource.IndexOf(result); dataGridView1.FirstDisplayedScrollingRowIndex rowIndex; dataGridView1.Rows[rowIndex].Selected true; dataGridView1.CurrentCell dataGridView1[0, rowIndex]; } else { MessageBox.Show($未找到条码 {barcode} 对应的日程); } }3.2.1 扫码枪兼容性调试要点问题现象原因解决方案扫码后无反应扫码枪设置为“不发送 Enter”只输出字符进入扫码枪设置模式通常扫特定配置码启用 “Suffix: Enter”扫码内容带乱码扫码枪键盘布局与系统不匹配如扫码枪设为美式键盘系统为中文输入法在扫码枪设置中切换为 “Keyboard Layout: US”多次扫码只响应第一次剪贴板内容被其他程序清空改用TextBox.KeyDown事件在 TextBox 获得焦点时捕获输入流需扫码枪设为“输出到焦点窗口”4. 后台提醒服务用 System.Threading.Timer 实现低资源占用的常驻提醒4.1 为什么不用 Windows Forms Timer—— UI 线程阻塞的本质System.Windows.Forms.Timer运行在 UI 线程若提醒逻辑包含文件读写或网络请求如发送邮件会直接冻结界面。而System.Threading.Timer在后台线程池中执行但带来新问题跨线程访问 UI 控件非法。正确做法是分离“检查逻辑”与“UI 响应”// Services/ReminderService.cs public class ReminderService { private readonly ScheduleContext _context; private readonly Timer _timer; private readonly ActionScheduleItem _onReminderTriggered; public ReminderService(ScheduleContext context, ActionScheduleItem onReminderTriggered) { _context context; _onReminderTriggered onReminderTriggered; // 每 30 秒检查一次平衡精度与 CPU 占用 _timer new Timer(CheckReminders, null, TimeSpan.Zero, TimeSpan.FromSeconds(30)); } private void CheckReminders(object state) { try { // 查询未来 5 分钟内、未提醒过的日程 var now DateTime.Now; var windowEnd now.AddMinutes(5); var dueItems _context.Items .Where(x x.StartDate.Date now.Date // 同一天 x.StartDate.TimeOfDay now.TimeOfDay x.StartDate.TimeOfDay windowEnd.TimeOfDay !x.IsCompleted x.LastModifiedAt now.AddMinutes(-1)) // 避免重复提醒 .ToList(); foreach (var item in dueItems) { // 标记为已提醒避免下次循环再触发 item.LastModifiedAt now; _context.SaveChanges(); // 通过委托通知 UI 线程 _onReminderTriggered(item); } } catch (Exception ex) { // 记录日志但不抛出避免 Timer 停止 Debug.WriteLine($Reminder check failed: {ex.Message}); } } }4.2 在 MainForm 中安全触发提醒窗体InvokeRequired 与 BeginInvoke// MainForm.cs private ReminderService _reminderService; public MainForm() { InitializeComponent(); _context new ScheduleContext(); // 构造 ReminderService传入 UI 更新委托 _reminderService new ReminderService( _context, item ShowReminderDialog(item) ); } private void ShowReminderDialog(ScheduleItem item) { // 必须检查跨线程调用 if (this.InvokeRequired) { this.BeginInvoke(new ActionScheduleItem(ShowReminderDialog), item); return; } // 此时一定在 UI 线程可安全创建窗体 var dialog new ReminderDialog(item); dialog.ShowDialog(); // 模态对话框阻止用户继续操作 // 用户关闭后根据选择更新状态 if (dialog.RemindLater) { item.StartDate item.StartDate.AddMinutes(15); } else if (dialog.MarkAsCompleted) { item.IsCompleted true; } _context.SaveChanges(); }4.2.1 提醒窗体的关键交互设计// Forms/ReminderDialog.cs public partial class ReminderDialog : Form { public bool RemindLater { get; private set; } public bool MarkAsCompleted { get; private set; } private readonly ScheduleItem _item; public ReminderDialog(ScheduleItem item) { InitializeComponent(); _item item; labelTitle.Text item.Title; labelTime.Text ${item.StartDate:MM-dd HH:mm} {item.Description}; // 设置窗体始终置顶但不抢占焦点避免打断用户当前工作 this.TopMost true; this.ShowInTaskbar false; } private void buttonSnooze_Click(object sender, EventArgs e) { RemindLater true; this.Close(); } private void buttonComplete_Click(object sender, EventArgs e) { MarkAsCompleted true; this.Close(); } // 按 Esc 关闭按 Enter 等同于“完成” protected override bool ProcessDialogKey(Keys keyData) { if (keyData Keys.Escape) { this.Close(); return true; } if (keyData Keys.Enter) { MarkAsCompleted true; this.Close(); return true; } return base.ProcessDialogKey(keyData); } }提示TopMost true可能被全屏应用如游戏、视频播放器压制。若需强提醒可调用FlashWindowExAPI 让任务栏图标闪烁但需 P/Invoke 声明。5. 导出与打印用 iText7 将日程按周/月分层输出 PDF文本严格落入指定矩形框5.1 为什么用 iText7 而非 PrintDocument—— 精确排版的不可替代性PrintDocument依赖打印机驱动不同设备渲染效果差异大而 PDF 是设备无关格式。iText7 的CanvasAPI 允许将文本精确绘制到坐标(x, y)的矩形框内完美适配“周计划表”这类固定模板// Export/PdfExporter.cs public static void ExportWeeklySchedule(ListScheduleItem items, string outputPath) { using var writer new PdfWriter(outputPath); using var pdf new PdfDocument(writer); var page pdf.AddNewPage(PageSize.A4); var canvas new Canvas(page.NewContentStream(), page.GetPageSize()); // 定义周计划表区域从 (50, 750) 开始宽 500高 600 var tableRect new Rectangle(50, 750, 500, 600); // 绘制表头 canvas.SetFontSize(16).SetFontColor(ColorConstants.BLACK) .ShowTextAligned(个人周计划表, 300, 800, TextAlignment.CENTER); // 绘制周一到周日列标题 var weekdays new[] { 周一, 周二, 周三, 周四, 周五, 周六, 周日 }; for (int i 0; i weekdays.Length; i) { float x 50 i * 70f; canvas.ShowTextAligned(weekdays[i], x 35, 730, TextAlignment.CENTER); } // 按日期分组日程 var grouped items.GroupBy(x x.StartDate.Date.DayOfWeek) .OrderBy(g g.Key) .ToList(); // 逐列填充日程每列宽 70从 y700 开始向下排列 for (int dayIndex 0; dayIndex grouped.Count; dayIndex) { var dayItems grouped[dayIndex].OrderBy(x x.StartTime).ToList(); float colX 50 dayIndex * 70f; for (int itemIndex 0; itemIndex Math.Min(dayItems.Count, 8); itemIndex) { var item dayItems[itemIndex]; float y 700 - itemIndex * 25f; // 关键文本严格限制在列内矩形中自动换行 var text new Paragraph(${item.StartTime:hh:mm} {item.Title}) .SetFontSize(9) .SetTextAlignment(TextAlignment.LEFT); // 创建约束矩形宽 65高 20左上角 (colX5, y-10) var rect new Rectangle(colX 5, y - 10, 65, 20); canvas.ShowTextAligned(text, rect.GetLeft() 2, y, TextAlignment.LEFT); } } canvas.Close(); }5.1.1 矩形框内文本换行的底层控制iText7 的Paragraph默认不换行需显式设置SetFixedLeading(12)行高和SetTextAlignment。但真正实现“文本落入指定矩形”靠的是ShowTextAligned的坐标计算rect.GetLeft() 2确保文本距矩形左边界 2pty以基线baseline为锚点而非顶部故y值需根据字体大小调整若文本超长需手动切分item.Title.Substring(0, Math.Min(15, item.Title.Length)) ...注意iText7 商业版需授权但开源版AGPL完全满足本需求。NuGet 包名itext7.kernel和itext7.layout。5.2 一键导出当前视图绑定 DataGridView 选中行到 PDF// MainForm.cs private void exportSelectedToolStripMenuItem_Click(object sender, EventArgs e) { var selectedRows dataGridView1.SelectedRows.CastDataGridViewRow() .Select(r r.DataBoundItem as ScheduleItem) .Where(x x ! null) .ToList(); if (!selectedRows.Any()) { MessageBox.Show(请先选择要导出的日程); return; } var saveDialog new SaveFileDialog { Filter PDF 文件 (*.pdf)|*.pdf, FileName $日程导出_{DateTime.Now:yyyyMMdd_HHmmss}.pdf }; if (saveDialog.ShowDialog() DialogResult.OK) { try { PdfExporter.ExportWeeklySchedule(selectedRows, saveDialog.FileName); MessageBox.Show($已导出 {selectedRows.Count} 条日程到 {saveDialog.FileName}); } catch (Exception ex) { MessageBox.Show($导出失败{ex.Message}); } } }本文还有配套的精品资源点击获取