最后更新:2026-05-07
- 动态生成带今日日期数字的托盘图标
- 左键点击弹出/收起日历面板
- 右键菜单(设置、退出)
- 面板贴近任务栏右下角定位
- 监听
ShellExperienceHost进程的窗口显示事件(SetWinEventHook) - 检测到右下角系统日历/通知中心弹出时,立即隐藏并替换为 WinCal 面板
- 防抖机制(500ms 冷却时间),避免重复触发
- 多显示器支持(
MonitorFromWindow获取窗口所在显示器工作区) - 退出时自动恢复被隐藏的系统日历窗口
- 弹出窗口焦点丢失检测(定时器轮询 + 前台窗口变化判断)
- 月历网格显示(6行7列)
- 月份切换(上一月/下一月)
- 今日蓝色圆形高亮
- 非当月日期灰显
- 农历显示(每格下方)
- 自定义周起始日(当前默认周日开始)
- 有事件的日期底部彩色圆点标记
- 圆点颜色与事件来源日历颜色一致
- 近期事件列表(面板下方,分隔线隔开)
- 事件列表固定高度,超出滚动
- 无事件时列表区域自动隐藏
- 事件项左侧颜色条标识
- 事件项显示标题 + 时间摘要
- 鼠标悬停事件项时,面板左侧弹出详情浮层
- 详情显示:标题、时间、地点、日历来源、描述
- 详情窗口底部与主面板底部对齐
- 鼠标移开自动关闭详情
- 圆角窗口(12px),圆角外完全透明
- ClipToBounds 防止内容溢出圆角
- 失焦自动关闭
- 事件项 hover 圆角背景高亮(不遮挡面板圆角)
- 浅色主题资源字典
- 深色主题资源字典
- 自动跟随系统主题(监听注册表
AppsUseLightTheme)
- 设置窗口 UI(独立顶层窗口)
- 界面颜色主题(深色 / 浅色 / 跟随系统)
- 界面字体大小(5 档切换)
- 开机自启动开关
- 日历数据源配置(系统邮箱 / 远程 .ics URL)
- 周起始日(周日 / 周一)
- 近期事件显示天数(1/3/7 天)
- 农历显示开关
- .ics 刷新频率(15分钟/30分钟/1小时)
- 主界面顶部时间日期显示开关
- 时间显示格式(12小时/24小时/跟随系统)
- 设置持久化(JSON 文件
%LOCALAPPDATA%/WinCal/settings.json)
- 日历服务接口(ICalendarService)
- 模拟数据服务(MockCalendarService,用于调试)
- .ics 远程日历服务(IcsCalendarService,使用 Ical.Net 解析)
- 聚合日历服务(AggregateCalendarService,多数据源合并)
- 空日历服务(EmptyCalendarService,无数据源时的默认服务)
- Windows 日历 API 接入(WindowsCalendarService,需 Windows SDK 暂未启用)
- 窗口定位(WindowPositionHelper)
- 开机自启注册表操作(StartupHelper)
- 托盘图标动态生成(TrayIconGenerator)
- 农历计算(LunarCalendarHelper)
- 系统主题监听(ThemeHelper)
- 系统日历拦截器(SystemCalendarInterceptor)
- publish.bat 一键发布脚本
- build.bat 开发构建脚本
- Self-contained + SingleFile 发布配置
- 移除 PublishTrimmed(.NET 8 SDK 对 WPF 不支持 Trim,且会破坏 SetWinEventHook 回调)
| 文件 | 说明 |
|---|---|
WinCal.csproj |
项目配置 |
App.xaml |
应用入口,托盘资源,主题引用 |
App.xaml.cs |
托盘初始化、弹窗切换、拦截器集成 |
Core/Models/CalendarEvent.cs |
事件数据模型 |
Core/Models/CalendarDay.cs |
日期数据模型 |
Core/Models/CalendarAccountInfo.cs |
日历账户信息模型 |
Core/Services/ICalendarService.cs |
日历服务接口 |
Core/Services/WindowsCalendarService.cs |
Windows 日历 API(需 WinRT 暂未启用) |
Core/Services/MockCalendarService.cs |
模拟数据服务 |
Core/Services/IcsCalendarService.cs |
.ics 远程日历服务 |
Core/Services/AggregateCalendarService.cs |
多数据源聚合服务 |
Core/Services/EmptyCalendarService.cs |
空日历服务 |
Core/Services/AppSettings.cs |
设置持久化(JSON) |
Core/Helpers/WindowPositionHelper.cs |
窗口定位 |
Core/Helpers/StartupHelper.cs |
开机自启 |
Core/Helpers/TrayIconGenerator.cs |
托盘图标生成 |
Core/Helpers/LunarCalendarHelper.cs |
农历计算 |
Core/Helpers/ThemeHelper.cs |
系统主题监听与切换 |
Core/Helpers/SystemCalendarInterceptor.cs |
系统日历拦截器(⭐ 新增) |
ViewModels/CalendarViewModel.cs |
日历 ViewModel |
ViewModels/EventListViewModel.cs |
事件列表 ViewModel |
Views/PopupWindow.xaml / .cs |
弹出日历主窗口 |
Views/SettingsWindow.xaml / .cs |
设置窗口 |
Views/EventDetailWindow.xaml / .cs |
事件详情浮层窗口 |
Views/Controls/MonthCalendar.xaml / .cs |
月历控件 |
Views/Controls/DayCell.xaml / .cs |
日期格子控件 |
Views/Controls/EventItem.xaml / .cs |
事件项控件 |
Views/Controls/EventDetailPopup.xaml / .cs |
事件详情弹出控件(已弃用,改用 EventDetailWindow) |
Views/Themes/Light.xaml |
浅色主题 |
Views/Themes/Dark.xaml |
深色主题 |
publish.bat |
发布脚本 |
build.bat |
构建脚本 |
开发调试构建(不暂停):
build_nopause.bat
:: 实际执行: "C:\Program Files\dotnet\dotnet.exe" build -c Debug开发调试构建(暂停查看输出):
build.bat
:: 实际执行: "C:\Program Files\dotnet\dotnet.exe" build发布 Release 单文件:
publish.bat
:: 实际执行: dotnet publish WinCal.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true -o ./dist
:: 注意:已移除 PublishTrimmed(.NET 8 对 WPF 不支持 Trim,且会破坏 SetWinEventHook 回调)
:: 产物: ./dist/WinCal.exe(单文件,无需 .NET 运行时)
⚠️ dotnet 必须使用完整路径"C:\Program Files\dotnet\dotnet.exe",部分终端环境变量中找不到dotnet。
核心入口:CalendarViewModel.SelectDate(DateTime date)
流程:
- 用户点击日历格子中的某一天 →
DayCell触发SelectDate CalendarViewModel.SelectDate()更新SelectedDate,计算SelectedDayIndex(选中格子在网格中的位置)- 调用
UpdateUpcomingEventsFromDateAsync(date)刷新事件列表:- 以选中日期为起点,向后查
UpcomingDays天(默认 3 天,可在设置中改为 1/3/7) - 调用
_calendarService.GetEventsAsync(fromDate, endDate)获取事件 - 按开始时间排序,最多显示 100 条
- 结果写入
UpcomingEventsObservableCollection → UI 自动刷新
- 以选中日期为起点,向后查
- 月历导航(上一月/下一月)不改变
SelectedDate,所以事件列表保持不变
关键点:
- 事件查询范围是
[选中日期, 选中日期 + UpcomingDays),不受当前显示月份限制 UpcomingDays从AppSettings加载,默认 3- 面板首次弹出时调用
RefreshDataAsync()→GenerateUpcomingEvents(),以SelectedDate(默认今天)为基准 - 月份切换时
RefreshDataAsync()也会重新调用GenerateUpcomingEvents(),以当前SelectedDate为基准
问题:农历节假日(如"端午节")在同一天显示两条事件——"端午节"和"端午节(休)",导致事件列表重复。
根因:ICS 文件中同一个节日用两个 VEVENT 定义:
DTSTART;VALUE=DATE:20250531→ 端午节(单天)DTSTART;VALUE=DATE:20250531 / DTEND;VALUE=DATE:20250602→ 端午节(休,3天假期)
两者标题前缀相同("端午节"),GetOccurrences 展开后在同一天产生两个 Occurrence。
修复方案:在 IcsCalendarService.GetEventsAsync() 中添加去重逻辑:
- 同日同标题前缀的事件(取第一个
(或(之前的部分),只保留 跨度最长(EndTime 最晚)的版本 - 效果:用户只看到"端午节(休)(5月31日-6月2日)",不再重复
问题:无 DTEND 的全天事件(如单天"端午节"),EndTime 等于 StartTime,EventItem 中 AddDays(-1) 导致 endDate < startDate,显示异常。
根因:EventItem.xaml.cs 中对全天事件统一做 endDate.AddDays(-1) 调整(假设 End 是次日),但对单天事件(Start == End)会倒挂。
修复方案:在 EventItem.xaml.cs 中添加保护:
if (endDate < startDate) endDate = startDate;问题:某些 ICS 节假日(如端午、中秋等单日节日)在日历格子中不显示事件圆点,但事件列表中能正常显示。
根因:
- 部分 ICS 事件的
DTEND == DTSTART(如DTSTART;VALUE=DATE:20250531, DTEND;VALUE=DATE:20250531),导致解析后EndTime == StartTime CalendarViewModel.GenerateCalendarGrid()中全天事件匹配条件为date >= Start && date < End,当End.Date == Start.Date时,条件date < End.Date永远为 false,事件无法匹配到任何日期
修复方案(三层防护):
① IcsCalendarService.ConvertOccurrence():当全天事件 EndTime <= StartTime 时,自动修正为 EndTime = StartTime + 1天:
if (isAllDay && endTime.Date <= startTime.Date)
endTime = startTime.AddDays(1);② CalendarViewModel.GenerateCalendarGrid():匹配逻辑增加防御性判断:
date.Date < (e.EndTime.Date > e.StartTime.Date ? e.EndTime.Date : e.StartTime.Date.AddDays(1))③ 即使 IcsCalendarService 已修正 EndTime,ViewModel 层仍做兜底,确保其他数据源(如 MockCalendarService)也不会出现此问题。
问题:原设计每天只显示1个事件圆点,当同一天有多个不同来源的日历事件时无法区分。
修复方案:
DayCell.xaml:将单个Ellipse(EventDot)替换为StackPanel(EventDots),内含Dot1/Dot2/Dot3三个圆点DayCell.xaml.cs:按事件颜色去重,最多显示3种不同颜色的圆点,每种颜色对应一个日历来源- 圆点尺寸 4×4,间距 2px,居中排列在日期格子底部
原因:Win11 任务栏用 XAML 重写,时钟窗口类名不同于 Win10,WindowFromPoint 定位困难。全局鼠标钩子 WH_MOUSE_LL 有性能风险(回调卡顿会导致鼠标卡顿),投入产出比不高。
问题:面板弹出时,近期事件列表需要等待 ICS 文件网络下载 + 解析后才显示,导致事件列表区域出现空白等待。
实现方案:磁盘缓存(IcsCalendarService)
加载优先级:内存缓存(毫秒级)→ 磁盘缓存(毫秒级)→ 网络下载(秒级)
核心改动:
EnsureCacheAsync()拆分为三层加载策略:- 内存缓存有效 → 直接返回
- 内存为空 → 从磁盘加载(
%LOCALAPPDATA%/WinCal/cache/{url_hash}.ics) - 已有数据但过期 → 后台异步刷新(不阻塞 UI)
- 无任何缓存 → 同步网络下载(仅首次启动)
- 新增
LoadFromDiskCacheAsync():从本地文件读取上次缓存的 ICS 内容 - 新增
RefreshFromNetworkAsync():网络下载 + 同时写入磁盘缓存 ForceRefreshAsync():跳过所有缓存直接网络刷新
效果:应用启动后(非首次),面板弹出时事件列表秒级显示,后台按刷新间隔(默认 30 分钟)静默更新。
系统日历拦截器开发与调试
- 新增
SystemCalendarInterceptor:使用SetWinEventHook监听ShellExperienceHost进程窗口事件 - 修复 static 构造函数
IndexOutOfRangeException:EventTypeNames字典初始化位置问题 - 修复 EventTypeNames 查找键不匹配:事件类型十六进制值与字典 key 不一致
- 修复多显示器定位:使用
MonitorFromWindow替代SystemParameters.WorkArea,正确获取窗口所在显示器 - 修复重复触发问题:添加 500ms 冷却时间防抖
- 修复
App.xaml.cs调用逻辑:拦截器使用ShowPopup而非TogglePopup,避免意外关闭已显示的面板 - 修复窗口可见性检查阻塞 UNCLOAK 事件:移除
IsWindowVisible预检查 - 修复弹出面板焦点丢失检测:采用定时器轮询策略,记录弹出时的前台窗口句柄,仅在前台变为新窗口时关闭面板(解决了通过拦截器弹出的窗口无法获得前台焦点导致
OnDeactivated不触发的问题) - 修复退出时系统日历恢复:确保
Dispose时恢复所有被隐藏的系统窗口 - 修复 dist 发布版本面板不显示:移除
PublishTrimmed,.NET 8 SDK 对 WPF 不支持 Trim,且 Trim 会破坏SetWinEventHook回调委托 - 修复 ICS 农历节假日重复显示:
IcsCalendarService添加去重逻辑——同日同标题前缀只保留跨度最长的版本 - 修复 EventItem 全天事件日期倒挂:
EventItem.xaml.cs添加if (endDate < startDate) endDate = startDate;保护 - 修复单日全天事件日历格子圆点缺失:
IcsCalendarService+CalendarViewModel三层防护修复 EndTime==StartTime 的匹配失败问题 - 日历格子多圆点支持:
DayCell控件从单圆点改为最多3个彩色圆点,按事件颜色去重显示
设置界面开发
- 实现完整的设置窗口 UI 和逻辑
- 实现所有 11 项设置功能
- 实现 .ics 远程日历服务
- 实现多数据源聚合
- 实现系统主题自动跟随
- 实现设置持久化到 JSON 文件
MVP 开发
- 完成系统托盘、月历视图、事件展示、事件详情、面板体验、主题等基础功能
- 完成 WPF 项目搭建和基础架构