news 2026/10/30 5:06:55

C# WinForm 日程提醒器:Timer调度、重复日程与幂等触发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# WinForm 日程提醒器:Timer调度、重复日程与幂等触发

简介:面向C#初学者和需要Windows桌面日程管理工具的开发者,这份WinForms日程提醒器项目以日期提醒、倒计时与弹窗通知为主线,覆盖事件录入、列表查看、提醒开关及提前量设置等常见需求,适合课程设计、毕业设计参考或轻量工具原型,也便于熟悉.NET桌面开发流程。压缩包共47个文件、约317KB,以cs源码、resx/resources界面资源、exe可执行程序、config配置、dll依赖、mdf/ldf数据库文件及sln/csproj工程文件为主,包含登录、注册、事件管理、主界面等多窗体模块与数据集设计,工程结构相对完整,可直接用Visual Studio打开解决方案阅读源码与调试。目前已有395人学习,读者可从中梳理WinForms控件布局、事件数据结构、按钮事件处理、Timer周期检查、剩余时间格式化及MessageBox通知等实现思路,理解各窗体之间的协作关系。对需要完成桌面应用练手或二次开发的人而言,这份源码能提供可运行的参照和排错线索,便于在此基础上扩展搜索、过滤或更高级通知机制,积累从界面设计到定时提醒落地的完整经验。

1. 需求不是「弹个窗」:日程提醒器的边界与交付标准

给日程提醒器做需求评审时,最容易吵起来的不是界面,而是「到点到底以谁的时间为准」。C# 加 WinForm 做桌面日程提醒器,看起来只是摆几个控件、开一个 Timer、到点弹窗,真做起来却要处理倒计时精度、重复日程、休眠唤醒、托盘常驻和本地数据不丢。这个项目的定位很明确:面向单机用户,不依赖账号和网络,打开就能记日程,关机重开还在,到点有声音和弹窗提醒。它适合两类人:学生拿它做课程设计或毕设,从业者拿它做内部小工具的原型。下面按需求、架构、实现、验收的顺序拆一遍,能直接照着搭。

2. 架构与技术选型:WinForm 桌面端的三层拆分与本地存储

2.1 为什么用 WinForm,而不是控制台或 WPF

这个项目的核心不是界面框架,而是调度正确性。选 WinForm 的理由很实际:控件成熟、事件模型简单、部署链路短,双击 exe 就能跑,不需要额外运行时配置;对课程设计和内部小工具来说,交付成本最低。WPF 的数据绑定和样式更强,但 XAML 学习曲线、发布体积、排查路径都更长,用在「日程提醒」这种以列表和对话框为主的工具上,收益不明显。控制台更不用考虑,托盘常驻、气泡提醒、非模态弹窗都不好做。

反直觉的地方在于:WinForm 的 Timer 精度不算高,默认消息循环下大约十几毫秒级,界面卡顿时还会延迟。但日程提醒的验收标准通常是「秒级到达」,不是「毫秒级」,所以 WinForm Timer 够用。真正需要设计的是「错过之后怎么补」和「重复触发怎么防」,这两件事跟 UI 框架无关。选型时我一般会问三个问题:用户会不会关掉主窗口?电脑会不会休眠?日程会不会重复?三个答案都是「会」,那就不能只写一个Timer_Tick里弹 MessageBox 就完事。

另一个边界要提前写清楚:这个工具按本地时间工作,不处理跨时区日程。单机日程的语义是「明天上午九点开会」,用户理解的是本地墙上时间;如果存 UTC,每天重复的日程在时区变化后会漂移。通常的做法是数据库里存本地时间的 ISO 格式字符串,显示和比较都用DateTime,并在说明里明确「跨时区请手动调整」。这样虽然不完美,但边界清晰,验收时不会扯皮。

2.2 三层结构:UI 层、调度层、持久层的职责边界

这套方案拆三层,不追求架构模式的名字,只追求「改一处不影响另一处」。UI 层负责主列表、编辑对话框、托盘图标和弹窗;调度层负责算下一次触发时间、判断是否到点、写触发日志;持久层负责 SQLite 的增删改查和事务。三条规则必须守住:UI 不直接拼 SQL;调度层不碰任何控件;持久层不处理「提前几分钟提醒」这类业务规则。

