☰
C# WinForms日程管理系统:SQLite+番茄钟+爬虫完整课程设计解析
2026/10/13 6:06:28 网站建设 项目流程

简介:一份面向计算机专业课程设计的个人日程管理系统完整资源包,基于C#语言开发,采用Windows窗体应用架构,适合需要完成桌面应用课题或学习日程管理开发流程的读者。系统内置待办事项增删改查、番茄钟专注计时、兴趣偏好爬取与自动生成待办、时间段任务统计、提醒铃声配置等实用功能,并辅以古风视觉风格,兼顾功能性与体验感。压缩包内共402个文件,整体约53.92MB,内容涵盖程序源码、课程报告、开题报告、演示PPT及多种素材:其中包括40个C#源码文件、62个依赖库DLL、89个用于提醒场景的WAV音频、17个PNG界面图片等,目录结构清晰,方便按模块检索与二次开发。该资源在下载站已有1171人学习浏览,可帮助读者理解日程管理系统从需求分析、数据库设计到功能实现与界面优化的完整思路,也可作为课程设计答辩、技术演练或日常管理工具搭建的参考。

1. 个人日程管理系统:一份能复现的 C# 课程设计包,拆开讲透

很多人看到“日程管理系统”第一反应是“无非就是 DataGridView 加几个按钮”,但真正把这份基于 C# 的 WinForms 源码跑起来才发现,翻车点全藏在 SQLite 的 DLL 位数、爬虫线程和 UI 消息循环这些不起眼的地方。这份资源是一套完整的个人日程管理桌面程序,除待办事项的增删改查外,还包含番茄钟、兴趣爬虫、统计报表和提醒铃声设置,并覆盖了课程报告、开题报告与演示 PPT。适合正在做课程设计的学生、想快速搭一个 WinForms + SQLite 完整项目的开发者,以及需要一份能拿去答辩又能动手改的参考源码的人。

2. 工程骨架与数据层:SQLite 选型依据和待办 CRUD 的最小实现

2.1 拿到压缩包先看什么:解决方案结构与本机 DLL 分布

解压这份课程设计压缩包后,第一眼会被一堆.cache文件和.dll.altconfig后缀的文件劝退——那些是 Visual Studio 构建过程产生的中间缓存,真正要关注的是ScheduleManagement.csproj和System.Data.SQLite.dll。前者是项目入口,后者是 SQLite 数据库在 .NET 环境下的驱动核心。压缩包内一般还会附带课程报告、开题报告和演示幻灯片,这几个文档对齐了答辩时需要讲清楚的设计动机和进度安排。

我一般会先用文本编辑器打开.csproj检查目标框架版本,确认是 .NET Framework 4.x 还是 .NET Core / .NET 5+,因为这两者对System.Data.SQLite.dll的原生依赖方式不同。如果是 .NET Framework 4.0 / 4.5 的 WinForms 项目,SQLite 通过System.Data.SQLite.dll混合模式程序集加载,打包时不仅要有托管 DLL,还要注意目录下是否附带对应平台(x86 / x64)的原生扩展文件。项目正文里那一串System.Data.SQLite.dll.altconfig正是 Visual Studio 为混合模式程序集生成的“备用配置”,实际部署时不需要手动拷贝它们,只需要在项目引用里配置正确路径。

数据库连接字符串放在App.config中,这是最常规也最好维护的方式。示例配置如下:

<?xml version="1.0" encoding="utf-8"?> <configuration> <connectionStrings> <add name="ScheduleDb" connectionString="Data Source=|DataDirectory|schedule.db;Version=3;Pooling=True;" providerName="System.Data.SQLite" /> </connectionStrings> </configuration>

Data Source=|DataDirectory|schedule.db是这段配置的关键,|DataDirectory|是一个占位符,运行时会自动替换为应用程序的数据目录,默认情况下指向 exe 所在目录下的bin\Debug或bin\Release文件夹。使用占位符而不是硬编码绝对路径,能让项目在换机器、换目录后直接跑起来。Version=3表示使用 SQLite 3.x 文件格式,Pooling=True建议保留,它能复用数据库连接,减少频繁打开关闭文件的开销。

