Compare commits

..

367 Commits

Author SHA1 Message Date
CJKmkp 8383002b5c improve:自动更新 2026-05-01 19:11:59 +08:00
CJKmkp 8c37b91b5b improve:自动更新 2026-05-01 18:58:10 +08:00
CJKmkp 5a387eef96 add:更新面板 2026-05-01 18:55:03 +08:00
CJKmkp c27759189d add:快捷键面板 2026-05-01 17:43:29 +08:00
CJKmkp a33a399989 add:快捷键面板 2026-05-01 17:39:27 +08:00
CJKmkp 6c7c76958f add:快捷键面板 2026-05-01 17:36:53 +08:00
CJKmkp 6980abe331 add:浮动栏自定义 2026-05-01 17:20:47 +08:00
CJKmkp 5fc92cdd10 improve:UI 2026-05-01 16:57:21 +08:00
CJKmkp 4bcc39a7ae improve:PPT控件 2026-05-01 16:36:17 +08:00
CJKmkp a044a8bc21 improve:PPT控件 2026-05-01 16:07:40 +08:00
CJKmkp f15a293a69 add:自定义文件名 2026-05-01 14:51:56 +08:00
CJKmkp 448a695503 improve:UI 2026-05-01 14:28:21 +08:00
CJKmkp 3915893ee6 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 13:08:29 +08:00
CJKmkp 914e6eea2e improve:PPT控件 2026-05-01 13:08:26 +08:00
PrefacedCorg 3434ab6227 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 12:34:26 +08:00
PrefacedCorg 90ba3f7fa6 refactor: 优化动画逻辑和UI组件结构
重构动画帮助类以支持自定义动画目标
简化颜色滑块更新逻辑
调整浮动工具栏显示逻辑
新增BoardMenuFrame自定义控件
2026-05-01 12:34:16 +08:00
CJKmkp f05bcf6cc8 improve:ROT模块 2026-05-01 10:51:28 +08:00
CJKmkp 21d5ee25ea improve:ROT模块 2026-05-01 10:39:04 +08:00
CJKmkp 13c73fbfe4 优化代码 2026-05-01 09:58:25 +08:00
CJKmkp 97dbedfed5 improve:UI 2026-05-01 09:31:10 +08:00
CJKmkp e5ef1c6472 improve:UI 2026-05-01 09:25:06 +08:00
CJKmkp 17945a9298 improve:OOBE 2026-05-01 09:10:28 +08:00
CJKmkp 17c0ecc0f5 improve:OOBE 2026-05-01 08:39:36 +08:00
CJKmkp 8c2fc15d81 improve:UI 2026-05-01 01:52:36 +08:00
CJKmkp ac399773d0 improve:OOBE 2026-05-01 01:49:56 +08:00
CJKmkp 199674feca improve:UI 2026-05-01 01:35:29 +08:00
CJKmkp cb6af3e21f add!:安全面板 2026-05-01 01:22:35 +08:00
CJKmkp efe0bb6ae2 add:存储管理 2026-05-01 01:00:12 +08:00
CJKmkp 4a1403eed3 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:54:15 +08:00
CJKmkp 77a7628453 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:50:23 +08:00
PrefacedCorg 21186d1edb Update DebugPage.xaml 2026-05-01 00:49:37 +08:00
PrefacedCorg 1cb77b4f48 refactor(设置页面): 移除独立的图标设置页面并合并到调试页面
将图标设置功能从独立的 IconographyPage 迁移到 DebugPage 中,简化页面结构
删除不再使用的 IconographyPage.xaml 和 IconographyPage.xaml.cs 文件
2026-05-01 00:47:47 +08:00
PrefacedCorg a6f92a883c Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:41:28 +08:00
PrefacedCorg 2c0b09a9ad feat(UI): 统一调整设置页面滑块控件布局和样式
调整多个设置页面的滑块控件布局,统一使用SimpleStackPanel水平排列滑块和数值显示文本,增加数值显示的Consolas字体样式,并统一滑块宽度为200像素
2026-05-01 00:36:41 +08:00
CJKmkp 003fffca2d improve:ROT模块 2026-05-01 00:33:35 +08:00
CJKmkp 28b719e9db Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:27:48 +08:00
CJKmkp 4d8fbef455 add:debug功能 2026-05-01 00:27:29 +08:00
PrefacedCorg e55f8e8ea1 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:25:45 +08:00
PrefacedCorg 8ad987a64b refactor(设置): 将黑板缩放功能从开关改为滑块控制
将黑板缩放功能从布尔开关改为可调节的滑块控制,并统一所有滑块值的精度为两位小数
2026-05-01 00:25:35 +08:00
CJKmkp b0aff1e378 improve:UI 2026-05-01 00:07:18 +08:00
CJKmkp 6239ac0b88 improve:UI 2026-04-30 23:43:13 +08:00
CJKmkp 875fa9c6f0 improve:悬浮窗拦截 2026-04-30 23:33:05 +08:00
CJKmkp a7b020b0ff improve:UIA置顶 2026-04-30 22:59:19 +08:00
CJKmkp b614517728 improve:UIA置顶 2026-04-30 22:51:36 +08:00
CJKmkp 329e9bd933 improve:UIA置顶 2026-04-30 22:49:05 +08:00
CJKmkp 73f27a9423 优化代码 2026-04-30 20:31:05 +08:00
CJKmkp ffee3aa564 improve:设备ID 2026-04-30 20:27:25 +08:00
CJKmkp 9268ebb580 improve:UI 2026-04-30 20:09:51 +08:00
CJKmkp 40fc4e89e0 add:PPT增强预览视图 2026-04-30 19:09:55 +08:00
CJKmkp 6fd4a4f036 add:多屏支持 2026-04-30 18:35:37 +08:00
CJKmkp 483991b85c add:多屏支持 2026-04-30 18:28:39 +08:00
CJKmkp a0d24ea6cc improve:ROT模块 2026-04-30 17:58:28 +08:00
CJKmkp 9613285513 improve:ROT模块 2026-04-30 17:55:56 +08:00
CJKmkp 649a939a57 improve:实时笔锋 2026-04-30 17:51:43 +08:00
CJKmkp f20e360c0b improve:实时笔锋 2026-04-30 17:21:40 +08:00
CJKmkp c1e599971e fix:退出软件实现 2026-04-30 16:55:38 +08:00
CJKmkp 21c93d38c7 fix:issue #463 2026-04-30 16:44:52 +08:00
CJKmkp 4902559cfa improve:启动速度 2026-04-30 15:38:21 +08:00
CJKmkp 49b22dc184 improve:启动计时 2026-04-30 15:26:47 +08:00
CJKmkp f1f75ff015 improve:启动速度 2026-04-30 15:18:35 +08:00
CJKmkp 6e68fa9cfc improve:实时笔锋 2026-04-30 15:14:17 +08:00
CJKmkp c68596b91e improve:启动速度 2026-04-30 15:04:54 +08:00
CJKmkp 7d52573595 improve:启动速度 2026-04-30 14:50:57 +08:00
CJKmkp 6ac34ba8aa improve:启动速度 2026-04-30 14:44:49 +08:00
CJKmkp 9136f1dbe3 improve:启动速度 2026-04-30 14:42:24 +08:00
CJKmkp 98cfdb53c3 improve:UI 2026-04-30 14:38:23 +08:00
CJKmkp e17387c99e fix:版本修复 2026-04-30 14:34:58 +08:00
CJKmkp d30ed9e726 优化代码 2026-04-30 14:29:06 +08:00
CJKmkp 988af60a30 improve:实时笔锋 2026-04-30 14:25:32 +08:00
PrefacedCorg 8b53456b5d refactor(插件设置): 移除静态属性并优化页面导航逻辑
将 PluginSettingsPage 的 CurrentPlugin 从静态属性改为实例属性,避免状态共享问题
重构 SettingsWindow 的导航逻辑,支持直接传递插件信息并避免重复导航
2026-04-30 00:08:09 +08:00
PrefacedCorg d23193527e fix(UI): 调整笔划选择控件的位置
将笔划选择控件的垂直位置从900改为400,使其更符合用户操作习惯
2026-04-29 23:23:58 +08:00
PrefacedCorg b58376847e refactor: 移除按笔画擦除功能及相关UI元素
移除不再需要的按笔画擦除功能代码和对应的XAML界面元素,简化代码结构
2026-04-29 17:18:55 +08:00
PrefacedCorg d0551db70a style: 调整XAML布局格式和可见性属性
统一XAML元素的格式和对齐方式,并添加设计时可见性属性以便于开发调试
2026-04-29 17:15:16 +08:00
PrefacedCorg e416151a56 style: 调整主窗口尺寸为1920x1080标准分辨率 2026-04-29 17:05:49 +08:00
CJK_mkp b02b496b88 Merge pull request #461 from InkCanvasForClass/all-contributors/add-Hao3288
docs: add Hao3288 as a contributor for code
2026-04-28 17:27:27 +08:00
PrefacedCorg e31dbbcedc Update MW_FloatingBarIcons.cs 2026-04-27 18:13:44 +08:00
allcontributors[bot] 09ad1bac86 docs: update .all-contributorsrc 2026-04-27 04:56:04 +00:00
allcontributors[bot] a2ac761f70 docs: update README.md 2026-04-27 04:56:03 +00:00
PrefacedCorg 9e5ad7e2b4 refactor(HomePage): 将操作按钮布局改为网格排列
将操作区域的三个按钮从垂直排列改为水平网格布局,使用Grid控件实现三列等宽排列,提升界面空间利用率和美观性
2026-04-27 10:06:05 +08:00
PrefacedCorg 4f015fb155 refactor(设置): 重构设置窗口为独立窗口并移除旧设置面板
移除旧的内置设置面板及相关代码,将设置功能迁移至独立的设置窗口
优化设置窗口的主题同步功能,调整自动化页面布局
2026-04-27 09:40:24 +08:00
PrefacedCorg b4526473fa feat(设置界面): 重构自动化设置页面布局并优化悬浮窗拦截逻辑
将自动化设置页面中的白板应用分类整理,使用折叠面板分组显示
移除独立的悬浮窗拦截开关,改为根据子选项自动启用拦截功能
调整画板设置菜单项在导航中的位置
2026-04-26 21:01:11 +08:00
PrefacedCorg 3ab9fcc857 refactor(UI): 重构悬浮窗拦截设置界面布局
将悬浮窗拦截设置从LabeledSettingsCard改为SettingsCard和ToggleSwitch组合
优化界面结构,使开关控件更直观
添加Expander控制拦截规则的显示状态
2026-04-26 20:26:24 +08:00
PrefacedCorg 7ef231a4c7 Merge pull request #460 from Hao3288/net6
Use LabeledSettingsCard with i18n and IconSource
2026-04-26 18:59:20 +08:00
RedstonCoder da3a1f7dea Use LabeledSettingsCard with i18n and IconSource
Replace multiple ui:SettingsCard blocks with compact controls:LabeledSettingsCard entries. Each card now uses Header bindings to i18n keys and an IconSource attribute instead of nested HeaderIcon content, while preserving existing Toggled event handlers. This refactors and simplifies the XAML, standardizes icon usage, and enables localization for the floating app interceptor settings.
2026-04-26 18:56:21 +08:00
PrefacedCorg 81001542b8 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-04-26 17:44:03 +08:00
PrefacedCorg aacf5b696c feat(主页): 添加重启、重置和退出应用程序功能
在主页设置页面新增操作区域,包含重启应用程序、重置为推荐设置和退出应用程序三个功能按钮。同时添加了对应的多语言资源字符串。
2026-04-26 17:40:21 +08:00
CJK_mkp 062c0feb65 Merge pull request #457 from InkCanvasForClass/trae/solo-agent-kMx5Zx
fix: 增强主窗口操作的稳定性
2026-04-26 17:17:30 +08:00
CJKmkp 92f97eab78 feat: 检查主窗口可见性时出错
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 09:05:58 +00:00
CJK_mkp 31120eab09 Merge pull request #456 from InkCanvasForClass/trae/solo-agent-hICuRf
Fix: 增强时光机功能处理笔划更改时的稳定性
2026-04-26 16:45:22 +08:00
CJKmkp 9b7d718f78 feat: WinRT Ink Analysis 性能优化重构方案
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 08:39:01 +00:00
PrefacedCorg 9132c9eb14 1 2026-04-26 16:38:54 +08:00
PrefacedCorg adebf8ec38 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-04-26 12:25:43 +08:00
CJK_mkp 854e14f9b0 Merge pull request #455 from InkCanvasForClass/trae/solo-agent-hICuRf
refactor: 优化墨迹分析功能,引入 ModernInkAnalyzer
2026-04-26 12:23:48 +08:00
CJKmkp d7faac257c feat: WinRT Ink Analysis 性能优化重构方案
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 04:19:30 +00:00
CJKmkp 1b3c5294d9 feat: WinRT Ink Analysis 性能优化重构方案
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 04:18:06 +00:00
CJKmkp f7802ba4d0 feat: WinRT Ink Analysis 性能优化重构方案
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 03:01:17 +00:00
PrefacedCorg 8e84dce73f Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-04-26 10:33:02 +08:00
doudou0720 620da0e645 build: 为项目添加 Windows 目标平台支持
在所有相关项目中启用 EnableWindowsTargeting 以支持 交叉编译

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-26 10:19:16 +08:00
PrefacedCorg c4657ebb86 1 2026-04-26 09:12:26 +08:00
PrefacedCorg 24cbaf69ea refactor(UI): 使用 LabeledSettingsCard 替换 SettingsCard 和 ToggleSwitch 组合
统一设置页面中的开关控件样式,将原有的 SettingsCard 和 ToggleSwitch 组合替换为自定义的 LabeledSettingsCard 控件,简化 XAML 结构并保持功能不变
2026-04-25 20:54:26 +08:00
PrefacedCorg f3ef2f7aec feat(设置): 新增高级设置和随机点名设置页面
refactor: 将高级和随机点名设置从主窗口迁移到独立页面
style: 优化墨迹识别页面的高精度直线拉直开关样式
fix: 修复URI处理中未刷新配置列表的问题
2026-04-25 20:26:56 +08:00
CJKmkp 77dff81217 improve:实时笔锋 2026-04-25 17:27:28 +08:00
CJKmkp 004364c3a9 improve:WinRT墨迹识别 2026-04-25 17:25:55 +08:00
CJKmkp 877d702978 improve:手掌擦 2026-04-25 17:23:48 +08:00
CJKmkp a8bc9f442b improve:快捷键 2026-04-25 17:22:52 +08:00
PrefacedCorg f09f05ab3c Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-04-25 17:00:42 +08:00
PrefacedCorg ef5377f85c feat(设置): 新增个性化设置页面并重构主题相关功能
重构主题和语言设置功能,将相关代码从主窗口迁移至新增的个性化设置页面
优化浮动工具栏图标选择逻辑,移除冗余代码
统一设置页面中开关控件的样式和行为
修复设置页面导航项的选择状态问题
2026-04-25 17:00:02 +08:00
CJKmkp 5dbdd7bf12 improve:UI 2026-04-25 16:48:07 +08:00
CJKmkp d73e3eb0ea improve:墨迹纠正 2026-04-25 16:40:39 +08:00
CJKmkp b7d03d694c fix:墨迹纠正 2026-04-25 16:40:39 +08:00
CJKmkp b7d4983af5 improve:墨迹纠正 2026-04-25 16:40:39 +08:00
CJKmkp 919cf8a736 更新版本号 2026-04-25 16:40:39 +08:00
CJKmkp 8a7d2ddf30 add:禁用硬件加速 & improve:UI 2026-04-25 16:40:35 +08:00
PrefacedCorg b1640f44c2 feat(设置): 新增墨迹纠正设置页面并重构设置界面
重构设置界面,将墨迹纠正相关功能从画板设置中分离出来,新增独立的墨迹纠正设置页面。主要变更包括:
- 新增 InkRecognitionPage 用于集中管理墨迹纠正功能
- 调整设置窗口导航结构,将墨迹纠正设为画板设置的子页面
- 优化设置项布局,使用折叠面板组织复杂选项
- 移除主窗口中冗余的崩溃处理和手势设置
- 修复多指手势时橡皮擦状态保存问题
- 新增笔尖模式相关字符串资源
2026-04-25 13:56:16 +08:00
PrefacedCorg 6e61538dec feat(设置界面): 更新首页导航文本和分组标题
修改首页导航项的提示文本为"快速导航",并将设置项分组标题调整为"基本"、"主界面"和"其他",使界面分类更清晰
2026-04-25 11:45:30 +08:00
PrefacedCorg f789a919e1 refactor(设置): 清理并重构设置页面代码
删除未使用的设置页面文件,包括外观、设计、字体、主题等页面
简化主页导航逻辑,移除动态生成导航项代码
更新导航菜单项图标和跳转逻辑
优化页面布局和代码结构
2026-04-25 11:32:50 +08:00
PrefacedCorg b9651240df feat(设置): 添加实验性选项页面并优化图标显示
添加新的实验性选项页面,将高级设置中的实验性功能移至该页面
优化设置页面中图标的显示,支持自定义字体
为外部协议调用和避免全屏助手添加设置项
默认启用避免全屏助手功能
2026-04-24 19:16:48 +08:00
PrefacedCorg 5266368f79 1 2026-04-24 10:24:20 +08:00
PrefacedCorg 42854ff924 style(SettingsViews): 为XAML文件添加设计时属性和修复格式
为多个设置页面添加d:IsExpanded和d:Visibility设计时属性
修复AboutPage.xaml的BOM头问题
统一SettingsExpander的格式
2026-04-24 07:39:31 +08:00
PrefacedCorg 48b0e09278 优化 2026-04-24 07:13:27 +08:00
PrefacedCorg f05062f902 优化 2026-04-23 23:53:52 +08:00
PrefacedCorg 532aa03c56 Revert "delete:视频展台"
This reverts commit 0683779e09.
2026-04-23 22:17:55 +08:00
PrefacedCorg b891cb6fe3 Revert "add:新设置"
This reverts commit b949b1651a.
2026-04-23 22:09:27 +08:00
PrefacedCorg 0683779e09 delete:视频展台
去你的视频展台
2026-04-23 22:07:09 +08:00
PrefacedCorg b949b1651a add:新设置 2026-04-23 06:48:52 +08:00
PrefacedCorg 259ce3dd11 feat(插件): 添加应用重启服务接口及实现
添加 IAppRestartService 接口及其实现 AppRestartService,用于插件系统调用应用重启功能
将原 StartupPage 中的重启逻辑提取到 AppRestartHelper 工具类中
在 App.xaml.cs 中注册 AppRestartService 供插件使用
2026-04-21 02:16:58 +08:00
PrefacedCorg cab703b98e fix(SettingsWindow): 修复导航逻辑中的重复导航问题
添加_isNavigating标志位防止在导航过程中重复触发导航事件
2026-04-21 01:02:22 +08:00
PrefacedCorg a57e0496ee add:i18n 2026-04-19 16:00:20 +08:00
PrefacedCorg df66a49ba5 Update CanvasPage.xaml 2026-04-19 15:17:13 +08:00
PrefacedCorg 245a29f797 refactor:迁移设置 2026-04-19 15:13:13 +08:00
PrefacedCorg 4e8f574fdb Update AboutPage.xaml 2026-04-19 14:50:44 +08:00
PrefacedCorg a5d6135f0c refactor:迁移设置 2026-04-19 14:42:11 +08:00
PrefacedCorg 12c7fb1713 refactor:迁移设置 2026-04-19 08:35:22 +08:00
doudou0720 4dd56a4e5d ci: 将项目剩余工作流从 .NET Framework 迁移至 .NET 6
更新 README.md 中的运行环境要求,修改贡献指南中的目标分支
调整 CI/CD 工作流以适配 .NET 6 构建路径
移除安装程序中对 .NET Framework 的依赖检查

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-18 22:38:10 +08:00
PrefacedCorg abe5992d21 add:新新设置 2026-04-18 22:32:08 +08:00
PrefacedCorg 8d74b6ee30 优化 2026-04-17 21:10:26 +08:00
PrefacedCorg 8b5797ac66 fix:背景设置菜单无法显示 2026-04-17 13:41:18 +08:00
PrefacedCorg 231b850f74 fix:窗口置顶异常 2026-04-17 13:14:23 +08:00
PrefacedCorg 7003bb8426 优化 2026-04-17 09:26:29 +08:00
PrefacedCorg c4b5be783e 优化 2026-04-17 06:39:27 +08:00
PrefacedCorg c38829f7d8 优化 2026-04-17 06:23:49 +08:00
PrefacedCorg fdf3633d8c 优化 2026-04-17 02:05:20 +08:00
PrefacedCorg 3916476af7 优化 2026-04-17 01:43:18 +08:00
PrefacedCorg edc94547ab 优化 2026-04-17 01:20:06 +08:00
PrefacedCorg 8a8cf9a679 更新 MainWindow.xaml 2026-04-17 00:30:45 +08:00
PrefacedCorg 21f3b99838 fix:修复浮动工具栏批注菜单位置错误
这才是真正的精准定位 都给我学好了
2026-04-17 00:14:08 +08:00
PrefacedCorg 3bfa0b1d85 fix:修复浮动工具栏批注菜单位置错误
被恶心到了🤢
2026-04-17 00:03:41 +08:00
PrefacedCorg de55fd43a0 更新 MainWindow.xaml 2026-04-16 00:27:56 +08:00
PrefacedCorg 6e68e53b1e 更新 BoardToolbarButton.xaml.cs 2026-04-15 14:21:20 +08:00
PrefacedCorg 4b8d89854e 优化 2026-04-15 14:15:05 +08:00
PrefacedCorg 8298f7d5bb 优化 2026-04-15 13:02:16 +08:00
PrefacedCorg 816153a1db 优化( 2026-04-14 13:57:13 +08:00
PrefacedCorg 1e8ddf4754 优化 2026-04-14 12:47:54 +08:00
PrefacedCorg d73e87b980 优化 2026-04-14 12:19:36 +08:00
PrefacedCorg b64b0a3618 add:新控件 2026-04-13 23:53:59 +08:00
PrefacedCorg d52ae90b56 优化 2026-04-13 22:54:13 +08:00
PrefacedCorg 0045f97569 优化 2026-04-13 13:33:08 +08:00
PrefacedCorg 41be1e901d 将自定义控件单开一个项目 2026-04-13 13:01:14 +08:00
PrefacedCorg 0a14d96e10 更新 MW_Icons.cs 2026-04-13 01:44:58 +08:00
PrefacedCorg 57c3fe358b 2 2026-04-13 01:40:39 +08:00
PrefacedCorg c255db6ff3 1 2026-04-13 00:51:17 +08:00
CJK_mkp f6484f759e Merge pull request #443 from InkCanvasForClass/beta
合并net6
2026-04-12 17:23:23 +08:00
PrefacedCorg b16636d06b add:gong-wpf-dragdrop 2026-04-12 15:00:31 +08:00
CJKmkp b50049c822 improve:UI面板弹出 2026-04-12 09:08:33 +08:00
PrefacedCorg 220a316d70 浓缩才是精华(?) 2026-04-11 22:46:38 +08:00
PrefacedCorg dbac80509a 将浮动工具栏使用新控件 2026-04-11 17:18:51 +08:00
PrefacedCorg bb82eb4891 Update MW_AutoTheme.cs 2026-04-11 17:03:20 +08:00
CJK_mkp c1d584e3e7 Merge pull request #438 from InkCanvasForClass/All-Contributers/chore
docs: 更新贡献者配置和排版
2026-04-11 16:44:50 +08:00
CJK_mkp f776dac04d Merge pull request #436 from Tayasui-rainnya/beta
feat: add '包含墨迹' toggle and ink-overlay support for area screenshots
2026-04-11 16:44:34 +08:00
PrefacedCorg b56f99fff9 Update MW_Icons.cs 2026-04-11 15:08:25 +08:00
PrefacedCorg 4151dfaf1f 将浮动工具栏使用新控件 2026-04-11 14:29:44 +08:00
PrefacedCorg 836e0b0fde 将浮动工具栏使用新控件 2026-04-11 13:42:33 +08:00
doudou0720 6e7a0e36f4 docs: 更新贡献者配置和README排版
- 添加JSON schema验证
- 调整每行贡献者显示数量为5
- 更新贡献者贡献类型
- 重构README表格布局为每行5列

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-11 13:06:26 +08:00
PrefacedCorg a7fe7937a5 将浮动工具栏使用新控件 2026-04-11 12:44:40 +08:00
PrefacedCorg dba59b92ac add:自定义控件
屎山给我吃饱了 把浮动工具栏用了自定义控件 不然重复代码太多了
2026-04-11 12:06:53 +08:00
PrefacedCorg 0d6ffa6de6 Update MainWindow.xaml 2026-04-11 08:22:33 +08:00
PrefacedCorg 9edf5ee36c Update MainWindow.xaml 2026-04-11 06:57:12 +08:00
tayasui rainnya! 585b712c4c Enhance XML comments for ShowScreenshotSelector method
Updated XML documentation for ShowScreenshotSelector method to provide clearer details on parameters and return values.
2026-04-11 06:27:19 +08:00
PrefacedCorg d6f20a365e 优化主窗口设置选项 2026-04-11 00:57:22 +08:00
PrefacedCorg f7befe387c add:自定义控件 2026-04-10 22:59:22 +08:00
PrefacedCorg b1a34b7a64 冗余得没边了 2026-04-10 21:02:17 +08:00
PrefacedCorg d556cf7741 移除冗余字体设置 2026-04-10 20:22:09 +08:00
PrefacedCorg 76a046cca6 重构?
去你妈的屎山(ai说的)
2026-04-10 19:49:22 +08:00
PrefacedCorg d6385aea1c 更新 Ink Canvas.sln 2026-04-10 13:55:57 +08:00
PrefacedCorg 31bf2b5af1 更新 InkCanvasService.cs 2026-04-10 13:05:15 +08:00
PrefacedCorg de613c8a4b 更新 PluginPage.xaml 2026-04-10 12:14:23 +08:00
PrefacedCorg 84b626d344 add:插件
屎山
2026-04-10 01:24:57 +08:00
tayasui rainnya! 2ca7d2a337 Merge pull request #1 from Tayasui-rainnya/codex/add-toggle-for-ink-inclusion-in-screenshot
feat: add '包含墨迹' toggle and ink-overlay support for area screenshots
2026-04-09 13:44:34 +08:00
tayasui rainnya! ea03e8e7c7 fix: keep include-ink available for image-only canvas and tighten bitmap disposal 2026-04-09 13:36:08 +08:00
tayasui rainnya! 778894482f fix: disambiguate WPF Matrix type in ink overlay render path 2026-04-09 13:22:19 +08:00
tayasui rainnya! 1035f7ef92 fix: render ink overlay from strokes to remove capture offset 2026-04-09 13:19:47 +08:00
tayasui rainnya! 6e4f8bc982 fix: correct ink overlay alignment in screenshot capture 2026-04-09 13:16:43 +08:00
tayasui rainnya! 36e47cec43 fix: align ink overlay DPI and ignore include-ink toolbar clicks 2026-04-09 13:07:28 +08:00
tayasui rainnya! 8b2bc352a6 feat: add include-ink toggle for area screenshot selector 2026-04-09 12:54:31 +08:00
PrefacedCorg 256b3c0887 Revert " add:基础插件框架"
This reverts commit 302ef307fe.
2026-04-08 12:44:00 +08:00
PrefacedCorg 77cf91e802 Revert "更新 PluginManager.cs"
This reverts commit ea55eb1738.
2026-04-08 12:43:55 +08:00
PrefacedCorg ea55eb1738 更新 PluginManager.cs 2026-04-08 12:41:56 +08:00
PrefacedCorg 302ef307fe add:基础插件框架 2026-04-08 00:51:28 +08:00
PrefacedCorg 3f3a10de7d 代码清理 2026-04-08 00:00:32 +08:00
PrefacedCorg c73123cd23 fix:触摸滑动(左右两边) 2026-04-07 23:58:17 +08:00
PrefacedCorg 73935e0f22 fix:开关显示英文 2026-04-07 23:46:26 +08:00
PrefacedCorg 0dfe835eef fix:触摸滑动 2026-04-07 23:32:20 +08:00
PrefacedCorg 1ec07421b5 Revert "啊"
This reverts commit 441e600b5d.
2026-04-07 00:33:19 +08:00
PrefacedCorg 441e600b5d 2026-04-06 18:48:31 +08:00
PrefacedCorg bd4b4bd233 Update MainWindow.xaml 2026-04-06 15:02:04 +08:00
CJK_mkp f0343c98b5 合并net6 (#432)
* 文件更名

* improve:UI

快捷键设置UI改进
2026-04-06 09:44:21 +08:00
doudou0720 36ff945384 fix:修复编译警告
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 22:54:55 +08:00
CJKmkp 057fb35d00 improve:UI
快捷键设置UI改进
2026-04-05 22:03:16 +08:00
CJKmkp bcbece5bd6 文件更名 2026-04-05 21:50:53 +08:00
PrefacedCorg 1b01e4a5c5 Merge branch 'beta' into net6 2026-04-05 21:42:11 +08:00
PrefacedCorg 5aad206e0d Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-04-05 21:37:05 +08:00
CJKmkp 74344c4782 improve:issue #423 2026-04-05 21:35:43 +08:00
PrefacedCorg f280358f56 删除新的旧插件 2026-04-05 21:35:40 +08:00
CJKmkp 243201502b add:issue #402 2026-04-05 21:32:01 +08:00
CJKmkp 8c090218d1 improve:新设置 2026-04-05 21:19:19 +08:00
CJKmkp 6ca1c598d4 improve:UI 2026-04-05 21:08:38 +08:00
CJK_mkp 5eff424b2d 同步net6分支 (#430)
* improve:pdf插入

* fix:手掌擦 (#419)
2026-04-05 20:49:41 +08:00
CJK_mkp 0c078ef863 fix:手掌擦 (#419) 2026-04-05 20:43:40 +08:00
CJKmkp fea6576dfb improve:pdf插入 2026-04-05 20:37:48 +08:00
CJKmkp 998d829783 解决nuget问题 2026-04-05 20:26:07 +08:00
CJKmkp 226a6942dc improve:pdf插入 2026-04-05 20:21:59 +08:00
PrefacedCorg 52a3d184c7 Merge pull request #427 from InkCanvasForClass/bata-test
主线合并
2026-04-05 20:18:35 +08:00
PrefacedCorg 44cdab063c Merge branch 'net6' into bata-test 2026-04-05 20:14:45 +08:00
PrefacedCorg 24bc2cf138 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-04-05 19:36:35 +08:00
PrefacedCorg e6b707ea51 Merge branch 'New-New-Settings' into beta 2026-04-05 19:35:17 +08:00
CJKmkp 6183011952 improve:UI 2026-04-05 19:23:00 +08:00
CJKmkp d80a59556e Revert "improve:pdf插入"
This reverts commit bad05f77b5.
2026-04-05 19:07:31 +08:00
CJKmkp ebbe018bae 代码优化 2026-04-05 18:52:19 +08:00
CJKmkp bad05f77b5 improve:pdf插入 2026-04-05 18:42:06 +08:00
CJKmkp ea375a9ce6 add:pdf插入 2026-04-05 18:36:47 +08:00
CJKmkp d3382d7856 add:pdf插入 2026-04-05 18:30:05 +08:00
CJKmkp acf0c17d7a add:pdf插入 2026-04-05 18:22:37 +08:00
CJKmkp 83568c8b33 improve:UI 2026-04-05 18:18:18 +08:00
CJKmkp cc80529498 add:pdf插入 2026-04-05 18:15:23 +08:00
PrefacedCorg 002970921d Update MainWindow.xaml 2026-04-05 18:08:48 +08:00
CJKmkp ea74592e89 add:pdf插入 2026-04-05 18:06:21 +08:00
PrefacedCorg fa38d3d664 remove:旧的新设置 2026-04-05 17:58:57 +08:00
CJKmkp cffedb8cb7 add:pdf插入 2026-04-05 17:49:05 +08:00
CJKmkp c77beb662e add:pdf插入 2026-04-05 17:31:35 +08:00
PrefacedCorg 77dd83c2d6 新设置
删除插件管理
2026-04-05 17:31:15 +08:00
PrefacedCorg ceb99cea2b 重命名和补回i18n 2026-04-05 17:27:00 +08:00
PrefacedCorg 04a8224484 Merge branch 'beta' into New-New-Settings 2026-04-05 17:08:03 +08:00
PrefacedCorg 2f54e9d7b3 add:复制按钮 2026-04-05 17:01:20 +08:00
CJKmkp 1165e5bbf2 add:临时窗口显示 2026-04-05 15:49:06 +08:00
doudou0720 24c6ca60a3 refactor: 替换任务栏图标库为 H.NotifyIcon (#425)
更新所有相关文件和引用,从 Hardcodet.Wpf.TaskbarNotification 迁移到 H.NotifyIcon
调整通知显示方式并添加 ForceCreate 调用确保图标正确显示

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 15:04:37 +08:00
CJKmkp 8f3b65c6d4 improve:仅PPT模式 2026-04-05 14:58:32 +08:00
CJKmkp 4835e3db50 优化代码 2026-04-05 14:18:33 +08:00
CJKmkp 7a30c93ff5 delete:插件系统 2026-04-05 14:12:35 +08:00
CJKmkp 1fca17d557 add:插件系统 2026-04-05 14:06:49 +08:00
doudou0720 67a70fd6e4 chore: 更改工作流触发条件
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 12:55:35 +08:00
doudou0720 2e8660de26 ci: 更新工作流以支持 .NET 6 和 Windows 10 19041
更新 GitHub Actions 工作流以构建针对 .NET 6 和 Windows 10 19041 平台的应用程序,并修改了程序集信息以声明支持的 OS 平台

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 12:54:55 +08:00
CJKmkp 069a478559 improve:手写体识别 2026-04-05 12:17:02 +08:00
CJKmkp c70d8e1c4e improve:手写体识别 2026-04-05 11:35:14 +08:00
CJKmkp 379d514bb5 update:net6 2026-04-05 11:15:11 +08:00
CJKmkp 25dc6a00d3 Revert "feat:Downgrade to .NET Framework 4.6.2 project (#415)"
This reverts commit eb2f65e6e5.
2026-04-05 10:52:45 +08:00
doudou0720 eb2f65e6e5 feat:Downgrade to .NET Framework 4.6.2 project (#415)
* chore:Init net 462

* feat: 将 .NET Framework 依赖从 4.7.2 降级至 4.6.2

更新应用配置、安装程序和文档以支持 .NET Framework 4.6.2

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 10:47:00 +08:00
PANDAJSR 16c86cd02d feat:为托盘右键菜单添加设置入口 (#405)
* 软件设置内的贡献者列表添加了 PANDA-JSR

* feat(tray): add separate entries for new/legacy settings

* feat(tray): merge settings menu entries

* fix: use ikw namespace for tray menu stack panel
2026-04-05 09:53:14 +08:00
CJKmkp f5a657d5c3 improve:展台
新增将展台替换为希沃展台快捷启动功能
2026-04-05 09:38:16 +08:00
CJKmkp dfc23b4428 fix:一言API设置重复写入 2026-04-05 09:25:53 +08:00
CJKmkp b62055e705 Revert "fix:一言API设置重复写入"
This reverts commit 3cf1ea438b.
2026-04-05 09:12:31 +08:00
CJKmkp 3190211f9a fix:一言API设置重复写入 2026-04-05 09:06:20 +08:00
CJKmkp e138f600a1 improve:自动更新 2026-04-05 07:56:47 +08:00
CJKmkp 2173b82fb4 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-04-05 07:42:27 +08:00
CJKmkp ceeb9bffba 更新版本号 2026-04-05 07:42:26 +08:00
doudou0720 ba70d67c89 ci:尝试修复softprops/action-gh-release@v2令牌疑似使用Action Token而非Octo-sts Token的未说明问题
神秘Action

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 00:34:56 +08:00
CJKmkp 74341cc162 优化注释 2026-04-04 23:34:56 +08:00
CJKmkp fc4a3a1194 improve:UI 2026-04-04 23:34:26 +08:00
CJKmkp c33ac03255 improve:墨迹渲染 2026-04-04 23:06:16 +08:00
CJKmkp 66afe271c5 improve:实时笔锋 2026-04-04 22:56:34 +08:00
PrefacedCorg 9279783fc3 Revert "新设置"
This reverts commit 140e92eeda.
2026-04-04 22:26:32 +08:00
CJKmkp 3cf1ea438b fix:一言API设置重复写入 2026-04-04 22:21:34 +08:00
CJKmkp 277e46030d improve:WinRT墨迹识别及笔锋 2026-04-04 22:16:37 +08:00
CJKmkp af19ffb736 fix:一言API设置重复写入 2026-04-04 22:16:32 +08:00
CJKmkp 1f00962c47 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-04-04 21:08:56 +08:00
PrefacedCorg 742a62b6ff Revert "新设置"
This reverts commit 140e92eeda.
2026-04-04 19:43:06 +08:00
PrefacedCorg 140e92eeda 新设置 2026-04-04 18:45:59 +08:00
PrefacedCorg 34c2dab82a i1145141919810n 2026-04-04 16:37:08 +08:00
PrefacedCorg fa2366c373 i1145141919810n 2026-04-04 15:40:36 +08:00
PrefacedCorg e2d898df14 Update SettingsWindow2.xaml 2026-04-04 12:45:47 +08:00
PrefacedCorg cacc67b11d 新设置 2026-04-04 11:33:47 +08:00
doudou0720 aef860a8cc ci: 修复发布工作流中的变量引用格式,避免changelog带有双引号而被Shell解析
神秘问题

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-04 11:22:54 +08:00
PrefacedCorg c890f95092 i18n
添加注释
2026-04-04 07:50:30 +08:00
PrefacedCorg 7c9a5f8265 Merge branch 'beta' into New-New-Settings 2026-04-04 00:56:43 +08:00
PrefacedCorg 7eafcb02cb 新设置 2026-04-04 00:54:22 +08:00
PrefacedCorg d4517d3c53 Update SettingsWindow2.xaml.cs 2026-04-04 00:54:04 +08:00
PrefacedCorg 62a9a097aa i18n 2026-04-04 00:00:52 +08:00
PrefacedCorg e7fa1caf6c Update SettingsWindow2.xaml 2026-04-03 22:11:57 +08:00
PrefacedCorg e347443a0a Update SettingsWindow2.xaml 2026-04-03 19:46:30 +08:00
PrefacedCorg b77e540863 Reapply "更新 SettingsWindow2.xaml.cs"
This reverts commit a6fdb07e8b.
2026-04-03 19:30:44 +08:00
PrefacedCorg df4168e8fa Reapply "更新 SettingsWindow2.xaml"
This reverts commit 11cb1815ad.
2026-04-03 19:30:40 +08:00
PrefacedCorg 576f86ce48 Reapply "add:高dpi适配 插件接口 设置窗口多显示器优化 fix:触摸后鼠标指针显示异常"
This reverts commit 26f79da2f9.
2026-04-03 19:30:29 +08:00
PrefacedCorg c3335c0c24 Update SettingsWindow.xaml.cs 2026-04-03 19:30:06 +08:00
PrefacedCorg 26f79da2f9 Revert "add:高dpi适配 插件接口 设置窗口多显示器优化 fix:触摸后鼠标指针显示异常"
This reverts commit ca53624149.
2026-04-03 00:14:28 +08:00
PrefacedCorg 11cb1815ad Revert "更新 SettingsWindow2.xaml"
This reverts commit 5f9b01ef04.
2026-04-03 00:14:23 +08:00
PrefacedCorg a6fdb07e8b Revert "更新 SettingsWindow2.xaml.cs"
This reverts commit ffdb3cd431.
2026-04-03 00:14:10 +08:00
PrefacedCorg ffdb3cd431 更新 SettingsWindow2.xaml.cs 2026-04-02 00:17:25 +08:00
PrefacedCorg 5f9b01ef04 更新 SettingsWindow2.xaml 2026-04-02 00:01:02 +08:00
PrefacedCorg ca53624149 add:高dpi适配 插件接口 设置窗口多显示器优化 fix:触摸后鼠标指针显示异常 2026-04-01 23:57:50 +08:00
PrefacedCorg 6484450ad3 更新 SettingsWindow2.xaml.cs 2026-04-01 13:56:46 +08:00
PrefacedCorg 7c97b683ea 新新设置的第一个开关设置选项 开机时启动 2026-04-01 00:46:40 +08:00
PrefacedCorg 378b3e73f2 1 2026-04-01 00:16:36 +08:00
PrefacedCorg a2f21357b6 我服了 2026-03-31 00:25:08 +08:00
doudou0720 79e03dc0d5 fix(ci):尝试使用通用指令获取文件大小
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-29 22:31:16 +08:00
PrefacedCorg e0f35450e1 add:NewNewSettings 2026-03-29 17:39:52 +08:00
PrefacedCorg cc9f58fb6a add:NewNewSettings 2026-03-29 14:21:27 +08:00
PrefacedCorg e8be85141b add:NewNewSettings 2026-03-29 13:47:19 +08:00
PrefacedCorg 1bf57ea2f5 1 2026-03-29 12:38:46 +08:00
CJKmkp 18b737b22b add:手写体识别 2026-03-29 12:24:13 +08:00
CJKmkp 4ec3332808 fix:自动收纳 2026-03-29 12:23:26 +08:00
doudou0720 b2290ecf65 feat(ci):支持多架构编译
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 23:56:31 +08:00
CJKmkp 55c336daab 更新项目文件 2026-03-28 23:19:14 +08:00
CJKmkp f2ed3f619c 更新项目文件 2026-03-28 22:32:35 +08:00
CJKmkp 114f400bd9 更新项目文件 2026-03-28 22:29:38 +08:00
CJKmkp 506d1118b2 更新项目文件 2026-03-28 22:25:50 +08:00
PrefacedCorg 20441543f0 Merge pull request #420 from InkCanvasForClass/beta
...
2026-03-28 22:05:28 +08:00
doudou0720 2dbc47ac3c chore:调整构建配置
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:53:10 +08:00
doudou0720 bbe99649cd chore:更新Nightly下载链接
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:41:11 +08:00
doudou0720 04806d2004 refactor(ci): 重构构建配置和CI工作流
统一项目构建输出路径结构,移除冗余的x86 Debug配置
简化CI工作流中的artifact上传逻辑,支持多架构构建
更新PR检查工作流名称以更准确反映其用途

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:35:24 +08:00
doudou0720 759a635026 chore:修复工作流语法错误
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:04:20 +08:00
doudou0720 e68bd9286f feat(ci): 分离PR检查工作流并添加多架构构建支持
将PR检查从主工作流中分离为独立文件,并添加对x86架构的构建支持

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:00:38 +08:00
CJKmkp a6b31e82c0 优化代码 2026-03-28 20:29:12 +08:00
CJKmkp bc3e37e541 improve:自动更新 2026-03-28 20:28:42 +08:00
CJKmkp 1124bb6bfa improve:自动更新 2026-03-28 20:20:44 +08:00
CJKmkp b19c19d73c 优化日志 2026-03-28 19:21:47 +08:00
CJKmkp feb3fad4da add:WinRT墨迹识别 2026-03-28 19:05:54 +08:00
CJKmkp 3db745d684 add:WinRT墨迹识别 2026-03-28 18:45:42 +08:00
CJKmkp b4089ae62e add:WinRT墨迹识别 2026-03-28 18:43:29 +08:00
CJKmkp 97bdf78b08 add:WinRT墨迹识别 2026-03-28 18:40:18 +08:00
CJKmkp dc9fb26260 add:WinRT墨迹识别 2026-03-28 18:30:40 +08:00
CJKmkp 97b0972fdf add:WinRT墨迹识别 2026-03-28 18:10:28 +08:00
CJKmkp ea23145349 add:WinRT墨迹识别 2026-03-28 18:01:14 +08:00
CJKmkp f7013196f7 delete:墨迹预测 2026-03-28 17:52:30 +08:00
CJKmkp 9d0baa0799 add:WinRT墨迹识别 2026-03-28 17:40:14 +08:00
CJK_mkp 91c2fa4eee Revert "feat: 选区截图时保留屏幕笔迹 (#406)" (#418)
This reverts commit f7aa107a62.
2026-03-28 17:23:27 +08:00
CJKmkp 837311b2b7 add:实时笔锋及墨迹预测 2026-03-28 17:16:47 +08:00
CJKmkp bc53eab669 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-03-28 17:12:23 +08:00
CJKmkp 55eb811193 add:实时笔锋及墨迹预测 2026-03-28 17:11:30 +08:00
tayasui rainnya! f7aa107a62 feat: 选区截图时保留屏幕笔迹 (#406)
* feat: 使用选区截图时,不清除 Strokes(Keep it on screen)

* fix: 浮动栏选区截图前强制保持墨迹可见

* fix: 避免选区截图回滚 inkCanvas 运行时状态

* fix: 截图前退出并在结束后恢复批注状态

* fix: 截图流程改用轻量批注暂停避免副作用

* feat: 选区截图添加包含墨迹开关

* fix: 避免选区截图墨迹重复渲染

* fix: 全屏基础截图排除主窗口后再叠加墨迹

* fix: 隐藏浮动栏后再进入选区截图

* fix: 添加到白板时不强制恢复浮动栏可见性

* fix: 防止重复启动选区截图实例

* fix: 仅在白板接管成功后跳过浮动栏恢复

* feat: 选区截图时实时预览包含墨迹开关

* fix: 合并截图选择器OnClosed逻辑避免重复定义
2026-03-28 17:11:18 +08:00
CJKmkp 56a65af9a7 add:实时笔锋及墨迹预测 2026-03-28 17:06:16 +08:00
CJKmkp de3f5d16a2 add:实时笔锋及墨迹预测 2026-03-28 17:04:50 +08:00
CJKmkp d325a58f17 add:实时笔锋及墨迹预测 2026-03-28 16:59:02 +08:00
CJKmkp fd137ae787 improve:安全面板 2026-03-28 16:46:35 +08:00
CJKmkp bb1b893961 improve:自动收纳 2026-03-28 16:46:21 +08:00
CJKmkp 01c247ac29 add:unget for 现代化墨迹识别 2026-03-27 17:45:21 +08:00
CJKmkp 1fa75640ee 更新nuget 2026-03-27 17:36:10 +08:00
CJKmkp 6e0191af6b 更新nuget 2026-03-27 17:30:01 +08:00
PrefacedCorg 9ac4070e4e fix:窗口标题栏最小化隐藏 oobe 上下一步无法触摸 2026-03-24 01:21:57 +08:00
CJK_mkp 2590ea5bdb Fix PPT disconnect null check crash (#413) 2026-03-23 16:46:28 +08:00
CJK_mkp 290e031f77 Handle RPC failures during PPT disconnect (#412) 2026-03-23 16:41:50 +08:00
PrefacedCorg 9bc9af5eec 改了下标题栏
不会约定式提交
2026-03-22 14:35:19 +08:00
PrefacedCorg 8faffe9d4e Revert "新设置右侧下部添加触摸支持"
This reverts commit c9dd98fa8d.
2026-03-21 23:09:45 +08:00
PrefacedCorg 95dfad64ce 空值检测?
去你的空值
炸了不关我事
2026-03-21 23:06:16 +08:00
PrefacedCorg c9dd98fa8d 新设置右侧下部添加触摸支持
右侧顶部依旧无法触摸 不会修 等重写标题栏
2026-03-21 23:04:52 +08:00
doudou0720 3ae99c82cc fix(SymbolIcon):修复SymbolIcon问题并修改打包过程以正确构建单文件程序 (#407)
* refactor(Icon): 将SymbolIcon替换为FontIcon以使用Segoe Fluent Icons

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* chore: 移除 Costura.Fody 的 IncludeAssets 配置

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-21 22:15:36 +08:00
doudou0720 016abafee4 fix(UI): 为新设置、云储存管理添加基本触控支持 (#409)
实测部分按钮(如新设置的云储存按钮)仍然有问题

统一设置所有ScrollViewer控件的PanningMode为VerticalOnly,防止水平滚动干扰用户体验

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-21 17:46:19 +08:00
doudou0720 dca606cf5f chore(deps):Bump microsoft/setup-msbuild v2 to v3
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-21 17:43:33 +08:00
CJKmkp e7c77b173d improve:PPT模块 2026-03-21 16:57:14 +08:00
CJKmkp 68b588ee7b improve:外部点名 2026-03-21 16:49:37 +08:00
CJKmkp b4c753dfb4 improve:PPT模块 2026-03-21 16:41:39 +08:00
CJKmkp 037bcf6007 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-03-21 16:40:32 +08:00
CJKmkp b2e0e7b9e2 improve:安全面板 2026-03-21 16:30:55 +08:00
PrefacedCorg 03c1399768 看得不够长 调长点 2026-03-21 16:29:19 +08:00
PrefacedCorg 9b3a59f089 改漏了再改一下 2026-03-21 16:28:27 +08:00
PrefacedCorg e8f824a046 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-03-21 16:19:38 +08:00
PrefacedCorg 4772b9cce7 改了两ui 适配了触摸 2026-03-21 16:18:36 +08:00
CJKmkp 74b64bd21f 代码优化 2026-03-21 16:13:42 +08:00
doudou0720 3934270ed2 refactor: 移除未使用的VBIDE COM引用
Github Action都没有这个lib怎么构建.jpg

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-14 21:20:40 +08:00
294 changed files with 34439 additions and 50036 deletions
+17 -4
View File
@@ -1,4 +1,5 @@
{
"$schema": "https://www.schemastore.org/all-contributors.json",
"projectName": "community",
"projectOwner": "InkCanvasForClass",
"files": [
@@ -6,7 +7,7 @@
],
"commitType": "docs",
"commitConvention": "angular",
"contributorsPerLine": 7,
"contributorsPerLine": 5,
"contributors": [
{
"login": "CJKmkp",
@@ -100,7 +101,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/156585442?v=4",
"profile": "https://github.com/Tayasui-rainnya",
"contributions": [
"design"
"design",
"code"
]
},
{
@@ -110,7 +112,8 @@
"profile": "https://github.com/doudou0720",
"contributions": [
"code",
"blog"
"blog",
"infra"
]
},
{
@@ -140,6 +143,16 @@
"infra",
"blog"
]
},
{
"login": "Hao3288",
"name": "NoobHao",
"avatar_url": "https://avatars.githubusercontent.com/u/119276078?v=4",
"profile": "https://github.com/Hao3288",
"contributions": [
"code"
]
}
]
],
"repoType": "github"
}
+13 -12
View File
@@ -2,12 +2,7 @@ name: .NET Build & Package
on:
push:
branches: [ main, beta ]
pull_request:
types: [opened, synchronize, reopened ]
branches: [ main, beta ]
paths-ignore:
- '**/*.md'
branches: [ net6 ]
workflow_dispatch:
concurrency:
@@ -21,6 +16,10 @@ jobs:
build-and-package:
name: Build & Package
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
architecture: [AnyCPU, x86]
steps:
- name: Checkout code
uses: actions/checkout@v6
@@ -28,7 +27,7 @@ jobs:
fetch-depth: 1
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
uses: microsoft/setup-msbuild@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v5
@@ -42,12 +41,12 @@ jobs:
- name: Build the Solution
env:
DLASS_SENTRY_DSN: ${{ secrets.DLASS_SENTRY_DSN }}
run: msbuild /p:platform="AnyCPU" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
run: msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
- name: Check if exe file is generated
id: check-exe
run: |
$exePath = "Ink Canvas\bin\Debug\net472\InkCanvasForClass.exe"
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
if (Test-Path $exePath) {
echo "build_success=true" >> $env:GITHUB_OUTPUT
@@ -74,8 +73,8 @@ jobs:
if: steps.check-exe.outputs.build_success == 'true'
uses: actions/upload-artifact@v7
with:
name: InkCanvasForClass.CE.debug
path: "Ink Canvas/bin/Debug/net472/*"
name: InkCanvasForClass.CE.debug.${{ matrix.architecture }}
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
- name: Create Summary
if: always()
@@ -90,15 +89,17 @@ jobs:
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "[Download Artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.zip)\)" >> $GITHUB_STEP_SUMMARY
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip)\)" >> $GITHUB_STEP_SUMMARY
else
echo "## ❌ Build Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
+104
View File
@@ -0,0 +1,104 @@
name: PR Check
on:
pull_request:
types: [opened, synchronize]
branches: [ main, net6 ]
paths-ignore:
- '**/*.md'
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.head_ref || github.sha }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build-and-package:
name: Build & Package
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
architecture: [AnyCPU, x86]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v5
- name: Restore Package
run: dotnet restore "Ink Canvas.sln" --locked-mode
- name: Build the Solution
run: msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
- name: Check if exe file is generated
id: check-exe
run: |
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
if (Test-Path $exePath) {
echo "build_success=true" >> $env:GITHUB_OUTPUT
} else {
echo "build_success=false" >> $env:GITHUB_OUTPUT
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
exit 1
}
}
- name: Create Package (if build succeeded)
id: create-archive
if: steps.check-exe.outputs.build_success == 'true'
env:
GITHUB_SHA: ${{ github.sha }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
$shortSha = $env:GITHUB_SHA.Substring(0, 7)
$version = "debug-$shortSha-$env:GITHUB_RUN_NUMBER"
echo "archive_name=$version" >> $env:GITHUB_OUTPUT
- name: Upload Artifact (if build succeeded)
if: steps.check-exe.outputs.build_success == 'true'
uses: actions/upload-artifact@v7
with:
name: InkCanvasForClass.CE.debug.${{ matrix.architecture }}
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
- name: Create Summary
if: always()
shell: bash
run: |
echo "# Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then
echo "## ✅ Build Successful" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "[Download Artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip)\)" >> $GITHUB_STEP_SUMMARY
else
echo "## ❌ Build Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
fi
+149 -41
View File
@@ -200,6 +200,10 @@ jobs:
needs: prepare
if: success()
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
architecture: [AnyCPU, x86]
outputs:
archive_name: ${{ steps.create_archive.outputs.archive_name }}
zip_size: ${{ steps.calculate_size.outputs.zip_size }}
@@ -212,7 +216,7 @@ jobs:
fetch-depth: 1
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
uses: microsoft/setup-msbuild@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v5
@@ -227,12 +231,12 @@ jobs:
env:
DLASS_SENTRY_DSN: ${{ secrets.DLASS_SENTRY_DSN }}
run: |
msbuild /p:platform="AnyCPU" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
- name: Check if exe file is generated
id: check-exe
run: |
$exePath = "Ink Canvas/bin/Release/net472/InkCanvasForClass.exe"
$exePath = "Ink Canvas\bin\Release\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
if (Test-Path $exePath) {
echo "build_success=true" >> $env:GITHUB_OUTPUT
@@ -265,13 +269,22 @@ jobs:
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$archiveName = "InkCanvasForClass.CE.$version.zip"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$archiveName = "InkCanvasForClass.CE.$version$suffix.zip"
# 创建发布目录
New-Item -ItemType Directory -Path "release" -Force
# 复制发布文件
Copy-Item "Ink Canvas/bin/Release/net472/*" "release/" -Recurse -Force
# 复制发布文件(使用架构特定的路径)
Copy-Item "Ink Canvas\bin\Release\$architecture\net6.0-windows10.0.19041.0\*" "release/" -Recurse -Force
# 创建压缩包
Compress-Archive -Path "release/*" -DestinationPath $archiveName -Force
@@ -282,6 +295,7 @@ jobs:
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$architecture = "${{ matrix.architecture }}"
# 更新 ISS 文件中的版本信息
$issPath = "build/InkCanvasForClass CE.iss"
@@ -317,8 +331,17 @@ jobs:
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$setupFile = "InkCanvasForClass CE Setup.exe"
$newSetupName = "InkCanvasForClass.CE.$version.Setup.exe"
$newSetupName = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
if (Test-Path $setupFile) {
Rename-Item -Path $setupFile -NewName $newSetupName
@@ -331,7 +354,16 @@ jobs:
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$archiveName = "InkCanvasForClass.CE.$version.zip"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$archiveName = "InkCanvasForClass.CE.$version$suffix.zip"
# 获取文件大小(字节)
$fileSize = (Get-Item $archiveName).Length
@@ -343,7 +375,16 @@ jobs:
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$installerName = "InkCanvasForClass.CE.$version.Setup.exe"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$installerName = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
if (Test-Path $installerName) {
# 获取文件大小(字节)
@@ -354,14 +395,34 @@ jobs:
Write-Error "Installer file not found: $installerName"
}
- name: Upload Build Artifacts
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$zipFile = "InkCanvasForClass.CE.$version$suffix.zip"
$setupFile = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
echo "zip_file=$zipFile" >> $env:GITHUB_OUTPUT
echo "setup_file=$setupFile" >> $env:GITHUB_OUTPUT
id: get_file_names
- name: Upload Build Artifacts
if: steps.check-exe.outputs.build_success == 'true'
uses: actions/upload-artifact@v7
with:
name: build-files-${{ needs.prepare.outputs.version }}
name: build-files-${{ needs.prepare.outputs.version }}-${{ matrix.architecture }}
path: |
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
${{ steps.get_file_names.outputs.zip_file }}
${{ steps.get_file_names.outputs.setup_file }}
- name: Create Build Summary
if: always()
@@ -376,6 +437,7 @@ jobs:
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Release Type:** ${{ needs.prepare.outputs.is_prerelease == 'true' && 'Pre-release' || 'Release' }}" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
@@ -393,6 +455,7 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
@@ -411,7 +474,8 @@ jobs:
- name: Download Build Artifacts
uses: actions/download-artifact@v8
with:
name: build-files-${{ needs.prepare.outputs.version }}
pattern: build-files-${{ needs.prepare.outputs.version }}-*
merge-multiple: false
- name: Setup Python
uses: actions/setup-python@v6
@@ -419,11 +483,13 @@ jobs:
python-version: '3.14'
- name: Sign release artifacts with sigstore-python
uses: sigstore/gh-action-sigstore-python@v3.2.0
uses: sigstore/gh-action-sigstore-python@v3.3.0
with:
inputs: |
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.zip
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.exe
build-files-${{ needs.prepare.outputs.version }}-x86/*.zip
build-files-${{ needs.prepare.outputs.version }}-x86/*.exe
release-signing-artifacts: true
upload-signing-artifacts: true
env:
@@ -434,8 +500,8 @@ jobs:
with:
name: signed-files-${{ needs.prepare.outputs.version }}
path: |
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe.sigstore.json
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.sigstore.json
build-files-${{ needs.prepare.outputs.version }}-x86/*.sigstore.json
release:
needs: [prepare, build, sign]
@@ -450,7 +516,8 @@ jobs:
- name: Download Build Artifacts
uses: actions/download-artifact@v8
with:
name: build-files-${{ needs.prepare.outputs.version }}
pattern: build-files-${{ needs.prepare.outputs.version }}-*
merge-multiple: true
- name: Download Signed Artifacts (if exists)
uses: actions/download-artifact@v8
@@ -461,13 +528,13 @@ jobs:
- name: Create enhanced changelog with file table
id: enhanced_changelog
run: |
version="${{ needs.prepare.outputs.version }}"
version='${{ needs.prepare.outputs.version }}'
# 读取git-cliff生成的changelog内容
originalChangelog="${{ needs.prepare.outputs.changelog }}"
originalChangelog='${{ needs.prepare.outputs.changelog }}'
# 检查是否为预发布版本,如果是则添加警告提示
if [ "${{ needs.prepare.outputs.is_prerelease }}" = "true" ]; then
if [ '${{ needs.prepare.outputs.is_prerelease }}' = "true" ]; then
warningText=$'\n> [!CAUTION]\n'
warningText+=$'> **注意:此版本为预览或测试版**\n'
warningText+=$'> \n'
@@ -480,25 +547,45 @@ jobs:
fileTable+=$'| 文件名 | 大小 |\n'
fileTable+=$'|--------|------|\n'
# ZIP 文件信息
fileTable+=$'| InkCanvasForClass.CE.'"$version"
fileTable+=$'.zip | ${{ needs.build.outputs.zip_size }} bytes |\n'
# AnyCPU (x64) 架构文件
if [ -f "InkCanvasForClass.CE.$version-x64.zip" ]; then
zipSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip | '"$zipSize"' bytes |\n'
fi
# 安装包文件信息
installerSize="${{ needs.build.outputs.installer_size }}"
if [ -n "$installerSize" ]; then
if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe" ]; then
installerSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe | '"$installerSize"' bytes |\n'
fi
if [ -f "InkCanvasForClass.CE.$version-x64.zip.sigstore.json" ]; then
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip.sigstore.json")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
fi
if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json" ]; then
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n'
fi
# x86 架构文件
if [ -f "InkCanvasForClass.CE.$version.zip" ]; then
zipSize=$(wc -c < "InkCanvasForClass.CE.$version.zip")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip | '"$zipSize"' bytes |\n'
fi
if [ -f "InkCanvasForClass.CE.$version.Setup.exe" ]; then
installerSize=$(wc -c < "InkCanvasForClass.CE.$version.Setup.exe")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe | '"$installerSize"' bytes |\n'
fi
# 检查是否有签名文件
if [ -f "InkCanvasForClass.CE.$version.zip.sigstore.json" ]; then
sigstoreSize=$(stat -c%s "InkCanvasForClass.CE.$version.zip.sigstore.json")
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version.zip.sigstore.json")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
fi
# 检查安装程序签名文件
if [ -f "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json" ]; then
sigstoreSize=$(stat -c%s "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json")
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n'
fi
@@ -531,6 +618,10 @@ jobs:
draft: ${{ github.event.inputs.draft || false }}
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
files: |
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip.sigstore.json
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe.sigstore.json
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json
@@ -541,7 +632,7 @@ jobs:
post_release:
needs: [prepare, release]
if: success()
if: success() && github.event.inputs.draft != 'true'
runs-on: ubuntu-slim
permissions:
id-token: write
@@ -550,7 +641,8 @@ jobs:
- name: Download Build Artifacts
uses: actions/download-artifact@v8
with:
name: build-files-${{ needs.prepare.outputs.version }}
pattern: build-files-${{ needs.prepare.outputs.version }}-*
merge-multiple: true
- name: Get beta token
uses: octo-sts/action@main
@@ -580,18 +672,32 @@ jobs:
cd $REPO_DIR
IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}"
VERSION="${{ needs.prepare.outputs.version }}"
ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION.zip"
X64_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION-x64.zip"
X86_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION.zip"
if [ "$IS_PRERELEASE" == "true" ]; then
mkdir -p Beta
cp "$ZIP_FILE" Beta/
git add Beta/InkCanvasForClass.CE.$VERSION.zip
if [ -f "$X64_ZIP_FILE" ]; then
cp "$X64_ZIP_FILE" Beta/
git add Beta/InkCanvasForClass.CE.$VERSION-x64.zip
fi
if [ -f "$X86_ZIP_FILE" ]; then
cp "$X86_ZIP_FILE" Beta/
git add Beta/InkCanvasForClass.CE.$VERSION.zip
fi
git commit -m "Add $VERSION PreRelease"
else
mkdir -p Release Beta
cp "$ZIP_FILE" Release/
cp "$ZIP_FILE" Beta/
git add Release/InkCanvasForClass.CE.$VERSION.zip Beta/InkCanvasForClass.CE.$VERSION.zip
if [ -f "$X64_ZIP_FILE" ]; then
cp "$X64_ZIP_FILE" Release/
cp "$X64_ZIP_FILE" Beta/
git add Release/InkCanvasForClass.CE.$VERSION-x64.zip Beta/InkCanvasForClass.CE.$VERSION-x64.zip
fi
if [ -f "$X86_ZIP_FILE" ]; then
cp "$X86_ZIP_FILE" Release/
cp "$X86_ZIP_FILE" Beta/
git add Release/InkCanvasForClass.CE.$VERSION.zip Beta/InkCanvasForClass.CE.$VERSION.zip
fi
git commit -m "Add $VERSION Release"
fi
git push origin main
@@ -622,9 +728,11 @@ jobs:
draft: false
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
files: |
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
fail_on_unmatched_files: false
repository: "InkCanvasForClass/community-beta"
token: ${{ steps.octo-sts-beta.outputs.token }}
env:
GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }}
@@ -642,5 +750,5 @@ jobs:
/repos/InkCanvasForClass/community/contents/AutomaticUpdateVersionControl.txt \
-f message="Update AutomaticUpdateVersionControl.txt" \
-f content="$CONTENT" \
-f branch="beta" \
-f branch="net6" \
${SHA:+-f sha="$SHA"}
+2 -1
View File
@@ -429,4 +429,5 @@ FodyWeavers.xsd
# Telemetry DSN configuration file (contains sensitive information)
telemetry_dsn.txt
**/telemetry_dsn.txt
**/telemetry_dsn.txt
.trae/skills/migrate-toggle-switch/SKILL.md
+57 -13
View File
@@ -1,10 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33530.505
# Visual Studio Version 18
VisualStudioVersion = 18.4.11626.88 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InkCanvasForClass", "Ink Canvas\InkCanvasForClass.csproj", "{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.PluginSdk", "InkCanvas.PluginSdk\InkCanvas.PluginSdk.csproj", "{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.Controls", "InkCanvas.Controls\InkCanvas.Controls.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -23,22 +27,62 @@ Global
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM.ActiveCfg = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM.Build.0 = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.ActiveCfg = Debug|ARM64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.Build.0 = Debug|ARM64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.Build.0 = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x64.ActiveCfg = Debug|x64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x64.Build.0 = Debug|x64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.ActiveCfg = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.Build.0 = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.ActiveCfg = Debug|x86
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.Build.0 = Debug|x86
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|Any CPU.Build.0 = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM.Build.0 = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.ActiveCfg = Release|ARM64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.Build.0 = Release|ARM64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.ActiveCfg = Release|x64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.Build.0 = Release|x64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.ActiveCfg = Release|x86
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.Build.0 = Release|x86
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.Build.0 = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.Build.0 = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM64.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x64.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x64.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x86.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x86.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|Any CPU.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM64.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM64.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x64.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x64.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x86.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x86.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+34 -3
View File
@@ -1,8 +1,9 @@
<Application x:Class="Ink_Canvas.App"
<Application x:Class="Ink_Canvas.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Ink_Canvas"
xmlns:tb="http://www.hardcodet.net/taskbar"
xmlns:props="clr-namespace:Ink_Canvas.Properties"
xmlns:tb="clr-namespace:H.NotifyIcon;assembly=H.NotifyIcon.Wpf"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
>
@@ -32,6 +33,36 @@
</Image>
</MenuItem.Icon>
</MenuItem>
<MenuItem Name="TempShowMainWindowTrayIconMenuItem" Click="TempShowMainWindowTrayIconMenuItem_Clicked">
<MenuItem.Header>
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="{x:Static props:Strings.Tray_TempShowMainWindow}" />
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="28" Height="28" Margin="-2">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="#27272a" Geometry="F0 M24,24z M0,0z M5,6C4.73478,6 4.48043,6.10536 4.29289,6.29289 4.10536,6.48043 4,6.73478 4,7L4,17C4,17.2652 4.10536,17.5196 4.29289,17.7071 4.48043,17.8946 4.73478,18 5,18L19,18C19.2652,18 19.5196,17.8946 19.7071,17.7071 19.8946,17.5196 20,17.2652 20,17L20,7C20,6.73478 19.8946,6.48043 19.7071,6.29289 19.5196,6.10536 19.2652,6 19,6L5,6z M2.87868,4.87868C3.44129,4.31607,4.20435,4,5,4L19,4C19.7957,4 20.5587,4.31607 21.1213,4.87868 21.6839,5.44129 22,6.20435 22,7L22,17C22,17.7957 21.6839,18.5587 21.1213,19.1213 20.5587,19.6839 19.7957,20 19,20L5,20C4.20435,20 3.44129,19.6839 2.87868,19.1213 2.31607,18.5587 2,17.7956 2,17L2,7C2,6.20435,2.31607,5.44129,2.87868,4.87868z M5,8C5,7.44772,5.44772,7,6,7L6.01,7C6.56228,7 7.01,7.44772 7.01,8 7.01,8.55228 6.56228,9 6.01,9L6,9C5.44772,9,5,8.55228,5,8z M9,7C8.44772,7 8,7.44772 8,8 8,8.55228 8.44772,9 9,9L9.01,9C9.56228,9 10.01,8.55228 10.01,8 10.01,7.44772 9.56228,7 9.01,7L9,7z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</MenuItem.Icon>
</MenuItem>
<MenuItem Name="OpenSettingsTrayIconMenuItem" Click="OpenSettingsTrayIconMenuItem_Clicked">
<MenuItem.Header>
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="{x:Static props:Strings.Tray_OpenSettings}" />
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="28" Height="28" Margin="-2" Source="/Resources/Icons-Fluent/ic_fluent_settings_24_regular.png" />
</MenuItem.Icon>
</MenuItem>
<Separator Margin="0,3" />
<MenuItem Name="DisableAllHotkeysMenuItem" Click="DisableAllHotkeysMenuItem_Clicked">
<MenuItem.Header>
@@ -232,7 +263,7 @@
ToolTipText="InkCanvasForClass"
ContextMenu="{StaticResource SysTrayMenu}"
IconSource="/Resources/icc.ico"/>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources/>
<ui:XamlControlsResources />
<ResourceDictionary Source="Resources/SeewoImageDictionary.xaml"/>
+301 -133
View File
@@ -1,10 +1,14 @@
using Hardcodet.Wpf.TaskbarNotification;
using H.NotifyIcon;
using Ink_Canvas.Helpers;
using Ink_Canvas.Plugins;
using Ink_Canvas.Properties;
using iNKORE.UI.WPF.Modern.Controls;
using Microsoft.Win32;
using Newtonsoft.Json;
using Sentry;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -15,13 +19,12 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Threading;
using Application = System.Windows.Application;
using MessageBox = System.Windows.MessageBox;
using Ink_Canvas.Properties;
using SplashScreen = Ink_Canvas.Windows.SplashScreen;
using Timer = System.Threading.Timer;
using Sentry;
namespace Ink_Canvas
{
@@ -32,6 +35,20 @@ namespace Ink_Canvas
{
Mutex mutex;
public void ReleaseMutexForRestart()
{
try
{
if (mutex != null)
{
mutex.ReleaseMutex();
mutex.Dispose();
mutex = null;
}
}
catch { }
}
public static string[] StartArgs;
public static string RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
@@ -61,9 +78,16 @@ namespace Ink_Canvas
private static string lastErrorMessage = string.Empty;
// 新增:是否已初始化崩溃监听器
private static bool crashListenersInitialized;
private IntPtr processDestroyHook = IntPtr.Zero;
private IntPtr monitoredMainWindowHandle = IntPtr.Zero;
private bool mainWindowDestroyedLogged;
private WinEventDelegate processDestroyHookCallback;
// 新增:启动画面相关
private static SplashScreen _splashScreen;
private static bool _isSplashScreenShown = false;
private static System.Resources.ResourceSet _pendingLocalizedResourceSet;
private static readonly Stopwatch startupStopwatch = new Stopwatch();
private static readonly Stopwatch splashStopwatch = new Stopwatch();
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int SetCurrentProcessExplicitAppUserModelID(string appId);
@@ -192,15 +216,13 @@ namespace Ink_Canvas
// 尝试注册Windows关闭消息监听
SetConsoleCtrlHandler(ConsoleCtrlHandler, true);
// 如果系统支持,添加Windows Management Instrumentation监听器
try
{
// 使用反射动态加载和调用WMI
TrySetupWmiMonitoring();
TrySetupTerminationMonitoring();
}
catch (Exception wmiEx)
catch (Exception monitorEx)
{
LogHelper.WriteLogToFile($"设置WMI进程监控失败: {wmiEx.Message}", LogHelper.LogType.Warning);
LogHelper.WriteLogToFile($"设置终止监控失败: {monitorEx.Message}", LogHelper.LogType.Warning);
}
crashListenersInitialized = true;
@@ -212,80 +234,114 @@ namespace Ink_Canvas
}
}
// 动态加载WMI监控
private void TrySetupWmiMonitoring()
private void TrySetupTerminationMonitoring()
{
try
{
// 检查System.Management程序集是否可用
var assemblyName = "System.Management, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
var assembly = Assembly.Load(assemblyName);
if (assembly == null)
{
LogHelper.WriteLogToFile("未找到System.Management程序集,跳过WMI监控", LogHelper.LogType.Warning);
return;
}
processDestroyHookCallback = OnWinEventMainWindowDestroyed;
// 使用反射创建WMI查询
var watcherType = assembly.GetType("System.Management.ManagementEventWatcher");
if (watcherType == null)
{
LogHelper.WriteLogToFile("未找到ManagementEventWatcher类型,跳过WMI监控", LogHelper.LogType.Warning);
return;
}
// 构建WMI查询字符串
string queryString = $"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.ProcessId = {currentProcessId}";
// 创建ManagementEventWatcher实例
object watcher = Activator.CreateInstance(watcherType, queryString);
// 获取EventArrived事件信息
var eventInfo = watcherType.GetEvent("EventArrived");
if (eventInfo == null)
{
LogHelper.WriteLogToFile("未找到EventArrived事件,跳过WMI监控", LogHelper.LogType.Warning);
return;
}
// 创建委托并订阅事件
Type delegateType = eventInfo.EventHandlerType;
var handler = Delegate.CreateDelegate(delegateType, this, GetType().GetMethod("WmiEventHandler", BindingFlags.NonPublic | BindingFlags.Instance));
eventInfo.AddEventHandler(watcher, handler);
// 启动监听
var startMethod = watcherType.GetMethod("Start");
startMethod.Invoke(watcher, null);
LogHelper.WriteLogToFile("已成功启动WMI进程监控");
// 等主窗口句柄可用后再开始监听
Dispatcher.BeginInvoke(new Action(BindMainWindowLifecycle), DispatcherPriority.ApplicationIdle);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"动态加载WMI监控失败: {ex.Message}", LogHelper.LogType.Warning);
LogHelper.WriteLogToFile($"初始化终止监控失败: {ex.GetType().FullName}: {ex.Message}", LogHelper.LogType.Warning);
}
}
// WMI事件处理方法
private void WmiEventHandler(object sender, EventArgs e)
private void BindMainWindowLifecycle()
{
try
{
// 尝试从事件参数中提取信息
dynamic eventArgs = e;
dynamic newEvent = eventArgs.NewEvent;
if (newEvent != null)
if (Current?.MainWindow == null)
{
dynamic targetInstance = newEvent["TargetInstance"];
if (targetInstance != null)
{
string processName = targetInstance["Name"]?.ToString() ?? "未知进程";
WriteCrashLog($"WMI检测到进程{processName}(ID:{currentProcessId})已终止");
}
return;
}
Current.MainWindow.SourceInitialized -= MainWindow_SourceInitialized;
Current.MainWindow.SourceInitialized += MainWindow_SourceInitialized;
}
catch (Exception)
{
}
}
private void MainWindow_SourceInitialized(object sender, EventArgs e)
{
try
{
if (!(sender is Window window))
{
return;
}
monitoredMainWindowHandle = new WindowInteropHelper(window).Handle;
if (monitoredMainWindowHandle == IntPtr.Zero)
{
return;
}
RegisterMainWindowDestroyHook();
}
catch (Exception)
{
}
}
private void RegisterMainWindowDestroyHook()
{
if (processDestroyHook != IntPtr.Zero || monitoredMainWindowHandle == IntPtr.Zero)
{
return;
}
processDestroyHook = SetWinEventHook(
EVENT_OBJECT_DESTROY,
EVENT_OBJECT_DESTROY,
IntPtr.Zero,
processDestroyHookCallback,
(uint)currentProcessId,
0,
WINEVENT_OUTOFCONTEXT);
if (processDestroyHook == IntPtr.Zero)
{
return;
}
}
private void OnWinEventMainWindowDestroyed(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
if (eventType != EVENT_OBJECT_DESTROY || mainWindowDestroyedLogged)
{
return;
}
if (idObject != OBJID_WINDOW || idChild != CHILDID_SELF)
{
return;
}
if (hwnd != monitoredMainWindowHandle || hwnd == IntPtr.Zero)
{
return;
}
mainWindowDestroyedLogged = true;
}
private void CleanupTerminationMonitoring()
{
try
{
if (processDestroyHook != IntPtr.Zero)
{
UnhookWinEvent(processDestroyHook);
processDestroyHook = IntPtr.Zero;
}
}
catch (Exception ex)
catch
{
LogHelper.WriteLogToFile($"处理WMI事件时出错: {ex.Message}", LogHelper.LogType.Warning);
}
}
@@ -294,6 +350,19 @@ namespace Ink_Canvas
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate handler, bool add);
private delegate bool ConsoleCtrlDelegate(int ctrlType);
private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
private const uint EVENT_OBJECT_DESTROY = 0x8001;
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
private const int OBJID_WINDOW = 0;
private const int CHILDID_SELF = 0;
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
private static bool ConsoleCtrlHandler(int ctrlType)
{
@@ -428,6 +497,7 @@ namespace Ink_Canvas
// 处理进程退出事件
private void CurrentDomain_ProcessExit(object sender, EventArgs e)
{
CleanupTerminationMonitoring();
TimeSpan runDuration = DateTime.Now - appStartTime;
string durationText = FormatTimeSpan(runDuration);
WriteCrashLog($"应用程序退出,运行时长: {durationText}");
@@ -476,6 +546,7 @@ namespace Ink_Canvas
_splashScreen.Show();
_isSplashScreenShown = true;
splashScreenStartTime = DateTime.Now;
splashStopwatch.Restart();
LogHelper.WriteLogToFile("启动画面已显示");
}
catch (Exception ex)
@@ -695,20 +766,25 @@ namespace Ink_Canvas
{
appStartTime = DateTime.Now;
appStartupStartTime = DateTime.Now;
startupStopwatch.Restart();
TryApplyPreferredLanguageFromSettings();
_pendingLocalizedResourceSet = Strings.ResourceManager.GetResourceSet(CultureInfo.CurrentUICulture, true, true);
// 根据设置决定是否显示启动画面
if (ShouldShowSplashScreen() && !IsLaunchByFileOrUri(e.Args))
{
await Task.Delay(100);
ShowSplashScreen();
SetSplashMessage(Strings.GetString("Splash_Starting"));
SetSplashProgress(20);
await Task.Delay(500);
// 强制刷新UI,确保启动画面显示
Application.Current.Dispatcher.Invoke(() => { }, DispatcherPriority.Render);
}
await Task.Delay(500);
await Task.Delay(100);
RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
LogHelper.NewLog(string.Format("Ink Canvas Starting (Version: {0})", Assembly.GetExecutingAssembly().GetName().Version));
@@ -744,15 +820,6 @@ namespace Ink_Canvas
{
SetSplashMessage("正在初始化组件...");
SetSplashProgress(40);
await Task.Delay(500);
}
try
{
IACoreDllExtractor.ExtractIACoreDlls();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放IACore DLL时出错: {ex.Message}", LogHelper.LogType.Error);
}
// 释放UIAccess DLL
@@ -760,43 +827,13 @@ namespace Ink_Canvas
{
SetSplashMessage("正在初始化组件...");
SetSplashProgress(50);
await Task.Delay(300);
}
try
{
UIAccessDllExtractor.ExtractUIAccessDlls();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
}
// 记录应用启动(设备标识符)
if (_isSplashScreenShown)
{
SetSplashMessage("正在加载配置...");
SetSplashProgress(60);
await Task.Delay(500);
}
DeviceIdentifier.RecordAppLaunch();
try
{
var systemVersion = DeviceIdentifier.GetSystemVersion();
if (!string.IsNullOrWhiteSpace(systemVersion))
{
SentrySdk.ConfigureScope(scope =>
{
scope.SetTag("system_version", systemVersion);
});
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"App | 初始化系统版本遥测标签失败: {ex.Message}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile($"App | 设备ID: {DeviceIdentifier.GetDeviceId()}");
LogHelper.WriteLogToFile($"App | 使用频率: {DeviceIdentifier.GetUsageFrequency()}");
LogHelper.WriteLogToFile($"App | 更新优先级: {DeviceIdentifier.GetUpdatePriority()}");
// 处理更新模式启动
bool isUpdateMode = AutoUpdateHelper.HandleUpdateModeStartup(e.Args);
@@ -1069,26 +1106,48 @@ namespace Ink_Canvas
{
SetSplashMessage("正在初始化主界面...");
SetSplashProgress(80);
await Task.Delay(500);
}
var mainWindow = new MainWindow();
MainWindow = mainWindow;
// 注册 InkCanvas 服务供插件使用
try
{
var inkCanvasService = new Plugins.InkCanvasService(mainWindow);
Plugins.PluginManager.Instance.RegisterService<Plugins.IInkCanvasService>(inkCanvasService);
LogHelper.WriteLogToFile("InkCanvasService registered for plugins");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"Failed to register InkCanvasService: {ex.Message}", LogHelper.LogType.Error);
}
try
{
var appRestartService = new Plugins.AppRestartService();
Plugins.PluginManager.Instance.RegisterService<Plugins.IAppRestartService>(appRestartService);
LogHelper.WriteLogToFile("AppRestartService registered for plugins");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"Failed to register AppRestartService: {ex.Message}", LogHelper.LogType.Error);
}
// 主窗口加载完成后关闭启动画面
mainWindow.Loaded += (s, args) =>
{
isStartupComplete = true;
startupCompleteHeartbeat = DateTime.Now;
if (_isSplashScreenShown && splashScreenStartTime != DateTime.MinValue)
if (_isSplashScreenShown && splashStopwatch.IsRunning)
{
LogHelper.WriteLogToFile($"启动完成心跳已记录,启动画面显示时长: {(startupCompleteHeartbeat - splashScreenStartTime).TotalSeconds:F2}秒");
LogHelper.WriteLogToFile($"启动完成心跳已记录,启动画面显示时长: {splashStopwatch.Elapsed.TotalSeconds:F2}秒");
}
else
{
LogHelper.WriteLogToFile($"启动完成心跳已记录");
}
LogHelper.WriteLogToFile($"启动时长: {(startupCompleteHeartbeat - appStartupStartTime).TotalSeconds:F2}秒");
LogHelper.WriteLogToFile($"启动时长: {startupStopwatch.Elapsed.TotalSeconds:F2}秒");
if (_isSplashScreenShown)
{
SetSplashMessage("完成初始化...");
@@ -1110,6 +1169,19 @@ namespace Ink_Canvas
};
mainWindow.Show();
_ = Task.Run(async () =>
{
await Task.Delay(600);
Dispatcher.Invoke(() => _taskbar?.ForceCreate());
});
Dispatcher.BeginInvoke(new Action(() =>
{
if (_pendingLocalizedResourceSet != null)
{
LoadLocalizedResources(_pendingLocalizedResourceSet);
_pendingLocalizedResourceSet = null;
}
}), DispatcherPriority.ApplicationIdle);
// 处理启动时的URI参数
string startupUriArg = e.Args.FirstOrDefault(a => a.StartsWith("icc:", StringComparison.OrdinalIgnoreCase));
@@ -1126,40 +1198,123 @@ namespace Ink_Canvas
});
}
// 注册.icstk文件关联
_ = RunDeferredStartupTasksAsync();
}
private async Task RunDeferredStartupTasksAsync()
{
try
{
LogHelper.WriteLogToFile("开始注册.icstk文件关联");
FileAssociationManager.RegisterFileAssociation();
FileAssociationManager.ShowFileAssociationStatus();
await Task.Delay(1200);
try
{
IACoreDllExtractor.ExtractIACoreDlls();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放IACore DLL时出错: {ex.Message}", LogHelper.LogType.Error);
}
try
{
LogHelper.WriteLogToFile("开始注册.icstk文件关联");
FileAssociationManager.RegisterFileAssociation();
FileAssociationManager.ShowFileAssociationStatus();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"注册文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
}
try
{
LogHelper.WriteLogToFile("启动IPC监听器");
FileAssociationManager.StartIpcListener();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动IPC监听器时出错: {ex.Message}", LogHelper.LogType.Error);
}
try
{
LogHelper.WriteLogToFile("初始化上传帮助类");
Helpers.UploadHelper.Initialize();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化上传帮助类时出错: {ex.Message}", LogHelper.LogType.Error);
}
try
{
LogHelper.WriteLogToFile("开始加载插件");
await PluginManager.Instance.LoadAllAsync();
LogHelper.WriteLogToFile(string.Format("插件加载完成,共加载 {0} 个插件", PluginManager.Instance.Plugins.Count));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile(string.Format("加载插件时出错: {0}", ex.Message), LogHelper.LogType.Error);
}
try
{
await Task.Delay(1500);
DeviceIdentifier.RecordAppLaunch();
var systemVersion = DeviceIdentifier.GetSystemVersion();
if (!string.IsNullOrWhiteSpace(systemVersion))
{
SentrySdk.ConfigureScope(scope =>
{
scope.SetTag("system_version", systemVersion);
});
}
LogHelper.WriteLogToFile($"App | 设备ID: {DeviceIdentifier.GetDeviceId()}");
LogHelper.WriteLogToFile($"App | 使用频率: {DeviceIdentifier.GetUsageFrequency()}");
LogHelper.WriteLogToFile($"App | 更新优先级: {DeviceIdentifier.GetUpdatePriority()}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"App | 初始化设备统计与遥测标签失败: {ex.Message}", LogHelper.LogType.Warning);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"注册文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"启动阶段任务执行失败: {ex.Message}", LogHelper.LogType.Error);
}
}
// 启动IPC监听器
private void TryApplyPreferredLanguageFromSettings()
{
try
{
LogHelper.WriteLogToFile("启动IPC监听器");
FileAssociationManager.StartIpcListener();
var settingsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configs", "Settings.json");
if (!File.Exists(settingsPath)) return;
var json = File.ReadAllText(settingsPath);
dynamic obj = JsonConvert.DeserializeObject(json);
string preferredLanguage = obj?["appearance"]?["language"]?.ToString();
if (!string.IsNullOrWhiteSpace(preferredLanguage))
{
LocalizationHelper.TrySetCulture(preferredLanguage);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动IPC监听器时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"启动时预加载语言失败: {ex.Message}", LogHelper.LogType.Error);
}
}
// 初始化上传帮助类
try
private void LoadLocalizedResources(System.Resources.ResourceSet resourceSet)
{
foreach (System.Collections.DictionaryEntry entry in resourceSet)
{
LogHelper.WriteLogToFile("初始化上传帮助类");
Helpers.UploadHelper.Initialize();
if (entry.Key is string key && entry.Value is string value)
Current.Resources[key] = value;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化上传帮助类时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
@@ -1223,8 +1378,8 @@ namespace Ink_Canvas
if (!isStartupComplete && appStartupStartTime != DateTime.MinValue)
{
DateTime startTime = _isSplashScreenShown && splashScreenStartTime != DateTime.MinValue
? splashScreenStartTime
DateTime startTime = _isSplashScreenShown && splashScreenStartTime != DateTime.MinValue
? splashScreenStartTime
: appStartupStartTime;
TimeSpan elapsedSinceStart = DateTime.Now - startTime;
if (elapsedSinceStart.TotalMinutes >= 2)
@@ -1405,7 +1560,7 @@ namespace Ink_Canvas
string assemblyLocation = Assembly.GetExecutingAssembly().Location;
string currentDir = Path.GetDirectoryName(assemblyLocation);
for (int i = 0; i < 5; i++)
{
string dsnFilePath = Path.Combine(currentDir, "telemetry_dsn.txt");
@@ -1417,7 +1572,7 @@ namespace Ink_Canvas
return dsn;
}
}
DirectoryInfo parentDir = Directory.GetParent(currentDir);
if (parentDir == null)
{
@@ -1436,6 +1591,19 @@ namespace Ink_Canvas
private void App_Exit(object sender, ExitEventArgs e)
{
CleanupTerminationMonitoring();
// 卸载所有插件
try
{
LogHelper.WriteLogToFile("正在卸载插件...");
PluginManager.Instance.UnloadAll();
LogHelper.WriteLogToFile("插件卸载完成");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"卸载插件时出错: {ex.Message}", LogHelper.LogType.Error);
}
// 仅在软件内主动退出时关闭看门狗,并写入退出信号
try
{
+3 -2
View File
@@ -2,6 +2,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows;
[assembly: System.Runtime.Versioning.SupportedOSPlatform("windows")]
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
@@ -43,5 +44,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.18.8")]
[assembly: AssemblyFileVersion("1.7.18.8")]
[assembly: AssemblyVersion("1.7.18.10")]
[assembly: AssemblyFileVersion("1.7.18.10")]
+141
View File
@@ -0,0 +1,141 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace Ink_Canvas.Controls
{
[TemplatePart(Name = PartCloseImage, Type = typeof(UIElement))]
[TemplatePart(Name = PartAnimationRoot, Type = typeof(UIElement))]
public class BoardMenuFrame : ContentControl
{
private const string PartCloseImage = "PART_CloseImage";
private const string PartAnimationRoot = "PART_AnimationRoot";
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(object), typeof(BoardMenuFrame), new PropertyMetadata(null));
public static readonly DependencyProperty TitleFontSizeProperty =
DependencyProperty.Register(nameof(TitleFontSize), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(11d));
public static readonly DependencyProperty HeaderHeightProperty =
DependencyProperty.Register(nameof(HeaderHeight), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(48d));
public static readonly DependencyProperty PanelCornerRadiusProperty =
DependencyProperty.Register(nameof(PanelCornerRadius), typeof(CornerRadius), typeof(BoardMenuFrame), new PropertyMetadata(new CornerRadius(5)));
public static readonly DependencyProperty HeaderCornerRadiusProperty =
DependencyProperty.Register(nameof(HeaderCornerRadius), typeof(CornerRadius), typeof(BoardMenuFrame), new PropertyMetadata(new CornerRadius(6, 6, 0, 0)));
public static readonly DependencyProperty PanelBackgroundProperty =
DependencyProperty.Register(nameof(PanelBackground), typeof(Brush), typeof(BoardMenuFrame), new PropertyMetadata(null));
public static readonly DependencyProperty HeaderBackgroundProperty =
DependencyProperty.Register(nameof(HeaderBackground), typeof(Brush), typeof(BoardMenuFrame),
new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2563eb"))));
public static readonly DependencyProperty HeaderBorderBrushProperty =
DependencyProperty.Register(nameof(HeaderBorderBrush), typeof(Brush), typeof(BoardMenuFrame),
new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1e3a8a"))));
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register(nameof(IsOpen), typeof(bool), typeof(BoardMenuFrame), new PropertyMetadata(false));
public static readonly DependencyProperty PlacementTargetProperty =
DependencyProperty.Register(nameof(PlacementTarget), typeof(UIElement), typeof(BoardMenuFrame), new PropertyMetadata(null));
public static readonly DependencyProperty PlacementProperty =
DependencyProperty.Register(nameof(Placement), typeof(PlacementMode), typeof(BoardMenuFrame), new PropertyMetadata(PlacementMode.Custom));
public static readonly DependencyProperty CustomPopupPlacementCallbackProperty =
DependencyProperty.Register(nameof(CustomPopupPlacementCallback), typeof(CustomPopupPlacementCallback), typeof(BoardMenuFrame),
new PropertyMetadata((CustomPopupPlacementCallback)PlaceCenteredAbove));
private static CustomPopupPlacement[] PlaceCenteredAbove(Size popupSize, Size targetSize, Point offset)
{
return new[]
{
new CustomPopupPlacement(
new Point((targetSize.Width - popupSize.Width) / 2 + offset.X,
-popupSize.Height + offset.Y),
PopupPrimaryAxis.Horizontal),
new CustomPopupPlacement(
new Point((targetSize.Width - popupSize.Width) / 2 + offset.X,
targetSize.Height - offset.Y),
PopupPrimaryAxis.Horizontal)
};
}
public static readonly DependencyProperty PopupHorizontalOffsetProperty =
DependencyProperty.Register(nameof(PopupHorizontalOffset), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(0d));
public static readonly DependencyProperty PopupVerticalOffsetProperty =
DependencyProperty.Register(nameof(PopupVerticalOffset), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(-4d));
public object Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
public double TitleFontSize { get => (double)GetValue(TitleFontSizeProperty); set => SetValue(TitleFontSizeProperty, value); }
public double HeaderHeight { get => (double)GetValue(HeaderHeightProperty); set => SetValue(HeaderHeightProperty, value); }
public CornerRadius PanelCornerRadius { get => (CornerRadius)GetValue(PanelCornerRadiusProperty); set => SetValue(PanelCornerRadiusProperty, value); }
public CornerRadius HeaderCornerRadius { get => (CornerRadius)GetValue(HeaderCornerRadiusProperty); set => SetValue(HeaderCornerRadiusProperty, value); }
public Brush PanelBackground { get => (Brush)GetValue(PanelBackgroundProperty); set => SetValue(PanelBackgroundProperty, value); }
public Brush HeaderBackground { get => (Brush)GetValue(HeaderBackgroundProperty); set => SetValue(HeaderBackgroundProperty, value); }
public Brush HeaderBorderBrush { get => (Brush)GetValue(HeaderBorderBrushProperty); set => SetValue(HeaderBorderBrushProperty, value); }
public bool IsOpen { get => (bool)GetValue(IsOpenProperty); set => SetValue(IsOpenProperty, value); }
public UIElement PlacementTarget { get => (UIElement)GetValue(PlacementTargetProperty); set => SetValue(PlacementTargetProperty, value); }
public PlacementMode Placement { get => (PlacementMode)GetValue(PlacementProperty); set => SetValue(PlacementProperty, value); }
public CustomPopupPlacementCallback CustomPopupPlacementCallback
{
get => (CustomPopupPlacementCallback)GetValue(CustomPopupPlacementCallbackProperty);
set => SetValue(CustomPopupPlacementCallbackProperty, value);
}
public double PopupHorizontalOffset { get => (double)GetValue(PopupHorizontalOffsetProperty); set => SetValue(PopupHorizontalOffsetProperty, value); }
public double PopupVerticalOffset { get => (double)GetValue(PopupVerticalOffsetProperty); set => SetValue(PopupVerticalOffsetProperty, value); }
public event MouseButtonEventHandler CloseMouseDown;
public event MouseButtonEventHandler CloseMouseUp;
public UIElement AnimationTarget { get; private set; }
private UIElement _closeImage;
static BoardMenuFrame()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(BoardMenuFrame), new FrameworkPropertyMetadata(typeof(BoardMenuFrame)));
VisibilityProperty.OverrideMetadata(typeof(BoardMenuFrame),
new FrameworkPropertyMetadata(Visibility.Collapsed, OnVisibilityChanged));
}
private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((BoardMenuFrame)d).IsOpen = (Visibility)e.NewValue == Visibility.Visible;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (_closeImage != null)
{
_closeImage.MouseDown -= CloseImage_MouseDown;
_closeImage.MouseUp -= CloseImage_MouseUp;
}
_closeImage = GetTemplateChild(PartCloseImage) as UIElement;
if (_closeImage != null)
{
_closeImage.MouseDown += CloseImage_MouseDown;
_closeImage.MouseUp += CloseImage_MouseUp;
}
AnimationTarget = GetTemplateChild(PartAnimationRoot) as UIElement;
}
private void CloseImage_MouseDown(object sender, MouseButtonEventArgs e)
{
CloseMouseDown?.Invoke(sender, e);
}
private void CloseImage_MouseUp(object sender, MouseButtonEventArgs e)
{
CloseMouseUp?.Invoke(sender, e);
}
}
}
+145
View File
@@ -0,0 +1,145 @@
using Ink_Canvas.Helpers;
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Ink_Canvas.Controls
{
/// <summary>
/// 画布上的多页 PDF:仅显示当前页;翻页与页码由主窗口 PDF 侧栏控制(无 XAML 文件)。
/// </summary>
public class PdfEmbeddedView : UserControl
{
private readonly Image _pageImage;
private string _pdfPath;
private uint _pageCount;
private uint _currentIndex;
private bool _compressLargePictures;
private bool _isPagingBusy;
private bool _layoutSizeCommitted;
/// <summary>页码或可翻页状态变化(用于更新侧栏)。</summary>
public event EventHandler PageNavigationStateChanged;
public PdfEmbeddedView()
{
MinWidth = 80;
MinHeight = 60;
var grid = new Grid { ClipToBounds = true };
_pageImage = new Image
{
Stretch = Stretch.Uniform,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
grid.Children.Add(_pageImage);
Content = grid;
}
/// <summary>
/// 初始化并显示指定页;由 MainWindow 在 UI 线程创建后调用。
/// </summary>
/// <param name="initialPageIndex">从 0 开始的页码,超出范围时夹紧到合法区间。</param>
public async Task InitializeAsync(string pdfFilePath, uint pageCount, bool compressLargePictures, uint initialPageIndex = 0)
{
_pdfPath = pdfFilePath ?? throw new ArgumentNullException(nameof(pdfFilePath));
_pageCount = pageCount;
_compressLargePictures = compressLargePictures;
if (_pageCount == 0)
_currentIndex = 0;
else
_currentIndex = initialPageIndex >= _pageCount ? _pageCount - 1 : initialPageIndex;
await ShowPageAsync(_currentIndex);
}
public string PdfPath => _pdfPath;
public uint PageCount => _pageCount;
public uint CurrentPageIndex => _currentIndex;
public string PageLabelText => _pageCount == 0 ? "" : $"{_currentIndex + 1} / {_pageCount}";
public bool CanGoPrevious => !_isPagingBusy && _pageCount > 1 && _currentIndex > 0;
public bool CanGoNext => !_isPagingBusy && _pageCount > 1 && _currentIndex < _pageCount - 1;
public async Task GoToPreviousPageAsync()
{
await GoRelativeAsync(-1);
}
public async Task GoToNextPageAsync()
{
await GoRelativeAsync(1);
}
private void NotifyPageNavigationStateChanged()
{
PageNavigationStateChanged?.Invoke(this, EventArgs.Empty);
}
private async Task GoRelativeAsync(int delta)
{
if (_isPagingBusy || _pageCount <= 1)
return;
int next = (int)_currentIndex + delta;
if (next < 0 || next >= _pageCount)
return;
_currentIndex = (uint)next;
await ShowPageAsync(_currentIndex);
}
private async Task ShowPageAsync(uint pageIndex)
{
_isPagingBusy = true;
NotifyPageNavigationStateChanged();
try
{
BitmapSource raw = await PdfWinRtHelper.RenderPageToBitmapSourceAsync(_pdfPath, pageIndex);
if (raw == null)
return;
BitmapSource display = ApplyCompressionIfNeeded(raw);
_pageImage.Source = display;
if (!_layoutSizeCommitted)
{
bool callerSized = !double.IsNaN(Width) && Width > 0 && !double.IsNaN(Height) && Height > 0;
if (!callerSized)
{
Width = display.PixelWidth;
Height = display.PixelHeight;
}
_layoutSizeCommitted = true;
}
}
finally
{
_isPagingBusy = false;
NotifyPageNavigationStateChanged();
}
}
private BitmapSource ApplyCompressionIfNeeded(BitmapSource rendered)
{
int width = rendered.PixelWidth;
int height = rendered.PixelHeight;
if (_compressLargePictures && (width > 1920 || height > 1080))
{
double scaleX = 1920.0 / width;
double scaleY = 1080.0 / height;
double scale = Math.Min(scaleX, scaleY);
return new TransformedBitmap(rendered, new ScaleTransform(scale, scale));
}
return rendered;
}
}
}
+136
View File
@@ -0,0 +1,136 @@
<UserControl x:Class="Ink_Canvas.Controls.PptNavBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
xmlns:local="clr-namespace:Ink_Canvas.Controls"
mc:Ignorable="d"
d:DesignHeight="50" d:DesignWidth="200">
<UserControl.Resources>
<SolidColorBrush x:Key="PptNavBarItemForeground" Color="#71717a"/>
<DataTemplate x:Key="PptPreviewItemTemplate">
<Grid Margin="2,4">
<Border CornerRadius="8" Background="Transparent" Padding="6">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"
FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource PptNavBarItemForeground}"
Text="{Binding SlideNumber}"/>
<Image Grid.Column="1" Source="{Binding Thumbnail}" Stretch="Uniform"/>
</Grid>
</Border>
</Grid>
</DataTemplate>
<Style x:Key="PptPreviewItemContainerStyle" TargetType="ListBoxItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="Bd" CornerRadius="8" Background="Transparent"
BorderBrush="Transparent" BorderThickness="2">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1affffff"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="#66CCFF"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Border x:Name="RootBorder" BorderThickness="1" BorderBrush="#a1a1aa"
Background="#f4f4f5" Opacity="1" CornerRadius="6">
<Border.Effect>
<DropShadowEffect Color="#000000" BlurRadius="14" ShadowDepth="2" Opacity="0.16"/>
</Border.Effect>
<DockPanel x:Name="LayoutRoot" LastChildFill="True">
<ListBox x:Name="PreviewList"
Visibility="Collapsed"
Background="Transparent" BorderThickness="0"
Padding="6,8,6,4"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemTemplate="{StaticResource PptPreviewItemTemplate}"
ItemContainerStyle="{StaticResource PptPreviewItemContainerStyle}"
MouseUp="PreviewList_MouseUp"/>
<ikw:SimpleStackPanel x:Name="ButtonRow" Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Border x:Name="PreviousButtonBorder" Width="50" Height="50"
MouseDown="PreviousButton_MouseDown"
MouseUp="PreviousButton_MouseUp"
MouseLeave="PreviousButton_MouseLeave"
CornerRadius="5 5 0 0" Background="Transparent">
<Grid>
<Border x:Name="PreviousButtonFeedbackBorder" Margin="3" CornerRadius="5"
Background="#18181b" Opacity="0"/>
<Image Height="28" Width="28">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing x:Name="PreviousButtonGeometry" Brush="#27272a"/>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Grid>
</Border>
<Border x:Name="PageButtonBorder" Visibility="Visible" TextBlock.Foreground="#171717"
MouseDown="PageButton_MouseDown"
MouseUp="PageButton_MouseUp"
MouseLeave="PageButton_MouseLeave"
MinWidth="50" MinHeight="50" Background="Transparent">
<Grid>
<Border x:Name="PageButtonFeedbackBorder" Margin="0,3" CornerRadius="5"
Background="#18181b" Opacity="0"/>
<ikw:SimpleStackPanel VerticalAlignment="Center" Orientation="Vertical"
Spacing="0.5" Margin="12,0">
<TextBlock x:Name="PageNowText" HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="17" FontWeight="Bold" Text="?"/>
<TextBlock x:Name="PageTotalText" HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="10" Text="/ ?"/>
</ikw:SimpleStackPanel>
</Grid>
</Border>
<Border x:Name="NextButtonBorder" Width="50" Height="50"
MouseDown="NextButton_MouseDown"
MouseUp="NextButton_MouseUp"
MouseLeave="NextButton_MouseLeave"
CornerRadius="0 0 5 5" Background="Transparent">
<Grid>
<Border x:Name="NextButtonFeedbackBorder" Margin="3" CornerRadius="5"
Background="#18181b" Opacity="0"/>
<Image VerticalAlignment="Center" HorizontalAlignment="Center" Height="28" Width="28">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing x:Name="NextButtonGeometry" Brush="#27272a"/>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Grid>
</Border>
</ikw:SimpleStackPanel>
</DockPanel>
</Border>
</UserControl>
+393
View File
@@ -0,0 +1,393 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace Ink_Canvas.Controls
{
/// <summary>
/// PPT 翻页 + 增强预览一体化控件。
/// 通过 <see cref="Direction"/> 切换底部条 (LB/RB) 与侧边条 (LS/RS) 布局,
/// 预览列表内嵌于同一个 Border,展开时占据按钮组之外的剩余空间。
/// </summary>
public partial class PptNavBar : UserControl
{
public sealed class PreviewItem
{
public int SlideNumber { get; set; }
public BitmapImage Thumbnail { get; set; }
}
public enum NavDirection
{
LeftBottom,
RightBottom,
LeftSide,
RightSide
}
public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register(
nameof(Direction), typeof(NavDirection), typeof(PptNavBar),
new PropertyMetadata(NavDirection.LeftBottom, OnDirectionChanged));
public static readonly DependencyProperty CurrentSlideProperty = DependencyProperty.Register(
nameof(CurrentSlide), typeof(int), typeof(PptNavBar),
new PropertyMetadata(0, OnPageChanged));
public static readonly DependencyProperty TotalSlidesProperty = DependencyProperty.Register(
nameof(TotalSlides), typeof(int), typeof(PptNavBar),
new PropertyMetadata(0, OnPageChanged));
public static readonly DependencyProperty PreviewItemsProperty = DependencyProperty.Register(
nameof(PreviewItems), typeof(IList<PreviewItem>), typeof(PptNavBar),
new PropertyMetadata(null, OnPreviewItemsChanged));
public static readonly DependencyProperty IsPreviewExpandedProperty = DependencyProperty.Register(
nameof(IsPreviewExpanded), typeof(bool), typeof(PptNavBar),
new PropertyMetadata(false, OnIsPreviewExpandedChanged));
public NavDirection Direction
{
get => (NavDirection)GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
public int CurrentSlide
{
get => (int)GetValue(CurrentSlideProperty);
set => SetValue(CurrentSlideProperty, value);
}
public int TotalSlides
{
get => (int)GetValue(TotalSlidesProperty);
set => SetValue(TotalSlidesProperty, value);
}
public IList<PreviewItem> PreviewItems
{
get => (IList<PreviewItem>)GetValue(PreviewItemsProperty);
set => SetValue(PreviewItemsProperty, value);
}
public bool IsPreviewExpanded
{
get => (bool)GetValue(IsPreviewExpandedProperty);
set => SetValue(IsPreviewExpandedProperty, value);
}
public event EventHandler PreviousClick;
public event EventHandler NextClick;
public event EventHandler PageClick;
public event EventHandler<int> SlideSelected;
public event EventHandler PreviousPressedDown;
public event EventHandler NextPressedDown;
public event EventHandler PressEnded;
public event EventHandler<bool> PreviewExpandedChanged;
// 静态几何(左下/右下:水平箭头;左侧/右侧:垂直箭头)
private static readonly Geometry HArrowLeft = Geometry.Parse("F0 M24,24z M0,0z M3.3994,12.9642C2.86687,12.4317,2.86687,11.5683,3.3994,11.0358L9.94485,4.49031C10.4774,3.95777 11.3408,3.95777 11.8733,4.49031 12.4059,5.02284 12.4059,5.88625 11.8733,6.41878L7.65575,10.6364 19.6364,10.6364C20.3895,10.6364 21,11.2469 21,12 21,12.7531 20.3895,13.3636 19.6364,13.3636L7.65575,13.3636 11.8733,17.5812C12.4059,18.1137 12.4059,18.9772 11.8733,19.5097 11.3408,20.0422 10.4774,20.0422 9.94485,19.5097L3.3994,12.9642z");
private static readonly Geometry HArrowRight = Geometry.Parse("F0 M24,24z M0,0z M20.6006,12.9642C21.1331,12.4317,21.1331,11.5683,20.6006,11.0358L14.0551,4.49031C13.5226,3.95777 12.6592,3.95777 12.1267,4.49031 11.5941,5.02284 11.5941,5.88625 12.1267,6.41878L16.3443,10.6364 4.36364,10.6364C3.61052,10.6364 3,11.2469 3,12 3,12.7531 3.61052,13.3636 4.36364,13.3636L16.3443,13.3636 12.1267,17.5812C11.5941,18.1137 11.5941,18.9772 12.1267,19.5097 12.6592,20.0422 13.5226,20.0422 14.0551,19.5097L20.6006,12.9642z");
private static readonly Geometry VArrowUp = Geometry.Parse("F0 M24,24z M0,0z M11.0357,3.3994C11.5682,2.86687,12.4316,2.86687,12.9641,3.3994L19.5096,9.94485C20.0421,10.4774 20.0421,11.3408 19.5096,11.8733 18.9771,12.4059 18.1137,12.4059 17.5811,11.8733L13.3635,7.65575 13.3635,19.6364C13.3635,20.3895 12.753,21 11.9999,21 11.2468,21 10.6363,20.3895 10.6363,19.6364L10.6363,7.65575 6.41869,11.8733C5.88616,12.4059 5.02275,12.4059 4.49022,11.8733 3.95769,11.3408 3.95769,10.4774 4.49022,9.94485L11.0357,3.3994z");
private static readonly Geometry VArrowDown = Geometry.Parse("F0 M24,24z M0,0z M11.0357,20.6006C11.5682,21.1331,12.4316,21.1331,12.9641,20.6006L19.5096,14.0551C20.0421,13.5226 20.0421,12.6592 19.5096,12.1267 18.9771,11.5941 18.1137,11.5941 17.5811,12.1267L13.3635,16.3443 13.3635,4.36364C13.3635,3.61052 12.753,3 11.9999,3 11.2468,3 10.6363,3.61052 10.6363,4.36364L10.6363,16.3443 6.41869,12.1267C5.88616,11.5941 5.02275,11.5941 4.49022,12.1267 3.95769,12.6592 3.95769,13.5226 4.49022,14.0551L11.0357,20.6006z");
public PptNavBar()
{
InitializeComponent();
ApplyDirection(Direction);
}
private static void OnDirectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PptNavBar bar) bar.ApplyDirection((NavDirection)e.NewValue);
}
private static void OnPageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PptNavBar bar) bar.RefreshPageText();
}
private static void OnPreviewItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PptNavBar bar)
{
bar.PreviewList.ItemsSource = e.NewValue as IList<PreviewItem>;
bar.SyncPreviewSelection();
}
}
private static void OnIsPreviewExpandedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PptNavBar bar)
{
bool expanded = (bool)e.NewValue;
bar.PreviewList.Visibility = expanded ? Visibility.Visible : Visibility.Collapsed;
bar.ApplyLayout();
if (expanded)
{
bar.SyncPreviewSelection();
bar.HookOutsideClick();
}
else
{
bar.UnhookOutsideClick();
}
bar.PreviewExpandedChanged?.Invoke(bar, expanded);
}
}
private Window _hookedWindow;
private void HookOutsideClick()
{
if (_hookedWindow != null) return;
_hookedWindow = Window.GetWindow(this);
if (_hookedWindow != null)
{
_hookedWindow.PreviewMouseDown += OnWindowPreviewMouseDown;
}
}
private void UnhookOutsideClick()
{
if (_hookedWindow != null)
{
_hookedWindow.PreviewMouseDown -= OnWindowPreviewMouseDown;
_hookedWindow = null;
}
}
private void OnWindowPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is DependencyObject d && !IsDescendantOf(d, this))
{
IsPreviewExpanded = false;
}
}
private static bool IsDescendantOf(DependencyObject child, DependencyObject ancestor)
{
while (child != null)
{
if (ReferenceEquals(child, ancestor)) return true;
child = System.Windows.Media.VisualTreeHelper.GetParent(child)
?? System.Windows.LogicalTreeHelper.GetParent(child);
}
return false;
}
private void ApplyDirection(NavDirection dir) => ApplyLayout();
private void ApplyLayout()
{
var dir = Direction;
bool expanded = IsPreviewExpanded;
// 重置可能在不同状态下被设置的属性
ButtonRow.ClearValue(WidthProperty);
ButtonRow.ClearValue(HeightProperty);
ButtonRow.ClearValue(HorizontalAlignmentProperty);
PreviewList.ClearValue(WidthProperty);
PreviewList.ClearValue(HeightProperty);
PreviewList.ClearValue(MaxHeightProperty);
PreviewList.ClearValue(MaxWidthProperty);
PreviewList.ClearValue(HorizontalAlignmentProperty);
ClearValue(HeightProperty);
ClearValue(MaxHeightProperty);
double availableHeight = ComputeAvailableHeight();
switch (dir)
{
case NavDirection.LeftBottom:
case NavDirection.RightBottom:
DockPanel.SetDock(PreviewList, Dock.Top);
DockPanel.SetDock(ButtonRow, Dock.Bottom);
ButtonRow.Orientation = Orientation.Horizontal;
ButtonRow.Height = 50;
if (expanded)
{
// 预览面板拉宽到 280,贴向同侧角落
PreviewList.Width = 280;
PreviewList.MaxHeight = Math.Max(200, availableHeight - 50);
PreviewList.HorizontalAlignment = dir == NavDirection.LeftBottom
? HorizontalAlignment.Left
: HorizontalAlignment.Right;
// 按钮组宽度限制为原始内容宽度,并贴向同侧,保持按钮位置不变
ButtonRow.HorizontalAlignment = dir == NavDirection.LeftBottom
? HorizontalAlignment.Left
: HorizontalAlignment.Right;
}
else
{
PreviewList.SetBinding(WidthProperty, new System.Windows.Data.Binding(nameof(ButtonRow.ActualWidth)) { Source = ButtonRow });
PreviewList.MaxHeight = 380;
}
PreviousButtonGeometry.Geometry = HArrowLeft;
NextButtonGeometry.Geometry = HArrowRight;
break;
case NavDirection.LeftSide:
DockPanel.SetDock(PreviewList, Dock.Right);
DockPanel.SetDock(ButtonRow, Dock.Left);
ButtonRow.Orientation = Orientation.Vertical;
ButtonRow.Width = 50;
PreviewList.Width = 240;
PreviewList.MaxHeight = 480;
PreviousButtonGeometry.Geometry = VArrowUp;
NextButtonGeometry.Geometry = VArrowDown;
break;
case NavDirection.RightSide:
DockPanel.SetDock(PreviewList, Dock.Left);
DockPanel.SetDock(ButtonRow, Dock.Right);
ButtonRow.Orientation = Orientation.Vertical;
ButtonRow.Width = 50;
PreviewList.Width = 240;
PreviewList.MaxHeight = 480;
PreviousButtonGeometry.Geometry = VArrowUp;
NextButtonGeometry.Geometry = VArrowDown;
break;
}
}
private double ComputeAvailableHeight()
{
var window = Window.GetWindow(this);
double h = window != null ? window.ActualHeight : SystemParameters.PrimaryScreenHeight;
return Math.Max(240, h - 12);
}
private void RefreshPageText()
{
if (CurrentSlide > 0 && TotalSlides > 0)
{
PageNowText.Text = CurrentSlide.ToString();
PageTotalText.Text = $"/ {TotalSlides}";
}
else
{
PageNowText.Text = "?";
PageTotalText.Text = "/ ?";
}
SyncPreviewSelection();
}
private void SyncPreviewSelection()
{
if (PreviewItems == null || CurrentSlide <= 0) return;
foreach (var item in PreviewItems)
{
if (item.SlideNumber == CurrentSlide)
{
PreviewList.SelectedItem = item;
PreviewList.ScrollIntoView(item);
return;
}
}
}
private void SetFeedback(Border feedback, double opacity) => feedback.Opacity = opacity;
private object _lastDown;
private void PreviousButton_MouseDown(object sender, MouseButtonEventArgs e)
{
_lastDown = sender;
SetFeedback(PreviousButtonFeedbackBorder, 0.15);
PreviousPressedDown?.Invoke(this, EventArgs.Empty);
}
private void PreviousButton_MouseUp(object sender, MouseButtonEventArgs e)
{
SetFeedback(PreviousButtonFeedbackBorder, 0);
PressEnded?.Invoke(this, EventArgs.Empty);
if (_lastDown != sender) return;
_lastDown = null;
PreviousClick?.Invoke(this, EventArgs.Empty);
}
private void PreviousButton_MouseLeave(object sender, MouseEventArgs e)
{
SetFeedback(PreviousButtonFeedbackBorder, 0);
_lastDown = null;
PressEnded?.Invoke(this, EventArgs.Empty);
}
private void NextButton_MouseDown(object sender, MouseButtonEventArgs e)
{
_lastDown = sender;
SetFeedback(NextButtonFeedbackBorder, 0.15);
NextPressedDown?.Invoke(this, EventArgs.Empty);
}
private void NextButton_MouseUp(object sender, MouseButtonEventArgs e)
{
SetFeedback(NextButtonFeedbackBorder, 0);
PressEnded?.Invoke(this, EventArgs.Empty);
if (_lastDown != sender) return;
_lastDown = null;
NextClick?.Invoke(this, EventArgs.Empty);
}
private void NextButton_MouseLeave(object sender, MouseEventArgs e)
{
SetFeedback(NextButtonFeedbackBorder, 0);
_lastDown = null;
PressEnded?.Invoke(this, EventArgs.Empty);
}
private void PageButton_MouseDown(object sender, MouseButtonEventArgs e)
{
_lastDown = sender;
SetFeedback(PageButtonFeedbackBorder, 0.15);
}
private void PageButton_MouseUp(object sender, MouseButtonEventArgs e)
{
SetFeedback(PageButtonFeedbackBorder, 0);
if (_lastDown != sender) return;
_lastDown = null;
PageClick?.Invoke(this, EventArgs.Empty);
}
private void PageButton_MouseLeave(object sender, MouseEventArgs e)
{
SetFeedback(PageButtonFeedbackBorder, 0);
_lastDown = null;
}
private void PreviewList_MouseUp(object sender, MouseButtonEventArgs e)
{
if (PreviewList.SelectedItem is PreviewItem item)
{
SlideSelected?.Invoke(this, item.SlideNumber);
}
}
public void ApplyTheme(bool isDark)
{
var fgBrush = isDark ? Brushes.White : new SolidColorBrush(Color.FromRgb(39, 39, 42));
var feedbackBrush = isDark ? Brushes.White : new SolidColorBrush(Color.FromRgb(24, 24, 27));
var bgBrush = isDark
? new SolidColorBrush(Color.FromRgb(39, 39, 42))
: new SolidColorBrush(Color.FromRgb(244, 244, 245));
var borderBrush = isDark
? new SolidColorBrush(Color.FromRgb(82, 82, 91))
: new SolidColorBrush(Color.FromRgb(161, 161, 170));
PreviousButtonGeometry.Brush = fgBrush;
NextButtonGeometry.Brush = fgBrush;
PreviousButtonFeedbackBorder.Background = feedbackBrush;
NextButtonFeedbackBorder.Background = feedbackBrush;
PageButtonFeedbackBorder.Background = feedbackBrush;
PageNowText.Foreground = fgBrush;
PageTotalText.Foreground = fgBrush;
RootBorder.Background = bgBrush;
RootBorder.BorderBrush = borderBrush;
Resources["PptNavBarItemForeground"] = fgBrush;
}
public void SetPageButtonVisibility(Visibility v) => PageButtonBorder.Visibility = v;
public void SetBarOpacity(double opacity) => RootBorder.Opacity = opacity;
}
}
@@ -0,0 +1,18 @@
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 工具栏按钮插件与宿主之间的桥梁。Phase 1 粗粒度暴露 MainWindow,后续收窄。
/// </summary>
public interface IToolbarHost
{
MainWindow Window { get; }
/// <summary>按 id 登记按钮的 view 实例(供 MainWindow 字段回填和互相查找)。</summary>
void RegisterView(string id, FrameworkElement view);
/// <summary>按 id 获取之前注册的 view。不存在返回 null。</summary>
FrameworkElement FindView(string id);
}
}
@@ -0,0 +1,29 @@
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 一个工具栏按钮(或任意浮动栏/白板栏条目)的插件化契约。
/// 实现类必须有无参构造函数,启动时会被 ToolbarRegistry 反射实例化。
/// </summary>
public interface IToolbarItem
{
/// <summary>稳定、唯一的 id,用于持久化用户配置。不要随便改。</summary>
string Id { get; }
ToolbarSlot DefaultSlot { get; }
/// <summary>同一 slot 内的默认顺序,小的在前。</summary>
int DefaultOrder { get; }
bool DefaultVisible { get; }
ToolbarInsertPosition DefaultPosition { get; }
/// <summary>仅当 Position 为 BeforeAnchor/AfterAnchor 时有意义,对应 XAML 里 x:Name。</summary>
string DefaultAnchorName { get; }
/// <summary>构造 UI 元素并接线所有行为。</summary>
FrameworkElement BuildView(IToolbarHost host);
}
}
@@ -0,0 +1,26 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
/// <summary>
/// 清空按钮。位置:夹在颜色面板与 StackPanelCanvasControls 之间,
/// 所以用 BeforeAnchor 锚到 StackPanelCanvasControls。
/// </summary>
internal sealed class ClearToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.clear";
public override string LocalizationKey => "FloatingBar_Clear";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
public override int DefaultOrder => 0;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.BeforeAnchor;
public override string DefaultAnchorName => "StackPanelCanvasControls";
protected override string IconBrushResourceKey => "RedBrush";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconDelete_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachSymbolIconDelete(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class CursorToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.cursor";
public override string LocalizationKey => "FloatingBar_Mouse";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
public override int DefaultOrder => 100;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.CursorIcon_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachCursorIconView(view);
}
}
@@ -0,0 +1,19 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class CursorWithDelToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.cursorWithDel";
public override string LocalizationKey => "FloatingBar_ClearAndMouse";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 320;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.CursorWithDelIcon_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachCursorWithDelBtn(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class EraserByStrokesToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.eraserByStrokes";
public override string LocalizationKey => "FloatingBar_StrokeEraser";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 110;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.EraserIconByStrokes_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachEraserByStrokesIcon(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class EraserToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.eraser";
public override string LocalizationKey => "FloatingBar_AreaEraser";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 100;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.EraserIcon_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachEraserIcon(view);
}
}
@@ -0,0 +1,20 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class FoldToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.fold";
public override string LocalizationKey => "FloatingBar_Hide";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
public override int DefaultOrder => 120;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
public override string DefaultAnchorName => "FloatingBarEndSeparator";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.FoldFloatingBar_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachFoldIcon(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class PenToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.pen";
public override string LocalizationKey => "FloatingBar_Annotate";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
public override int DefaultOrder => 110;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.PenIcon_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachPenIconView(view);
}
}
@@ -0,0 +1,23 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class RedoToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.redo";
public override string LocalizationKey => "Board_Redo";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 310;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconRedo_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
{
host.Window.AttachSymbolIconRedo(view);
view.SetBinding(System.Windows.UIElement.IsEnabledProperty,
new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnRedo" });
}
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class SelectToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.select";
public override string LocalizationKey => "FloatingBar_LassoSelect";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 120;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconSelect_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachSymbolIconSelect(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class ShapeDrawToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.shapeDraw";
public override string LocalizationKey => "FloatingBar_Geometry";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 130;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.ImageDrawShape_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachShapeDrawBtn(view);
}
}
@@ -0,0 +1,47 @@
using Ink_Canvas.Properties;
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Ink_Canvas.Controls.Toolbar.Items
{
/// <summary>
/// 通用 ToolbarImageButton 工具栏条目基类——大幅减少每个按钮的样板代码。
/// 派生类通常只需给 Id / 本地化键 / Slot / Order / 点击处理 / Attach 回填。
/// </summary>
internal abstract class ToolbarImageButtonItemBase : IToolbarItem
{
public abstract string Id { get; }
public abstract string LocalizationKey { get; }
public abstract ToolbarSlot DefaultSlot { get; }
public abstract int DefaultOrder { get; }
public virtual bool DefaultVisible => true;
public virtual ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Prepend;
public virtual string DefaultAnchorName => null;
/// <summary>DynamicResource 名称,用于 IconBrush。默认为 null(使用控件自带前景色)。</summary>
protected virtual string IconBrushResourceKey => null;
protected abstract void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e);
/// <summary>构建后调用,用于回填 MainWindow 的原命名属性(partial 扩展里的 Attach*)。可选。</summary>
protected virtual void AfterBuild(IToolbarHost host, ToolbarImageButton view) { }
public FrameworkElement BuildView(IToolbarHost host)
{
var btn = new ToolbarImageButton
{
Label = Strings.GetString(LocalizationKey) ?? LocalizationKey
};
if (!string.IsNullOrEmpty(IconBrushResourceKey))
{
if (btn.TryFindResource(IconBrushResourceKey) is Brush brush) btn.IconBrush = brush;
else btn.SetResourceReference(ToolbarImageButton.IconBrushProperty, IconBrushResourceKey);
}
btn.ButtonMouseUp += (s, e) => OnClick(host, s, e);
AfterBuild(host, btn);
return btn;
}
}
}
@@ -0,0 +1,20 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class ToolsToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.tools";
public override string LocalizationKey => "Board_Tools";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
public override int DefaultOrder => 110;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
public override string DefaultAnchorName => "FloatingBarEndSeparator";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconTools_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachToolsBtn(view);
}
}
@@ -0,0 +1,23 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class UndoToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.undo";
public override string LocalizationKey => "Board_Undo";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 300;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconUndo_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
{
host.Window.AttachSymbolIconUndo(view);
view.SetBinding(System.Windows.UIElement.IsEnabledProperty,
new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnUndo" });
}
}
}
@@ -0,0 +1,20 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class WhiteboardToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.whiteboard";
public override string LocalizationKey => "FloatingBar_Whiteboard";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
public override int DefaultOrder => 100;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
public override string DefaultAnchorName => "FloatingBarEndSeparator";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.ImageBlackboard_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachWhiteboardBtn(view);
}
}
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// MainWindow 版的 IToolbarHost 实现。Phase 1 直接把 MainWindow 引用暴露给插件,
/// 插件可通过 host.Window 访问私有/内部成员(partial class 扩展或 internal 字段)。
/// 后续阶段逐步把具体行为抽成 Host 上的方法/事件,收窄这个接口。
/// </summary>
public sealed class ToolbarHost : IToolbarHost
{
private readonly Dictionary<string, FrameworkElement> _views = new Dictionary<string, FrameworkElement>();
public ToolbarHost(MainWindow window)
{
Window = window;
}
public MainWindow Window { get; }
public void RegisterView(string id, FrameworkElement view)
{
if (string.IsNullOrEmpty(id) || view == null) return;
_views[id] = view;
}
public FrameworkElement FindView(string id)
{
if (string.IsNullOrEmpty(id)) return null;
return _views.TryGetValue(id, out var v) ? v : null;
}
}
}
@@ -0,0 +1,14 @@
namespace Ink_Canvas.Controls.Toolbar
{
public enum ToolbarInsertPosition
{
/// <summary>从容器头部依次插入;Order 小的在前。</summary>
Prepend,
/// <summary>追加到容器末尾。</summary>
Append,
/// <summary>插入到由 AnchorName 指定的已有元素之前。</summary>
BeforeAnchor,
/// <summary>插入到由 AnchorName 指定的已有元素之后(同一锚点多项按 Order 依次排列)。</summary>
AfterAnchor
}
}
@@ -0,0 +1,33 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 单个工具栏按钮的用户配置(可见性、顺序、所属 slot、插入位置)。
/// 由 Settings.Toolbar 持久化。
/// </summary>
public class ToolbarItemConfig
{
[JsonProperty("visible")]
public bool Visible { get; set; } = true;
[JsonProperty("order")]
public int Order { get; set; }
[JsonProperty("slot")]
public ToolbarSlot Slot { get; set; } = ToolbarSlot.FloatingBarMain;
[JsonProperty("position")]
public ToolbarInsertPosition Position { get; set; } = ToolbarInsertPosition.Prepend;
[JsonProperty("anchorName")]
public string AnchorName { get; set; }
}
public class ToolbarLayoutSettings
{
[JsonProperty("items")]
public Dictionary<string, ToolbarItemConfig> Items { get; set; } = new Dictionary<string, ToolbarItemConfig>();
}
}
@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using Ink_Canvas.Helpers;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 扫描当前程序集里的 IToolbarItem 实现,按用户配置(Settings.Toolbar)排序/过滤后注入到目标容器。
/// </summary>
public static class ToolbarRegistry
{
private static List<IToolbarItem> _items;
public static IReadOnlyList<IToolbarItem> Discover()
{
if (_items != null) return _items;
var itemType = typeof(IToolbarItem);
_items = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => !t.IsAbstract && !t.IsInterface && itemType.IsAssignableFrom(t))
.Select(t =>
{
try { return (IToolbarItem)Activator.CreateInstance(t); }
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 实例化 {t.FullName} 失败: {ex.Message}", LogHelper.LogType.Warning);
return null;
}
})
.Where(i => i != null)
.ToList();
return _items;
}
/// <summary>按 slot 分配工具栏条目到对应容器。调用者负责清空目标容器里要被接管的旧内容。</summary>
public static void Populate(IToolbarHost host, IDictionary<ToolbarSlot, Panel> slots, ToolbarLayoutSettings layout)
{
if (host == null || slots == null) return;
layout = layout ?? new ToolbarLayoutSettings();
var grouped = new Dictionary<ToolbarSlot, List<(IToolbarItem item, ToolbarItemConfig cfg)>>();
foreach (var item in Discover())
{
if (!layout.Items.TryGetValue(item.Id, out var cfg))
{
cfg = new ToolbarItemConfig
{
Visible = item.DefaultVisible,
Order = item.DefaultOrder,
Slot = item.DefaultSlot,
Position = item.DefaultPosition,
AnchorName = item.DefaultAnchorName
};
}
if (!cfg.Visible) continue;
if (!grouped.TryGetValue(cfg.Slot, out var list))
{
list = new List<(IToolbarItem, ToolbarItemConfig)>();
grouped[cfg.Slot] = list;
}
list.Add((item, cfg));
}
foreach (var kv in grouped)
{
if (!slots.TryGetValue(kv.Key, out var container) || container == null) continue;
InjectIntoContainer(host, container, kv.Value);
}
}
private static void InjectIntoContainer(IToolbarHost host, Panel container,
List<(IToolbarItem item, ToolbarItemConfig cfg)> entries)
{
// 按 Position 分桶,每桶内按 Order 升序。
var prepend = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Prepend).OrderBy(e => e.cfg.Order).ToList();
var append = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Append).OrderBy(e => e.cfg.Order).ToList();
var before = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.BeforeAnchor).ToList();
var after = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.AfterAnchor).ToList();
var prependIndex = 0;
foreach (var entry in prepend)
{
var view = BuildAndRegister(host, entry.item);
if (view == null) continue;
container.Children.Insert(prependIndex++, view);
}
foreach (var entry in append)
{
var view = BuildAndRegister(host, entry.item);
if (view == null) continue;
container.Children.Add(view);
}
foreach (var group in before.GroupBy(e => e.cfg.AnchorName))
{
var anchor = FindNamedChild(container, group.Key);
if (anchor == null)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (BeforeAnchor)", LogHelper.LogType.Warning);
continue;
}
var idx = container.Children.IndexOf(anchor);
foreach (var entry in group.OrderBy(e => e.cfg.Order))
{
var view = BuildAndRegister(host, entry.item);
if (view == null) continue;
container.Children.Insert(idx++, view);
}
}
foreach (var group in after.GroupBy(e => e.cfg.AnchorName))
{
var anchor = FindNamedChild(container, group.Key);
if (anchor == null)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (AfterAnchor)", LogHelper.LogType.Warning);
continue;
}
var idx = container.Children.IndexOf(anchor) + 1;
foreach (var entry in group.OrderBy(e => e.cfg.Order))
{
var view = BuildAndRegister(host, entry.item);
if (view == null) continue;
container.Children.Insert(idx++, view);
}
}
}
private static UIElement FindNamedChild(Panel container, string name)
{
if (string.IsNullOrEmpty(name)) return null;
foreach (UIElement child in container.Children)
{
if (child is FrameworkElement fe && fe.Name == name) return child;
}
return null;
}
private static FrameworkElement BuildAndRegister(IToolbarHost host, IToolbarItem item)
{
try
{
var view = item.BuildView(host);
if (view == null) return null;
host.RegisterView(item.Id, view);
return view;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 构建 {item.Id} 失败: {ex.Message}", LogHelper.LogType.Warning);
return null;
}
}
}
}
@@ -0,0 +1,11 @@
namespace Ink_Canvas.Controls.Toolbar
{
public enum ToolbarSlot
{
FloatingBarMain,
FloatingBarCanvasControls,
FloatingBarEnd,
BlackboardLeft,
BlackboardRight
}
}
+27 -15
View File
@@ -2,11 +2,22 @@ using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Ink_Canvas.Controls;
namespace Ink_Canvas.Helpers
{
internal class AnimationsHelper
{
private static UIElement ResolveAnimationTarget(UIElement element)
{
if (element is BoardMenuFrame frame)
{
frame.ApplyTemplate();
return frame.AnimationTarget ?? element;
}
return element;
}
public static void ShowWithFadeIn(UIElement element, double duration = 0.15)
{
if (element.Visibility == Visibility.Visible) return;
@@ -36,14 +47,17 @@ namespace Ink_Canvas.Helpers
{
try
{
if (element.Visibility == Visibility.Visible) return;
if (element == null)
throw new ArgumentNullException(nameof(element));
if (element.Visibility == Visibility.Visible) return;
element.Visibility = Visibility.Visible;
var target = ResolveAnimationTarget(element);
var sb = new Storyboard();
// 渐变动画
var fadeInAnimation = new DoubleAnimation
{
From = 0.5,
@@ -54,10 +68,9 @@ namespace Ink_Canvas.Helpers
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(UIElement.OpacityProperty));
// 滑动动画
var slideAnimation = new DoubleAnimation
{
From = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
From = 10,
To = 0,
Duration = TimeSpan.FromSeconds(duration)
};
@@ -68,10 +81,9 @@ namespace Ink_Canvas.Helpers
sb.Children.Add(fadeInAnimation);
sb.Children.Add(slideAnimation);
element.Visibility = Visibility.Visible;
element.RenderTransform = new TranslateTransform();
target.RenderTransform = new TranslateTransform();
sb.Begin((FrameworkElement)element);
sb.Begin((FrameworkElement)target);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
@@ -207,14 +219,15 @@ namespace Ink_Canvas.Helpers
{
try
{
if (element.Visibility == Visibility.Collapsed) return;
if (element == null)
throw new ArgumentNullException(nameof(element));
if (element.Visibility == Visibility.Collapsed) return;
var target = ResolveAnimationTarget(element);
var sb = new Storyboard();
// 渐变动画
var fadeOutAnimation = new DoubleAnimation
{
From = 1,
@@ -224,11 +237,10 @@ namespace Ink_Canvas.Helpers
fadeOutAnimation.EasingFunction = new CubicEase();
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(UIElement.OpacityProperty));
// 滑动动画
var slideAnimation = new DoubleAnimation
{
From = 0,
To = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
To = 10,
Duration = TimeSpan.FromSeconds(duration)
};
slideAnimation.EasingFunction = new CubicEase();
@@ -243,8 +255,8 @@ namespace Ink_Canvas.Helpers
element.Visibility = Visibility.Collapsed;
};
element.RenderTransform = new TranslateTransform();
sb.Begin((FrameworkElement)element);
target.RenderTransform = new TranslateTransform();
sb.Begin((FrameworkElement)target);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
+113
View File
@@ -0,0 +1,113 @@
using Ink_Canvas.Windows.SettingsViews.Helpers;
using System;
using System.Diagnostics;
using System.Security.Principal;
using System.Windows;
namespace Ink_Canvas.Helpers
{
public static class AppRestartHelper
{
public static bool IsRunningAsAdmin()
{
try
{
var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
catch
{
return false;
}
}
public static void RestartApp(bool asAdmin)
{
try
{
App.IsAppExitByUser = true;
(Application.Current as App)?.ReleaseMutexForRestart();
string exePath = Process.GetCurrentProcess().MainModule.FileName;
if (asAdmin)
{
var psi = new ProcessStartInfo(exePath) { UseShellExecute = true, Verb = "runas" };
Process.Start(psi);
}
else
{
// 当前已是管理员时,直接通过用户令牌降权启动,避免经由 explorer 中转的延迟
if (IsRunningAsAdmin() && UIAccessHelper.RestartAsNormalUser())
{
Application.Current.Shutdown();
return;
}
Process.Start("explorer.exe", "\"" + exePath + "\"");
}
Application.Current.Shutdown();
}
catch (Exception ex)
{
Debug.WriteLine($"重启应用时出错: {ex.Message}");
}
}
public static void RestartWithCurrentPrivileges()
{
RestartApp(IsRunningAsAdmin());
}
public static void RestartAsAdmin()
{
RestartApp(true);
}
public static void RestartAsNormal()
{
RestartApp(false);
}
public static void SwitchToUIATopMostAndRestart()
{
try
{
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = true;
if (!SettingsManager.Settings.Advanced.IsAlwaysOnTop)
{
SettingsManager.Settings.Advanced.IsAlwaysOnTop = true;
}
SettingsManager.SaveSettingsToFile();
App.IsUIAccessTopMostEnabled = true;
RestartApp(true);
}
catch (Exception ex)
{
Debug.WriteLine($"切换到UIA置顶模式时出错: {ex.Message}");
}
}
public static void SwitchToNormalTopMostAndRestart()
{
try
{
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = false;
SettingsManager.SaveSettingsToFile();
App.IsUIAccessTopMostEnabled = false;
RestartApp(IsRunningAsAdmin());
}
catch (Exception ex)
{
Debug.WriteLine($"切换到普通置顶模式时出错: {ex.Message}");
}
}
}
}
+247 -18
View File
@@ -27,6 +27,118 @@ namespace Ink_Canvas.Helpers
private static readonly string updatesFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "AutoUpdate");
private static string statusFilePath;
// 全局下载取消令牌;UI 通过 RequestCancelDownload 取消当前下载
private static CancellationTokenSource _activeDownloadCts;
private static readonly object _activeDownloadLock = new object();
public static void RequestCancelDownload()
{
lock (_activeDownloadLock)
{
try { _activeDownloadCts?.Cancel(); } catch { }
}
}
private static CancellationTokenSource BeginDownloadSession()
{
lock (_activeDownloadLock)
{
try { _activeDownloadCts?.Cancel(); } catch { }
_activeDownloadCts = new CancellationTokenSource();
return _activeDownloadCts;
}
}
private static void EndDownloadSession(CancellationTokenSource cts)
{
lock (_activeDownloadLock)
{
if (ReferenceEquals(_activeDownloadCts, cts)) _activeDownloadCts = null;
}
try { cts?.Dispose(); } catch { }
}
public static bool IsX64UpdatePackageSelected()
{
try
{
return MainWindow.Settings?.Startup?.UpdatePackageArchitecture == UpdatePackageArchitecture.X64;
}
catch
{
return false;
}
}
private static string NormalizeVersionForUpdate(string version)
{
if (string.IsNullOrWhiteSpace(version)) return version;
return version.Trim().TrimStart('v', 'V');
}
public static string AppendX64SuffixBeforeZipExtension(string url)
{
if (string.IsNullOrEmpty(url) || !IsX64UpdatePackageSelected()) return url;
int query = url.IndexOf('?');
string pathPart = query >= 0 ? url.Substring(0, query) : url;
string qs = query >= 0 ? url.Substring(query) : "";
const string ext = ".zip";
int idx = pathPart.LastIndexOf(ext, StringComparison.OrdinalIgnoreCase);
if (idx < 0) return url;
var basePart = pathPart.Substring(0, idx);
if (basePart.EndsWith("-x64", StringComparison.OrdinalIgnoreCase)) return url;
return basePart + "-x64" + ext + qs;
}
public static string GetUpdateZipFileName(string version)
{
var v = NormalizeVersionForUpdate(version);
return IsX64UpdatePackageSelected()
? $"InkCanvasForClass.CE.{v}-x64.zip"
: $"InkCanvasForClass.CE.{v}.zip";
}
public static string GetUpdateDownloadStatusFilePath(string version)
{
var v = NormalizeVersionForUpdate(version);
string name = IsX64UpdatePackageSelected()
? $"DownloadV{v}_x64Status.txt"
: $"DownloadV{v}Status.txt";
return Path.Combine(updatesFolderPath, name);
}
public static string GetLocalUpdateZipFilePath(string version)
{
return Path.Combine(updatesFolderPath, GetUpdateZipFileName(version));
}
private static string PickBrowserDownloadUrlFromAssets(JToken assets)
{
if (assets == null || !assets.Any()) return null;
bool wantX64 = IsX64UpdatePackageSelected();
string anyZip = null;
string x64Zip = null;
string nonX64Zip = null;
foreach (JToken a in assets)
{
string name = a["name"]?.ToString();
string url = a["browser_download_url"]?.ToString();
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(url)) continue;
if (!name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) continue;
if (anyZip == null) anyZip = url;
if (name.EndsWith("-x64.zip", StringComparison.OrdinalIgnoreCase))
{
if (x64Zip == null) x64Zip = url;
}
else
{
if (nonX64Zip == null) nonX64Zip = url;
}
}
if (wantX64)
return x64Zip ?? anyZip;
return nonX64Zip ?? anyZip;
}
// 线路组结构体(包含版本、下载、日志地址)
public class UpdateLineGroup
@@ -302,6 +414,8 @@ namespace Ink_Canvas.Helpers
// 获取所有可用线路组,按延迟排序
public static async Task<List<UpdateLineGroup>> GetAvailableLineGroupsOrdered(UpdateChannel channel)
{
var cached = TryGetCachedOrderedGroups(channel);
if (cached != null) return cached;
var groups = ChannelLineGroups[channel];
var availableGroups = new List<(UpdateLineGroup group, long delay)>();
@@ -317,6 +431,7 @@ namespace Ink_Canvas.Helpers
if (!string.IsNullOrEmpty(group.DownloadUrlFormat))
{
testUrl = group.DownloadUrlFormat.Replace("{0}", "test");
testUrl = AppendX64SuffixBeforeZipExtension(testUrl);
}
}
catch
@@ -386,9 +501,46 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error);
}
CacheOrderedGroups(channel, orderedGroups);
return orderedGroups;
}
// 缓存按延迟排序后的线路组,避免短时间内重复测速
private static readonly Dictionary<UpdateChannel, (List<UpdateLineGroup> groups, DateTime cachedAt)> _orderedGroupsCache
= new Dictionary<UpdateChannel, (List<UpdateLineGroup>, DateTime)>();
private static readonly TimeSpan _orderedGroupsCacheTtl = TimeSpan.FromMinutes(15);
private static List<UpdateLineGroup> TryGetCachedOrderedGroups(UpdateChannel channel)
{
lock (_orderedGroupsCache)
{
if (_orderedGroupsCache.TryGetValue(channel, out var entry) &&
entry.groups != null && entry.groups.Count > 0 &&
DateTime.UtcNow - entry.cachedAt < _orderedGroupsCacheTtl)
{
LogHelper.WriteLogToFile($"AutoUpdate | 复用线路组延迟检测缓存({entry.groups.Count} 个)");
return new List<UpdateLineGroup>(entry.groups);
}
return null;
}
}
private static void CacheOrderedGroups(UpdateChannel channel, List<UpdateLineGroup> groups)
{
lock (_orderedGroupsCache)
{
_orderedGroupsCache[channel] = (new List<UpdateLineGroup>(groups), DateTime.UtcNow);
}
}
public static void InvalidateOrderedGroupsCache()
{
lock (_orderedGroupsCache)
{
_orderedGroupsCache.Clear();
}
}
private static async Task<long> GetDownloadUrlDelay(string url)
{
try
@@ -608,7 +760,7 @@ namespace Ink_Canvas.Helpers
if (version == targetVersion || version == $"v{targetVersion}" || version == $"V{targetVersion}")
{
string releaseNotes = release["body"]?.ToString();
string downloadUrl = release["assets"]?.First?["browser_download_url"]?.ToString();
string downloadUrl = PickBrowserDownloadUrlFromAssets(release["assets"]);
// 解析发布时间
DateTime? releaseTime = null;
@@ -649,7 +801,7 @@ namespace Ink_Canvas.Helpers
var latestRelease = releases[0];
string version = latestRelease["tag_name"]?.ToString();
string releaseNotes = latestRelease["body"]?.ToString();
string downloadUrl = latestRelease["assets"]?.First?["browser_download_url"]?.ToString();
string downloadUrl = PickBrowserDownloadUrlFromAssets(latestRelease["assets"]);
DateTime? releaseTime = null;
if (latestRelease["published_at"] != null && DateTime.TryParse(latestRelease["published_at"].ToString(), out DateTime parsedTime))
@@ -675,7 +827,7 @@ namespace Ink_Canvas.Helpers
var json = JObject.Parse(response);
string version = json["tag_name"]?.ToString();
string releaseNotes = json["body"]?.ToString();
string downloadUrl = json["assets"]?.First?["browser_download_url"]?.ToString();
string downloadUrl = PickBrowserDownloadUrlFromAssets(json["assets"]);
DateTime? releaseTime = null;
if (json["published_at"] != null && DateTime.TryParse(json["published_at"].ToString(), out DateTime parsedTime))
@@ -863,9 +1015,11 @@ namespace Ink_Canvas.Helpers
// 使用多线路组下载新版(支持自动切换)
public static async Task<bool> DownloadSetupFileWithFallback(string version, List<UpdateLineGroup> groups, Action<double, string> progressCallback = null)
{
var session = BeginDownloadSession();
try
{
statusFilePath = Path.Combine(updatesFolderPath, $"DownloadV{version}Status.txt");
version = NormalizeVersionForUpdate(version);
statusFilePath = GetUpdateDownloadStatusFilePath(version);
if (File.Exists(statusFilePath) && File.ReadAllText(statusFilePath).Trim().ToLower() == "true")
{
@@ -881,7 +1035,7 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 创建更新目录: {updatesFolderPath}");
}
string zipFilePath = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip");
string zipFilePath = GetLocalUpdateZipFilePath(version);
LogHelper.WriteLogToFile($"AutoUpdate | 目标文件路径: {zipFilePath}");
SaveDownloadStatus(false);
@@ -896,9 +1050,21 @@ namespace Ink_Canvas.Helpers
}
// 依次尝试每个线路组
CancellationToken groupLoopToken;
lock (_activeDownloadLock)
{
groupLoopToken = _activeDownloadCts?.Token ?? CancellationToken.None;
}
foreach (var group in groups)
{
if (groupLoopToken.IsCancellationRequested)
{
LogHelper.WriteLogToFile("AutoUpdate | 用户已取消,停止尝试后续线路组");
break;
}
string url = string.Format(group.DownloadUrlFormat, version);
url = AppendX64SuffixBeforeZipExtension(url);
// 智教联盟需要先获取真实下载地址
if (group.GroupName == "智教联盟")
{
@@ -922,6 +1088,12 @@ namespace Ink_Canvas.Helpers
bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback);
if (groupLoopToken.IsCancellationRequested)
{
LogHelper.WriteLogToFile("AutoUpdate | 用户已取消,停止尝试后续线路组");
break;
}
if (downloadSuccess)
{
SaveDownloadStatus(true);
@@ -937,6 +1109,13 @@ namespace Ink_Canvas.Helpers
progressCallback?.Invoke(0, "所有线路组下载均失败");
return false;
}
catch (OperationCanceledException)
{
LogHelper.WriteLogToFile("AutoUpdate | 下载已被用户取消", LogHelper.LogType.Warning);
SaveDownloadStatus(false);
progressCallback?.Invoke(0, "下载已取消");
return false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载更新时出错: {ex.Message}", LogHelper.LogType.Error);
@@ -949,6 +1128,10 @@ namespace Ink_Canvas.Helpers
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
return false;
}
finally
{
EndDownloadSession(session);
}
}
// 下载文件的具体实现
@@ -959,6 +1142,12 @@ namespace Ink_Canvas.Helpers
// 降低并发数,减少网络压力
int[] threadOptions = { 32, 16, 8, 4, 1 };
CancellationToken externalToken;
lock (_activeDownloadLock)
{
externalToken = _activeDownloadCts?.Token ?? CancellationToken.None;
}
// 检查服务器是否支持Range分块下载
bool supportRange = false;
long totalSize = -1;
@@ -1062,7 +1251,7 @@ namespace Ink_Canvas.Helpers
// 增加连接超时设置
client.Timeout = TimeSpan.FromSeconds(30);
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, externalToken);
var lastReadTime = DateTime.UtcNow;
bool dataReceived = false;
@@ -1122,8 +1311,20 @@ namespace Ink_Canvas.Helpers
success = true;
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载成功");
}
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException)
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException || ex is OperationCanceledException)
{
// 用户主动取消:不再重试
if (externalToken.IsCancellationRequested)
{
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载已被用户取消", LogHelper.LogType.Warning);
if (File.Exists(tempPath))
{
try { File.Delete(tempPath); } catch { }
}
cts.Cancel();
return;
}
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}");
@@ -1134,7 +1335,8 @@ namespace Ink_Canvas.Helpers
}
// 增加重试间隔,避免频繁重试
await Task.Delay(2000 * (retry + 1));
try { await Task.Delay(2000 * (retry + 1), externalToken); }
catch (OperationCanceledException) { cts.Cancel(); return; }
}
}
if (success)
@@ -1255,12 +1457,18 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 开始单线程下载: {fileUrl}");
progressCallback?.Invoke(0, "开始单线程下载");
CancellationToken token;
lock (_activeDownloadLock)
{
token = _activeDownloadCts?.Token ?? CancellationToken.None;
}
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
client.Timeout = TimeSpan.FromMinutes(10); // 单线程下载设置更长的超时时间
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead))
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token))
{
resp.EnsureSuccessStatusCode();
using (var fs = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
@@ -1271,9 +1479,9 @@ namespace Ink_Canvas.Helpers
long downloaded = 0;
var lastProgressUpdate = DateTime.UtcNow;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
{
await fs.WriteAsync(buffer, 0, read);
await fs.WriteAsync(buffer, 0, read, token);
downloaded += read;
// 限制进度更新频率,避免UI卡顿
@@ -1295,6 +1503,13 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile("AutoUpdate | 单线程下载完成");
return true;
}
catch (OperationCanceledException)
{
LogHelper.WriteLogToFile("AutoUpdate | 单线程下载已被取消", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, "下载已取消");
try { if (File.Exists(destinationPath)) File.Delete(destinationPath); } catch { }
return false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 单线程下载失败: {ex.Message}", LogHelper.LogType.Error);
@@ -1352,7 +1567,7 @@ namespace Ink_Canvas.Helpers
/// </summary>
/// <remarks>
/// 该方法会临时将 App.IsUpdateInstalling 置为 true、尝试关闭进程保护(并在结束时还原)、在必要时备份当前设置、解压更新 ZIP、启动解压后的新可执行文件(以更新模式传递旧进程 ID、解压路径和目标路径等参数),并在新进程启动后关闭当前进程。方法会记录日志并在遇到错误时安全退出相应步骤,但不会抛出异常给调用方以外的上下文。</remarks>
/// <param name="version">要安装的版本号,用于定位更新包文件名(例如 InkCanvasForClass.CE.{version}.zip)。</param>
/// <param name="version">要安装的版本号,用于定位更新包文件名(与 <see cref="GetLocalUpdateZipFilePath"/> 一致;选择 x64 包时为 InkCanvasForClass.CE.{version}-x64.zip)。</param>
/// <param name="isInSilence">指示是否以静默模式启动新版本(影响传递给新进程的参数和可能的用户提示)。</param>
public static void InstallNewVersionApp(string version, bool isInSilence)
{
@@ -1403,7 +1618,7 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"更新前自动备份设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
string zipFilePath = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip");
string zipFilePath = GetLocalUpdateZipFilePath(version);
LogHelper.WriteLogToFile($"AutoUpdate | 检查ZIP文件: {zipFilePath}");
if (!File.Exists(zipFilePath))
@@ -2117,9 +2332,9 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile($"AutoUpdate | 开始修复版本,通道: {channel}");
// 获取远程版本号(自动选择最快线路组,始终下载远程版本,版本修复模式)
var (remoteVersion, group, _) = await CheckForUpdates(channel, true, true);
if (string.IsNullOrEmpty(remoteVersion) || group == null)
// 获取远程版本号(始终下载远程版本,版本修复模式)
var (remoteVersion, preferredGroup, _) = await CheckForUpdates(channel, true, true);
if (string.IsNullOrEmpty(remoteVersion))
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时获取远程版本失败", LogHelper.LogType.Error);
return false;
@@ -2127,8 +2342,22 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 修复版本远程版本: {remoteVersion}");
var availableGroups = await GetAvailableLineGroupsOrdered(channel);
if (availableGroups.Count == 0)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时无可用线路组", LogHelper.LogType.Error);
return false;
}
if (preferredGroup != null)
{
availableGroups.RemoveAll(g => g.GroupName == preferredGroup.GroupName);
availableGroups.Insert(0, preferredGroup);
LogHelper.WriteLogToFile($"AutoUpdate | 修复版本下载优先使用线路组: {preferredGroup.GroupName}");
}
// 无论版本是否为最新,都下载远程版本
bool downloadResult = await DownloadSetupFile(remoteVersion, group);
bool downloadResult = await DownloadSetupFileWithFallback(remoteVersion, availableGroups);
if (!downloadResult)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时下载更新失败", LogHelper.LogType.Error);
@@ -2170,7 +2399,7 @@ namespace Ink_Canvas.Helpers
{
string version = item["tag_name"]?.ToString();
string releaseNotes = item["body"]?.ToString();
string downloadUrl = item["assets"]?.First?["browser_download_url"]?.ToString();
string downloadUrl = PickBrowserDownloadUrlFromAssets(item["assets"]);
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
result.Add((version, downloadUrl, releaseNotes));
}
+24
View File
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace Ink_Canvas.Converter
{
@@ -152,4 +153,27 @@ namespace Ink_Canvas.Converter
return null;
}
}
public class StringToGeometryConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
if (value is string geometryString && !string.IsNullOrEmpty(geometryString))
{
return Geometry.Parse(geometryString);
}
}
catch (Exception)
{
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
+96
View File
@@ -0,0 +1,96 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace Ink_Canvas.Helpers
{
public static class DebugConsoleManager
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32.dll")]
private static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
[DllImport("kernel32.dll")]
private static extern bool SetConsoleTitle(string lpConsoleTitle);
private const int SW_HIDE = 0;
private const int SW_SHOW = 5;
private const uint SC_CLOSE = 0xF060;
private const uint MF_BYCOMMAND = 0x00000000;
private static bool _allocated;
public static bool IsVisible { get; private set; }
public static void Show()
{
try
{
if (!_allocated)
{
if (GetConsoleWindow() == IntPtr.Zero)
{
if (!AllocConsole()) return;
}
_allocated = true;
Console.OutputEncoding = Encoding.UTF8;
SetConsoleTitle("InkCanvasForClass - Debug Console");
// 移除关闭菜单,避免用户点 X 时直接结束进程
var hWnd = GetConsoleWindow();
if (hWnd != IntPtr.Zero)
{
var hMenu = GetSystemMenu(hWnd, false);
if (hMenu != IntPtr.Zero) DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
}
}
else
{
var hWnd = GetConsoleWindow();
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_SHOW);
}
IsVisible = true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Show failed: {ex.Message}");
}
}
public static void Hide()
{
try
{
var hWnd = GetConsoleWindow();
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_HIDE);
IsVisible = false;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Hide failed: {ex.Message}");
}
}
public static void WriteLine(string line)
{
if (!IsVisible) return;
try { Console.WriteLine(line); }
catch { }
}
}
}
+156 -113
View File
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
@@ -22,6 +23,9 @@ namespace Ink_Canvas.Helpers
private static readonly string DeviceId;
private static readonly object fileLock = new object();
private static UsageStats usageStatsCache;
private static DateTime usageStatsCacheTime;
private static readonly TimeSpan UsageStatsCacheDuration = TimeSpan.FromMinutes(2);
static DeviceIdentifier()
{
@@ -116,114 +120,26 @@ namespace Ink_Canvas.Helpers
/// </summary>
private static string GenerateHardwareFingerprint()
{
// 收集硬件信息
var hardwareInfo = new StringBuilder();
AppendFingerprintPart(hardwareInfo, "CPU",
GetWmiProperty("SELECT ProcessorId FROM Win32_Processor", "ProcessorId"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0", "ProcessorNameString"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0", "Identifier"));
try
{
var assembly = Assembly.Load("System.Management");
if (assembly != null)
{
// CPU信息
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT ProcessorId FROM Win32_Processor");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
AppendFingerprintPart(hardwareInfo, "BOARD",
GetWmiProperty("SELECT SerialNumber FROM Win32_BaseBoard", "SerialNumber"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BaseBoardSerialNumber"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BaseBoardProduct"));
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
AppendFingerprintPart(hardwareInfo, "BIOS",
GetWmiProperty("SELECT SerialNumber FROM Win32_BIOS", "SerialNumber"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BIOSVersion"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BIOSVendor"));
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var processorId = indexer.GetValue(obj, new object[] { "ProcessorId" });
hardwareInfo.Append(processorId?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
// 主板序列号
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BaseBoard");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
hardwareInfo.Append(serialNumber?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
// BIOS序列号
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BIOS");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
hardwareInfo.Append(serialNumber?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
// 主硬盘序列号
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
hardwareInfo.Append(serialNumber?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
}
catch
{
}
AppendFingerprintPart(hardwareInfo, "DISK",
GetWmiProperty("SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'", "SerialNumber"),
GetSystemDriveVolumeSerial(),
GetRegistryValue(@"SOFTWARE\Microsoft\Cryptography", "MachineGuid"));
if (hardwareInfo.Length < 10)
{
@@ -235,6 +151,108 @@ namespace Ink_Canvas.Helpers
return hardwareInfo.ToString();
}
private static void AppendFingerprintPart(StringBuilder hardwareInfo, string key, params string[] candidates)
{
foreach (var candidate in candidates)
{
if (!string.IsNullOrWhiteSpace(candidate))
{
hardwareInfo.Append(key).Append(':').Append(candidate.Trim()).Append(';');
return;
}
}
}
private static string GetWmiProperty(string query, string propertyName)
{
try
{
var assembly = Assembly.Load("System.Management");
var searcherType = assembly?.GetType("System.Management.ManagementObjectSearcher");
if (searcherType == null)
{
return null;
}
var searcher = Activator.CreateInstance(searcherType, query);
var getMethod = searcherType.GetMethod("Get");
var resultCollection = getMethod?.Invoke(searcher, null);
if (resultCollection == null)
{
return null;
}
var enumerator = resultCollection.GetType().GetMethod("GetEnumerator")?.Invoke(resultCollection, null);
var moveNextMethod = enumerator?.GetType().GetMethod("MoveNext");
var currentProperty = enumerator?.GetType().GetProperty("Current");
if (enumerator == null || moveNextMethod == null || currentProperty == null)
{
return null;
}
if (!(bool)moveNextMethod.Invoke(enumerator, null))
{
return null;
}
var currentObject = currentProperty.GetValue(enumerator);
var indexer = currentObject?.GetType().GetProperty("Item", new[] { typeof(string) });
var result = indexer?.GetValue(currentObject, new object[] { propertyName })?.ToString();
searcher?.GetType().GetMethod("Dispose")?.Invoke(searcher, null);
return result;
}
catch
{
return null;
}
}
private static string GetRegistryValue(string subKey, string valueName)
{
try
{
return Microsoft.Win32.Registry.GetValue($@"HKEY_LOCAL_MACHINE\{subKey}", valueName, null)?.ToString();
}
catch
{
return null;
}
}
private static string GetSystemDriveVolumeSerial()
{
try
{
var rootPath = Path.GetPathRoot(Environment.SystemDirectory);
if (string.IsNullOrWhiteSpace(rootPath))
{
return null;
}
if (GetVolumeInformation(rootPath, null, 0, out uint serialNumber, out _, out _, null, 0))
{
return serialNumber.ToString("X8");
}
}
catch
{
}
return null;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool GetVolumeInformation(
string rootPathName,
StringBuilder volumeNameBuffer,
uint volumeNameSize,
out uint volumeSerialNumber,
out uint maximumComponentLength,
out uint fileSystemFlags,
StringBuilder fileSystemNameBuffer,
uint nFileSystemNameSize);
/// <summary>
/// 基于硬件指纹生成25字符的设备ID
/// </summary>
@@ -654,7 +672,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var stats = LoadUsageStats();
var stats = GetUsageStatsCached();
return stats.SystemVersion;
}
catch (Exception ex)
@@ -773,7 +791,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var stats = LoadUsageStats();
var stats = GetUsageStatsCached();
return stats.UpdatePriority;
}
catch (Exception ex)
@@ -790,7 +808,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var stats = LoadUsageStats();
var stats = GetUsageStatsCached();
return stats.UsageFrequency;
}
catch (Exception ex)
@@ -892,6 +910,23 @@ namespace Ink_Canvas.Helpers
}
}
private static UsageStats GetUsageStatsCached(bool forceRefresh = false)
{
lock (fileLock)
{
if (!forceRefresh
&& usageStatsCache != null
&& (DateTime.Now - usageStatsCacheTime) < UsageStatsCacheDuration)
{
return usageStatsCache;
}
usageStatsCache = LoadUsageStats();
usageStatsCacheTime = DateTime.Now;
return usageStatsCache;
}
}
/// <summary>
/// 保存使用统计
/// </summary>
@@ -902,6 +937,9 @@ namespace Ink_Canvas.Helpers
// 保存到备份文件
SaveUsageStatsToFile(UsageStatsBackupPath, stats);
usageStatsCache = stats;
usageStatsCacheTime = DateTime.Now;
}
@@ -1242,15 +1280,20 @@ namespace Ink_Canvas.Helpers
int versionDiff = CalculateVersionGenerationDifference(localVersion, updateVersion);
LogHelper.WriteLogToFile($"DeviceIdentifier | 无法获取版本发布时间,使用版本号差异判断 - 本地版本: {localVersion}, 远程版本: {updateVersion}, 代数差异: {versionDiff}");
if (versionDiff >= 1)
if (versionDiff <= 0)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=1,允许更新");
}
else
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})<1,可能是相同版本或降级,暂不更新");
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})<=0,可能是相同版本或降级,暂不更新");
return false;
}
// 当代数差异较大(>=3)时直接放行,避免被分级策略卡住
if (versionDiff >= 3)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=3,跳过分级策略直接推送");
return true;
}
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=1,进入分级策略判断");
}
// 计算最近活跃度(最后一次使用距今的天数)
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Ink_Canvas.Helpers
{
public static class ExternalCallerLauncher
{
private static readonly string[] ClassIslandProtocols =
{
"classisland://plugins/IslandCaller/Simple/1",
"classisland://plugins/IslandCaller/Simple",
"classisland://plugins/IslandCaller/Run"
};
public static string[] GetProtocolsByType(int externalCallerType)
{
switch (externalCallerType)
{
case 0:
return ClassIslandProtocols;
case 1:
return new[]
{
"secrandom://roll_call/quick_draw",
"secrandom://direct_extraction"
};
case 2:
return new[] { "namepicker://" };
default:
return ClassIslandProtocols;
}
}
public static string[] GetProtocolsByName(string externalCallerName)
{
switch (externalCallerName)
{
case "ClassIsland":
return ClassIslandProtocols;
case "SecRandom":
return new[]
{
"secrandom://roll_call/quick_draw",
"secrandom://direct_extraction"
};
case "NamePicker":
return new[] { "namepicker://" };
default:
return ClassIslandProtocols;
}
}
public static bool TryLaunch(IEnumerable<string> protocols, out Exception lastException)
{
lastException = null;
if (protocols == null) return false;
foreach (var protocol in protocols)
{
if (string.IsNullOrWhiteSpace(protocol)) continue;
try
{
Process.Start(new ProcessStartInfo
{
FileName = protocol,
UseShellExecute = true
});
return true;
}
catch (Exception ex)
{
lastException = ex;
}
}
return false;
}
}
}
+6 -1
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
@@ -54,6 +54,11 @@ namespace Ink_Canvas.Helpers
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromRect(ref RECT lprc, uint dwFlags);
public static IntPtr GetForegroundWindowHandle()
{
return GetForegroundWindow();
}
public static string WindowTitle()
{
IntPtr foregroundWindowHandle = GetForegroundWindow();
-3
View File
@@ -1,5 +1,4 @@
using System;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
@@ -189,9 +188,7 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 确保窗口全屏的Hook
/// 使用HandleProcessCorruptedStateExceptions,防止访问内存过程中因为一些致命异常导致程序崩溃
/// </summary>
[HandleProcessCorruptedStateExceptions]
private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
//处理WM_WINDOWPOSCHANGING消息
+60 -6
View File
@@ -285,6 +285,7 @@ namespace Ink_Canvas.Helpers
// 功能快捷键
RegisterHotkey("DrawLine", Key.L, ModifierKeys.Alt, () => _mainWindow.BtnDrawLine_Click(null, null));
RegisterHotkey("Screenshot", Key.C, ModifierKeys.Alt, () => _mainWindow.SaveScreenShotToDesktop());
RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, () => _mainWindow.OpenQuickDrawFromHotkey());
RegisterHotkey("Hide", Key.V, ModifierKeys.Alt, () => _mainWindow.SymbolIconEmoji_MouseUp(null, null));
// 退出快捷键
@@ -566,6 +567,36 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 刷新多屏相关设置(开关和跟随鼠标策略)。
/// </summary>
public void RefreshMultiScreenSettings()
{
try
{
var advanced = MainWindow.Settings.Advanced;
_isMultiScreenMode = advanced.EnableMultiScreenSupport && ScreenDetectionHelper.HasMultipleScreens();
_enableScreenSpecificHotkeys = _isMultiScreenMode;
if (_isMultiScreenMode)
{
_currentScreen = advanced.FollowMouseForScreenSelection
? Screen.FromPoint(Control.MousePosition)
: ScreenDetectionHelper.GetWindowScreen(_mainWindow);
}
else
{
_currentScreen = ScreenDetectionHelper.GetPrimaryScreen();
}
RefreshHotkeysForCurrentScreen();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"刷新多屏设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 获取当前屏幕信息
/// </summary>
@@ -623,13 +654,15 @@ namespace Ink_Canvas.Helpers
{
try
{
// 检测是否有多个屏幕
_isMultiScreenMode = ScreenDetectionHelper.HasMultipleScreens();
var advanced = MainWindow.Settings.Advanced;
_isMultiScreenMode = advanced.EnableMultiScreenSupport && ScreenDetectionHelper.HasMultipleScreens();
_enableScreenSpecificHotkeys = _isMultiScreenMode;
if (_isMultiScreenMode)
{
// 获取当前窗口所在的屏幕
_currentScreen = ScreenDetectionHelper.GetWindowScreen(_mainWindow);
_currentScreen = advanced.FollowMouseForScreenSelection
? Screen.FromPoint(Control.MousePosition)
: ScreenDetectionHelper.GetWindowScreen(_mainWindow);
// 监听窗口位置变化事件
_mainWindow.LocationChanged += OnWindowLocationChanged;
@@ -687,6 +720,9 @@ namespace Ink_Canvas.Helpers
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
return;
if (MainWindow.Settings.Advanced.FollowMouseForScreenSelection)
return;
var newScreen = ScreenDetectionHelper.GetWindowScreen(_mainWindow);
if (newScreen != null && newScreen != _currentScreen)
{
@@ -799,9 +835,16 @@ namespace Ink_Canvas.Helpers
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
return;
// 检查鼠标是否在当前窗口所在的屏幕上
var mousePosition = Control.MousePosition;
var currentScreen = Screen.FromPoint(mousePosition);
var mouseScreen = Screen.FromPoint(mousePosition);
if (MainWindow.Settings.Advanced.FollowMouseForScreenSelection &&
mouseScreen != null &&
mouseScreen != _currentScreen)
{
_currentScreen = mouseScreen;
RefreshHotkeysForCurrentScreen();
}
// 无论屏幕是否变化,都检查热键状态
// 这样可以确保热键状态始终与当前上下文保持一致
@@ -1033,6 +1076,7 @@ namespace Ink_Canvas.Helpers
new HotkeyConfigItem { Name = "Pen5", Key = Key.D5, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "DrawLine", Key = Key.L, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Screenshot", Key = Key.C, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "QuickDraw", Key = Key.K, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Hide", Key = Key.V, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Exit", Key = Key.Escape, Modifiers = ModifierKeys.None }
});
@@ -1111,6 +1155,14 @@ namespace Ink_Canvas.Helpers
}
}
// 旧版 HotkeyConfig.json 无「快抽」项时补注册默认组合,避免升级后无快捷键
if (successCount > 0 && !IsHotkeyRegistered("QuickDraw"))
{
var quickDrawAction = GetActionByName("QuickDraw");
if (quickDrawAction != null && RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, quickDrawAction))
successCount++;
}
if (successCount > 0)
{
_hotkeysShouldBeRegistered = true;
@@ -1221,6 +1273,8 @@ namespace Ink_Canvas.Helpers
return () => _mainWindow.BtnDrawLine_Click(null, null);
case "Screenshot":
return () => _mainWindow.SaveScreenShotToDesktop();
case "QuickDraw":
return () => _mainWindow.OpenQuickDrawFromHotkey();
case "Hide":
return () => _mainWindow.SymbolIconEmoji_MouseUp(null, null);
case "Exit":
+1 -3
View File
@@ -264,7 +264,6 @@ namespace Ink_Canvas.Helpers
public void Enable()
{
IsEnabled = true;
LogHelper.WriteLogToFile("墨迹渐隐功能已启用");
}
/// <summary>
@@ -273,7 +272,6 @@ namespace Ink_Canvas.Helpers
public void Disable()
{
IsEnabled = false;
LogHelper.WriteLogToFile("墨迹渐隐功能已禁用");
}
#endregion
@@ -894,4 +892,4 @@ namespace Ink_Canvas.Helpers
}
#endregion
}
}
}
+246
View File
@@ -0,0 +1,246 @@
using System;
using System.Threading.Tasks;
using System.Windows.Ink;
namespace Ink_Canvas.Helpers
{
public sealed class InkRecognitionManager
{
private static InkRecognitionManager _instance;
private static readonly object _lock = new object();
private readonly object _initSync = new object();
private ModernInkProcessor _modernProcessor;
private bool _isModernSystemAvailable;
private bool _isInitialized;
public static InkRecognitionManager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
_instance = new InkRecognitionManager();
}
}
return _instance;
}
}
private InkRecognitionManager() { }
private void Initialize()
{
if (_isInitialized) return;
try
{
// 启动阶段只做能力探测,不做 WinRT 组件实例化(避免冷启动延迟)
_isModernSystemAvailable = WinRtInkShapeRecognizer.IsApiAvailable;
_isInitialized = true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("墨迹识别管理器初始化失败: " + ex.Message, LogHelper.LogType.Error);
_isInitialized = false;
}
}
private void EnsureInitialized()
{
if (_isInitialized) return;
lock (_initSync)
{
if (_isInitialized) return;
Initialize();
}
}
private void EnsureModernAnalyzerInitialized()
{
if (_modernProcessor != null || !_isModernSystemAvailable) return;
lock (_initSync)
{
if (_modernProcessor != null || !_isModernSystemAvailable) return;
try
{
_modernProcessor ??= new ModernInkProcessor();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("WinRT 墨迹模块懒加载失败: " + ex.Message, LogHelper.LogType.Warning);
_isModernSystemAvailable = false;
_modernProcessor = null;
}
}
}
public Task<InkShapeRecognitionResult> RecognizeShapeAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
EnsureInitialized();
if (!_isInitialized || strokes == null || strokes.Count == 0)
return Task.FromResult(InkShapeRecognitionResult.Empty);
try
{
if (ShapeRecognitionRouter.ResolveUseWinRt(mode)
&& WinRtInkShapeRecognizer.IsApiAvailable)
{
return RecognizeShapeWinRtOnDispatcherContext(strokes);
}
var legacy = InkRecognizeHelper.RecognizeShapeIACore(strokes);
return Task.FromResult(InkRecognizeHelper.FromIACoreOrEmpty(legacy));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("墨迹形状识别失败: " + ex.Message, LogHelper.LogType.Error);
return Task.FromResult(InkShapeRecognitionResult.Empty);
}
}
private static async Task<InkShapeRecognitionResult> RecognizeShapeWinRtOnDispatcherContext(
StrokeCollection strokes)
{
return await WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes).ConfigureAwait(true);
}
/// <param name="applyHandwritingBeautify">为 true 且走 WinRT 时,将识别成功的词替换为手写风格字体的轮廓墨迹(见设置中的字体列表)。</param>
/// <param name="handwritingFontFamilyList">逗号分隔的字体回退列表(WPF FontFamily);null 时使用内置默认。</param>
public Task<StrokeCollection> CorrectInkAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode,
bool applyHandwritingBeautify = false,
string handwritingFontFamilyList = null)
{
EnsureInitialized();
if (!_isInitialized)
{
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
if (strokes == null || strokes.Count == 0)
{
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:无笔画。", LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
try
{
var useWinRt = ShapeRecognitionRouter.ResolveUseWinRt(mode);
if (!applyHandwritingBeautify)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:未开启「识别转手写体字形」(applyHandwritingBeautify=false)。笔画数=" +
strokes.Count,
LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
if (!useWinRt)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:当前引擎非 WinRT(模式=" + mode + ")。笔画数=" + strokes.Count,
LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
EnsureModernAnalyzerInitialized();
if (_modernProcessor == null)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:ModernInkProcessor 未就绪(WinRT 初始化失败?)。笔画数=" +
strokes.Count,
LogHelper.LogType.Warning);
return Task.FromResult(strokes);
}
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count +
",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()),
LogHelper.LogType.Info);
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(strokes, handwritingFontFamilyList);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("墨迹纠正失败: " + ex.Message, LogHelper.LogType.Error);
return Task.FromResult(strokes);
}
}
/// <summary>
/// WinRT 手写体识别(需 Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。
/// </summary>
public Task<HandwritingRecognitionResult> RecognizeHandwritingAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
EnsureInitialized();
if (!_isInitialized || strokes == null || strokes.Count == 0)
return Task.FromResult(HandwritingRecognitionResult.Empty);
try
{
if (!ShapeRecognitionRouter.ResolveUseWinRt(mode)
|| !WinRtHandwritingRecognizer.IsApiAvailable)
return Task.FromResult(HandwritingRecognitionResult.Empty);
return WinRtHandwritingRecognizer.RecognizeHandwritingAsync(strokes);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("手写识别失败: " + ex.Message, LogHelper.LogType.Error);
return Task.FromResult(HandwritingRecognitionResult.Empty);
}
}
public bool IsValidShapeType(string shapeName)
{
return !string.IsNullOrEmpty(shapeName)
&& (shapeName.Contains("Triangle") || shapeName.Contains("Circle")
|| shapeName.Contains("Rectangle") || shapeName.Contains("Diamond")
|| shapeName.Contains("Parallelogram") || shapeName.Contains("Square")
|| shapeName.Contains("Ellipse") || shapeName.Contains("Line")
|| shapeName.Contains("Arrow"));
}
public string GetSystemInfo()
{
return _isModernSystemAvailable
? $"现代化墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}"
: $"传统墨迹识别系统 (IACore) - 进程架构: {Environment.Is64BitProcess}";
}
public void Dispose()
{
_modernProcessor?.Dispose();
_isInitialized = false;
}
}
internal sealed class ModernInkProcessor : IDisposable
{
public ModernInkProcessor()
{
if (!WinRtInkShapeRecognizer.IsApiAvailable)
throw new InvalidOperationException("WinRT 墨迹分析需要 Windows 10 及以上。");
}
public Task<InkShapeRecognitionResult> RecognizeShapeAsync(StrokeCollection strokes)
{
return WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes);
}
public void Dispose()
{
}
}
}
+134 -10
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Media;
@@ -7,8 +8,8 @@ namespace Ink_Canvas.Helpers
{
public class InkRecognizeHelper
{
//识别形状
public static ShapeRecognizeResult RecognizeShape(StrokeCollection strokes)
/// <summary>IACore / IAWinFX 形状识别(典型用于 32 位进程)。</summary>
public static ShapeRecognizeResult RecognizeShapeIACore(StrokeCollection strokes)
{
if (strokes == null || strokes.Count == 0)
return default;
@@ -25,35 +26,158 @@ namespace Ink_Canvas.Helpers
var alternates = analyzer.GetAlternates();
if (alternates.Count > 0)
{
while ((!alternates[0].Strokes.Contains(strokes.Last()) ||
!IsContainShapeType(((InkDrawingNode)alternates[0].AlternateNodes[0]).GetShapeName()))
&& strokesCount >= 2)
while (strokesCount >= 2)
{
var alt0 = alternates[0];
if (alt0?.AlternateNodes == null || alt0.AlternateNodes.Count == 0)
break;
var drawNode = alt0.AlternateNodes[0] as InkDrawingNode;
if (drawNode == null)
break;
var shapeOk = IsContainShapeType(drawNode.GetShapeName());
if (alt0.Strokes.Contains(strokes.Last()) && shapeOk)
break;
analyzer.RemoveStroke(strokes[strokes.Count - strokesCount]);
strokesCount--;
sfsaf = analyzer.Analyze();
if (sfsaf.Successful)
{
alternates = analyzer.GetAlternates();
}
else
break;
if (alternates.Count == 0)
break;
}
if (alternates.Count > 0)
{
var altFinal = alternates[0];
if (altFinal?.AlternateNodes != null && altFinal.AlternateNodes.Count > 0)
analysisAlternate = altFinal;
}
analysisAlternate = alternates[0];
}
}
analyzer.Dispose();
if (analysisAlternate != null && analysisAlternate.AlternateNodes.Count > 0)
if (analysisAlternate != null && analysisAlternate.AlternateNodes != null && analysisAlternate.AlternateNodes.Count > 0)
{
var node = analysisAlternate.AlternateNodes[0] as InkDrawingNode;
if (node == null)
return default;
return new ShapeRecognizeResult(node.Centroid, node.HotPoints, analysisAlternate, node);
}
return default;
}
/// <summary>兼容旧调用:等价于 <see cref="RecognizeShapeIACore"/>。</summary>
public static ShapeRecognizeResult RecognizeShape(StrokeCollection strokes) =>
RecognizeShapeIACore(strokes);
/// <summary>按设置选择 WinRT<see cref="InkRecognitionManager"/>)或 IACoreWinRT 请用 <see cref="RecognizeShapeUnifiedAsync"/>。</summary>
public static InkShapeRecognitionResult RecognizeShapeUnified(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
if (strokes == null || strokes.Count == 0)
return InkShapeRecognitionResult.Empty;
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
return InkShapeRecognitionResult.Empty;
var legacy = RecognizeShapeIACore(strokes);
return FromIACoreOrEmpty(legacy);
}
/// <summary>与 CE 反编译版 <c>InkRecognitionManager.RecognizeShapeAsync</c> 对齐的统一入口。</summary>
public static Task<InkShapeRecognitionResult> RecognizeShapeUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
if (strokes == null || strokes.Count == 0)
return Task.FromResult(InkShapeRecognitionResult.Empty);
return InkRecognitionManager.Instance.RecognizeShapeAsync(strokes, mode);
}
public static void WarmupShapeRecognition(ShapeRecognitionEngineMode mode)
{
try
{
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
{
WinRtInkShapeRecognizer.Warmup();
WinRtHandwritingRecognizer.Warmup();
}
else
RecognizeShapeIACore(new StrokeCollection());
}
catch
{
// 预热失败不影响启动
}
}
/// <summary>WinRT 手写识别(Windows 10+)。</summary>
public static Task<HandwritingRecognitionResult> RecognizeHandwritingUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode) =>
InkRecognitionManager.Instance.RecognizeHandwritingAsync(strokes, mode);
/// <summary>WinRT 下将识别成功的词替换为手写体字形墨迹;是否应用由设置「WinRT 识别转手写体字形」控制。</summary>
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode) =>
InkRecognitionManager.Instance.CorrectInkAsync(
strokes,
mode,
MainWindow.Settings?.InkToShape?.EnableWinRtHandwritingStrokeBeautify ?? false,
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
/// <summary>显式指定是否应用手写体字形替换(忽略开关);字体仍从设置读取。</summary>
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode,
bool applyHandwritingBeautify) =>
InkRecognitionManager.Instance.CorrectInkAsync(
strokes,
mode,
applyHandwritingBeautify,
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
internal static InkShapeRecognitionResult FromIACoreOrEmpty(ShapeRecognizeResult legacy)
{
if (legacy?.InkDrawingNode == null)
return InkShapeRecognitionResult.Empty;
var node = legacy.InkDrawingNode;
var shape = node.GetShape();
if (shape == null)
return InkShapeRecognitionResult.Empty;
var hot = ClonePointCollection(node.HotPoints);
return new InkShapeRecognitionResult(
node.GetShapeName(),
legacy.Centroid,
hot,
shape.Width,
shape.Height,
node.Strokes);
}
private static PointCollection ClonePointCollection(PointCollection src)
{
var dst = new PointCollection();
if (src == null) return dst;
foreach (System.Windows.Point p in src)
dst.Add(p);
return dst;
}
public static bool IsContainShapeType(string name)
{
if (string.IsNullOrEmpty(name))
return false;
if (name.Contains("Triangle") || name.Contains("Circle") ||
name.Contains("Rectangle") || name.Contains("Diamond") ||
name.Contains("Parallelogram") || name.Contains("Square")
+85
View File
@@ -0,0 +1,85 @@
using OSVersionExtension;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Media;
namespace Ink_Canvas.Helpers
{
/// <summary>墨迹形状识别后端:自动 / IACore / WinRT。</summary>
public enum ShapeRecognitionEngineMode
{
Auto = 0,
IACore = 1,
WinRT = 2,
}
public static class ShapeRecognitionRouter
{
/// <summary>
/// 自动模式:在 Windows 10 及以上系统默认使用 WinRT,否则使用 IACore。
/// </summary>
public static bool ResolveUseWinRt(ShapeRecognitionEngineMode mode)
{
if (mode == ShapeRecognitionEngineMode.WinRT) return true;
if (mode == ShapeRecognitionEngineMode.IACore) return false;
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
}
public static bool ShouldRunShapeRecognition(bool inkToShapeEnabled, ShapeRecognitionEngineMode mode)
{
if (!inkToShapeEnabled) return false;
if (ResolveUseWinRt(mode))
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
return true;
}
public static ShapeRecognitionEngineMode FromSettingsInt(int value)
{
if (value == (int)ShapeRecognitionEngineMode.IACore) return ShapeRecognitionEngineMode.IACore;
if (value == (int)ShapeRecognitionEngineMode.WinRT) return ShapeRecognitionEngineMode.WinRT;
return ShapeRecognitionEngineMode.Auto;
}
}
/// <summary>与具体识别后端无关的形状识别结果,供统一纠正模块消费。</summary>
public sealed class InkShapeRecognitionResult
{
public static readonly InkShapeRecognitionResult Empty = new InkShapeRecognitionResult();
private InkShapeRecognitionResult()
{
IsSuccess = false;
ShapeName = string.Empty;
Centroid = new Point();
HotPoints = new PointCollection();
StrokesToRemove = new StrokeCollection();
}
public InkShapeRecognitionResult(
string shapeName,
Point centroid,
PointCollection hotPoints,
double shapeWidth,
double shapeHeight,
StrokeCollection strokesToRemove)
{
ShapeName = shapeName ?? string.Empty;
Centroid = centroid;
HotPoints = hotPoints ?? new PointCollection();
ShapeWidth = shapeWidth;
ShapeHeight = shapeHeight;
StrokesToRemove = strokesToRemove ?? new StrokeCollection();
IsSuccess = StrokesToRemove.Count > 0
&& !string.IsNullOrEmpty(ShapeName)
&& ShapeName != "Drawing";
}
public bool IsSuccess { get; }
public string ShapeName { get; }
public Point Centroid { get; set; }
public PointCollection HotPoints { get; }
public double ShapeWidth { get; }
public double ShapeHeight { get; }
public StrokeCollection StrokesToRemove { get; }
}
}
+1
View File
@@ -83,6 +83,7 @@ namespace Ink_Canvas.Helpers
}
}
string logLine = string.Format("{0} [T{1}] [{2}] [{3}] {4}", DateTime.Now.ToString("O"), threadId, strLogType, callerInfo, str);
DebugConsoleManager.WriteLine(logLine);
ProcessProtectionManager.WithWriteAccess(file, () =>
{
using (StreamWriter sw = new StreamWriter(file, true))
+20 -6
View File
@@ -111,6 +111,14 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 绘制点段到新的DrawingVisual
/// </summary>
private static double PressureToVisualScale(float pressureFactor, bool ignorePressure)
{
if (ignorePressure)
return 1.0;
// 与 WPF 墨迹观感接近:0.5 为标称,压低变细、抬高变粗(预览此前固定 Pen 宽,等同忽略压感)
return Math.Max(0.22, Math.Min(2.1, 0.42 + 1.16 * pressureFactor));
}
private void DrawSegmentToNewVisual(int startIndex, int endIndex)
{
if (Stroke == null || Stroke.StylusPoints.Count == 0 || _visualCanvas == null) return;
@@ -118,6 +126,7 @@ namespace Ink_Canvas.Helpers
var points = Stroke.StylusPoints;
var drawingAttributes = Stroke.DrawingAttributes;
var ignorePressure = drawingAttributes.IgnorePressure;
// 创建新的DrawingVisual用于绘制这个点段
var segmentVisual = new DrawingVisual();
@@ -128,11 +137,6 @@ namespace Ink_Canvas.Helpers
using (var dc = segmentVisual.RenderOpen())
{
var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), drawingAttributes.Width);
pen.StartLineCap = PenLineCap.Round;
pen.EndLineCap = PenLineCap.Round;
pen.LineJoin = PenLineJoin.Round;
// 绘制指定范围内的点段
if (endIndex - startIndex >= 2)
{
@@ -141,6 +145,15 @@ namespace Ink_Canvas.Helpers
{
var startPoint = new Point(points[i].X, points[i].Y);
var endPoint = new Point(points[i + 1].X, points[i + 1].Y);
var s0 = PressureToVisualScale(points[i].PressureFactor, ignorePressure);
var s1 = PressureToVisualScale(points[i + 1].PressureFactor, ignorePressure);
var thickness = Math.Max(0.35, (drawingAttributes.Width * s0 + drawingAttributes.Width * s1) / 2.0);
var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), thickness)
{
StartLineCap = PenLineCap.Round,
EndLineCap = PenLineCap.Round,
LineJoin = PenLineJoin.Round
};
dc.DrawLine(pen, startPoint, endPoint);
}
}
@@ -149,8 +162,9 @@ namespace Ink_Canvas.Helpers
// 只有一个点,绘制圆点
var brush = new SolidColorBrush(drawingAttributes.Color);
var point = points[startIndex];
var s = PressureToVisualScale(point.PressureFactor, ignorePressure);
dc.DrawEllipse(brush, null, new Point(point.X, point.Y),
drawingAttributes.Width / 2, drawingAttributes.Height / 2);
drawingAttributes.Width * s / 2, drawingAttributes.Height * s / 2);
}
}
+26
View File
@@ -0,0 +1,26 @@
using System;
using System.Runtime.InteropServices;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// .NET Core / 5+ 未提供 <see cref="Marshal.GetActiveObject"/>,通过 OLE 实现等效行为。
/// </summary>
internal static class OleActiveObject
{
[DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern int CLSIDFromProgID(string lpszProgId, out Guid lpclsid);
[DllImport("oleaut32.dll", PreserveSig = true)]
private static extern int GetActiveObject(ref Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
public static object GetActiveObject(string progId)
{
int hr = CLSIDFromProgID(progId, out Guid clsid);
Marshal.ThrowExceptionForHR(hr);
hr = GetActiveObject(ref clsid, IntPtr.Zero, out object obj);
Marshal.ThrowExceptionForHR(hr);
return obj;
}
}
}
-1
View File
@@ -460,7 +460,6 @@ namespace Ink_Canvas.Helpers
_memoryStreams = new MemoryStream[_maxSlides + 2];
}
CurrentStrokes?.Clear();
LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace);
}
/// <summary>
+39 -28
View File
@@ -67,6 +67,7 @@ namespace Ink_Canvas.Helpers
private readonly object _lockObject = new object();
private bool _disposed;
private static bool IsPptBusyHResult(uint hr) => hr == 0x80010001 || hr == 0x8001010A;
private static bool IsNoActivePresentationHResult(uint hr) => hr == 0x80048240;
private const int ConnectionCheckIntervalTicks = 3;
private const int SlideShowCheckIntervalTicks = 2;
private const int WpsCheckIntervalTicks = 6;
@@ -150,13 +151,13 @@ namespace Ink_Canvas.Helpers
private void CheckAndConnectToPPT()
{
if (_isModuleUnloading) return;
lock (_lockObject)
{
try
{
if (_isModuleUnloading) return;
// 尝试连接到PowerPoint
var pptApp = TryConnectToPowerPoint();
if (pptApp == null && IsSupportWPS)
@@ -270,7 +271,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("PowerPoint.Application");
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("PowerPoint.Application");
if (pptApp != null && Marshal.IsComObject(pptApp))
{
@@ -297,7 +298,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("kwpp.Application");
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("kwpp.Application");
if (wpsApp != null && Marshal.IsComObject(wpsApp))
{
@@ -328,7 +329,7 @@ namespace Ink_Canvas.Helpers
_cachedIsConnected = true;
// 在主线程中注册事件,确保COM对象在正确的线程中
Application.Current?.Dispatcher?.Invoke(() =>
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
{
try
{
@@ -343,9 +344,8 @@ namespace Ink_Canvas.Helpers
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PPT事件注册失败: {ex}", LogHelper.LogType.Error);
throw; // 重新抛出异常,让外层处理
}
}, DispatcherPriority.Normal, CancellationToken.None, TimeSpan.FromSeconds(2));
}), DispatcherPriority.Normal);
// 获取当前演示文稿信息
UpdateCurrentPresentationInfo();
@@ -386,15 +386,19 @@ namespace Ink_Canvas.Helpers
if (Marshal.IsComObject(PPTApplication))
{
// 尝试在主线程中取消事件注册
Application.Current?.Dispatcher?.Invoke(() =>
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
{
try
{
PPTApplication.PresentationOpen -= OnPresentationOpen;
PPTApplication.PresentationClose -= OnPresentationClose;
PPTApplication.SlideShowBegin -= OnSlideShowBegin;
PPTApplication.SlideShowNextSlide -= OnSlideShowNextSlide;
PPTApplication.SlideShowEnd -= OnSlideShowEnd;
// 再次检查PPTApplication是否为null,因为可能在异步操作期间被修改
if (PPTApplication != null && Marshal.IsComObject(PPTApplication))
{
PPTApplication.PresentationOpen -= OnPresentationOpen;
PPTApplication.PresentationClose -= OnPresentationClose;
PPTApplication.SlideShowBegin -= OnSlideShowBegin;
PPTApplication.SlideShowNextSlide -= OnSlideShowNextSlide;
PPTApplication.SlideShowEnd -= OnSlideShowEnd;
}
}
catch (COMException comEx)
{
@@ -406,11 +410,20 @@ namespace Ink_Canvas.Helpers
// COM对象类型转换失败,通常是因为对象已经被释放
LogHelper.WriteLogToFile("PPT COM对象已被释放,跳过事件注册取消", LogHelper.LogType.Trace);
}
catch (System.Reflection.TargetInvocationException tie) when (tie.InnerException is InvalidComObjectException)
{
// RCW 已分离:Office Interop 内部通过反射创建 EventProvider 时抛出,是正常情况
LogHelper.WriteLogToFile("PPT COM对象RCW已分离,跳过事件注册取消", LogHelper.LogType.Trace);
}
catch (InvalidComObjectException)
{
LogHelper.WriteLogToFile("PPT COM对象RCW已分离,跳过事件注册取消", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"取消PPT事件注册时发生异常: {ex}", LogHelper.LogType.Warning);
}
}, DispatcherPriority.Normal, CancellationToken.None, TimeSpan.FromSeconds(1));
}), DispatcherPriority.Normal);
}
}
catch (Exception ex)
@@ -421,7 +434,7 @@ namespace Ink_Canvas.Helpers
SafeReleaseComObject(CurrentSlide, "CurrentSlide");
SafeReleaseComObject(CurrentSlides, "CurrentSlides");
SafeReleaseComObject(CurrentPresentation, "CurrentPresentation");
if (PPTApplication != null && Marshal.IsComObject(PPTApplication))
{
try
@@ -474,13 +487,13 @@ namespace Ink_Canvas.Helpers
_isModuleUnloading = false;
return;
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Thread.Sleep(1000);
_isModuleUnloading = false;
try
@@ -494,7 +507,7 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile("PPT联动模块重载时计时器已释放,跳过重启", LogHelper.LogType.Trace);
}
LogHelper.WriteLogToFile("PPT联动模块已重新加载", LogHelper.LogType.Trace);
}
catch (Exception ex)
@@ -555,7 +568,7 @@ namespace Ink_Canvas.Helpers
object view = null;
object selection = null;
object slideRange = null;
try
{
if (PPTApplication != null && Marshal.IsComObject(PPTApplication))
@@ -605,7 +618,7 @@ namespace Ink_Canvas.Helpers
if (view != null)
{
dynamic viewObj = view;
CurrentSlide = viewObj.Slide as Slide;
CurrentSlide = viewObj.Slide as Slide;
}
}
}
@@ -632,7 +645,7 @@ namespace Ink_Canvas.Helpers
}
}
}
if (CurrentSlide == null && SlidesCount > 0)
{
CurrentSlide = CurrentSlides[1];
@@ -642,7 +655,7 @@ namespace Ink_Canvas.Helpers
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr != 0x8001010E && hr != 0x80004005)
if (hr != 0x8001010E && hr != 0x80004005 && !IsNoActivePresentationHResult(hr))
{
LogHelper.WriteLogToFile($"获取当前幻灯片失败: {comEx.Message}", LogHelper.LogType.Warning);
}
@@ -664,7 +677,7 @@ namespace Ink_Canvas.Helpers
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706B5 || hr == 0x80048240)
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706B5 || IsNoActivePresentationHResult(hr))
{
CurrentPresentation = null;
CurrentSlides = null;
@@ -1076,7 +1089,7 @@ namespace Ink_Canvas.Helpers
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr == 0x80048240 || IsPptBusyHResult(hr))
if (IsNoActivePresentationHResult(hr) || IsPptBusyHResult(hr))
return null;
throw;
}
@@ -1094,7 +1107,7 @@ namespace Ink_Canvas.Helpers
var hr = (uint)comEx.HResult;
if (!IsPptBusyHResult(hr) && (hr == 0x8001010E || hr == 0x80004005))
DisconnectFromPPT();
if (!IsPptBusyHResult(hr) && hr != 0x80048240)
if (!IsPptBusyHResult(hr) && !IsNoActivePresentationHResult(hr))
LogHelper.WriteLogToFile($"获取当前活跃演示文稿失败: {comEx.Message}", LogHelper.LogType.Warning);
return CurrentPresentation;
}
@@ -1251,7 +1264,6 @@ namespace Ink_Canvas.Helpers
object slideNavigation = null;
try
{
LogHelper.WriteLogToFile($"尝试显示幻灯片导航 - 连接状态: {IsConnected}, 放映状态: {IsInSlideShow}", LogHelper.LogType.Trace);
if (!IsConnected || !IsInSlideShow || PPTApplication == null)
{
@@ -1284,7 +1296,6 @@ namespace Ink_Canvas.Helpers
{
dynamic sn = slideNavigation;
sn.Visible = true;
LogHelper.WriteLogToFile("成功显示幻灯片导航(PowerPoint模式)", LogHelper.LogType.Event);
return true;
}
+2 -2
View File
@@ -104,7 +104,7 @@ namespace Ink_Canvas.Helpers
try
{
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("PowerPoint.Application");
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("PowerPoint.Application");
if (pptApp != null && Marshal.IsComObject(pptApp))
{
try
@@ -124,7 +124,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("kwpp.Application");
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("kwpp.Application");
if (wpsApp != null && Marshal.IsComObject(wpsApp))
{
try
+50 -127
View File
@@ -86,18 +86,8 @@ namespace Ink_Canvas.Helpers
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Collapsed;
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Visible;
// 只有在页数有效时才更新页码显示
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
}
else
{
// 页数无效时清空页码显示
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
}
// 同步页码到所有翻页条 + 兼容旧绑定的隐藏 placeholder
SetPageNumberOnAllBars(currentSlide, totalSlides);
UpdateNavigationPanelsVisibility();
UpdateNavigationButtonStyles();
@@ -112,6 +102,11 @@ namespace Ink_Canvas.Helpers
MainWindow.MoveWindow(new WindowInteropHelper(_mainWindow).Handle, 0, 0,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height, true);
// MoveWindow 触发的 WM_WINDOWPOSCHANGING + 重绘会打断面板的 ShowWithFadeIn 动画,
// 在窗口尺寸最终确定后重新评估一次翻页面板的可见性。
UpdateNavigationPanelsVisibility();
UpdateNavigationButtonStyles();
}), DispatcherPriority.ApplicationIdle);
_mainWindow.isFullScreenApplied = true; // 标记已应用全屏处理
@@ -158,18 +153,7 @@ namespace Ink_Canvas.Helpers
{
try
{
// 只有在页数有效时才更新页码显示
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
}
else
{
// 页数无效时清空页码显示
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
}
SetPageNumberOnAllBars(currentSlide, totalSlides);
}
catch (Exception ex)
{
@@ -178,6 +162,34 @@ namespace Ink_Canvas.Helpers
});
}
private void SetPageNumberOnAllBars(int currentSlide, int totalSlides)
{
var bars = new[]
{
_mainWindow.LeftBottomPanelForPPTNavigation,
_mainWindow.RightBottomPanelForPPTNavigation,
_mainWindow.LeftSidePanelForPPTNavigation,
_mainWindow.RightSidePanelForPPTNavigation,
};
foreach (var bar in bars)
{
if (bar == null) continue;
bar.CurrentSlide = currentSlide;
bar.TotalSlides = totalSlides;
}
// 兼容旧绑定(其它界面通过 ElementName 引用 PPTBtnPageNow / PPTBtnPageTotal
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
}
else
{
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
}
}
/// <summary>
/// 处理PPT放映状态变化
/// </summary>
@@ -386,16 +398,17 @@ namespace Ink_Canvas.Helpers
// 页码按钮显示
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
_mainWindow.PPTLSPageButton.Visibility = pageButtonVisibility;
_mainWindow.PPTRSPageButton.Visibility = pageButtonVisibility;
_mainWindow.LeftSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
_mainWindow.RightSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
// 透明度设置 - 直接使用用户设置的透明度值
_mainWindow.PPTBtnLSBorder.Opacity = PPTLSButtonOpacity;
_mainWindow.PPTBtnRSBorder.Opacity = PPTRSButtonOpacity;
// 透明度
_mainWindow.LeftSidePanelForPPTNavigation.SetBarOpacity(PPTLSButtonOpacity);
_mainWindow.RightSidePanelForPPTNavigation.SetBarOpacity(PPTRSButtonOpacity);
// 颜色主题
bool isDarkTheme = options[2] == '2';
ApplyButtonTheme(_mainWindow.PPTBtnLSBorder, _mainWindow.PPTBtnRSBorder, isDarkTheme, true);
_mainWindow.LeftSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
_mainWindow.RightSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
}
catch (Exception ex)
{
@@ -414,113 +427,23 @@ namespace Ink_Canvas.Helpers
// 页码按钮显示
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
_mainWindow.PPTLBPageButton.Visibility = pageButtonVisibility;
_mainWindow.PPTRBPageButton.Visibility = pageButtonVisibility;
_mainWindow.LeftBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
_mainWindow.RightBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
// 透明度设置 - 直接使用用户设置的透明度值
_mainWindow.PPTBtnLBBorder.Opacity = PPTLBButtonOpacity;
_mainWindow.PPTBtnRBBorder.Opacity = PPTRBButtonOpacity;
// 透明度
_mainWindow.LeftBottomPanelForPPTNavigation.SetBarOpacity(PPTLBButtonOpacity);
_mainWindow.RightBottomPanelForPPTNavigation.SetBarOpacity(PPTRBButtonOpacity);
// 颜色主题
bool isDarkTheme = options[2] == '2';
ApplyButtonTheme(_mainWindow.PPTBtnLBBorder, _mainWindow.PPTBtnRBBorder, isDarkTheme, false);
_mainWindow.LeftBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
_mainWindow.RightBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新底部按钮样式失败: {ex}", LogHelper.LogType.Error);
}
}
private void ApplyButtonTheme(Border leftBorder, Border rightBorder, bool isDarkTheme, bool isSideButton)
{
try
{
Color backgroundColor, borderColor, foregroundColor, feedbackColor;
if (isDarkTheme)
{
backgroundColor = Color.FromRgb(39, 39, 42);
borderColor = Color.FromRgb(82, 82, 91);
foregroundColor = Colors.White;
feedbackColor = Colors.White;
}
else
{
backgroundColor = Color.FromRgb(244, 244, 245);
borderColor = Color.FromRgb(161, 161, 170);
foregroundColor = Color.FromRgb(39, 39, 42);
feedbackColor = Color.FromRgb(24, 24, 27);
}
// 应用背景和边框颜色
var backgroundBrush = new SolidColorBrush(backgroundColor);
var borderBrush = new SolidColorBrush(borderColor);
leftBorder.Background = backgroundBrush;
leftBorder.BorderBrush = borderBrush;
rightBorder.Background = backgroundBrush;
rightBorder.BorderBrush = borderBrush;
// 应用图标和文字颜色
var foregroundBrush = new SolidColorBrush(foregroundColor);
var feedbackBrush = new SolidColorBrush(feedbackColor);
if (isSideButton)
{
ApplySideButtonColors(foregroundBrush, feedbackBrush);
}
else
{
ApplyBottomButtonColors(foregroundBrush, feedbackBrush);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用按钮主题失败: {ex}", LogHelper.LogType.Error);
}
}
private void ApplySideButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
{
// 图标颜色
_mainWindow.PPTLSPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRSPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTLSNextButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRSNextButtonGeometry.Brush = foregroundBrush;
// 反馈背景颜色
_mainWindow.PPTLSPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRSPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLSPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRSPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLSNextButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRSNextButtonFeedbackBorder.Background = feedbackBrush;
// 文字颜色
TextBlock.SetForeground(_mainWindow.PPTLSPageButton, foregroundBrush);
TextBlock.SetForeground(_mainWindow.PPTRSPageButton, foregroundBrush);
}
private void ApplyBottomButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
{
// 图标颜色
_mainWindow.PPTLBPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRBPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTLBNextButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRBNextButtonGeometry.Brush = foregroundBrush;
// 反馈背景颜色
_mainWindow.PPTLBPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRBPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLBPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRBPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLBNextButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRBNextButtonFeedbackBorder.Background = feedbackBrush;
// 文字颜色
TextBlock.SetForeground(_mainWindow.PPTLBPageButton, foregroundBrush);
TextBlock.SetForeground(_mainWindow.PPTRBPageButton, foregroundBrush);
}
#endregion
}
}
+79
View File
@@ -0,0 +1,79 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using Windows.Data.Pdf;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 使用 Windows.Data.PdfWinRT)将 PDF 页渲染为 WPF 可用的位图。
/// </summary>
internal static class PdfWinRtHelper
{
public static async Task<uint> GetPageCountAsync(string pdfPath)
{
if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath))
return 0;
var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask();
var doc = await PdfDocument.LoadFromFileAsync(file).AsTask();
if (doc.IsPasswordProtected)
return 0;
return doc.PageCount;
}
public static async Task<BitmapSource> RenderPageToBitmapSourceAsync(string pdfPath, uint pageIndex)
{
if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath))
return null;
var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask();
var doc = await PdfDocument.LoadFromFileAsync(file).AsTask();
if (doc.IsPasswordProtected)
return null;
if (pageIndex >= doc.PageCount)
return null;
var page = doc.GetPage(pageIndex);
try
{
using (var ras = new InMemoryRandomAccessStream())
{
await page.RenderToStreamAsync(ras).AsTask();
ras.Seek(0);
var ms = new MemoryStream();
using (var netStream = ras.AsStreamForRead())
netStream.CopyTo(ms);
ms.Position = 0;
try
{
return await Application.Current.Dispatcher.InvokeAsync(() =>
{
var bi = new BitmapImage();
bi.BeginInit();
bi.StreamSource = ms;
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.EndInit();
bi.Freeze();
return (BitmapSource)bi;
});
}
finally
{
ms.Dispose();
}
}
}
finally
{
(page as IDisposable)?.Dispose();
}
}
}
}
@@ -1,274 +0,0 @@
using iNKORE.UI.WPF.Controls;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
{
/// <summary>
/// 启动台按钮控件
/// </summary>
public class LauncherButton
{
/// <summary>
/// 父插件
/// </summary>
private readonly SuperLauncherPlugin _plugin;
/// <summary>
/// 实际按钮控件
/// </summary>
private readonly SimpleStackPanel _panel;
/// <summary>
/// 获取按钮UI元素
/// </summary>
public UIElement Element => _panel;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="plugin">父插件</param>
public LauncherButton(SuperLauncherPlugin plugin)
{
try
{
_plugin = plugin;
LogHelper.WriteLogToFile("开始创建启动台按钮");
// 创建SimpleStackPanel
_panel = new SimpleStackPanel
{
Name = "Launcher_Icon",
Orientation = Orientation.Vertical,
HorizontalAlignment = HorizontalAlignment.Center,
Width = 28,
Margin = new Thickness(0, -2, 0, 0),
Background = Brushes.Transparent
};
LogHelper.WriteLogToFile("创建SimpleStackPanel完成");
// 添加图标
var image = CreateIconImage();
_panel.Children.Add(image);
// 添加文本
TextBlock textBlock = new TextBlock
{
Text = "启动台",
Foreground = Brushes.Black,
FontSize = 8,
Margin = new Thickness(0, 1, 0, 0),
TextAlignment = TextAlignment.Center
};
_panel.Children.Add(textBlock);
// 设置鼠标事件
_panel.MouseDown += Panel_MouseDown;
_panel.MouseUp += Panel_MouseUp;
_panel.MouseLeave += Panel_MouseLeave;
// 右键菜单支持
_panel.ContextMenu = CreateContextMenu();
// 设置工具提示
_panel.ToolTip = "启动台";
LogHelper.WriteLogToFile("启动台按钮创建完成");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
/// <summary>
/// 创建右键菜单
/// </summary>
private ContextMenu CreateContextMenu()
{
try
{
// 创建菜单
ContextMenu menu = new ContextMenu();
// 创建位置切换菜单项
MenuItem positionMenuItem = new MenuItem();
positionMenuItem.Header = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ?
"移至右侧" : "移至左侧";
positionMenuItem.Click += (s, e) =>
{
// 切换位置
_plugin.Config.ButtonPosition = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ?
LauncherButtonPosition.Right : LauncherButtonPosition.Left;
// 更新按钮位置
_plugin.UpdateButtonPosition();
// 保存配置
_plugin.SaveConfig();
LogHelper.WriteLogToFile($"通过右键菜单切换启动台按钮位置为: {_plugin.Config.ButtonPosition}");
};
menu.Items.Add(positionMenuItem);
// 添加设置菜单项
MenuItem settingsMenuItem = new MenuItem();
settingsMenuItem.Header = "打开设置";
settingsMenuItem.Click += (s, e) =>
{
// 打开插件设置窗口
var mainWindow = Application.Current.MainWindow;
if (mainWindow != null)
{
try
{
// 使用反射调用主窗口的ShowPluginSettings方法
var method = mainWindow.GetType().GetMethod("ShowPluginSettings");
if (method != null)
{
method.Invoke(mainWindow, null);
LogHelper.WriteLogToFile("已打开插件设置窗口");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"打开插件设置窗口失败: {ex.Message}", LogHelper.LogType.Error);
}
}
};
menu.Items.Add(settingsMenuItem);
return menu;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建右键菜单时出错: {ex.Message}", LogHelper.LogType.Error);
return null;
}
}
/// <summary>
/// 获取实际的UI元素
/// </summary>
[Obsolete("使用Element属性代替")]
public UIElement GetUIElement()
{
return _panel;
}
/// <summary>
/// 创建图标图像
/// </summary>
private Image CreateIconImage()
{
try
{
// 创建图像
Image image = new Image
{
Height = 17,
Margin = new Thickness(0, 3, 0, 0)
};
// 设置位图缩放模式
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
// 创建绘图图像
DrawingImage drawingImage = new DrawingImage();
DrawingGroup drawingGroup = new DrawingGroup();
drawingGroup.ClipGeometry = Geometry.Parse("M0,0 V24 H24 V0 H0 Z");
// 使用提供的应用网格图标
GeometryDrawing geometryDrawing = new GeometryDrawing
{
Brush = new SolidColorBrush(Color.FromRgb(0x1B, 0x1B, 0x1B)),
Geometry = Geometry.Parse("F0 M24,24z M0,0z M4.41721,4.29873C4.35178,4.29873,4.29873,4.35178,4.29873,4.41721L4.29873,9.15646C4.29873,9.22189,4.35178,9.27494,4.41721,9.27494L9.15646,9.27494C9.22189,9.27494,9.27494,9.22189,9.27494,9.15646L9.27494,4.41721C9.27494,4.35178,9.22189,4.29873,9.15646,4.29873L4.41721,4.29873z M2.64,4.41721C2.64,3.43569,3.43569,2.64,4.41721,2.64L9.15646,2.64C10.138,2.64,10.9337,3.43569,10.9337,4.41721L10.9337,9.15646C10.9337,10.138,10.138,10.9337,9.15646,10.9337L4.41721,10.9337C3.43569,10.9337,2.64,10.138,2.64,9.15646L2.64,4.41721z M14.8435,4.29873C14.7781,4.29873,14.7251,4.35178,14.7251,4.41721L14.7251,9.15646C14.7251,9.22189,14.7781,9.27494,14.8435,9.27494L19.5828,9.27494C19.6482,9.27494,19.7013,9.22189,19.7013,9.15646L19.7013,4.41721C19.7013,4.35178,19.6482,4.29873,19.5828,4.29873L14.8435,4.29873z M13.0663,4.41721C13.0663,3.43569,13.862,2.64,14.8435,2.64L19.5828,2.64C20.5643,2.64,21.36,3.43569,21.36,4.41721L21.36,9.15646C21.36,10.138,20.5643,10.9337,19.5828,10.9337L14.8435,10.9337C13.862,10.9337,13.0663,10.138,13.0663,9.15646L13.0663,4.41721z M14.8435,14.7251C14.7781,14.7251,14.7251,14.7781,14.7251,14.8435L14.7251,19.5828C14.7251,19.6482,14.7781,19.7013,14.8435,19.7013L19.5828,19.7013C19.6482,19.7013,19.7013,19.6482,19.7013,19.5828L19.7013,14.8435C19.7013,14.7781,19.6482,14.7251,19.5828,14.7251L14.8435,14.7251z M13.0663,14.8435C13.0663,13.862,13.862,13.0663,14.8435,13.0663L19.5828,13.0663C20.5643,13.0663,21.36,13.862,21.36,14.8435L21.36,19.5828C21.36,20.5643,20.5643,21.36,19.5828,21.36L14.8435,21.36C13.862,21.36,13.0663,20.5643,13.0663,19.5828L13.0663,14.8435z M4.41721,14.7251C4.35178,14.7251,4.29873,14.7781,4.29873,14.8435L4.29873,19.5828C4.29873,19.6482,4.35178,19.7013,4.41721,19.7013L9.15646,19.7013C9.22189,19.7013,9.27494,19.6482,9.27494,19.5828L9.27494,14.8435C9.27494,14.7781,9.22189,14.7251,9.15646,14.7251L4.41721,14.7251z M2.64,14.8435C2.64,13.862,3.43569,13.0663,4.41721,13.0663L9.15646,13.0663C10.138,13.0663,10.9337,13.862,10.9337,14.8435L10.9337,19.5828C10.9337,20.5643,10.138,21.36,9.15646,21.36L4.41721,21.36C3.43569,21.36,2.64,20.5643,2.64,19.5828L2.64,14.8435z")
};
drawingGroup.Children.Add(geometryDrawing);
// 设置图像源
drawingImage.Drawing = drawingGroup;
image.Source = drawingImage;
return image;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建图标图像时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
// 返回一个空图像
return new Image();
}
}
/// <summary>
/// 鼠标按下事件
/// </summary>
private void Panel_MouseDown(object sender, MouseButtonEventArgs e)
{
try
{
// 提供反馈
_panel.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
LogHelper.WriteLogToFile("启动台按钮鼠标按下");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动台按钮鼠标按下事件出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 鼠标抬起事件
/// </summary>
private void Panel_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
// 只有左键点击才显示启动台窗口
if (e.ChangedButton != MouseButton.Left)
{
return;
}
// 恢复背景
_panel.Background = Brushes.Transparent;
LogHelper.WriteLogToFile("启动台按钮鼠标抬起,准备显示启动台窗口");
// 获取按钮在屏幕上的位置
Point buttonPosition = _panel.PointToScreen(new Point(_panel.ActualWidth / 2, 0));
// 显示启动台窗口
_plugin.ShowLauncherWindow(buttonPosition);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动台按钮鼠标抬起事件出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
/// <summary>
/// 鼠标离开事件
/// </summary>
private void Panel_MouseLeave(object sender, MouseEventArgs e)
{
try
{
// 恢复背景
_panel.Background = Brushes.Transparent;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动台按钮鼠标离开事件出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
@@ -1,332 +0,0 @@
using Microsoft.Win32;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
{
/// <summary>
/// 启动台按钮位置
/// </summary>
public enum LauncherButtonPosition
{
/// <summary>
/// 左侧
/// </summary>
Left,
/// <summary>
/// 右侧
/// </summary>
Right
}
/// <summary>
/// 启动台配置
/// </summary>
public class LauncherConfig
{
/// <summary>
/// 启动台按钮位置
/// </summary>
public LauncherButtonPosition ButtonPosition { get; set; } = LauncherButtonPosition.Right;
/// <summary>
/// 启动台应用程序列表
/// </summary>
public List<LauncherItem> Items { get; set; } = new List<LauncherItem>();
}
/// <summary>
/// 启动台应用项
/// </summary>
public class LauncherItem
{
/// <summary>
/// 应用程序名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 应用程序路径
/// </summary>
public string Path { get; set; }
/// <summary>
/// 是否可见
/// </summary>
public bool IsVisible { get; set; } = true;
/// <summary>
/// 在启动台中的位置(0-39
/// </summary>
public int Position { get; set; } = -1;
/// <summary>
/// 是否已固定位置
/// </summary>
public bool IsPositionFixed { get; set; } = false;
/// <summary>
/// 图标缓存
/// </summary>
[JsonIgnore]
private ImageSource _iconCache;
/// <summary>
/// 获取应用程序图标
/// </summary>
[JsonIgnore]
public ImageSource Icon
{
get
{
if (_iconCache != null)
{
return _iconCache;
}
try
{
if (File.Exists(Path))
{
// 从文件中获取图标
Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(Path);
if (icon != null)
{
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
icon.Dispose();
return _iconCache;
}
}
else
{
// 从注册表中获取文件类型关联图标
string extension = System.IO.Path.GetExtension(Path);
if (!string.IsNullOrEmpty(extension))
{
string fileType = Registry.ClassesRoot.OpenSubKey(extension)?.GetValue(string.Empty) as string;
if (!string.IsNullOrEmpty(fileType))
{
string iconPath = Registry.ClassesRoot.OpenSubKey(fileType + "\\DefaultIcon")?.GetValue(string.Empty) as string;
if (!string.IsNullOrEmpty(iconPath))
{
string[] parts = iconPath.Split(',');
string iconFile = parts[0].Trim('"');
int iconIndex = parts.Length > 1 ? Convert.ToInt32(parts[1]) : 0;
if (File.Exists(iconFile))
{
Icon icon = IconExtractor.Extract(iconFile, iconIndex, true);
if (icon != null)
{
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
icon.Dispose();
return _iconCache;
}
}
}
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取应用图标时出错: {ex.Message}", LogHelper.LogType.Error);
}
// 返回默认图标
return GetDefaultIcon();
}
}
/// <summary>
/// 获取默认图标
/// </summary>
private ImageSource GetDefaultIcon()
{
try
{
// 对于资源管理器,使用特定图标
if (Path.EndsWith("explorer.exe", StringComparison.OrdinalIgnoreCase))
{
try
{
// 直接从C:\Windows\explorer.exe获取图标
string explorerPath = @"C:\Windows\explorer.exe";
if (File.Exists(explorerPath))
{
Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(explorerPath);
if (icon != null)
{
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
icon.Dispose();
return _iconCache;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取资源管理器图标时出错: {ex.Message}", LogHelper.LogType.Warning);
// 如果获取Windows图标失败,回退到默认图标
}
// 回退到备用图标
string explorerIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_folder_24_regular.png");
if (File.Exists(explorerIconPath))
{
Uri uri = new Uri(explorerIconPath);
BitmapImage image = new BitmapImage(uri);
_iconCache = image;
return _iconCache;
}
}
// 返回一个简单的默认图标
string iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-png", "icc.png");
if (File.Exists(iconPath))
{
Uri uri = new Uri(iconPath);
BitmapImage image = new BitmapImage(uri);
_iconCache = image;
return _iconCache;
}
// 如果还是没有找到,尝试使用应用程序图标
string appIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_apps_24_regular.png");
if (File.Exists(appIconPath))
{
Uri uri = new Uri(appIconPath);
BitmapImage image = new BitmapImage(uri);
_iconCache = image;
return _iconCache;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取默认图标时出错: {ex.Message}", LogHelper.LogType.Error);
}
return null;
}
/// <summary>
/// 启动应用程序
/// </summary>
public void Launch()
{
try
{
if (string.IsNullOrEmpty(Path))
{
LogHelper.WriteLogToFile("无法启动应用程序:路径为空", LogHelper.LogType.Error);
return;
}
// 检查文件是否存在
if (!File.Exists(Path) && !Path.Contains(":\\"))
{
// 可能是系统命令,如explorer.exe
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = Path,
UseShellExecute = true
};
Process.Start(psi);
}
else
{
// 使用Process.Start启动应用程序
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = Path,
UseShellExecute = true
};
Process.Start(psi);
}
LogHelper.WriteLogToFile($"已启动应用程序: {Path}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动应用程序时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"启动应用程序时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
/// <summary>
/// 图标提取工具类
/// </summary>
public static class IconExtractor
{
/// <summary>
/// 从文件中提取图标
/// </summary>
/// <param name="file">文件路径</param>
/// <param name="index">图标索引</param>
/// <param name="largeIcon">是否提取大图标</param>
/// <returns>提取的图标</returns>
public static Icon Extract(string file, int index, bool largeIcon)
{
try
{
IntPtr large;
IntPtr small;
ExtractIconEx(file, index, out large, out small, 1);
try
{
return Icon.FromHandle(largeIcon ? large : small);
}
catch
{
return null;
}
finally
{
if (large != IntPtr.Zero)
DestroyIcon(large);
if (small != IntPtr.Zero)
DestroyIcon(small);
}
}
catch
{
return null;
}
}
[DllImport("Shell32.dll", EntryPoint = "ExtractIconEx")]
private static extern int ExtractIconEx(
[MarshalAs(UnmanagedType.LPStr)] string lpszFile,
int nIconIndex,
out IntPtr phiconLarge,
out IntPtr phiconSmall,
int nIcons);
[DllImport("User32.dll")]
private static extern int DestroyIcon(IntPtr hIcon);
}
}
@@ -1,143 +0,0 @@
<UserControl x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="600">
<UserControl.Resources>
<!-- 自定义按钮样式 -->
<Style x:Key="DefaultButtonStyle" TargetType="Button">
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.8"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="Black" Direction="270" ShadowDepth="2" Opacity="0.3" BlurRadius="4"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.6"/>
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
</Setter.Value>
</Setter>
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.4"/>
<Setter Property="Cursor" Value="Arrow"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题 -->
<TextBlock Grid.Row="0" Text="超级启动台设置" FontSize="16" FontWeight="Bold" Margin="0,0,0,15" Foreground="Black"/>
<!-- 基本设置 -->
<StackPanel Grid.Row="1" Margin="0,0,0,15">
<TextBlock Text="基本设置" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 按钮位置 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="按钮位置:" VerticalAlignment="Center" Foreground="Black"/>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="0,5">
<RadioButton x:Name="RbtnLeft" Content="浮动栏左侧" Margin="0,0,20,0" Checked="RbtnPosition_Checked" Foreground="Black"/>
<RadioButton x:Name="RbtnRight" Content="浮动栏右侧" IsChecked="True" Checked="RbtnPosition_Checked" Foreground="Black"/>
</StackPanel>
</Grid>
</StackPanel>
<!-- 应用管理 -->
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="应用管理" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
<Border Grid.Row="1" BorderThickness="1" BorderBrush="#CCCCCC" CornerRadius="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 应用列表 -->
<DataGrid Grid.Row="0" x:Name="DgApps" AutoGenerateColumns="False" Margin="5"
CanUserAddRows="False" CanUserDeleteRows="False"
HeadersVisibility="Column" SelectionMode="Single"
SelectionChanged="DgApps_SelectionChanged">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="显示" Binding="{Binding IsVisible}" Width="50"/>
<DataGridTextColumn Header="名称" Binding="{Binding Name}" Width="150"/>
<DataGridTextColumn Header="路径" Binding="{Binding Path}" Width="*"/>
<DataGridTextColumn Header="位置" Binding="{Binding Position}" Width="50"/>
</DataGrid.Columns>
</DataGrid>
<!-- 操作按钮 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="5">
<Button x:Name="BtnAdd" Content="添加" Padding="10,5" Margin="0,5,5,5" Click="BtnAdd_Click"
Background="#FF007ACC" Foreground="White" BorderBrush="#FF005A9B" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
<Button x:Name="BtnEdit" Content="编辑" Padding="10,5" Margin="5" Click="BtnEdit_Click"
Background="#FF6C757D" Foreground="White" BorderBrush="#FF5A6268" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
<Button x:Name="BtnDelete" Content="删除" Padding="10,5" Margin="5" Click="BtnDelete_Click"
Background="#FFDC3545" Foreground="White" BorderBrush="#FFBD2130" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
<!-- 底部按钮 -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0">
<Button x:Name="BtnSave" Content="保存设置" Padding="15,5" Click="BtnSave_Click"
Background="#FF28A745" Foreground="White" BorderBrush="#FF1E7E34" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
</StackPanel>
</Grid>
</UserControl>
@@ -1,396 +0,0 @@
using Ink_Canvas.Windows;
using Microsoft.Win32;
using System;
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
{
/// <summary>
/// LauncherSettingsControl.xaml 的交互逻辑
/// </summary>
public partial class LauncherSettingsControl : UserControl
{
/// <summary>
/// 父插件
/// </summary>
private readonly SuperLauncherPlugin _plugin;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="plugin">父插件</param>
public LauncherSettingsControl(SuperLauncherPlugin plugin)
{
InitializeComponent();
_plugin = plugin;
// 设置按钮位置
RbtnLeft.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left;
RbtnRight.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Right;
// 绑定应用列表
DgApps.ItemsSource = _plugin.LauncherItems;
// 初始化按钮状态
UpdateButtonStates();
}
/// <summary>
/// 更新按钮状态
/// </summary>
private void UpdateButtonStates()
{
bool hasSelection = DgApps.SelectedItem != null;
BtnEdit.IsEnabled = hasSelection;
BtnDelete.IsEnabled = hasSelection;
}
/// <summary>
/// 位置单选按钮选择事件
/// </summary>
private void RbtnPosition_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded) return;
LauncherButtonPosition oldPosition = _plugin.Config.ButtonPosition;
if (sender == RbtnLeft)
{
_plugin.Config.ButtonPosition = LauncherButtonPosition.Left;
}
else if (sender == RbtnRight)
{
_plugin.Config.ButtonPosition = LauncherButtonPosition.Right;
}
// 如果位置发生变化,更新按钮位置
if (oldPosition != _plugin.Config.ButtonPosition)
{
try
{
// 更新按钮位置
_plugin.UpdateButtonPosition();
// 保存配置
_plugin.SaveConfig();
LogHelper.WriteLogToFile($"启动台按钮位置已更改为: {_plugin.Config.ButtonPosition}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"更新启动台按钮位置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
/// <summary>
/// 添加按钮点击事件
/// </summary>
private void BtnAdd_Click(object sender, RoutedEventArgs e)
{
try
{
// 创建新的启动项
LauncherItem item = new LauncherItem
{
Name = "",
Path = "",
IsVisible = true,
Position = -1 // 让插件管理器分配位置
};
// 直接显示编辑对话框
EditLauncherItem(item, true);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"添加启动项时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"添加启动项时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 编辑应用按钮点击事件
/// </summary>
private void BtnEdit_Click(object sender, RoutedEventArgs e)
{
if (DgApps.SelectedItem is LauncherItem item)
{
EditLauncherItem(item, false);
}
}
/// <summary>
/// 删除应用按钮点击事件
/// </summary>
private void BtnDelete_Click(object sender, RoutedEventArgs e)
{
if (DgApps.SelectedItem is LauncherItem item)
{
// 确认删除
MessageBoxResult result = MessageBox.Show(
$"确定要删除 {item.Name} 吗?",
"删除确认",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
// 从集合中移除
_plugin.LauncherItems.Remove(item);
// 保存配置
_plugin.SaveConfig();
}
}
}
/// <summary>
/// 保存设置按钮点击事件
/// </summary>
private void BtnSave_Click(object sender, RoutedEventArgs e)
{
try
{
// 保存配置
_plugin.SaveConfig();
// 如果插件已启用,重新加载启动台按钮
if (_plugin.IsEnabled)
{
_plugin.Disable();
_plugin.Enable();
}
else
{
// 如果插件未启用,则启用它
_plugin.Enable();
// 通知PluginSettingsWindow刷新插件列表
var window = Window.GetWindow(this);
if (window is PluginSettingsWindow pluginSettingsWindow)
{
// 触发刷新
pluginSettingsWindow.RefreshPluginList();
}
}
MessageBox.Show("设置已保存并应用!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存设置时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"保存设置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 应用项选择变更事件
/// </summary>
private void DgApps_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
UpdateButtonStates();
}
/// <summary>
/// 编辑启动项
/// </summary>
/// <param name="item">启动项</param>
/// <param name="isNew">是否为新建</param>
private void EditLauncherItem(LauncherItem item, bool isNew)
{
// 创建简单的编辑窗口
Window editWindow = new Window
{
Title = isNew ? "添加" : "编辑应用",
Width = 400,
Height = 200,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
ResizeMode = ResizeMode.NoResize
};
// 创建编辑表单
Grid grid = new Grid
{
Margin = new Thickness(20)
};
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
// 名称输入框
TextBlock nameLabel = new TextBlock
{
Text = "名称:",
VerticalAlignment = VerticalAlignment.Center
};
TextBox nameTextBox = new TextBox
{
Text = item.Name,
Margin = new Thickness(0, 5, 0, 5)
};
Grid.SetRow(nameLabel, 0);
Grid.SetColumn(nameLabel, 0);
Grid.SetRow(nameTextBox, 0);
Grid.SetColumn(nameTextBox, 1);
grid.Children.Add(nameLabel);
grid.Children.Add(nameTextBox);
// 路径输入框
TextBlock pathLabel = new TextBlock
{
Text = "路径:",
VerticalAlignment = VerticalAlignment.Center
};
Grid pathGrid = new Grid();
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength() });
TextBox pathTextBox = new TextBox
{
Text = item.Path,
Margin = new Thickness(0, 5, 5, 5)
};
Button browseButton = new Button
{
Content = "浏览",
Padding = new Thickness(5, 0, 5, 0),
Margin = new Thickness(0, 5, 0, 5)
};
browseButton.Click += (s, e) =>
{
OpenFileDialog dialog = new OpenFileDialog
{
Title = "选择应用程序",
Filter = "应用程序 (*.exe)|*.exe|所有文件 (*.*)|*.*",
Multiselect = false,
FileName = pathTextBox.Text
};
if (dialog.ShowDialog() == true)
{
pathTextBox.Text = dialog.FileName;
// 如果选择的是.exe文件,自动获取文件名填入名称字段
if (Path.GetExtension(dialog.FileName).ToLower() == ".exe")
{
string fileName = Path.GetFileNameWithoutExtension(dialog.FileName);
// 只有在名称字段为空或者是新建项目时才自动填入
if (string.IsNullOrWhiteSpace(nameTextBox.Text) || isNew)
{
nameTextBox.Text = fileName;
}
}
}
};
Grid.SetColumn(pathTextBox, 0);
Grid.SetColumn(browseButton, 1);
pathGrid.Children.Add(pathTextBox);
pathGrid.Children.Add(browseButton);
Grid.SetRow(pathLabel, 1);
Grid.SetColumn(pathLabel, 0);
Grid.SetRow(pathGrid, 1);
Grid.SetColumn(pathGrid, 1);
grid.Children.Add(pathLabel);
grid.Children.Add(pathGrid);
// 确认和取消按钮
StackPanel buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 10, 0, 0)
};
Button okButton = new Button
{
Content = "确定",
Padding = new Thickness(15, 5, 15, 5),
Margin = new Thickness(0, 0, 10, 0),
IsDefault = true
};
Button cancelButton = new Button
{
Content = "取消",
Padding = new Thickness(15, 5, 15, 5),
IsCancel = true
};
okButton.Click += (s, e) =>
{
// 验证输入
if (string.IsNullOrWhiteSpace(nameTextBox.Text))
{
MessageBox.Show("请输入应用名称!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (string.IsNullOrWhiteSpace(pathTextBox.Text))
{
MessageBox.Show("请输入应用路径!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
// 更新项目
item.Name = nameTextBox.Text;
item.Path = pathTextBox.Text;
// 如果是新建,添加到集合
if (isNew)
{
_plugin.AddLauncherItem(item);
}
else
{
// 触发属性变更通知,刷新DataGrid
if (DgApps.ItemsSource is ICollectionView view)
{
view.Refresh();
}
// 保存配置
_plugin.SaveConfig();
}
editWindow.DialogResult = true;
editWindow.Close();
};
cancelButton.Click += (s, e) =>
{
editWindow.DialogResult = false;
editWindow.Close();
};
buttonPanel.Children.Add(okButton);
buttonPanel.Children.Add(cancelButton);
Grid.SetRow(buttonPanel, 2);
Grid.SetColumnSpan(buttonPanel, 2);
grid.Children.Add(buttonPanel);
// 设置窗口内容
editWindow.Content = grid;
// 显示窗口
editWindow.ShowDialog();
}
}
}
@@ -1,91 +0,0 @@
<Window x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
mc:Ignorable="d"
Title="启动台"
Width="400"
Height="300"
WindowStyle="None"
AllowsTransparency="True"
Background="#80000000"
ResizeMode="NoResize"
Topmost="True"
Deactivated="Window_Deactivated"
ShowInTaskbar="False">
<Window.Resources>
<!-- 应用项样式 -->
<Style x:Key="LauncherItemStyle" TargetType="Button">
<Setter Property="Width" Value="80"/>
<Setter Property="Height" Value="80"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Background" Value="#40FFFFFF"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Image Grid.Row="0" Source="{Binding Icon}" Width="32" Height="32" Margin="0,10,0,5"/>
<TextBlock Grid.Row="1" Text="{Binding Name}" TextWrapping="Wrap" TextAlignment="Center"
Margin="2,0,2,8" FontSize="11" Foreground="White"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#80FFFFFF"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#C0FFFFFF"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Border CornerRadius="15" Background="#80000000" BorderThickness="1" BorderBrush="#40FFFFFF">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 标题栏 -->
<Grid Grid.Row="0" Height="40">
<TextBlock Text="启动台" Foreground="White" FontSize="18" FontWeight="Bold"
VerticalAlignment="Center" Margin="15,0,0,0"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,10,0">
<Button x:Name="BtnFixMode" Click="BtnFixMode_Click" Width="30" Height="30"
Margin="5,0" Background="Transparent" BorderThickness="0"
ToolTip="切换固定模式">
<Path x:Name="FixModeIcon" Data="M7,2V13H10V22L17,10H13L17,2H7Z" Fill="White" Stretch="Uniform" Width="16" Height="16"/>
</Button>
<Button x:Name="BtnClose" Click="BtnClose_Click" Width="30" Height="30"
Background="Transparent" BorderThickness="0"
ToolTip="关闭">
<Path Data="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
Fill="White" Stretch="Uniform" Width="16" Height="16"/>
</Button>
</StackPanel>
</Grid>
<!-- 应用网格 -->
<ScrollViewer Grid.Row="1" Margin="10" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<WrapPanel x:Name="AppPanel" Orientation="Horizontal" HorizontalAlignment="Center"/>
</ScrollViewer>
</Grid>
</Border>
</Window>
@@ -1,466 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
{
/// <summary>
/// LauncherWindow.xaml 的交互逻辑
/// </summary>
public partial class LauncherWindow : Window
{
/// <summary>
/// 父插件
/// </summary>
private readonly SuperLauncherPlugin _plugin;
/// <summary>
/// 是否处于固定模式
/// </summary>
private bool _isFixMode;
/// <summary>
/// 应用项按钮列表
/// </summary>
private readonly Dictionary<Button, LauncherItem> _appButtons = new Dictionary<Button, LauncherItem>();
/// <summary>
/// 拖拽中的按钮
/// </summary>
private Button _draggingButton;
/// <summary>
/// 拖拽开始位置
/// </summary>
private Point _dragStartPoint;
/// <summary>
/// 构造函数
/// </summary>
public LauncherWindow(SuperLauncherPlugin plugin)
{
InitializeComponent();
_plugin = plugin;
// 加载应用项
LoadLauncherItems();
// 添加鼠标按下事件(用于拖动窗口)
MouseDown += (s, e) =>
{
if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed)
{
DragMove();
}
};
// 根据应用数量调整窗口大小
AdjustWindowSize();
}
/// <summary>
/// 加载启动台应用项
/// </summary>
private void LoadLauncherItems()
{
// 清空现有应用项
AppPanel.Children.Clear();
_appButtons.Clear();
// 获取显示的应用项
var visibleItems = _plugin.LauncherItems
.Where(item => item.IsVisible)
.OrderBy(item => item.Position)
.ToList();
foreach (var item in visibleItems)
{
// 创建应用按钮
Button appButton = new Button
{
Style = (Style)FindResource("LauncherItemStyle"),
DataContext = item,
Tag = item.Position
};
// 添加点击事件
appButton.Click += AppButton_Click;
// 在固定模式下,添加拖拽事件
appButton.PreviewMouseDown += AppButton_PreviewMouseDown;
appButton.PreviewMouseMove += AppButton_PreviewMouseMove;
appButton.PreviewMouseUp += AppButton_PreviewMouseUp;
// 记录按钮和项目的对应关系
_appButtons.Add(appButton, item);
// 添加到面板
AppPanel.Children.Add(appButton);
}
}
/// <summary>
/// 根据应用数量调整窗口大小
/// </summary>
private void AdjustWindowSize()
{
try
{
// 每行最多显示4个应用
const int appsPerRow = 4;
// 计算行数
int visibleCount = _appButtons.Count;
int rowCount = (int)Math.Ceiling(visibleCount / (double)appsPerRow);
// 设置窗口宽度(每个应用90像素宽 = 80 + 5*2
Width = Math.Min(appsPerRow * 90 + 40, 400); // 最大宽度400
// 设置窗口高度(每个应用90像素高 = 80 + 5*2
Height = Math.Min(rowCount * 90 + 60, 600); // 最大高度600,标题栏40 + 边距20
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"调整启动台窗口大小时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 应用按钮点击事件
/// </summary>
private void AppButton_Click(object sender, RoutedEventArgs e)
{
try
{
if (_isFixMode) return; // 在固定模式下,不响应点击事件
if (sender is Button button && _appButtons.TryGetValue(button, out LauncherItem item))
{
// 获取应用路径和名称,用于后续启动
string appPath = item.Path;
string appName = item.Name;
LogHelper.WriteLogToFile($"点击启动应用: {appName}, 路径: {appPath}");
// 首先标记窗口正在关闭
IsClosing = true;
// 创建一个应用启动任务
var launchTask = new Task(() =>
{
try
{
// 等待一段时间,确保窗口关闭流程已经开始
Thread.Sleep(200);
// 使用UI线程启动应用
Application.Current.Dispatcher.Invoke(() =>
{
try
{
// 检查应用路径是否存在
if (File.Exists(appPath) || !appPath.Contains(":\\"))
{
// 创建进程启动信息
var psi = new ProcessStartInfo
{
FileName = appPath,
UseShellExecute = true,
};
// 启动应用程序
var process = Process.Start(psi);
LogHelper.WriteLogToFile($"应用程序 {appName} 已启动");
}
else
{
LogHelper.WriteLogToFile($"应用路径不存在: {appPath}", LogHelper.LogType.Error);
MessageBox.Show($"找不到应用程序: {appPath}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动应用程序失败: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"启动应用程序失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用启动任务出错: {ex.Message}", LogHelper.LogType.Error);
}
});
// 关闭窗口
try
{
Dispatcher.BeginInvoke(new Action(() =>
{
try { Close(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
// 启动应用程序任务
launchTask.Start();
}), DispatcherPriority.Background);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"关闭窗口或启动任务时出错: {ex.Message}", LogHelper.LogType.Error);
// 如果无法通过UI关闭窗口,直接启动任务
launchTask.Start();
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用按钮点击事件出错: {ex.Message}", LogHelper.LogType.Error);
try { IsClosing = true; Close(); } catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
}
}
#region
/// <summary>
/// 应用按钮鼠标按下事件
/// </summary>
private void AppButton_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (!_isFixMode) return;
if (e.ChangedButton == MouseButton.Left && sender is Button button)
{
_draggingButton = button;
_dragStartPoint = e.GetPosition(AppPanel);
button.CaptureMouse();
button.Opacity = 0.7;
// 阻止事件冒泡,以避免触发按钮点击
e.Handled = true;
}
}
/// <summary>
/// 应用按钮鼠标移动事件
/// </summary>
private void AppButton_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (!_isFixMode || _draggingButton == null) return;
if (e.LeftButton == MouseButtonState.Pressed)
{
Point currentPosition = e.GetPosition(AppPanel);
// 移动按钮
System.Windows.Controls.Canvas.SetLeft(_draggingButton, currentPosition.X - _draggingButton.ActualWidth / 2);
System.Windows.Controls.Canvas.SetTop(_draggingButton, currentPosition.Y - _draggingButton.ActualHeight / 2);
// 将按钮移到最上层
Panel.SetZIndex(_draggingButton, 100);
// 阻止事件冒泡
e.Handled = true;
}
}
/// <summary>
/// 应用按钮鼠标释放事件
/// </summary>
private void AppButton_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (!_isFixMode || _draggingButton == null) return;
// 释放鼠标捕获
_draggingButton.ReleaseMouseCapture();
// 计算新位置
Point releasePoint = e.GetPosition(AppPanel);
int newPosition = CalculateGridPosition(releasePoint);
// 获取当前项目
LauncherItem currentItem = _appButtons[_draggingButton];
// 重新排序
ReorderItems(currentItem, newPosition);
// 重新加载应用项
LoadLauncherItems();
// 保存配置
_plugin.SaveConfig();
// 清除拖拽状态
_draggingButton.Opacity = 1;
Panel.SetZIndex(_draggingButton, 0);
_draggingButton = null;
// 阻止事件冒泡
e.Handled = true;
}
/// <summary>
/// 计算网格位置
/// </summary>
private int CalculateGridPosition(Point point)
{
// 计算行和列
int columnCount = 4; // 每行最多4个应用
int columnWidth = 90; // 应用宽度(包括边距)
int rowHeight = 90; // 应用高度(包括边距)
int column = (int)(point.X / columnWidth);
int row = (int)(point.Y / rowHeight);
// 确保在有效范围内
column = Math.Max(0, Math.Min(column, columnCount - 1));
row = Math.Max(0, row);
// 计算位置索引
return row * columnCount + column;
}
/// <summary>
/// 重新排序应用项
/// </summary>
private void ReorderItems(LauncherItem item, int newPosition)
{
try
{
// 设置项目为固定位置
item.IsPositionFixed = true;
// 如果位置相同,无需调整
if (item.Position == newPosition)
{
return;
}
// 获取所有可见项目
var visibleItems = _plugin.LauncherItems
.Where(i => i.IsVisible)
.OrderBy(i => i.Position)
.ToList();
// 移除当前项目
visibleItems.Remove(item);
// 查找插入位置
int insertIndex = 0;
for (int i = 0; i < visibleItems.Count; i++)
{
if (visibleItems[i].Position >= newPosition)
{
insertIndex = i;
break;
}
insertIndex = i + 1;
}
// 插入项目
visibleItems.Insert(insertIndex, item);
// 重新分配位置
for (int i = 0; i < visibleItems.Count; i++)
{
visibleItems[i].Position = i;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"重新排序应用项时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region
/// <summary>
/// 窗口失去焦点事件
/// </summary>
private void Window_Deactivated(object sender, EventArgs e)
{
try
{
// 只有在非固定模式、窗口已加载、未处于关闭状态且IsLoaded=true时关闭窗口
if (!_isFixMode && IsLoaded && !IsClosing)
{
// 标记为正在关闭
IsClosing = true;
// 使用Dispatcher.BeginInvoke而不是直接调用Close,避免冲突
Dispatcher.BeginInvoke(new Action(() =>
{
try
{
// 再次检查窗口状态
if (IsLoaded && !IsClosing)
{
Close();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"延迟关闭窗口时出错: {ex.Message}", LogHelper.LogType.Error);
}
}), DispatcherPriority.Background);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"窗口失去焦点关闭时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 窗口是否正在关闭
/// </summary>
private bool IsClosing { get; set; }
/// <summary>
/// 重写OnClosing方法,标记窗口正在关闭
/// </summary>
protected override void OnClosing(CancelEventArgs e)
{
IsClosing = true;
base.OnClosing(e);
}
/// <summary>
/// 关闭按钮点击事件
/// </summary>
private void BtnClose_Click(object sender, RoutedEventArgs e)
{
Close();
}
/// <summary>
/// 固定模式按钮点击事件
/// </summary>
private void BtnFixMode_Click(object sender, RoutedEventArgs e)
{
// 切换固定模式
_isFixMode = !_isFixMode;
// 更新固定模式按钮图标颜色
FixModeIcon.Fill = _isFixMode ? Brushes.Yellow : Brushes.White;
// 显示提示
if (_isFixMode)
{
MessageBox.Show("已进入固定模式,您可以拖动应用图标调整位置。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
#endregion
}
}
@@ -1,589 +0,0 @@
using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn
{
/// <summary>
/// 超级启动台插件
/// </summary>
public class SuperLauncherPlugin : PluginBase
{
#region
public override string Name => "超级启动台";
public override string Description => "在浮动栏添加一个启动台按钮,可快速启动常用应用程序。";
public override Version Version => new Version(1, 0, 1);
public override string Author => "ICC CE 团队";
public override bool IsBuiltIn => true;
#endregion
#region
/// <summary>
/// 启动台配置
/// </summary>
public LauncherConfig Config { get; private set; }
/// <summary>
/// 启动台应用程序列表
/// </summary>
public ObservableCollection<LauncherItem> LauncherItems { get; private set; }
/// <summary>
/// 启动台按钮
/// </summary>
private LauncherButton _launcherButton;
/// <summary>
/// 启动台窗口
/// </summary>
private LauncherWindow _launcherWindow;
/// <summary>
/// 配置文件路径
/// </summary>
private readonly string _configPath = Path.Combine(App.RootPath, "PluginConfigs", "SuperLauncher.json");
/// <summary>
/// 标记是否已添加到浮动栏
/// </summary>
private bool _isAddedToFloatingBar;
#endregion
#region
public override void Initialize()
{
try
{
base.Initialize();
// 创建配置目录
string configDir = Path.Combine(App.RootPath, "PluginConfigs");
if (!Directory.Exists(configDir))
{
Directory.CreateDirectory(configDir);
}
// 加载配置
LoadConfig();
LogHelper.WriteLogToFile("超级启动台插件已初始化");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
public override void Enable()
{
try
{
if (IsEnabled) return; // 防止重复启用
// 创建启动台按钮
if (_launcherButton == null)
{
_launcherButton = new LauncherButton(this);
LogHelper.WriteLogToFile("超级启动台按钮已创建");
}
// 添加启动台按钮到浮动栏
AddLauncherButtonToFloatingBar();
// 设置启用状态
base.Enable();
// 保存插件配置
SavePluginSettings();
LogHelper.WriteLogToFile("超级启动台插件已启用");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
public override void Disable()
{
try
{
if (!IsEnabled) return; // 防止重复禁用
// 从浮动栏移除启动台按钮
RemoveLauncherButtonFromFloatingBar();
// 如果启动台窗口打开,则关闭
if (_launcherWindow != null && _launcherWindow.IsVisible)
{
_launcherWindow.Close();
_launcherWindow = null;
}
// 设置禁用状态
base.Disable();
// 保存插件配置
SavePluginSettings();
LogHelper.WriteLogToFile("超级启动台插件已禁用");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"禁用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
public override UserControl GetSettingsView()
{
return new LauncherSettingsControl(this);
}
public override void Cleanup()
{
// 保存配置
SaveConfig();
// 从浮动栏移除启动台按钮
RemoveLauncherButtonFromFloatingBar();
// 如果启动台窗口打开,则关闭
if (_launcherWindow != null && _launcherWindow.IsVisible)
{
_launcherWindow.Close();
_launcherWindow = null;
}
base.Cleanup();
}
/// <summary>
/// 保存插件设置
/// </summary>
public override void SavePluginSettings()
{
try
{
// 确保配置已加载
if (Config == null)
{
LoadConfig();
}
// 更新其他设置,但不更改插件启用状态
// 保存配置
SaveConfig();
LogHelper.WriteLogToFile("超级启动台插件设置已保存");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存超级启动台插件设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region
/// <summary>
/// 加载配置
/// </summary>
private void LoadConfig()
{
try
{
if (File.Exists(_configPath))
{
string json = File.ReadAllText(_configPath);
Config = JsonConvert.DeserializeObject<LauncherConfig>(json) ?? CreateDefaultConfig();
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items ?? new List<LauncherItem>());
// 注意:不再根据配置更改插件启用状态
// 插件状态由PluginManager统一管理
}
else
{
Config = CreateDefaultConfig();
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items);
SaveConfig();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error);
Config = CreateDefaultConfig();
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items);
}
}
/// <summary>
/// 保存配置
/// </summary>
public void SaveConfig()
{
try
{
// 同步LauncherItems到Config
Config.Items = new List<LauncherItem>(LauncherItems);
// 序列化并保存配置
string json = JsonConvert.SerializeObject(Config, Formatting.Indented);
File.WriteAllText(_configPath, json);
LogHelper.WriteLogToFile("超级启动台配置已保存");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 创建默认配置
/// </summary>
private LauncherConfig CreateDefaultConfig()
{
var config = new LauncherConfig
{
ButtonPosition = LauncherButtonPosition.Right,
// 不再使用IsEnabled,插件状态由PluginManager管理
Items = new List<LauncherItem>
{
new LauncherItem
{
Name = "资源管理器",
Path = @"C:\Windows\explorer.exe",
IsVisible = true,
Position = 0
}
}
};
return config;
}
#endregion
#region
/// <summary>
/// 将启动台按钮添加到浮动栏
/// </summary>
private void AddLauncherButtonToFloatingBar()
{
try
{
// 如果已经添加,先移除
if (_isAddedToFloatingBar)
{
RemoveLauncherButtonFromFloatingBar();
_isAddedToFloatingBar = false;
}
// 获取主窗口实例
var mainWindow = Application.Current.MainWindow;
if (mainWindow == null)
{
LogHelper.WriteLogToFile("未找到主窗口实例,无法添加启动台按钮", LogHelper.LogType.Error);
return;
}
// 创建启动台按钮
_launcherButton = new LauncherButton(this);
var buttonElement = _launcherButton.Element;
// 查找浮动栏
var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel;
if (floatingBar == null)
{
// 如果直接查找失败,则尝试遍历可视树查找
Panel floatingBarPanelFromTree = null;
FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree);
floatingBar = floatingBarPanelFromTree;
}
if (floatingBar == null)
{
LogHelper.WriteLogToFile("未找到浮动栏,无法添加启动台按钮", LogHelper.LogType.Error);
return;
}
// 添加启动台按钮到浮动栏
if (Config.ButtonPosition == LauncherButtonPosition.Left)
{
floatingBar.Children.Insert(0, buttonElement);
LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏左侧");
}
else
{
floatingBar.Children.Add(buttonElement);
LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏右侧");
}
_isAddedToFloatingBar = true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"添加启动台按钮到浮动栏时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
/// <summary>
/// 递归查找StackPanelFloatingBar
/// </summary>
private void FindStackPanelFloatingBar(DependencyObject parent, ref Panel result)
{
if (parent == null || result != null) return;
try
{
// 检查当前对象是否为我们要找的面板
if (parent is Panel panel && panel.Name == "StackPanelFloatingBar")
{
result = panel;
return;
}
// 获取子元素数量
int childCount = VisualTreeHelper.GetChildrenCount(parent);
// 遍历所有子元素
for (int i = 0; i < childCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
FindStackPanelFloatingBar(child, ref result);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"查找StackPanelFloatingBar时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 从浮动栏移除启动台按钮
/// </summary>
private void RemoveLauncherButtonFromFloatingBar()
{
try
{
if (!_isAddedToFloatingBar || _launcherButton == null)
{
return;
}
// 获取主窗口实例
var mainWindow = Application.Current.MainWindow;
if (mainWindow == null)
{
LogHelper.WriteLogToFile("未找到主窗口实例,无法移除启动台按钮", LogHelper.LogType.Error);
return;
}
// 获取按钮元素
var buttonElement = _launcherButton.Element;
// 查找浮动栏
var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel;
if (floatingBar == null)
{
// 如果直接查找失败,则尝试遍历可视树查找
Panel floatingBarPanelFromTree = null;
FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree);
floatingBar = floatingBarPanelFromTree;
}
if (floatingBar == null)
{
LogHelper.WriteLogToFile("未找到浮动栏,无法移除启动台按钮", LogHelper.LogType.Error);
return;
}
// 从浮动栏移除启动台按钮
if (floatingBar.Children.Contains(buttonElement))
{
floatingBar.Children.Remove(buttonElement);
LogHelper.WriteLogToFile("启动台按钮已从浮动栏移除");
}
_isAddedToFloatingBar = false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"移除启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
/// <summary>
/// 更新启动台按钮位置
/// </summary>
public void UpdateButtonPosition()
{
try
{
// 如果按钮已添加到浮动栏,重新添加以更新位置
if (_isAddedToFloatingBar)
{
RemoveLauncherButtonFromFloatingBar();
AddLauncherButtonToFloatingBar();
LogHelper.WriteLogToFile($"启动台按钮位置已更新为: {Config.ButtonPosition}");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
#endregion
#region
/// <summary>
/// 显示启动台窗口
/// </summary>
/// <param name="buttonPosition">按钮在屏幕上的位置</param>
public void ShowLauncherWindow(Point buttonPosition)
{
try
{
// 如果窗口已存在,关闭它
if (_launcherWindow != null && _launcherWindow.IsVisible)
{
_launcherWindow.Close();
_launcherWindow = null;
return;
}
// 创建新的启动台窗口
_launcherWindow = new LauncherWindow(this);
// 计算窗口位置,使其位于按钮上方
PositionLauncherWindow(_launcherWindow, buttonPosition);
// 显示窗口
_launcherWindow.Show();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"显示启动台窗口时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 设置启动台窗口位置
/// </summary>
/// <param name="window">启动台窗口</param>
/// <param name="buttonPosition">按钮在屏幕上的位置</param>
private void PositionLauncherWindow(LauncherWindow window, Point buttonPosition)
{
// 确保窗口已加载
if (window.ActualWidth == 0 || window.ActualHeight == 0)
{
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
// 设置窗口加载完成后的位置
window.Loaded += (s, e) =>
{
// 窗口位于按钮上方居中
double left = buttonPosition.X - (window.ActualWidth / 2);
double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距
// 确保窗口在屏幕内
left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth));
top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight));
window.Left = left;
window.Top = top;
};
}
else
{
// 窗口位于按钮上方居中
double left = buttonPosition.X - (window.ActualWidth / 2);
double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距
// 确保窗口在屏幕内
left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth));
top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight));
window.Left = left;
window.Top = top;
}
}
/// <summary>
/// 添加应用到启动台
/// </summary>
/// <param name="item">启动台项</param>
public void AddLauncherItem(LauncherItem item)
{
// 如果项目数量已达上限,则不添加
if (LauncherItems.Count >= 40)
{
MessageBox.Show("启动台项目数量已达上限(40个)!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
// 寻找合适的位置
if (item.Position < 0)
{
item.Position = FindNextAvailablePosition();
}
// 添加项目并保存配置
LauncherItems.Add(item);
SaveConfig();
}
/// <summary>
/// 查找下一个可用位置
/// </summary>
private int FindNextAvailablePosition()
{
// 获取已使用的位置列表
var usedPositions = new HashSet<int>();
foreach (var item in LauncherItems)
{
usedPositions.Add(item.Position);
}
// 查找第一个可用位置
for (int i = 0; i < 40; i++)
{
if (!usedPositions.Contains(i))
{
return i;
}
}
// 如果所有位置都已使用,则返回0
return 0;
}
#endregion
}
}
@@ -1,92 +0,0 @@
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 增强的插件基类,提供对插件服务的访问和基本实现
/// </summary>
public abstract class EnhancedPluginBase : PluginBase, IEnhancedPlugin
{
/// <summary>
/// 插件服务实例
/// </summary>
public IPluginService PluginService { get; private set; }
/// <summary>
/// 构造函数
/// </summary>
protected EnhancedPluginBase()
{
PluginService = PluginServiceManager.Instance;
}
/// <summary>
/// 插件启动时调用,在Initialize之后
/// </summary>
public virtual void OnStartup()
{
LogHelper.WriteLogToFile($"插件 {Name} 已启动");
}
/// <summary>
/// 插件关闭时调用,在Cleanup之前
/// </summary>
public virtual void OnShutdown()
{
LogHelper.WriteLogToFile($"插件 {Name} 正在关闭");
}
/// <summary>
/// 获取插件的菜单项
/// </summary>
/// <returns>菜单项集合</returns>
public virtual MenuItem[] GetMenuItems()
{
return new MenuItem[0];
}
/// <summary>
/// 获取插件的工具栏按钮
/// </summary>
/// <returns>工具栏按钮集合</returns>
public virtual Button[] GetToolbarButtons()
{
return new Button[0];
}
/// <summary>
/// 获取插件的状态栏信息
/// </summary>
/// <returns>状态栏信息</returns>
public virtual string GetStatusBarInfo()
{
return $"{Name} v{Version} - {(IsEnabled ? "" : "")}";
}
/// <summary>
/// 插件配置变更时调用
/// </summary>
public virtual void OnConfigurationChanged()
{
LogHelper.WriteLogToFile($"插件 {Name} 配置已变更");
}
/// <summary>
/// 重写初始化方法,调用OnStartup
/// </summary>
public override void Initialize()
{
base.Initialize();
OnStartup();
}
/// <summary>
/// 重写清理方法,调用OnShutdown
/// </summary>
public override void Cleanup()
{
OnShutdown();
base.Cleanup();
}
}
}
@@ -1,241 +0,0 @@
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 增强的插件基类 V2,提供对三个专门服务接口的访问
/// 插件开发者可以根据需要选择性地使用这些服务
/// </summary>
public abstract class EnhancedPluginBaseV2 : PluginBase, IEnhancedPlugin
{
/// <summary>
/// 获取服务实例
/// </summary>
public IGetService GetService { get; private set; }
/// <summary>
/// 窗口服务实例
/// </summary>
public IWindowService WindowService { get; private set; }
/// <summary>
/// 操作服务实例
/// </summary>
public IActionService ActionService { get; private set; }
/// <summary>
/// 插件服务实例(兼容性)
/// </summary>
public IPluginService PluginService { get; private set; }
/// <summary>
/// 构造函数
/// </summary>
protected EnhancedPluginBaseV2()
{
// 初始化所有服务实例
PluginService = PluginServiceManager.Instance;
GetService = PluginServiceManager.Instance;
WindowService = PluginServiceManager.Instance;
ActionService = PluginServiceManager.Instance;
}
/// <summary>
/// 插件启动时调用,在Initialize之后
/// </summary>
public virtual void OnStartup()
{
LogHelper.WriteLogToFile($"插件 {Name} 已启动");
}
/// <summary>
/// 插件关闭时调用,在Cleanup之前
/// </summary>
public virtual void OnShutdown()
{
LogHelper.WriteLogToFile($"插件 {Name} 正在关闭");
}
/// <summary>
/// 获取插件的菜单项
/// </summary>
/// <returns>菜单项集合</returns>
public virtual MenuItem[] GetMenuItems()
{
return new MenuItem[0];
}
/// <summary>
/// 获取插件的工具栏按钮
/// </summary>
/// <returns>工具栏按钮集合</returns>
public virtual Button[] GetToolbarButtons()
{
return new Button[0];
}
/// <summary>
/// 获取插件的状态栏信息
/// </summary>
/// <returns>状态栏信息</returns>
public virtual string GetStatusBarInfo()
{
return $"{Name} v{Version} - {(IsEnabled ? "" : "")}";
}
/// <summary>
/// 插件配置变更时调用
/// </summary>
public virtual void OnConfigurationChanged()
{
LogHelper.WriteLogToFile($"插件 {Name} 配置已变更");
}
#region 便
/// <summary>
/// 显示通知消息
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="type">消息类型</param>
protected void ShowNotification(string message, NotificationType type = NotificationType.Info)
{
WindowService.ShowNotification(message, type);
}
/// <summary>
/// 显示确认对话框
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题</param>
/// <returns>用户选择结果</returns>
protected bool ShowConfirmDialog(string message, string title = "确认")
{
return WindowService.ShowConfirmDialog(message, title);
}
/// <summary>
/// 显示输入对话框
/// </summary>
/// <param name="message">提示消息</param>
/// <param name="title">标题</param>
/// <param name="defaultValue">默认值</param>
/// <returns>用户输入内容</returns>
protected string ShowInputDialog(string message, string title = "输入", string defaultValue = "")
{
return WindowService.ShowInputDialog(message, title, defaultValue);
}
/// <summary>
/// 获取系统设置
/// </summary>
/// <typeparam name="T">设置类型</typeparam>
/// <param name="key">设置键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>设置值</returns>
protected T GetSetting<T>(string key, T defaultValue = default(T))
{
return GetService.GetSetting(key, defaultValue);
}
/// <summary>
/// 设置系统设置
/// </summary>
/// <typeparam name="T">设置类型</typeparam>
/// <param name="key">设置键</param>
/// <param name="value">设置值</param>
protected void SetSetting<T>(string key, T value)
{
ActionService.SetSetting(key, value);
}
/// <summary>
/// 保存设置
/// </summary>
protected void SaveSettings()
{
ActionService.SaveSettings();
}
/// <summary>
/// 清除当前画布
/// </summary>
protected void ClearCanvas()
{
ActionService.ClearCanvas();
}
/// <summary>
/// 撤销操作
/// </summary>
protected void Undo()
{
ActionService.Undo();
}
/// <summary>
/// 重做操作
/// </summary>
protected void Redo()
{
ActionService.Redo();
}
/// <summary>
/// 检查是否可以撤销
/// </summary>
protected bool CanUndo => GetService.CanUndo;
/// <summary>
/// 检查是否可以重做
/// </summary>
protected bool CanRedo => GetService.CanRedo;
/// <summary>
/// 获取当前绘制模式
/// </summary>
protected int CurrentDrawingMode => GetService.CurrentDrawingMode;
/// <summary>
/// 设置绘制模式
/// </summary>
/// <param name="mode">绘制模式</param>
protected void SetDrawingMode(int mode)
{
ActionService.SetDrawingMode(mode);
}
/// <summary>
/// 注册事件处理器
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="handler">事件处理器</param>
protected void RegisterEventHandler(string eventName, System.EventHandler handler)
{
ActionService.RegisterEventHandler(eventName, handler);
}
/// <summary>
/// 注销事件处理器
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="handler">事件处理器</param>
protected void UnregisterEventHandler(string eventName, System.EventHandler handler)
{
ActionService.UnregisterEventHandler(eventName, handler);
}
/// <summary>
/// 触发事件
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="sender">事件发送者</param>
/// <param name="args">事件参数</param>
protected void TriggerEvent(string eventName, object sender, System.EventArgs args)
{
ActionService.TriggerEvent(eventName, sender, args);
}
#endregion
}
}
@@ -1,296 +0,0 @@
using System;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 操作服务接口,统一所有执行操作相关的方法
/// </summary>
public interface IActionService
{
#region
/// <summary>
/// 清除当前画布
/// </summary>
void ClearCanvas();
/// <summary>
/// 清除所有画布
/// </summary>
void ClearAllCanvases();
/// <summary>
/// 添加新页面
/// </summary>
void AddNewPage();
/// <summary>
/// 删除当前页面
/// </summary>
void DeleteCurrentPage();
/// <summary>
/// 切换到指定页面
/// </summary>
/// <param name="pageIndex">页面索引</param>
void SwitchToPage(int pageIndex);
/// <summary>
/// 切换到下一页
/// </summary>
void NextPage();
/// <summary>
/// 切换到上一页
/// </summary>
void PreviousPage();
#endregion
#region
/// <summary>
/// 设置绘制模式
/// </summary>
/// <param name="mode">绘制模式</param>
void SetDrawingMode(int mode);
/// <summary>
/// 设置笔触宽度
/// </summary>
/// <param name="width">宽度</param>
void SetInkWidth(double width);
/// <summary>
/// 设置笔触颜色
/// </summary>
/// <param name="color">颜色</param>
void SetInkColor(Color color);
/// <summary>
/// 设置高亮笔宽度
/// </summary>
/// <param name="width">宽度</param>
void SetHighlighterWidth(double width);
/// <summary>
/// 设置橡皮擦大小
/// </summary>
/// <param name="size">大小</param>
void SetEraserSize(int size);
/// <summary>
/// 设置橡皮擦类型
/// </summary>
/// <param name="type">类型</param>
void SetEraserType(int type);
/// <summary>
/// 设置橡皮擦形状
/// </summary>
/// <param name="shape">形状</param>
void SetEraserShape(int shape);
/// <summary>
/// 设置笔触透明度
/// </summary>
/// <param name="alpha">透明度</param>
void SetInkAlpha(double alpha);
/// <summary>
/// 设置笔触样式
/// </summary>
/// <param name="style">样式</param>
void SetInkStyle(int style);
/// <summary>
/// 设置背景颜色
/// </summary>
/// <param name="color">颜色</param>
void SetBackgroundColor(string color);
#endregion
#region
/// <summary>
/// 保存画布内容
/// </summary>
/// <param name="filePath">文件路径</param>
void SaveCanvas(string filePath);
/// <summary>
/// 加载画布内容
/// </summary>
/// <param name="filePath">文件路径</param>
void LoadCanvas(string filePath);
/// <summary>
/// 导出为图片
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="format">图片格式</param>
void ExportAsImage(string filePath, string format);
/// <summary>
/// 导出为PDF
/// </summary>
/// <param name="filePath">文件路径</param>
void ExportAsPDF(string filePath);
#endregion
#region
/// <summary>
/// 撤销操作
/// </summary>
void Undo();
/// <summary>
/// 重做操作
/// </summary>
void Redo();
#endregion
#region
/// <summary>
/// 全选
/// </summary>
void SelectAll();
/// <summary>
/// 取消选择
/// </summary>
void DeselectAll();
/// <summary>
/// 删除选中内容
/// </summary>
void DeleteSelected();
/// <summary>
/// 复制选中内容
/// </summary>
void CopySelected();
/// <summary>
/// 剪切选中内容
/// </summary>
void CutSelected();
/// <summary>
/// 粘贴内容
/// </summary>
void Paste();
#endregion
#region
/// <summary>
/// 设置系统设置
/// </summary>
/// <typeparam name="T">设置类型</typeparam>
/// <param name="key">设置键</param>
/// <param name="value">设置值</param>
void SetSetting<T>(string key, T value);
/// <summary>
/// 保存设置到文件
/// </summary>
void SaveSettings();
/// <summary>
/// 从文件加载设置
/// </summary>
void LoadSettings();
/// <summary>
/// 重置设置为默认值
/// </summary>
void ResetSettings();
#endregion
#region
/// <summary>
/// 启用插件
/// </summary>
/// <param name="pluginName">插件名称</param>
void EnablePlugin(string pluginName);
/// <summary>
/// 禁用插件
/// </summary>
/// <param name="pluginName">插件名称</param>
void DisablePlugin(string pluginName);
/// <summary>
/// 卸载插件
/// </summary>
/// <param name="pluginName">插件名称</param>
void UnloadPlugin(string pluginName);
#endregion
#region
/// <summary>
/// 注册事件处理器
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="handler">事件处理器</param>
void RegisterEventHandler(string eventName, EventHandler handler);
/// <summary>
/// 注销事件处理器
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="handler">事件处理器</param>
void UnregisterEventHandler(string eventName, EventHandler handler);
/// <summary>
/// 触发事件
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="sender">事件发送者</param>
/// <param name="args">事件参数</param>
void TriggerEvent(string eventName, object sender, EventArgs args);
#endregion
#region
/// <summary>
/// 重启应用程序
/// </summary>
void RestartApplication();
/// <summary>
/// 退出应用程序
/// </summary>
void ExitApplication();
/// <summary>
/// 检查更新
/// </summary>
void CheckForUpdates();
/// <summary>
/// 打开帮助文档
/// </summary>
void OpenHelpDocument();
/// <summary>
/// 打开关于页面
/// </summary>
void OpenAboutPage();
#endregion
}
}
@@ -1,178 +0,0 @@
using System;
using System.IO;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// ICCPP 插件适配器,用于加载和管理 .iccpp 格式的插件
/// </summary>
public class ICCPPPluginAdapter : PluginBase
{
private readonly byte[] _pluginData;
private readonly string _pluginPath;
private readonly string _pluginName;
private readonly Version _pluginVersion;
private bool _isInitialized;
/// <summary>
/// 创建 ICCPP 插件适配器
/// </summary>
/// <param name="pluginPath">插件文件路径</param>
/// <param name="pluginData">插件文件数据</param>
public ICCPPPluginAdapter(string pluginPath, byte[] pluginData)
{
_pluginPath = pluginPath;
_pluginData = pluginData;
PluginPath = pluginPath;
// 从文件名获取插件名称
_pluginName = Path.GetFileNameWithoutExtension(pluginPath);
_pluginVersion = new Version(1, 0, 0); // 默认版本
// 尝试从插件数据中读取更多信息
TryReadPluginMetadata();
}
public ICCPPPluginAdapter()
{
_pluginPath = string.Empty;
_pluginData = new byte[0];
PluginPath = string.Empty;
_pluginName = "ICCPPPlugin";
_pluginVersion = new Version(1, 0, 0);
// 可选:初始化其他字段
}
/// <summary>
/// 尝试从插件数据中读取元数据
/// </summary>
private void TryReadPluginMetadata()
{
try
{
// 这里可以根据 .iccpp 文件的实际格式解析元数据
// 例如,如果文件有特定的头部结构,可以在这里解析
// 示例:如果前100字节包含元数据
if (_pluginData.Length > 100)
{
// 解析元数据的代码...
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"解析插件 {_pluginName} 元数据时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#region IPlugin
/// <summary>
/// 插件名称
/// </summary>
public override string Name => _pluginName;
/// <summary>
/// 插件描述
/// </summary>
public override string Description => $"{_pluginName} (ICCPP 格式插件)";
/// <summary>
/// 插件版本
/// </summary>
public override Version Version => _pluginVersion;
/// <summary>
/// 插件作者
/// </summary>
public override string Author => "未知";
/// <summary>
/// 是否为内置插件
/// </summary>
public override bool IsBuiltIn => false;
/// <summary>
/// 初始化插件
/// </summary>
public override void Initialize()
{
if (_isInitialized) return;
try
{
// 这里可以添加 .iccpp 插件的初始化逻辑
// 例如,根据文件格式加载特定资源
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已初始化");
_isInitialized = true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 启用插件
/// </summary>
public override void Enable()
{
if (IsEnabled) return;
try
{
// 这里可以添加 .iccpp 插件的启用逻辑
// 例如,加载动态库、注册事件等
base.Enable(); // 设置启用状态并触发事件
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已启用");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 禁用插件
/// </summary>
public override void Disable()
{
if (!IsEnabled) return;
try
{
// 这里可以添加 .iccpp 插件的禁用逻辑
// 例如,卸载动态库、注销事件等
base.Disable(); // 设置禁用状态并触发事件
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已禁用");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"禁用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 清理插件资源
/// </summary>
public override void Cleanup()
{
try
{
// 这里可以添加 .iccpp 插件的清理逻辑
// 例如,释放资源等
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已清理资源");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理 ICCPP 插件 {Name} 资源时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
}
}
@@ -1,48 +0,0 @@
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 增强的插件接口,提供对插件服务的访问
/// </summary>
public interface IEnhancedPlugin : IPlugin
{
/// <summary>
/// 获取插件服务实例
/// </summary>
IPluginService PluginService { get; }
/// <summary>
/// 插件启动时调用,在Initialize之后
/// </summary>
void OnStartup();
/// <summary>
/// 插件关闭时调用,在Cleanup之前
/// </summary>
void OnShutdown();
/// <summary>
/// 获取插件的菜单项
/// </summary>
/// <returns>菜单项集合</returns>
MenuItem[] GetMenuItems();
/// <summary>
/// 获取插件的工具栏按钮
/// </summary>
/// <returns>工具栏按钮集合</returns>
Button[] GetToolbarButtons();
/// <summary>
/// 获取插件的状态栏信息
/// </summary>
/// <returns>状态栏信息</returns>
string GetStatusBarInfo();
/// <summary>
/// 插件配置变更时调用
/// </summary>
void OnConfigurationChanged();
}
}
-214
View File
@@ -1,214 +0,0 @@
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 获取服务接口,统一所有获取类的方法
/// </summary>
public interface IGetService
{
#region UI获取
/// <summary>
/// 获取主窗口引用
/// </summary>
Window MainWindow { get; }
/// <summary>
/// 获取当前画布
/// </summary>
InkCanvas CurrentCanvas { get; }
/// <summary>
/// 获取所有画布页面
/// </summary>
List<Canvas> AllCanvasPages { get; }
/// <summary>
/// 获取当前页面索引
/// </summary>
int CurrentPageIndex { get; }
/// <summary>
/// 获取当前页面数量
/// </summary>
int TotalPageCount { get; }
/// <summary>
/// 获取浮动工具栏
/// </summary>
FrameworkElement FloatingToolBar { get; }
/// <summary>
/// 获取左侧面板
/// </summary>
FrameworkElement LeftPanel { get; }
/// <summary>
/// 获取右侧面板
/// </summary>
FrameworkElement RightPanel { get; }
/// <summary>
/// 获取顶部面板
/// </summary>
FrameworkElement TopPanel { get; }
/// <summary>
/// 获取底部面板
/// </summary>
FrameworkElement BottomPanel { get; }
#endregion
#region
/// <summary>
/// 获取当前绘制模式
/// </summary>
int CurrentDrawingMode { get; }
/// <summary>
/// 获取当前笔触宽度
/// </summary>
double CurrentInkWidth { get; }
/// <summary>
/// 获取当前笔触颜色
/// </summary>
Color CurrentInkColor { get; }
/// <summary>
/// 获取当前高亮笔宽度
/// </summary>
double CurrentHighlighterWidth { get; }
/// <summary>
/// 获取当前橡皮擦大小
/// </summary>
int CurrentEraserSize { get; }
/// <summary>
/// 获取当前橡皮擦类型
/// </summary>
int CurrentEraserType { get; }
/// <summary>
/// 获取当前橡皮擦形状
/// </summary>
int CurrentEraserShape { get; }
/// <summary>
/// 获取当前笔触透明度
/// </summary>
double CurrentInkAlpha { get; }
/// <summary>
/// 获取当前笔触样式
/// </summary>
int CurrentInkStyle { get; }
/// <summary>
/// 获取当前背景颜色
/// </summary>
string CurrentBackgroundColor { get; }
#endregion
#region
/// <summary>
/// 获取当前主题模式
/// </summary>
bool IsDarkTheme { get; }
/// <summary>
/// 获取当前是否为白板模式
/// </summary>
bool IsWhiteboardMode { get; }
/// <summary>
/// 获取当前是否为PPT模式
/// </summary>
bool IsPPTMode { get; }
/// <summary>
/// 获取当前是否为全屏模式
/// </summary>
bool IsFullScreenMode { get; }
/// <summary>
/// 获取当前是否为画板模式
/// </summary>
bool IsCanvasMode { get; }
/// <summary>
/// 获取当前是否为选择模式
/// </summary>
bool IsSelectionMode { get; }
/// <summary>
/// 获取当前是否为擦除模式
/// </summary>
bool IsEraserMode { get; }
/// <summary>
/// 获取当前是否为形状绘制模式
/// </summary>
bool IsShapeDrawingMode { get; }
/// <summary>
/// 获取当前是否为高亮模式
/// </summary>
bool IsHighlighterMode { get; }
#endregion
#region
/// <summary>
/// 获取是否可以撤销
/// </summary>
bool CanUndo { get; }
/// <summary>
/// 获取是否可以重做
/// </summary>
bool CanRedo { get; }
#endregion
#region
/// <summary>
/// 获取系统设置
/// </summary>
/// <typeparam name="T">设置类型</typeparam>
/// <param name="key">设置键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>设置值</returns>
T GetSetting<T>(string key, T defaultValue = default(T));
#endregion
#region
/// <summary>
/// 获取所有已加载的插件
/// </summary>
/// <returns>插件列表</returns>
List<IPlugin> GetAllPlugins();
/// <summary>
/// 获取指定插件
/// </summary>
/// <param name="pluginName">插件名称</param>
/// <returns>插件实例</returns>
IPlugin GetPlugin(string pluginName);
#endregion
}
}
-67
View File
@@ -1,67 +0,0 @@
using System;
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 定义插件的基本接口
/// </summary>
public interface IPlugin
{
/// <summary>
/// 插件名称
/// </summary>
string Name { get; }
/// <summary>
/// 插件描述
/// </summary>
string Description { get; }
/// <summary>
/// 插件版本
/// </summary>
Version Version { get; }
/// <summary>
/// 插件作者
/// </summary>
string Author { get; }
/// <summary>
/// 是否为内置插件
/// </summary>
bool IsBuiltIn { get; }
/// <summary>
/// 初始化插件
/// 此方法在插件加载时被调用,用于执行一些初始化工作
/// </summary>
void Initialize();
/// <summary>
/// 启用插件
/// 此方法在插件被用户或系统启用时调用,激活插件功能
/// </summary>
void Enable();
/// <summary>
/// 禁用插件
/// 此方法在插件被用户或系统禁用时调用,停用插件功能
/// </summary>
void Disable();
/// <summary>
/// 获取插件设置界面
/// 此方法返回插件的设置界面控件,用于展示在设置窗口
/// </summary>
/// <returns>插件设置界面</returns>
UserControl GetSettingsView();
/// <summary>
/// 插件卸载时的清理工作
/// 此方法在插件被卸载前调用,用于释放资源和执行清理
/// </summary>
void Cleanup();
}
}
@@ -1,38 +0,0 @@
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 插件服务接口,提供对软件内部功能的访问
/// 继承自三个专门的服务接口:获取服务、窗口服务、操作服务
/// </summary>
public interface IPluginService : IGetService, IWindowService, IActionService
{
// 这个接口现在继承自三个专门的服务接口
// 所有方法都在子接口中定义,这里不需要重复定义
}
/// <summary>
/// 通知类型枚举
/// </summary>
public enum NotificationType
{
/// <summary>
/// 信息
/// </summary>
Info,
/// <summary>
/// 成功
/// </summary>
Success,
/// <summary>
/// 警告
/// </summary>
Warning,
/// <summary>
/// 错误
/// </summary>
Error
}
}
@@ -1,152 +0,0 @@
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 窗口服务接口,统一所有窗口操作相关的方法
/// </summary>
public interface IWindowService
{
#region
/// <summary>
/// 显示设置窗口
/// </summary>
void ShowSettingsWindow();
/// <summary>
/// 隐藏设置窗口
/// </summary>
void HideSettingsWindow();
/// <summary>
/// 显示插件设置窗口
/// </summary>
void ShowPluginSettingsWindow();
/// <summary>
/// 隐藏插件设置窗口
/// </summary>
void HidePluginSettingsWindow();
/// <summary>
/// 显示帮助窗口
/// </summary>
void ShowHelpWindow();
/// <summary>
/// 隐藏帮助窗口
/// </summary>
void HideHelpWindow();
/// <summary>
/// 显示关于窗口
/// </summary>
void ShowAboutWindow();
/// <summary>
/// 隐藏关于窗口
/// </summary>
void HideAboutWindow();
#endregion
#region
/// <summary>
/// 显示通知消息
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="type">消息类型</param>
void ShowNotification(string message, NotificationType type = NotificationType.Info);
/// <summary>
/// 显示确认对话框
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题</param>
/// <returns>用户选择结果</returns>
bool ShowConfirmDialog(string message, string title = "确认");
/// <summary>
/// 显示输入对话框
/// </summary>
/// <param name="message">提示消息</param>
/// <param name="title">标题</param>
/// <param name="defaultValue">默认值</param>
/// <returns>用户输入内容</returns>
string ShowInputDialog(string message, string title = "输入", string defaultValue = "");
#endregion
#region
/// <summary>
/// 设置窗口全屏状态
/// </summary>
/// <param name="isFullScreen">是否全屏</param>
void SetFullScreen(bool isFullScreen);
/// <summary>
/// 设置窗口置顶状态
/// </summary>
/// <param name="isTopMost">是否置顶</param>
void SetTopMost(bool isTopMost);
/// <summary>
/// 设置窗口可见性
/// </summary>
/// <param name="isVisible">是否可见</param>
void SetWindowVisibility(bool isVisible);
/// <summary>
/// 最小化窗口
/// </summary>
void MinimizeWindow();
/// <summary>
/// 最大化窗口
/// </summary>
void MaximizeWindow();
/// <summary>
/// 恢复窗口
/// </summary>
void RestoreWindow();
/// <summary>
/// 关闭窗口
/// </summary>
void CloseWindow();
#endregion
#region
/// <summary>
/// 设置窗口位置
/// </summary>
/// <param name="x">X坐标</param>
/// <param name="y">Y坐标</param>
void SetWindowPosition(double x, double y);
/// <summary>
/// 设置窗口大小
/// </summary>
/// <param name="width">宽度</param>
/// <param name="height">高度</param>
void SetWindowSize(double width, double height);
/// <summary>
/// 获取窗口位置
/// </summary>
/// <returns>窗口位置</returns>
(double x, double y) GetWindowPosition();
/// <summary>
/// 获取窗口大小
/// </summary>
/// <returns>窗口大小</returns>
(double width, double height) GetWindowSize();
#endregion
}
}
-161
View File
@@ -1,161 +0,0 @@
using System;
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 插件基类,提供基本实现
/// </summary>
public abstract class PluginBase : IPlugin
{
/// <summary>
/// 插件状态(私有字段)
/// </summary>
private bool _isEnabled;
/// <summary>
/// 插件状态(公共属性)
/// </summary>
public bool IsEnabled
{
get => _isEnabled;
protected set
{
if (_isEnabled != value)
{
_isEnabled = value;
OnEnabledStateChanged(value);
}
}
}
/// <summary>
/// 插件ID
/// </summary>
public string Id { get; protected set; }
/// <summary>
/// 插件路径
/// </summary>
public string PluginPath { get; set; }
/// <summary>
/// 插件名称
/// </summary>
public abstract string Name { get; }
/// <summary>
/// 插件描述
/// </summary>
public abstract string Description { get; }
/// <summary>
/// 插件版本
/// </summary>
public abstract Version Version { get; }
/// <summary>
/// 插件作者
/// </summary>
public abstract string Author { get; }
/// <summary>
/// 是否为内置插件
/// </summary>
public virtual bool IsBuiltIn => false;
/// <summary>
/// 状态变更事件
/// </summary>
public event EventHandler<bool> EnabledStateChanged;
/// <summary>
/// 初始化插件
/// </summary>
public virtual void Initialize()
{
Id = GetType().FullName;
// 添加日志,记录插件名称
try
{
string name = Name;
LogHelper.WriteLogToFile($"初始化插件: ID={Id}, 名称={name ?? ""}");
if (string.IsNullOrEmpty(name))
{
LogHelper.WriteLogToFile($"警告: 插件 {Id} 的名称为空", LogHelper.LogType.Warning);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取插件名称时出错: {ex.Message}", LogHelper.LogType.Error);
}
LogHelper.WriteLogToFile($"插件 {Name} 已初始化");
}
/// <summary>
/// 启用插件
/// </summary>
public virtual void Enable()
{
if (!IsEnabled)
{
IsEnabled = true;
LogHelper.WriteLogToFile($"插件 {Name} 已启用");
}
}
/// <summary>
/// 禁用插件
/// </summary>
public virtual void Disable()
{
if (IsEnabled)
{
IsEnabled = false;
LogHelper.WriteLogToFile($"插件 {Name} 已禁用");
}
}
/// <summary>
/// 获取插件设置界面
/// </summary>
/// <returns>插件设置界面</returns>
public virtual UserControl GetSettingsView()
{
// 默认返回空设置页面
return new UserControl();
}
/// <summary>
/// 插件卸载时的清理工作
/// </summary>
public virtual void Cleanup()
{
LogHelper.WriteLogToFile($"插件 {Name} 已卸载");
}
/// <summary>
/// 保存插件自身的设置
/// 注意:此方法仅用于保存插件的特定设置,不应影响插件启用/禁用状态
/// 插件启用状态由PluginManager统一管理
/// </summary>
public virtual void SavePluginSettings()
{
// 默认实现不做任何事情
// 子类可以重写此方法,将自身设置保存到配置文件中
LogHelper.WriteLogToFile($"插件 {Name} 设置已保存", LogHelper.LogType.Event);
}
/// <summary>
/// 触发状态变更事件
/// </summary>
/// <param name="isEnabled">是否启用</param>
protected virtual void OnEnabledStateChanged(bool isEnabled)
{
EnabledStateChanged?.Invoke(this, isEnabled);
}
}
}
@@ -1,273 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 插件配置管理器,允许插件管理自己的配置
/// </summary>
public class PluginConfigurationManager
{
private static readonly string PluginConfigDirectory = Path.Combine(App.RootPath, "PluginConfigs");
private static readonly Dictionary<string, Dictionary<string, object>> _pluginConfigs = new Dictionary<string, Dictionary<string, object>>();
private static readonly object _lockObject = new object();
static PluginConfigurationManager()
{
// 确保配置目录存在
if (!Directory.Exists(PluginConfigDirectory))
{
Directory.CreateDirectory(PluginConfigDirectory);
}
}
/// <summary>
/// 获取插件配置值
/// </summary>
/// <typeparam name="T">配置值类型</typeparam>
/// <param name="pluginName">插件名称</param>
/// <param name="key">配置键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>配置值</returns>
public static T GetConfiguration<T>(string pluginName, string key, T defaultValue = default(T))
{
lock (_lockObject)
{
try
{
if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig))
{
if (pluginConfig.TryGetValue(key, out var value))
{
if (value is T typedValue)
{
return typedValue;
}
// 尝试类型转换
try
{
return (T)Convert.ChangeType(value, typeof(T));
}
catch
{
return defaultValue;
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error);
}
return defaultValue;
}
}
/// <summary>
/// 设置插件配置值
/// </summary>
/// <typeparam name="T">配置值类型</typeparam>
/// <param name="pluginName">插件名称</param>
/// <param name="key">配置键</param>
/// <param name="value">配置值</param>
public static void SetConfiguration<T>(string pluginName, string key, T value)
{
lock (_lockObject)
{
try
{
if (!_pluginConfigs.ContainsKey(pluginName))
{
_pluginConfigs[pluginName] = new Dictionary<string, object>();
}
_pluginConfigs[pluginName][key] = value;
// 异步保存配置
Task.Run(() => SavePluginConfiguration(pluginName));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"设置插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 删除插件配置
/// </summary>
/// <param name="pluginName">插件名称</param>
/// <param name="key">配置键</param>
public static void RemoveConfiguration(string pluginName, string key)
{
lock (_lockObject)
{
try
{
if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig))
{
if (pluginConfig.Remove(key))
{
// 异步保存配置
Task.Run(() => SavePluginConfiguration(pluginName));
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"删除插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 获取插件的所有配置
/// </summary>
/// <param name="pluginName">插件名称</param>
/// <returns>配置字典</returns>
public static Dictionary<string, object> GetAllConfigurations(string pluginName)
{
lock (_lockObject)
{
if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig))
{
return new Dictionary<string, object>(pluginConfig);
}
return new Dictionary<string, object>();
}
}
/// <summary>
/// 清除插件的所有配置
/// </summary>
/// <param name="pluginName">插件名称</param>
public static void ClearAllConfigurations(string pluginName)
{
lock (_lockObject)
{
try
{
if (_pluginConfigs.Remove(pluginName))
{
// 删除配置文件
string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json");
if (File.Exists(configFile))
{
File.Delete(configFile);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清除插件 {pluginName} 所有配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 加载插件配置
/// </summary>
/// <param name="pluginName">插件名称</param>
public static void LoadPluginConfiguration(string pluginName)
{
try
{
string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json");
if (File.Exists(configFile))
{
string json = File.ReadAllText(configFile);
var config = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
lock (_lockObject)
{
_pluginConfigs[pluginName] = config ?? new Dictionary<string, object>();
}
LogHelper.WriteLogToFile($"已加载插件 {pluginName} 的配置");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载插件 {pluginName} 配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 保存插件配置
/// </summary>
/// <param name="pluginName">插件名称</param>
private static void SavePluginConfiguration(string pluginName)
{
try
{
Dictionary<string, object> pluginConfig;
lock (_lockObject)
{
if (!_pluginConfigs.TryGetValue(pluginName, out pluginConfig))
{
return;
}
}
string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json");
string json = JsonConvert.SerializeObject(pluginConfig, Formatting.Indented);
File.WriteAllText(configFile, json);
LogHelper.WriteLogToFile($"已保存插件 {pluginName} 的配置");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存插件 {pluginName} 配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 加载所有插件的配置
/// </summary>
public static void LoadAllPluginConfigurations()
{
try
{
if (Directory.Exists(PluginConfigDirectory))
{
string[] configFiles = Directory.GetFiles(PluginConfigDirectory, "*.json");
foreach (string configFile in configFiles)
{
string pluginName = Path.GetFileNameWithoutExtension(configFile);
LoadPluginConfiguration(pluginName);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载所有插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 保存所有插件的配置
/// </summary>
public static void SaveAllPluginConfigurations()
{
try
{
lock (_lockObject)
{
foreach (string pluginName in _pluginConfigs.Keys)
{
SavePluginConfiguration(pluginName);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存所有插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,509 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 插件服务管理器,实现IPluginService接口,提供对软件内部功能的访问
/// </summary>
public class PluginServiceManager : IPluginService
{
private static PluginServiceManager _instance;
private MainWindow _mainWindow;
private Dictionary<string, EventHandler> _eventHandlers;
/// <summary>
/// 单例实例
/// </summary>
public static PluginServiceManager Instance
{
get
{
if (_instance == null)
{
_instance = new PluginServiceManager();
}
return _instance;
}
}
private PluginServiceManager()
{
_eventHandlers = new Dictionary<string, EventHandler>();
}
/// <summary>
/// 设置主窗口引用
/// </summary>
/// <param name="mainWindow">主窗口实例</param>
public void SetMainWindow(MainWindow mainWindow)
{
_mainWindow = mainWindow;
}
#region UI访问
public Window MainWindow => _mainWindow;
public InkCanvas CurrentCanvas => null; // 暂时返回null,避免访问权限问题
public List<Canvas> AllCanvasPages => new List<Canvas>(); // 暂时返回空列表
public int CurrentPageIndex => 0; // 暂时返回0
public int TotalPageCount => 0; // 暂时返回0
public FrameworkElement FloatingToolBar => _mainWindow?.ViewboxFloatingBar;
public FrameworkElement LeftPanel => _mainWindow?.BlackboardLeftSide;
public FrameworkElement RightPanel => _mainWindow?.BlackboardRightSide;
public FrameworkElement TopPanel => _mainWindow?.BorderTools;
public FrameworkElement BottomPanel => _mainWindow?.BorderSettings;
#endregion
#region
public int CurrentDrawingMode => 0; // 暂时返回0
public double CurrentInkWidth => 2.5; // 暂时返回默认值
public Color CurrentInkColor => Colors.Black; // 暂时返回默认值
public double CurrentHighlighterWidth => 20.0; // 暂时返回默认值
public int CurrentEraserSize => 2; // 暂时返回默认值
public int CurrentEraserType => 0; // 暂时返回默认值
public int CurrentEraserShape => 0; // 暂时返回默认值
public double CurrentInkAlpha => 255.0; // 暂时返回默认值
public int CurrentInkStyle => 0; // 暂时返回默认值
public string CurrentBackgroundColor => "#162924"; // 暂时返回默认值
#endregion
#region
public bool IsDarkTheme => false; // 暂时返回默认值
public bool IsWhiteboardMode => false; // 暂时返回默认值
public bool IsPPTMode => false; // 暂时返回默认值
public bool IsFullScreenMode => false; // 暂时返回默认值
public bool IsCanvasMode => true; // 暂时返回默认值
public bool IsSelectionMode => false; // 暂时返回默认值
public bool IsEraserMode => false; // 暂时返回默认值
public bool IsShapeDrawingMode => false; // 暂时返回默认值
public bool IsHighlighterMode => false; // 暂时返回默认值
#endregion
#region IGetService
public bool CanUndo => false; // 暂时返回默认值
public bool CanRedo => false; // 暂时返回默认值
public T GetSetting<T>(string key, T defaultValue = default(T))
{
// 暂时不实现,避免访问权限问题
return defaultValue;
}
public List<IPlugin> GetAllPlugins()
{
return new List<IPlugin>(PluginManager.Instance.Plugins);
}
public IPlugin GetPlugin(string pluginName)
{
return PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName);
}
#endregion
#region IWindowService
public void ShowSettingsWindow()
{
// 暂时不实现,避免访问权限问题
}
public void HideSettingsWindow()
{
// 暂时不实现,避免访问权限问题
}
public void ShowPluginSettingsWindow()
{
// 暂时不实现,避免访问权限问题
}
public void HidePluginSettingsWindow()
{
// 暂时不实现,避免访问权限问题
}
public void ShowHelpWindow()
{
// 暂时不实现,避免访问权限问题
}
public void HideHelpWindow()
{
// 暂时不实现,避免访问权限问题
}
public void ShowAboutWindow()
{
// 暂时不实现,避免访问权限问题
}
public void HideAboutWindow()
{
// 暂时不实现,避免访问权限问题
}
public void ShowNotification(string message, NotificationType type = NotificationType.Info)
{
// 暂时不实现,避免访问权限问题
}
public bool ShowConfirmDialog(string message, string title = "确认")
{
// 暂时不实现,避免访问权限问题
return false;
}
public string ShowInputDialog(string message, string title = "输入", string defaultValue = "")
{
// 暂时不实现,避免访问权限问题
return defaultValue;
}
public void SetFullScreen(bool isFullScreen)
{
// 暂时不实现,避免访问权限问题
}
public void SetTopMost(bool isTopMost)
{
// 暂时不实现,避免访问权限问题
}
public void SetWindowVisibility(bool isVisible)
{
// 暂时不实现,避免访问权限问题
}
public void MinimizeWindow()
{
// 暂时不实现,避免访问权限问题
}
public void MaximizeWindow()
{
// 暂时不实现,避免访问权限问题
}
public void RestoreWindow()
{
// 暂时不实现,避免访问权限问题
}
public void CloseWindow()
{
// 暂时不实现,避免访问权限问题
}
public void SetWindowPosition(double x, double y)
{
// 暂时不实现,避免访问权限问题
}
public void SetWindowSize(double width, double height)
{
// 暂时不实现,避免访问权限问题
}
public (double x, double y) GetWindowPosition()
{
// 暂时不实现,避免访问权限问题
return (0, 0);
}
public (double width, double height) GetWindowSize()
{
// 暂时不实现,避免访问权限问题
return (800, 600);
}
#endregion
#region IActionService
public void ClearCanvas()
{
// 暂时不实现,避免访问权限问题
}
public void ClearAllCanvases()
{
// 暂时不实现,避免访问权限问题
}
public void AddNewPage()
{
// 暂时不实现,避免访问权限问题
}
public void DeleteCurrentPage()
{
// 暂时不实现,避免访问权限问题
}
public void SwitchToPage(int pageIndex)
{
// 暂时不实现,避免访问权限问题
}
public void NextPage()
{
// 暂时不实现,避免访问权限问题
}
public void PreviousPage()
{
// 暂时不实现,避免访问权限问题
}
public void SetDrawingMode(int mode)
{
// 暂时不实现,避免访问权限问题
}
public void SetInkWidth(double width)
{
// 暂时不实现,避免访问权限问题
}
public void SetInkColor(Color color)
{
// 暂时不实现,避免访问权限问题
}
public void SetHighlighterWidth(double width)
{
// 暂时不实现,避免访问权限问题
}
public void SetEraserSize(int size)
{
// 暂时不实现,避免访问权限问题
}
public void SetEraserType(int type)
{
// 暂时不实现,避免访问权限问题
}
public void SetEraserShape(int shape)
{
// 暂时不实现,避免访问权限问题
}
public void SetInkAlpha(double alpha)
{
// 暂时不实现,避免访问权限问题
}
public void SetInkStyle(int style)
{
// 暂时不实现,避免访问权限问题
}
public void SetBackgroundColor(string color)
{
// 暂时不实现,避免访问权限问题
}
public void SaveCanvas(string filePath)
{
// 暂时不实现,避免访问权限问题
}
public void LoadCanvas(string filePath)
{
// 暂时不实现,避免访问权限问题
}
public void ExportAsImage(string filePath, string format)
{
// 暂时不实现,避免访问权限问题
}
public void ExportAsPDF(string filePath)
{
// 暂时不实现,避免访问权限问题
}
public void Undo()
{
// 暂时不实现,避免访问权限问题
}
public void Redo()
{
// 暂时不实现,避免访问权限问题
}
public void SelectAll()
{
// 暂时不实现,避免访问权限问题
}
public void DeselectAll()
{
// 暂时不实现,避免访问权限问题
}
public void DeleteSelected()
{
// 暂时不实现,避免访问权限问题
}
public void CopySelected()
{
// 暂时不实现,避免访问权限问题
}
public void CutSelected()
{
// 暂时不实现,避免访问权限问题
}
public void Paste()
{
// 暂时不实现,避免访问权限问题
}
public void SetSetting<T>(string key, T value)
{
// 暂时不实现,避免访问权限问题
}
public void SaveSettings()
{
// 暂时不实现,避免访问权限问题
}
public void LoadSettings()
{
// 暂时不实现,避免访问权限问题
}
public void ResetSettings()
{
// 暂时不实现,避免访问权限问题
}
public void EnablePlugin(string pluginName)
{
var plugin = GetPlugin(pluginName);
if (plugin != null)
{
PluginManager.Instance.TogglePlugin(plugin, true);
}
}
public void DisablePlugin(string pluginName)
{
var plugin = GetPlugin(pluginName);
if (plugin != null)
{
PluginManager.Instance.TogglePlugin(plugin, false);
}
}
public void UnloadPlugin(string pluginName)
{
var plugin = GetPlugin(pluginName);
if (plugin != null)
{
PluginManager.Instance.UnloadPlugin(plugin);
}
}
public void RegisterEventHandler(string eventName, EventHandler handler)
{
if (!_eventHandlers.ContainsKey(eventName))
{
_eventHandlers[eventName] = handler;
}
else
{
_eventHandlers[eventName] += handler;
}
}
public void UnregisterEventHandler(string eventName, EventHandler handler)
{
if (_eventHandlers.ContainsKey(eventName))
{
_eventHandlers[eventName] -= handler;
}
}
public void TriggerEvent(string eventName, object sender, EventArgs args)
{
if (_eventHandlers.ContainsKey(eventName))
{
_eventHandlers[eventName]?.Invoke(sender, args);
}
}
public void RestartApplication()
{
// 暂时不实现,避免访问权限问题
}
public void ExitApplication()
{
// 暂时不实现,避免访问权限问题
}
public void CheckForUpdates()
{
// 暂时不实现,避免访问权限问题
}
public void OpenHelpDocument()
{
// 暂时不实现,避免访问权限问题
}
public void OpenAboutPage()
{
// 暂时不实现,避免访问权限问题
}
#endregion
}
}
@@ -1,276 +0,0 @@
using System;
using System.Windows;
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 插件模板,用于开发者参考
/// 注意:实际开发时,请将此类移到单独的程序集中
/// </summary>
public class PluginTemplate : PluginBase
{
#region
/// <summary>
/// 插件名称
/// </summary>
public override string Name => "插件模板";
/// <summary>
/// 插件描述
/// </summary>
public override string Description => "这是一个插件开发模板,用于开发者参考。";
/// <summary>
/// 插件版本
/// </summary>
public override Version Version => new Version(1, 0, 0);
/// <summary>
/// 插件作者
/// </summary>
public override string Author => "Your Name";
/// <summary>
/// 是否为内置插件(外部插件请返回false)
/// </summary>
public override bool IsBuiltIn => false;
#endregion
#region
/// <summary>
/// 插件初始化
/// 在这里进行插件的初始化工作,如加载配置、注册事件等
/// </summary>
public override void Initialize()
{
// 先调用基类方法,这样会设置插件ID和记录日志
base.Initialize();
// TODO: 在这里进行插件初始化工作
// 示例:记录初始化信息
LogHelper.WriteLogToFile($"插件 {Name} 开始初始化");
// 示例:加载配置
LoadConfig();
// 示例:注册自定义事件
// MainWindow.Instance.SomeEvent += OnSomeEvent;
LogHelper.WriteLogToFile($"插件 {Name} 初始化完成");
}
/// <summary>
/// 启用插件
/// 在这里激活插件功能
/// </summary>
public override void Enable()
{
// 先调用基类方法,这样会设置插件状态和记录日志
base.Enable();
// TODO: 在这里启用插件功能
LogHelper.WriteLogToFile($"插件 {Name} 已启用");
}
/// <summary>
/// 禁用插件
/// 在这里停用插件功能
/// </summary>
public override void Disable()
{
// 先调用基类方法,这样会设置插件状态和记录日志
base.Disable();
// TODO: 在这里禁用插件功能
LogHelper.WriteLogToFile($"插件 {Name} 已禁用");
}
/// <summary>
/// 清理资源
/// 在插件卸载时调用,清理资源
/// </summary>
public override void Cleanup()
{
// TODO: 在这里清理插件资源
// 示例:取消注册事件
// MainWindow.Instance.SomeEvent -= OnSomeEvent;
// 示例:保存配置
SaveConfig();
// 最后调用基类方法
base.Cleanup();
}
#endregion
#region
/// <summary>
/// 加载插件配置
/// </summary>
private void LoadConfig()
{
try
{
// TODO: 从文件或其他位置加载配置
// 示例:
// string configPath = Path.Combine(App.RootPath, "PluginConfigs", "YourPluginName.json");
// if (File.Exists(configPath))
// {
// string json = File.ReadAllText(configPath);
// YourConfig = Newtonsoft.Json.JsonConvert.DeserializeObject<YourConfigClass>(json);
// }
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 保存插件配置
/// </summary>
private void SaveConfig()
{
try
{
// TODO: 保存配置到文件或其他位置
// 示例:
// string configDir = Path.Combine(App.RootPath, "PluginConfigs");
// if (!Directory.Exists(configDir))
// {
// Directory.CreateDirectory(configDir);
// }
// string configPath = Path.Combine(configDir, "YourPluginName.json");
// string json = Newtonsoft.Json.JsonConvert.SerializeObject(YourConfig, Newtonsoft.Json.Formatting.Indented);
// File.WriteAllText(configPath, json);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region
/// <summary>
/// 获取插件设置界面
/// </summary>
/// <returns>插件设置界面</returns>
public override UserControl GetSettingsView()
{
// 创建插件设置界面
return new PluginTemplateSettingsControl();
}
#endregion
#region
// TODO: 在这里添加插件的具体功能方法
/// <summary>
/// 示例方法:执行一些功能
/// </summary>
public void DoSomething()
{
if (!IsEnabled) return;
try
{
// TODO: 实现你的功能
MessageBox.Show("插件功能执行示例", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"执行插件功能时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
}
/// <summary>
/// 插件设置控件
/// </summary>
public class PluginTemplateSettingsControl : UserControl
{
public PluginTemplateSettingsControl()
{
// 创建设置界面布局
var panel = new StackPanel
{
Margin = new Thickness(10)
};
// 添加标题
panel.Children.Add(new TextBlock
{
Text = "插件模板设置",
FontSize = 16,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 0, 0, 10)
});
// 添加说明文字
panel.Children.Add(new TextBlock
{
Text = "这是一个示例设置界面,你可以在这里添加自己的设置控件。",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 15)
});
// 添加示例设置选项
var checkBox = new CheckBox
{
Content = "启用某项功能",
Margin = new Thickness(0, 0, 0, 10)
};
panel.Children.Add(checkBox);
// 添加文本输入框
panel.Children.Add(new TextBlock
{
Text = "设置项:",
Margin = new Thickness(0, 5, 0, 5)
});
panel.Children.Add(new TextBox
{
Margin = new Thickness(0, 0, 0, 10),
Width = 200,
HorizontalAlignment = HorizontalAlignment.Left
});
// 添加按钮
var button = new Button
{
Content = "保存设置",
Padding = new Thickness(10, 5, 10, 5),
Margin = new Thickness(0, 10, 0, 0),
HorizontalAlignment = HorizontalAlignment.Left
};
button.Click += (sender, e) =>
{
MessageBox.Show("设置已保存!", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information);
};
panel.Children.Add(button);
// 设置控件内容
Content = panel;
}
}
}
@@ -366,17 +366,21 @@ namespace Ink_Canvas.Helpers
/// </summary>
/// <param name="root">要开始扫描的根目录路径。</param>
/// <remarks>
/// 仅处理扩展名为 `.exe`, `.dll`, `.config`, `.manifest`, `.dat`, `.enc` 的文件;会跳过被 IsExcludedPath 判定为排除的路径。遇到任何 I/O 或访问错误时会静默忽略,不会抛出异常。</remarks>
/// 仅处理扩展名为 `.exe`, `.dll`, `.config`, `.manifest`, `.dat`, `.enc` 的文件,以及应用根目录下的点名名单 `Names.txt`
/// 会跳过被 IsExcludedPath 判定为排除的路径。遇到任何 I/O 或访问错误时会静默忽略,不会抛出异常。</remarks>
private static void LockFilesRecursive(string root)
{
try
{
var rollCallNamesPath = NormalizePath(Path.Combine(root, "Names.txt"));
foreach (var file in Directory.GetFiles(root, "*", SearchOption.AllDirectories))
{
if (!IsExcludedPath(file))
{
var ext = Path.GetExtension(file);
if (string.Equals(ext, ".exe", StringComparison.OrdinalIgnoreCase) ||
var normFile = NormalizePath(file);
if (string.Equals(normFile, rollCallNamesPath, StringComparison.OrdinalIgnoreCase) ||
string.Equals(ext, ".exe", StringComparison.OrdinalIgnoreCase) ||
string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase) ||
string.Equals(ext, ".config", StringComparison.OrdinalIgnoreCase) ||
string.Equals(ext, ".manifest", StringComparison.OrdinalIgnoreCase) ||
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 渲染保存文件名模板。支持占位符: {date} {time} {datetime} {mode} {page} {count} {type}。
/// 当模板为空、渲染结果非法或仅含分隔符时,回退到默认时间戳命名。
/// </summary>
public static class SaveFileNameHelper
{
private const string DefaultDateTime = "yyyy-MM-dd HH-mm-ss-fff";
public static string Render(string template, SaveFileNameContext ctx)
{
if (ctx == null) ctx = new SaveFileNameContext();
var now = ctx.Time ?? DateTime.Now;
if (string.IsNullOrWhiteSpace(template))
return now.ToString(DefaultDateTime);
try
{
string result = template
.Replace("{date}", now.ToString("yyyy-MM-dd"))
.Replace("{time}", now.ToString("HH-mm-ss"))
.Replace("{datetime}", now.ToString(DefaultDateTime))
.Replace("{mode}", ctx.Mode ?? "")
.Replace("{page}", ctx.Page?.ToString() ?? "")
.Replace("{count}", ctx.Count?.ToString() ?? "")
.Replace("{type}", ctx.Type ?? "");
result = SanitizeFileName(result);
if (string.IsNullOrWhiteSpace(result) || Regex.IsMatch(result, @"^[\s\-_]+$"))
return now.ToString(DefaultDateTime);
return result;
}
catch
{
return now.ToString(DefaultDateTime);
}
}
private static string SanitizeFileName(string name)
{
if (string.IsNullOrEmpty(name)) return name;
foreach (var c in Path.GetInvalidFileNameChars())
{
name = name.Replace(c, '_');
}
return name.Trim();
}
}
public class SaveFileNameContext
{
public DateTime? Time { get; set; }
/// <summary>"Annotation" or "BlackBoard" or "Screenshot" etc.</summary>
public string Mode { get; set; }
/// <summary>"User" or "Auto"</summary>
public string Type { get; set; }
public int? Page { get; set; }
public int? Count { get; set; }
}
}
+10
View File
@@ -57,6 +57,16 @@ namespace Ink_Canvas.Helpers
public static bool IsPasswordRequiredForResetConfig(Settings settings)
=> IsPasswordFeatureEnabled(settings) && HasPasswordConfigured(settings) && settings.Security.RequirePasswordOnResetConfig;
/// <summary>
/// 指示在修改或清空点名名单前是否需要输入安全密码。
/// </summary>
/// <param name="settings">应用设置对象。</param>
/// <returns>当已启用密码功能、已配置密码且开启了对应开关时返回 true;否则返回 false。</returns>
public static bool IsPasswordRequiredForModifyOrClearNameList(Settings settings)
=> IsPasswordFeatureEnabled(settings)
&& HasPasswordConfigured(settings)
&& settings.Security.RequirePasswordOnModifyOrClearNameList;
/// <summary>
/// 将提供的明文密码与 Settings 中存储的密码散列进行比对以验证密码是否正确。
/// </summary>
+122 -46
View File
@@ -1,72 +1,148 @@
using Microsoft.Win32;
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
namespace Ink_Canvas.Helpers
{
internal class SoftwareLauncher
internal static class SoftwareLauncher
{
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
/// <summary>与 ICA 一致:在「程序和功能」卸载列表中按 DisplayName 匹配后启动 sweclauncher.exe。</summary>
public static void LaunchEasiCamera(string softwareName)
{
string executablePath = FindEasiCameraExecutablePath(softwareName);
if (!string.IsNullOrEmpty(executablePath))
if (string.IsNullOrEmpty(executablePath))
{
try
{
Process.Start(executablePath);
//Console.WriteLine(softwareName + " 启动成功!");
}
catch (Exception ex)
{
Console.WriteLine("启动失败: " + ex.Message);
//MessageBox.Show("启动失败: " + ex.Message);
}
MessageBox.Show(
"未找到希沃视频展台安装信息(已扫描 64 位与 32 位卸载注册表)。请确认已通过官方安装包安装「希沃视频展台」。",
"Ink Canvas",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
}
try
{
var directory = Path.GetDirectoryName(executablePath);
var psi = new ProcessStartInfo
{
FileName = executablePath,
UseShellExecute = true,
WorkingDirectory = string.IsNullOrEmpty(directory) ? Environment.SystemDirectory : directory
};
Process.Start(psi);
}
catch (Exception ex)
{
MessageBox.Show(
"无法启动希沃视频展台:" + ex.Message,
"Ink Canvas",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
//Console.WriteLine(softwareName + " 未找到可执行文件路径。");
}
private static string FindEasiCameraExecutablePath(string softwareName)
{
string executablePath = null;
if (string.IsNullOrWhiteSpace(softwareName))
return null;
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall"))
// 须用 OpenBaseKey + RegistryView 显式指定视图:Registry.LocalMachine.OpenSubKey 跟随进程位数,
// 32 位进程下无法靠拼接 WOW6432Node 路径进入 64 位视图,会找不到 64 位安装的展台。
const string uninstallSubKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
foreach (RegistryView view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
{
foreach (string subkeyName in key.GetSubKeyNames())
using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
using (RegistryKey key = baseKey.OpenSubKey(uninstallSubKey))
{
using (RegistryKey subkey = key.OpenSubKey(subkeyName))
{
string displayName = subkey.GetValue("DisplayName") as string;
string installLocation = subkey.GetValue("InstallLocation") as string;
string uninstallString = subkey.GetValue("UninstallString") as string;
if (!string.IsNullOrEmpty(displayName) && displayName.Contains(softwareName))
{
if (!string.IsNullOrEmpty(installLocation))
{
executablePath = Path.Combine(installLocation, "sweclauncher.exe");
}
else if (!string.IsNullOrEmpty(uninstallString))
{
int lastSlashIndex = uninstallString.LastIndexOf("\\");
if (lastSlashIndex >= 0)
{
string folderPath = uninstallString.Substring(0, lastSlashIndex);
executablePath = Path.Combine(folderPath, "sweclauncher", "sweclauncher.exe");
}
}
break;
}
}
if (key == null) continue;
string found = FindInUninstallKey(key, softwareName);
if (!string.IsNullOrEmpty(found))
return found;
}
}
return executablePath;
return null;
}
private static string FindInUninstallKey(RegistryKey uninstallKey, string softwareName)
{
foreach (string subkeyName in uninstallKey.GetSubKeyNames())
{
using (RegistryKey subkey = uninstallKey.OpenSubKey(subkeyName))
{
if (subkey == null) continue;
string displayName = subkey.GetValue("DisplayName") as string;
if (string.IsNullOrEmpty(displayName) || !displayName.Contains(softwareName))
continue;
string installLocation = subkey.GetValue("InstallLocation") as string;
string uninstallString = subkey.GetValue("UninstallString") as string;
string resolved = TryResolveSweclauncher(installLocation, uninstallString);
if (!string.IsNullOrEmpty(resolved) && File.Exists(resolved))
return resolved;
}
}
return null;
}
private static string TryResolveSweclauncher(string installLocation, string uninstallString)
{
if (!string.IsNullOrWhiteSpace(installLocation))
{
string fromLoc = ResolveSweclauncherUnderInstallRoot(installLocation.Trim().TrimEnd('\\'));
if (!string.IsNullOrEmpty(fromLoc))
return fromLoc;
}
if (!string.IsNullOrWhiteSpace(uninstallString))
{
// 常见:"...\uninstall.exe" 或带引号路径
string trimmed = uninstallString.Trim();
if (trimmed.Length >= 2 && trimmed[0] == '"')
{
int end = trimmed.IndexOf('"', 1);
if (end > 1)
trimmed = trimmed.Substring(1, end - 1);
}
int lastSlash = trimmed.LastIndexOf('\\');
if (lastSlash < 0)
return null;
string folderPath = trimmed.Substring(0, lastSlash);
string candidate = Path.Combine(folderPath, "sweclauncher", "sweclauncher.exe");
if (File.Exists(candidate))
return candidate;
candidate = Path.Combine(folderPath, "sweclauncher.exe");
if (File.Exists(candidate))
return candidate;
}
return null;
}
private static string ResolveSweclauncherUnderInstallRoot(string installRoot)
{
string[] candidates =
{
Path.Combine(installRoot, "sweclauncher.exe"),
Path.Combine(installRoot, "sweclauncher", "sweclauncher.exe"),
};
foreach (string p in candidates)
{
if (File.Exists(p))
return p;
}
return null;
}
}
}
+11 -5
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Ink;
@@ -100,8 +100,8 @@ namespace Ink_Canvas.Helpers
var item = _currentStrokeHistory[_currentIndex];
item.StrokeHasBeenCleared = !item.StrokeHasBeenCleared;
_currentIndex--;
OnUndoStateChanged?.Invoke(_currentIndex > -1);
OnRedoStateChanged?.Invoke(_currentStrokeHistory.Count - _currentIndex - 1 > 0);
OnUndoStateChanged?.Invoke(CanUndo);
OnRedoStateChanged?.Invoke(CanRedo);
return item;
}
@@ -137,9 +137,15 @@ namespace Ink_Canvas.Helpers
}
private void NotifyUndoRedoState()
{
OnUndoStateChanged?.Invoke(_currentIndex > -1);
OnRedoStateChanged?.Invoke(_currentStrokeHistory.Count - _currentIndex - 1 > 0);
OnUndoStateChanged?.Invoke(CanUndo);
OnRedoStateChanged?.Invoke(CanRedo);
}
/// <summary>当前历史是否允许撤销。</summary>
public bool CanUndo => _currentIndex > -1;
/// <summary>当前历史是否允许重做。</summary>
public bool CanRedo => _currentStrokeHistory.Count > 0 && _currentStrokeHistory.Count - _currentIndex - 1 > 0;
}
public class TimeMachineHistory
-165
View File
@@ -1,165 +0,0 @@
using System;
using System.IO;
using System.Reflection;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// UIAccess DLL释放器
/// </summary>
public static class UIAccessDllExtractor
{
private static readonly string[] RequiredDlls = {
"UIAccessDLL_x64.dll",
"UIAccessDLL_x86.dll"
};
/// <summary>
/// 在应用启动时释放UIAccess相关DLL
/// </summary>
public static void ExtractUIAccessDlls()
{
try
{
string appDirectory = AppDomain.CurrentDomain.BaseDirectory;
LogHelper.WriteLogToFile("开始检查并释放UIAccess相关DLL文件");
foreach (string dllName in RequiredDlls)
{
string targetPath = Path.Combine(appDirectory, dllName);
// 检查文件是否已存在且有效
if (File.Exists(targetPath) && IsValidDll(targetPath))
{
LogHelper.WriteLogToFile($"{dllName} 已存在且有效,跳过释放");
continue;
}
// 从嵌入资源中释放DLL
if (ExtractDllFromResource(dllName, targetPath))
{
LogHelper.WriteLogToFile($"成功释放 {dllName} 到 {targetPath}");
}
else
{
LogHelper.WriteLogToFile($"警告:无法释放 {dllName},可能影响UIA置顶功能", LogHelper.LogType.Warning);
}
}
LogHelper.WriteLogToFile("UIAccess DLL释放检查完成");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 从嵌入资源中提取DLL文件
/// </summary>
private static bool ExtractDllFromResource(string dllName, string targetPath)
{
try
{
Assembly assembly = Assembly.GetExecutingAssembly();
string resourceName = $"Ink_Canvas.{dllName}";
using (Stream resourceStream = assembly.GetManifestResourceStream(resourceName))
{
if (resourceStream == null)
{
LogHelper.WriteLogToFile($"未找到嵌入资源: {resourceName}", LogHelper.LogType.Warning);
return false;
}
// 确保目标目录存在
string targetDirectory = Path.GetDirectoryName(targetPath);
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}
// 写入文件
using (FileStream fileStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write))
{
resourceStream.CopyTo(fileStream);
}
return true;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从资源提取 {dllName} 失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 检查DLL文件是否有效
/// </summary>
private static bool IsValidDll(string filePath)
{
try
{
if (!File.Exists(filePath))
return false;
FileInfo fileInfo = new FileInfo(filePath);
// 检查文件大小(空文件或过小的文件可能无效)
if (fileInfo.Length < 1024) // 小于1KB可能无效
return false;
// 简单检查PE头(DLL文件应该以MZ开头)
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[2];
if (fs.Read(buffer, 0, 2) == 2)
{
return buffer[0] == 0x4D && buffer[1] == 0x5A; // "MZ"
}
}
return false;
}
catch
{
return false;
}
}
/// <summary>
/// 清理释放的DLL文件(可选,在应用退出时调用)
/// </summary>
public static void CleanupExtractedDlls()
{
try
{
string appDirectory = AppDomain.CurrentDomain.BaseDirectory;
foreach (string dllName in RequiredDlls)
{
string filePath = Path.Combine(appDirectory, dllName);
if (File.Exists(filePath))
{
try
{
File.Delete(filePath);
LogHelper.WriteLogToFile($"已清理 {dllName}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理 {dllName} 失败: {ex.Message}", LogHelper.LogType.Warning);
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
+672
View File
@@ -0,0 +1,672 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 通过 Winlogon 令牌模拟实现 UIAccess 提权重启。
/// 1. 找到当前会话中 winlogon.exe 的令牌,复制为模拟令牌;
/// 2. SetThreadToken 暂时模拟 winlogon(拥有 TCB 权限);
/// 3. 在自身令牌副本上 SetTokenInformation(TokenUIAccess, TRUE)
/// 4. RevertToSelf 后用 CreateProcessWithTokenW 启动新进程;
/// 5. 新进程具有 UIAccess 权限,可置顶于 UAC 提示之上。
/// </summary>
public static class UIAccessHelper
{
#region Constants
private const uint TOKEN_QUERY = 0x0008;
private const uint TOKEN_DUPLICATE = 0x0002;
private const uint TOKEN_IMPERSONATE = 0x0004;
private const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
private const uint TOKEN_ADJUST_DEFAULT = 0x0080;
private const uint TOKEN_ADJUST_SESSIONID = 0x0100;
private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
private const int SecurityAnonymous = 0;
private const int SecurityImpersonation = 2;
private const int TokenPrimary = 1;
private const int TokenImpersonation = 2;
// TOKEN_INFORMATION_CLASS
private const int TokenSessionId = 12;
private const int TokenElevationType = 18;
private const int TokenUIAccess = 26;
// TOKEN_ELEVATION_TYPE
private const int TokenElevationTypeDefault = 1;
private const int TokenElevationTypeFull = 2;
private const int TokenElevationTypeLimited = 3;
private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
private const uint TH32CS_SNAPPROCESS = 0x00000002;
private const uint LOGON_WITH_PROFILE = 0x00000001;
private const uint CREATE_NEW_CONSOLE = 0x00000010;
private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
private const uint SE_PRIVILEGE_ENABLED = 0x00000002;
private const string SE_ASSIGNPRIMARYTOKEN_NAME = "SeAssignPrimaryTokenPrivilege";
private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
#endregion
#region Structs
[StructLayout(LayoutKind.Sequential)]
private struct LUID
{
public uint LowPart;
public int HighPart;
}
[StructLayout(LayoutKind.Sequential)]
private struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public uint Attributes;
}
[StructLayout(LayoutKind.Sequential)]
private struct TOKEN_PRIVILEGES
{
public uint PrivilegeCount;
public LUID_AND_ATTRIBUTES Privilege;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct PROCESSENTRY32W
{
public uint dwSize;
public uint cntUsage;
public uint th32ProcessID;
public IntPtr th32DefaultHeapID;
public uint th32ModuleID;
public uint cntThreads;
public uint th32ParentProcessID;
public int pcPriClassBase;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szExeFile;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFOW
{
public uint cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public uint dwX, dwY, dwXSize, dwYSize;
public uint dwXCountChars, dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public ushort wShowWindow;
public ushort cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput, hStdOutput, hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
#endregion
#region P/Invoke
[DllImport("kernel32.dll")]
private static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DuplicateTokenEx(
IntPtr hExistingToken,
uint dwDesiredAccess,
IntPtr lpTokenAttributes,
int ImpersonationLevel,
int TokenType,
out IntPtr phNewToken);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetTokenInformation(
IntPtr TokenHandle,
int TokenInformationClass,
IntPtr TokenInformation,
uint TokenInformationLength,
out uint ReturnLength);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetTokenInformation(
IntPtr TokenHandle,
int TokenInformationClass,
IntPtr TokenInformation,
uint TokenInformationLength);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetThreadToken(IntPtr Thread, IntPtr Token);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool RevertToSelf();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Process32FirstW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Process32NextW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool LookupPrivilegeValueW(string lpSystemName, string lpName, out LUID lpLuid);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AdjustTokenPrivileges(
IntPtr TokenHandle,
[MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
uint BufferLength,
IntPtr PreviousState,
IntPtr ReturnLength);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateProcessWithTokenW(
IntPtr hToken,
uint dwLogonFlags,
string lpApplicationName,
StringBuilder lpCommandLine,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFOW lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern void GetStartupInfoW(ref STARTUPINFOW lpStartupInfo);
#endregion
#region Public API
/// <summary>
/// 检查当前进程是否已具有 UIAccess 标志。
/// </summary>
public static bool HasUIAccess()
{
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hToken))
return false;
try
{
IntPtr buf = Marshal.AllocHGlobal(sizeof(uint));
try
{
Marshal.WriteInt32(buf, 0);
if (!GetTokenInformation(hToken, TokenUIAccess, buf, sizeof(uint), out _))
return false;
return Marshal.ReadInt32(buf) != 0;
}
finally
{
Marshal.FreeHGlobal(buf);
}
}
finally
{
CloseHandle(hToken);
}
}
/// <summary>
/// 以 UIAccess 令牌重启自身。当前进程必须已经以管理员身份运行。
/// 成功时新进程已启动,调用方应立即退出当前进程。
/// </summary>
/// <param name="extraArgs">追加到新进程的额外命令行参数(例如 --skip-mutex-check)。</param>
public static bool RestartWithUIAccess(string extraArgs = null)
{
try
{
if (HasUIAccess())
{
LogHelper.WriteLogToFile("UIAccess | 当前进程已具有 UIAccess,跳过重启");
return true;
}
if (!CreateUIAccessToken(out IntPtr uiaToken))
{
LogHelper.WriteLogToFile($"UIAccess | 创建 UIAccess 令牌失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
return false;
}
try
{
return LaunchWithToken(uiaToken, extraArgs);
}
finally
{
CloseHandle(uiaToken);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"UIAccess | RestartWithUIAccess 异常: {ex}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 以普通用户权限(非提升)重启自身。
/// 通过获取 explorer.exe / ctfmon.exe 的非特权令牌,再用 CreateProcessWithTokenW 启动新进程,
/// 避免经由 explorer.exe 中转可能产生的 UAC 提示或丢失参数问题。
/// 成功时调用方应立即退出当前进程。
/// </summary>
/// <param name="extraArgs">追加到新进程的额外命令行参数。</param>
public static bool RestartAsNormalUser(string extraArgs = null)
{
try
{
if (!GetUserPrimaryToken(out IntPtr userToken))
{
LogHelper.WriteLogToFile($"UIAccess | 获取用户令牌失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
return false;
}
try
{
return LaunchWithToken(userToken, extraArgs);
}
finally
{
CloseHandle(userToken);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"UIAccess | RestartAsNormalUser 异常: {ex}", LogHelper.LogType.Error);
return false;
}
}
#endregion
#region Token Manipulation
private static bool CreateUIAccessToken(out IntPtr uiaToken)
{
uiaToken = IntPtr.Zero;
// 1. 获取当前进程的 session id
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hSelfQuery))
{
LogHelper.WriteLogToFile($"UIAccess | OpenProcessToken(query) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
uint sessionId;
try
{
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
if (!GetTokenInformation(hSelfQuery, TokenSessionId, sesBuf, sizeof(uint), out _))
{
LogHelper.WriteLogToFile($"UIAccess | GetTokenInformation(SessionId) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
sessionId = (uint)Marshal.ReadInt32(sesBuf);
}
finally { Marshal.FreeHGlobal(sesBuf); }
}
finally { CloseHandle(hSelfQuery); }
// 2. 找到同一会话的 winlogon 模拟令牌
if (!GetWinlogonImpersonationToken(sessionId, out IntPtr winlogonToken))
{
LogHelper.WriteLogToFile("UIAccess | 未能获取 winlogon 模拟令牌(需要管理员权限)", LogHelper.LogType.Error);
return false;
}
try
{
// 3. 模拟 winlogon
if (!SetThreadToken(IntPtr.Zero, winlogonToken))
{
LogHelper.WriteLogToFile($"UIAccess | SetThreadToken(winlogon) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
try
{
// 4. 复制自身令牌为主令牌
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, out IntPtr hSelfDup))
{
LogHelper.WriteLogToFile($"UIAccess | OpenProcessToken(dup) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
IntPtr dupToken;
try
{
bool ok = DuplicateTokenEx(
hSelfDup,
TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
IntPtr.Zero,
SecurityAnonymous,
TokenPrimary,
out dupToken);
if (!ok)
{
LogHelper.WriteLogToFile($"UIAccess | DuplicateTokenEx 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
}
finally { CloseHandle(hSelfDup); }
// 5. 在副本上设置 UIAccess = TRUE
IntPtr uiBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
Marshal.WriteInt32(uiBuf, 1);
if (!SetTokenInformation(dupToken, TokenUIAccess, uiBuf, sizeof(uint)))
{
int err = Marshal.GetLastWin32Error();
LogHelper.WriteLogToFile($"UIAccess | SetTokenInformation(UIAccess) 失败: {err}", LogHelper.LogType.Error);
CloseHandle(dupToken);
return false;
}
}
finally { Marshal.FreeHGlobal(uiBuf); }
uiaToken = dupToken;
return true;
}
finally
{
RevertToSelf();
}
}
finally
{
CloseHandle(winlogonToken);
}
}
private static bool GetWinlogonImpersonationToken(uint sessionId, out IntPtr winlogonToken)
{
winlogonToken = IntPtr.Zero;
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE || snapshot == IntPtr.Zero)
{
LogHelper.WriteLogToFile($"UIAccess | CreateToolhelp32Snapshot 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
try
{
var pe = new PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32W)) };
bool more = Process32FirstW(snapshot, ref pe);
while (more)
{
if (string.Equals(pe.szExeFile, "winlogon.exe", StringComparison.OrdinalIgnoreCase))
{
if (TryDuplicateWinlogonToken(pe.th32ProcessID, sessionId, out winlogonToken))
return true;
}
more = Process32NextW(snapshot, ref pe);
}
}
finally { CloseHandle(snapshot); }
return false;
}
private static bool TryDuplicateWinlogonToken(uint pid, uint sessionId, out IntPtr dupToken)
{
dupToken = IntPtr.Zero;
IntPtr hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (hProc == IntPtr.Zero) return false;
try
{
if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE, out IntPtr hToken))
return false;
try
{
// 检查 session id 匹配
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
if (!GetTokenInformation(hToken, TokenSessionId, sesBuf, sizeof(uint), out _))
return false;
if ((uint)Marshal.ReadInt32(sesBuf) != sessionId)
return false;
}
finally { Marshal.FreeHGlobal(sesBuf); }
if (!DuplicateTokenEx(
hToken,
TOKEN_IMPERSONATE | TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | TOKEN_DUPLICATE,
IntPtr.Zero,
SecurityImpersonation,
TokenImpersonation,
out dupToken))
{
return false;
}
// 启用 SeAssignPrimaryTokenPrivilegeInkeys 行为)
var tkp = new TOKEN_PRIVILEGES
{
PrivilegeCount = 1,
Privilege = new LUID_AND_ATTRIBUTES { Attributes = SE_PRIVILEGE_ENABLED }
};
if (LookupPrivilegeValueW(null, SE_ASSIGNPRIMARYTOKEN_NAME, out tkp.Privilege.Luid))
{
AdjustTokenPrivileges(dupToken, false, ref tkp, (uint)Marshal.SizeOf(tkp), IntPtr.Zero, IntPtr.Zero);
}
return true;
}
finally { CloseHandle(hToken); }
}
finally { CloseHandle(hProc); }
}
/// <summary>
/// 从 explorer.exe / ctfmon.exe 取得普通用户(非提升)令牌的主令牌副本,用于降权启动。
/// 仅当当前进程为管理员时才能成功。
/// </summary>
private static bool GetUserPrimaryToken(out IntPtr userToken)
{
userToken = IntPtr.Zero;
string[] candidates = { "explorer.exe", "ctfmon.exe" };
foreach (var name in candidates)
{
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE || snapshot == IntPtr.Zero) continue;
try
{
var pe = new PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32W)) };
bool more = Process32FirstW(snapshot, ref pe);
while (more)
{
if (string.Equals(pe.szExeFile, name, StringComparison.OrdinalIgnoreCase))
{
if (TryDuplicateUserPrimaryToken(pe.th32ProcessID, out userToken))
{
LogHelper.WriteLogToFile($"UIAccess | 已从 {name} (PID={pe.th32ProcessID}) 取得用户令牌");
return true;
}
}
more = Process32NextW(snapshot, ref pe);
}
}
finally { CloseHandle(snapshot); }
}
return false;
}
private static bool TryDuplicateUserPrimaryToken(uint pid, out IntPtr dupToken)
{
dupToken = IntPtr.Zero;
IntPtr hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (hProc == IntPtr.Zero) return false;
try
{
if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE, out IntPtr hToken))
return false;
try
{
// 仅接受非提升令牌(否则降权失败)
IntPtr elevBuf = Marshal.AllocHGlobal(sizeof(int));
try
{
if (!GetTokenInformation(hToken, TokenElevationType, elevBuf, sizeof(int), out _))
return false;
int elev = Marshal.ReadInt32(elevBuf);
if (elev == TokenElevationTypeFull)
return false; // 该进程是提升令牌,跳过
}
finally { Marshal.FreeHGlobal(elevBuf); }
return DuplicateTokenEx(
hToken,
TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
IntPtr.Zero,
SecurityAnonymous,
TokenPrimary,
out dupToken);
}
finally { CloseHandle(hToken); }
}
finally { CloseHandle(hProc); }
}
#endregion
#region Process Launch
private static bool LaunchWithToken(IntPtr token, string extraArgs)
{
string exePath = Process.GetCurrentProcess().MainModule.FileName;
string workDir = System.IO.Path.GetDirectoryName(exePath);
// 重建命令行:保留原始参数,追加 --skip-mutex-check 防止单实例阻塞
var cmdBuilder = new StringBuilder(32768);
cmdBuilder.Append('"').Append(exePath).Append('"');
string[] args = Environment.GetCommandLineArgs();
for (int i = 1; i < args.Length; i++)
{
cmdBuilder.Append(' ');
AppendQuoted(cmdBuilder, args[i]);
}
if (!string.IsNullOrEmpty(extraArgs))
cmdBuilder.Append(' ').Append(extraArgs);
// 防止单实例 Mutex 阻塞新进程
if (Array.IndexOf(args, "--skip-mutex-check") < 0
&& (extraArgs == null || extraArgs.IndexOf("--skip-mutex-check", StringComparison.Ordinal) < 0))
{
cmdBuilder.Append(" --skip-mutex-check");
}
var si = new STARTUPINFOW { cb = (uint)Marshal.SizeOf(typeof(STARTUPINFOW)) };
GetStartupInfoW(ref si);
bool ok = CreateProcessWithTokenW(
token,
LOGON_WITH_PROFILE,
null,
cmdBuilder,
CREATE_UNICODE_ENVIRONMENT,
IntPtr.Zero,
workDir,
ref si,
out PROCESS_INFORMATION pi);
if (!ok)
{
int err = Marshal.GetLastWin32Error();
LogHelper.WriteLogToFile($"UIAccess | CreateProcessWithTokenW 失败: {err}", LogHelper.LogType.Error);
return false;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
LogHelper.WriteLogToFile($"UIAccess | 已使用 UIAccess 令牌启动新进程 (PID={pi.dwProcessId})");
return true;
}
private static void AppendQuoted(StringBuilder sb, string arg)
{
if (arg == null) { sb.Append("\"\""); return; }
bool needQuote = arg.Length == 0 || arg.IndexOfAny(new[] { ' ', '\t', '"' }) >= 0;
if (!needQuote) { sb.Append(arg); return; }
sb.Append('"');
int backslashes = 0;
foreach (char c in arg)
{
if (c == '\\') { backslashes++; continue; }
if (c == '"')
{
sb.Append('\\', backslashes * 2 + 1);
sb.Append('"');
}
else
{
sb.Append('\\', backslashes);
sb.Append(c);
}
backslashes = 0;
}
sb.Append('\\', backslashes * 2);
sb.Append('"');
}
#endregion
}
}
@@ -0,0 +1,830 @@
using OSVersionExtension;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using WinAnalysis = global::Windows.UI.Input.Inking.Analysis;
using WinRtInk = global::Windows.UI.Input.Inking;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// WinRT 手写体识别,以及将识别结果用手写风格字体轮廓转为墨迹笔画(「识别转手写体字形」)。
/// </summary>
internal static class WinRtHandwritingRecognizer
{
private static WinRtInk.InkRecognizer _preferredHandwritingRecognizer;
private static bool _preferredHandwritingRecognizerResolved;
private static void LogHandwriting(string message, LogHelper.LogType logType = LogHelper.LogType.Info)
{
LogHelper.WriteLogToFile("[手写体] " + message, logType);
}
public static bool IsApiAvailable =>
OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
/// <summary>
/// 启动阶段不再预热线程内 WinRT 手写管线。历史上曾用 <see cref="WinRtInkShapeRecognizer.CreateMinimalWarmupStrokeCollection"/> 跑全链路,
/// 会显著拖慢启动;与更早的「空 <see cref="StrokeCollection"/>」一样,此处不再在 Idle 上做任何工作。
/// 首次真正需要手写识别时由 <see cref="RecognizeHandwritingAsync"/> 承担冷启动成本。
/// </summary>
public static void Warmup()
{
}
/// <summary>
/// 将当前笔画集合识别为文字片段(含候选):先用墨迹分析得到分词与 <see cref="WinAnalysis.InkAnalysisInkWord.RecognizedText"/>
/// 再对每一分词用 <see cref="WinRtInk.InkRecognizerContainer"/> 取 <c>GetTextCandidates</c>(与当前 SDK 中部分版本的
/// <see cref="WinRtInk.InkRecognitionResult"/> 未暴露笔画映射的局限兼容)。
/// </summary>
/// <param name="verboseTrace">为 false 时跳过详细识别日志(用于 <see cref="Warmup"/> 等)。</param>
public static async Task<HandwritingRecognitionResult> RecognizeHandwritingAsync(
StrokeCollection strokes,
bool verboseTrace = true)
{
if (!IsApiAvailable || strokes == null || strokes.Count == 0)
return HandwritingRecognitionResult.Empty;
var traceRecognition = verboseTrace;
try
{
var recognizer = new WinRtInk.InkRecognizerContainer();
TryApplyPreferredHandwritingRecognizer(recognizer, traceRecognition);
var analyzer = new WinAnalysis.InkAnalyzer();
var idToWpf = new Dictionary<uint, Stroke>();
foreach (Stroke s in strokes)
{
var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s);
if (ink == null) continue;
analyzer.AddDataForStroke(ink);
analyzer.SetStrokeDataKind(ink.Id, WinAnalysis.InkAnalysisStrokeKind.Writing);
idToWpf[ink.Id] = s;
}
if (idToWpf.Count == 0)
{
if (traceRecognition)
LogHandwriting("识别:无有效 WinRT 笔画(全部转换失败),输入笔画数=" + strokes.Count);
return HandwritingRecognitionResult.Empty;
}
var analysisResult = await analyzer.AnalyzeAsync().AsTask().ConfigureAwait(true);
if (analysisResult == null || analysisResult.Status != WinAnalysis.InkAnalysisStatus.Updated)
{
if (traceRecognition)
LogHandwriting(
"识别:AnalyzeAsync 未得到 UpdatedStatus=" +
(analysisResult == null ? "null" : analysisResult.Status.ToString()) +
",有效笔画数=" + idToWpf.Count +
",尝试整批 RecognizeAsync 回退。");
return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true);
}
var wordNodes = analyzer.AnalysisRoot?.FindNodes(WinAnalysis.InkAnalysisNodeKind.InkWord);
if (wordNodes == null || wordNodes.Count == 0)
{
if (traceRecognition)
LogHandwriting(
"识别:未找到 InkWord 节点(墨迹分析常将非横平笔划判为绘图),有效笔画数=" + idToWpf.Count +
",改用整批 RecognizeAsync 回退。");
return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true);
}
var segments = new List<HandwritingWordSegment>();
foreach (var node in wordNodes)
{
if (!(node is WinAnalysis.InkAnalysisInkWord word))
continue;
var ids = word.GetStrokeIds();
if (ids == null || ids.Count == 0)
continue;
var group = new List<Stroke>();
foreach (var sid in ids)
{
if (idToWpf.TryGetValue(sid, out var st))
group.Add(st);
}
if (group.Count == 0)
continue;
var wbr = word.BoundingRect;
var wpfRect = new Rect(wbr.X, wbr.Y, wbr.Width, wbr.Height);
var analysisText = word.RecognizedText ?? string.Empty;
IReadOnlyList<string> candList = Array.Empty<string>();
try
{
if (recognizer != null)
{
var mini = new WinRtInk.InkStrokeContainer();
foreach (var st in group)
{
var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(st);
if (ink != null)
mini.AddStroke(ink);
}
var miniStrokes = mini.GetStrokes();
if (miniStrokes != null && miniStrokes.Count > 0)
{
var rr = await recognizer
.RecognizeAsync(mini, WinRtInk.InkRecognitionTarget.All)
.AsTask()
.ConfigureAwait(true);
if (rr != null && rr.Count > 0 && rr[0] != null)
{
var cands = rr[0].GetTextCandidates();
if (cands != null && cands.Count > 0)
candList = cands.ToList();
}
}
}
}
catch
{
candList = Array.Empty<string>();
}
var primary = candList.Count > 0 ? candList[0] : analysisText;
var mergedCandidates = new List<string>();
if (candList.Count > 0)
{
foreach (var c in candList)
{
if (!string.IsNullOrEmpty(c) && !mergedCandidates.Contains(c))
mergedCandidates.Add(c);
}
}
if (!string.IsNullOrEmpty(analysisText) && !mergedCandidates.Contains(analysisText))
mergedCandidates.Insert(0, analysisText);
if (mergedCandidates.Count == 0 && !string.IsNullOrEmpty(primary))
mergedCandidates.Add(primary);
segments.Add(new HandwritingWordSegment(
primary,
mergedCandidates,
wpfRect,
group));
}
if (segments.Count == 0)
{
if (traceRecognition)
LogHandwriting("识别:分词列表为空(InkWord 无有效笔画映射)。");
return HandwritingRecognitionResult.Empty;
}
var hr = new HandwritingRecognitionResult(segments);
if (traceRecognition)
{
var preview = hr.CombinedText;
if (preview.Length > 120)
preview = preview.Substring(0, 117) + "...";
LogHandwriting(
"识别成功:词数=" + segments.Count +
",合并文本=\"" + preview + "\"" +
",进程位数=" + (Environment.Is64BitProcess ? "x64" : "x86"));
for (var i = 0; i < segments.Count; i++)
{
var seg = segments[i];
var t = seg.Text ?? "";
if (t.Length > 40)
t = t.Substring(0, 37) + "...";
LogHandwriting(
" 词[" + i + "] 文本=\"" + t + "\",笔画数=" + seg.Strokes.Count +
",候选数=" + (seg.TextCandidates?.Count ?? 0) +
",框=(" + Math.Round(seg.BoundingRectangle.X, 1) + "," +
Math.Round(seg.BoundingRectangle.Y, 1) + "," +
Math.Round(seg.BoundingRectangle.Width, 1) + "×" +
Math.Round(seg.BoundingRectangle.Height, 1) + ")");
}
}
return hr;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("WinRT 手写识别失败: " + ex.Message, LogHelper.LogType.Warning);
if (strokes != null && strokes.Count > 0)
LogHandwriting("识别异常:" + ex.Message, LogHelper.LogType.Warning);
return HandwritingRecognitionResult.Empty;
}
}
private static void TryApplyPreferredHandwritingRecognizer(
WinRtInk.InkRecognizerContainer container,
bool logDetail)
{
if (container == null)
return;
try
{
if (!_preferredHandwritingRecognizerResolved)
{
_preferredHandwritingRecognizerResolved = true;
var all = container.GetRecognizers();
_preferredHandwritingRecognizer = SelectBestInkRecognizer(all);
if (logDetail)
{
if (_preferredHandwritingRecognizer != null)
LogHandwriting("识别器:已选用 \"" + _preferredHandwritingRecognizer.Name + "\"。");
else if (all != null && all.Count > 0)
LogHandwriting("识别器:未匹配到与 UI/区域语言对应的引擎,使用系统默认(共 " + all.Count + " 个)。");
}
}
if (_preferredHandwritingRecognizer != null)
container.SetDefaultRecognizer(_preferredHandwritingRecognizer);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("[手写体] 设置默认手写识别器失败: " + ex.Message, LogHelper.LogType.Warning);
}
}
private static WinRtInk.InkRecognizer SelectBestInkRecognizer(
IReadOnlyList<WinRtInk.InkRecognizer> list)
{
if (list == null || list.Count == 0)
return null;
var culture = PrimaryHandwritingCulture();
var lang = (culture?.TwoLetterISOLanguageName ?? string.Empty).ToLowerInvariant();
var name = culture?.Name ?? string.Empty;
bool wantZhHans = lang == "zh" &&
(name.IndexOf("hans", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.Equals("zh-cn", StringComparison.OrdinalIgnoreCase) ||
name.Equals("zh-sg", StringComparison.OrdinalIgnoreCase) ||
(name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) < 0 &&
!name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) &&
!name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) &&
!name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase)));
bool wantZhHant = lang == "zh" &&
(name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) ||
name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) ||
name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase));
WinRtInk.InkRecognizer Pick(Func<string, bool> match)
{
foreach (var r in list)
{
var n = r?.Name;
if (string.IsNullOrEmpty(n))
continue;
if (match(n))
return r;
}
return null;
}
if (wantZhHans)
{
var r = Pick(n =>
n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0 ||
(n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 &&
(n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0)) ||
(n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 &&
(n.IndexOf("Simplified", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("Hans", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("PRC", StringComparison.OrdinalIgnoreCase) >= 0)));
if (r != null)
return r;
r = Pick(n =>
n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0);
if (r != null)
return r;
}
else if (wantZhHant)
{
var r = Pick(n =>
n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0 ||
(n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 &&
(n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0)) ||
(n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 &&
(n.IndexOf("Traditional", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("Hant", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("Taiwan", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("Hong Kong", StringComparison.OrdinalIgnoreCase) >= 0)));
if (r != null)
return r;
r = Pick(n =>
n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0);
if (r != null)
return r;
}
else if (lang == "ja")
{
var r = Pick(n =>
n.IndexOf("Japanese", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("日本語", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("日语", StringComparison.OrdinalIgnoreCase) >= 0);
if (r != null)
return r;
}
else if (lang == "en")
{
var r = Pick(n => n.IndexOf("English", StringComparison.OrdinalIgnoreCase) >= 0);
if (r != null)
return r;
}
if (lang == "zh")
{
var r = Pick(n =>
n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 ||
n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0);
if (r != null)
return r;
}
return null;
}
private static CultureInfo PrimaryHandwritingCulture()
{
var ui = CultureInfo.CurrentUICulture;
var ct = CultureInfo.CurrentCulture;
if (string.Equals(ui.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase))
return ui;
if (string.Equals(ct.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase))
return ct;
return ui;
}
private static async Task<HandwritingRecognitionResult> RecognizeHandwritingWholeInkAsync(
StrokeCollection strokes,
bool traceRecognition)
{
if (strokes == null || strokes.Count == 0)
return HandwritingRecognitionResult.Empty;
var container = new WinRtInk.InkStrokeContainer();
foreach (Stroke s in strokes)
{
var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s);
if (ink != null)
container.AddStroke(ink);
}
var winStrokes = container.GetStrokes();
if (winStrokes == null || winStrokes.Count == 0)
{
if (traceRecognition)
LogHandwriting("整批回退:无有效 WinRT 笔画。");
return HandwritingRecognitionResult.Empty;
}
var reco = new WinRtInk.InkRecognizerContainer();
TryApplyPreferredHandwritingRecognizer(reco, false);
IReadOnlyList<WinRtInk.InkRecognitionResult> rr;
try
{
rr = await reco
.RecognizeAsync(container, WinRtInk.InkRecognitionTarget.All)
.AsTask()
.ConfigureAwait(true);
}
catch (Exception ex)
{
if (traceRecognition)
LogHandwriting("整批回退:RecognizeAsync 异常:" + ex.Message);
return HandwritingRecognitionResult.Empty;
}
if (rr == null || rr.Count == 0 || rr[0] == null)
{
if (traceRecognition)
LogHandwriting("整批回退:RecognizeAsync 无结果。");
return HandwritingRecognitionResult.Empty;
}
var cands = rr[0].GetTextCandidates();
var primary = (cands != null && cands.Count > 0) ? cands[0] : string.Empty;
if (string.IsNullOrWhiteSpace(primary))
{
if (traceRecognition)
LogHandwriting("整批回退:候选文本为空。");
return HandwritingRecognitionResult.Empty;
}
var merged = new List<string>();
if (cands != null)
{
foreach (var c in cands)
{
if (!string.IsNullOrEmpty(c) && !merged.Contains(c))
merged.Add(c);
}
}
var bounds = UnionStrokeBounds(strokes);
var group = new List<Stroke>();
foreach (Stroke s in strokes)
group.Add(s);
var seg = new HandwritingWordSegment(primary, merged, bounds, group);
return new HandwritingRecognitionResult(new List<HandwritingWordSegment> { seg });
}
private static Rect UnionStrokeBounds(StrokeCollection strokes)
{
if (strokes == null || strokes.Count == 0)
return Rect.Empty;
var r = strokes[0].GetBounds();
for (var i = 1; i < strokes.Count; i++)
r = Rect.Union(r, strokes[i].GetBounds());
return r;
}
private const string DefaultHandwritingFontFamilyList = "Ink Free,KaiTi,Segoe Script";
/// <summary>
/// 识别手写词后,将「有识别文本」的分词替换为指定手写风格字体的字形轮廓墨迹;未识别或空文本的词保留原笔画。
/// </summary>
public static async Task<StrokeCollection> ConvertRecognizedTextToHandwritingInkAsync(
StrokeCollection strokes,
string handwritingFontFamilyList)
{
if (!IsApiAvailable || strokes == null || strokes.Count == 0)
{
if (strokes != null && strokes.Count > 0 && !IsApiAvailable)
LogHandwriting("字形替换:跳过,IsApiAvailable=false。");
return strokes;
}
var fontList = string.IsNullOrWhiteSpace(handwritingFontFamilyList)
? DefaultHandwritingFontFamilyList
: handwritingFontFamilyList.Trim();
LogHandwriting(
"字形替换开始:输入笔画数=" + strokes.Count +
",字体链=\"" + fontList + "\"" +
"PixelsPerDip=" + Math.Round(GetPixelsPerDipSafe(), 3));
try
{
var reco = await RecognizeHandwritingAsync(strokes).ConfigureAwait(true);
if (!reco.IsSuccess || reco.Words == null || reco.Words.Count == 0)
{
LogHandwriting(
"字形替换中止:识别未成功(IsSuccess=" + reco.IsSuccess +
",词数=" + (reco.Words?.Count ?? 0) + "),原样返回笔画。");
return strokes;
}
var firstStrokeToSegment = new Dictionary<Stroke, HandwritingWordSegment>();
foreach (var w in reco.Words)
{
if (w?.Strokes == null || w.Strokes.Count == 0)
continue;
var ordered = w.Strokes.OrderBy(st => IndexOfStrokeInCollection(strokes, st)).ToList();
var first = ordered[0];
if (!firstStrokeToSegment.ContainsKey(first))
firstStrokeToSegment[first] = w;
}
if (firstStrokeToSegment.Count == 0)
{
LogHandwriting("字形替换中止:无法建立「首笔画→分词」映射,原样返回。");
return strokes;
}
var consumed = new HashSet<Stroke>();
var result = new StrokeCollection();
var pixelsPerDip = GetPixelsPerDipSafe();
var replacedWordCount = 0;
var keptOriginalWordCount = 0;
var glyphStrokeTotal = 0;
foreach (Stroke s in strokes)
{
if (consumed.Contains(s))
continue;
if (!firstStrokeToSegment.TryGetValue(s, out var seg))
{
result.Add(s);
continue;
}
if (string.IsNullOrWhiteSpace(seg.Text))
{
LogHandwriting(
" 分词:文本为空,保留原笔画,笔画数=" + seg.Strokes.Count);
keptOriginalWordCount++;
foreach (var z in seg.Strokes)
{
if (!consumed.Contains(z))
{
result.Add(z);
consumed.Add(z);
}
}
continue;
}
var templateDa = seg.Strokes[0]?.DrawingAttributes?.Clone() ?? new DrawingAttributes();
OutlineAttributesForGlyphInk(templateDa);
var glyphStrokes = CreateHandwritingGlyphStrokes(
seg.Text.Trim(),
seg.BoundingRectangle,
templateDa,
fontList,
pixelsPerDip);
if (glyphStrokes == null || glyphStrokes.Count == 0)
{
LogHandwriting(
" 分词:字形轮廓生成失败,保留原笔画。文本=\"" +
(seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) + "\"");
keptOriginalWordCount++;
foreach (var z in seg.Strokes)
{
if (!consumed.Contains(z))
{
result.Add(z);
consumed.Add(z);
}
}
continue;
}
foreach (var nk in glyphStrokes)
result.Add(nk);
glyphStrokeTotal += glyphStrokes.Count;
replacedWordCount++;
LogHandwriting(
" 分词:已替换为手写体字形墨迹,文本=\"" +
(seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) +
"\",生成笔画数=" + glyphStrokes.Count + ",移除原笔画数=" + seg.Strokes.Count);
foreach (var z in seg.Strokes)
consumed.Add(z);
}
LogHandwriting(
"字形替换结束:输出笔画数=" + result.Count +
"(输入=" + strokes.Count + "),替换词数=" + replacedWordCount +
",保留原迹词数=" + keptOriginalWordCount +
",字形子笔画合计=" + glyphStrokeTotal);
return result;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("WinRT 手写体字形替换失败: " + ex.Message, LogHelper.LogType.Warning);
LogHandwriting("字形替换异常:" + ex, LogHelper.LogType.Warning);
return strokes;
}
}
private static int IndexOfStrokeInCollection(StrokeCollection collection, Stroke stroke)
{
if (collection == null || stroke == null)
return int.MaxValue;
for (var i = 0; i < collection.Count; i++)
{
if (ReferenceEquals(collection[i], stroke))
return i;
}
return int.MaxValue;
}
private static void OutlineAttributesForGlyphInk(DrawingAttributes da)
{
if (da == null) return;
var w = Math.Max(0.8, Math.Min(da.Width, da.Height) * 0.2);
da.Width = w;
da.Height = w;
da.StylusTip = StylusTip.Ellipse;
da.IsHighlighter = false;
}
private static double GetPixelsPerDipSafe()
{
try
{
if (Application.Current?.MainWindow is Visual v)
return VisualTreeHelper.GetDpi(v).PixelsPerDip;
}
catch
{
// ignore
}
return 1.0;
}
private static Typeface ResolveHandwritingTypeface(string fontFamilyList)
{
try
{
var ff = new FontFamily(fontFamilyList ?? DefaultHandwritingFontFamilyList);
return new Typeface(ff, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
}
catch
{
return new Typeface(
SystemFonts.MessageFontFamily,
SystemFonts.MessageFontStyle,
SystemFonts.MessageFontWeight,
FontStretches.Normal);
}
}
private static List<Stroke> CreateHandwritingGlyphStrokes(
string text,
Rect placeRect,
DrawingAttributes templateDa,
string fontFamilyList,
double pixelsPerDip)
{
var list = new List<Stroke>();
if (string.IsNullOrEmpty(text) || placeRect.Width < 1 || placeRect.Height < 1)
return list;
var typeface = ResolveHandwritingTypeface(fontFamilyList);
var culture = CultureInfo.CurrentCulture;
var em = Math.Max(6.0, placeRect.Height * 0.72);
FormattedText ft = null;
for (var i = 0; i < 14; i++)
{
ft = new FormattedText(
text,
culture,
FlowDirection.LeftToRight,
typeface,
em,
Brushes.Black,
new NumberSubstitution(NumberCultureSource.Text, culture, NumberSubstitutionMethod.Context),
TextFormattingMode.Display,
pixelsPerDip);
if (ft.Width <= placeRect.Width * 0.96 && ft.Height <= placeRect.Height * 0.96)
break;
em *= 0.9;
if (em < 4.5)
break;
}
if (ft == null || ft.Width < 0.5 || ft.Height < 0.5)
return list;
var scale = Math.Min(
placeRect.Width * 0.94 / Math.Max(1e-6, ft.Width),
placeRect.Height * 0.94 / Math.Max(1e-6, ft.Height));
var tx = placeRect.Left + (placeRect.Width - ft.Width * scale) / 2.0;
var ty = placeRect.Top + (placeRect.Height - ft.Height * scale) / 2.0;
Geometry geom;
try
{
geom = ft.BuildGeometry(new Point(0, 0));
}
catch
{
return list;
}
if (geom == null || geom.IsEmpty())
return list;
var m = new Matrix(scale, 0, 0, scale, tx, ty);
geom.Transform = new MatrixTransform(m);
return StrokesFromOutlinedGeometry(geom, templateDa, 0.35);
}
private static List<Stroke> StrokesFromOutlinedGeometry(Geometry geometry, DrawingAttributes da, double tolerance)
{
var list = new List<Stroke>();
if (geometry == null || geometry.IsEmpty() || da == null)
return list;
Geometry outlined;
try
{
outlined = geometry.GetOutlinedPathGeometry(tolerance, ToleranceType.Absolute);
}
catch
{
return list;
}
if (outlined == null || outlined.IsEmpty())
return list;
Geometry flat;
try
{
flat = outlined.GetFlattenedPathGeometry(tolerance, ToleranceType.Absolute);
}
catch
{
return list;
}
if (!(flat is PathGeometry pg))
return list;
foreach (var fig in pg.Figures)
{
var pts = new StylusPointCollection();
pts.Add(new StylusPoint(fig.StartPoint.X, fig.StartPoint.Y, 0.5f));
foreach (var seg in fig.Segments)
{
switch (seg)
{
case LineSegment ls:
pts.Add(new StylusPoint(ls.Point.X, ls.Point.Y, 0.5f));
break;
case PolyLineSegment pls:
foreach (var p in pls.Points)
pts.Add(new StylusPoint(p.X, p.Y, 0.5f));
break;
}
}
if (pts.Count >= 2)
list.Add(new Stroke(pts) { DrawingAttributes = da.Clone() });
}
return list;
}
}
/// <summary>单个手写词片段的识别结果。</summary>
public sealed class HandwritingWordSegment
{
public HandwritingWordSegment(
string text,
IReadOnlyList<string> textCandidates,
Rect boundingRectangle,
IReadOnlyList<Stroke> strokes)
{
Text = text ?? string.Empty;
TextCandidates = textCandidates ?? Array.Empty<string>();
BoundingRectangle = boundingRectangle;
Strokes = strokes ?? Array.Empty<Stroke>();
}
public string Text { get; }
public IReadOnlyList<string> TextCandidates { get; }
public Rect BoundingRectangle { get; }
public IReadOnlyList<Stroke> Strokes { get; }
}
/// <summary>一次手写识别批次的汇总结果。</summary>
public sealed class HandwritingRecognitionResult
{
public static readonly HandwritingRecognitionResult Empty = new HandwritingRecognitionResult();
private HandwritingRecognitionResult()
{
Words = Array.Empty<HandwritingWordSegment>();
IsSuccess = false;
CombinedText = string.Empty;
}
public HandwritingRecognitionResult(IReadOnlyList<HandwritingWordSegment> words)
{
Words = words ?? Array.Empty<HandwritingWordSegment>();
IsSuccess = Words.Count > 0;
CombinedText = string.Join("", Words.Select(w => w.Text ?? string.Empty));
}
public bool IsSuccess { get; }
public IReadOnlyList<HandwritingWordSegment> Words { get; }
public string CombinedText { get; }
}
}
@@ -0,0 +1,405 @@
using OSVersionExtension;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using SysPoint = System.Windows.Point;
using WinRtInkAnalyzer = global::Windows.UI.Input.Inking.Analysis.InkAnalyzer;
namespace Ink_Canvas.Helpers
{
internal class ModernInkAnalyzer : IDisposable
{
public static readonly Guid ShapeStrokePropertyGuid = new Guid("11111111-2222-3333-4444-555555555555");
private global::Windows.UI.Input.Inking.Analysis.InkAnalyzer _internalAnalyzer;
private readonly Dictionary<Stroke, uint> _strokeIdMap = new Dictionary<Stroke, uint>();
private readonly Dictionary<uint, Stroke> _reverseIdMap = new Dictionary<uint, Stroke>();
private readonly object _syncLock = new object();
public ModernInkAnalyzer()
{
if (!WinRtInkShapeRecognizer.IsApiAvailable)
return;
_internalAnalyzer = new global::Windows.UI.Input.Inking.Analysis.InkAnalyzer();
}
private void AddStrokeInternal(Stroke stroke)
{
if (stroke.ContainsPropertyData(ShapeStrokePropertyGuid))
return;
var inkStroke = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(stroke);
if (inkStroke == null) return;
_internalAnalyzer.AddDataForStroke(inkStroke);
_internalAnalyzer.SetStrokeDataKind(
inkStroke.Id,
global::Windows.UI.Input.Inking.Analysis.InkAnalysisStrokeKind.Drawing);
_strokeIdMap[stroke] = inkStroke.Id;
_reverseIdMap[inkStroke.Id] = stroke;
}
private CancellationTokenSource _cts;
public async Task<InkShapeRecognitionResult> AnalyzeAsync(StrokeCollection strokes)
{
if (_internalAnalyzer == null || strokes == null || strokes.Count == 0)
return InkShapeRecognitionResult.Empty;
_cts?.Cancel();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
lock (_syncLock)
{
_internalAnalyzer.ClearDataForAllStrokes();
_strokeIdMap.Clear();
_reverseIdMap.Clear();
foreach (var stroke in strokes)
{
AddStrokeInternal(stroke);
}
}
if (_strokeIdMap.Count == 0)
return InkShapeRecognitionResult.Empty;
var result = await _internalAnalyzer.AnalyzeAsync().AsTask(token).ConfigureAwait(true);
if (token.IsCancellationRequested) return InkShapeRecognitionResult.Empty;
// Use the internal method from WinRtInkShapeRecognizer to find the primary drawing
var drawing = WinRtInkShapeRecognizer.FindPrimaryDrawing(_internalAnalyzer);
if (drawing == null)
return InkShapeRecognitionResult.Empty;
if (drawing.DrawingKind == global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Drawing)
return InkShapeRecognitionResult.Empty;
var name = WinRtInkShapeRecognizer.MapDrawingKindToShapeName(drawing.DrawingKind);
if (string.IsNullOrEmpty(name) || name == "Drawing")
return InkShapeRecognitionResult.Empty;
var winPts = WinRtInkShapeRecognizer.CopyWinRtPoints(drawing);
var hot = WinRtInkShapeRecognizer.ToWpfPointCollection(winPts);
var c = drawing.Center;
var centroid = new SysPoint(c.X, c.Y);
WinRtInkShapeRecognizer.BoundsFromPoints(winPts, out double w, out double h);
var toRemove = new StrokeCollection();
lock (_syncLock)
{
foreach (var id in drawing.GetStrokeIds())
{
if (_reverseIdMap.TryGetValue(id, out var stroke))
{
toRemove.Add(stroke);
}
}
}
if (toRemove.Count == 0)
return InkShapeRecognitionResult.Empty;
return new InkShapeRecognitionResult(name, centroid, hot, w, h, toRemove);
}
catch (Exception)
{
return InkShapeRecognitionResult.Empty;
}
}
public Task<StrokeCollection> AnalyzeAndCorrectAsync(
StrokeCollection strokes,
string handwritingFontFamilyList)
{
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(
strokes,
handwritingFontFamilyList);
}
public void Dispose()
{
_internalAnalyzer = null;
}
}
/// <summary>基于 Windows.UI.Input.Inking.Analysis 的形状识别(适用于 64 位进程等场景)。</summary>
internal static class WinRtInkShapeRecognizer
{
public static bool IsApiAvailable =>
OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
public static void Warmup()
{
if (!IsApiAvailable) return;
try
{
var d = Application.Current?.Dispatcher;
if (d == null) return;
d.BeginInvoke(new Action(async () =>
{
try
{
// 空 StrokeCollection 在 RecognizeShapeAsync 入口会直接返回,无法预热 WinRT InkAnalyzer。
await RecognizeShapeAsync(CreateMinimalWarmupStrokeCollection()).ConfigureAwait(true);
}
catch
{
// ignore
}
}));
}
catch
{
// ignore
}
}
/// <summary>由 <see cref="ModernInkProcessor"/> / <see cref="InkRecognitionManager"/> 在 UI 上 await(勿在收笔回调中同步阻塞)。</summary>
internal static async Task<InkShapeRecognitionResult> RecognizeShapeAsync(StrokeCollection strokes)
{
if (!IsApiAvailable || strokes == null || strokes.Count == 0)
return InkShapeRecognitionResult.Empty;
try
{
var analyzer = new WinRtInkAnalyzer();
var added = 0;
foreach (Stroke s in strokes)
{
var inkStroke = CreateInkStrokeFromWpf(s);
if (inkStroke == null)
continue;
analyzer.AddDataForStroke(inkStroke);
analyzer.SetStrokeDataKind(
inkStroke.Id,
global::Windows.UI.Input.Inking.Analysis.InkAnalysisStrokeKind.Drawing);
added++;
}
if (added == 0)
return InkShapeRecognitionResult.Empty;
await analyzer.AnalyzeAsync().AsTask().ConfigureAwait(true);
var drawing = FindPrimaryDrawing(analyzer);
if (drawing == null)
return InkShapeRecognitionResult.Empty;
if (drawing.DrawingKind == global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Drawing)
return InkShapeRecognitionResult.Empty;
var name = MapDrawingKindToShapeName(drawing.DrawingKind);
if (string.IsNullOrEmpty(name) || name == "Drawing")
return InkShapeRecognitionResult.Empty;
var winPts = CopyWinRtPoints(drawing);
var hot = ToWpfPointCollection(winPts);
var c = drawing.Center;
var centroid = new SysPoint(c.X, c.Y);
BoundsFromPoints(winPts, out double w, out double h);
var toRemove = new StrokeCollection();
foreach (Stroke s in strokes)
toRemove.Add(s);
return new InkShapeRecognitionResult(name, centroid, hot, w, h, toRemove);
}
catch (Exception)
{
return InkShapeRecognitionResult.Empty;
}
}
/// <summary>
/// 极短合成笔画,供 <see cref="Warmup"/> 等场景走完整 WinRT 转换与分析管线(空集合在入口处会被直接返回)。
/// </summary>
internal static StrokeCollection CreateMinimalWarmupStrokeCollection()
{
var da = new DrawingAttributes { Color = Colors.Black, Width = 2, Height = 2 };
var pts = new StylusPointCollection
{
new StylusPoint(8, 8),
new StylusPoint(14, 10),
new StylusPoint(20, 8),
};
var col = new StrokeCollection();
col.Add(new Stroke(pts, da));
return col;
}
/// <summary>供 WinRT 手写等模块复用:将 WPF <see cref="Stroke"/> 转为 WinRT <see cref="global::Windows.UI.Input.Inking.InkStroke"/>。</summary>
internal static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke)
{
if (stroke?.StylusPoints == null || stroke.StylusPoints.Count == 0)
return null;
var da = stroke.DrawingAttributes;
if (da == null)
return null;
var wda = new global::Windows.UI.Input.Inking.InkDrawingAttributes
{
PenTip = global::Windows.UI.Input.Inking.PenTipShape.Circle,
Color = global::Windows.UI.Color.FromArgb(da.Color.A, da.Color.R, da.Color.G, da.Color.B),
Size = new global::Windows.Foundation.Size((float)da.Width, (float)da.Height)
};
var builder = new global::Windows.UI.Input.Inking.InkStrokeBuilder();
builder.SetDefaultDrawingAttributes(wda);
var points = new List<global::Windows.Foundation.Point>(stroke.StylusPoints.Count);
foreach (StylusPoint sp in stroke.StylusPoints)
{
var pi = sp.ToPoint();
points.Add(new global::Windows.Foundation.Point((float)pi.X, (float)pi.Y));
}
if (points.Count == 0)
return null;
return builder.CreateStroke(points);
}
internal static global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing FindPrimaryDrawing(
global::Windows.UI.Input.Inking.Analysis.InkAnalyzer analyzer)
{
global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing best = null;
double bestArea = -1;
if (analyzer?.AnalysisRoot != null)
Visit(analyzer.AnalysisRoot);
return best;
void Visit(global::Windows.UI.Input.Inking.Analysis.IInkAnalysisNode node)
{
if (node == null) return;
if (node is global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing d &&
d.DrawingKind != global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Drawing)
{
double area = EstimateDrawingArea(d);
if (area > bestArea)
{
bestArea = area;
best = d;
}
}
// WinRT IInkAnalysisNode.Children 可能为 null,不可直接 foreach。
var children = node.Children;
if (children == null) return;
foreach (var child in children)
Visit(child);
}
}
private static double EstimateDrawingArea(global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing drawing)
{
var pts = CopyWinRtPoints(drawing);
BoundsFromPoints(pts, out double w, out double h);
return w * h;
}
internal static global::Windows.Foundation.Point[] CopyWinRtPoints(
global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing drawing)
{
var src = drawing?.Points;
if (src == null)
return Array.Empty<global::Windows.Foundation.Point>();
var n = src.Count;
if (n == 0)
return Array.Empty<global::Windows.Foundation.Point>();
var arr = new global::Windows.Foundation.Point[n];
for (var i = 0; i < n; i++)
arr[i] = src[i];
return arr;
}
internal static void BoundsFromPoints(
System.Collections.Generic.IReadOnlyList<global::Windows.Foundation.Point> points,
out double w,
out double h)
{
if (points == null || points.Count == 0)
{
w = h = 0;
return;
}
double minX = double.MaxValue, maxX = double.MinValue, minY = double.MaxValue, maxY = double.MinValue;
for (int i = 0; i < points.Count; i++)
{
var pt = points[i];
minX = Math.Min(minX, pt.X);
maxX = Math.Max(maxX, pt.X);
minY = Math.Min(minY, pt.Y);
maxY = Math.Max(maxY, pt.Y);
}
w = Math.Max(0, maxX - minX);
h = Math.Max(0, maxY - minY);
}
internal static PointCollection ToWpfPointCollection(
System.Collections.Generic.IReadOnlyList<global::Windows.Foundation.Point> points)
{
var hot = new PointCollection();
if (points == null) return hot;
for (int i = 0; i < points.Count; i++)
{
var pt = points[i];
hot.Add(new SysPoint(pt.X, pt.Y));
}
return hot;
}
internal static string MapDrawingKindToShapeName(
global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind kind)
{
switch (kind)
{
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Circle:
return "Circle";
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Ellipse:
return "Ellipse";
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Triangle:
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.IsoscelesTriangle:
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.EquilateralTriangle:
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.RightTriangle:
return "Triangle";
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Rectangle:
return "Rectangle";
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Square:
return "Square";
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Diamond:
return "Diamond";
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Trapezoid:
return "Trapezoid";
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Parallelogram:
return "Parallelogram";
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Quadrilateral:
return "Quadrilateral";
default:
return kind == global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Drawing
? "Drawing"
: kind.ToString();
}
}
}
}
@@ -1,4 +1,4 @@
using Hardcodet.Wpf.TaskbarNotification;
using H.NotifyIcon;
using Microsoft.Toolkit.Uwp.Notifications;
using System;
using System.Windows;
@@ -40,10 +40,9 @@ namespace Ink_Canvas.Helpers
taskbar.Visibility = Visibility.Visible;
taskbar.ShowBalloonTip(
taskbar.ShowNotification(
"InkCanvasForClass CE",
$"发现新版本!:{version}",
BalloonIcon.Info);
$"发现新版本!:{version}");
}
catch
{
+61 -95
View File
@@ -1,10 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RuntimeIdentifiers>win;win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<OutputType>WinExe</OutputType>
<RootNamespace>Ink_Canvas</RootNamespace>
<AssemblyName>InkCanvasForClass</AssemblyName>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
@@ -25,91 +27,79 @@
<BootstrapperEnabled>false</BootstrapperEnabled>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<UseWPF>true</UseWPF>
<Configurations>Debug;Release;x86 Debug</Configurations>
<UseWindowsForms>true</UseWindowsForms>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<Configurations>Debug;Release</Configurations>
<Platforms>AnyCPU;x86;x64</Platforms>
<LangVersion>10</LangVersion>
<NoWarn>$(NoWarn);CA1416;NU1701;MSB3270;CS8012;NETSDK1138</NoWarn>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>embedded</DebugType>
<OutputPath>bin\$(Configuration)\</OutputPath>
<Prefer32Bit>True</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='x86 Debug|AnyCPU'">
<DebugType>embedded</DebugType>
<OutputPath>bin\$(Configuration)\</OutputPath>
<Prefer32Bit>True</Prefer32Bit>
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>embedded</DebugType>
<OutputPath>bin\$(Configuration)\</OutputPath>
<Prefer32Bit>True</Prefer32Bit>
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Resources\icc.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
<DebugType>full</DebugType>
<LangVersion>7.3</LangVersion>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='x86 Debug|x86'">
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
<DebugType>full</DebugType>
<LangVersion>7.3</LangVersion>
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>embedded</DebugType>
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
<DebugType>pdbonly</DebugType>
<LangVersion>7.3</LangVersion>
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>embedded</DebugType>
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Title>InkCanvasForClass</Title>
<Version>5.0.4</Version>
<Authors>Dubi906w</Authors>
<Version>1.7</Version>
<Authors>CJK_mkp</Authors>
<Product>InkCanvasForClass</Product>
<Copyright>© Copyright HARKOTEK Studio 2024-now</Copyright>
<PackageProjectUrl>https://icc.bliemhax.com</PackageProjectUrl>
<Copyright>© Copyright CJK_mkp 2025-now</Copyright>
<PackageProjectUrl>https://inkcanvasforclass.github.io</PackageProjectUrl>
<FileVersion>bundled</FileVersion>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>full</DebugType>
<LangVersion>7.3</LangVersion>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='x86 Debug|ARM64'">
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
<DebugType>full</DebugType>
<LangVersion>7.3</LangVersion>
<Prefer32Bit>true</Prefer32Bit>
<PlatformTarget>ARM64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>pdbonly</DebugType>
<LangVersion>7.3</LangVersion>
<Prefer32Bit>true</Prefer32Bit>
<PlatformTarget>ARM64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
<DebugType>full</DebugType>
<LangVersion>7.3</LangVersion>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='x86 Debug|x64'">
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
<DebugType>full</DebugType>
<LangVersion>7.3</LangVersion>
<Prefer32Bit>true</Prefer32Bit>
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
<DebugType>pdbonly</DebugType>
<LangVersion>7.3</LangVersion>
<Prefer32Bit>true</Prefer32Bit>
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="IACore">
@@ -124,22 +114,6 @@
<HintPath>.\IAWinFX.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualBasic" />
<Reference Include="netstandard" />
<Reference Include="System.Windows.Forms" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Management" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml" />
<Reference Include="UIAutomationClient" />
<Reference Include="UIAutomationTypes" />
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="WindowsFormsIntegration" />
</ItemGroup>
<ItemGroup>
<None Include="app.manifest" />
@@ -147,9 +121,9 @@
<ItemGroup>
<PackageReference Include="Costura.Fody" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="2.0.1" />
<PackageReference Include="gong-wpf-dragdrop" Version="4.0.0" />
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.0.131" />
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.10.2.1" />
<PackageReference Include="iNKORE.UI.WPF" Version="1.2.8" />
<PackageReference Include="MdXaml" Version="1.27.0" />
@@ -157,10 +131,14 @@
<PackageReference Include="MicrosoftOfficeCore" Version="15.0.0" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Microsoft.International.Converters.PinYinConverter" Version="1.0.0" />
<PackageReference Include="Sentry" Version="6.1.0" />
<PackageReference Include="Sentry" Version="6.3.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualBasic" Version="10.3.0" />
<PackageReference Include="System.ComponentModel.Composition" Version="10.0.5" />
<PackageReference Include="System.Composition.AttributedModel" Version="10.0.5" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
<PackageReference Include="System.Management" Version="10.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="NHotkey.Wpf" Version="4.0.0" />
<PackageReference Include="OSVersionExt" Version="4.1.0" />
@@ -168,7 +146,13 @@
<PackageReference Include="AForge.Video.DirectShow" Version="2.2.5" />
<PackageReference Include="AForge.Imaging" Version="2.2.5" />
<PackageReference Include="AForge.Math" Version="2.2.5" />
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
<PackageReference Include="WebDav.Client" Version="2.9.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\InkCanvas.PluginSdk\InkCanvas.PluginSdk.csproj" />
<ProjectReference Include="..\InkCanvas.Controls\InkCanvas.Controls.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(MSBuildRuntimeType)' == 'Full'">
<COMReference Include="IWshRuntimeLibrary">
@@ -189,15 +173,6 @@
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
<COMReference Include="VBIDE">
<Guid>{0002E157-0000-0000-C000-000000000046}</Guid>
<VersionMajor>5</VersionMajor>
<VersionMinor>3</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>primary</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup Condition="'$(MSBuildRuntimeType)' != 'Full'">
<Reference Include="Interop.IWshRuntimeLibrary">
@@ -219,8 +194,6 @@
<EmbeddedResource Include="Resources\IACore\IACore.dll" />
<EmbeddedResource Include="Resources\IACore\IALoader.dll" />
<EmbeddedResource Include="Resources\IACore\IAWinFX.dll" />
<EmbeddedResource Include="UIAccessDLL_x64.dll" />
<EmbeddedResource Include="UIAccessDLL_x86.dll" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Cursors\Cursor.cur" />
@@ -585,14 +558,7 @@
<ItemGroup>
<Compile Remove="AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="Properties\Strings.en-US.resx" />
<EmbeddedResource Remove="**\Strings.en-US.resx" />
<None Include="Properties\Strings.en-US.resx" />
<EmbeddedResource Include="Properties\Strings.enUS.xml">
<LogicalName>Ink_Canvas.Properties.Strings.enUS.xml</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Remove="MainWindow.xaml~RF6c3144.TMP" />
<None Remove="Resources\Cursors\Cursor.cur" />
+1177 -8393
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+82 -16
View File
@@ -4,7 +4,6 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
@@ -64,7 +63,7 @@ namespace Ink_Canvas
/// 处理折叠浮动栏的鼠标点击事件。
/// </summary>
/// <param name="sender">事件发送者。</param>
/// <param name="e">鼠标按钮事件参数。</param>
/// <param name="e">路由事件参数。</param>
public async void FoldFloatingBar_MouseUp(object sender, MouseButtonEventArgs e)
{
await FoldFloatingBar(sender);
@@ -91,19 +90,6 @@ namespace Ink_Canvas
/// </remarks>
public async Task FoldFloatingBar(object sender, bool isAutoFoldCommand = false)
{
var isShouldRejectAction = false;
await Dispatcher.InvokeAsync(() =>
{
if (lastBorderMouseDownObject != null && lastBorderMouseDownObject is Panel)
((Panel)lastBorderMouseDownObject).Background = new SolidColorBrush(Colors.Transparent);
if (sender == Fold_Icon && lastBorderMouseDownObject != Fold_Icon) isShouldRejectAction = true;
});
if (isShouldRejectAction)
{
return;
}
// FloatingBarIcons_MouseUp_New(sender);
if (sender == null)
@@ -338,7 +324,7 @@ namespace Ink_Canvas
/// 处理展开浮动栏的鼠标点击事件。
/// </summary>
/// <param name="sender">事件发送者。</param>
/// <param name="e">鼠标按钮事件参数。</param>
/// <param name="e">路由事件参数。</param>
public async void UnFoldFloatingBar_MouseUp(object sender, MouseButtonEventArgs e)
{
await UnFoldFloatingBar(sender);
@@ -525,5 +511,85 @@ namespace Ink_Canvas
});
isFloatingBarChangingHideMode = false;
}
private bool IsFloatingBarUiAbsentFromScreens()
{
try
{
if (ViewboxFloatingBar == null) return true;
if (Settings?.Automation?.ThoroughlyHideWhenFolded == true &&
(!IsVisible || Visibility != Visibility.Visible))
return true;
if (ViewboxFloatingBar.Visibility != Visibility.Visible)
return true;
return IsOutsideOfScreenHelper.IsOutsideOfScreen(ViewboxFloatingBar);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动收纳校验:检测浮动栏可见性时出错: {ex.Message}", LogHelper.LogType.Warning);
return true;
}
}
private async Task WaitUntilFloatingBarHideModeIdleAsync(TimeSpan timeout)
{
var start = DateTime.UtcNow;
while (DateTime.UtcNow - start < timeout)
{
bool busy = await Dispatcher.InvokeAsync(() => isFloatingBarChangingHideMode);
if (!busy) return;
await Task.Delay(50).ConfigureAwait(false);
}
}
private async Task WaitForStartupFoldSettledAsync(TimeSpan timeout)
{
var start = DateTime.UtcNow;
while (DateTime.UtcNow - start < timeout)
{
var settled = await Dispatcher.InvokeAsync(() => isFloatingBarFolded && !isFloatingBarChangingHideMode);
if (settled) return;
await Task.Delay(50).ConfigureAwait(false);
}
}
private async Task VerifyStartupFoldAbsenceAfterDelayAsync()
{
try
{
if (!Settings.Startup.IsFoldAtStartup || App.StartWithBoardMode || App.StartWithShowMode)
return;
await WaitForStartupFoldSettledAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false);
await Task.Delay(3000).ConfigureAwait(false);
bool needRetry = await Dispatcher.InvokeAsync(() =>
{
if (!isFloatingBarFolded) return false;
if (currentMode != 0) return false;
return !IsFloatingBarUiAbsentFromScreens();
});
if (!needRetry) return;
LogHelper.WriteLogToFile("启动收纳校验:检测到浮动栏仍在屏幕上,将展开后等待 0.2s 再次收纳", LogHelper.LogType.Event);
await UnFoldFloatingBar(null);
await WaitUntilFloatingBarHideModeIdleAsync(TimeSpan.FromSeconds(15)).ConfigureAwait(false);
await Task.Delay(200).ConfigureAwait(false);
await FoldFloatingBar(new object()).ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动收纳校验失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ScheduleStartupFoldAbsenceVerification()
{
_ = VerifyStartupFoldAbsenceAfterDelayAsync();
}
}
}
-76
View File
@@ -1,76 +0,0 @@
using IWshRuntimeLibrary;
using System;
using System.Windows;
using Application = System.Windows.Forms.Application;
using File = System.IO.File;
namespace Ink_Canvas
{
public partial class MainWindow : Window
{
/// <summary>
/// 创建开机自启动快捷方式。
/// </summary>
/// <param name="exeName">可执行文件名,用于命名快捷方式。</param>
/// <returns>创建成功返回true,失败返回false。</returns>
/// <remarks>
/// 操作包括:
/// 1. 创建Windows Shell对象
/// 2. 在启动文件夹中创建快捷方式
/// 3. 设置快捷方式的目标路径为当前可执行文件路径
/// 4. 设置工作目录为当前目录
/// 5. 设置窗口样式为普通窗口
/// 6. 设置快捷方式描述
/// 7. 保存快捷方式
/// 8. 捕获可能的异常,确保方法不会因异常而崩溃
/// </remarks>
public static bool StartAutomaticallyCreate(string exeName)
{
try
{
var shell = new WshShell();
var shortcut = (IWshShortcut)shell.CreateShortcut(
Environment.GetFolderPath(Environment.SpecialFolder.Startup) + "\\" + exeName + ".lnk");
//设置快捷方式的目标所在的位置(源程序完整路径)
shortcut.TargetPath = Application.ExecutablePath;
//应用程序的工作目录
//当用户没有指定一个具体的目录时,快捷方式的目标应用程序将使用该属性所指定的目录来装载或保存文件。
shortcut.WorkingDirectory = Environment.CurrentDirectory;
//目标应用程序窗口类型(1.Normal window普通窗口,3.Maximized最大化窗口,7.Minimized最小化)
shortcut.WindowStyle = 1;
//快捷方式的描述
shortcut.Description = exeName + "_Ink";
//设置快捷键(如果有必要的话.)
//shortcut.Hotkey = "CTRL+ALT+D";
shortcut.Save();
return true;
}
catch (Exception) { }
return false;
}
/// <summary>
/// 删除开机自启动快捷方式。
/// </summary>
/// <param name="exeName">可执行文件名,用于定位要删除的快捷方式。</param>
/// <returns>删除成功返回true,失败返回false。</returns>
/// <remarks>
/// 操作包括:
/// 1. 在启动文件夹中删除指定名称的快捷方式
/// 2. 捕获可能的异常,确保方法不会因异常而崩溃
/// </remarks>
public static bool StartAutomaticallyDel(string exeName)
{
try
{
File.Delete(Environment.GetFolderPath(Environment.SpecialFolder.Startup) + "\\" + exeName +
".lnk");
return true;
}
catch (Exception) { }
return false;
}
}
}
+129 -345
View File
@@ -14,19 +14,18 @@ namespace Ink_Canvas
{
public partial class MainWindow : Window
{
/// <summary>
/// 浮动栏前景色,根据当前主题动态更新。
/// </summary>
private const string ThemeLight = "Light";
private const string ThemeDark = "Dark";
private const string LightThemePath = "Resources/Styles/Light.xaml";
private const string DarkThemePath = "Resources/Styles/Dark.xaml";
private const string DrawShapeImagePath = "Resources/DrawShapeImageDictionary.xaml";
private const string SeewoImagePath = "Resources/SeewoImageDictionary.xaml";
private const string IconImagePath = "Resources/IconImageDictionary.xaml";
private Color FloatBarForegroundColor;
/// <summary>
/// 应用并切换到指定的主题("Light" 或 "Dark"),更新主题资源并刷新相关 UI 元素以反映主题变化。
/// </summary>
/// <param name="theme">主题标识,支持 "Light" 或 "Dark"(区分大小写)。</param>
/// <param name="autoSwitchIcon">若为 true,则根据主题自动切换并保存浮动工具栏的图标设置。</param>
private void SetTheme(string theme, bool autoSwitchIcon = false)
{
// 清理现有的主题资源
var resourcesToRemove = new List<ResourceDictionary>();
foreach (var dict in Application.Current.Resources.MergedDictionaries)
{
@@ -43,195 +42,85 @@ namespace Ink_Canvas
Application.Current.Resources.MergedDictionaries.Remove(dict);
}
if (theme == "Light")
var isLightTheme = theme == ThemeLight;
var themePath = isLightTheme ? LightThemePath : DarkThemePath;
var elementTheme = isLightTheme ? ElementTheme.Light : ElementTheme.Dark;
var rd1 = new ResourceDictionary { Source = new Uri(themePath, UriKind.Relative) };
Application.Current.Resources.MergedDictionaries.Add(rd1);
_ = Task.Run(async () =>
{
// 先加载主题
var rd1 = new ResourceDictionary
await Task.Delay(100);
Dispatcher.Invoke(() =>
{
Source = new Uri("Resources/Styles/Light.xaml", UriKind.Relative)
};
Application.Current.Resources.MergedDictionaries.Add(rd1);
// 异步加载图形资源,避免阻塞启动
_ = Task.Run(async () =>
{
await Task.Delay(100);
Dispatcher.Invoke(() =>
{
var rd2 = new ResourceDictionary
{
Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative)
};
Application.Current.Resources.MergedDictionaries.Add(rd2);
var rd3 = new ResourceDictionary
{
Source = new Uri("Resources/SeewoImageDictionary.xaml", UriKind.Relative)
};
Application.Current.Resources.MergedDictionaries.Add(rd3);
var rd4 = new ResourceDictionary
{
Source = new Uri("Resources/IconImageDictionary.xaml", UriKind.Relative)
};
Application.Current.Resources.MergedDictionaries.Add(rd4);
});
LoadImageResourceDictionary(DrawShapeImagePath);
LoadImageResourceDictionary(SeewoImagePath);
LoadImageResourceDictionary(IconImagePath);
});
});
ThemeManager.SetRequestedTheme(window, ElementTheme.Light);
ThemeManager.SetRequestedTheme(window, elementTheme);
InitializeFloatBarForegroundColor();
InitializeFloatBarForegroundColor();
RefreshQuickPanelIcons();
RefreshStrokeSelectionIcons();
RefreshImageSelectionIcons();
RefreshGestureButtonIcon();
RefreshFloatingBarHighlightColors();
// 刷新快速面板图标
RefreshQuickPanelIcons();
// 刷新墨迹选中栏图标
RefreshStrokeSelectionIcons();
// 刷新图片选中栏图标
RefreshImageSelectionIcons();
// 刷新手势按钮图标
RefreshGestureButtonIcon();
RefreshFloatingBarHighlightColors();
if (autoSwitchIcon)
{
AutoSwitchFloatingBarIconForTheme("Light");
}
// 强制刷新UI
window.InvalidateVisual();
// 通知其他窗口刷新主题
RefreshOtherWindowsTheme();
}
else if (theme == "Dark")
if (autoSwitchIcon)
{
// 先加载主题
var rd1 = new ResourceDictionary { Source = new Uri("Resources/Styles/Dark.xaml", UriKind.Relative) };
Application.Current.Resources.MergedDictionaries.Add(rd1);
// 异步加载图形资源,避免阻塞启动
_ = Task.Run(async () =>
{
await Task.Delay(100);
Dispatcher.Invoke(() =>
{
var rd2 = new ResourceDictionary
{
Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative)
};
Application.Current.Resources.MergedDictionaries.Add(rd2);
var rd3 = new ResourceDictionary
{
Source = new Uri("Resources/SeewoImageDictionary.xaml", UriKind.Relative)
};
Application.Current.Resources.MergedDictionaries.Add(rd3);
var rd4 = new ResourceDictionary
{
Source = new Uri("Resources/IconImageDictionary.xaml", UriKind.Relative)
};
Application.Current.Resources.MergedDictionaries.Add(rd4);
});
});
ThemeManager.SetRequestedTheme(window, ElementTheme.Dark);
InitializeFloatBarForegroundColor();
// 刷新快速面板图标
RefreshQuickPanelIcons();
// 刷新墨迹选中栏图标
RefreshStrokeSelectionIcons();
// 刷新图片选中栏图标
RefreshImageSelectionIcons();
// 刷新手势按钮图标
RefreshGestureButtonIcon();
RefreshFloatingBarHighlightColors();
if (autoSwitchIcon)
{
AutoSwitchFloatingBarIconForTheme("Dark");
}
// 强制刷新UI
window.InvalidateVisual();
// 通知其他窗口刷新主题
RefreshOtherWindowsTheme();
AutoSwitchFloatingBarIconForTheme(theme);
}
window.InvalidateVisual();
RefreshOtherWindowsTheme();
}
void LoadImageResourceDictionary(string path)
{
var rd = new ResourceDictionary { Source = new Uri(path, UriKind.Relative) };
Application.Current.Resources.MergedDictionaries.Add(rd);
}
/// <summary>
/// 初始化FloatBarForegroundColor,从当前主题资源中加载颜色
/// </summary>
private void InitializeFloatBarForegroundColor()
{
try
{
FloatBarForegroundColor = (Color)Application.Current.FindResource("FloatBarForegroundColor");
// 强制刷新浮动工具栏按钮颜色
RefreshFloatingBarButtonColors();
}
catch (Exception)
{
// 如果无法从资源中加载,使用默认颜色
FloatBarForegroundColor = Color.FromRgb(0, 0, 0);
}
}
/// <summary>
/// 刷新快速面板图标
/// </summary>
private void RefreshQuickPanelIcons()
{
try
{
if (LeftUnFoldButtonQuickPanel != null)
{
LeftUnFoldButtonQuickPanel.InvalidateVisual();
}
if (RightUnFoldButtonQuickPanel != null)
{
RightUnFoldButtonQuickPanel.InvalidateVisual();
}
if (LeftSidePanel != null)
{
LeftSidePanel.InvalidateVisual();
}
if (RightSidePanel != null)
{
RightSidePanel.InvalidateVisual();
}
LeftUnFoldButtonQuickPanel?.InvalidateVisual();
RightUnFoldButtonQuickPanel?.InvalidateVisual();
LeftSidePanel?.InvalidateVisual();
RightSidePanel?.InvalidateVisual();
}
catch (Exception)
{
}
}
/// <summary>
/// 刷新浮动栏高光条颜色
/// </summary>
private void RefreshFloatingBarHighlightColors()
{
try
{
if (FloatingbarSelectionBG != null && FloatingbarSelectionBG.Visibility == Visibility.Visible)
{
// 根据主题设置高光颜色
bool isDarkTheme = IsCurrentThemeDark();
Color highlightBackgroundColor;
Color highlightBarColor;
bool isDarkTheme = Settings.Appearance.Theme == 1 ||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
if (isDarkTheme)
{
@@ -244,7 +133,6 @@ namespace Ink_Canvas
highlightBarColor = Color.FromRgb(37, 99, 235);
}
// 设置高光背景颜色
FloatingbarSelectionBG.Background = new SolidColorBrush(highlightBackgroundColor);
if (FloatingbarSelectionBG.Child is System.Windows.Controls.Canvas canvas && canvas.Children.Count > 0)
{
@@ -261,73 +149,67 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 刷新浮动工具栏按钮颜色
/// </summary>
private bool IsCurrentThemeDark()
{
return Settings.Appearance.Theme == 1 ||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
}
private void RefreshFloatingBarButtonColors()
{
try
{
// 根据主题选择高光颜色
Color selectedColor;
bool isDarkTheme = Settings.Appearance.Theme == 1 ||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
SymbolIconDelete.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.DeleteIcon);
ShapeDrawFloatingBarBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ShapesIcon);
SymbolIconUndo.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.UndoIcon);
SymbolIconRedo.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.RedoIcon);
CursorWithDelFloatingBarBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.CursorWithDelFloatingBarBtnIcon);
WhiteboardFloatingBarBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.WhiteboardFloatingBarBtnIcon);
ToolsFloatingBarBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ToolsFloatingBarBtnIcon);
Fold_Icon.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.FoldIcon);
if (isDarkTheme)
{
selectedColor = Color.FromRgb(102, 204, 255);
}
else
{
selectedColor = Color.FromRgb(30, 58, 138);
}
TimerToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.TimerIconGeometry);
RandomDrawToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.RandomDrawIconGeometry);
SingleDrawToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SingleDrawIconGeometry);
SaveToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SaveIconGeometry);
OpenToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.OpenIconGeometry);
ReplayToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ReplayIconGeometry);
ScreenshotToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ScreenshotIconGeometry);
ManualToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ManualIconGeometry);
SettingsToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SettingsIconGeometry);
BoardTimerToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.TimerIconGeometry);
BoardRandomDrawToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.RandomDrawIconGeometry);
BoardSingleDrawToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SingleDrawIconGeometry);
BoardSaveToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SaveIconGeometry);
BoardOpenToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.OpenIconGeometry);
BoardReplayToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ReplayIconGeometry);
BoardScreenshotToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ScreenshotIconGeometry);
BoardManualToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ManualIconGeometry);
BoardSettingsToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SettingsIconGeometry);
bool isDarkTheme = IsCurrentThemeDark();
Color selectedColor = isDarkTheme ? Color.FromRgb(102, 204, 255) : Color.FromRgb(30, 58, 138);
SetAllFloatingBarButtonsToColor(FloatBarForegroundColor);
// 根据当前模式设置按钮颜色
switch (_currentToolMode)
{
case "cursor":
CursorIconGeometry.Brush = new SolidColorBrush(selectedColor);
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
Cursor_Icon.Icon.Brush = new SolidColorBrush(selectedColor);
break;
case "pen":
case "color":
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
PenIconGeometry.Brush = new SolidColorBrush(selectedColor);
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
Pen_Icon.Icon.Brush = new SolidColorBrush(selectedColor);
break;
case "eraser":
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
CircleEraserIconGeometry.Brush = new SolidColorBrush(selectedColor);
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
Eraser_Icon.Icon.Brush = new SolidColorBrush(selectedColor);
break;
case "eraserByStrokes":
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
StrokeEraserIconGeometry.Brush = new SolidColorBrush(selectedColor);
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
EraserByStrokes_Icon.Icon.Brush = new SolidColorBrush(selectedColor);
break;
case "select":
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
LassoSelectIconGeometry.Brush = new SolidColorBrush(selectedColor);
break;
default:
// 默认情况,所有按钮都使用主题颜色
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
SymbolIconSelect.Icon.Brush = new SolidColorBrush(selectedColor);
break;
}
}
@@ -336,79 +218,60 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 处理系统主题偏好变化事件,根据当前设置更新应用主题。
/// </summary>
/// <param name="sender">事件发送者。</param>
/// <param name="e">用户偏好变化事件参数。</param>
/// <remarks>
/// 操作包括:
/// 1. 根据当前主题设置(Settings.Appearance.Theme)决定使用哪种主题
/// 2. 如果设置为0(浅色主题),则设置为Light主题
/// 3. 如果设置为1(深色主题),则设置为Dark主题
/// 4. 如果设置为2(跟随系统主题),则根据系统主题设置应用相应的主题
/// </remarks>
void SetAllFloatingBarButtonsToColor(Color color)
{
var brush = new SolidColorBrush(color);
Cursor_Icon.Icon.Brush = brush;
Pen_Icon.Icon.Brush = brush;
EraserByStrokes_Icon.Icon.Brush = brush;
Eraser_Icon.Icon.Brush = brush;
SymbolIconSelect.Icon.Brush = brush;
ShapeDrawFloatingBarBtn.Icon.Brush = brush;
SymbolIconUndo.Icon.Brush = brush;
SymbolIconRedo.Icon.Brush = brush;
CursorWithDelFloatingBarBtn.Icon.Brush = brush;
WhiteboardFloatingBarBtn.Icon.Brush = brush;
ToolsFloatingBarBtn.Icon.Brush = brush;
Fold_Icon.Icon.Brush = brush;
}
private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
switch (Settings.Appearance.Theme)
{
case 0:
SetTheme("Light");
SetTheme(ThemeLight);
break;
case 1:
SetTheme("Dark");
SetTheme(ThemeDark);
break;
case 2:
if (IsSystemThemeLight()) SetTheme("Light");
else SetTheme("Dark");
SetTheme(IsSystemThemeLight() ? ThemeLight : ThemeDark);
break;
}
}
/// <summary>
/// 检查系统主题是否为浅色主题。
/// </summary>
/// <returns>系统主题为浅色返回true,深色返回false。</returns>
/// <remarks>
/// 操作包括:
/// 1. 从注册表中读取系统主题设置
/// 2. 检查"SystemUsesLightTheme"键的值
/// 3. 如果值为1,则表示系统使用浅色主题
/// 4. 捕获可能的异常,确保方法不会因异常而崩溃
/// </remarks>
private bool IsSystemThemeLight()
{
var light = false;
try
{
var registryKey = Registry.CurrentUser;
var themeKey =
registryKey.OpenSubKey("software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
var keyValue = 0;
if (themeKey != null) keyValue = (int)themeKey.GetValue("SystemUsesLightTheme");
if (keyValue == 1) light = true;
var themeKey = registryKey.OpenSubKey("software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
if (themeKey != null)
{
int keyValue = (int)themeKey.GetValue("SystemUsesLightTheme");
return keyValue == 1;
}
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
return light;
return false;
}
/// <summary>
/// 根据主题自动切换浮动栏图标
/// </summary>
private void AutoSwitchFloatingBarIconForTheme(string theme)
{
try
{
if (theme == "Light")
{
Settings.Appearance.FloatingBarImg = 0;
}
else if (theme == "Dark")
{
Settings.Appearance.FloatingBarImg = 3;
}
Settings.Appearance.FloatingBarImg = theme == ThemeLight ? 0 : 3;
UpdateFloatingBarIcon();
UpdateFloatingBarIconComboBox();
}
@@ -417,111 +280,49 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 更新设置界面中的浮动栏图标选择下拉框显示
/// </summary>
private void UpdateFloatingBarIconComboBox()
{
try
{
if (ComboBoxFloatingBarImg != null)
{
ComboBoxFloatingBarImg.SelectedIndex = Settings.Appearance.FloatingBarImg;
}
}
catch (Exception)
{
}
}
/// <summary>
/// 刷新墨迹选中栏图标
/// </summary>
private void RefreshStrokeSelectionIcons()
{
try
{
if (BorderStrokeSelectionControl != null)
{
// 强制刷新墨迹选中栏的视觉状态
BorderStrokeSelectionControl.InvalidateVisual();
// 刷新墨迹选中栏内的所有图标
var viewbox = BorderStrokeSelectionControl.Child as Viewbox;
if (viewbox?.Child is ui.SimpleStackPanel stackPanel)
{
RefreshStrokeSelectionIconsRecursive(stackPanel);
RefreshIconsRecursive(stackPanel);
}
}
}
catch (Exception)
{
// 忽略异常,确保主题切换不会因为图标刷新失败而中断
}
}
/// <summary>
/// 递归刷新墨迹选中栏内的图标
/// </summary>
private void RefreshStrokeSelectionIconsRecursive(System.Windows.Controls.Panel panel)
{
try
{
foreach (var child in panel.Children)
{
if (child is Image image)
{
// 强制刷新图像
image.InvalidateVisual();
}
else if (child is System.Windows.Controls.Panel childPanel)
{
// 递归处理子面板
RefreshStrokeSelectionIconsRecursive(childPanel);
}
else if (child is Border border && border.Child is System.Windows.Controls.Panel borderPanel)
{
// 处理Border内的面板
RefreshStrokeSelectionIconsRecursive(borderPanel);
}
}
}
catch (Exception)
{
// 忽略异常
}
}
/// <summary>
/// 刷新图片选中栏图标
/// </summary>
private void RefreshImageSelectionIcons()
{
try
{
if (BorderImageSelectionControl != null)
{
// 强制刷新图片选中栏的视觉状态
BorderImageSelectionControl.InvalidateVisual();
// 刷新图片选中栏内的所有图标
var viewbox = BorderImageSelectionControl.Child as Viewbox;
if (viewbox?.Child is ui.SimpleStackPanel stackPanel)
{
RefreshImageSelectionIconsRecursive(stackPanel);
RefreshIconsRecursive(stackPanel);
}
}
}
catch (Exception)
{
// 忽略异常,确保主题切换不会因为图标刷新失败而中断
}
}
/// <summary>
/// 递归刷新图片选中栏内的图标
/// </summary>
private void RefreshImageSelectionIconsRecursive(System.Windows.Controls.Panel panel)
private void RefreshIconsRecursive(System.Windows.Controls.Panel panel)
{
try
{
@@ -529,22 +330,18 @@ namespace Ink_Canvas
{
if (child is Image image)
{
// 强制刷新图像
image.InvalidateVisual();
}
else if (child is System.Windows.Controls.Panel childPanel)
{
// 递归处理子面板
RefreshImageSelectionIconsRecursive(childPanel);
RefreshIconsRecursive(childPanel);
}
else if (child is Border border && border.Child is System.Windows.Controls.Panel borderPanel)
{
// 处理Border内的面板
RefreshImageSelectionIconsRecursive(borderPanel);
RefreshIconsRecursive(borderPanel);
}
else if (child is Grid grid)
{
// 处理Grid内的子元素
foreach (var gridChild in grid.Children)
{
if (gridChild is Image gridImage)
@@ -557,18 +354,13 @@ namespace Ink_Canvas
}
catch (Exception)
{
// 忽略异常
}
}
/// <summary>
/// 刷新手势按钮图标
/// </summary>
private void RefreshGestureButtonIcon()
{
try
{
// 调用手势按钮颜色和图标更新方法,该方法会根据当前主题和手势状态设置正确的图标
CheckEnableTwoFingerGestureBtnColorPrompt();
}
catch (Exception)
@@ -576,14 +368,10 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 刷新其他窗口的主题
/// </summary>
private void RefreshOtherWindowsTheme()
{
try
{
// 刷新所有打开的窗口
foreach (Window window in Application.Current.Windows)
{
if (window is CountdownTimerWindow timerWindow)
@@ -598,22 +386,18 @@ namespace Ink_Canvas
{
operatingGuideWindow.RefreshTheme();
}
else if (window is Windows.SettingsViews.SettingsWindow settingsWindow)
{
settingsWindow.RefreshTheme();
}
}
// 刷新计时器控件
if (TimerControl != null)
{
TimerControl.RefreshTheme();
}
if (MinimizedTimerControl != null)
{
MinimizedTimerControl.RefreshTheme();
}
TimerControl?.RefreshTheme();
MinimizedTimerControl?.RefreshTheme();
}
catch (Exception)
{
}
}
}
}
}
+47 -19
View File
@@ -1,3 +1,4 @@
using Ink_Canvas.Controls;
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
@@ -5,6 +6,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
@@ -81,7 +83,7 @@ namespace Ink_Canvas
var missingElements = 0;
foreach (UIElement child in inkCanvas.Children)
{
if (child is Image || child is MediaElement)
if (child is Image || child is MediaElement || child is PdfEmbeddedView)
{
if (child is Image img && img.Tag is string tag && tag == VideoPresenterLiveFrameTag)
{
@@ -225,6 +227,7 @@ namespace Ink_Canvas
if (TimeMachineHistories[targetIndex] == null)
{
timeMachine.ClearStrokeHistory();
SyncPdfPageSidebarWithCanvas();
return;
}
@@ -281,7 +284,11 @@ namespace Ink_Canvas
/// </summary>
private void ProcessElementsAfterRestore(List<UIElement> elements)
{
if (elements == null || elements.Count == 0) return;
if (elements == null || elements.Count == 0)
{
SyncPdfPageSidebarWithCanvas();
return;
}
// 使用低优先级异步处理,让 UI 先响应,图片位置和事件绑定稍后完成
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
@@ -310,7 +317,19 @@ namespace Ink_Canvas
}
BindElementEvents(media);
}
else if (element is PdfEmbeddedView pdf)
{
double left = InkCanvas.GetLeft(pdf);
double top = InkCanvas.GetTop(pdf);
if (double.IsNaN(left) || double.IsNaN(top))
{
CenterAndScaleElement(pdf);
}
BindElementEvents(pdf);
}
}
SyncPdfPageSidebarWithCanvas();
}));
}
@@ -408,7 +427,10 @@ namespace Ink_Canvas
/// <remarks>
/// 该方法在切换前会取消当前选中元素(同时保留并恢复编辑模式)、调用视频呈现器的离开页前钩子、保存当前页的笔迹与元素、清空画布;切换到前一页后恢复该页内容、调用视频呈现器的页已更改钩子并刷新页面索引显示。
/// </remarks>
private void BtnWhiteBoardSwitchPrevious_Click(object sender, EventArgs e)
private void BoardBtnWhiteBoardSwitchPrevious_MouseUp(object sender, MouseButtonEventArgs e)
=> BtnWhiteBoardSwitchPrevious_Click(sender, e);
private void BtnWhiteBoardSwitchPrevious_Click(object sender, RoutedEventArgs e)
{
if (CurrentWhiteboardIndex <= 1) return;
@@ -440,7 +462,10 @@ namespace Ink_Canvas
/// </summary>
/// <param name="sender">触发事件的源对象(通常为按钮)。</param>
/// <param name="e">事件参数。</param>
private void BtnWhiteBoardSwitchNext_Click(object sender, EventArgs e)
private void BoardBtnWhiteBoardSwitchNext_MouseUp(object sender, MouseButtonEventArgs e)
=> BtnWhiteBoardSwitchNext_Click(sender, e);
private void BtnWhiteBoardSwitchNext_Click(object sender, RoutedEventArgs e)
{
if (CurrentWhiteboardIndex < WhiteboardTotalCount &&
Settings.Automation.IsAutoSaveStrokesAtClear &&
@@ -487,7 +512,10 @@ namespace Ink_Canvas
/// - 将当前页面的历史保存到时间轴并清空画布,然后在白板集合中插入一个空白页面(其历史为 null),随后恢复该页面并触发页面变更回调。
/// - 更新页码显示并在达到上限时禁用添加按钮;若侧边页列表可见,则刷新该列表。
/// </remarks>
private void BtnWhiteBoardAdd_Click(object sender, EventArgs e)
private void BoardBtnWhiteBoardAdd_MouseUp(object sender, MouseButtonEventArgs e)
=> BtnWhiteBoardAdd_Click(sender, e);
private void BtnWhiteBoardAdd_Click(object sender, RoutedEventArgs e)
{
if (WhiteboardTotalCount >= 99) return;
if (Settings.Automation.IsAutoSaveStrokesAtClear &&
@@ -634,8 +662,8 @@ namespace Ink_Canvas
bool isMaxPage = WhiteboardTotalCount >= 99;
// 设置按钮文本
BtnLeftWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
BtnRightWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
BtnLeftWhiteBoardSwitchNext.LabelTextBlockControl.Text = isLastPage ? "新页面" : "下一页";
BtnRightWhiteBoardSwitchNext.LabelTextBlockControl.Text = isLastPage ? "新页面" : "下一页";
if (isLastPage)
{
@@ -652,11 +680,11 @@ namespace Ink_Canvas
// 设置下一页按钮颜色
if (iconForegroundBrush != null)
{
BtnLeftWhiteBoardSwitchNextGeometry.Brush = iconForegroundBrush;
BtnRightWhiteBoardSwitchNextGeometry.Brush = iconForegroundBrush;
BtnLeftWhiteBoardSwitchNext.IconGeometryDrawing.Brush = iconForegroundBrush;
BtnRightWhiteBoardSwitchNext.IconGeometryDrawing.Brush = iconForegroundBrush;
}
BtnLeftWhiteBoardSwitchNextLabel.Opacity = 1;
BtnRightWhiteBoardSwitchNextLabel.Opacity = 1;
BtnLeftWhiteBoardSwitchNext.LabelTextBlockControl.Opacity = 1;
BtnRightWhiteBoardSwitchNext.LabelTextBlockControl.Opacity = 1;
BtnWhiteBoardSwitchPrevious.IsEnabled = true;
@@ -666,21 +694,21 @@ namespace Ink_Canvas
if (iconForegroundBrush != null)
{
var disabledBrush = new SolidColorBrush(Color.FromArgb(127, iconForegroundBrush.Color.R, iconForegroundBrush.Color.G, iconForegroundBrush.Color.B));
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = disabledBrush;
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = disabledBrush;
BtnLeftWhiteBoardSwitchPrevious.IconGeometryDrawing.Brush = disabledBrush;
BtnRightWhiteBoardSwitchPrevious.IconGeometryDrawing.Brush = disabledBrush;
}
BtnLeftWhiteBoardSwitchPreviousLabel.Opacity = 0.5;
BtnRightWhiteBoardSwitchPreviousLabel.Opacity = 0.5;
BtnLeftWhiteBoardSwitchPrevious.LabelTextBlockControl.Opacity = 0.5;
BtnRightWhiteBoardSwitchPrevious.LabelTextBlockControl.Opacity = 0.5;
}
else
{
if (iconForegroundBrush != null)
{
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = iconForegroundBrush;
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = iconForegroundBrush;
BtnLeftWhiteBoardSwitchPrevious.IconGeometryDrawing.Brush = iconForegroundBrush;
BtnRightWhiteBoardSwitchPrevious.IconGeometryDrawing.Brush = iconForegroundBrush;
}
BtnLeftWhiteBoardSwitchPreviousLabel.Opacity = 1;
BtnRightWhiteBoardSwitchPreviousLabel.Opacity = 1;
BtnLeftWhiteBoardSwitchPrevious.LabelTextBlockControl.Opacity = 1;
BtnRightWhiteBoardSwitchPrevious.LabelTextBlockControl.Opacity = 1;
}
BtnWhiteBoardDelete.IsEnabled = WhiteboardTotalCount != 1;
+127 -642
View File
@@ -3,10 +3,8 @@ using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Ink_Canvas
{
@@ -29,594 +27,159 @@ namespace Ink_Canvas
{
if (!isLoaded) return;
// 创建背景选项面板(如果不存在)
if (BackgroundPalette == null)
if (BackgroundPalette.Visibility == Visibility.Visible)
{
CreateBackgroundPalette();
}
// 显示或隐藏背景选项面板
if (BackgroundPalette != null)
{
if (BackgroundPalette.Visibility == Visibility.Visible)
{
// 如果面板已经显示,则隐藏它
AnimationsHelper.HideWithSlideAndFade(BackgroundPalette);
}
else
{
// 隐藏其他可能显示的面板
AnimationsHelper.HideWithSlideAndFade(EraserSizePanel);
AnimationsHelper.HideWithSlideAndFade(BorderTools);
AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
AnimationsHelper.HideWithSlideAndFade(PenPalette);
AnimationsHelper.HideWithSlideAndFade(BoardPenPalette);
AnimationsHelper.HideWithSlideAndFade(BorderDrawShape);
AnimationsHelper.HideWithSlideAndFade(BoardBorderDrawShape);
AnimationsHelper.HideWithSlideAndFade(BoardEraserSizePanel);
AnimationsHelper.HideWithSlideAndFade(TwoFingerGestureBorder);
AnimationsHelper.HideWithSlideAndFade(BoardTwoFingerGestureBorder);
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
// 显示背景选项面板
AnimationsHelper.ShowWithSlideFromBottomAndFade(BackgroundPalette);
}
return;
}
// 原有的背景切换代码
Settings.Canvas.UsingWhiteboard = !Settings.Canvas.UsingWhiteboard;
SaveSettingsToFile();
if (Settings.Canvas.UsingWhiteboard)
{
if (inkColor == 5) lastBoardInkColor = 0;
ICCWaterMarkDark.Visibility = Visibility.Visible;
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
// 设置为白板默认背景色
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
if (currentMode == 1) // 白板模式
{
// 设置背景为默认白板背景色
GridBackgroundCover.Background = new SolidColorBrush(defaultWhiteboardColor);
// 更新RGB滑块的值为默认白板背景色
if (BackgroundPalette != null && BackgroundPalette.Visibility == Visibility.Visible)
{
UpdateRGBSliders(defaultWhiteboardColor);
}
// 更新自定义背景色为默认白板背景色
CustomBackgroundColor = defaultWhiteboardColor;
// 保存到设置
string colorHex = $"#{defaultWhiteboardColor.R:X2}{defaultWhiteboardColor.G:X2}{defaultWhiteboardColor.B:X2}";
Settings.Canvas.CustomBackgroundColor = colorHex;
SaveSettingsToFile();
}
// 设置墨迹颜色为黑色
CheckLastColor(0);
forceEraser = false;
AnimationsHelper.HideWithSlideAndFade(BackgroundPalette);
}
else
{
if (inkColor == 0) lastBoardInkColor = 5;
ICCWaterMarkWhite.Visibility = Visibility.Visible;
ICCWaterMarkDark.Visibility = Visibility.Collapsed;
AnimationsHelper.HideWithSlideAndFade(EraserSizePanel);
AnimationsHelper.HideWithSlideAndFade(BorderTools);
AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
AnimationsHelper.HideWithSlideAndFade(PenPalette);
AnimationsHelper.HideWithSlideAndFade(BoardPenPalette);
AnimationsHelper.HideWithSlideAndFade(BorderDrawShape);
AnimationsHelper.HideWithSlideAndFade(BoardBorderDrawShape);
AnimationsHelper.HideWithSlideAndFade(BoardEraserSizePanel);
AnimationsHelper.HideWithSlideAndFade(TwoFingerGestureBorder);
AnimationsHelper.HideWithSlideAndFade(BoardTwoFingerGestureBorder);
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
// 设置为黑板默认背景色
Color defaultBlackboardColor = Color.FromRgb(22, 41, 36);
if (currentMode == 1) // 黑板模式
{
// 设置背景为默认黑板背景色
GridBackgroundCover.Background = new SolidColorBrush(defaultBlackboardColor);
// 更新RGB滑块的值为默认黑板背景色
if (BackgroundPalette != null && BackgroundPalette.Visibility == Visibility.Visible)
{
UpdateRGBSliders(defaultBlackboardColor);
}
// 更新自定义背景色为默认黑板背景色
CustomBackgroundColor = defaultBlackboardColor;
// 保存到设置
string colorHex = $"#{defaultBlackboardColor.R:X2}{defaultBlackboardColor.G:X2}{defaultBlackboardColor.B:X2}";
Settings.Canvas.CustomBackgroundColor = colorHex;
SaveSettingsToFile();
}
// 设置墨迹颜色为白色
CheckLastColor(5);
forceEraser = false;
LoadCustomBackgroundColor();
UpdateBackgroundButtonsState();
AnimationsHelper.ShowWithSlideFromBottomAndFade(BackgroundPalette);
}
CheckColorTheme(true);
}
/// <summary>
/// 创建背景颜色选项面板
/// </summary>
/// <remarks>
/// - 加载自定义背景色
/// - 创建背景选项面板UI
/// - 添加标题栏和关闭按钮
/// - 添加白板/黑板模式选择按钮
/// - 添加RGB颜色选择器
/// - 添加颜色预览和应用按钮
/// - 将面板添加到主网格
/// </remarks>
private void CreateBackgroundPalette()
private void WhiteboardModeBtn_MouseUp(object sender, MouseButtonEventArgs e)
{
// 确保加载自定义背景色
LoadCustomBackgroundColor();
Settings.Canvas.UsingWhiteboard = true;
SaveSettingsToFile();
ICCWaterMarkDark.Visibility = Visibility.Visible;
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
// 创建一个类似于PenPalette的面板
BackgroundPalette = new Border
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
if (currentMode == 1)
{
Name = "BackgroundPalette",
Visibility = Visibility.Collapsed,
Background = (SolidColorBrush)Application.Current.FindResource("SettingsPageBackground"),
Opacity = 1,
BorderBrush = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Width = 300,
MaxHeight = 400
};
// 确保面板显示在顶层
Panel.SetZIndex(BackgroundPalette, 1000);
// 创建面板内容
var stackPanel = new StackPanel();
// 创建标题栏
var titleBorder = new Border
{
BorderBrush = new SolidColorBrush(Color.FromRgb(0x1e, 0x3a, 0x8a)),
Height = 32,
BorderThickness = new Thickness(0, 0, 0, 1),
CornerRadius = new CornerRadius(8, 8, 0, 0),
Background = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)),
Margin = new Thickness(-1, -1, -1, 0),
Padding = new Thickness(1, 1, 1, 0)
};
var titleCanvas = new System.Windows.Controls.Canvas { Height = 24, ClipToBounds = true };
var titleText = new TextBlock
{
Text = "背景设置",
Foreground = (SolidColorBrush)Application.Current.FindResource("FloatBarForeground"),
Padding = new Thickness(0, 5, 0, 0),
FontSize = 11,
FontWeight = FontWeights.Bold,
TextAlignment = TextAlignment.Center
};
System.Windows.Controls.Canvas.SetLeft(titleText, 8);
titleCanvas.Children.Add(titleText);
// 关闭按钮
var closeImage = new Image
{
Source = new BitmapImage(new Uri("/Resources/new-icons/close-white.png", UriKind.Relative)),
Height = 16,
Width = 16
};
RenderOptions.SetBitmapScalingMode(closeImage, BitmapScalingMode.HighQuality);
closeImage.MouseUp += CloseBordertools_MouseUp;
System.Windows.Controls.Canvas.SetRight(closeImage, 8);
System.Windows.Controls.Canvas.SetTop(closeImage, 4);
titleCanvas.Children.Add(closeImage);
titleBorder.Child = titleCanvas;
stackPanel.Children.Add(titleBorder);
// 创建背景选项内容区域
var contentPanel = new StackPanel { Margin = new Thickness(8) };
// 黑板/白板选择
var modeTitle = new TextBlock
{
Text = "白板模式",
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground"),
FontSize = 10,
FontWeight = FontWeights.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 4, 0, 8)
};
contentPanel.Children.Add(modeTitle);
var modePanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
// 白板按钮
var whiteboardButton = new Border
{
Width = 60,
Height = 30,
Background = Settings.Canvas.UsingWhiteboard ? new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) : new SolidColorBrush(Colors.LightGray),
CornerRadius = new CornerRadius(4),
Margin = new Thickness(0, 0, 8, 0)
};
var whiteboardText = new TextBlock
{
Text = "白板",
Foreground = Settings.Canvas.UsingWhiteboard ? new SolidColorBrush(Colors.White) : new SolidColorBrush(Colors.Black),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
whiteboardButton.Child = whiteboardText;
whiteboardButton.MouseUp += (s, args) =>
{
Settings.Canvas.UsingWhiteboard = true;
GridBackgroundCover.Background = new SolidColorBrush(defaultWhiteboardColor);
UpdateRGBSliders(defaultWhiteboardColor);
CustomBackgroundColor = defaultWhiteboardColor;
string colorHex = $"#{defaultWhiteboardColor.R:X2}{defaultWhiteboardColor.G:X2}{defaultWhiteboardColor.B:X2}";
Settings.Canvas.CustomBackgroundColor = colorHex;
SaveSettingsToFile();
ICCWaterMarkDark.Visibility = Visibility.Visible;
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
// 设置为白板默认背景色
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
if (currentMode == 1) // 白板模式
{
// 设置背景为默认白板背景色
GridBackgroundCover.Background = new SolidColorBrush(defaultWhiteboardColor);
// 更新RGB滑块的值为默认白板背景色
UpdateRGBSliders(defaultWhiteboardColor);
// 更新自定义背景色为默认白板背景色
CustomBackgroundColor = defaultWhiteboardColor;
// 保存到设置
string colorHex = $"#{defaultWhiteboardColor.R:X2}{defaultWhiteboardColor.G:X2}{defaultWhiteboardColor.B:X2}";
Settings.Canvas.CustomBackgroundColor = colorHex;
SaveSettingsToFile();
}
// 设置墨迹颜色为黑色
CheckLastColor(0);
forceEraser = false;
CheckColorTheme(true);
UpdateBackgroundButtonsState();
};
modePanel.Children.Add(whiteboardButton);
// 黑板按钮
var blackboardButton = new Border
{
Width = 60,
Height = 30,
Background = !Settings.Canvas.UsingWhiteboard ? new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) : new SolidColorBrush(Colors.LightGray),
CornerRadius = new CornerRadius(4)
};
var blackboardText = new TextBlock
{
Text = "黑板",
Foreground = !Settings.Canvas.UsingWhiteboard ? new SolidColorBrush(Colors.White) : new SolidColorBrush(Colors.Black),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
blackboardButton.Child = blackboardText;
blackboardButton.MouseUp += (s, args) =>
{
Settings.Canvas.UsingWhiteboard = false;
SaveSettingsToFile();
ICCWaterMarkWhite.Visibility = Visibility.Visible;
ICCWaterMarkDark.Visibility = Visibility.Collapsed;
// 设置为黑板默认背景色
Color defaultBlackboardColor = Color.FromRgb(22, 41, 36);
if (currentMode == 1) // 黑板模式
{
// 设置背景为默认黑板背景色
GridBackgroundCover.Background = new SolidColorBrush(defaultBlackboardColor);
// 更新RGB滑块的值为默认黑板背景色
UpdateRGBSliders(defaultBlackboardColor);
// 更新自定义背景色为默认黑板背景色
CustomBackgroundColor = defaultBlackboardColor;
// 保存到设置
string colorHex = $"#{defaultBlackboardColor.R:X2}{defaultBlackboardColor.G:X2}{defaultBlackboardColor.B:X2}";
Settings.Canvas.CustomBackgroundColor = colorHex;
SaveSettingsToFile();
}
// 设置墨迹颜色为白色
CheckLastColor(5);
forceEraser = false;
CheckColorTheme(true);
UpdateBackgroundButtonsState();
};
modePanel.Children.Add(blackboardButton);
contentPanel.Children.Add(modePanel);
// 添加一条分隔线
var separator = new Border
{
Height = 1,
Background = (SolidColorBrush)Application.Current.FindResource("SettingsPageBorderBrush"),
Margin = new Thickness(0, 12, 0, 12)
};
contentPanel.Children.Add(separator);
// 添加RGB颜色选择器部分
var colorTitle = new TextBlock
{
Text = "背景颜色",
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground"),
FontSize = 10,
FontWeight = FontWeights.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 4, 0, 8)
};
contentPanel.Children.Add(colorTitle);
// 创建颜色预览
Border colorPreview = new Border
{
Width = 100,
Height = 40,
BorderThickness = new Thickness(1),
BorderBrush = (SolidColorBrush)Application.Current.FindResource("SettingsPageBorderBrush"),
Background = new SolidColorBrush(Colors.White),
CornerRadius = new CornerRadius(4),
Margin = new Thickness(0, 0, 0, 10),
HorizontalAlignment = HorizontalAlignment.Center
};
contentPanel.Children.Add(colorPreview);
// 获取当前背景颜色
Color currentBackgroundColor;
if (currentMode == 1) // 白板或黑板模式
{
if (GridBackgroundCover.Background is SolidColorBrush brush)
{
currentBackgroundColor = brush.Color;
}
else
{
// 默认颜色
currentBackgroundColor = Settings.Canvas.UsingWhiteboard ?
Color.FromRgb(234, 235, 237) : // 白板默认颜色
Color.FromRgb(22, 41, 36); // 黑板默认颜色
}
}
else
{
// 默认白色
currentBackgroundColor = Colors.White;
}
// 更新颜色预览
colorPreview.Background = new SolidColorBrush(currentBackgroundColor);
CheckLastColor(0);
forceEraser = false;
CheckColorTheme(true);
UpdateBackgroundButtonsState();
}
// 先创建所有滑块控件
// R滑块和文本框
var rPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 0, 10, 5) };
var rLabel = new TextBlock { Text = "R:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
var rSlider = new Slider
private void BlackboardModeBtn_MouseUp(object sender, MouseButtonEventArgs e)
{
Settings.Canvas.UsingWhiteboard = false;
SaveSettingsToFile();
ICCWaterMarkWhite.Visibility = Visibility.Visible;
ICCWaterMarkDark.Visibility = Visibility.Collapsed;
Color defaultBlackboardColor = Color.FromRgb(22, 41, 36);
if (currentMode == 1)
{
Minimum = 0,
Maximum = 255,
Value = currentBackgroundColor.R,
Width = 150,
Margin = new Thickness(5, 0, 5, 0),
VerticalAlignment = VerticalAlignment.Center
};
var rValueText = new TextBlock
GridBackgroundCover.Background = new SolidColorBrush(defaultBlackboardColor);
UpdateRGBSliders(defaultBlackboardColor);
CustomBackgroundColor = defaultBlackboardColor;
string colorHex = $"#{defaultBlackboardColor.R:X2}{defaultBlackboardColor.G:X2}{defaultBlackboardColor.B:X2}";
Settings.Canvas.CustomBackgroundColor = colorHex;
SaveSettingsToFile();
}
CheckLastColor(5);
forceEraser = false;
CheckColorTheme(true);
UpdateBackgroundButtonsState();
}
private void BackgroundRSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (BackgroundRValue != null)
{
Text = currentBackgroundColor.R.ToString(),
Width = 30,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Right,
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
};
// G滑块和文本框
var gPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 0, 10, 5) };
var gLabel = new TextBlock { Text = "G:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
var gSlider = new Slider
{
Minimum = 0,
Maximum = 255,
Value = currentBackgroundColor.G,
Width = 150,
Margin = new Thickness(5, 0, 5, 0),
VerticalAlignment = VerticalAlignment.Center
};
var gValueText = new TextBlock
{
Text = currentBackgroundColor.G.ToString(),
Width = 30,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Right,
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
};
// B滑块和文本框
var bPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 0, 10, 5) };
var bLabel = new TextBlock { Text = "B:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
var bSlider = new Slider
{
Minimum = 0,
Maximum = 255,
Value = currentBackgroundColor.B,
Width = 150,
Margin = new Thickness(5, 0, 5, 0),
VerticalAlignment = VerticalAlignment.Center
};
var bValueText = new TextBlock
{
Text = currentBackgroundColor.B.ToString(),
Width = 30,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Right,
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
};
// 现在添加事件处理程序
rSlider.ValueChanged += (s, e) =>
{
int value = (int)e.NewValue;
rValueText.Text = value.ToString();
UpdateColorPreview(colorPreview, rSlider, gSlider, bSlider);
};
gSlider.ValueChanged += (s, e) =>
{
int value = (int)e.NewValue;
gValueText.Text = value.ToString();
UpdateColorPreview(colorPreview, rSlider, gSlider, bSlider);
};
bSlider.ValueChanged += (s, e) =>
{
int value = (int)e.NewValue;
bValueText.Text = value.ToString();
UpdateColorPreview(colorPreview, rSlider, gSlider, bSlider);
};
// 添加控件到面板
rPanel.Children.Add(rLabel);
rPanel.Children.Add(rSlider);
rPanel.Children.Add(rValueText);
contentPanel.Children.Add(rPanel);
gPanel.Children.Add(gLabel);
gPanel.Children.Add(gSlider);
gPanel.Children.Add(gValueText);
contentPanel.Children.Add(gPanel);
bPanel.Children.Add(bLabel);
bPanel.Children.Add(bSlider);
bPanel.Children.Add(bValueText);
contentPanel.Children.Add(bPanel);
// 应用按钮
var applyButton = new Button
{
Content = "应用颜色",
Margin = new Thickness(0, 10, 0, 0),
Padding = new Thickness(10, 5, 10, 5),
Background = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)),
Foreground = new SolidColorBrush(Colors.White),
BorderThickness = new Thickness(0),
HorizontalAlignment = HorizontalAlignment.Center
};
applyButton.Click += (s, e) =>
{
Color selectedColor = Color.FromRgb(
(byte)rSlider.Value,
(byte)gSlider.Value,
(byte)bSlider.Value
);
ApplyCustomBackgroundColor(selectedColor);
};
contentPanel.Children.Add(applyButton);
stackPanel.Children.Add(contentPanel);
// 将面板添加到父容器
BackgroundPalette.Child = stackPanel;
// 获取主窗口中的根网格,确保面板添加到顶层
Grid mainGrid = FindName("Main_Grid") as Grid;
if (mainGrid != null)
{
// 删除可能已存在的BackgroundPalette
foreach (UIElement element in mainGrid.Children)
{
if (element is Border border && border.Name == "BackgroundPalette")
{
mainGrid.Children.Remove(border);
break;
}
}
// 重新定位面板
BackgroundPalette.HorizontalAlignment = HorizontalAlignment.Center;
BackgroundPalette.VerticalAlignment = VerticalAlignment.Center;
BackgroundPalette.Margin = new Thickness(0, 0, 0, 0);
// 添加到主网格
mainGrid.Children.Add(BackgroundPalette);
// 设置面板位置
var clickElement = FindName("BoardChangeBackgroundColorBtn") as FrameworkElement;
if (clickElement != null)
{
Point position = clickElement.TranslatePoint(new Point(0, 0), mainGrid);
BackgroundPalette.Margin = new Thickness(
position.X - 150,
position.Y + clickElement.ActualHeight + 5,
0, 0);
BackgroundPalette.HorizontalAlignment = HorizontalAlignment.Left;
BackgroundPalette.VerticalAlignment = VerticalAlignment.Top;
}
BackgroundRValue.Text = ((int)e.NewValue).ToString();
UpdateColorPreviewFromSliders();
}
}
private void BackgroundGSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (BackgroundGValue != null)
{
BackgroundGValue.Text = ((int)e.NewValue).ToString();
UpdateColorPreviewFromSliders();
}
}
private void BackgroundBSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (BackgroundBValue != null)
{
BackgroundBValue.Text = ((int)e.NewValue).ToString();
UpdateColorPreviewFromSliders();
}
}
private void ApplyBackgroundColorBtn_Click(object sender, RoutedEventArgs e)
{
Color selectedColor = Color.FromRgb(
(byte)BackgroundRSlider.Value,
(byte)BackgroundGSlider.Value,
(byte)BackgroundBSlider.Value
);
ApplyCustomBackgroundColor(selectedColor);
}
private void UpdateColorPreviewFromSliders()
{
if (BackgroundColorPreview != null)
{
Color previewColor = Color.FromRgb(
(byte)BackgroundRSlider.Value,
(byte)BackgroundGSlider.Value,
(byte)BackgroundBSlider.Value
);
BackgroundColorPreview.Background = new SolidColorBrush(previewColor);
}
}
/// <summary>
/// 更新背景颜色选项面板中的按钮状态
/// </summary>
/// <remarks>
/// - 更新白板和黑板按钮的背景和前景色
/// - 根据当前使用的模式设置按钮状态
/// </remarks>
private void UpdateBackgroundButtonsState()
{
if (BackgroundPalette != null && BackgroundPalette.Child is StackPanel stackPanel)
if (WhiteboardModeBtn != null)
{
if (stackPanel.Children.Count > 1 && stackPanel.Children[1] is StackPanel contentPanel)
WhiteboardModeBtn.Background = Settings.Canvas.UsingWhiteboard ?
new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) :
new SolidColorBrush(Colors.LightGray);
if (WhiteboardModeBtn.Child is TextBlock whiteboardText)
{
if (contentPanel.Children.Count > 1 && contentPanel.Children[1] is StackPanel modePanel)
{
if (modePanel.Children.Count > 1)
{
var whiteboardButton = modePanel.Children[0] as Border;
var blackboardButton = modePanel.Children[1] as Border;
whiteboardText.Foreground = Settings.Canvas.UsingWhiteboard ?
new SolidColorBrush(Colors.White) :
new SolidColorBrush(Colors.Black);
}
}
if (whiteboardButton != null && whiteboardButton.Child is TextBlock whiteboardText)
{
whiteboardButton.Background = Settings.Canvas.UsingWhiteboard ?
new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) :
new SolidColorBrush(Colors.LightGray);
whiteboardText.Foreground = Settings.Canvas.UsingWhiteboard ?
new SolidColorBrush(Colors.White) :
new SolidColorBrush(Colors.Black);
}
if (blackboardButton != null && blackboardButton.Child is TextBlock blackboardText)
{
blackboardButton.Background = !Settings.Canvas.UsingWhiteboard ?
new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) :
new SolidColorBrush(Colors.LightGray);
blackboardText.Foreground = !Settings.Canvas.UsingWhiteboard ?
new SolidColorBrush(Colors.White) :
new SolidColorBrush(Colors.Black);
}
}
}
if (BlackboardModeBtn != null)
{
BlackboardModeBtn.Background = !Settings.Canvas.UsingWhiteboard ?
new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) :
new SolidColorBrush(Colors.LightGray);
if (BlackboardModeBtn.Child is TextBlock blackboardText)
{
blackboardText.Foreground = !Settings.Canvas.UsingWhiteboard ?
new SolidColorBrush(Colors.White) :
new SolidColorBrush(Colors.Black);
}
}
}
/// <summary>
/// 背景颜色选项面板
/// </summary>
private Border BackgroundPalette { get; set; }
/// <summary>
/// 当前自定义背景色
/// </summary>
@@ -767,41 +330,7 @@ namespace Ink_Canvas
/// - 启用橡皮擦模式
/// - 设置橡皮擦形状为圆形
/// - 设置当前工具模式为按笔画擦除
/// - 禁用形状绘制模式
/// - 重置钢笔类型和属性
/// - 触发编辑模式变更事件
/// - 取消单指拖动模式
/// - 隐藏子面板
/// </remarks>
private void BoardEraserIconByStrokes_Click(object sender, RoutedEventArgs e)
{
//if (BoardEraserByStrokes.Background.ToString() == "#FF679CF4") {
// AnimationsHelper.ShowWithSlideFromBottomAndFade(BoardDeleteIcon);
//}
//else {
// 禁用高级橡皮擦系统
DisableEraserOverlay();
forceEraser = true;
forcePointEraser = false;
inkCanvas.EraserShape = new EllipseStylusShape(5, 5);
// 使用集中化的工具模式切换方法
SetCurrentToolMode(InkCanvasEditingMode.EraseByStroke);
drawingShapeMode = 0;
penType = 0;
drawingAttributes.IsHighlighter = false;
drawingAttributes.StylusTip = StylusTip.Ellipse;
inkCanvas_EditingModeChanged(inkCanvas, null);
CancelSingleFingerDragMode();
HideSubPanels("eraserByStrokes");
//}
}
/// <summary>
/// 处理删除图标点击事件,清空画布内容
/// </summary>
/// <param name="sender">事件发送者</param>
@@ -913,53 +442,9 @@ namespace Ink_Canvas
/// </summary>
private void UpdateRGBSliders(Color color)
{
if (BackgroundPalette != null && BackgroundPalette.Child is StackPanel stackPanel)
{
if (stackPanel.Children.Count > 1 && stackPanel.Children[1] is StackPanel contentPanel)
{
// 查找RGB滑块
Slider rSlider = null;
Slider gSlider = null;
Slider bSlider = null;
// 遍历面板查找RGB滑块
foreach (var child in contentPanel.Children)
{
if (child is StackPanel panel && panel.Orientation == Orientation.Horizontal)
{
foreach (var panelChild in panel.Children)
{
if (panelChild is Slider slider)
{
if (panel.Children.Count > 0 && panel.Children[0] is TextBlock label)
{
if (label.Text == "R:")
{
rSlider = slider;
}
else if (label.Text == "G:")
{
gSlider = slider;
}
else if (label.Text == "B:")
{
bSlider = slider;
}
}
}
}
}
}
// 更新滑块值
if (rSlider != null && gSlider != null && bSlider != null)
{
rSlider.Value = color.R;
gSlider.Value = color.G;
bSlider.Value = color.B;
}
}
}
if (BackgroundRSlider != null) BackgroundRSlider.Value = color.R;
if (BackgroundGSlider != null) BackgroundGSlider.Value = color.G;
if (BackgroundBSlider != null) BackgroundBSlider.Value = color.B;
}
}
}
+117 -159
View File
@@ -1,13 +1,11 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
namespace Ink_Canvas
@@ -52,6 +50,7 @@ namespace Ink_Canvas
AnimationsHelper.HideWithSlideAndFade(TwoFingerGestureBorder);
AnimationsHelper.HideWithSlideAndFade(BoardTwoFingerGestureBorder);
EnableTwoFingerGestureBorder.Visibility = Visibility.Collapsed;
SyncPdfPageSidebarWithCanvas();
}
BtnHideInkCanvas_Click(BtnHideInkCanvas, null);
@@ -282,26 +281,26 @@ namespace Ink_Canvas
{
// 亮系
// 亮色的红色
BorderPenColorRed.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68));
BoardBorderPenColorRed.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68));
BorderPenColorRed.Color = Color.FromRgb(239, 68, 68);
BoardBorderPenColorRed.Color = Color.FromRgb(239, 68, 68);
// 亮色的绿色
BorderPenColorGreen.Background = new SolidColorBrush(Color.FromRgb(34, 197, 94));
BoardBorderPenColorGreen.Background = new SolidColorBrush(Color.FromRgb(34, 197, 94));
BorderPenColorGreen.Color = Color.FromRgb(34, 197, 94);
BoardBorderPenColorGreen.Color = Color.FromRgb(34, 197, 94);
// 亮色的蓝色
BorderPenColorBlue.Background = new SolidColorBrush(Color.FromRgb(59, 130, 246));
BoardBorderPenColorBlue.Background = new SolidColorBrush(Color.FromRgb(59, 130, 246));
BorderPenColorBlue.Color = Color.FromRgb(59, 130, 246);
BoardBorderPenColorBlue.Color = Color.FromRgb(59, 130, 246);
// 亮色的黄色
BorderPenColorYellow.Background = new SolidColorBrush(Color.FromRgb(250, 204, 21));
BoardBorderPenColorYellow.Background = new SolidColorBrush(Color.FromRgb(250, 204, 21));
BorderPenColorYellow.Color = Color.FromRgb(250, 204, 21);
BoardBorderPenColorYellow.Color = Color.FromRgb(250, 204, 21);
// 亮色的粉色
BorderPenColorPink.Background = new SolidColorBrush(Color.FromRgb(236, 72, 153));
BoardBorderPenColorPink.Background = new SolidColorBrush(Color.FromRgb(236, 72, 153));
BorderPenColorPink.Color = Color.FromRgb(236, 72, 153);
BoardBorderPenColorPink.Color = Color.FromRgb(236, 72, 153);
// 亮色的Teal
BorderPenColorTeal.Background = new SolidColorBrush(Color.FromRgb(20, 184, 166));
BoardBorderPenColorTeal.Background = new SolidColorBrush(Color.FromRgb(20, 184, 166));
BorderPenColorTeal.Color = Color.FromRgb(20, 184, 166);
BoardBorderPenColorTeal.Color = Color.FromRgb(20, 184, 166);
// 亮色的Orange
BorderPenColorOrange.Background = new SolidColorBrush(Color.FromRgb(249, 115, 22));
BoardBorderPenColorOrange.Background = new SolidColorBrush(Color.FromRgb(249, 115, 22));
BorderPenColorOrange.Color = Color.FromRgb(249, 115, 22);
BoardBorderPenColorOrange.Color = Color.FromRgb(249, 115, 22);
var newImageSource = new BitmapImage();
newImageSource.BeginInit();
@@ -318,26 +317,26 @@ namespace Ink_Canvas
{
// 暗系
// 暗色的红色
BorderPenColorRed.Background = new SolidColorBrush(Color.FromRgb(220, 38, 38));
BoardBorderPenColorRed.Background = new SolidColorBrush(Color.FromRgb(220, 38, 38));
BorderPenColorRed.Color = Color.FromRgb(220, 38, 38);
BoardBorderPenColorRed.Color = Color.FromRgb(220, 38, 38);
// 暗色的绿色
BorderPenColorGreen.Background = new SolidColorBrush(Color.FromRgb(22, 163, 74));
BoardBorderPenColorGreen.Background = new SolidColorBrush(Color.FromRgb(22, 163, 74));
BorderPenColorGreen.Color = Color.FromRgb(22, 163, 74);
BoardBorderPenColorGreen.Color = Color.FromRgb(22, 163, 74);
// 暗色的蓝色
BorderPenColorBlue.Background = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardBorderPenColorBlue.Background = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BorderPenColorBlue.Color = Color.FromRgb(37, 99, 235);
BoardBorderPenColorBlue.Color = Color.FromRgb(37, 99, 235);
// 暗色的黄色
BorderPenColorYellow.Background = new SolidColorBrush(Color.FromRgb(234, 179, 8));
BoardBorderPenColorYellow.Background = new SolidColorBrush(Color.FromRgb(234, 179, 8));
BorderPenColorYellow.Color = Color.FromRgb(234, 179, 8);
BoardBorderPenColorYellow.Color = Color.FromRgb(234, 179, 8);
// 暗色的紫色对应亮色的粉色
BorderPenColorPink.Background = new SolidColorBrush(Color.FromRgb(147, 51, 234));
BoardBorderPenColorPink.Background = new SolidColorBrush(Color.FromRgb(147, 51, 234));
BorderPenColorPink.Color = Color.FromRgb(147, 51, 234);
BoardBorderPenColorPink.Color = Color.FromRgb(147, 51, 234);
// 暗色的Teal
BorderPenColorTeal.Background = new SolidColorBrush(Color.FromRgb(13, 148, 136));
BoardBorderPenColorTeal.Background = new SolidColorBrush(Color.FromRgb(13, 148, 136));
BorderPenColorTeal.Color = Color.FromRgb(13, 148, 136);
BoardBorderPenColorTeal.Color = Color.FromRgb(13, 148, 136);
// 暗色的Orange
BorderPenColorOrange.Background = new SolidColorBrush(Color.FromRgb(234, 88, 12));
BoardBorderPenColorOrange.Background = new SolidColorBrush(Color.FromRgb(234, 88, 12));
BorderPenColorOrange.Color = Color.FromRgb(234, 88, 12);
BoardBorderPenColorOrange.Color = Color.FromRgb(234, 88, 12);
var newImageSource = new BitmapImage();
newImageSource.BeginInit();
@@ -352,127 +351,129 @@ namespace Ink_Canvas
}
// 改变选中提示
ViewboxBtnColorBlackContent.Visibility = Visibility.Collapsed;
ViewboxBtnColorBlueContent.Visibility = Visibility.Collapsed;
ViewboxBtnColorGreenContent.Visibility = Visibility.Collapsed;
ViewboxBtnColorRedContent.Visibility = Visibility.Collapsed;
ViewboxBtnColorYellowContent.Visibility = Visibility.Collapsed;
ViewboxBtnColorWhiteContent.Visibility = Visibility.Collapsed;
ViewboxBtnColorPinkContent.Visibility = Visibility.Collapsed;
ViewboxBtnColorTealContent.Visibility = Visibility.Collapsed;
ViewboxBtnColorOrangeContent.Visibility = Visibility.Collapsed;
BorderPenColorBlack.IsChecked = false;
BorderPenColorBlue.IsChecked = false;
BorderPenColorGreen.IsChecked = false;
BorderPenColorRed.IsChecked = false;
BorderPenColorYellow.IsChecked = false;
BorderPenColorWhite.IsChecked = false;
BorderPenColorPink.IsChecked = false;
BorderPenColorTeal.IsChecked = false;
BorderPenColorOrange.IsChecked = false;
BoardViewboxBtnColorBlackContent.Visibility = Visibility.Collapsed;
BoardViewboxBtnColorBlueContent.Visibility = Visibility.Collapsed;
BoardViewboxBtnColorGreenContent.Visibility = Visibility.Collapsed;
BoardViewboxBtnColorRedContent.Visibility = Visibility.Collapsed;
BoardViewboxBtnColorYellowContent.Visibility = Visibility.Collapsed;
BoardViewboxBtnColorWhiteContent.Visibility = Visibility.Collapsed;
BoardViewboxBtnColorPinkContent.Visibility = Visibility.Collapsed;
BoardViewboxBtnColorTealContent.Visibility = Visibility.Collapsed;
BoardViewboxBtnColorOrangeContent.Visibility = Visibility.Collapsed;
BoardBorderPenColorBlack.IsChecked = false;
BoardBorderPenColorBlue.IsChecked = false;
BoardBorderPenColorGreen.IsChecked = false;
BoardBorderPenColorRed.IsChecked = false;
BoardBorderPenColorYellow.IsChecked = false;
BoardBorderPenColorWhite.IsChecked = false;
BoardBorderPenColorPink.IsChecked = false;
BoardBorderPenColorTeal.IsChecked = false;
BoardBorderPenColorOrange.IsChecked = false;
HighlighterPenViewboxBtnColorBlackContent.Visibility = Visibility.Collapsed;
HighlighterPenViewboxBtnColorBlueContent.Visibility = Visibility.Collapsed;
HighlighterPenViewboxBtnColorGreenContent.Visibility = Visibility.Collapsed;
HighlighterPenViewboxBtnColorOrangeContent.Visibility = Visibility.Collapsed;
HighlighterPenViewboxBtnColorPurpleContent.Visibility = Visibility.Collapsed;
HighlighterPenViewboxBtnColorRedContent.Visibility = Visibility.Collapsed;
HighlighterPenViewboxBtnColorTealContent.Visibility = Visibility.Collapsed;
HighlighterPenViewboxBtnColorWhiteContent.Visibility = Visibility.Collapsed;
HighlighterPenViewboxBtnColorYellowContent.Visibility = Visibility.Collapsed;
HighlighterPenViewboxBtnColorZincContent.Visibility = Visibility.Collapsed;
HighlighterPenColorBlack.IsChecked = false;
HighlighterPenColorBlue.IsChecked = false;
HighlighterPenColorGreen.IsChecked = false;
HighlighterPenColorOrange.IsChecked = false;
HighlighterPenPenColorPurple.IsChecked = false;
HighlighterPenColorRed.IsChecked = false;
HighlighterPenColorTeal.IsChecked = false;
HighlighterPenColorWhite.IsChecked = false;
HighlighterPenColorYellow.IsChecked = false;
HighlighterPenColorZinc.IsChecked = false;
BoardHighlighterPenViewboxBtnColorBlackContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenViewboxBtnColorBlueContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenViewboxBtnColorGreenContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenViewboxBtnColorOrangeContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenViewboxBtnColorPurpleContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenViewboxBtnColorRedContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenViewboxBtnColorTealContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenViewboxBtnColorWhiteContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenViewboxBtnColorYellowContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenViewboxBtnColorZincContent.Visibility = Visibility.Collapsed;
BoardHighlighterPenColorBlack.IsChecked = false;
BoardHighlighterPenColorBlue.IsChecked = false;
BoardHighlighterPenColorGreen.IsChecked = false;
BoardHighlighterPenColorOrange.IsChecked = false;
BoardHighlighterPenPenColorPurple.IsChecked = false;
BoardHighlighterPenColorRed.IsChecked = false;
BoardHighlighterPenColorTeal.IsChecked = false;
BoardHighlighterPenColorWhite.IsChecked = false;
BoardHighlighterPenColorYellow.IsChecked = false;
BoardHighlighterPenColorZinc.IsChecked = false;
switch (inkColor)
{
case 0:
ViewboxBtnColorBlackContent.Visibility = Visibility.Visible;
BoardViewboxBtnColorBlackContent.Visibility = Visibility.Visible;
BorderPenColorBlack.IsChecked = true;
BoardBorderPenColorBlack.IsChecked = true;
break;
case 1:
ViewboxBtnColorRedContent.Visibility = Visibility.Visible;
BoardViewboxBtnColorRedContent.Visibility = Visibility.Visible;
BorderPenColorRed.IsChecked = true;
BoardBorderPenColorRed.IsChecked = true;
break;
case 2:
ViewboxBtnColorGreenContent.Visibility = Visibility.Visible;
BoardViewboxBtnColorGreenContent.Visibility = Visibility.Visible;
BorderPenColorGreen.IsChecked = true;
BoardBorderPenColorGreen.IsChecked = true;
break;
case 3:
ViewboxBtnColorBlueContent.Visibility = Visibility.Visible;
BoardViewboxBtnColorBlueContent.Visibility = Visibility.Visible;
BorderPenColorBlue.IsChecked = true;
BoardBorderPenColorBlue.IsChecked = true;
break;
case 4:
ViewboxBtnColorYellowContent.Visibility = Visibility.Visible;
BoardViewboxBtnColorYellowContent.Visibility = Visibility.Visible;
BorderPenColorYellow.IsChecked = true;
BoardBorderPenColorYellow.IsChecked = true;
break;
case 5:
ViewboxBtnColorWhiteContent.Visibility = Visibility.Visible;
BoardViewboxBtnColorWhiteContent.Visibility = Visibility.Visible;
BorderPenColorWhite.IsChecked = true;
BoardBorderPenColorWhite.IsChecked = true;
break;
case 6:
ViewboxBtnColorPinkContent.Visibility = Visibility.Visible;
BoardViewboxBtnColorPinkContent.Visibility = Visibility.Visible;
BorderPenColorPink.IsChecked = true;
BoardBorderPenColorPink.IsChecked = true;
break;
case 7:
ViewboxBtnColorTealContent.Visibility = Visibility.Visible;
BorderPenColorTeal.IsChecked = true;
BoardBorderPenColorTeal.IsChecked = true;
break;
case 8:
ViewboxBtnColorOrangeContent.Visibility = Visibility.Visible;
BorderPenColorOrange.IsChecked = true;
BoardBorderPenColorOrange.IsChecked = true;
break;
}
switch (highlighterColor)
{
case 100:
HighlighterPenViewboxBtnColorBlackContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorBlackContent.Visibility = Visibility.Visible;
HighlighterPenColorBlack.IsChecked = true;
BoardHighlighterPenColorBlack.IsChecked = true;
break;
case 101:
HighlighterPenViewboxBtnColorWhiteContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorWhiteContent.Visibility = Visibility.Visible;
HighlighterPenColorWhite.IsChecked = true;
BoardHighlighterPenColorWhite.IsChecked = true;
break;
case 102:
HighlighterPenViewboxBtnColorRedContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorRedContent.Visibility = Visibility.Visible;
HighlighterPenColorRed.IsChecked = true;
BoardHighlighterPenColorRed.IsChecked = true;
break;
case 103:
HighlighterPenViewboxBtnColorYellowContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorYellowContent.Visibility = Visibility.Visible;
HighlighterPenColorYellow.IsChecked = true;
BoardHighlighterPenColorYellow.IsChecked = true;
break;
case 104:
HighlighterPenViewboxBtnColorGreenContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorGreenContent.Visibility = Visibility.Visible;
HighlighterPenColorGreen.IsChecked = true;
BoardHighlighterPenColorGreen.IsChecked = true;
break;
case 105:
HighlighterPenViewboxBtnColorZincContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorZincContent.Visibility = Visibility.Visible;
HighlighterPenColorZinc.IsChecked = true;
BoardHighlighterPenColorZinc.IsChecked = true;
break;
case 106:
HighlighterPenViewboxBtnColorBlueContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorBlueContent.Visibility = Visibility.Visible;
HighlighterPenColorBlue.IsChecked = true;
BoardHighlighterPenColorBlue.IsChecked = true;
break;
case 107:
HighlighterPenViewboxBtnColorPurpleContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorPurpleContent.Visibility = Visibility.Visible;
HighlighterPenPenColorPurple.IsChecked = true;
BoardHighlighterPenPenColorPurple.IsChecked = true;
break;
case 108:
HighlighterPenViewboxBtnColorTealContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorTealContent.Visibility = Visibility.Visible;
HighlighterPenColorTeal.IsChecked = true;
BoardHighlighterPenColorTeal.IsChecked = true;
break;
case 109:
HighlighterPenViewboxBtnColorOrangeContent.Visibility = Visibility.Visible;
BoardHighlighterPenViewboxBtnColorOrangeContent.Visibility = Visibility.Visible;
HighlighterPenColorOrange.IsChecked = true;
BoardHighlighterPenColorOrange.IsChecked = true;
break;
}
@@ -553,37 +554,15 @@ namespace Ink_Canvas
BoardHighlightPenTabButton.Background = new SolidColorBrush(Colors.Transparent);
BoardHighlightPenTabButtonIndicator.Visibility = Visibility.Collapsed;
// PenPalette.Margin = new Thickness(-160, -200, -33, 32);
// 动态计算面板位置,使其对齐笔按钮(考虑快捷调色盘等动态宽度)
await Dispatcher.InvokeAsync(() =>
{
var marginAnimation = new ThicknessAnimation
{
Duration = TimeSpan.FromSeconds(0.1),
From = PenPalette.Margin,
To = new Thickness(-160, -200, -33, 32),
EasingFunction = new CubicEase()
};
PenPalette.BeginAnimation(MarginProperty, marginAnimation);
PenPalette.BeginAnimation(MarginProperty, null);
var currentMargin = PenPalette.Margin;
// 先设置正确的Top/Bottom,保持当前Left/Right
PenPalette.Margin = new Thickness(currentMargin.Left, -200, currentMargin.Right, 32);
UpdatePenPalettePosition();
});
await Dispatcher.InvokeAsync(() =>
{
var marginAnimation = new ThicknessAnimation
{
Duration = TimeSpan.FromSeconds(0.1),
From = PenPalette.Margin,
To = new Thickness(-160, -200, -33, 50),
EasingFunction = new CubicEase()
};
BoardPenPaletteGrid.BeginAnimation(MarginProperty, marginAnimation);
});
await Task.Delay(100);
await Dispatcher.InvokeAsync(() => { PenPalette.Margin = new Thickness(-160, -200, -33, 32); });
await Dispatcher.InvokeAsync(() => { BoardPenPaletteGrid.Margin = new Thickness(-160, -200, -33, 50); });
}
else if (penType == 1)
{
@@ -621,36 +600,15 @@ namespace Ink_Canvas
BoardHighlightPenTabButton.Background = new SolidColorBrush(Color.FromArgb(72, 219, 234, 254));
BoardHighlightPenTabButtonIndicator.Visibility = Visibility.Visible;
// PenPalette.Margin = new Thickness(-160, -157, -33, 32);
// 动态计算面板位置,使其对齐笔按钮(考虑快捷调色盘等动态宽度)
await Dispatcher.InvokeAsync(() =>
{
var marginAnimation = new ThicknessAnimation
{
Duration = TimeSpan.FromSeconds(0.1),
From = PenPalette.Margin,
To = new Thickness(-160, -157, -33, 32),
EasingFunction = new CubicEase()
};
PenPalette.BeginAnimation(MarginProperty, marginAnimation);
PenPalette.BeginAnimation(MarginProperty, null);
var currentMargin = PenPalette.Margin;
// 荧光笔模式面板稍小,使用不同的Top/Bottom
PenPalette.Margin = new Thickness(currentMargin.Left, -157, currentMargin.Right, 32);
UpdatePenPalettePosition();
});
await Dispatcher.InvokeAsync(() =>
{
var marginAnimation = new ThicknessAnimation
{
Duration = TimeSpan.FromSeconds(0.1),
From = PenPalette.Margin,
To = new Thickness(-160, -154, -33, 50),
EasingFunction = new CubicEase()
};
BoardPenPaletteGrid.BeginAnimation(MarginProperty, marginAnimation);
});
await Task.Delay(100);
await Dispatcher.InvokeAsync(() => { PenPalette.Margin = new Thickness(-160, -157, -33, 32); });
await Dispatcher.InvokeAsync(() => { BoardPenPaletteGrid.Margin = new Thickness(-160, -154, -33, 50); });
}
}
+490 -46
View File
@@ -1,3 +1,4 @@
using Ink_Canvas.Controls;
using Ink_Canvas.Helpers;
using Microsoft.Win32;
using System;
@@ -34,6 +35,14 @@ namespace Ink_Canvas
/// </summary>
private Point dragStartPoint;
/// <summary>页码侧栏当前订阅 <see cref="PdfEmbeddedView.PageNavigationStateChanged"/> 的 PDF 视图。</summary>
private PdfEmbeddedView _pdfPageSidebarEventSource;
private bool _pdfSidebarPositionRefreshPending;
/// <summary>为 true 时,下一次成功算出 PDF 边界后的侧栏定位使用宿主 Visual 变换(仅用于刚插入/恢复 PDF 的首帧对齐)。</summary>
private bool _pdfSidebarNextPositionUseHostTransform;
#region Image
/// <summary>
/// 处理图片插入按钮点击事件
@@ -56,48 +65,52 @@ namespace Ink_Canvas
private async void BtnImageInsert_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "Image files (*.jpg; *.jpeg; *.png; *.bmp)|*.jpg;*.jpeg;*.png;*.bmp";
openFileDialog.Filter = "图片与 PDF|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.pdf|图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif|PDF|*.pdf";
if (openFileDialog.ShowDialog() == true)
{
string filePath = openFileDialog.FileName;
Image image = await CreateAndCompressImageAsync(filePath);
FrameworkElement element = await CreateAndCompressImageAsync(filePath);
if (image != null)
if (element != null)
{
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
image.Name = timestamp;
element.Name = timestamp;
// 设置图片属性,避免被InkCanvas选择系统处理
image.IsHitTestVisible = true;
image.Focusable = false;
element.IsHitTestVisible = true;
element.Focusable = false;
// 初始化InkCanvas选择设置
InitializeInkCanvasSelectionSettings();
// 先添加到画布
inkCanvas.Children.Add(image);
inkCanvas.Children.Add(element);
// 等待图片加载完成后再进行后续处理
image.Loaded += (s, args) =>
element.Loaded += (s, args) =>
{
Dispatcher.BeginInvoke(new Action(() =>
{
// 初始化TransformGroup
InitializeElementTransform(image);
InitializeElementTransform(element);
// 居中缩放
CenterAndScaleElement(image);
CenterAndScaleElement(element);
// 最后绑定事件处理器
BindElementEvents(image);
BindElementEvents(element);
LogHelper.WriteLogToFile($"图片插入完成: {image.Name}");
if (element is PdfEmbeddedView)
_pdfSidebarNextPositionUseHostTransform = true;
SyncPdfPageSidebarWithCanvas();
LogHelper.WriteLogToFile($"图片插入完成: {element.Name}");
}), DispatcherPriority.Loaded);
};
timeMachine.CommitElementInsertHistory(image);
timeMachine.CommitElementInsertHistory(element);
// 插入图片后切换到选择模式并刷新浮动栏高光显示
SetCurrentToolMode(InkCanvasEditingMode.Select);
@@ -270,13 +283,13 @@ namespace Ink_Canvas
ApplyMouseDragTransform(element, currentPoint, dragStartPoint);
// 如果是图片元素,更新工具栏位置
if (element is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(element);
}
// 如果是图片元素,更新选择点位置
if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
{
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
}
@@ -306,13 +319,13 @@ namespace Ink_Canvas
ApplyWheelScaleTransform(element, e);
// 如果是图片元素,更新工具栏位置
if (element is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(element);
}
// 如果是图片元素,更新选择点位置
if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
{
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
}
@@ -396,13 +409,13 @@ namespace Ink_Canvas
ApplyTouchManipulationTransform(element, e);
// 如果是图片元素,更新工具栏位置
if (element is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(element);
}
// 如果是图片元素,更新选择点位置
if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
{
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
}
@@ -523,12 +536,12 @@ namespace Ink_Canvas
currentSelectedElement = element;
// 根据元素类型显示不同的选择工具栏
if (element is Image)
if (IsBitmapLikeCanvasElement(element))
{
// 显示图片选择工具栏并设置位置
if (BorderImageSelectionControl != null)
{
// 计算工具栏位置
// 计算工具栏位置(内部会同步 PDF 右侧栏位置)
UpdateImageSelectionToolbarPosition(element);
BorderImageSelectionControl.Visibility = Visibility.Visible;
}
@@ -568,6 +581,8 @@ namespace Ink_Canvas
// 保持选择模式,这样用户可以直接点击墨迹来选择
inkCanvas.EditingMode = InkCanvasEditingMode.Select;
}
SyncPdfPageSidebarWithCanvas();
}
/// <summary>
@@ -607,6 +622,8 @@ namespace Ink_Canvas
{
inkCanvas.EditingMode = InkCanvasEditingMode.Select;
}
SyncPdfPageSidebarWithCanvas();
}
/// <summary>
@@ -914,15 +931,24 @@ namespace Ink_Canvas
/// - 否则使用原始尺寸
/// - 返回创建的Image对象
/// </remarks>
private async Task<Image> CreateAndCompressImageAsync(string filePath)
/// <summary>与图片选择工具栏、缩放控制点联动的画布位图类元素(普通图片或多页 PDF 嵌入)。</summary>
private static bool IsBitmapLikeCanvasElement(FrameworkElement fe)
{
return fe is Image || fe is PdfEmbeddedView;
}
private async Task<FrameworkElement> CreateAndCompressImageAsync(string filePath)
{
string fileExtension = Path.GetExtension(filePath);
if (string.Equals(fileExtension, ".pdf", StringComparison.OrdinalIgnoreCase))
return await CreateAndCompressImageFromPdfAsync(filePath);
string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency");
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
}
string fileExtension = Path.GetExtension(filePath);
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
string newFilePath = Path.Combine(savePath, timestamp + fileExtension);
@@ -965,6 +991,84 @@ namespace Ink_Canvas
return image;
});
}
/// <summary>
/// 插入完整 PDF:嵌入控件内可翻页,右下角显示页码(类似希沃白板交互)。
/// </summary>
private async Task<PdfEmbeddedView> CreateAndCompressImageFromPdfAsync(string filePath)
{
try
{
string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency");
if (!Directory.Exists(savePath))
Directory.CreateDirectory(savePath);
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
string newFilePath = Path.Combine(savePath, timestamp + ".pdf");
await Task.Run(() => File.Copy(filePath, newFilePath, true));
uint pageCount = await PdfWinRtHelper.GetPageCountAsync(newFilePath);
if (pageCount == 0)
{
ShowNotification("无法打开 PDF(可能已加密、损坏或不支持)。");
return null;
}
bool compress = isLoaded && Settings.Canvas.IsCompressPicturesUploaded;
var view = new PdfEmbeddedView();
await view.InitializeAsync(newFilePath, pageCount, compress);
view.Tag = filePath;
return view;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"插入 PDF 失败: {ex.Message}", LogHelper.LogType.Error);
ShowNotification($"插入 PDF 失败: {ex.Message}");
return null;
}
}
/// <summary>从保存的 <see cref="CanvasElementInfo"/> 恢复 PDF(与打开墨迹时的图片恢复流程一致,不单独写入时间轴)。</summary>
private async Task RestorePdfFromElementInfoAsync(CanvasElementInfo info)
{
if (info == null || inkCanvas == null) return;
if (!string.Equals(info.Type, "Pdf", StringComparison.OrdinalIgnoreCase)) return;
if (string.IsNullOrEmpty(info.SourcePath) || !File.Exists(info.SourcePath)) return;
try
{
uint pageCount = await PdfWinRtHelper.GetPageCountAsync(info.SourcePath);
if (pageCount == 0) return;
bool compress = isLoaded && Settings.Canvas.IsCompressPicturesUploaded;
uint initial = 0;
if (info.PdfCurrentPage.HasValue)
initial = (uint)Math.Max(0, Math.Min(info.PdfCurrentPage.Value, (int)pageCount - 1));
var view = new PdfEmbeddedView
{
Name = "pdf_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff")
};
await view.InitializeAsync(info.SourcePath, pageCount, compress, initial);
if (info.Width > 0) view.Width = info.Width;
if (info.Height > 0) view.Height = info.Height;
InkCanvas.SetLeft(view, info.Left);
InkCanvas.SetTop(view, info.Top);
InitializeElementTransform(view);
BindElementEvents(view);
inkCanvas.Children.Add(view);
_pdfSidebarNextPositionUseHostTransform = true;
SyncPdfPageSidebarWithCanvas();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从 .elements.json 恢复 PDF 失败: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region Media
@@ -1453,6 +1557,10 @@ namespace Ink_Canvas
// 设置工具栏位置
BorderImageSelectionControl.Margin = new Thickness(toolbarLeft, toolbarTop, 0, 0);
var pdfTarget = GetPdfSidebarTargetElement();
if (pdfTarget != null && BorderPdfPageSidebar != null && BorderPdfPageSidebar.Visibility == Visibility.Visible)
UpdatePdfPageSidebarPosition(pdfTarget);
}
catch (Exception ex)
{
@@ -1460,6 +1568,245 @@ namespace Ink_Canvas
}
}
private const double PdfPageSidebarGap = 10;
/// <summary>
/// 侧栏绑定的 PDF:若当前选中的是 PDF 则用该项;否则用画布上最后一个 PdfEmbeddedView。
/// </summary>
private PdfEmbeddedView GetPdfSidebarTargetElement()
{
if (inkCanvas == null) return null;
var pdfs = inkCanvas.Children.OfType<PdfEmbeddedView>().ToList();
if (pdfs.Count == 0) return null;
if (currentSelectedElement is PdfEmbeddedView sel && pdfs.Contains(sel))
return sel;
return pdfs[pdfs.Count - 1];
}
private void AttachPdfPageSidebarEvents(PdfEmbeddedView pdf)
{
if (pdf == null || _pdfPageSidebarEventSource == pdf) return;
DetachPdfPageSidebarEvents();
_pdfPageSidebarEventSource = pdf;
_pdfPageSidebarEventSource.PageNavigationStateChanged += SelectedPdf_PageNavigationStateChanged;
_pdfPageSidebarEventSource.LayoutUpdated += OnPdfSidebarTargetLayoutUpdated;
}
private void DetachPdfPageSidebarEvents()
{
if (_pdfPageSidebarEventSource != null)
{
_pdfPageSidebarEventSource.PageNavigationStateChanged -= SelectedPdf_PageNavigationStateChanged;
_pdfPageSidebarEventSource.LayoutUpdated -= OnPdfSidebarTargetLayoutUpdated;
_pdfPageSidebarEventSource = null;
}
}
private void OnPdfSidebarTargetLayoutUpdated(object sender, EventArgs e)
{
if (BorderPdfPageSidebar?.Visibility != Visibility.Visible || inkCanvas == null) return;
if (!(sender is PdfEmbeddedView p) || !ReferenceEquals(_pdfPageSidebarEventSource, p)) return;
if (!inkCanvas.Children.Contains(p))
SyncPdfPageSidebarWithCanvas();
else
RequestPdfSidebarPositionRefresh();
}
/// <summary>在下一帧合并更新侧栏位置(初始布局、翻页改尺寸后 ActualWidth 仍为 0 时尤其需要)。</summary>
private void RequestPdfSidebarPositionRefresh()
{
if (_pdfSidebarPositionRefreshPending) return;
_pdfSidebarPositionRefreshPending = true;
Dispatcher.BeginInvoke(new Action(() =>
{
_pdfSidebarPositionRefreshPending = false;
try
{
var t = GetPdfSidebarTargetElement();
if (t == null || BorderPdfPageSidebar?.Visibility != Visibility.Visible)
{
SyncPdfPageSidebarWithCanvas();
return;
}
if (!inkCanvas.Children.Contains(t))
{
SyncPdfPageSidebarWithCanvas();
return;
}
t.UpdateLayout();
inkCanvas.UpdateLayout();
UpdatePdfPageSidebarPosition(t);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PDF 侧栏延迟定位失败: {ex.Message}", LogHelper.LogType.Warning);
}
}), DispatcherPriority.Render);
}
/// <summary>
/// 画布上存在 PDF 时始终显示右侧页码栏并跟随目标 PDF;无任何 PDF 时隐藏。
/// </summary>
private void SyncPdfPageSidebarWithCanvas()
{
if (BorderPdfPageSidebar == null || inkCanvas == null) return;
// 屏幕模式(已退出白板/黑板)下不显示侧栏,避免画布仍含 PDF 时栏残留在桌面上
if (currentMode == 0)
{
DetachPdfPageSidebarEvents();
BorderPdfPageSidebar.Visibility = Visibility.Collapsed;
ResetPdfSidebarToIdle();
return;
}
var pdf = GetPdfSidebarTargetElement();
if (pdf == null)
{
DetachPdfPageSidebarEvents();
BorderPdfPageSidebar.Visibility = Visibility.Collapsed;
ResetPdfSidebarToIdle();
return;
}
AttachPdfPageSidebarEvents(pdf);
BorderPdfPageSidebar.Visibility = Visibility.Visible;
UpdatePdfSidebarFromPdf(pdf);
pdf.UpdateLayout();
inkCanvas.UpdateLayout();
UpdatePdfPageSidebarPosition(pdf);
RequestPdfSidebarPositionRefresh();
Dispatcher.BeginInvoke(new Action(() =>
{
var t = GetPdfSidebarTargetElement();
if (t != null && BorderPdfPageSidebar?.Visibility == Visibility.Visible && inkCanvas.Children.Contains(t))
UpdatePdfPageSidebarPosition(t);
}), DispatcherPriority.ContextIdle);
}
/// <summary>
/// 将 PDF 专用页码栏贴在当前 PDF 右侧。常态与早期实现一致:画布坐标 + <c>Measure(Width, ∞)</c>;仅在 <see cref="_pdfSidebarNextPositionUseHostTransform"/> 为 true 时用宿主 Visual 变换对齐首帧。
/// </summary>
private void UpdatePdfPageSidebarPosition(FrameworkElement element)
{
try
{
if (BorderPdfPageSidebar == null || inkCanvas == null || !(element is PdfEmbeddedView pdfEl))
return;
if (!inkCanvas.Children.Contains(pdfEl))
{
SyncPdfPageSidebarWithCanvas();
return;
}
bool wantHostOnce = _pdfSidebarNextPositionUseHostTransform;
// 插入首帧:先布局再取界,便于 Transform 与墨迹一致;常态与最初 PDF 侧栏实现一致,不在此强制 UpdateLayout。
if (wantHostOnce)
pdfEl.UpdateLayout();
Rect b = GetElementActualBounds(pdfEl);
if (b.Width <= 0 || b.Height <= 0 || double.IsNaN(b.Width) || double.IsNaN(b.Height))
{
Dispatcher.BeginInvoke(new Action(() =>
{
var t = GetPdfSidebarTargetElement();
if (t is PdfEmbeddedView pe && inkCanvas.Children.Contains(pe) && BorderPdfPageSidebar?.Visibility == Visibility.Visible)
UpdatePdfPageSidebarPosition(pe);
}), DispatcherPriority.Loaded);
return;
}
Visual sidebarHost = VisualTreeHelper.GetParent(BorderPdfPageSidebar) as Visual;
double left = 0, top = 0, maxLeft = 0, maxTop = 0;
bool hostOk = false;
if (wantHostOnce && sidebarHost != null)
{
BorderPdfPageSidebar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
double sidebarW = BorderPdfPageSidebar.DesiredSize.Width;
double sidebarH = BorderPdfPageSidebar.DesiredSize.Height;
if (sidebarW <= 0)
sidebarW = BorderPdfPageSidebar.Width;
if (sidebarH <= 0)
sidebarH = BorderPdfPageSidebar.ActualHeight;
if (sidebarH <= 0)
sidebarH = 220;
try
{
GeneralTransform inkToHost = inkCanvas.TransformToVisual(sidebarHost);
Point tl = inkToHost.Transform(new Point(b.Left, b.Top));
Point br = inkToHost.Transform(new Point(b.Right, b.Bottom));
double rightX = Math.Max(tl.X, br.X);
double midY = (tl.Y + br.Y) * 0.5;
left = rightX + PdfPageSidebarGap;
top = midY - sidebarH * 0.5;
var feHost = sidebarHost as FrameworkElement;
double hostW = feHost != null && feHost.ActualWidth > 0 ? feHost.ActualWidth : inkCanvas.ActualWidth;
double hostH = feHost != null && feHost.ActualHeight > 0 ? feHost.ActualHeight : inkCanvas.ActualHeight;
maxLeft = Math.Max(0, hostW - sidebarW);
maxTop = Math.Max(0, hostH - sidebarH);
if (left > maxLeft)
{
double leftEdge = Math.Min(tl.X, br.X);
double leftAlt = leftEdge - PdfPageSidebarGap - sidebarW;
if (leftAlt >= 0)
left = leftAlt;
}
hostOk = true;
}
catch
{
hostOk = false;
}
}
if (!hostOk)
{
// 与 ea74592「PDF 侧栏」初版一致:固定宽度测量竖向所需高度,再按墨迹边界与 inkCanvas 尺寸夹紧。
BorderPdfPageSidebar.Measure(new Size(BorderPdfPageSidebar.Width, double.PositiveInfinity));
double sidebarW = BorderPdfPageSidebar.DesiredSize.Width;
double sidebarH = BorderPdfPageSidebar.DesiredSize.Height;
if (sidebarW <= 0)
sidebarW = BorderPdfPageSidebar.Width;
if (sidebarH <= 0)
sidebarH = BorderPdfPageSidebar.ActualHeight;
if (sidebarH <= 0)
sidebarH = 220;
left = b.Right + PdfPageSidebarGap;
top = b.Top + (b.Height * 0.5) - (sidebarH * 0.5);
maxLeft = Math.Max(0, inkCanvas.ActualWidth - sidebarW);
maxTop = Math.Max(0, inkCanvas.ActualHeight - sidebarH);
if (left > maxLeft)
{
double leftAlt = b.Left - PdfPageSidebarGap - sidebarW;
if (leftAlt >= 0)
left = leftAlt;
}
}
if (wantHostOnce)
_pdfSidebarNextPositionUseHostTransform = false;
left = Math.Max(0, Math.Min(left, maxLeft));
top = Math.Max(0, Math.Min(top, maxTop));
BorderPdfPageSidebar.Margin = new Thickness(left, top, 0, 0);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新 PDF 右侧页码栏位置失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 获取元素的实际边界(考虑变换)
/// </summary>
@@ -1644,7 +1991,7 @@ namespace Ink_Canvas
ApplyRotateTransform(currentSelectedElement, -45);
// 更新工具栏位置
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
@@ -1678,7 +2025,7 @@ namespace Ink_Canvas
ApplyRotateTransform(currentSelectedElement, 45);
// 更新工具栏位置
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
@@ -1714,7 +2061,7 @@ namespace Ink_Canvas
ApplyScaleTransform(currentSelectedElement, 0.9, elementCenter);
// 更新工具栏位置
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
@@ -1750,7 +2097,7 @@ namespace Ink_Canvas
ApplyScaleTransform(currentSelectedElement, 1.1, elementCenter);
// 更新工具栏位置
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
}
@@ -1764,19 +2111,110 @@ namespace Ink_Canvas
}
}
private void ResetPdfSidebarToIdle()
{
if (TextBlockPdfSidebarPageLabel != null)
{
TextBlockPdfSidebarPageLabel.Text = "— / —";
TextBlockPdfSidebarPageLabel.Opacity = 0.55;
}
if (BorderPdfSidebarPagePrev != null)
{
BorderPdfSidebarPagePrev.Opacity = 0.35;
BorderPdfSidebarPagePrev.IsHitTestVisible = false;
}
if (BorderPdfSidebarPageNext != null)
{
BorderPdfSidebarPageNext.Opacity = 0.35;
BorderPdfSidebarPageNext.IsHitTestVisible = false;
}
}
private void UpdatePdfSidebarFromPdf(PdfEmbeddedView pdf)
{
if (pdf == null) return;
if (TextBlockPdfSidebarPageLabel != null)
{
TextBlockPdfSidebarPageLabel.Text = pdf.PageLabelText;
TextBlockPdfSidebarPageLabel.Opacity = 1.0;
}
bool prevOk = pdf.CanGoPrevious;
bool nextOk = pdf.CanGoNext;
if (BorderPdfSidebarPagePrev != null)
{
BorderPdfSidebarPagePrev.Opacity = prevOk ? 1.0 : 0.35;
BorderPdfSidebarPagePrev.IsHitTestVisible = prevOk;
}
if (BorderPdfSidebarPageNext != null)
{
BorderPdfSidebarPageNext.Opacity = nextOk ? 1.0 : 0.35;
BorderPdfSidebarPageNext.IsHitTestVisible = nextOk;
}
}
private void SelectedPdf_PageNavigationStateChanged(object sender, EventArgs e)
{
Dispatcher.BeginInvoke(new Action(() =>
{
if (sender is PdfEmbeddedView pdf)
{
if (!inkCanvas.Children.Contains(pdf))
{
SyncPdfPageSidebarWithCanvas();
return;
}
UpdatePdfSidebarFromPdf(pdf);
UpdatePdfPageSidebarPosition(pdf);
RequestPdfSidebarPositionRefresh();
}
if (currentSelectedElement != null && IsBitmapLikeCanvasElement(currentSelectedElement))
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
if (ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
UpdateImageResizeHandlesPosition(GetElementActualBounds(currentSelectedElement));
}
}), DispatcherPriority.Loaded);
}
private async void BorderPdfSidebarPagePrev_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
var pdf = GetPdfSidebarTargetElement();
if (pdf != null && pdf.CanGoPrevious)
await pdf.GoToPreviousPageAsync();
SyncPdfPageSidebarWithCanvas();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PDF 上一页失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private async void BorderPdfSidebarPageNext_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
var pdf = GetPdfSidebarTargetElement();
if (pdf != null && pdf.CanGoNext)
await pdf.GoToNextPageAsync();
SyncPdfPageSidebarWithCanvas();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PDF 下一页失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 处理图片删除功能
/// </summary>
/// <param name="sender">事件发送者</param>
/// <param name="e">事件参数</param>
/// <remarks>
/// - 检查当前是否有选中元素
/// - 保存删除前的编辑模式
/// - 记录删除历史
/// - 从画布中移除
/// - 清除选中状态
/// - 包含异常处理
/// </remarks>
private void BorderImageDelete_MouseUp(object sender, MouseButtonEventArgs e)
{
try
@@ -1789,16 +2227,19 @@ namespace Ink_Canvas
// 记录删除历史
timeMachine.CommitElementRemoveHistory(currentSelectedElement);
var toRemove = currentSelectedElement;
// 从画布中移除
inkCanvas.Children.Remove(currentSelectedElement);
inkCanvas.Children.Remove(toRemove);
// 清除选中状态
UnselectElement(currentSelectedElement);
UnselectElement(toRemove);
currentSelectedElement = null;
// 恢复到删除前的编辑模式
inkCanvas.EditingMode = previousEditingMode;
SyncPdfPageSidebarWithCanvas();
LogHelper.WriteLogToFile($"图片删除完成,已恢复到编辑模式: {previousEditingMode}");
}
}
@@ -2029,7 +2470,7 @@ namespace Ink_Canvas
{
try
{
if (currentSelectedElement is Image image && sender is Ellipse ellipse)
if (IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse)
{
isResizingImage = true;
imageResizeStartPoint = e.GetPosition(inkCanvas);
@@ -2072,10 +2513,10 @@ namespace Ink_Canvas
{
try
{
if (isResizingImage && currentSelectedElement is Image image && sender is Ellipse ellipse)
if (isResizingImage && IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse)
{
var currentPoint = e.GetPosition(inkCanvas);
ResizeImageByHandle(image, imageResizeStartPoint, currentPoint, activeResizeHandle);
ResizeImageByHandle(currentSelectedElement, imageResizeStartPoint, currentPoint, activeResizeHandle);
imageResizeStartPoint = currentPoint;
e.Handled = true;
}
@@ -2087,11 +2528,11 @@ namespace Ink_Canvas
}
// 根据控制点缩放图片
private void ResizeImageByHandle(Image image, Point startPoint, Point currentPoint, string handleName)
private void ResizeImageByHandle(FrameworkElement element, Point startPoint, Point currentPoint, string handleName)
{
try
{
if (image.RenderTransform is TransformGroup transformGroup)
if (element.RenderTransform is TransformGroup transformGroup)
{
var scaleTransform = transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault();
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
@@ -2099,7 +2540,7 @@ namespace Ink_Canvas
if (scaleTransform == null || translateTransform == null) return;
// 获取图片的当前边界
Rect currentBounds = GetElementActualBounds(image);
Rect currentBounds = GetElementActualBounds(element);
double deltaX = currentPoint.X - startPoint.X;
double deltaY = currentPoint.Y - startPoint.Y;
@@ -2160,7 +2601,10 @@ namespace Ink_Canvas
translateTransform.Y += translateY;
// 更新选择点位置
UpdateImageResizeHandlesPosition(GetElementActualBounds(image));
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
UpdateImageSelectionToolbarPosition(element);
}
}
catch (Exception ex)
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,5 @@
using Ink_Canvas.Helpers;
using iNKORE.UI.WPF.Modern.Controls;
using System;
using System.Linq;
using System.Windows;
namespace Ink_Canvas
@@ -10,25 +8,14 @@ namespace Ink_Canvas
{
#region
/// <summary>
/// 初始化悬浮窗拦截管理器
/// </summary>
private void InitializeFloatingWindowInterceptor()
{
try
{
_floatingWindowInterceptorManager = new FloatingWindowInterceptorManager();
// 订阅事件
_floatingWindowInterceptorManager.WindowIntercepted += OnFloatingWindowIntercepted;
_floatingWindowInterceptorManager.WindowRestored += OnFloatingWindowRestored;
// 初始化拦截器
_floatingWindowInterceptorManager.Initialize(Settings.Automation.FloatingWindowInterceptor);
// 加载UI状态
LoadFloatingWindowInterceptorUI();
LogHelper.WriteLogToFile("悬浮窗拦截管理器初始化完成", LogHelper.LogType.Event);
}
catch (Exception ex)
@@ -37,83 +24,11 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 加载悬浮窗拦截UI状态
/// </summary>
private void LoadFloatingWindowInterceptorUI()
{
try
{
if (!isLoaded) return;
// 设置主开关状态
ToggleSwitchFloatingWindowInterceptorEnabled.IsOn = Settings.Automation.FloatingWindowInterceptor.IsEnabled;
// 设置各个拦截规则的状态
foreach (var kvp in Settings.Automation.FloatingWindowInterceptor.InterceptRules)
{
var toggleName = $"ToggleSwitch{kvp.Key}";
var toggle = FindName(toggleName) as ToggleSwitch;
if (toggle != null)
{
toggle.IsOn = kvp.Value;
}
}
// 更新UI可见性
UpdateFloatingWindowInterceptorUI();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载悬浮窗拦截UI状态失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 更新悬浮窗拦截UI
/// </summary>
private void UpdateFloatingWindowInterceptorUI()
{
try
{
var isEnabled = Settings.Automation.FloatingWindowInterceptor.IsEnabled;
FloatingWindowInterceptorGrid.Visibility = isEnabled ? Visibility.Visible : Visibility.Collapsed;
// 计算启用的规则数量
var enabledRulesCount = Settings.Automation.FloatingWindowInterceptor.InterceptRules.Where(kvp => kvp.Value).Count();
var totalRulesCount = Settings.Automation.FloatingWindowInterceptor.InterceptRules.Count;
// 更新状态文本
if (_floatingWindowInterceptorManager != null)
{
var stats = _floatingWindowInterceptorManager.GetStatistics();
TextBlockFloatingWindowInterceptorStatus.Text = stats.IsRunning
? $"拦截器运行中 - 已启用 {enabledRulesCount}/{totalRulesCount} 个规则"
: $"拦截器未启动 - 已启用 {enabledRulesCount}/{totalRulesCount} 个规则";
}
else
{
TextBlockFloatingWindowInterceptorStatus.Text = $"拦截器未初始化 - 已启用 {enabledRulesCount}/{totalRulesCount} 个规则";
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新悬浮窗拦截UI失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 窗口被拦截事件处理
/// </summary>
private void OnFloatingWindowIntercepted(object sender, FloatingWindowInterceptor.WindowInterceptedEventArgs e)
{
try
{
// 在UI线程中更新状态
Dispatcher.BeginInvoke(new Action(() =>
{
UpdateFloatingWindowInterceptorUI();
}));
Dispatcher.BeginInvoke(new Action(() => { }));
}
catch (Exception ex)
{
@@ -121,18 +36,11 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 窗口被恢复事件处理
/// </summary>
private void OnFloatingWindowRestored(object sender, FloatingWindowInterceptor.WindowRestoredEventArgs e)
{
try
{
// 在UI线程中更新状态
Dispatcher.BeginInvoke(new Action(() =>
{
UpdateFloatingWindowInterceptorUI();
}));
Dispatcher.BeginInvoke(new Action(() => { }));
}
catch (Exception ex)
{
@@ -144,30 +52,24 @@ namespace Ink_Canvas
#region
/// <summary>
/// 主开关切换事件
/// </summary>
private void ToggleSwitchFloatingWindowInterceptorEnabled_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
try
{
Settings.Automation.FloatingWindowInterceptor.IsEnabled = ToggleSwitchFloatingWindowInterceptorEnabled.IsOn;
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null)
Settings.Automation.FloatingWindowInterceptor.IsEnabled = toggle.IsOn;
if (_floatingWindowInterceptorManager != null)
{
if (Settings.Automation.FloatingWindowInterceptor.IsEnabled)
{
_floatingWindowInterceptorManager.Start();
}
else
{
_floatingWindowInterceptorManager.Stop();
}
}
UpdateFloatingWindowInterceptorUI();
SaveSettingsToFile();
}
catch (Exception ex)
@@ -176,129 +78,98 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 希沃白板3拦截开关
/// </summary>
private void ToggleSwitchSeewoWhiteboard3Floating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard3Floating, ToggleSwitchSeewoWhiteboard3Floating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard3Floating, toggle.IsOn);
}
/// <summary>
/// 希沃白板5拦截开关
/// </summary>
private void ToggleSwitchSeewoWhiteboard5Floating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5Floating, ToggleSwitchSeewoWhiteboard5Floating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5Floating, toggle.IsOn);
}
/// <summary>
/// 希沃白板5C拦截开关
/// </summary>
private void ToggleSwitchSeewoWhiteboard5CFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5CFloating, ToggleSwitchSeewoWhiteboard5CFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5CFloating, toggle.IsOn);
}
/// <summary>
/// 希沃品课侧栏拦截开关
/// </summary>
private void ToggleSwitchSeewoPincoSideBarFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoSideBarFloating, ToggleSwitchSeewoPincoSideBarFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoSideBarFloating, toggle.IsOn);
}
/// <summary>
/// 希沃品课画笔拦截开关
/// </summary>
private void ToggleSwitchSeewoPincoDrawingFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoDrawingFloating, ToggleSwitchSeewoPincoDrawingFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoDrawingFloating, toggle.IsOn);
}
/// <summary>
/// 希沃PPT小工具拦截开关
/// </summary>
private void ToggleSwitchSeewoPPTFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPPTFloating, ToggleSwitchSeewoPPTFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPPTFloating, toggle.IsOn);
}
/// <summary>
/// AiClass拦截开关
/// </summary>
private void ToggleSwitchAiClassFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.AiClassFloating, ToggleSwitchAiClassFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.AiClassFloating, toggle.IsOn);
}
/// <summary>
/// 鸿合屏幕书写拦截开关
/// </summary>
private void ToggleSwitchHiteAnnotationFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.HiteAnnotationFloating, ToggleSwitchHiteAnnotationFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.HiteAnnotationFloating, toggle.IsOn);
}
/// <summary>
/// 畅言智慧课堂拦截开关
/// </summary>
private void ToggleSwitchChangYanFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanFloating, ToggleSwitchChangYanFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanFloating, toggle.IsOn);
}
/// <summary>
/// 畅言PPT拦截开关
/// </summary>
private void ToggleSwitchChangYanPptFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanPptFloating, ToggleSwitchChangYanPptFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanPptFloating, toggle.IsOn);
}
/// <summary>
/// 天喻教育云拦截开关
/// </summary>
private void ToggleSwitchIntelligentClassFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.IntelligentClassFloating, ToggleSwitchIntelligentClassFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.IntelligentClassFloating, toggle.IsOn);
}
/// <summary>
/// 希沃桌面画笔拦截开关
/// </summary>
private void ToggleSwitchSeewoDesktopAnnotationFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopAnnotationFloating, ToggleSwitchSeewoDesktopAnnotationFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopAnnotationFloating, toggle.IsOn);
}
/// <summary>
/// 希沃桌面侧栏拦截开关
/// </summary>
private void ToggleSwitchSeewoDesktopSideBarFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopSideBarFloating, ToggleSwitchSeewoDesktopSideBarFloating.IsOn);
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopSideBarFloating, toggle.IsOn);
}
/// <summary>
/// 设置拦截规则
/// </summary>
/// <param name="type">拦截类型</param>
/// <param name="enabled">是否启用拦截</param>
private void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
public void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
{
try
{
@@ -307,18 +178,15 @@ namespace Ink_Canvas
_floatingWindowInterceptorManager.SetInterceptRule(type, enabled);
}
// 更新设置
var ruleName = type.ToString();
if (Settings.Automation.FloatingWindowInterceptor.InterceptRules.ContainsKey(ruleName))
{
Settings.Automation.FloatingWindowInterceptor.InterceptRules[ruleName] = enabled;
}
// 获取规则信息以处理父子关系
var rule = _floatingWindowInterceptorManager?.GetInterceptRule(type);
if (rule != null)
{
// 如果是父规则,更新所有子规则的设置
if (rule.ChildTypes.Count > 0)
{
foreach (var childType in rule.ChildTypes)
@@ -330,7 +198,6 @@ namespace Ink_Canvas
}
}
}
// 如果是子规则,更新父规则的设置
else if (rule.ParentType.HasValue)
{
var parentRule = _floatingWindowInterceptorManager?.GetInterceptRule(rule.ParentType.Value);
@@ -345,9 +212,6 @@ namespace Ink_Canvas
}
}
// 更新UI显示
UpdateFloatingWindowInterceptorUI();
SaveSettingsToFile();
}
catch (Exception ex)
@@ -357,4 +221,4 @@ namespace Ink_Canvas
}
#endregion
}
}
}

Some files were not shown because too many files have changed in this diff Show More