消息流是这样的:用户在编辑对话框保存日程,UI 调用持久层写入,然后通知调度层重新加载;调度层每秒扫描一次,算出下一次触发时间,到点后先尝试写TriggerLog,写入成功才回调 UI 弹窗;UI 弹窗完成后,托盘图标和主列表状态刷新。这样即使弹窗失败,日志里也能查到「本该提醒但没弹出来」的记录,排查时不是黑匣子。

层主要类职责不该做的事
UIMainForm、EditScheduleForm、TrayService展示列表、收集输入、弹提醒、托盘菜单不写 SQL,不计算重复规则
调度ReminderScheduler、ReminderCalculator单 Timer 扫描、算下次触发、幂等控制不直接操作DataGridView
持久ScheduleRepository、TriggerLogRepository建表、参数化查询、事务不判断「要不要提醒」

2.3 数据表与配置:三张表撑起日程、日志和设置

SQLite 足够,单文件、零配置、方便打包。表结构不用多,三张表:Schedule存日程,TriggerLog存触发记录,AppSetting存托盘、声音、开机自启这些开关。DueTime用yyyy-MM-dd HH:mm:ss文本存,SQLite 没有原生日期类型,排序和比较都按这个固定格式来,不会出现区域设置导致的解析歧义。

-- 日程主表 CREATE TABLE IF NOT EXISTS Schedule ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Title TEXT NOT NULL, Content TEXT, DueTime TEXT NOT NULL, -- 本地时间,格式 yyyy-MM-dd HH:mm:ss AdvanceMinute INTEGER NOT NULL DEFAULT 0, -- 提前提醒分钟,0 表示准点 RepeatType INTEGER NOT NULL DEFAULT 0, -- 0 不重复 1 每天 2 每周 3 每月 4 工作日 Enabled INTEGER NOT NULL DEFAULT 1, -- 1 启用 0 暂停 CreatedAt TEXT NOT NULL ); -- 触发日志,唯一索引用来防重复提醒 CREATE TABLE IF NOT EXISTS TriggerLog ( Id INTEGER PRIMARY KEY AUTOINCREMENT, ScheduleId INTEGER NOT NULL, FireTime TEXT NOT NULL, -- 本次应触发的时间点 FiredAt TEXT NOT NULL, -- 实际触发时间 Result INTEGER NOT NULL -- 0 已提醒 1 已忽略 2 错过 ); CREATE UNIQUE INDEX IF NOT EXISTS UX_TriggerLog_Key ON TriggerLog(ScheduleId, FireTime); CREATE INDEX IF NOT EXISTS IX_Schedule_Enabled_DueTime ON Schedule(Enabled, DueTime);

AdvanceMinute限制在 0 到 1440 之间,超过一天的提前提醒没有意义,编辑框里直接用NumericUpDown卡住范围。RepeatType用整数枚举,别存中文,后面加规则时不用改表。TriggerLog的唯一索引是防重复的关键:同一个日程的同一个触发时刻只能插入一次,插入成功才弹窗,从数据库层面兜住「重启后重复弹」的问题。

3. 倒计时与提醒引擎:从时间模型到触发链路

3.1 时间模型:本地时间、基准时间和短月处理

日程提醒最容易翻车的地方是「重复日程越算越偏」。比如每天九点的日程,如果每次触发后都把DueTime改成DateTime.Now.AddDays(1),几次之后就会变成九点零几分甚至十点。正确做法是保留一个「基准时间」,每次从基准往后推,而不是从当前时间往后推。计算下一次触发时,从基准时间开始循环推进,直到大于当前时间为止。

短月要单独处理。每月 31 号的日程,到 2 月没有 31 号,通常有两种策略:取当月最后一天,或者跳过当月。我一般选「取当月最后一天」,并在编辑对话框里写一句提示,避免用户以为提醒丢了。代码里用DateTime.DaysInMonth判断,AddMonths之后如果日期变了,就回退到当月最后一天。