2.2 待办事项 CRUD:用 Repository 类隔离 SQL 细节

日程管理最底层的操作就是对待办事项的增删改查。这部分如果直接在窗体事件里写 SQLiteCommand,界面代码很快就会变得臃肿。常见做法是把所有 SQL 封装到一个 Repository 类中,UI 层只调用方法。源码里的待办模型大致长这样:

using System.Data.SQLite; public class TodoItem { public long Id { get; set; } public string Title { get; set; } public string Detail { get; set; } public bool IsDone { get; set; } } public class TodoRepository { private readonly string _conn; public TodoRepository(string connectionString) { _conn = connectionString; } public int Insert(TodoItem item) { const string sql = @" INSERT INTO todos (title, detail, is_done, created_at) VALUES (@title, @detail, @is_done, datetime('now', 'localtime'));"; using (var conn = new SQLiteConnection(_conn)) { conn.Open(); using (var cmd = new SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue("@title", item.Title); cmd.Parameters.AddWithValue("@detail", item.Detail ?? ""); cmd.Parameters.AddWithValue("@is_done", item.IsDone ? 1 : 0); return cmd.ExecuteNonQuery(); } } } }

这段代码里有几个细节值得说明。SQL 里使用datetime('now', 'localtime')而不是在 C# 端传DateTime.Now,是为了让数据库层面的时间生成统一,避免由于机器时区设置不同造成记录时间偏差。参数化查询不是可有可无的规范,而是为了防止用户输入包含单引号时直接把 SQL 语句炸掉——标题里写“It's done”这类内容,如果拼字符串就会语法错误。@is_done参数传入item.IsDone ? 1 : 0,是因为 SQLite 没有独立的布尔类型,内部存的是整数 0 或 1,这样写入和读取都不容易产生类型推断歧义。

更新和删除操作遵循同样的套路,区别只在 SQL 语句的UPDATE与DELETE关键字,以及WHERE id = @id条件。删除操作返回的影响行数可以用来判断这条待办是否真的存在,如果返回 0,就说明用户选中的是一个已经被其他窗口删掉的数据,界面需要刷新而不是继续操作。

2.3 为什么选 SQLite:和 Access、SQL Server Express 的取舍逻辑

课程设计里数据库选型,最怕的是“别人用啥我用啥”却说不清理由。SQLite 在这个项目里的优势非常直观,可以和 Access 和 SQL Server Express 做一个横向对比:

对比项SQLiteAccessSQL Server Express
外部依赖一个 DLL 即可需要安装 Office 驱动或 ACE Provider需要安装 SQL Server 实例或 LocalDB
数据库文件单个.db文件单个.mdb/.accdb文件多个数据文件,需要附加数据库
部署复杂度拷贝即用目标机器需有对应驱动环境配置成本高
并发能力支持多读单写并发锁较多完整客户端/服务器架构
适合场景桌面单机应用老旧 OA 系统Web/大型系统

WinForms 桌面应用本身就是单机形态,SQLite 的单文件存储方式让“拷贝整个项目到另一台电脑就能跑”成为可能。相比之下,SQL Server Express 在答辩演示时如果临时连不上服务,整个演示流程就会卡在读数据库这步。Access 虽然也是单文件,但 OLEDB 驱动在不同 Office 版本间差异较大,而且锁定机制在并发读写时更容易报错。SQLite 的另一个优势是 SQL 语法接近标准 SQL,课程设计里写过的查询语句,以后接手业务系统时迁移成本很低。

3. 番茄钟与提醒服务:计时状态机、铃声接入与窗口交互细节

3.1 番茄钟为什么要用状态机:从点击开始到真正计时

