Compare commits

...

363 Commits

Author SHA1 Message Date
CJK_mkp 3a917d529d Add Star History section to README
Added a Star History section with a chart to README.
2026-05-02 17:48:04 +08:00
CJKmkp e45e8fad43 improve:UI 2026-05-02 17:36:05 +08:00
CJKmkp 8364dde2e3 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-02 17:27:53 +08:00
CJKmkp 6105f8759d improve:OOBE 2026-05-02 17:26:40 +08:00
PrefacedCorg c8e3bceab2 fix(UI): 优化弹出菜单位置和层级管理
修复弹出菜单在移动时位置不更新的问题,添加节流机制避免频繁刷新
使用Win32 API强制刷新弹出菜单位置并提升到最顶层
调整浮动工具栏和弹出菜单的样式和位置
移除未使用的字体样式定义
2026-05-02 17:06:08 +08:00
CJKmkp f825211987 add:issue #115 2026-05-02 16:22:27 +08:00
CJKmkp aea4c2ce3c add:issue #115 2026-05-02 16:18:53 +08:00
PrefacedCorg 35c8e980f8 refactor(工具栏): 将BorderTools从Border改为Popup控件并优化显示逻辑
重构工具栏中的BorderTools控件,将其从Border改为Popup控件,并添加自定义位置回调
优化显示/隐藏逻辑,使用IsOpen属性替代Visibility控制
更新相关动画效果调用为对应的Popup版本
调整UI样式和布局,提升视觉一致性
2026-05-02 15:55:37 +08:00
PrefacedCorg 09713f70bf add:按钮添加圆角和阴影 2026-05-02 15:34:18 +08:00
PrefacedCorg ed7157163c Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-02 15:34:02 +08:00
PrefacedCorg 86fdc04616 add:鸿蒙SC字体 2026-05-02 15:33:25 +08:00
CJKmkp ed44a22edb Revert "improve:悬浮窗拦截"
This reverts commit 81b291f2e6.
2026-05-02 15:23:38 +08:00
CJKmkp 2c4d6f124e Revert "improve:悬浮窗拦截"
This reverts commit f39d0c0f82.
2026-05-02 15:23:36 +08:00
CJKmkp 2664ee812c Revert "improve:悬浮窗拦截"
This reverts commit b8b07f90cd.
2026-05-02 15:23:33 +08:00
CJKmkp b8b07f90cd improve:悬浮窗拦截 2026-05-02 15:19:55 +08:00
CJKmkp f39d0c0f82 improve:悬浮窗拦截 2026-05-02 15:16:59 +08:00
CJKmkp 81b291f2e6 improve:悬浮窗拦截 2026-05-02 14:52:46 +08:00
PrefacedCorg a25ea95c19 style(MainWindow): 调整工具栏边框样式和边距
更新工具栏边框的厚度为1像素并优化边距设置,提升视觉一致性
2026-05-02 13:52:19 +08:00
PrefacedCorg 16724fd0ae Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-02 13:41:08 +08:00
PrefacedCorg 723c0b9cdc feat(工具栏设置): 添加工具栏配置页面及功能实现
实现工具栏配置页面,允许用户调整工具栏按钮的顺序和可见性
包含主工具栏、画布控制和尾部按钮三个区域的配置
支持恢复默认布局功能
2026-05-02 13:41:02 +08:00
CJKmkp 39e098221c Improve:设置启动 2026-05-02 12:36:36 +08:00
CJKmkp 005ba66bc2 improve:UI 2026-05-02 12:33:38 +08:00
CJKmkp d15db2d8a9 improve:UI 2026-05-02 12:21:13 +08:00
CJKmkp 191fd25c66 improve:手写纠正 2026-05-02 12:10:48 +08:00
CJKmkp 5f1f190613 improve:PPT增强预览 2026-05-02 12:07:32 +08:00
CJKmkp 7736d88657 improve:UI 2026-05-02 10:42:11 +08:00
CJKmkp cc5ba1364c improve:UI 2026-05-02 10:35:41 +08:00
CJKmkp fcbfb326c0 improve:UI 2026-05-02 10:13:13 +08:00
CJKmkp d6c1a1fd71 improve:UI 2026-05-02 10:12:36 +08:00
CJKmkp 8db0e8c95c improve:计时器 2026-05-02 10:07:49 +08:00
CJKmkp 860dc43c8d improve:UI 2026-05-02 10:06:21 +08:00
CJKmkp be34eda535 improve:自动更新 2026-05-02 09:54:51 +08:00
CJKmkp 2d9901791c improve:PPT增强预览 2026-05-02 09:53:41 +08:00
CJKmkp ef9b211677 improve:主题切换 2026-05-02 09:51:34 +08:00
CJKmkp 53fda7e512 improve:UIA 2026-05-02 09:50:28 +08:00
CJKmkp bbb2981208 improve:自定义文件名 2026-05-02 09:49:19 +08:00
CJKmkp e1aa003b77 improve:ROT模块 2026-05-02 09:47:43 +08:00
CJKmkp b7e88371ac improve:自动更新 2026-05-02 09:44:04 +08:00
CJKmkp 2b8dcfdc86 improve:图片选中 2026-05-02 09:41:28 +08:00
CJKmkp 9691263aa5 improve:图片选中 2026-05-02 09:23:19 +08:00
CJKmkp 95ee002765 improve:图片选中 2026-05-02 09:17:15 +08:00
CJKmkp 786945f6c8 improve:UI 2026-05-02 09:01:22 +08:00
CJKmkp 7a7c5f266d add:放大镜 2026-05-02 09:00:17 +08:00
CJKmkp 15884c5901 add:放大镜 2026-05-02 08:28:49 +08:00
PrefacedCorg 16f53acf42 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-02 01:44:24 +08:00
PrefacedCorg 2e61777469 refactor(UI): 优化更多功能标题栏的布局和图标
使用 Grid 布局替代 SimpleStackPanel 以提升布局灵活性
将关闭图标替换为 Segoe Fluent 图标以保持风格一致
2026-05-02 01:43:43 +08:00
CJKmkp 46f4a0523c improve:PPT侧面板 2026-05-02 01:17:09 +08:00
CJKmkp c8848f6cde Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-02 01:03:13 +08:00
PrefacedCorg 1fe66790e6 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-02 01:01:44 +08:00
PrefacedCorg dada05aabb feat(浮动工具栏): 添加退出按钮并优化UI样式
- 在浮动工具栏中添加退出按钮的本地化文本
- 移除未使用的BoardMenuFrame相关动画代码
- 调整浮动工具栏中元素的边距和圆角样式
- 将退出按钮文本改为使用i18n资源
2026-05-02 00:56:58 +08:00
CJKmkp a18d476415 add:基于IPC的IACore在net6的实现 2026-05-02 00:53:49 +08:00
CJKmkp 94142ec8a5 add:基于IPC的IACore在net6的实现 2026-05-02 00:40:49 +08:00
CJKmkp fff52aa282 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-02 00:06:12 +08:00
PrefacedCorg 07ebbfbd24 refactor: 移除 BoardMenuFrame 控件并内联实现图像选项面板
将 BoardMenuFrame 自定义控件及其样式从项目中移除,改为直接在 MainWindow.xaml 中内联实现图像选项面板的布局和样式,以简化代码结构并减少自定义控件的使用
2026-05-01 23:54:31 +08:00
CJKmkp 2a17ea1bd1 add:基于IPC的IACore在net6的实现 2026-05-01 23:54:24 +08:00
PrefacedCorg e801394dbe Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 23:04:08 +08:00
PrefacedCorg 9fb18e020e ```
fix(ui): 修复画布界面元素布局和间距问题

- 修复背景设置面板的位置偏移问题,调整Margin值从-115,-360,-55,50到-189,-360,-55,50
- 为各个工具面板添加设计时可见性属性(d:Visibility="Visible")以便预览
- 修复多个堆叠面板的Spacing属性,将负值改为正值以正确显示元素间距
- 在各个工具菜单区域添加中文注释标识(如<!--笔菜单-->, <!--橡皮菜单-->等)
- 调整了工具面板中的Spacing属性从-2到2,解决元素重叠问题
```
2026-05-01 23:03:20 +08:00
CJKmkp 82ec0ad1cd improve:ROT模块 2026-05-01 22:32:00 +08:00
doudou0720 3853fd31f4 chore(MW_PPT): 修正PowerPoint守护监控的docstring
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-05-01 22:10:34 +08:00
CJKmkp 41a59136c7 refactor(主题): 将主题相关逻辑提取到ThemeHelper类中 2026-05-01 22:05:30 +08:00
CJKmkp 8c7eeb51d0 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 22:00:14 +08:00
CJKmkp 4f5f3ed8fe improve:UI 2026-05-01 21:58:09 +08:00
PrefacedCorg 207321651d Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 21:54:25 +08:00
PrefacedCorg 267d9b4450 refactor(主题): 将主题相关逻辑提取到ThemeHelper类中
重构主题相关代码,将重复的IsSystemThemeLight方法和主题应用逻辑提取到新的ThemeHelper工具类中
简化多个窗口的主题处理代码,统一使用ThemeHelper进行主题管理
2026-05-01 21:54:15 +08:00
CJKmkp 31aee3bd9a improve:设置索引 2026-05-01 21:38:53 +08:00
CJKmkp a8c9734a5a improve:UI 2026-05-01 21:28:54 +08:00
CJKmkp 138484ac19 improve:启动速度 2026-05-01 21:04:09 +08:00
CJKmkp 1217ef7ef9 优化代码 2026-05-01 20:38:59 +08:00
CJKmkp 02cf10ab13 优化代码 2026-05-01 20:37:39 +08:00
CJKmkp db181d994a 优化代码 2026-05-01 20:32:01 +08:00
CJKmkp ee4e9032f2 build(installer): 修改安装配置并添加.NET 6运行时安装选项 2026-05-01 20:28:36 +08:00
CJKmkp 54fd9bcde6 improve:UI 2026-05-01 20:21:29 +08:00
doudou0720 6fbd8b6fac build(installer): 修改安装配置并添加.NET 6运行时安装选项
- 将默认安装目录改为用户本地应用数据目录
- 添加.NET 6运行时下载和安装任务
- 优化文件复制方式,使用通配符递归复制

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-05-01 19:58:23 +08:00
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
PrefacedCorg 9279783fc3 Revert "新设置"
This reverts commit 140e92eeda.
2026-04-04 22:26:32 +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
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
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
PrefacedCorg 20441543f0 Merge pull request #420 from InkCanvasForClass/beta
...
2026-03-28 22:05:28 +08:00
310 changed files with 35879 additions and 51255 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"
}
+3 -3
View File
@@ -2,7 +2,7 @@ name: .NET Build & Package
on:
push:
branches: [ main, beta ]
branches: [ net6 ]
workflow_dispatch:
concurrency:
@@ -46,7 +46,7 @@ jobs:
- name: Check if exe file is generated
id: check-exe
run: |
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\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,7 +74,7 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: InkCanvasForClass.CE.debug.${{ matrix.architecture }}
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net472/*"
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
- name: Create Summary
if: always()
+3 -3
View File
@@ -3,7 +3,7 @@ name: PR Check
on:
pull_request:
types: [opened, synchronize]
branches: [ main, beta ]
branches: [ main, net6 ]
paths-ignore:
- '**/*.md'
@@ -43,7 +43,7 @@ jobs:
- name: Check if exe file is generated
id: check-exe
run: |
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\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
@@ -71,7 +71,7 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: InkCanvasForClass.CE.debug.${{ matrix.architecture }}
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net472/*"
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
- name: Create Summary
+4 -3
View File
@@ -236,7 +236,7 @@ jobs:
- name: Check if exe file is generated
id: check-exe
run: |
$exePath = "Ink Canvas\bin\Release\${{ matrix.architecture }}\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
@@ -284,7 +284,7 @@ jobs:
New-Item -ItemType Directory -Path "release" -Force
# 复制发布文件(使用架构特定的路径)
Copy-Item "Ink Canvas\bin\Release\$architecture\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
@@ -305,6 +305,7 @@ jobs:
$issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`""
# 替换源文件路径为相对路径(考虑到ISS文件在build目录下,需要返回上级目录)
$issContent = $issContent -replace 'Source: "release\\\*";', 'Source: "../release/*";'
$issContent = $issContent -replace 'Source: ".*\\{#MyAppExeName}";', 'Source: "../release/{#MyAppExeName}";'
$issContent = $issContent -replace 'Source: ".*\\InkCanvasForClass.exe.config";', 'Source: "../release/InkCanvasForClass.exe.config";'
@@ -750,5 +751,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
+69 -2
View File
@@ -1,9 +1,16 @@
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.IACoreHelper", "InkCanvas.IACoreHelper\InkCanvas.IACoreHelper.csproj", "{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -38,6 +45,66 @@ Global
{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
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|Any CPU.ActiveCfg = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|Any CPU.Build.0 = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|ARM.ActiveCfg = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|ARM.Build.0 = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|ARM64.ActiveCfg = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|ARM64.Build.0 = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|x64.ActiveCfg = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|x64.Build.0 = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|x86.ActiveCfg = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|x86.Build.0 = Debug|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|Any CPU.ActiveCfg = Release|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|Any CPU.Build.0 = Release|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|ARM.ActiveCfg = Release|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|ARM.Build.0 = Release|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|ARM64.ActiveCfg = Release|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|ARM64.Build.0 = Release|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|x64.ActiveCfg = Release|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|x64.Build.0 = Release|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|x86.ActiveCfg = Release|x86
{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+35 -6
View File
@@ -1,16 +1,15 @@
<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"
>
<Application.Resources>
<ResourceDictionary>
<Style TargetType="ui:ScrollViewerEx">
<EventSetter Event="PreviewMouseWheel" Handler="ScrollViewer_PreviewMouseWheel"/>
</Style>
<FontFamily x:Key="HarmonyOSFont">./Resources/Fonts/#HarmonyOS Sans SC</FontFamily>
<ContextMenu Opened="SysTrayMenu_Opened" Closed="SysTrayMenu_Closed" x:Shared="false" x:Key="SysTrayMenu" Padding="6" ui:ThemeManager.RequestedTheme="Light">
<MenuItem IsCheckable="True" IsChecked="False" Checked="HideICCMainWindowTrayIconMenuItem_Checked" Unchecked="HideICCMainWindowTrayIconMenuItem_UnChecked" Name="HideICCMainWindowTrayIconMenuItem">
<MenuItem.Header>
@@ -32,6 +31,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 +261,7 @@
ToolTipText="InkCanvasForClass"
ContextMenu="{StaticResource SysTrayMenu}"
IconSource="/Resources/icc.ico"/>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources/>
<ui:XamlControlsResources />
<ResourceDictionary Source="Resources/SeewoImageDictionary.xaml"/>
+326 -150
View File
@@ -1,5 +1,6 @@
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;
@@ -7,6 +8,7 @@ using Newtonsoft.Json;
using Sentry;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -17,6 +19,7 @@ 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;
@@ -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,24 @@ 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))
{
ShowSplashScreen();
SetSplashMessage(Strings.GetString("Splash_Starting"));
SetSplashProgress(20);
await Task.Delay(500);
SetSplashProgress(25);
// 强制刷新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));
@@ -739,64 +814,13 @@ namespace Ink_Canvas
LogHelper.WriteLogToFile("App | 检测到最终应用启动(更新后的应用)");
}
// 释放IACore相关DLL
if (_isSplashScreenShown)
{
SetSplashMessage("正在初始化组件...");
SetSplashProgress(40);
await Task.Delay(500);
}
try
{
IACoreDllExtractor.ExtractIACoreDlls();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放IACore DLL时出错: {ex.Message}", LogHelper.LogType.Error);
}
// 释放UIAccess DLL
if (_isSplashScreenShown)
{
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);
SetSplashProgress(50);
await Task.Delay(100);
}
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);
@@ -831,7 +855,7 @@ namespace Ink_Canvas
LogHelper.WriteLogToFile($"App | 清理更新标记文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
Task.Run(async () =>
_ = Task.Run(async () =>
{
try
{
@@ -1068,39 +1092,59 @@ namespace Ink_Canvas
if (_isSplashScreenShown)
{
SetSplashMessage("正在初始化主界面...");
SetSplashProgress(80);
await Task.Delay(500);
SetSplashProgress(75);
}
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("完成初始化...");
SetSplashProgress(80);
Task.Delay(300).ContinueWith(_ =>
SetSplashMessage("启动完成!");
SetSplashProgress(100);
Task.Delay(100).ContinueWith(_ =>
{
Dispatcher.Invoke(() =>
{
SetSplashMessage("启动完成!");
SetSplashProgress(100);
// 延迟关闭启动画面,让用户看到完成消息
Task.Delay(500).ContinueWith(__ =>
Task.Delay(100).ContinueWith(__ =>
{
Dispatcher.Invoke(() => CloseSplashScreen());
});
@@ -1110,6 +1154,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));
@@ -1117,7 +1174,7 @@ namespace Ink_Canvas
{
LogHelper.WriteLogToFile($"App | 处理启动URI参数: {startupUriArg}", LogHelper.LogType.Event);
// 延迟一点执行,确保窗口初始化完成
Task.Delay(1000).ContinueWith(_ =>
_ = Task.Delay(1000).ContinueWith(_ =>
{
mainWindow.Dispatcher.Invoke(() =>
{
@@ -1126,40 +1183,139 @@ 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
{
var shapeMode = ShapeRecognitionRouter.FromSettingsInt(
Ink_Canvas.Windows.SettingsViews.Helpers.SettingsManager.Settings?.InkToShape?.ShapeRecognitionEngine ?? 0);
if (!ShapeRecognitionRouter.ResolveUseWinRt(shapeMode))
{
LogHelper.WriteLogToFile("启动 IACore IPC 辅助进程");
bool ipcStarted = IpcIACoreClient.Instance.Start();
LogHelper.WriteLogToFile($"IACore IPC 辅助进程{(ipcStarted ? "" : "")}");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动 IACore IPC 辅助进程时出错: {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)
@@ -1436,6 +1592,26 @@ namespace Ink_Canvas
private void App_Exit(object sender, ExitEventArgs e)
{
CleanupTerminationMonitoring();
try
{
IpcIACoreClient.Instance.Dispose();
}
catch { }
// 卸载所有插件
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.9")]
[assembly: AssemblyFileVersion("1.7.18.9")]
[assembly: AssemblyVersion("1.7.18.10")]
[assembly: AssemblyFileVersion("1.7.18.10")]
@@ -0,0 +1,60 @@
<UserControl x:Class="Ink_Canvas.Controls.ImageSelectionOverlay"
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"
mc:Ignorable="d"
HorizontalAlignment="Left" VerticalAlignment="Top"
Background="{x:Null}"
IsHitTestVisible="True">
<Canvas x:Name="OverlayRoot" Background="Transparent" IsHitTestVisible="True" ClipToBounds="False">
<Rectangle x:Name="MoveSurface"
Fill="Transparent"
Cursor="SizeAll"
IsHitTestVisible="True" />
<Rectangle x:Name="FrameBorder"
Stroke="#22D3A9"
StrokeThickness="1.5"
Fill="Transparent"
IsHitTestVisible="False"
SnapsToDevicePixels="True" />
<Line x:Name="RotationLine"
Stroke="#22D3A9"
StrokeThickness="1.5"
IsHitTestVisible="False"
SnapsToDevicePixels="True" />
<Ellipse x:Name="RotationHandle"
Width="14" Height="14"
Fill="#22D3A9"
Stroke="White"
StrokeThickness="2"
Cursor="Hand" />
<Ellipse x:Name="TopLeftHandle"
Width="12" Height="12"
Fill="White"
Stroke="#22D3A9"
StrokeThickness="1.5"
Cursor="SizeNWSE" />
<Ellipse x:Name="TopRightHandle"
Width="12" Height="12"
Fill="White"
Stroke="#22D3A9"
StrokeThickness="1.5"
Cursor="SizeNESW" />
<Ellipse x:Name="BottomLeftHandle"
Width="12" Height="12"
Fill="White"
Stroke="#22D3A9"
StrokeThickness="1.5"
Cursor="SizeNESW" />
<Ellipse x:Name="BottomRightHandle"
Width="12" Height="12"
Fill="White"
Stroke="#22D3A9"
StrokeThickness="1.5"
Cursor="SizeNWSE" />
</Canvas>
</UserControl>
@@ -0,0 +1,260 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Ink_Canvas.Controls
{
public enum ImageResizeCorner
{
TopLeft,
TopRight,
BottomLeft,
BottomRight
}
public class ImageResizeDeltaEventArgs : EventArgs
{
public ImageResizeCorner Corner { get; }
public Vector CanvasDelta { get; }
public bool LockAspectRatio { get; }
public ImageResizeDeltaEventArgs(ImageResizeCorner corner, Vector canvasDelta, bool lockAspect)
{
Corner = corner;
CanvasDelta = canvasDelta;
LockAspectRatio = lockAspect;
}
}
public class ImageMoveDeltaEventArgs : EventArgs
{
public Vector CanvasDelta { get; }
public ImageMoveDeltaEventArgs(Vector delta) { CanvasDelta = delta; }
}
public class ImageRotateDeltaEventArgs : EventArgs
{
public double AngleDelta { get; }
public ImageRotateDeltaEventArgs(double angleDelta) { AngleDelta = angleDelta; }
}
public partial class ImageSelectionOverlay : UserControl
{
private const double HandleSize = 12;
private const double RotationHandleSize = 14;
private const double RotationHandleOffset = 28;
public event EventHandler<ImageResizeDeltaEventArgs> ResizeDelta;
public event EventHandler<ImageMoveDeltaEventArgs> MoveDelta;
public event EventHandler<ImageRotateDeltaEventArgs> RotateDelta;
public event EventHandler InteractionStarted;
public event EventHandler InteractionEnded;
public IInputElement CoordinateSource { get; set; }
private Point _rotationCenterCanvas;
private readonly RotateTransform _overlayRotation = new RotateTransform(0);
private bool _isResizing;
private bool _isRotating;
private bool _isMoving;
private ImageResizeCorner _activeCorner;
private Point _lastPoint;
private double _lastRotationAngle;
public ImageSelectionOverlay()
{
InitializeComponent();
RenderTransform = _overlayRotation;
TopLeftHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopLeft, e, TopLeftHandle);
TopRightHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopRight, e, TopRightHandle);
BottomLeftHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.BottomLeft, e, BottomLeftHandle);
BottomRightHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.BottomRight, e, BottomRightHandle);
TopLeftHandle.MouseMove += ResizeMove;
TopRightHandle.MouseMove += ResizeMove;
BottomLeftHandle.MouseMove += ResizeMove;
BottomRightHandle.MouseMove += ResizeMove;
TopLeftHandle.MouseLeftButtonUp += EndResize;
TopRightHandle.MouseLeftButtonUp += EndResize;
BottomLeftHandle.MouseLeftButtonUp += EndResize;
BottomRightHandle.MouseLeftButtonUp += EndResize;
RotationHandle.MouseLeftButtonDown += BeginRotate;
RotationHandle.MouseMove += RotateMove;
RotationHandle.MouseLeftButtonUp += EndRotate;
MoveSurface.MouseLeftButtonDown += BeginMove;
MoveSurface.MouseMove += MoveMove;
MoveSurface.MouseLeftButtonUp += EndMove;
}
/// <summary>
/// Position overlay so its logical rect (width × height) is centered at centerCanvas,
/// then rotated by rotationAngleDegrees around that center to match the target element.
/// </summary>
public void UpdateFrame(Point centerCanvas, double width, double height, double rotationAngleDegrees)
{
if (width <= 0 || height <= 0) return;
_rotationCenterCanvas = centerCanvas;
double left = centerCanvas.X - width / 2;
double top = centerCanvas.Y - height / 2;
Margin = new Thickness(left, top, 0, 0);
Width = width;
Height = height;
RenderTransformOrigin = new Point(0, 0);
_overlayRotation.Angle = rotationAngleDegrees;
_overlayRotation.CenterX = width / 2;
_overlayRotation.CenterY = height / 2;
FrameBorder.Width = width;
FrameBorder.Height = height;
System.Windows.Controls.Canvas.SetLeft(FrameBorder, 0);
System.Windows.Controls.Canvas.SetTop(FrameBorder, 0);
MoveSurface.Width = width;
MoveSurface.Height = height;
System.Windows.Controls.Canvas.SetLeft(MoveSurface, 0);
System.Windows.Controls.Canvas.SetTop(MoveSurface, 0);
double h = HandleSize / 2;
System.Windows.Controls.Canvas.SetLeft(TopLeftHandle, -h);
System.Windows.Controls.Canvas.SetTop(TopLeftHandle, -h);
System.Windows.Controls.Canvas.SetLeft(TopRightHandle, width - h);
System.Windows.Controls.Canvas.SetTop(TopRightHandle, -h);
System.Windows.Controls.Canvas.SetLeft(BottomLeftHandle, -h);
System.Windows.Controls.Canvas.SetTop(BottomLeftHandle, height - h);
System.Windows.Controls.Canvas.SetLeft(BottomRightHandle, width - h);
System.Windows.Controls.Canvas.SetTop(BottomRightHandle, height - h);
double rh = RotationHandleSize / 2;
double midX = width / 2;
System.Windows.Controls.Canvas.SetLeft(RotationHandle, midX - rh);
System.Windows.Controls.Canvas.SetTop(RotationHandle, -RotationHandleOffset - rh);
RotationLine.X1 = midX;
RotationLine.Y1 = 0;
RotationLine.X2 = midX;
RotationLine.Y2 = -RotationHandleOffset;
}
private IInputElement GetSource() => CoordinateSource ?? (IInputElement)Parent;
private void BeginResize(ImageResizeCorner corner, MouseButtonEventArgs e, Ellipse handle)
{
var source = GetSource();
if (source == null) return;
_isResizing = true;
_activeCorner = corner;
_lastPoint = e.GetPosition(source);
handle.CaptureMouse();
InteractionStarted?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void ResizeMove(object sender, MouseEventArgs e)
{
if (!_isResizing || !(sender is Ellipse handle) || !handle.IsMouseCaptured) return;
var source = GetSource();
if (source == null) return;
var current = e.GetPosition(source);
var delta = current - _lastPoint;
bool lockAspect = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
ResizeDelta?.Invoke(this, new ImageResizeDeltaEventArgs(_activeCorner, delta, lockAspect));
_lastPoint = current;
e.Handled = true;
}
private void EndResize(object sender, MouseButtonEventArgs e)
{
if (!_isResizing) return;
if (sender is Ellipse handle) handle.ReleaseMouseCapture();
_isResizing = false;
InteractionEnded?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void BeginRotate(object sender, MouseButtonEventArgs e)
{
var source = GetSource();
if (source == null) return;
_isRotating = true;
var p = e.GetPosition(source);
_lastRotationAngle = AngleFromCenter(p);
RotationHandle.CaptureMouse();
InteractionStarted?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void RotateMove(object sender, MouseEventArgs e)
{
if (!_isRotating || !RotationHandle.IsMouseCaptured) return;
var source = GetSource();
if (source == null) return;
var p = e.GetPosition(source);
double angle = AngleFromCenter(p);
double delta = angle - _lastRotationAngle;
if (delta > 180) delta -= 360;
else if (delta < -180) delta += 360;
_lastRotationAngle = angle;
RotateDelta?.Invoke(this, new ImageRotateDeltaEventArgs(delta));
e.Handled = true;
}
private void EndRotate(object sender, MouseButtonEventArgs e)
{
if (!_isRotating) return;
RotationHandle.ReleaseMouseCapture();
_isRotating = false;
InteractionEnded?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void BeginMove(object sender, MouseButtonEventArgs e)
{
var source = GetSource();
if (source == null) return;
_isMoving = true;
_lastPoint = e.GetPosition(source);
MoveSurface.CaptureMouse();
InteractionStarted?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void MoveMove(object sender, MouseEventArgs e)
{
if (!_isMoving || !MoveSurface.IsMouseCaptured) return;
var source = GetSource();
if (source == null) return;
var current = e.GetPosition(source);
var delta = current - _lastPoint;
_lastPoint = current;
MoveDelta?.Invoke(this, new ImageMoveDeltaEventArgs(delta));
e.Handled = true;
}
private void EndMove(object sender, MouseButtonEventArgs e)
{
if (!_isMoving) return;
MoveSurface.ReleaseMouseCapture();
_isMoving = false;
InteractionEnded?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private double AngleFromCenter(Point p)
{
double dx = p.X - _rotationCenterCanvas.X;
double dy = p.Y - _rotationCenterCanvas.Y;
return Math.Atan2(dy, dx) * 180.0 / Math.PI;
}
}
}
+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>
+392
View File
@@ -0,0 +1,392 @@
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;
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,25 @@
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
public interface IToolbarItem
{
string Id { get; }
ToolbarSlot DefaultSlot { get; }
int DefaultOrder { get; }
bool DefaultVisible { get; }
ToolbarInsertPosition DefaultPosition { get; }
string DefaultAnchorName { get; }
string DisplayName { get; }
string MenuPanelName { get; }
FrameworkElement BuildView(IToolbarHost host);
}
}
@@ -0,0 +1,27 @@
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 string LabelBrushResourceKey => "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,19 @@
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;
public override string MenuPanelName => "EraserSizePanel";
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,19 @@
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;
public override string MenuPanelName => "PenPalette";
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,19 @@
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;
public override string MenuPanelName => "BorderDrawShape";
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,50 @@
using Ink_Canvas.Properties;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Ink_Canvas.Controls.Toolbar.Items
{
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;
public string DisplayName => Strings.GetString(LocalizationKey) ?? LocalizationKey;
public virtual string MenuPanelName => null;
protected virtual string IconBrushResourceKey => null;
protected virtual string LabelBrushResourceKey => null;
protected abstract void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e);
protected virtual void AfterBuild(IToolbarHost host, ToolbarImageButton view) { }
public FrameworkElement BuildView(IToolbarHost host)
{
var btn = new ToolbarImageButton
{
Label = Strings.GetString(LocalizationKey) ?? LocalizationKey,
Tag = "ToolbarRegistryInjected"
};
if (!string.IsNullOrEmpty(IconBrushResourceKey))
{
if (btn.TryFindResource(IconBrushResourceKey) is Brush brush) btn.IconBrush = brush;
else btn.SetResourceReference(ToolbarImageButton.IconBrushProperty, IconBrushResourceKey);
}
if (!string.IsNullOrEmpty(LabelBrushResourceKey))
{
if (btn.TryFindResource(LabelBrushResourceKey) is Brush brush) btn.LabelBrush = brush;
else btn.SetResourceReference(ToolbarImageButton.LabelBrushProperty, LabelBrushResourceKey);
}
btn.ButtonMouseUp += (s, e) => OnClick(host, s, e);
AfterBuild(host, btn);
return btn;
}
}
}
@@ -0,0 +1,21 @@
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";
public override string MenuPanelName => "BorderTools";
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,209 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
namespace Ink_Canvas.Controls.Toolbar
{
public static class ToolbarRegistry
{
private static List<IToolbarItem> _items;
internal const string InjectedTag = "ToolbarRegistryInjected";
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();
LogHelper.WriteLogToFile($"ToolbarRegistry: Discover 完成, 发现 {_items.Count} 个条目", LogHelper.LogType.Info);
return _items;
}
public static void ClearInjected(Panel container)
{
if (container == null) return;
var toRemove = container.Children.OfType<FrameworkElement>()
.Where(e => e.Tag as string == InjectedTag)
.ToList();
foreach (var element in toRemove)
container.Children.Remove(element);
LogHelper.WriteLogToFile($"ToolbarRegistry: ClearInjected 清除 {toRemove.Count} 个元素 [{container.Name}]", LogHelper.LogType.Info);
}
public static void Populate(IToolbarHost host, IDictionary<ToolbarSlot, Panel> slots, ToolbarLayoutSettings layout)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: Populate 开始", LogHelper.LogType.Info);
if (host == null || slots == null) { LogHelper.WriteLogToFile("ToolbarRegistry: Populate host 或 slots 为空", LogHelper.LogType.Warning); 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));
}
LogHelper.WriteLogToFile($"ToolbarRegistry: 分组完成, {grouped.Count} 个 slot 有可见条目", LogHelper.LogType.Info);
foreach (var kv in grouped)
{
if (!slots.TryGetValue(kv.Key, out var container) || container == null) continue;
LogHelper.WriteLogToFile($"ToolbarRegistry: 注入到 {kv.Key}, 条目数={kv.Value.Count}", LogHelper.LogType.Info);
InjectIntoContainer(host, container, kv.Value);
}
ApplyMenuVisibility(host, layout);
LogHelper.WriteLogToFile($"ToolbarRegistry: Populate 完成", LogHelper.LogType.Info);
}
public static void ApplyMenuVisibility(IToolbarHost host, ToolbarLayoutSettings layout)
{
if (host == null || layout == null) return;
foreach (var item in Discover())
{
if (string.IsNullOrEmpty(item.MenuPanelName)) continue;
bool visible = true;
if (layout.Items.TryGetValue(item.Id, out var cfg))
visible = cfg.Visible;
try
{
var menuElement = host.Window.FindName(item.MenuPanelName);
if (menuElement is System.Windows.Controls.Primitives.Popup popup)
{
popup.IsOpen = visible;
LogHelper.WriteLogToFile($"ToolbarRegistry: 菜单 Popup [{item.MenuPanelName}] -> {(visible ? "Open" : "Closed")}", LogHelper.LogType.Info);
}
else if (menuElement is FrameworkElement fe)
{
fe.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
LogHelper.WriteLogToFile($"ToolbarRegistry: 菜单 [{item.MenuPanelName}] -> {(visible ? "Visible" : "Collapsed")}", LogHelper.LogType.Info);
}
else
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 找不到菜单面板 [{item.MenuPanelName}]", LogHelper.LogType.Warning);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 设置菜单可见性异常 [{item.MenuPanelName}]: {ex.Message}", LogHelper.LogType.Warning);
}
}
}
private static void InjectIntoContainer(IToolbarHost host, Panel container,
List<(IToolbarItem item, ToolbarItemConfig cfg)> entries)
{
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.GetType().Name}: {ex.Message}\n{ex.StackTrace}", LogHelper.LogType.Error);
return null;
}
}
}
}
@@ -0,0 +1,11 @@
namespace Ink_Canvas.Controls.Toolbar
{
public enum ToolbarSlot
{
FloatingBarMain,
FloatingBarCanvasControls,
FloatingBarEnd,
BlackboardLeft,
BlackboardRight
}
}
+256 -16
View File
@@ -1,5 +1,8 @@
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
@@ -7,6 +10,138 @@ namespace Ink_Canvas.Helpers
{
internal class AnimationsHelper
{
#region Win32 API - Popup
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
private const uint GW_HWNDPREV = 3;
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private static readonly IntPtr HWND_TOP = new IntPtr(0);
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint SWP_SHOWWINDOW = 0x0040;
/// <summary>
/// 强制刷新 Popup 的实际窗口位置(终极方案)
/// 通过 Win32 API 直接操作窗口句柄
/// </summary>
public static void ForceRefreshPopupPosition(Popup popup)
{
if (popup?.Child == null || !popup.IsOpen) return;
try
{
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
try
{
var source = PresentationSource.FromVisual(popup.Child) as HwndSource;
if (source?.Handle == null) return;
var hwnd = source.Handle;
// 获取当前窗口位置
if (GetWindowRect(hwnd, out RECT rect))
{
// 使用相同的参数调用 SetWindowPos,但加上 SWP_SHOWWINDOW
// 这会强制窗口管理器重新评估并更新窗口位置
SetWindowPos(
hwnd,
HWND_TOP,
rect.Left, rect.Top,
rect.Right - rect.Left,
rect.Bottom - rect.Top,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
System.Diagnostics.Debug.WriteLine($"[PopupZOrder] Force refreshed position: ({rect.Left}, {rect.Top})");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[PopupZOrder] ForceRefreshPopupPosition failed: {ex.Message}");
}
}), System.Windows.Threading.DispatcherPriority.Render);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[PopupZOrder] ForceRefreshPopupPosition error: {ex.Message}");
}
}
/// <summary>
/// 将 Popup 窗口提升到最顶层,确保不被其他控件遮挡
/// 采用多重策略确保置顶生效
/// </summary>
private static void BringPopupToFront(Popup popup)
{
try
{
if (popup?.Child == null) return;
Action bringToTopAction = () =>
{
try
{
var source = PresentationSource.FromVisual(popup.Child) as HwndSource;
if (source?.Handle == null) return;
var hwnd = source.Handle;
// 策略1:直接设置为 TOPMOST(最高优先级)
SetWindowPos(hwnd, HWND_TOPMOST,
0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
System.Diagnostics.Debug.WriteLine($"[PopupZOrder] Set TOPMOST for popup");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[PopupZOrder] BringPopupToFront failed: {ex.Message}");
}
};
// 立即执行第一次
Application.Current.Dispatcher.BeginInvoke(bringToTopAction,
System.Windows.Threading.DispatcherPriority.Render);
// 延迟 50ms 后再次执行(确保在其他窗口操作之后)
Application.Current.Dispatcher.BeginInvoke(bringToTopAction,
System.Windows.Threading.DispatcherPriority.Normal);
// 延迟 100ms 后第三次执行(最终确认)
Application.Current.Dispatcher.BeginInvoke(bringToTopAction,
System.Windows.Threading.DispatcherPriority.Background);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[PopupZOrder] BringPopupToFront error: {ex.Message}");
}
}
#endregion
private static UIElement ResolveAnimationTarget(UIElement element)
{
return element;
}
public static void ShowWithFadeIn(UIElement element, double duration = 0.15)
{
if (element.Visibility == Visibility.Visible) return;
@@ -36,14 +171,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 +192,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 +205,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 +343,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 +361,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 +379,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); }
}
@@ -258,7 +394,6 @@ namespace Ink_Canvas.Helpers
var sb = new Storyboard();
// 渐变动画
var fadeOutAnimation = new DoubleAnimation
{
From = 1,
@@ -277,5 +412,110 @@ namespace Ink_Canvas.Helpers
sb.Begin((FrameworkElement)element);
}
public static void ShowPopupWithSlideAndFade(Popup popup, double duration = 0.15)
{
try
{
if (popup == null)
throw new ArgumentNullException(nameof(popup));
if (popup.IsOpen) return;
var child = popup.Child as FrameworkElement;
if (child == null)
{
popup.IsOpen = true;
BringPopupToFront(popup);
return;
}
child.Opacity = 0.5;
child.RenderTransform = new TranslateTransform(0, 10);
popup.IsOpen = true;
// 提升 Popup 到最顶层
BringPopupToFront(popup);
var sb = new Storyboard();
var fadeInAnimation = new DoubleAnimation
{
From = 0.5,
To = 1,
Duration = TimeSpan.FromSeconds(duration)
};
fadeInAnimation.EasingFunction = new CubicEase();
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(UIElement.OpacityProperty));
var slideAnimation = new DoubleAnimation
{
From = 10,
To = 0,
Duration = TimeSpan.FromSeconds(duration)
};
slideAnimation.EasingFunction = new CubicEase();
Storyboard.SetTargetProperty(slideAnimation, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)"));
sb.Children.Add(fadeInAnimation);
sb.Children.Add(slideAnimation);
sb.Begin(child);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
public static void HidePopupWithSlideAndFade(Popup popup, double duration = 0.15)
{
try
{
if (popup == null)
throw new ArgumentNullException(nameof(popup));
if (!popup.IsOpen) return;
var child = popup.Child as FrameworkElement;
if (child == null)
{
popup.IsOpen = false;
return;
}
var sb = new Storyboard();
var fadeOutAnimation = new DoubleAnimation
{
From = 1,
To = 0,
Duration = TimeSpan.FromSeconds(duration)
};
fadeOutAnimation.EasingFunction = new CubicEase();
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(UIElement.OpacityProperty));
var slideAnimation = new DoubleAnimation
{
From = 0,
To = 10,
Duration = TimeSpan.FromSeconds(duration)
};
slideAnimation.EasingFunction = new CubicEase();
Storyboard.SetTargetProperty(slideAnimation, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)"));
sb.Children.Add(fadeOutAnimation);
sb.Children.Add(slideAnimation);
sb.Completed += (s, e) =>
{
popup.IsOpen = false;
child.Opacity = 1;
child.RenderTransform = new TranslateTransform();
};
child.RenderTransform = new TranslateTransform();
sb.Begin(child);
}
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}");
}
}
}
}
+155 -10
View File
@@ -27,6 +27,37 @@ 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
@@ -383,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)>();
@@ -468,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
@@ -945,6 +1015,7 @@ namespace Ink_Canvas.Helpers
// 使用多线路组下载新版(支持自动切换)
public static async Task<bool> DownloadSetupFileWithFallback(string version, List<UpdateLineGroup> groups, Action<double, string> progressCallback = null)
{
var session = BeginDownloadSession();
try
{
version = NormalizeVersionForUpdate(version);
@@ -979,8 +1050,19 @@ 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);
// 智教联盟需要先获取真实下载地址
@@ -1006,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);
@@ -1021,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);
@@ -1033,6 +1128,10 @@ namespace Ink_Canvas.Helpers
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
return false;
}
finally
{
EndDownloadSession(session);
}
}
// 下载文件的具体实现
@@ -1043,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;
@@ -1146,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;
@@ -1206,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}");
@@ -1218,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)
@@ -1339,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))
@@ -1355,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卡顿
@@ -1379,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);
@@ -2201,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;
@@ -2211,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);
+6 -6
View File
@@ -519,7 +519,7 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 异步上传文件
/// </summary>
public async Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
public Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
{
try
{
@@ -528,19 +528,19 @@ namespace Ink_Canvas.Helpers
// 检查是否启用
if (!IsUploadEnabled())
{
return false;
return Task.FromResult(false);
}
// 基本验证
if (!File.Exists(filePath))
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
return false;
return Task.FromResult(false);
}
if (!IsValidFile(filePath))
{
return false;
return Task.FromResult(false);
}
// 确保队列已初始化
@@ -552,7 +552,7 @@ namespace Ink_Canvas.Helpers
// 加入队列
EnqueueFile(filePath, 0, cancellationToken);
return true;
return Task.FromResult(true);
}
catch (OperationCanceledException)
{
@@ -562,7 +562,7 @@ namespace Ink_Canvas.Helpers
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
return Task.FromResult(false);
}
}
+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 { }
}
}
}
+168 -135
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,进入分级策略判断");
}
// 计算最近活跃度(最后一次使用距今的天数)
@@ -1466,32 +1509,22 @@ namespace Ink_Canvas.Helpers
int.TryParse(remoteParts[2], out int remoteBuild) &&
int.TryParse(remoteParts[3], out int remoteRevision))
{
// 计算代数差异:主版本号差异 * 1000 + 次版本号差异 * 100 + 构建号差异 * 10 + 修订号差异
int majorDiff = remoteMajor - localMajor;
int minorDiff = remoteMinor - localMinor;
int buildDiff = remoteBuild - localBuild;
int revisionDiff = remoteRevision - localRevision;
var localSemver = new Version(localMajor, localMinor, localBuild, localRevision);
var remoteSemver = new Version(remoteMajor, remoteMinor, remoteBuild, remoteRevision);
int direction = remoteSemver.CompareTo(localSemver);
if (direction == 0) return 0;
int sign = direction > 0 ? 1 : -1;
// 如果主版本号不同,则代数差异很大
if (majorDiff != 0)
{
return majorDiff * 1000 + minorDiff * 100 + buildDiff * 10 + revisionDiff;
}
int majorDiff = Math.Abs(remoteMajor - localMajor);
if (majorDiff != 0) return sign * (majorDiff * 1000);
// 如果次版本号不同,则代数差异中等
if (minorDiff != 0)
{
return minorDiff * 100 + buildDiff * 10 + revisionDiff;
}
int minorDiff = Math.Abs(remoteMinor - localMinor);
if (minorDiff != 0) return sign * (minorDiff * 100);
// 如果构建号不同,则代数差异较小
if (buildDiff != 0)
{
return buildDiff * 10 + revisionDiff;
}
int buildDiff = Math.Abs(remoteBuild - localBuild);
if (buildDiff != 0) return sign * (buildDiff * 10);
// 只有修订号不同,代数差异最小
return revisionDiff;
return sign * Math.Abs(remoteRevision - localRevision);
}
return 0;
+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":
+54 -61
View File
@@ -8,9 +8,9 @@ namespace Ink_Canvas.Helpers
{
private static InkRecognitionManager _instance;
private static readonly object _lock = new object();
private readonly object _initSync = new object();
private ModernInkProcessor _modernProcessor;
private ModernInkAnalyzer _modernAnalyzer;
private bool _isModernSystemAvailable;
private bool _isInitialized;
@@ -31,35 +31,16 @@ namespace Ink_Canvas.Helpers
}
}
private InkRecognitionManager()
{
Initialize();
}
private InkRecognitionManager() { }
private void Initialize()
{
if (_isInitialized) return;
try
{
var tryModern = WinRtInkShapeRecognizer.IsApiAvailable && Environment.Is64BitProcess;
_isModernSystemAvailable = false;
if (tryModern)
{
try
{
_modernProcessor = new ModernInkProcessor();
_modernAnalyzer = new ModernInkAnalyzer();
_isModernSystemAvailable = true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("WinRT 墨迹初始化失败: " + ex.Message, LogHelper.LogType.Warning);
_isModernSystemAvailable = false;
_modernProcessor = null;
_modernAnalyzer = null;
}
}
// 启动阶段只做能力探测,不做 WinRT 组件实例化(避免冷启动延迟)
_isModernSystemAvailable = WinRtInkShapeRecognizer.IsApiAvailable;
_isInitialized = true;
}
catch (Exception ex)
@@ -69,10 +50,41 @@ namespace Ink_Canvas.Helpers
}
}
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);
@@ -84,8 +96,10 @@ namespace Ink_Canvas.Helpers
return RecognizeShapeWinRtOnDispatcherContext(strokes);
}
var legacy = InkRecognizeHelper.RecognizeShapeIACore(strokes);
return Task.FromResult(InkRecognizeHelper.FromIACoreOrEmpty(legacy));
// IACore 必须走 IPC 辅助进程(x86/.NET 4.7.2)。
// 在 .NET 6 x64 主进程中本地加载 IAWinFX 会失败,故不再本地回退。
var ipcResult = IpcIACoreClient.Instance.Recognize(strokes);
return Task.FromResult(ipcResult);
}
catch (Exception ex)
{
@@ -108,6 +122,7 @@ namespace Ink_Canvas.Helpers
bool applyHandwritingBeautify = false,
string handwritingFontFamilyList = null)
{
EnsureInitialized();
if (!_isInitialized)
{
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info);
@@ -140,18 +155,11 @@ namespace Ink_Canvas.Helpers
return Task.FromResult(strokes);
}
if (!Environment.Is64BitProcess)
EnsureModernAnalyzerInitialized();
if (_modernProcessor == null)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:非 64 位进程,WinRT 手写体替换不可用。笔画数=" + strokes.Count,
LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
if (_modernAnalyzer == null)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:ModernInkAnalyzer 未就绪(WinRT 初始化失败?)。笔画数=" +
"[手写体] CorrectInkAsync 跳过:ModernInkProcessor 未就绪(WinRT 初始化失败?)。笔画数=" +
strokes.Count,
LogHelper.LogType.Warning);
return Task.FromResult(strokes);
@@ -161,7 +169,7 @@ namespace Ink_Canvas.Helpers
"[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count +
",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()),
LogHelper.LogType.Info);
return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes, handwritingFontFamilyList);
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(strokes, handwritingFontFamilyList);
}
catch (Exception ex)
{
@@ -171,19 +179,19 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// WinRT 手写体识别(需 64 位进程、Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。
/// 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 (!Environment.Is64BitProcess
|| !ShapeRecognitionRouter.ResolveUseWinRt(mode)
if (!ShapeRecognitionRouter.ResolveUseWinRt(mode)
|| !WinRtHandwritingRecognizer.IsApiAvailable)
return Task.FromResult(HandwritingRecognitionResult.Empty);
@@ -208,15 +216,16 @@ namespace Ink_Canvas.Helpers
public string GetSystemInfo()
{
return _isModernSystemAvailable
? $"现代化64位墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}"
: $"传统墨迹识别系统 (IACore) - 进程架构: {Environment.Is64BitProcess}";
if (_isModernSystemAvailable)
return $"现代化墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}";
if (IpcIACoreClient.Instance.IsAvailable)
return $"传统墨迹识别系统 (IACore via IPC) - 进程架构: {Environment.Is64BitProcess}";
return $"传统墨迹识别系统 (IACore 本地) - 进程架构: {Environment.Is64BitProcess}";
}
public void Dispose()
{
_modernProcessor?.Dispose();
_modernAnalyzer?.Dispose();
_isInitialized = false;
}
}
@@ -238,20 +247,4 @@ namespace Ink_Canvas.Helpers
{
}
}
internal sealed class ModernInkAnalyzer : IDisposable
{
public Task<StrokeCollection> AnalyzeAndCorrectAsync(
StrokeCollection strokes,
string handwritingFontFamilyList)
{
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(
strokes,
handwritingFontFamilyList);
}
public void Dispose()
{
}
}
}
+19 -223
View File
@@ -1,76 +1,15 @@
using Ink_Canvas;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Media;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 墨迹形状/手写识别的对外门面。
/// IACore 路径通过 IPC 调用 x86 辅助进程;WinRT 路径在主进程内直接调用。
/// 主进程 (.NET 6 x64) 不再直接引用 IAWinFX 类型。
/// </summary>
public class InkRecognizeHelper
{
/// <summary>IACore / IAWinFX 形状识别(典型用于 32 位进程)。</summary>
public static ShapeRecognizeResult RecognizeShapeIACore(StrokeCollection strokes)
{
if (strokes == null || strokes.Count == 0)
return default;
var analyzer = new InkAnalyzer();
analyzer.AddStrokes(strokes);
analyzer.SetStrokesType(strokes, StrokeType.Drawing);
AnalysisAlternate analysisAlternate = null;
int strokesCount = strokes.Count;
var sfsaf = analyzer.Analyze();
if (sfsaf.Successful)
{
var alternates = analyzer.GetAlternates();
if (alternates.Count > 0)
{
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)
analysisAlternate = alternates[0];
}
}
analyzer.Dispose();
if (analysisAlternate != 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)
@@ -81,11 +20,9 @@ namespace Ink_Canvas.Helpers
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
return InkShapeRecognitionResult.Empty;
var legacy = RecognizeShapeIACore(strokes);
return FromIACoreOrEmpty(legacy);
return IpcIACoreClient.Instance.Recognize(strokes);
}
/// <summary>与 CE 反编译版 <c>InkRecognitionManager.RecognizeShapeAsync</c> 对齐的统一入口。</summary>
public static Task<InkShapeRecognitionResult> RecognizeShapeUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
@@ -100,14 +37,15 @@ namespace Ink_Canvas.Helpers
{
try
{
_ = InkRecognitionManager.Instance;
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
{
WinRtInkShapeRecognizer.Warmup();
WinRtHandwritingRecognizer.Warmup();
}
else
RecognizeShapeIACore(new StrokeCollection());
{
IpcIACoreClient.Instance.Start();
}
}
catch
{
@@ -115,13 +53,11 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>WinRT 手写识别(64 位 + 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) =>
@@ -131,7 +67,6 @@ namespace Ink_Canvas.Helpers
MainWindow.Settings?.InkToShape?.EnableWinRtHandwritingStrokeBeautify ?? false,
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
/// <summary>显式指定是否应用手写体字形替换(忽略开关);字体仍从设置读取。</summary>
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode,
@@ -142,47 +77,18 @@ namespace Ink_Canvas.Helpers
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();
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 (name.Contains("Triangle") || name.Contains("Circle") ||
name.Contains("Rectangle") || name.Contains("Diamond") ||
name.Contains("Parallelogram") || name.Contains("Square")
|| name.Contains("Ellipse"))
{
return true;
}
return false;
if (string.IsNullOrEmpty(name))
return false;
return name.Contains("Triangle") || name.Contains("Circle") ||
name.Contains("Rectangle") || name.Contains("Diamond") ||
name.Contains("Parallelogram") || name.Contains("Square") ||
name.Contains("Ellipse");
}
}
//Recognizer 的实现
public enum RecognizeLanguage
{
SimplifiedChinese = 0x0804,
@@ -190,127 +96,17 @@ namespace Ink_Canvas.Helpers
English = 0x0809
}
public class ShapeRecognizeResult
{
public ShapeRecognizeResult(Point centroid, PointCollection hotPoints, AnalysisAlternate analysisAlternate, InkDrawingNode node)
{
Centroid = centroid;
HotPoints = hotPoints;
AnalysisAlternate = analysisAlternate;
InkDrawingNode = node;
}
public AnalysisAlternate AnalysisAlternate { get; }
public Point Centroid { get; set; }
public PointCollection HotPoints { get; }
public InkDrawingNode InkDrawingNode { get; }
}
/// <summary>
/// 图形识别类
/// </summary>
//public class ShapeRecogniser
//{
// public InkAnalyzer _inkAnalyzer = null;
// private ShapeRecogniser()
// {
// this._inkAnalyzer = new InkAnalyzer
// {
// AnalysisModes = AnalysisModes.AutomaticReconciliationEnabled
// };
// }
// /// <summary>
// /// 根据笔迹集合返回图形名称字符串
// /// </summary>
// /// <param name="strokeCollection"></param>
// /// <returns></returns>
// public InkDrawingNode Recognition(StrokeCollection strokeCollection)
// {
// if (strokeCollection == null)
// {
// //MessageBox.Show("dddddd");
// return null;
// }
// InkDrawingNode result = null;
// try
// {
// this._inkAnalyzer.AddStrokes(strokeCollection);
// if (this._inkAnalyzer.Analyze().Successful)
// {
// result = _internalAnalyzer(this._inkAnalyzer);
// this._inkAnalyzer.RemoveStrokes(strokeCollection);
// }
// }
// catch (System.Exception ex)
// {
// //result = ex.Message;
// System.Diagnostics.Debug.WriteLine(ex.Message);
// }
// return result;
// }
// /// <summary>
// /// 实现笔迹的分析,返回图形对应的字符串
// /// 你在实际的应用中根据返回的字符串来生成对应的Shape
// /// </summary>
// /// <param name="ink"></param>
// /// <returns></returns>
// private InkDrawingNode _internalAnalyzer(InkAnalyzer ink)
// {
// try
// {
// ContextNodeCollection nodecollections = ink.FindNodesOfType(ContextNodeType.InkDrawing);
// foreach (ContextNode node in nodecollections)
// {
// InkDrawingNode drawingNode = node as InkDrawingNode;
// if (drawingNode != null)
// {
// return drawingNode;//.GetShapeName();
// }
// }
// }
// catch (System.Exception ex)
// {
// System.Diagnostics.Debug.WriteLine(ex.Message);
// }
// return null;
// }
// private static ShapeRecogniser instance = null;
// public static ShapeRecogniser Instance
// {
// get
// {
// return instance == null ? (instance = new ShapeRecogniser()) : instance;
// }
// }
//}
//用于自动控制其他形状相对于圆的位置
public class Circle
{
public Circle(Point centroid, double r, Stroke stroke)
public Circle(System.Windows.Point centroid, double r, Stroke stroke)
{
Centroid = centroid;
R = r;
Stroke = stroke;
}
public Point Centroid { get; set; }
public System.Windows.Point Centroid { get; set; }
public double R { get; set; }
public Stroke Stroke { get; set; }
}
}
}
+3 -4
View File
@@ -1,5 +1,4 @@
using OSVersionExtension;
using System;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Media;
@@ -17,13 +16,13 @@ namespace Ink_Canvas.Helpers
public static class ShapeRecognitionRouter
{
/// <summary>
/// 自动模式:按当前进程位数选择——<c>64</c> 位进程用 WinRT<c>32</c> 位进程(含 x86 目标在 WOW64 下运行)用 IACore。
/// 自动模式:在 Windows 10 及以上系统默认使用 WinRT,否则使用 IACore。
/// </summary>
public static bool ResolveUseWinRt(ShapeRecognitionEngineMode mode)
{
if (mode == ShapeRecognitionEngineMode.WinRT) return true;
if (mode == ShapeRecognitionEngineMode.IACore) return false;
return Environment.Is64BitProcess;
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
}
public static bool ShouldRunShapeRecognition(bool inkToShapeEnabled, ShapeRecognitionEngineMode mode)
@@ -31,7 +30,7 @@ namespace Ink_Canvas.Helpers
if (!inkToShapeEnabled) return false;
if (ResolveUseWinRt(mode))
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
return !Environment.Is64BitProcess;
return true;
}
public static ShapeRecognitionEngineMode FromSettingsInt(int value)
+255
View File
@@ -0,0 +1,255 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Media;
namespace Ink_Canvas.Helpers
{
public sealed class IpcIACoreClient : IDisposable
{
private static IpcIACoreClient _instance;
private static readonly object _instanceLock = new object();
public static IpcIACoreClient Instance
{
get
{
if (_instance == null)
lock (_instanceLock)
if (_instance == null)
_instance = new IpcIACoreClient();
return _instance;
}
}
private Process _helperProcess;
private readonly object _pipeLock = new object();
private bool _disposed;
private bool _available;
private static string HelperExePath =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "InkCanvas.IACoreHelper.exe");
private string PipeName =>
string.Format("ICC_IACoreHelper_{0}", Process.GetCurrentProcess().Id);
private IpcIACoreClient() { }
public bool Start()
{
if (_disposed) return false;
if (IsAvailable) return true;
if (!File.Exists(HelperExePath))
{
_available = false;
return false;
}
return LaunchHelper();
}
public bool IsAvailable => _available && _helperProcess != null && !_helperProcess.HasExited;
public InkShapeRecognitionResult Recognize(StrokeCollection strokes)
{
if (strokes == null || strokes.Count == 0)
return InkShapeRecognitionResult.Empty;
EnsureHelperAlive();
if (!IsAvailable)
return InkShapeRecognitionResult.Empty;
lock (_pipeLock)
{
try
{
return SendRecognizeRequest(strokes);
}
catch
{
KillHelper();
return InkShapeRecognitionResult.Empty;
}
}
}
private bool LaunchHelper()
{
try
{
KillHelper();
var psi = new ProcessStartInfo
{
FileName = HelperExePath,
Arguments = Process.GetCurrentProcess().Id.ToString(),
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory
};
_helperProcess = Process.Start(psi);
if (_helperProcess == null)
{
_available = false;
return false;
}
_helperProcess.EnableRaisingEvents = true;
_helperProcess.Exited += OnHelperExited;
bool pipeReady = WaitForPipe(3000);
_available = pipeReady;
return pipeReady;
}
catch
{
_available = false;
return false;
}
}
private bool WaitForPipe(int timeoutMs)
{
int elapsed = 0;
while (elapsed < timeoutMs)
{
if (_helperProcess == null || _helperProcess.HasExited)
return false;
try
{
using (var probe = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut))
{
probe.Connect(200);
return true;
}
}
catch
{
Thread.Sleep(100);
elapsed += 300;
}
}
return false;
}
private InkShapeRecognitionResult SendRecognizeRequest(StrokeCollection strokes)
{
using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut))
{
client.Connect(IpcTimeoutMs);
using (var writer = new BinaryWriter(client, System.Text.Encoding.UTF8, leaveOpen: true))
using (var reader = new BinaryReader(client, System.Text.Encoding.UTF8, leaveOpen: true))
{
writer.Write(CmdRecognize);
writer.Write(strokes.Count);
foreach (var stroke in strokes)
{
var pts = stroke.StylusPoints;
writer.Write(pts.Count);
foreach (var pt in pts)
{
writer.Write((float)pt.X);
writer.Write((float)pt.Y);
writer.Write(pt.PressureFactor);
}
}
writer.Flush();
bool success = reader.ReadBoolean();
string shape = reader.ReadString();
float cx = reader.ReadSingle();
float cy = reader.ReadSingle();
float width = reader.ReadSingle();
float height = reader.ReadSingle();
int hotLen = reader.ReadInt32();
var hotPoints = new PointCollection();
for (int i = 0; i < hotLen; i++)
hotPoints.Add(new Point(reader.ReadSingle(), reader.ReadSingle()));
int idxLen = reader.ReadInt32();
var indices = new int[idxLen];
for (int i = 0; i < idxLen; i++)
indices[i] = reader.ReadInt32();
if (!success || string.IsNullOrEmpty(shape))
return InkShapeRecognitionResult.Empty;
var recognized = new StrokeCollection();
foreach (int idx in indices)
if (idx >= 0 && idx < strokes.Count)
recognized.Add(strokes[idx]);
return new InkShapeRecognitionResult(
shape,
new Point(cx, cy),
hotPoints,
width,
height,
recognized);
}
}
}
private void EnsureHelperAlive()
{
if (!IsAvailable)
LaunchHelper();
}
private void OnHelperExited(object sender, EventArgs e)
{
_available = false;
}
private void KillHelper()
{
if (_helperProcess == null) return;
try
{
try { _helperProcess.Exited -= OnHelperExited; } catch { }
if (!_helperProcess.HasExited)
{
try
{
using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut))
{
client.Connect(500);
using (var w = new BinaryWriter(client))
w.Write(CmdShutdown);
}
}
catch { }
if (!_helperProcess.WaitForExit(800))
_helperProcess.Kill();
}
}
catch { }
finally
{
_helperProcess?.Dispose();
_helperProcess = null;
_available = false;
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
KillHelper();
}
private const int IpcTimeoutMs = 5000;
private const byte CmdRecognize = 0x01;
private const byte CmdShutdown = 0xFF;
}
}
+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))
+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>
+20 -9
View File
@@ -271,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))
{
@@ -298,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))
{
@@ -390,11 +390,15 @@ namespace Ink_Canvas.Helpers
{
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,6 +410,15 @@ 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);
@@ -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;
}
+79 -7
View File
@@ -17,6 +17,9 @@ namespace Ink_Canvas.Helpers
[DllImport("ole32.dll")]
private static extern int CreateBindCtx(int reserved, out IBindCtx ppbc);
[DllImport("ole32.dll", CharSet = CharSet.Unicode)]
private static extern int CLSIDFromProgID(string lpszProgID, out Guid pclsid);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
@@ -104,7 +107,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 +127,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
@@ -189,6 +192,7 @@ namespace Ink_Canvas.Helpers
IMoniker[] moniker = new IMoniker[1];
IntPtr fetched = IntPtr.Zero;
string[] applicationMonikersFromProgIds = GetApplicationMonikersFromProgIds();
while (enumMoniker.Next(1, moniker, fetched) == 0)
{
@@ -205,17 +209,32 @@ namespace Ink_Canvas.Helpers
CreateBindCtx(0, out bindCtx);
moniker[0].GetDisplayName(bindCtx, null, out displayName);
if (LooksLikePresentationFile(displayName) || displayName == "!{91493441-5A91-11CF-8700-00AA0060263B}")
bool looksLikePresentationFile = LooksLikePresentationFile(displayName);
bool isApplicationMoniker = ContainsMoniker(applicationMonikersFromProgIds, displayName);
if (!isApplicationMoniker)
{
isApplicationMoniker = IsFallbackApplicationMoniker(displayName);
}
if (looksLikePresentationFile || isApplicationMoniker)
{
rot.GetObject(moniker[0], out comObject);
if (comObject != null)
{
try
if (isApplicationMoniker)
{
object appObj = comObject.GetType().InvokeMember("Application", BindingFlags.GetProperty, null, comObject, null);
candidateApp = appObj;
candidateApp = comObject;
comObject = null;
}
else
{
try
{
object appObj = comObject.GetType().InvokeMember("Application", BindingFlags.GetProperty, null, comObject, null);
candidateApp = appObj;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
}
bool isDuplicate = false;
@@ -398,6 +417,59 @@ namespace Ink_Canvas.Helpers
return false;
}
private static string[] GetApplicationMonikersFromProgIds()
{
string[] ApplicationProgIds = new[]
{
"PowerPoint.Application",
"KWPP.Application",
"Wpp.Application",
"WPP.Application",
};
List<string> monikers = new List<string>();
foreach (string progId in ApplicationProgIds)
{
Guid clsid;
if (CLSIDFromProgID(progId, out clsid) == 0 && clsid != Guid.Empty)
{
string moniker = "!" + clsid.ToString("B").ToUpperInvariant();
if (!ContainsMoniker(monikers, moniker))
{
monikers.Add(moniker);
}
}
}
return monikers.ToArray();
}
private static bool ContainsMoniker(IEnumerable<string> monikers, string displayName)
{
if (monikers == null || string.IsNullOrEmpty(displayName))
return false;
foreach (string moniker in monikers)
{
if (string.Equals(displayName, moniker, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private static bool IsFallbackApplicationMoniker(string displayName)
{
string[] FallbackApplicationMonikers = new[]
{
"!{91493441-5A91-11CF-8700-00AA0060263B}",
"!{44720441-94BF-4940-926D-4F38FECF2A48}",
};
return ContainsMoniker(FallbackApplicationMonikers, displayName);
}
public static bool IsSlideShowWindowActive(object sswObj)
{
try
+50 -129
View File
@@ -1,8 +1,6 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers
@@ -86,18 +84,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 +100,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 +151,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 +160,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 +396,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 +425,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;
}
}
}
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
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";
// Windows 保留设备名(不区分大小写)。这些名称无论是否带扩展名,CreateFile 都会失败。
private static readonly HashSet<string> ReservedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
};
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, '_');
}
// Windows 禁止文件名以点号或空格结尾(会被静默截断甚至创建失败)。
name = name.Trim().TrimEnd('.', ' ');
if (string.IsNullOrEmpty(name)) return name;
// 保留设备名:比较时忽略扩展名,命中则加下划线前缀以规避。
var stem = Path.GetFileNameWithoutExtension(name);
if (!string.IsNullOrEmpty(stem) && ReservedNames.Contains(stem))
{
name = "_" + name;
}
return name;
}
}
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; }
}
}
+6 -9
View File
@@ -49,16 +49,13 @@ namespace Ink_Canvas.Helpers
if (string.IsNullOrWhiteSpace(softwareName))
return null;
// 64 位进程默认只枚举 64 位注册表视图;32 位希沃常写在 WOW6432Node 下,需一并扫描。
string[] uninstallRoots =
// 须用 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 })
{
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
};
foreach (string root in uninstallRoots)
{
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(root))
using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
using (RegistryKey key = baseKey.OpenSubKey(uninstallSubKey))
{
if (key == null) continue;
string found = FindInUninstallKey(key, softwareName);
+92
View File
@@ -0,0 +1,92 @@
using iNKORE.UI.WPF.Modern;
using Microsoft.Win32;
using System;
using System.Windows;
namespace Ink_Canvas.Helpers
{
public static class ThemeHelper
{
public static bool IsSystemThemeLight()
{
try
{
var registryKey = Registry.CurrentUser;
var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (themeKey != null)
{
var value = themeKey.GetValue("AppsUseLightTheme");
if (value != null)
{
bool result = (int)value == 1;
themeKey.Close();
return result;
}
themeKey.Close();
}
}
catch
{
}
return true;
}
public static bool IsSystemThemeLightLegacy()
{
try
{
var registryKey = Registry.CurrentUser;
var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (themeKey != null)
{
int keyValue = (int)themeKey.GetValue("SystemUsesLightTheme");
themeKey.Close();
return keyValue == 1;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
return false;
}
public static ElementTheme GetEffectiveTheme(Settings settings)
{
if (settings.Appearance.Theme == 0)
return ElementTheme.Light;
if (settings.Appearance.Theme == 1)
return ElementTheme.Dark;
return IsSystemThemeLight() ? ElementTheme.Light : ElementTheme.Dark;
}
public static void ApplyTheme(FrameworkElement element, Settings settings)
{
if (element == null || settings == null) return;
try
{
ThemeManager.SetRequestedTheme(element, GetEffectiveTheme(settings));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用主题失败: {ex.Message}", LogHelper.LogType.Error);
}
}
public static void ApplyTheme(FrameworkElement element, Settings settings, Action<string> onThemeApplied)
{
if (element == null || settings == null) return;
try
{
var theme = GetEffectiveTheme(settings);
ThemeManager.SetRequestedTheme(element, theme);
onThemeApplied?.Invoke(theme == ElementTheme.Dark ? "Dark" : "Light");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用主题失败: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
+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);
}
}
}
}
+710
View File
@@ -0,0 +1,710 @@
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 (!GetCurrentProcessSessionId(out uint sessionId))
{
LogHelper.WriteLogToFile($"UIAccess | 获取当前会话 ID 失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
return false;
}
if (!GetUserPrimaryToken(sessionId, 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); }
}
private static bool GetCurrentProcessSessionId(out uint sessionId)
{
sessionId = 0;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hSelfQuery))
return false;
try
{
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
if (!GetTokenInformation(hSelfQuery, TokenSessionId, sesBuf, sizeof(uint), out _))
return false;
sessionId = (uint)Marshal.ReadInt32(sesBuf);
return true;
}
finally { Marshal.FreeHGlobal(sesBuf); }
}
finally { CloseHandle(hSelfQuery); }
}
/// <summary>
/// 从 explorer.exe / ctfmon.exe 取得普通用户(非提升)令牌的主令牌副本,用于降权启动。
/// 仅当当前进程为管理员时才能成功。
/// </summary>
private static bool GetUserPrimaryToken(uint sessionId, 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, sessionId, out userToken))
{
LogHelper.WriteLogToFile($"UIAccess | 已从 {name} (PID={pe.th32ProcessID}, Session={sessionId}) 取得用户令牌");
return true;
}
}
more = Process32NextW(snapshot, ref pe);
}
}
finally { CloseHandle(snapshot); }
}
return false;
}
private static bool TryDuplicateUserPrimaryToken(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
{
// 会话隔离:拒绝来自其他登录会话(RDP / 终端服务 / 快速用户切换)的令牌,
// 否则降权后进程会落到错误用户的桌面上下文中。
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); }
// 仅接受非提升令牌(否则降权失败)
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
}
}
+306 -79
View File
@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
@@ -19,6 +18,9 @@ namespace Ink_Canvas.Helpers
/// </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);
@@ -27,29 +29,13 @@ namespace Ink_Canvas.Helpers
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()
{
if (!IsApiAvailable || !Environment.Is64BitProcess) return;
try
{
var d = Application.Current?.Dispatcher;
if (d == null) return;
d.BeginInvoke(new Action(async () =>
{
try
{
await RecognizeHandwritingAsync(new StrokeCollection()).ConfigureAwait(true);
}
catch
{
// ignore
}
}));
}
catch
{
// ignore
}
}
/// <summary>
@@ -57,15 +43,21 @@ namespace Ink_Canvas.Helpers
/// 再对每一分词用 <see cref="WinRtInk.InkRecognizerContainer"/> 取 <c>GetTextCandidates</c>(与当前 SDK 中部分版本的
/// <see cref="WinRtInk.InkRecognitionResult"/> 未暴露笔画映射的局限兼容)。
/// </summary>
public static async Task<HandwritingRecognitionResult> RecognizeHandwritingAsync(StrokeCollection strokes)
/// <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 = strokes.Count > 0;
var traceRecognition = verboseTrace;
try
{
var recognizer = new WinRtInk.InkRecognizerContainer();
TryApplyPreferredHandwritingRecognizer(recognizer, traceRecognition);
var analyzer = new WinAnalysis.InkAnalyzer();
var idToWpf = new Dictionary<uint, Stroke>();
@@ -92,19 +84,21 @@ namespace Ink_Canvas.Helpers
LogHandwriting(
"识别:AnalyzeAsync 未得到 UpdatedStatus=" +
(analysisResult == null ? "null" : analysisResult.Status.ToString()) +
",有效笔画数=" + idToWpf.Count);
return HandwritingRecognitionResult.Empty;
",有效笔画数=" + 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);
return HandwritingRecognitionResult.Empty;
LogHandwriting(
"识别:未找到 InkWord 节点(墨迹分析常将非横平笔划判为绘图),有效笔画数=" + idToWpf.Count +
",改用整批 RecognizeAsync 回退。");
return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true);
}
var recognizer = new WinRtInk.InkRecognizerContainer();
var segments = new List<HandwritingWordSegment>();
foreach (var node in wordNodes)
@@ -232,6 +226,243 @@ namespace Ink_Canvas.Helpers
}
}
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>
@@ -491,64 +722,60 @@ namespace Ink_Canvas.Helpers
var m = new Matrix(scale, 0, 0, scale, tx, ty);
geom.Transform = new MatrixTransform(m);
return StrokesFromOutlinedGeometry(geom, templateDa, 0.35);
var filled = FilledGlyphStroke.TryCreate(geom, templateDa);
if (filled == null)
return list;
list.Add(filled);
return list;
}
}
/// <summary>
/// 把字形几何作为「实心填充」绘制的笔画。仍是 WPF <see cref="Stroke"/>,可被 InkCanvas 选择/移动/删除,
/// 但渲染时直接 DrawGeometry(brush, null, geom),不再走 StylusPoints 描边路径。
/// </summary>
internal sealed class FilledGlyphStroke : Stroke
{
private readonly Geometry _geometry;
private FilledGlyphStroke(StylusPointCollection pts, Geometry geometry, DrawingAttributes da)
: base(pts)
{
_geometry = geometry;
if (da != null)
DrawingAttributes = da.Clone();
}
private static List<Stroke> StrokesFromOutlinedGeometry(Geometry geometry, DrawingAttributes da, double tolerance)
public static FilledGlyphStroke TryCreate(Geometry geometry, DrawingAttributes templateDa)
{
var list = new List<Stroke>();
if (geometry == null || geometry.IsEmpty() || da == null)
return list;
if (geometry == null || geometry.IsEmpty())
return null;
Geometry outlined;
try
var b = geometry.Bounds;
if (b.IsEmpty || b.Width < 0.5 || b.Height < 0.5)
return null;
// StylusPoints 用 bounds 四角,保证命中测试 / 选区 / 包围盒计算正常。
var pts = new StylusPointCollection
{
outlined = geometry.GetOutlinedPathGeometry(tolerance, ToleranceType.Absolute);
}
catch
{
return list;
}
new StylusPoint(b.Left, b.Top, 0.5f),
new StylusPoint(b.Right, b.Top, 0.5f),
new StylusPoint(b.Right, b.Bottom, 0.5f),
new StylusPoint(b.Left, b.Bottom, 0.5f),
};
if (outlined == null || outlined.IsEmpty())
return list;
return new FilledGlyphStroke(pts, geometry, templateDa);
}
Geometry flat;
try
{
flat = outlined.GetFlattenedPathGeometry(tolerance, ToleranceType.Absolute);
}
catch
{
return list;
}
protected override void DrawCore(DrawingContext drawingContext, DrawingAttributes drawingAttributes)
{
if (drawingContext == null || _geometry == null)
return;
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;
var color = drawingAttributes != null ? drawingAttributes.Color : Colors.Black;
drawingContext.DrawGeometry(new SolidColorBrush(color), null, _geometry);
}
}
+151 -7
View File
@@ -1,6 +1,7 @@
using OSVersionExtension;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
@@ -11,6 +12,128 @@ 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
{
@@ -28,7 +151,8 @@ namespace Ink_Canvas.Helpers
{
try
{
await RecognizeShapeAsync(new StrokeCollection());
// 空 StrokeCollection 在 RecognizeShapeAsync 入口会直接返回,无法预热 WinRT InkAnalyzer。
await RecognizeShapeAsync(CreateMinimalWarmupStrokeCollection()).ConfigureAwait(true);
}
catch
{
@@ -99,6 +223,23 @@ namespace Ink_Canvas.Helpers
}
}
/// <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)
{
@@ -106,6 +247,9 @@ namespace Ink_Canvas.Helpers
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,
@@ -129,8 +273,8 @@ namespace Ink_Canvas.Helpers
return builder.CreateStroke(points);
}
private static global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing FindPrimaryDrawing(
WinRtInkAnalyzer analyzer)
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;
@@ -169,7 +313,7 @@ namespace Ink_Canvas.Helpers
return w * h;
}
private static global::Windows.Foundation.Point[] CopyWinRtPoints(
internal static global::Windows.Foundation.Point[] CopyWinRtPoints(
global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing drawing)
{
var src = drawing?.Points;
@@ -186,7 +330,7 @@ namespace Ink_Canvas.Helpers
return arr;
}
private static void BoundsFromPoints(
internal static void BoundsFromPoints(
System.Collections.Generic.IReadOnlyList<global::Windows.Foundation.Point> points,
out double w,
out double h)
@@ -211,7 +355,7 @@ namespace Ink_Canvas.Helpers
h = Math.Max(0, maxY - minY);
}
private static PointCollection ToWpfPointCollection(
internal static PointCollection ToWpfPointCollection(
System.Collections.Generic.IReadOnlyList<global::Windows.Foundation.Point> points)
{
var hot = new PointCollection();
@@ -225,7 +369,7 @@ namespace Ink_Canvas.Helpers
return hot;
}
private static string MapDrawingKindToShapeName(
internal static string MapDrawingKindToShapeName(
global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind kind)
{
switch (kind)
@@ -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
{
+45 -54
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,8 +27,15 @@
<BootstrapperEnabled>false</BootstrapperEnabled>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<Configurations>Debug;Release</Configurations>
<Platforms>AnyCPU;x86;x64;ARM64</Platforms>
<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>
@@ -46,14 +55,12 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>embedded</DebugType>
<LangVersion>7.3</LangVersion>
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>embedded</DebugType>
<LangVersion>7.3</LangVersion>
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
@@ -71,14 +78,12 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>full</DebugType>
<LangVersion>7.3</LangVersion>
<PlatformTarget>ARM64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>pdbonly</DebugType>
<LangVersion>7.3</LangVersion>
<PlatformTarget>ARM64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
@@ -86,7 +91,6 @@
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<LangVersion>7.3</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
@@ -94,40 +98,9 @@
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<LangVersion>7.3</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="IACore">
<HintPath>.\IACore.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="IALoader">
<HintPath>.\IALoader.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="IAWinFX">
<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" />
</ItemGroup>
@@ -135,19 +108,23 @@
<PackageReference Include="Costura.Fody" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
</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" />
<PackageReference Include="Microsoft.Office.Interop.PowerPoint" Version="15.0.4420.1018" />
<PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.19041.2" />
<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.2.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" />
@@ -155,7 +132,17 @@
<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" />
<ProjectReference Include="..\InkCanvas.IACoreHelper\InkCanvas.IACoreHelper.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup Condition="'$(MSBuildRuntimeType)' == 'Full'">
<COMReference Include="IWshRuntimeLibrary">
@@ -197,8 +184,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" />
@@ -563,14 +548,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" />
@@ -636,6 +614,12 @@
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Fonts\LXGWWenKaiTC-Regular.ttf" />
<Resource Include="Resources\Fonts\HarmonyOS_SansSC_Black.ttf" />
<Resource Include="Resources\Fonts\HarmonyOS_SansSC_Bold.ttf" />
<Resource Include="Resources\Fonts\HarmonyOS_SansSC_Light.ttf" />
<Resource Include="Resources\Fonts\HarmonyOS_SansSC_Medium.ttf" />
<Resource Include="Resources\Fonts\HarmonyOS_SansSC_Regular.ttf" />
<Resource Include="Resources\Fonts\HarmonyOS_SansSC_Thin.ttf" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\HiteAnnotation.png" />
@@ -680,4 +664,11 @@
<Target Name="CleanTelemetryDsn" AfterTargets="Build;Clean" Condition="Exists('$(MSBuildProjectDirectory)\telemetry_dsn.generated.txt')">
<Delete Files="$(MSBuildProjectDirectory)\telemetry_dsn.generated.txt" />
</Target>
<Target Name="CopyIACoreHelper" AfterTargets="Build">
<ItemGroup>
<IACoreHelperFiles Include="$(MSBuildProjectDirectory)\..\InkCanvas.IACoreHelper\bin\$(Configuration)\*.*" />
</ItemGroup>
<Copy SourceFiles="@(IACoreHelperFiles)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true" Condition="'@(IACoreHelperFiles)' != ''" />
</Target>
</Project>
+1251 -8584
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -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);
-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;
}
}
}
+125 -354
View File
@@ -1,3 +1,4 @@
using Ink_Canvas.Helpers;
using iNKORE.UI.WPF.Modern;
using Microsoft.Win32;
using System;
@@ -14,19 +15,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 +43,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 +134,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 +150,67 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 刷新浮动工具栏按钮颜色
/// </summary>
private bool IsCurrentThemeDark()
{
return Settings.Appearance.Theme == 1 ||
(Settings.Appearance.Theme == 2 && !ThemeHelper.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 +219,46 @@ 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");
// 与 IsCurrentThemeDark / GetEffectiveTheme / 浮动栏一致,统一读 AppsUseLightTheme
// 否则 SystemUsesLightTheme 与 AppsUseLightTheme 可独立取值时主题会混搭
SetTheme(ThemeHelper.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;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
return light;
}
/// <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 +267,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 +317,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 +341,13 @@ namespace Ink_Canvas
}
catch (Exception)
{
// 忽略异常
}
}
/// <summary>
/// 刷新手势按钮图标
/// </summary>
private void RefreshGestureButtonIcon()
{
try
{
// 调用手势按钮颜色和图标更新方法,该方法会根据当前主题和手势状态设置正确的图标
CheckEnableTwoFingerGestureBtnColorPrompt();
}
catch (Exception)
@@ -576,14 +355,10 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 刷新其他窗口的主题
/// </summary>
private void RefreshOtherWindowsTheme()
{
try
{
// 刷新所有打开的窗口
foreach (Window window in Application.Current.Windows)
{
if (window is CountdownTimerWindow timerWindow)
@@ -598,22 +373,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.HidePopupWithSlideAndFade(BorderTools);
AnimationsHelper.HidePopupWithSlideAndFade(BoardBorderToolsPopup);
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;
}
}
}
@@ -272,21 +272,21 @@ namespace Ink_Canvas
/// - 显示通知
/// - 包含异常处理
/// </remarks>
private async Task PasteImageFromClipboard(Point? position = null)
private Task PasteImageFromClipboard(Point? position = null)
{
try
{
if (!Clipboard.ContainsImage())
{
ShowNotification("剪贴板中没有图片");
return;
return Task.CompletedTask;
}
var clipboardImage = Clipboard.GetImage();
if (clipboardImage == null)
{
ShowNotification("无法获取剪贴板图片");
return;
return Task.CompletedTask;
}
// 创建Image控件
@@ -383,6 +383,7 @@ namespace Ink_Canvas
ShowNotification($"粘贴图片失败: {ex.Message}");
LogHelper.WriteLogToFile($"粘贴图片失败: {ex.Message}", LogHelper.LogType.Error);
}
return Task.CompletedTask;
}
+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); });
}
}
File diff suppressed because it is too large Load Diff
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