public static class ReminderCalculator { // 从基准时间推进,避免每次基于 now 累加造成漂移 public static DateTime? GetNextFireTime(ScheduleItem item, DateTime now) { var fire = item.DueTime; if (item.RepeatType == RepeatType.None) return fire > now ? fire : null; // 最多推进 1000 次,防止脏数据导致死循环 for (var i = 0; i < 1000 && fire <= now; i++) { fire = NextOccurrence(fire, item.RepeatType); } return fire > now ? fire : null; } private static DateTime NextOccurrence(DateTime current, RepeatType type) { return type switch { RepeatType.Daily => current.AddDays(1), RepeatType.Weekly => current.AddDays(7), RepeatType.Monthly => AddMonthsSafe(current, 1), RepeatType.Workday => NextWorkday(current), _ => current }; } private static DateTime AddMonthsSafe(DateTime current, int months) { var target = current.AddMonths(months); // 1月31日加一个月会落到2月28/29日,这里保留“每月最后一天”的语义 if (target.Day != current.Day) { target = new DateTime( target.Year, target.Month, DateTime.DaysInMonth(target.Year, target.Month), current.Hour, current.Minute, 0); } return target; } private static DateTime NextWorkday(DateTime current) { var next = current.AddDays(1); while (next.DayOfWeek == DayOfWeek.Saturday || next.DayOfWeek == DayOfWeek.Sunday) next = next.AddDays(1); return next; } }

这里的GetNextFireTime只负责「下一次什么时候触发」,不负责「要不要弹」。now传当前时间,RepeatType.None的日程如果已经过期就返回null,表示不再触发。循环上限 1000 次是防御性写法,正常数据不会走到。AddMonthsSafe的target.Day != current.Day判断的是「加月后日期被压缩」的情况,比如 1 月 31 日加一个月变成 2 月 28 日,这时把日期修正到当月最后一天,时间部分保持原样。

3.2 Timer 选型:单计时器扫描,不搞一日程一 Timer

新手常犯的错是给每个日程建一个System.Timers.Timer,到点触发。日程少的时候看不出问题,日程一多,线程池被占满,关窗后 Timer 没释放,进程还挂在任务管理器里。更麻烦的是,每个 Timer 各自算时间,系统时间一跳变,触发顺序全乱。这套方案只用System.Windows.Forms.Timer,间隔 1000 毫秒,每次 Tick 重新扫描启用的日程,算出下一次触发时间,到点就走触发链路。

WinForms Timer 跑在 UI 线程,回调里可以直接更新控件,不用Invoke,这是它最大的好处。缺点是界面忙的时候会延迟,所以 Tick 里不能做耗时操作,数据库查询要快,弹窗要异步或者排队。为了防止上一次扫描没跑完又进来,加一个_busy标志,进回调先判断,finally里复位。这个细节很小,但少了它,数据库慢的时候会出现重入,同一个提醒弹两次。

时间跳变也要处理。用户改系统时间、笔记本合盖休眠、程序被挂起,都会让 Timer 的 Tick 不连续。做法是记录上一次 Tick 的DateTime.Now,每次进来比较:如果当前时间比上次还早,说明时间被回拨;如果差距超过 5 秒,说明发生了休眠或卡顿。这两种情况都触发一次全量重扫,并且对已经过点的日程走「补提醒」逻辑,宽限窗口设 5 分钟,超过 5 分钟的标记为「错过」,不再弹窗,只在日志里留痕。

public sealed class ReminderScheduler : IDisposable { private readonly System.Windows.Forms.Timer _timer; private readonly Func<List<ScheduleItem>> _loadItems; private readonly Action<ScheduleItem, DateTime> _onFire; private DateTime _lastTick; private bool _busy; public ReminderScheduler(Func<List<ScheduleItem>> loadItems, Action<ScheduleItem, DateTime> onFire) { _loadItems = loadItems; _onFire = onFire; _lastTick = DateTime.Now; _timer = new System.Windows.Forms.Timer { Interval = 1000 }; _timer.Tick += OnTick; } public void Start() => _timer.Start(); private void OnTick(object? sender, EventArgs e) { if (_busy) return; // 防止数据库慢时重入 _busy = true; try { var now = DateTime.Now; var jumped = now < _lastTick || (now - _lastTick).TotalSeconds > 5; _lastTick = now; foreach (var item in _loadItems().Where(x => x.Enabled)) { var next = ReminderCalculator.GetNextFireTime(item, now); if (next == null) continue; var fireTime = next.Value.AddMinutes(-item.AdvanceMinute); if (fireTime > now) continue; var late = now - fireTime; if (late > TimeSpan.FromMinutes(5) && !jumped) continue; // 唯一索引保证同一日程同一触发时刻只成功一次 if (TriggerLogRepository.TryAcquire(item.Id, fireTime)) { _onFire(item, fireTime); } else if (late <= TimeSpan.FromMinutes(5)) { // 已提醒过,忽略即可 } } } finally { _busy = false; } } public void Dispose() { _timer.Tick -= OnTick; _timer.Dispose(); } }

Interval = 1000是倒计时显示和触发精度的折中,1 秒扫描一次对单机工具足够。_lastTick用来判断时间跳变,jumped为真时放宽补提醒窗口。fireTime是「到期时间减去提前分钟」,这样提前提醒和准点提醒走同一套逻辑。TryAcquire内部是一个事务:先查唯一索引有没有冲突,没有就插入,返回true;有冲突返回false。这一步把「弹不弹」的决定权交给数据库,比在内存里记LastTriggered可靠。

3.3 触发链路:弹窗队列、托盘气泡和声音

到点之后不要直接MessageBox.Show。多个日程同一分钟触发时,MessageBox 会一个叠一个,用户点完第一个,后面的已经过期。通常的做法是维护一个待提醒队列,UI 每次只展示一个非模态提醒窗口,窗口里显示标题、时间、备注,提供「知道了」和「稍后 5 分钟」两个按钮。点「稍后」时把这条记录放进一个内存中的延迟队列,5 分钟后重新入队,不修改原始日程。

托盘气泡用NotifyIcon.ShowBalloonTip,适合不打断用户的轻提醒;声音用System.Media.SystemSounds.Exclamation.Play(),不要自己塞一大段 wav,部署麻烦还容易吓人。弹窗和气泡二选一还是同时开,放到设置里让用户选。提醒窗口不要设TopMost永久置顶,否则用户切不走,体验很差;短暂置顶几秒再取消,或者只激活一次。

4. 界面与交互实现:主列表、编辑对话框和托盘常驻

4.1 主界面:DataGridView 绑定与倒计时列刷新

主界面用ToolStrip放新增、编辑、删除、暂停、搜索,中间DataGridView显示日程,底部StatusStrip显示「下一条提醒还有多久」。列包括标题、到期时间、提前分钟、重复规则、状态、倒计时。数据绑定用BindingList<ScheduleItem>,增删改后自动通知网格,不用手动Rows.Add。

倒计时列每秒刷新,但不能每秒重新绑定数据源,否则网格会闪烁、选中行会丢、滚动位置会跳。正确做法是只更新倒计时单元格的值,而且只更新可见行。DataGridViewRow.Displayed判断当前是否在视口内,减少无谓计算。倒计时格式按剩余时间分档:大于 1 天显示「x 天 x 小时」,1 小时内显示「x 分 x 秒」,已过点显示「已提醒」或「已错过」。

private void RefreshCountdownCells() { var now = DateTime.Now; foreach (DataGridViewRow row in _grid.Rows) { if (!row.Displayed) continue; // 只刷新可见行,避免整表重绘 if (row.DataBoundItem is not ScheduleItem item) continue; var next = ReminderCalculator.GetNextFireTime(item, now); var cell = row.Cells["colCountdown"]; if (next == null) { cell.Value = item.Enabled ? "已结束" : "已暂停"; continue; } var left = next.Value.AddMinutes(-item.AdvanceMinute) - now; cell.Value = left.TotalDays >= 1 ? $"{(int)left.TotalDays} 天 {left.Hours} 小时" : $"{(int)left.TotalMinutes} 分 {left.Seconds} 秒"; } }

这段代码挂在同一个 1 秒 Timer 的 Tick 里,排在触发扫描之后。row.Displayed是性能关键,日程多的时候只算屏幕上看得见的十几行。colCountdown用列名索引,不要用数字下标,后面加列不会错位。倒计时只是展示,真正的触发判断仍在调度器里,两边不要混在一起。

搜索框用BindingSource.Filter做标题和备注的模糊匹配,输入变化时刷新,不要每次敲键盘都查数据库。删除加二次确认,暂停用右键菜单切换Enabled,状态列用文字而不是图标,避免高 DPI 下图标模糊。

4.2 编辑对话框:输入校验和重复规则联动

编辑对话框不用花哨,DateTimePicker选日期时间,NumericUpDown选提前分钟,ComboBox选重复规则,TextBox填标题和备注。标题必填,去空格后不能为空;时间允许选过去,但保存时提示「该时间已过,是否仍要保存」,因为用户可能想补录一条已完成的日程。提前分钟限制在 0 到 1440,重复规则切换时更新提示文字,比如选「每月」时提示「短月按当月最后一天提醒」。

校验用ErrorProvider,不要用 MessageBox 一个个弹。保存按钮先调ValidateChildren(),再检查业务规则,全部通过才写库。写库成功后关闭对话框并返回DialogResult.OK,主界面收到后重新加载列表并通知调度器。编辑已有日程时,如果修改了DueTime,要清掉对应的TriggerLog吗?通常不清,历史日志保留,但新时间点的幂等键是新的,不会冲突。如果用户把时间改到过去,调度器下次扫描会按「已过期」处理,不再触发。

private bool ValidateInput() { if (string.IsNullOrWhiteSpace(txtTitle.Text)) { errorProvider.SetError(txtTitle, "标题不能为空"); return false; } errorProvider.SetError(txtTitle, string.Empty); if (numAdvance.Value < 0 || numAdvance.Value > 1440) { errorProvider.SetError(numAdvance, "提前分钟需在 0 到 1440 之间"); return false; } errorProvider.SetError(numAdvance, string.Empty); if (dtpDue.Value < DateTime.Now && MessageBox.Show("这个时间已经过了,仍然保存吗?", "确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No) { return false; } return true; }

ValidateChildren()会触发控件的Validating事件,和ErrorProvider配合使用。业务校验放在后面,顺序是先格式后语义。numAdvance是NumericUpDown,本身有Minimum和Maximum,代码里再判一次是防御性写法,防止设计器里被改掉。

4.3 托盘常驻与开机自启:关闭窗口不等于退出进程

日程提醒器要常驻,用户点关闭按钮时通常希望最小化到托盘,而不是退出。做法是在FormClosing里判断CloseReason.UserClosing,取消关闭、隐藏窗口、显示托盘图标。真正退出走托盘右键菜单的「退出」,这时才释放调度器、隐藏并销毁NotifyIcon、调用Application.Exit。托盘图标双击恢复主窗口,右键菜单至少三项:打开主界面、暂停全部提醒、退出。

开机自启用当前用户的注册表Run键,不写机器级,不需要管理员权限。路径带空格时必须加引号,否则 Windows 会把C:\Program Files\...截断,开机报错。这个坑很常见,血泪经验是发布到带空格的目录后自启失效,排查半天以为是权限问题。

using Microsoft.Win32; public static class AutoStartHelper { private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; private const string AppName = "ScheduleReminder"; public static void SetEnabled(bool enabled) { using var key = Registry.CurrentUser.OpenSubKey(RunKey, writable: true); if (key == null) return; if (enabled) { var exe = Application.ExecutablePath; // 路径带空格必须加引号,否则开机启动会被截断 key.SetValue(AppName, $"\"{exe}\""); } else { key.DeleteValue(AppName, throwOnMissingValue: false); } } }

Registry.CurrentUser只影响当前登录用户,换用户不会互相干扰。DeleteValue的第二个参数设为false,值不存在时不抛异常,避免卸载或关闭自启时报错。托盘退出时要把NotifyIcon.Visible设为false再Dispose,否则图标可能残留在托盘区,鼠标划过才消失。

5. 常见问题与排查:时间跳变、重复提醒和托盘失联

5.1 休眠唤醒后提醒漏掉:时间跳变没有兜底

现象:笔记本合盖一晚上,第二天打开,昨晚的日程没有补提醒,日志里也没有记录。原因:WinForms Timer 在休眠期间不触发,醒来后只比较当前这一秒,已经过点的日程被当成过期数据跳过。解决:在 Tick 里记录_lastTick,当前时间与上次差距超过 5 秒就判定为跳变,触发一次全量重扫;对 5 分钟内过点的日程补提醒,超过 5 分钟的写入TriggerLog并标记为「错过」。补提醒窗口不要弹太多,队列逐个展示,超过 3 条合并成一条汇总提示。

5.2 同一日程重复弹窗:幂等键没落到数据库

现象:点「稍后 5 分钟」后,或者重启程序,同一个日程又弹一次;日程多了以后,同一分钟能弹好几个窗口。原因:只在内存里记LastTriggered,进程重启就丢;或者先弹窗再写日志,弹窗失败日志没写,下次又弹。解决:把幂等键做成TriggerLog(ScheduleId, FireTime)的唯一索引,触发前先TryAcquire,插入成功才弹;插入失败直接跳过。弹窗队列用Queue<ReminderItem>,UI 一次只展示一个,关闭后再取下一个,避免弹窗风暴。

5.3 托盘退出后进程还在:资源释放不彻底

现象:点了托盘「退出」,任务管理器里进程还在,或者托盘图标消失但程序仍占着数据库文件。原因:NotifyIcon没有Dispose,Timer 没有停,事件没有解绑,后台还有线程在跑。解决:退出流程统一到一个方法里,先_scheduler.Dispose(),再_notifyIcon.Visible = false; _notifyIcon.Dispose();,最后Application.Exit()。FormClosing里区分「用户点关闭」和「程序退出」,前者最小化,后者走完整释放。数据库连接用using包住,别长期持有。

5.4 发布后新增日程报错:数据库路径不可写

现象:开发环境正常,安装到Program Files下后新增日程失败,提示只读或拒绝访问。原因:数据库文件放在 exe 同目录,普通用户对该目录没有写权限。解决:把数据库放到%LocalAppData%下的应用目录,首次启动时如果文件不存在就创建并执行建表脚本;需要种子数据就从安装目录复制一份。日志文件同样放LocalAppData,不要写安装目录。发布前用普通用户账号跑一遍新增、编辑、删除,能提前发现这类问题。

5.5 高 DPI 下界面错位:字体和列宽没有自适应

现象:在 125% 或 150% 缩放的显示器上,按钮文字被截断,DataGridView列宽不够,提醒窗口跑到屏幕外。原因:WinForm 默认的自动缩放模式在混合 DPI 下表现不稳定,控件坐标写死。解决:在Program.cs里设置Application.SetHighDpiMode(HighDpiMode.SystemAware),窗体用AutoScaleMode.Font,布局用TableLayoutPanel和Dock,少用绝对坐标;提醒窗口显示前用Screen.FromPoint判断工作区,把位置限制在屏幕内。多显示器切换后重新计算位置,别只算一次。

6. 验收与答辩:用可复现的用例证明提醒不丢

验收不要只看「能弹窗」,要拿用例说话。下面这张表可以直接放进课程设计文档,逐条跑一遍,记录实际结果和耗时。正常运行的提醒误差控制在 2 秒以内;休眠唤醒后 30 秒内补提醒;连续运行 24 小时内存增长不超过 20MB;重启后数据不丢、不重复弹。

编号用例操作预期结果
1一次性准点提醒新增 1 分钟后的日程,不提前到点 2 秒内弹窗,日志 Result=0
2提前提醒新增 10 分钟后日程,提前 5 分钟第 5 分钟弹窗,倒计时列归零
3每天重复新增每天重复日程,改系统时间到次日次日同一时刻再次提醒,不漂移
4休眠补提醒日程到点前合盖,过点 2 分钟后唤醒30 秒内补弹窗,日志可查
5重复触发同一日程点「稍后 5 分钟」两次只保留一条延迟队列,不叠弹
6重启恢复提醒后关闭程序再打开同一触发时刻不重复弹
7托盘退出托盘菜单退出进程结束,图标消失,数据库可删
8只读目录安装到无写权限目录启动时自动落到 LocalAppData,不报错

答辩演示按「数据 → 触发 → 异常 → 恢复」的顺序走:先打开数据库文件展示表结构和一条日程,再新建一条 1 分钟后的提醒,让评委看到倒计时和弹窗;然后改系统时间或合盖演示补提醒;最后展示TriggerLog里的记录,说明重复提醒为什么不会发生。整个过程控制在 5 分钟,重点讲「怎么证明它可靠」,而不是逐个控件介绍。

从那以后我每次做桌面提醒类项目,都先把「时间跳变」和「重复触发」两条用例写进验收清单,再动界面;界面可以后补,调度错了整个方案就立不住。这套日程提醒器的完整工程、建表脚本和验收用例表我整理在同一份资源里,目录按 UI、Scheduler、Data、Docs 分开,打开解决方案就能编译,要交课程设计的话把用例表换成模板字段即可。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 5:06:52

Python算法交易全链路:回测撮合、实盘执行与滚动验证

简介&#xff1a;面向希望用 Python 搭建全自动交易系统的量化爱好者与进阶开发者&#xff0c;这份教程型代码包围绕行情数据获取、Pandas 数据处理、技术指标计算、策略回测、交易执行、风险与资金管理、实时运行及日志监控等环节展开&#xff0c;帮助读者理解从策略想法到自动…

作者头像 李华
网站建设 2026/10/30 5:05:58

UWB 测角实战:标称 ±90° 的 PDOA,实测为什么只剩 ±65°?

最近在调一套 UWB 测角&#xff0c;遇到件很别扭的事&#xff1a;设备规格写着 90&#xff0c;可实际把标签移到 70 附近&#xff0c;读数就开始飘&#xff1b;再往外挪&#xff0c;干脆给你一串整整齐齐的 90——不报错、不打标&#xff0c;看起来完全正常。后来把阵列的天线间…

作者头像 李华
网站建设 2026/10/30 5:05:23

TikTok AI 带货视频人物总在变?保持出镜形象一致的实用方法

TikTok AI 带货视频人物总在变&#xff1f;保持出镜形象一致的实用方法 使用 AI 批量制作 TikTok 商品视频时&#xff0c;常见问题不仅是商品变形&#xff0c;出镜人物也可能在不同镜头或不同视频中发生变化&#xff1a; 脸型和五官不一致发型、服装或妆容改变年龄感出现波动人…

作者头像 李华
网站建设 2026/10/30 5:03:08

SECS/GEM协议深度解析:半导体设备通信的可靠性设计原理

简介&#xff1a;本资源是一份面向制造业自动化工程师、设备集成开发人员及工业通信协议学习者的SECS/GEM协议中文详解文档&#xff0c;聚焦半导体及泛制造领域设备联网集成中的核心通信标准问题。当前行业相关资料零散且深度不足&#xff0c;该文档系统梳理协议原理与工程实践…

作者头像 李华
网站建设 2026/10/30 5:02:14

消息观察与条件子系统观察

1. 概念讲解1.1 为什么要观察消息Simulink 用消息&#xff08;Messages&#xff09;在模型组件之间通信。当模型中存在一条或多条消息信号时&#xff0c;普通的信号观察只拿到载荷数据&#xff0c;却看不到消息在仿真中"何时到达、何时离开、是否被丢弃"这一类事件语…

作者头像 李华
网站建设 2026/10/30 5:01:35

一部短剧做多版解说,批量生产服务商怎么选?

一部短剧要持续制作推广素材&#xff0c;CutGist适合希望获得不同剧情入口、人物视角和叙事长度完整解说成片的团队&#xff1b;OpusClip与Vizard更适合先从原剧中生成候选短片&#xff0c;再由内部人员继续改稿和剪辑。选择短剧解说批量生产服务商或工具时&#xff0c;重点不是…

作者头像 李华