番茄钟模块表面上是一个倒计时标签,但完整功能涉及“工作 25 分钟 → 休息 5 分钟”的循环切换,以及开始、暂停、继续、结束四个操作。如果不用状态机,只用两个 bool 变量控制,很容易出现“暂停后继续却重新计时”“休息段结束后没切回工作段”这类逻辑漏洞

源码里通常用枚举定义四种状态,配合 Windows Forms 的 Timer 控件驱动。

public enum PomodoroState { Idle, // 空闲,尚未开始 Running, // 工作计时中 Paused, // 暂停 Finished // 本轮结束 } public class PomodoroTimer { private System.Windows.Forms.Timer _timer; private int _remainingSeconds; public PomodoroState State { get; private set; } // 默认工作时间 25 分钟,休息时间 5 分钟,可通过界面配置 public int WorkSeconds { get; set; } = 25 * 60; public int BreakSeconds { get; set; } = 5 * 60; public event EventHandler<int> Tick; public event EventHandler Finished; public PomodoroTimer() { _timer = new System.Windows.Forms.Timer(); _timer.Interval = 1000; _timer.Tick += (s, e) => OnTick(); State = PomodoroState.Idle; _remainingSeconds = WorkSeconds; } public void Start() { if (State == PomodoroState.Idle || State == PomodoroState.Paused) { State = PomodoroState.Running; _timer.Start(); } } public void Pause() { if (State == PomodoroState.Running) { State = PomodoroState.Paused; _timer.Stop(); } } private void OnTick() { _remainingSeconds--; Tick?.Invoke(this, _remainingSeconds); if (_remainingSeconds <= 0) { _timer.Stop(); State = PomodoroState.Finished; Finished?.Invoke(this, EventArgs.Empty); } } }

这里采用System.Windows.Forms.Timer而不是System.Threading.Timer,是因为 WinForms 的 Timer 直接运行在 UI 线程的消息循环里,更新Label.Text时不需要跨线程 Invoke,代码更简洁,也不容易踩“线程间操作无效”的异常。Interval=1000表示每 1000 毫秒触发一次 Tick,但实际运行时由于消息循环繁忙,触发间隔会略大于 1 秒。如果对精度有要求,不要依赖累计 Tick 次数,而是记录开始时间然后计算差值,这个坑在第 5 章会展开。

// 在窗体中初始化并订阅事件 _pomoTimer = new PomodoroTimer(); _pomoTimer.WorkSeconds = int.Parse(txtWorkMinute.Text) * 60; _pomoTimer.BreakSeconds = int.Parse(txtBreakMinute.Text) * 60; _pomoTimer.Tick += (s, remaining) => lblRemain.Text = FormatTime(remaining); _pomoTimer.Finished += (s, e) => { // 切到休息模式或用 MessageBox 提醒 _pomoTimer.Start(); };

工作与休息的秒数从界面的 TextBox 读取后重新赋值,这样用户就能按自己的节奏调整番茄时长。常见的教学 demo 会把 25 分钟写成常量,但实际使用中不同人对专注时长的耐受度差异很大,我一般会把 WorkSeconds 和 BreakSeconds 暴露为可配置属性,然后在设置保存时写回数据库。

3.2 铃声提醒与托盘交互:SoundPlayer 的选用边界

番茄钟结束时需要提醒用户。源码里常见做法是用System.Media.SoundPlayer播放嵌入资源的 wav 文件,而不是用 Console.Beep 或 MessageBox 闪一下。SoundPlayer 的优势是代码少、无需引入第三方库,而且 wav 可以随项目一起编译到资源文件中,不存在路径丢失问题。

using System.Media; private SoundPlayer _player; private void LoadRingtone(string wavFilePath) { // 从外部文件加载,方便用户自行替换铃声 _player?.Stop(); _player?.Dispose(); _player = new SoundPlayer(wavFilePath); _player.Load(); } private void PlayFinishedRing() { if (_player == null) return; _player.PlayLooping(); // 循环播放,直到用户点击“停止提醒” }

代码里优先从外部路径加载 wav,而不是直接调用Properties.Resources.notify,是为了让提醒铃声成为可配置项。用户在“提醒设置”界面选择一个 wav 文件后,路径会被写入数据库,下次启动时从记录里读取,重新 new 一个 SoundPlayer。如果用嵌入资源,更改铃声就需要重新编译程序,失去了设置界面的意义。

PlayLooping()是循环播放,番茄钟结束一次后不能让它一直响下去,还需要在窗体上提供一个“停止提醒”按钮,或者监听提醒弹窗的关闭事件,在关闭时调用_player.Stop()并把_player.Dispose()释放资源。另外一个容易被忽视的交互点是:当用户把窗口最小化或最小化到系统托盘时,提醒必须仍然生效。WinForms 的 Timer 在程序最小化后并不会自动停止,但如果你用 NotifyIcon 把窗口隐藏而不是真正最小化,需要考虑消息循环是否还被阻塞。这里更稳妥的方案是让窗口在“关闭”时最小化到托盘,并设置 NotifyIcon 的 BalloonTip 作为次要提醒通道,声音和气泡提示同时触发。

4. 兴趣爬虫与统计报表:从请求到可视化的完整链路

4.1 爬虫模块:把“NBA”变成待办事项的流程设计

这个项目的爬虫功能是亮点,也是答辩时最容易吸引提问的地方:“根据用户兴趣偏好,在指定网站或邮件中爬取相关内容并添加到待办事项”。实际源码中邮件爬取依赖协议支持和授权,复杂度高,通常情况下实现的是网页爬取——用户输入如“NBA”,系统在后台线程请求目标网页,解析出标题和地址,截取前几条插入待办表。

using System.Net.Http; public async Task<List<string>> FetchEventsAsync(string keyword) { string url = $"https://example-news.example/search?q={Uri.EscapeDataString(keyword)}"; using (var client = new HttpClient()) { client.Timeout = TimeSpan.FromSeconds(10); client.DefaultRequestHeaders.UserAgent.ParseAdd( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); HttpResponseMessage resp = await client.GetAsync(url); resp.EnsureSuccessStatusCode(); string html = await resp.Content.ReadAsStringAsync(); return ParseTitlesFromHtml(html, keyword); } }

这里的Uri.EscapeDataString(keyword)一定要记得,把中文关键词如“篮球 直播”编码成 URL 安全的字符串,否则服务器可能返回 400。UserAgent请求头也要设置,很多站点会对空 UA 的请求直接拦截。解析 HTML 标题时,如果项目里引入了 HtmlAgilityPack,可以用 XPath 或 CSS 选择器提取节点;如果没有引入第三方库,则用正则抓取<h3>或<a>标签内的文本,但正则方案在页面结构变化后很容易失效。常见做法是加入一个简单去重:解析出来的标题和数据库已有待办做比对,相同标题不重复插入。

爬虫运行在后台线程,绝不可以在 UI 线程里直接同步HttpGet,否则网络慢时整个窗口会卡死。使用async/await是 WinForms 中最省事的异步方案,调用处保持同步写法,线程切换由上下文接管:

private async void BtnFetch_Click(object sender, EventArgs e) { btnFetch.Enabled = false; try { var titles = await FetchEventsAsync(txtKeyword.Text.Trim()); foreach (var title in titles.Take(5)) { _repository.Insert(new TodoItem { Title = title, Detail = "自动爬取", IsDone = false }); } RefreshGrid(); } catch (Exception ex) { MessageBox.Show("抓取失败,请检查网络或关键词:" + ex.Message); } finally { btnFetch.Enabled = true; } }

titles.Take(5)限制写入条数,避免一次爬几十条把待办列表塞满。finally里恢复按钮可用状态,即使抛异常也不会让界面按钮失效。这段代码的容错逻辑对应了摘要里说的“非网络环境下只能运行部分功能”:断网或站点不可达时,爬虫分支报错提示,但增删改查、番茄钟、统计仍能正常用,SQLite 数据库不依赖网络。

4.2 统计模块:时间段完成率与各月番茄数的 SQL 聚合

统计功能需要回答两个问题:指定时间段内完成了多少、未完成多少;该时间段内每个月完成了多少个番茄钟。这些数据完全可以在 SQLite 层面用聚合查询完成,而不是把明细数据全捞到 C# 里手动数。

SELECT date(created_at) AS day, SUM(CASE WHEN is_done = 1 THEN 1 ELSE 0 END) AS done_count, COUNT(*) AS total_count FROM todos WHERE created_at >= datetime(@start) AND created_at < datetime(@end, '+1 day') GROUP BY date(created_at) ORDER BY day;

created_at < datetime(@end, '+1 day')这个条件很关键,它能覆盖用户选择“今天”作为结束日期的场景,把当天零点以后创建的待办也包含进来,避免由于时间戳带时分秒导致当天数据被漏掉。SUM(CASE WHEN ...)是 SQLite 里做条件计数的通用写法,没有独立布尔类型的环境下比COUNT(IF())更兼容。穿搭提醒:统计日期范围做完后,同样的时间过滤条件应用到番茄钟表即可:

SELECT strftime('%Y-%m', finish_time) AS month, COUNT(*) AS pomo_count FROM pomodoro_log WHERE finish_time >= datetime(@start) AND finish_time < datetime(@end, '+1 day') GROUP BY month ORDER BY month;

strftime('%Y-%m', finish_time)把finish_time归一化成“年-月”格式,例如2024-03-15 12:00:00变成2024-03,这样 GROUP BY 后每个月份一行。统计模块在 UI 上通常用 Chart 控件展示,柱状图放每日完成率,折线图放每月番茄数。Chart 控件绑定时不需要手动循环,直接chart.Series[0].Points.DataBindXY(dayList, countList)即可,但要注意 X 轴日期类型的格式设置,否则显示出来的坐标刻度可能是数字而非日期。

数据量小时在代码里分组也能跑,但我更推荐把聚合逻辑留在 SQL 里,因为后续如果要支持“按周统计”“按标签统计”,只需要改 SQL 的 GROUP BY 表达式,界面代码可以完全不碰。

5. 避坑与排查:五个让我返工的真实案例

这一章是血泪经验汇总。排查思路完全围绕这份源码展开,每条都按“现象 → 原因 → 解决”组织,拿到项目后照着检查能省下大量调试时间。

5.1 现象:运行报错 BadImageFormatException

双击 exe 后直接弹出“试图加载格式不正确的程序集”,或者System.Data.SQLite.dll无法加载。原因几乎都出在平台位数不匹配:SQLite 的原生扩展分 x86 和 x64 两份,而项目属性里设置的是“AnyCPU”。AnyCPU 程序集在 64 位系统上会以 64 位进程运行,如果引用的 System.Data.SQLite 是 32 位版本,加载原生库时就会报这个错。解决方式是把项目的“平台目标”固定为 x86 或 x64,不要用 AnyCPU,再确保引用的System.Data.SQLite.dll位数与目标平台一致。

5.2 现象:爬虫写入时提示 database is locked

爬虫线程抓取完数据,调用Insert写入待办表时,偶尔弹出SQLite error: database is locked。原因是 SQLite 默认只允许一个写事务占用数据库文件,UI 线程此时正在执行RefreshGrid()读操作,或者上一次写事务还没提交。解决方式有两条,任选其一都可行:给所有写操作统一走同一个SQLiteConnection实例,并开启 WAL 模式;或者在写入时加重试逻辑,遇到锁竞争就延迟 200ms 再试。WAL 模式开启语句是PRAGMA journal_mode=WAL;,在数据库连接打开后执行一次即可。

5.3 现象:番茄钟最小化后计时变慢或暂停

窗口最小化后,番茄钟明明设了 25 分钟,回来却发现只走了 10 分钟。原因是 WinForms 的Timer依赖消息循环,窗口最小化后系统会降低该进程的时钟优先级,大量消息被合并延迟处理,Tick 触发间隔被拉大。解决方式是不依赖 Tick 次数累计,而是在Start()时记录DateTime.Now,Tick 事件里通过(DateTime.Now - _startTime).TotalSeconds重算剩余时间;更彻底的做法是日志表只记录开始时间和实际结束时间,UI 显示时做差值计算。

5.4 现象:中文标题写入 SQLite 后显示为乱码

界面录入“开会讨论项目进度”,保存后重新加载变成“???”或一坨编码错误。常见原因有两个:一是数据库连接串没有加utf8相关配置,二是.db文件本身是早期版本创建的旧编码。解决方式是把连接字符串补上UTF8Encoding=True;,同时确保代码文件本身以 UTF-8 带 BOM 保存。如果已有数据已经乱码且无法恢复,重建表再导数据是最省事的后悔药。

5.5 现象:DataGridView 双击修改行后,刷新时选中行跳到第一行

在 DataGridView 中修改某条待办并点击保存,RefreshGrid()重新绑定数据源后,焦点总是回到第一行,用户不得不重新滚动查找。原因是重新绑定后当前行索引复位。解决方式是刷新前记录当前行的主键 Id,刷新后再循坏查找并设置CurrentCell:

int currentId = (int)grid.CurrentRow.Cells["Id"].Value; RefreshGrid(); foreach (DataGridViewRow row in grid.Rows) { if ((int)row.Cells["Id"].Value == currentId) { grid.CurrentCell = row.Cells["Title"]; break; } }

这条是高频体验优化,做不做直接影响用户对这个“系统”好不好用的评价。

6. 收尾技巧:古风 UI 的沉浸感营造与交付前自检清单

6.1 古风不只是换字体:色彩和控件的克制

这个项目的特色之一是古风风格界面。做古风 UI 最容易翻车的做法是把所有控件都弄成雕花边框、背景塞大图,结果窗体加载慢还显得廉价。真正出效果的是色彩系统:取宣纸底色、墨色文字和朱砂色强调色。我记得这套源码里主窗体背景用的是低饱和米色,文字用深灰而不是纯黑,按钮去掉默认的灰色边框改为FlatStyle.Flat,整体立刻安静下来。

this.BackColor = Color.FromArgb(250, 245, 235); this.ForeColor = Color.FromArgb(60, 50, 40); this.Font = new Font("楷体", 10F); foreach (Control ctrl in Controls) { if (ctrl is Button btn) { btn.FlatStyle = FlatStyle.Flat; btn.BackColor = Color.FromArgb(198, 108, 73); btn.ForeColor = Color.White; btn.FlatAppearance.BorderSize = 0; } }

这段代码适合放在Form_Load中统一应用,但要注意:如果按钮设置了FlatAppearance.BorderSize = 0,鼠标悬停时的反馈效果会减弱,需要在FlatAppearance.MouseOverBackColor里补一个高亮色,否则用户点按钮不知道有没有点到。

6.2 交付前半小时验证清单

答辩和演示前,我每次都会强制走一遍这几步:把整个项目复制到一台没有安装过 SQLite 的干净虚拟机;断网启动程序,确认所有非爬虫功能正常;用“管理员身份”运行一次,排除权限导致无法创建数据库文件的问题;修改一条待办并重新打开程序,确认数据持久化成功。这份资源包已经包含了源码、课程报告、开题报告和演示 PPT,所以拿到手最关键的是先动手跑通以上流程。从那以后我每次拆课程设计包,第一件事就是按这个清单检查数据库连接和平台位数,而不是急着看界面代码——这两处才是最容易被环境坑到的地方,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询