Compare commits

..

796 Commits

Author SHA1 Message Date
CJKmkp 87b717f6a9 improve:倒计时音效 2025-12-20 21:04:48 +08:00
PrefacedCorg 51dc45988e Revert "撤回commit8042b91"
This reverts commit 81621cb9d0.
2025-12-20 20:46:18 +08:00
CJKmkp 2f8b986f1f 更新版本号 2025-12-20 20:23:38 +08:00
CJKmkp 7f01e7acb6 improve:点名算法
改进不放回随机
2025-12-20 19:57:11 +08:00
CJKmkp d011d2ba8a improve:多指书写
优化性能
2025-12-20 19:55:37 +08:00
CJKmkp a922654c17 improve:多指书写 2025-12-20 19:36:16 +08:00
CJKmkp e70a486362 add:一言API
新增名言种类
2025-12-20 19:28:47 +08:00
CJKmkp 1683bc8418 improve:自动更新
优化线路自动测速
2025-12-20 19:18:58 +08:00
CJKmkp b6368fb0e4 add:有更新时系统级弹窗 2025-12-20 19:16:39 +08:00
CJKmkp ddfa9c2676 improve:PPT翻页打断
改进打断逻辑
2025-12-20 18:42:25 +08:00
CJKmkp f3ddd5a11a improve:PPT翻页打断
新增设置项
2025-12-20 18:37:57 +08:00
CJKmkp 39e8b2359e improve:PPT自动播放及记忆上次播放页码
改进弹窗出现时机
2025-12-20 18:25:07 +08:00
CJKmkp 4fb73c155b improve:PPT翻页打断 2025-12-20 18:11:27 +08:00
CJKmkp 3287603d0a fix:快捷键问题 2025-12-20 18:04:08 +08:00
CJKmkp 1abb317054 add;自动清理过期配置
避免老版本配置保留
2025-12-20 17:52:56 +08:00
CJKmkp 719e37c26b improve:清空点名名单
改进清空历史记录
2025-12-20 17:50:00 +08:00
CJKmkp 45d2f99fd7 improve:历史版本回滚
支持最近不再接收更新
2025-12-20 17:45:35 +08:00
CJKmkp f75fdded98 fix:开关显示异常 2025-12-20 17:38:36 +08:00
CJKmkp 22a6d87771 fix:几何绘制中断
解决绘制过程中增加触摸点导致后续失效
2025-12-20 17:33:50 +08:00
CJKmkp 2479da4bbc fix:错误注释 2025-12-20 17:31:57 +08:00
CJKmkp c22424d798 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-12-20 17:31:03 +08:00
CJKmkp bbb30b7c25 improve:几何长按状态显示 2025-12-20 17:30:18 +08:00
CJKmkp 71cbe12cee improve:浮动栏按钮UI
优化按钮显示
2025-12-20 17:28:11 +08:00
CJKmkp 011effa047 优化代码 2025-12-20 17:22:17 +08:00
PrefacedCorg b1648dd702 更改避免全屏的解释说明 2025-12-20 17:07:52 +08:00
CJKmkp 81621cb9d0 撤回commit8042b91 2025-12-20 16:05:37 +08:00
PrefacedCorg 3f460d7a5c 代码清理 2025-12-20 13:56:46 +08:00
PrefacedCorg 6fe34c1250 feat: 白板和批注模式自动全屏 2025-12-20 13:28:47 +08:00
PrefacedCorg d5cac938c2 feat: 启用避免全屏助手时自动全屏处理白板和批注模式
- 进入白板/批注模式时自动全屏
- 退出时恢复工作区域大小
- 添加状态跟踪避免重复处理
- PPT放映模式和白板模式切换时正确管理全屏状态
2025-12-20 13:03:31 +08:00
CJKmkp 6222fabdd4 add:新下载新路 2025-12-13 22:01:21 +08:00
CJKmkp 8ed4f25499 improve:GitHub工作流 2025-12-13 21:12:18 +08:00
CJKmkp 3afd4641cd 更新版本号 2025-12-13 21:06:31 +08:00
CJKmkp e9fde97453 improve:GitHub工作流 2025-12-13 21:02:11 +08:00
CJKmkp 441f8b6e26 优化按钮名称 2025-12-13 20:47:30 +08:00
CJKmkp 8042b917a0 improve:浮动栏按钮UI
优化按钮显示
2025-12-13 20:45:31 +08:00
CJKmkp 343e7281fe improve:端点吸附
避免点线和虚线吸附
2025-12-13 20:32:43 +08:00
CJKmkp c64e6a4554 improve:issue #252
改进双指拖动
2025-12-13 20:22:23 +08:00
CJKmkp 8190bf275c Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-12-13 20:13:14 +08:00
CJKmkp e792f2637d improve:PPT墨迹加载 2025-12-13 20:13:08 +08:00
CJKmkp 3cd26323dc improve:翻页墨迹加载及浮动栏定位 2025-12-13 20:07:25 +08:00
PrefacedCorg 40e1c4d467 Update MW_FloatingBarIcons.cs 2025-12-13 19:54:10 +08:00
PrefacedCorg 86c22d373a Revert "feat: 添加白板模式自动全屏功能" 2025-12-13 19:48:21 +08:00
PrefacedCorg cb7a76efc5 Delete .vscode directory 2025-12-13 19:39:08 +08:00
PrefacedCorg 545425c4d3 Delete .kiro/steering directory 2025-12-13 19:38:34 +08:00
PrefacedCorg eb1aaa10e4 feat: 添加白板模式自动全屏功能
- 新增"白板模式自动全屏"设置选项
- 进入白板模式时自动全屏,退出时恢复工作区域大小
- 需配合"避免全屏助手"功能使用
- 用户可独立控制白板模式的全屏行为
2025-12-13 19:27:47 +08:00
CJKmkp daf0db312b improve:仅PPT模式
优化浮动栏显示
2025-12-13 18:02:05 +08:00
CJKmkp 8b2bc2f064 improve:退出放映时收纳及PPT记忆上次播放页数 2025-12-13 17:38:35 +08:00
CJKmkp 2e343cbbf9 improve:计时器
优化计时器行为
2025-12-13 17:20:18 +08:00
CJKmkp a75f0470bc improve:点名算法
缩小极差
2025-12-13 17:12:00 +08:00
CJKmkp 287d31a3a9 improve:窗口模式 2025-12-13 17:03:58 +08:00
CJKmkp 430fff0515 优化注释 2025-12-06 23:04:27 +08:00
CJKmkp fbbb7b8ad7 improve:issue #252
改进几何绘制
2025-12-06 23:01:41 +08:00
CJKmkp 54b74d7411 improve:issue #252
改进手掌擦及手势
2025-12-06 22:55:55 +08:00
CJKmkp 82dba31b2a improve:直线拉直
改进高精度拉直
2025-12-06 22:06:28 +08:00
CJKmkp 38d7e782e0 improve:issue #252
修复了几何绘制和多指书写状态问题
2025-11-30 11:51:19 +08:00
CJKmkp 05e5ceeb43 improve:计时器
改进主题加载
2025-11-29 23:11:01 +08:00
CJKmkp fcbbad71d2 更新版本号 2025-11-29 22:23:14 +08:00
CJKmkp 6f9161439f improve:直线拉直
改进直线拉直算法
2025-11-29 22:20:13 +08:00
CJKmkp aa0c4fb841 improve:Dlass服务
优化上传体验
2025-11-29 17:26:47 +08:00
CJKmkp cff50d1f81 fix:快捷键管理 2025-11-29 17:26:41 +08:00
CJKmkp b8581b6368 improve:点名算法
优化概率模型
2025-11-29 16:58:40 +08:00
CJKmkp 094f1223d1 improve:崩溃日志位置 2025-11-29 16:46:33 +08:00
CJKmkp 6802476afa improve:计时器UI
将计时器窗口整合至主窗口,优化全屏计时逻辑
2025-11-29 16:27:35 +08:00
CJKmkp a0539dce9b 更新版本号 2025-11-15 21:36:49 +08:00
CJKmkp bf2b8fec35 improve:计时器UI与点名UI
改进视觉反馈和按钮逻辑及窗口置顶
2025-11-15 21:14:08 +08:00
CJKmkp 082c9a03ec fix:issue #287 2025-11-15 20:48:04 +08:00
CJKmkp 4ccdd862ba improve:PPT模块
修复无焦点状态下键盘翻页无效
2025-11-15 20:24:28 +08:00
CJKmkp e5a20ed0fc improve:点名历史查看 2025-11-15 20:20:56 +08:00
CJKmkp a72022704e improve:点名算法 2025-11-15 19:34:52 +08:00
CJKmkp 2acc7ada30 improve:错误检测 2025-11-15 19:31:57 +08:00
CJKmkp e61882c331 improve:点名
点名算法优化
2025-11-15 19:20:35 +08:00
CJKmkp 61c145689a fix:issue #281 2025-11-15 18:41:42 +08:00
CJKmkp 40ea9664a7 improve:快抽置顶 2025-11-15 18:37:23 +08:00
CJKmkp bccd2d0f3e improve:PPT墨迹保存 2025-11-15 18:19:53 +08:00
CJKmkp b918809dca improve:线擦擦除 2025-11-15 18:06:36 +08:00
CJKmkp e7c2e92879 improve:PPT联动及墨迹管理
改回原处理模式,优化翻页操作
2025-11-15 18:04:22 +08:00
CJK_mkp cf03c921a7 更新版本号 2025-11-08 23:16:36 +08:00
CJK_mkp 2c45c839b1 Revert "improve:无焦点模式"
This reverts commit 83f5fc58d1.
2025-11-08 23:09:04 +08:00
CJK_mkp 83f5fc58d1 improve:无焦点模式 2025-11-08 22:55:18 +08:00
CJK_mkp 1a267f1e5a improve:点名 2025-11-08 22:23:44 +08:00
CJK_mkp c3fd5551d8 improve:快抽按钮 2025-11-08 22:17:52 +08:00
CJK_mkp 1baa74bb69 improve:快抽按钮 2025-11-08 22:07:38 +08:00
CJK_mkp 261ecefb17 improve:注释 2025-11-08 21:42:33 +08:00
CJK_mkp d81d8f7c5d improve:Dlass联动 2025-11-08 21:01:50 +08:00
CJK_mkp de1af12157 更新版本号 2025-11-08 20:41:43 +08:00
CJK_mkp 803cbbdee9 improve:默认设置 2025-11-08 20:29:18 +08:00
CJK_mkp 008477d5fa improve:默认设置 2025-11-08 20:17:51 +08:00
CJK_mkp 7f0d29ebd2 improve:快抽窗口 2025-11-08 20:07:00 +08:00
CJK_mkp 11bf8cffb2 improve:PPT墨迹加载 2025-11-08 19:55:10 +08:00
CJK_mkp 87b9ebc7e1 improve:PPT墨迹显示 2025-11-08 19:36:05 +08:00
CJK_mkp b89d27411b improve:计时器逻辑 2025-11-08 19:20:26 +08:00
CJK_mkp 24c37f1d3e fix:计时器时间不一致 2025-11-08 19:17:58 +08:00
CJK_mkp 58b0a0a3be fix:负数导致的计时器崩溃 2025-11-08 19:15:16 +08:00
CJK_mkp ed58873a82 fix:issue #272 2025-11-08 19:02:37 +08:00
CJK_mkp a8dcbd4af0 add:点名历史查看 2025-11-08 18:01:56 +08:00
CJK_mkp 4b2f29442a improve:点名 2025-11-07 10:36:24 +08:00
CJK_mkp dfab0d7ddf improvve:点名快抽 2025-11-07 10:35:27 +08:00
CJK_mkp ce1998b701 improve:Dlass 界面UI 2025-11-07 09:46:21 +08:00
CJK_mkp 92c631d6ce improve:操作逻辑 2025-11-02 12:34:50 +08:00
CJK_mkp 01009f9e35 更新版本号 2025-11-02 11:47:52 +08:00
CJK_mkp 74eca093da improve:悬浮快抽按钮 2025-11-02 11:40:27 +08:00
CJK_mkp e7d89e65b2 improve:AutoUpdate 2025-11-02 11:27:46 +08:00
CJK_mkp 24b2bffe8e improve:计时器窗口 2025-11-02 11:12:13 +08:00
CJK_mkp d2906476c8 add:Dlass联动 2025-11-02 10:46:16 +08:00
CJK_mkp 2b31a355ae add:Dlass联动 2025-11-02 10:30:36 +08:00
CJK_mkp b602048186 add:Dlass联动 2025-11-02 10:16:31 +08:00
CJK_mkp 4fb7031060 add:Dlass联动 2025-11-02 10:11:15 +08:00
CJK_mkp 4ef77c2e72 add:Dlass联动 2025-11-02 09:41:53 +08:00
CJK_mkp 72ba1a9f58 add:Dlass联动 2025-11-02 09:29:06 +08:00
CJK_mkp 0c3938b652 Merge pull request #274 from MKStoler1024/patch-1
Readme
2025-11-01 22:18:46 +08:00
CJK_mkp 16f80adb0d fix:issue #210 2025-11-01 22:12:47 +08:00
CJK_mkp 637b6bb4f9 fix:issue #210 2025-11-01 22:05:19 +08:00
CJK_mkp 12e91927a5 fix:issue #210 2025-11-01 21:56:31 +08:00
CJK_mkp 8f6f22ba7f 更新版本号 2025-11-01 21:37:55 +08:00
CJK_mkp b520d6a334 fix:issue #210 2025-11-01 21:00:23 +08:00
CJK_mkp b7bff30445 improve:UI 2025-11-01 20:49:37 +08:00
CJK_mkp 0d790bbd80 add:墨迹自动保存 2025-11-01 20:42:18 +08:00
CJK_mkp 8394e99127 improve:主题切换 2025-11-01 20:26:52 +08:00
CJK_mkp 16ae32bfd7 improve:点名UI 2025-11-01 19:17:52 +08:00
CJK_mkp cc6423e384 improve:点名UI 2025-11-01 18:56:38 +08:00
CJK_mkp 06cc587599 improve:启动动画 2025-11-01 18:46:04 +08:00
CJK_mkp 9d36088f1d fix:issue #210 2025-11-01 18:39:52 +08:00
CJK_mkp f9f73b015c improve:启动动画 2025-11-01 18:31:27 +08:00
CJK_mkp 77ffd696bb fix:手势面板不显示 2025-11-01 18:29:58 +08:00
CJK_mkp bdb8bed053 fix:issue #210 2025-11-01 18:25:35 +08:00
MKStoler1024 4b17c8e96e chore: remove sth in readme 2025-10-31 17:56:02 +08:00
MKStoler1024 8109711f4e chore: readme 2025-10-31 16:07:03 +08:00
CJK_mkp 327eba3fa7 Update MW_FloatingBarIcons.cs 2025-10-31 15:35:27 +08:00
CJK_mkp f3dccb2e99 Update MW_FloatingBarIcons.cs 2025-10-31 14:46:20 +08:00
CJK_mkp 7112d58e7c Update MW_ShapeDrawing.cs 2025-10-31 14:44:59 +08:00
CJK_mkp 6e0aad853c Update MW_TouchEvents.cs 2025-10-31 14:44:05 +08:00
CJK_mkp c64b1d0846 Update MW_BoardIcons.cs 2025-10-31 14:43:15 +08:00
CJK_mkp 6eba16ce99 Update MW_BoardIcons.cs 2025-10-31 12:19:30 +08:00
CJK_mkp 2f6f719843 Update MW_ShapeDrawing.cs 2025-10-31 12:18:45 +08:00
CJK_mkp f34bac49e4 Update MW_TouchEvents.cs 2025-10-31 12:16:52 +08:00
CJK_mkp 39cdc6231f Update MW_FloatingBarIcons.cs 2025-10-31 12:16:05 +08:00
CJK_mkp 745e24da70 Update MW_FloatingBarIcons.cs 2025-10-31 12:15:26 +08:00
CJK_mkp a4f4f4fb15 Update MW_TouchEvents.cs 2025-10-31 12:12:51 +08:00
CJK_mkp 3833c229c6 Update MW_FloatingBarIcons.cs 2025-10-31 12:11:02 +08:00
CJK_mkp 3dc3e9b5a8 Update MW_FloatingBarIcons.cs 2025-10-31 10:49:10 +08:00
CJK_mkp 12eeb79e9f 回滚一下 2025-10-30 12:02:29 +08:00
CJK_mkp e08c84f70d improve:点名快抽 2025-10-27 17:54:58 +08:00
CJK_mkp 10f55d5b65 fix:抽选结果截断 2025-10-27 17:45:41 +08:00
CJKmkp 761992d089 Delete PositionConverters.cs 2025-10-26 00:31:32 +08:00
CJKmkp 991a823700 更新版本号 2025-10-26 00:24:28 +08:00
CJKmkp 61dbcf762c add:点名快抽 2025-10-26 00:21:10 +08:00
CJKmkp 60b0149a9c add:新点名UI 2025-10-26 00:00:13 +08:00
CJKmkp 501c034cfa fix:点名UI按钮主题显示 2025-10-25 21:14:59 +08:00
CJKmkp 11593db23c 更新版本号 2025-10-25 20:14:07 +08:00
CJKmkp a9ae2a004f improve:注释 2025-10-25 20:09:04 +08:00
CJKmkp 7b2a31781c 优化代码 2025-10-25 19:57:56 +08:00
CJKmkp ab73eb9632 优化代码 2025-10-25 19:44:32 +08:00
CJKmkp 8ade170b4e improve:计时器数字 2025-10-25 17:27:19 +08:00
CJKmkp 91c3d1161d fix:issue #210 2025-10-25 17:03:44 +08:00
CJK_mkp bb63805e87 fix:issue #210
Removed the SimulateMultiTouchToggle method and its related comments.
2025-10-23 16:27:39 +08:00
CJK_mkp 8caf04990c 更新信息 2025-10-22 17:30:38 +08:00
CJK_mkp f28dd0a965 Update AssemblyInfo.cs 2025-10-22 17:29:12 +08:00
CJK_mkp b5d83268e5 更新信息 2025-10-22 17:28:56 +08:00
CJK_mkp 19adc42122 Change PPTNavigationBtn_MouseUp to async method 2025-10-22 12:18:03 +08:00
CJK_mkp 8afee913be Update MW_PPT.cs 2025-10-22 12:16:42 +08:00
CJK_mkp 82d101365f Update MW_PPT.cs 2025-10-22 12:12:43 +08:00
CJK_mkp 73e679f268 Make PPTNavigationBtn_MouseDown async
Changed PPTNavigationBtn_MouseDown to be asynchronous and added a delay for animation.
2025-10-22 12:08:29 +08:00
CJK_mkp 372a8a1de1 fix:PPT状态浮动栏位置异常 2025-10-20 17:45:14 +08:00
CJK_mkp 171cc34c91 撤销修改 2025-10-20 12:10:47 +08:00
CJK_mkp fbd217d674 撤销修改8d778a
Removed quick color palette visibility logic from ink editing mode.
2025-10-20 10:49:04 +08:00
CJKmkp f24960ab26 improve:UIA窗口置顶 2025-10-19 01:34:03 +08:00
CJKmkp d1a871c8f6 Reapply "fix:issue # 262"
This reverts commit 086b97906b.
2025-10-18 21:20:54 +08:00
CJKmkp 086b97906b Revert "fix:issue # 262"
This reverts commit 5bfd0c7b2f.
2025-10-18 21:16:24 +08:00
CJKmkp db76a11347 improve:窗口置顶 2025-10-18 20:45:23 +08:00
CJKmkp 729b10cce8 Reapply "improve:UIA窗口置顶"
This reverts commit 4cbd25ccb3.
2025-10-18 20:41:09 +08:00
CJKmkp 4cbd25ccb3 Revert "improve:UIA窗口置顶"
This reverts commit 0ef7b738a9.
2025-10-18 20:38:08 +08:00
CJKmkp 0ef7b738a9 improve:UIA窗口置顶 2025-10-18 20:15:40 +08:00
CJKmkp 33b5563601 更新版本号 2025-10-18 19:05:16 +08:00
CJKmkp f6f799f534 improve:墨迹清空 2025-10-18 19:03:30 +08:00
CJKmkp 8c3f1360e1 improve:注释 2025-10-18 18:45:08 +08:00
CJKmkp fafbabd603 improve:清空墨迹 2025-10-18 18:42:29 +08:00
CJKmkp 254e38895c fix:无焦点功能丢失 2025-10-18 18:29:52 +08:00
CJKmkp aa0bb22cdd add:UIA窗口置顶 2025-10-18 18:26:13 +08:00
CJKmkp 5bfd0c7b2f fix:issue # 262 2025-10-18 17:36:47 +08:00
CJKmkp c3885c170c improve:PPT墨迹管理 2025-10-18 17:24:47 +08:00
CJKmkp 88a7cce269 fix:issue #133 2025-10-18 16:46:09 +08:00
CJKmkp a1fccc2905 fix:PPT上次页数记忆跳转 2025-10-18 16:41:55 +08:00
CJKmkp a889041896 fix:issue #260 2025-10-18 16:31:07 +08:00
CJKmkp 8d778aba2c improve:PPT模块 2025-10-18 16:27:26 +08:00
CJKmkp ba5db63e0b improve:计时器UI 2025-10-18 16:15:19 +08:00
CJKmkp 0259d83429 improve:计时器 2025-10-12 18:06:44 +08:00
CJKmkp b6999e57ae improve:计时器 2025-10-12 18:05:40 +08:00
CJKmkp 04184cf731 improve:计时器 2025-10-12 17:44:44 +08:00
CJKmkp 04f98eb9e7 improve:计时器 2025-10-12 17:17:51 +08:00
CJKmkp 869dd045af improve:计时器 2025-10-12 16:08:36 +08:00
CJK_mkp 339ebb862e improve:计时器UI 2025-10-08 17:34:12 +08:00
CJKmkp 7b8598bf9f Revert "Update MainWindow.xaml.cs"
This reverts commit 298164d6ed.
2025-10-06 22:41:53 +08:00
CJKmkp ab1c460225 Reapply "add:手动更新"
This reverts commit 703a8a4e0d.
2025-10-06 22:41:42 +08:00
CJKmkp 298164d6ed Update MainWindow.xaml.cs 2025-10-06 22:39:18 +08:00
CJKmkp 703a8a4e0d Revert "add:手动更新"
This reverts commit 1fd95a2f2e.
2025-10-06 22:36:59 +08:00
CJKmkp deaf5fcbf6 更新版本号 2025-10-06 22:05:27 +08:00
CJKmkp 18b46689f1 improve:计时器 2025-10-06 21:49:19 +08:00
CJKmkp edbe3f8311 improve:计时器 2025-10-06 21:17:14 +08:00
CJKmkp 94c4c3e2d4 improve:计时器UI 2025-10-06 21:08:47 +08:00
CJKmkp 077f72737d improve:计时器UI 2025-10-06 21:05:22 +08:00
CJKmkp ae089a9390 improve:计时器UI 2025-10-06 20:57:33 +08:00
CJKmkp 0f087c2aa6 improve:计时器UI 2025-10-06 20:46:19 +08:00
CJKmkp 19d2ffb48e improve:计时器UI 2025-10-06 20:44:09 +08:00
CJKmkp a9bb6f73de improve:计时器UI 2025-10-06 20:33:50 +08:00
CJKmkp ca124732fa improve:计时器 2025-10-06 20:23:55 +08:00
CJKmkp 11da14aeab improve:计时器 2025-10-06 20:11:49 +08:00
CJKmkp 7d5f037c85 improve:计时器 2025-10-06 19:59:01 +08:00
CJKmkp f62b415227 improve:计时器UI 2025-10-06 19:56:00 +08:00
CJKmkp f2b8d4014e improve:计时器UI 2025-10-06 19:46:14 +08:00
CJKmkp 3ef047fb41 improve:计时器UI 2025-10-06 19:43:04 +08:00
CJKmkp aa3be4ab0d improve:计时器UI 2025-10-06 19:05:22 +08:00
CJKmkp 7500049ea2 improve:计时器UI 2025-10-06 19:03:10 +08:00
CJKmkp 44600df75c improve:计时器UI 2025-10-06 18:44:00 +08:00
CJKmkp dec2a15773 improve:计时器UI 2025-10-06 18:43:14 +08:00
PrefacedCorg 4da78a04cb 代码清理 2025-10-06 18:29:12 +08:00
CJKmkp 98ec204bab improve:计时器UI 2025-10-06 18:03:15 +08:00
CJKmkp cd9499b064 improve:计时器UI 2025-10-06 17:55:05 +08:00
CJKmkp f5e824be86 improve:计时器 2025-10-06 17:49:43 +08:00
CJKmkp c76254f4f9 improve:计时器UI 2025-10-06 17:28:52 +08:00
CJKmkp 77ac6f88ca improve:计时器UI 2025-10-06 17:28:32 +08:00
CJKmkp 7e10911991 improve:计时器UI 2025-10-06 17:23:16 +08:00
CJKmkp 161b67b09d improve:计时器UI 2025-10-06 17:17:56 +08:00
CJKmkp 9614536a29 improve:计时器UI 2025-10-06 17:14:40 +08:00
CJKmkp 8ba7aab468 improve:计时器UI 2025-10-06 17:08:38 +08:00
CJKmkp 4ea9f79de1 improve:计时器UI 2025-10-06 17:06:02 +08:00
CJKmkp b7f7025d97 improve:计时器UI 2025-10-06 17:00:27 +08:00
CJKmkp 20d5dd2668 improve:计时器UI 2025-10-06 16:53:00 +08:00
CJKmkp adc2d02fbb Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-10-06 16:39:08 +08:00
CJK_mkp ef2dbdc93b Merge pull request #258 from LiuYan-xwx/beta
feat: 优化滑动动画与设置面板逻辑
2025-10-06 16:38:11 +08:00
CJKmkp 9721ec1f0b improve:计时器UI 2025-10-06 16:35:21 +08:00
LiuYan-xwx 4d069d87d7 feat: 优化滑动动画与设置面板逻辑
- 调整滑动动画的目标位置,更新 From 和 To 属性值。
- 移动并优化 `BorderSettings.Visibility` 的设置逻辑。
- 提升代码可读性,减少重复代码,改进动画效果。
2025-10-06 16:32:33 +08:00
CJKmkp 0308f9ce65 improve:计时器UI 2025-10-06 16:30:55 +08:00
CJKmkp 6bcd3cb217 improve:计时器UI 2025-10-06 16:29:03 +08:00
CJKmkp 7f83c490db improve:计时器UI 2025-10-06 16:14:52 +08:00
CJKmkp bbf9c895b8 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-10-06 16:06:15 +08:00
CJKmkp ffa2063c52 improve:计时器UI 2025-10-06 16:04:03 +08:00
CJKmkp d464b1f78e improve:计时器UI 2025-10-06 15:59:48 +08:00
CJKmkp 92cb071408 improve:计时器UI 2025-10-06 15:52:38 +08:00
PrefacedCorg 9141b60d03 Merge pull request #257 from LiuYan-xwx/beta
Update .gitignore
2025-10-06 15:44:52 +08:00
LiuYan-xwx df0a196931 Update .gitignore 2025-10-06 15:41:52 +08:00
CJKmkp a8cb1dd495 improve:计时器 2025-10-06 15:26:46 +08:00
CJKmkp adc4966d49 improve:计时器 2025-10-06 15:00:42 +08:00
CJKmkp 1b2ea8c522 improve:计时器UI 2025-10-06 14:55:52 +08:00
CJKmkp 7c8bdb489b improve:计时器 2025-10-06 14:50:56 +08:00
CJKmkp 16458fbb42 improve:计时器 2025-10-06 14:31:54 +08:00
CJKmkp c72839cdcb improve:计时器 2025-10-06 14:31:04 +08:00
CJKmkp a31ad5803c improve:计时器 2025-10-06 14:28:53 +08:00
CJKmkp cf800cbd36 improve:自动更新 2025-10-06 14:19:24 +08:00
CJKmkp 3c06ef0b1a 优化注释 2025-10-06 14:11:32 +08:00
CJKmkp 2c3b921f09 improve:计时器逻辑 2025-10-06 13:39:31 +08:00
CJKmkp 402ecc66ae improve:计时器逻辑 2025-10-06 13:38:12 +08:00
CJKmkp e25f56a9b5 improve:计时器逻辑 2025-10-06 13:34:52 +08:00
CJKmkp b28fa887a2 improve:计时器逻辑 2025-10-06 13:31:54 +08:00
CJKmkp f83a02e619 improve:UI 2025-10-06 13:15:03 +08:00
CJKmkp eaad089d68 更新版本号 2025-10-06 09:56:07 +08:00
CJKmkp a2fda16df9 improve:主题切换 2025-10-05 23:58:39 +08:00
CJKmkp e9e8ff57ae improve:主题切换 2025-10-05 23:22:02 +08:00
CJKmkp 1fd95a2f2e add:手动更新 2025-10-05 23:09:05 +08:00
CJK_mkp 228584ee48 Update README.md 2025-10-05 21:31:30 +08:00
CJK_mkp c651df0f6e Update README.md 2025-10-05 21:27:56 +08:00
CJKmkp 5cced9baf2 fix:图标数组错误 2025-10-05 21:13:37 +08:00
CJKmkp 5dd26e554b 更新版本号 2025-10-05 12:20:20 +08:00
CJK_mkp 696fd3e8cd Update privacy.txt 2025-10-05 12:07:42 +08:00
CJKmkp 1d19b705d3 fix:issue #202 2025-10-05 11:49:22 +08:00
CJKmkp d54074cb57 improve:主题切换 2025-10-05 09:44:54 +08:00
CJKmkp cc054aeb75 improve:主题切换 2025-10-05 09:33:25 +08:00
CJKmkp c32eaed534 improve:UI 2025-10-05 09:29:31 +08:00
CJKmkp fa23f73ec4 improve:UI 2025-10-05 09:26:14 +08:00
CJKmkp fbf6a13f92 improve:主题切换 2025-10-05 09:16:36 +08:00
CJKmkp 3eba662772 improve:主题切换 2025-10-05 09:14:29 +08:00
CJKmkp e007ee271f improve:主题切换 2025-10-05 08:58:16 +08:00
CJKmkp 59a8d65a89 improve:主题切换 2025-10-05 08:57:04 +08:00
CJKmkp 793519ae1b improve:主题切换 2025-10-05 08:55:18 +08:00
CJKmkp 5c44062aa2 improve:主题切换 2025-10-05 00:26:18 +08:00
CJKmkp fe03a2c2c0 improve:主题切换 2025-10-05 00:24:23 +08:00
CJKmkp d1d74a3770 improve:主题切换 2025-10-05 00:18:58 +08:00
CJKmkp 16108bf779 add:图标 2025-10-04 23:49:12 +08:00
CJKmkp 5bebf077e4 improve:主题切换 2025-10-04 22:59:11 +08:00
CJKmkp efcc01ad6b improve:主题切换 2025-10-04 22:44:47 +08:00
CJKmkp e8a4b45446 improve:主题切换 2025-10-04 22:39:32 +08:00
CJKmkp 6cba297d77 improve:主题切换 2025-10-04 22:37:46 +08:00
CJKmkp b149dc3cb9 improve:主题切换 2025-10-04 22:31:42 +08:00
CJKmkp 1ee4f934b9 improve:主题切换 2025-10-04 22:30:40 +08:00
CJKmkp f7b4adb85d Revert "improve:主题"
This reverts commit da3c41567d.
2025-10-04 22:12:47 +08:00
CJKmkp da3c41567d improve:主题 2025-10-04 22:08:30 +08:00
CJKmkp 5f73795220 improve:主题切换 2025-10-04 22:05:58 +08:00
CJKmkp 36bf1122c6 improve:主题切换 2025-10-04 22:04:24 +08:00
CJKmkp 402f8bb9f9 improve:主题切换 2025-10-04 21:55:55 +08:00
CJKmkp eef2a915fa improve:主题 2025-10-04 21:48:44 +08:00
CJKmkp 26ee4d172f improve:主题切换 2025-10-04 21:44:15 +08:00
CJKmkp 08eb450446 improve:主题切换 2025-10-04 21:40:55 +08:00
CJKmkp ecae7d818c improve:主题切换 2025-10-04 21:00:51 +08:00
CJKmkp 47ffccff68 improve:主题切换 2025-10-04 19:59:42 +08:00
CJKmkp 0ba5286c94 improve:主题切换 2025-10-04 18:42:55 +08:00
CJKmkp db8e1e3589 improve:主题切换 2025-10-04 18:29:42 +08:00
CJKmkp d0764a6a77 improve:主题切换 2025-10-04 18:19:43 +08:00
CJKmkp 7b6c347d6b improve:主题切换 2025-10-04 18:17:11 +08:00
CJKmkp d58fec180c improve:主题切换 2025-10-04 18:10:43 +08:00
CJKmkp ac4e4877a1 improve:主题切换 2025-10-04 17:49:44 +08:00
CJKmkp ddea61245d improve:主题切换 2025-10-04 17:47:52 +08:00
CJKmkp 98915bcff2 improve:主题切换 2025-10-04 17:32:17 +08:00
CJKmkp 8e0f0450df improve:主题切换 2025-10-04 17:30:26 +08:00
CJKmkp 1d9a669829 improve:主题切换 2025-10-04 17:26:11 +08:00
CJKmkp 97bcf168b7 improve:主题 2025-10-04 17:15:58 +08:00
CJKmkp e81198165b improve:主题 2025-10-04 17:14:14 +08:00
CJKmkp b03b8da586 improve:主题 2025-10-04 17:12:14 +08:00
CJKmkp d497e07187 improve:主题 2025-10-04 17:10:11 +08:00
CJKmkp f284a99194 improve:UI 2025-10-04 17:03:03 +08:00
CJKmkp 7d6dd6f805 add:仿希沃计时器 2025-10-04 17:00:15 +08:00
CJKmkp d70e28d198 improve:图标 2025-10-04 16:56:15 +08:00
CJKmkp 4c6f138e5c add:图标 2025-10-04 16:49:03 +08:00
CJKmkp 5974841a7b add:仿希沃计时器 2025-10-04 16:21:14 +08:00
CJKmkp 5df54439ba add:仿希沃计时器 2025-10-04 16:14:56 +08:00
CJKmkp 8f627c6b7f add:图标 2025-10-04 14:55:49 +08:00
CJKmkp 3f28bc5c6c improve:自动更新 2025-10-04 14:47:53 +08:00
CJKmkp 903fa699ca improve:侧边栏图标 2025-10-04 09:49:37 +08:00
PrefacedCorg d9e8f64699 1145141919810
代码清理
2025-10-03 17:08:46 +08:00
CJKmkp 8ad6ca8d41 优化代码 2025-10-03 12:26:15 +08:00
CJK_mkp 4017efc65e Merge pull request #248 from InkCanvasForClass/main
合并一下
2025-10-03 11:51:13 +08:00
CJK_mkp 6ed15d52de Merge pull request #247 from InkCanvasForClass/beta
test action
2025-10-03 11:50:15 +08:00
CJKmkp 5246a2f79b test action 2025-10-03 11:48:09 +08:00
CJK_mkp b670932dd7 Merge pull request #246 from InkCanvasForClass/beta
test action
2025-10-03 11:32:51 +08:00
CJKmkp ddf30bd96b test action 2025-10-03 11:31:54 +08:00
CJKmkp 7e92428485 test action 2025-10-03 11:30:28 +08:00
CJKmkp 4c16050c85 test action 2025-10-03 11:29:46 +08:00
CJK_mkp 2ed035525a Merge pull request #245 from InkCanvasForClass/beta
test action
2025-10-03 11:24:14 +08:00
CJKmkp 1b89b0d7b6 test action 2025-10-03 11:23:46 +08:00
CJK_mkp 0364821f0b Merge pull request #244 from InkCanvasForClass/beta
test action
2025-10-03 11:18:13 +08:00
CJKmkp fa9d6f37ae test action 2025-10-03 11:17:42 +08:00
CJK_mkp a4777904b9 Merge pull request #243 from InkCanvasForClass/beta
test action
2025-10-03 11:14:40 +08:00
CJKmkp 0b3c2f95c5 test action 2025-10-03 11:14:16 +08:00
CJK_mkp 8f54955432 Merge pull request #242 from InkCanvasForClass/beta
test action
2025-10-03 11:11:21 +08:00
CJKmkp 034db2fc27 test action 2025-10-03 11:07:58 +08:00
CJKmkp 59141b0241 test action 2025-10-03 11:04:00 +08:00
CJK_mkp 2e43b96a2c Merge pull request #241 from InkCanvasForClass/beta
合并pre-release action
2025-10-03 11:01:21 +08:00
CJKmkp edff96299c test action 2025-10-03 10:59:36 +08:00
CJKmkp 8d9587b790 test action 2025-10-03 10:57:58 +08:00
CJKmkp ad0cf2a849 test action 2025-10-03 10:56:55 +08:00
CJKmkp f928946c61 improve:白板模式切换 2025-10-03 10:47:28 +08:00
CJKmkp 6cc9d0bee2 fix:issue #219 2025-10-03 09:19:07 +08:00
CJKmkp 287e6bb91f fix:issue #219 2025-10-03 09:12:39 +08:00
CJKmkp 3509036d85 fix:issue #219 2025-10-03 09:04:40 +08:00
CJKmkp 752901dbb9 fix:issue #219 2025-10-03 08:55:28 +08:00
CJKmkp e3add94546 fix:issue #219 2025-10-03 08:52:47 +08:00
CJKmkp 8d31c74b39 add:图标 2025-10-02 22:51:44 +08:00
CJKmkp e6f608d206 add:图标 2025-10-02 22:49:29 +08:00
CJKmkp d80b9e29a7 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-10-02 22:30:00 +08:00
CJK_mkp 092674465d Merge pull request #239 from InkCanvasForClass/all-contributors/add-Tayasui-rainnya
docs: add Tayasui-rainnya as a contributor for design
2025-10-02 22:28:55 +08:00
allcontributors[bot] b2e3a5bb18 docs: update .all-contributorsrc 2025-10-02 14:28:32 +00:00
allcontributors[bot] 147a2f957e docs: update README.md 2025-10-02 14:28:31 +00:00
CJKmkp ce56f2919c add:图标 2025-10-02 22:24:20 +08:00
CJKmkp 18059102a3 add:图标 2025-10-02 22:15:07 +08:00
CJKmkp 4542fcccbb 默认线路组改为inkeys 2025-10-02 21:52:55 +08:00
CJKmkp d12009d30c 修改 2025-10-02 20:31:08 +08:00
CJK_mkp d85e0fbd13 Merge pull request #238 from Tayasui-rainnya/beta
添加dark♂icc-ce图标
2025-10-02 20:29:50 +08:00
CJKmkp f7a5f9ae6b improve:启动动画 2025-10-02 19:51:31 +08:00
tayasui rainnya! 3d4c3d0acc dark♂icc-ce 图标 2025-10-02 19:42:24 +08:00
tayasui rainnya! c106c41048 dark♂图标 2025-10-02 19:41:18 +08:00
CJKmkp cea777e8b2 improve:启动动画 2025-10-02 19:25:57 +08:00
CJKmkp 6f069b73da improve:启动动画 2025-10-02 19:20:03 +08:00
CJKmkp c80af8c984 improve:aovidfullsreen 2025-10-02 19:13:20 +08:00
CJKmkp d51cbd0682 improve:aovidfullsreen 2025-10-02 19:08:33 +08:00
CJKmkp 7e94c945ff improve:aovidfullsreen 2025-10-02 19:05:24 +08:00
CJKmkp bd9095b4c2 improve:aovidfullsreen 2025-10-02 19:04:28 +08:00
CJKmkp c135bf8fb8 improve:aovidfullsreen 2025-10-02 18:57:49 +08:00
CJKmkp f6aebb15b4 improve:aovidfullsreen 2025-10-02 18:54:09 +08:00
CJKmkp 2b4f88becd fix:issue #237 2025-10-02 18:48:11 +08:00
CJKmkp 7fbd3639b6 fix:退出白板后收纳 2025-10-02 18:15:39 +08:00
CJKmkp 9e52c2c31d fix:退出白板后收纳 2025-10-02 18:06:08 +08:00
CJKmkp 46a1e5ff14 improve:主题切换 2025-10-02 17:39:56 +08:00
CJKmkp fa7c1fd646 fix:屏蔽压感 2025-10-02 17:04:12 +08:00
CJKmkp c347809eea improve:启动动画 2025-10-02 16:38:40 +08:00
CJKmkp dd09a42f02 improve:启动动画 2025-10-02 16:33:54 +08:00
CJKmkp db8ffd05ea improve:启动动画 2025-10-02 16:22:50 +08:00
CJK_mkp 045c29ca20 Merge pull request #236 from InkCanvasForClass/beta
ICC CE 1.7.12.0
2025-10-02 16:03:39 +08:00
CJKmkp 4ea6d19602 更新版本号 2025-10-02 15:58:33 +08:00
CJKmkp df00447c41 improve:启动动画 2025-10-02 15:36:52 +08:00
CJKmkp db5b1caea7 improve:启动动画 2025-10-02 15:35:53 +08:00
CJKmkp 8432954b8d add:issue #235 2025-10-02 15:30:51 +08:00
CJK_mkp 4a000e23e9 Update README.md 2025-10-02 15:09:27 +08:00
CJKmkp b42db51346 fix:忽略希沃白板五桌面批注窗口 2025-10-02 15:05:55 +08:00
CJKmkp 1ef968ea93 improve:悬浮窗拦截 2025-10-02 14:57:55 +08:00
CJKmkp 5b50537333 improve:开机动画 2025-10-02 14:53:57 +08:00
CJKmkp 8c624b48fb 删除无用信息 2025-10-02 14:08:22 +08:00
CJKmkp ad329fc2c8 improve:悬浮窗拦截 2025-10-02 11:50:00 +08:00
CJKmkp f045b8f659 fix:悬浮窗拦截 2025-10-02 11:45:55 +08:00
CJKmkp 00a9da45bb improve:启动动画 2025-10-02 02:59:49 +08:00
CJKmkp b7ecd12f8c improve:启动动画 2025-10-02 02:55:53 +08:00
CJKmkp a8a3164ee7 improve:启动动画 2025-10-02 02:44:45 +08:00
CJKmkp b1aec69e98 fix:issue #234 2025-10-02 02:32:29 +08:00
CJKmkp d713a8c5a4 优化注释 2025-10-02 02:30:24 +08:00
CJKmkp 3895faf941 fix:issue #133 2025-10-02 02:16:02 +08:00
CJKmkp bf336fdb10 add:issue #214 2025-10-02 02:11:54 +08:00
CJKmkp c96c26288c add:软件启动动画 2025-10-02 01:51:10 +08:00
CJKmkp 83558c089d add:软件启动动画 2025-10-02 00:17:11 +08:00
CJKmkp 26acb72e17 add:软件启动动画 2025-10-02 00:05:09 +08:00
CJKmkp 4724a432ab add:软件启动动画 2025-10-01 23:26:38 +08:00
CJKmkp 5b9c1627c0 add:软件启动动画 2025-10-01 22:38:40 +08:00
CJKmkp d06c2585cf add:软件启动动画 2025-10-01 22:24:49 +08:00
CJKmkp 3e96e51efd delete:office注册表修改 2025-10-01 22:06:23 +08:00
CJKmkp 9afa7191c4 fix:issue #232 2025-10-01 18:55:03 +08:00
CJKmkp dfddec5586 add:软件启动动画 2025-10-01 18:45:26 +08:00
CJKmkp 93022424b3 add:软件启动动画 2025-10-01 18:38:01 +08:00
CJKmkp aed77a187f add:软件启动动画 2025-10-01 18:17:39 +08:00
CJKmkp 583f47c98b 更新版本号 2025-10-01 15:20:14 +08:00
CJK_mkp ff528b3f9d Merge pull request #233 from InkCanvasForClass/beta
ICC CE 1.7.11.5
2025-10-01 15:17:17 +08:00
CJKmkp 6d98d96ccb fix:issue #217 2025-10-01 14:13:09 +08:00
CJKmkp 6f8e08c21a fix:issue #217 2025-10-01 13:03:11 +08:00
CJKmkp 3a7417f493 优化代码 2025-10-01 12:57:16 +08:00
CJKmkp 66164a0c33 improve:配置文件损坏自动恢复 2025-10-01 11:34:37 +08:00
CJKmkp 0d7a478e16 improve:配置文件损坏自动恢复 2025-10-01 11:29:59 +08:00
CJKmkp 9caef310df 优化代码 2025-10-01 10:04:40 +08:00
CJKmkp 688f742c16 add:老版UI切换 2025-10-01 09:52:15 +08:00
CJKmkp 38dd083cdc add:老版UI切换 2025-10-01 09:48:44 +08:00
CJKmkp 851bd1c3c6 improve:主题切换 2025-10-01 09:13:48 +08:00
CJKmkp c91b8a1a7a add:issue #224 2025-10-01 00:54:03 +08:00
CJKmkp 69c45764b2 Revert "add:issue #224"
This reverts commit 07a62d2f78.
2025-10-01 00:53:22 +08:00
CJKmkp 07a62d2f78 add:issue #224 2025-10-01 00:49:12 +08:00
CJKmkp 46b064b0a8 add:配置文件损坏自动恢复 2025-10-01 00:01:35 +08:00
CJKmkp 1d3b96bb65 更新Action 2025-09-30 23:45:40 +08:00
CJKmkp 916425b7f5 更新Action 2025-09-30 23:38:58 +08:00
CJKmkp 1c570ca242 fix:issue #210 2025-09-30 23:18:16 +08:00
CJKmkp ae41108971 优化代码 2025-09-30 22:56:32 +08:00
CJKmkp 395e7609a4 fix:issue #210 2025-09-30 22:55:06 +08:00
CJKmkp 185c9dc284 Reapply "fix:issue #201"
This reverts commit f40ef15a24.
2025-09-30 19:16:56 +08:00
CJKmkp a9b0ac0595 Revert "优化代码"
This reverts commit 92bb458345.
2025-09-30 19:15:03 +08:00
CJKmkp d9e3524211 fix:issue #201 2025-09-30 19:10:34 +08:00
CJKmkp f40ef15a24 Revert "fix:issue #201"
This reverts commit 9b5d157333.
2025-09-30 19:02:45 +08:00
CJKmkp 4d7544eefb deletefix:issue #210 2025-09-30 18:45:57 +08:00
CJKmkp 92bb458345 优化代码 2025-09-30 17:46:35 +08:00
CJKmkp 92e695ef7c fix:issue #213 2025-09-30 17:22:32 +08:00
CJKmkp 45ff3fbb9b fix:issue #213 2025-09-30 17:18:00 +08:00
CJKmkp ba252e92d3 fix:issue #229 2025-09-30 16:55:44 +08:00
CJK_mkp 4778f459e3 优化日志 2025-09-29 18:19:36 +08:00
CJK_mkp c997854ae0 优化日志 2025-09-29 18:14:39 +08:00
CJK_mkp 65b5322ad5 优化日志 2025-09-29 18:13:11 +08:00
CJK_mkp dd43bf0476 优化日志 2025-09-29 18:11:21 +08:00
CJKmkp 15c808eebd 更新版本号 2025-09-27 18:00:12 +08:00
CJKmkp 28748a99ca improve:手掌擦 2025-09-27 17:58:50 +08:00
CJKmkp fb37d3b9e6 improve:更新弹窗 2025-09-27 17:52:25 +08:00
CJKmkp e1f10e054c improve:issue #223 2025-09-27 17:09:06 +08:00
CJKmkp c670357c01 improve:橡皮擦 2025-09-27 17:01:33 +08:00
CJK_mkp 92dce9b36e Update MainWindow.xaml 2025-09-26 22:14:12 +08:00
PrefacedCorg aa2b62e8da 修复退出放映按钮字体显示不完全
把字体调小了()
2025-09-22 20:23:36 +08:00
CJKmkp a34a65f354 优化代码 2025-09-22 13:26:53 +08:00
CJKmkp eea5f8496c fix:高光显示 2025-09-22 13:11:22 +08:00
CJKmkp 091a256bcc Revert "Replace SymbolIcon with FontIcon throughout UI"
This reverts commit 37a69032f6.
2025-09-22 13:09:43 +08:00
CJKmkp 14c9ce3ce1 fix:issue #219 2025-09-22 12:32:53 +08:00
CJKmkp fd1e5e13fe fix:进入PPT自动收纳 2025-09-22 12:11:03 +08:00
CJKmkp 19685b8f95 fix:进入PPT自动收纳 2025-09-22 11:58:54 +08:00
CJKmkp a9da8dc10c fix:进入PPT自动收纳 2025-09-22 11:48:05 +08:00
CJKmkp fcfba7a978 fix:进入PPT自动收纳 2025-09-22 11:36:55 +08:00
PrefacedCorg 2c8c4351dd Update WenXiang.png 2025-09-25 10:26:38 +08:00
PrefacedCorg 79264b187d Remove unused exception variable in catch block
The exception variable 'ex' in the catch block was unused and has been removed for cleaner code.
2025-09-22 09:44:38 +08:00
PrefacedCorg 52eba9117b Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-22 09:34:45 +08:00
PrefacedCorg 37a69032f6 Replace SymbolIcon with FontIcon throughout UI
Updated all XAML and code-behind references from SymbolIcon/Symbol to FontIcon/Glyph for consistency and compatibility with the UI framework. This affects button icons, window controls, and shape drawing logic across multiple windows and components.
2025-09-22 09:34:39 +08:00
CJKmkp dd16b4f5a1 更新版本号 2025-09-21 08:28:32 +08:00
CJKmkp e9c20255a6 imporve:PPT墨迹保存 2025-09-21 08:26:55 +08:00
CJKmkp 630b4edf91 improve:PPT墨迹保存 2025-09-21 07:52:11 +08:00
CJKmkp 04ef638e17 fix:issue #190 2025-09-21 07:43:29 +08:00
CJKmkp 35bad240c2 更新版本号 2025-09-21 02:08:29 +08:00
CJKmkp 0cb4749cf3 add:主题切换 2025-09-21 02:05:49 +08:00
CJKmkp 7c8281eb00 add:主题切换 2025-09-21 01:35:13 +08:00
CJKmkp 1957288219 add:主题切换 2025-09-21 01:15:01 +08:00
CJKmkp c4d2b15c48 add:主题切换 2025-09-21 01:06:42 +08:00
CJKmkp beebfb0dae add:主题切换 2025-09-21 00:39:30 +08:00
CJKmkp d67172b795 优化代码 2025-09-21 00:28:30 +08:00
CJKmkp 2b012bc042 add:主题切换 2025-09-21 00:25:09 +08:00
CJKmkp 84da68f950 improve:历史版本回滚窗口 2025-09-20 22:27:26 +08:00
CJKmkp ad97fd13bf improve:更新弹窗 2025-09-20 22:21:05 +08:00
CJKmkp ebf6d0d5f7 improve:历史版本回滚窗口 2025-09-20 22:20:13 +08:00
CJKmkp 0f3b4b4384 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-20 22:02:43 +08:00
PrefacedCorg 226cf46d6b Merge pull request #212 from Hao3288/beta
readme文件中的部分url地址填写错误
2025-09-20 21:55:56 +08:00
CJKmkp 5689e33541 improve:更新弹窗 2025-09-20 21:54:02 +08:00
NoobHao 15e59e7117 Update README.md 2025-09-20 21:46:43 +08:00
NoobHao 6b538f6662 Update README.md 2025-09-20 21:45:38 +08:00
CJKmkp ad6808b696 improve:图片拖动 2025-09-20 21:33:21 +08:00
CJKmkp 8347b18efc 优化代码 2025-09-20 21:25:30 +08:00
CJKmkp f92b132f0a 优化代码 2025-09-20 19:49:16 +08:00
CJKmkp 79057d6757 fix:issue #160 2025-09-20 19:36:00 +08:00
CJKmkp a9b5ee8f62 fix:issue #205 2025-09-20 19:03:00 +08:00
CJKmkp 65da2c449e 更新版本号 2025-09-20 17:13:04 +08:00
CJKmkp 0ef8c12650 improve:issue #180 2025-09-20 17:06:11 +08:00
PrefacedCorg 1dfa48aecb Update MainWindow.xaml 2025-09-20 16:42:35 +08:00
PrefacedCorg b5d1713b43 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-20 16:36:02 +08:00
PrefacedCorg 102421997d add DeveloperAvatars 2025-09-20 16:34:55 +08:00
CJKmkp 43370bb8d1 fix:issue #202 2025-09-20 16:13:35 +08:00
CJKmkp 310f50fcde fix:issue #202 2025-09-20 16:10:52 +08:00
CJKmkp 9b5d157333 fix:issue #201 2025-09-20 15:36:45 +08:00
CJKmkp 53fbb7a0bd improve:更新弹窗 2025-09-20 15:36:24 +08:00
CJKmkp fe92117c4e improve:更新弹窗 2025-09-20 15:25:41 +08:00
CJKmkp 4a0d13457a improve:更新弹窗 2025-09-20 15:25:08 +08:00
CJKmkp 1ad6405003 improve:issue #197 2025-09-20 14:49:40 +08:00
CJKmkp 75e7e36011 improve:issue #133 2025-09-20 14:31:38 +08:00
CJKmkp 933c695b8c fix:issue #133 2025-09-20 14:02:29 +08:00
CJKmkp b34f7142f6 improve:墨迹平滑 2025-09-20 13:44:01 +08:00
CJKmkp 7392fa8165 fix:issue #204 2025-09-20 12:38:22 +08:00
CJKmkp 349d417869 fix:issue #205 2025-09-20 12:35:30 +08:00
CJKmkp b8e94cac9c fix:issue #210 2025-09-20 12:22:21 +08:00
CJKmkp cd90490b8d fix:issue #210 2025-09-20 12:15:21 +08:00
CJKmkp a9cc94ccb6 fix:issue #210 2025-09-20 12:03:30 +08:00
CJKmkp 17f137af09 fix:issue #210 2025-09-20 11:55:50 +08:00
CJKmkp ea7d0bbf71 fix:issue #206 2025-09-20 11:49:25 +08:00
CJKmkp 176f1cf405 fix:issue #208 2025-09-20 11:45:35 +08:00
CJKmkp 2ee93bbcc1 fix:issue #209 2025-09-20 11:42:33 +08:00
CJKmkp ba0629000e add:退出白板收纳 2025-09-20 11:22:09 +08:00
CJKmkp 313049f873 fix:issue #203 2025-09-20 11:08:52 +08:00
CJK_mkp d1a1b17d29 fix:白板参数 2025-09-18 18:07:45 +08:00
CJK_mkp eb91d2ce0a fix:白板参数 2025-09-18 18:06:53 +08:00
CJK_mkp 71c77da898 fix:issue #203 2025-09-18 17:58:51 +08:00
CJK_mkp 1b96c70386 fix:issue #160 2025-09-18 17:50:47 +08:00
CJK_mkp 703cecbd4f Merge pull request #196 from InkCanvasForClass/main
没事干合并一下commit
2025-09-13 23:15:56 +08:00
CJK_mkp e20bf41cc7 Merge pull request #195 from InkCanvasForClass/beta
ICC CE 1.7.11.0
2025-09-13 23:05:18 +08:00
CJKmkp 0f7b9524f9 fix:收纳模式下进入PPT自动进入批注异常 2025-09-13 22:53:31 +08:00
CJKmkp 53a9e901ec fix:退出PPT时错误的快捷键状态 2025-09-13 22:44:29 +08:00
CJKmkp 9a79fc71ed improve:PPT墨迹 2025-09-13 22:20:41 +08:00
CJKmkp e7f7a8038f 优化代码 2025-09-13 21:41:34 +08:00
CJKmkp 5361f8ae6f improve:墨迹延迟 2025-09-13 21:38:03 +08:00
CJKmkp ec3bcddc9d 更新版本号 2025-09-13 21:15:58 +08:00
CJKmkp 326a9f1d75 优化代码 2025-09-13 21:10:36 +08:00
CJKmkp 8c07e9b8a3 优化代码 2025-09-13 19:54:21 +08:00
CJKmkp 406d01febf 优化代码 2025-09-13 19:35:36 +08:00
CJKmkp 98663422b1 优化代码 2025-09-13 19:29:17 +08:00
CJKmkp 879dfcea28 add:issue #190 2025-09-13 19:10:40 +08:00
CJKmkp 40edb4c8de Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-13 18:53:49 +08:00
CJKmkp 07b6d142ad add:issue #190 2025-09-13 18:52:23 +08:00
2,2,3-三甲基戊烷 5716b3e3cf docs&fix: 移除ssh还存在于readme贡献者名单的bug 2025-09-13 18:45:59 +08:00
CJKmkp 1b242a07e1 delete:ssh 2025-09-13 18:29:13 +08:00
CJK_mkp 1516a73228 Merge pull request #192 from ShihaoShen2025/patch-1
删除all contributors src中有关hydro的信息
2025-09-13 18:22:46 +08:00
CJKmkp eef00eb28f add:issue #190 2025-09-13 18:20:56 +08:00
CJKmkp a5ac282ab6 add:issue #190 2025-09-13 18:13:31 +08:00
CJKmkp dbb88d4999 add:issue #190 2025-09-13 18:07:42 +08:00
电教沈同学 39addad3a1 Delete myself from ac-src
是时候给一切画上一个句号了。
再见,ICC-CE。
2025-09-13 17:58:06 +08:00
电教沈同学 87fd9a4470 Update .all-contributorsrc 2025-09-13 17:52:52 +08:00
电教沈同学 7a9156449f Update .all-contributorsrc 2025-09-13 17:50:36 +08:00
CJKmkp 6e2938e9c1 add:issue #190 2025-09-13 17:37:11 +08:00
CJKmkp 39e1498b26 add:issue #180 2025-09-13 16:36:41 +08:00
CJKmkp cc2dc9c1a7 add:issue #180 2025-09-13 16:28:31 +08:00
CJKmkp fa87198131 add:issue #180 2025-09-13 16:26:23 +08:00
CJKmkp 9b70b952f6 add:issue #180 2025-09-13 16:13:22 +08:00
CJKmkp b29fca1cdb add:issue #180 2025-09-13 16:09:51 +08:00
CJKmkp 501af800ce add:issue #180 2025-09-13 16:06:19 +08:00
CJKmkp 979be117c6 add:issue #180 2025-09-13 16:02:39 +08:00
CJKmkp 13594371bb add:issue #180 2025-09-13 15:34:50 +08:00
CJKmkp 9628d1b27f add:issue #181 2025-09-13 14:59:47 +08:00
CJKmkp fd0bd0b343 fix:托盘功能不可用 2025-09-13 14:33:49 +08:00
CJKmkp 9ea58bfdad 优化代码 2025-09-13 14:21:07 +08:00
CJKmkp c54d140107 improve:截图 2025-09-13 14:16:11 +08:00
CJKmkp 98d4a4213c improve:截图 2025-09-13 13:59:54 +08:00
CJKmkp a7d1de5ee3 improve:截图 2025-09-13 13:50:41 +08:00
CJKmkp fab9e4b265 improve:截图 2025-09-13 13:37:51 +08:00
CJKmkp d5fa46033e 优化日志 2025-09-13 13:03:04 +08:00
CJKmkp 32c179bbf9 improve:弹窗 2025-09-13 12:22:08 +08:00
CJKmkp 99f9886876 improve:直接调用外部点名 2025-09-13 12:08:45 +08:00
CJKmkp c1fcaff28a fix:issue #175 简易实现(后续会改) 2025-09-13 12:05:13 +08:00
CJKmkp 2da5e8d71b fix:动画导致的收纳问题 2025-09-13 11:56:26 +08:00
CJKmkp e80e64a287 fix:手势开关状态显示错误 2025-09-13 11:46:49 +08:00
CJKmkp 0344e51ef7 fix:issue #185 2025-09-13 11:38:46 +08:00
CJKmkp 9fd31b0584 improve:描述性文字 2025-09-13 11:32:08 +08:00
CJKmkp c6a48f79da fix:issue #184 2025-09-13 11:29:48 +08:00
CJKmkp 110d050cc6 fix:issue #188 2025-09-13 11:08:56 +08:00
CJKmkp 18188ef235 improve:弹窗 2025-09-13 11:04:04 +08:00
CJKmkp 3e1c397132 fix:issue #160 2025-09-13 10:58:43 +08:00
CJKmkp cda1f0b77d fix:无焦点功能状态丢失 2025-09-13 10:55:27 +08:00
CJKmkp afd49f154c improve:模式切换 issue #179 2025-09-13 10:51:08 +08:00
CJKmkp 505c620103 improve:PowerPoint联动增强 2025-09-13 10:48:33 +08:00
CJKmkp 5d9e340d6d improve:模式切换 issue #179 2025-09-13 10:46:02 +08:00
CJKmkp f705c4575c Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-13 10:43:50 +08:00
CJKmkp 1508bd806f 优化日志 2025-09-13 10:43:22 +08:00
CJK_mkp 1c542e0615 Update bug_report.yml 2025-09-12 15:59:11 +08:00
CJK_mkp ed5bcbde18 Update feature_request.yml 2025-09-12 15:58:36 +08:00
PrefacedCorg 084cbcd362 代码清理 2025-09-07 13:30:46 +08:00
CJKmkp ad8369cfe9 更新版本号 2025-09-07 13:25:20 +08:00
CJKmkp 7d36b3993e 优化代码 2025-09-07 09:41:42 +08:00
CJKmkp ead0854c8e improve:墨迹选择 2025-09-07 07:59:21 +08:00
CJKmkp 60b5655574 improve:墨迹选择 2025-09-07 07:53:05 +08:00
CJKmkp 9037630990 fix:issue #175 2025-09-07 01:50:44 +08:00
CJKmkp ba23fc9506 add:issue #171 2025-09-07 01:45:50 +08:00
CJKmkp 938ca7c0ea improve:墨迹选择 2025-09-07 01:22:46 +08:00
CJKmkp a034f7a9a0 improve:墨迹选择 2025-09-07 01:16:27 +08:00
PrefacedCorg d4c4fcfd74 Update PluginSettingsWindow.xaml 2025-09-07 01:02:34 +08:00
PrefacedCorg 5b457f2a8a 将插件管理器的页面改为无边框 2025-09-07 00:59:24 +08:00
CJKmkp adc22441fc fix:issue #173 2025-09-07 00:57:48 +08:00
CJKmkp 1ba59136c6 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-07 00:49:58 +08:00
CJKmkp 77702b2ac9 fix:issue #170 2025-09-07 00:49:10 +08:00
PrefacedCorg 0d6c814908 好像改错了() 2025-09-07 00:49:01 +08:00
PrefacedCorg 42ce58db60 贡献名单add PrefacedCorg(2) 2025-09-07 00:47:30 +08:00
PrefacedCorg 1ef56033fb 贡献名单add PrefacedCorg(1) 2025-09-07 00:31:22 +08:00
CJKmkp 8c1d3c0248 fix:issue #168 2025-09-07 00:20:48 +08:00
CJKmkp a725a12d25 fix:issue #169 2025-09-07 00:14:59 +08:00
CJKmkp f67db9beed fix:issue #178 #177 2025-09-07 00:11:22 +08:00
CJKmkp f22fd7b5d1 fix:issue #172 2025-09-07 00:08:11 +08:00
CJKmkp 060664260b fix:issue #172 2025-09-07 00:03:15 +08:00
CJKmkp 9da5ec7413 撤销操作 2025-09-06 23:58:00 +08:00
CJKmkp 076240f7cd 测试构建 2025-09-06 23:55:54 +08:00
CJKmkp d97a4ad243 fix:墨迹识别 2025-09-06 23:21:05 +08:00
CJKmkp 56209d8491 fix:issue #159 2025-09-06 22:22:41 +08:00
CJK_mkp 61fe7197f5 Merge pull request #166 from InkCanvasForClass/beta
ICC CE 1.7.10.0
2025-09-06 21:40:14 +08:00
CJKmkp 1881bfd69e 更新版本号 2025-09-06 21:35:07 +08:00
CJKmkp 4287b7def5 improve:用户分级 2025-09-06 21:33:06 +08:00
CJKmkp 27d683a7cc improve:PPT及自动更新 2025-09-06 21:26:46 +08:00
CJKmkp 38902c8f88 撤回操作 2025-09-06 20:59:45 +08:00
CJKmkp d8e8142ff4 发行说明测试 2025-09-06 20:57:24 +08:00
CJKmkp fdaf9cb3ef 发行说明测试 2025-09-06 20:37:05 +08:00
CJKmkp b2fe091c88 发行说明测试 2025-09-06 20:27:52 +08:00
CJKmkp 8548244cef 发行说明测试 2025-09-06 20:24:56 +08:00
CJKmkp 91a5881600 发行说明测试 2025-09-06 20:22:59 +08:00
CJKmkp 93fd043b14 发行说明测试 2025-09-06 20:21:24 +08:00
CJKmkp 47948ef530 更新版本号 2025-09-06 20:08:26 +08:00
CJKmkp 61fa618673 improve:用户分级 2025-09-06 20:02:50 +08:00
CJKmkp 545aecc6ae 更新版本号 2025-09-06 19:31:14 +08:00
CJKmkp ee41f53286 add:issue #156 2025-09-06 19:27:17 +08:00
CJKmkp cc8036f736 improve:无焦点 2025-09-06 18:12:45 +08:00
CJKmkp 67a982e6e4 improve:用户分级 2025-09-06 18:03:23 +08:00
CJKmkp b16000f8e5 improve:窗口置顶 2025-09-06 17:51:25 +08:00
CJKmkp a142ce0542 回滚操作 2025-09-06 17:41:34 +08:00
CJKmkp c87b8b65fc improve:高光显示 2025-09-06 17:12:25 +08:00
CJKmkp 7332df1d56 fix:issue #165 #133 及插件窗口问题 2025-09-06 15:50:16 +08:00
CJKmkp de90c17ab1 fix:issue #159 2025-09-06 15:36:40 +08:00
CJKmkp 3d54edf25b fix:issue #161 2025-09-06 15:28:14 +08:00
CJKmkp 111819dbf3 fix:issue #164 #155 2025-09-06 15:16:03 +08:00
CJKmkp 3665753ba2 优化日志 2025-09-06 15:14:53 +08:00
CJKmkp 75e38aa8f3 improve:issue #160 2025-09-06 15:09:34 +08:00
CJKmkp 29dc6938c3 improve:窗口置顶 2025-09-06 15:07:26 +08:00
CJKmkp 304933b02b improve:快捷键 2025-09-06 14:54:49 +08:00
CJKmkp 49f268bb62 improve:快捷键 2025-09-06 14:48:36 +08:00
CJKmkp d0793c546d improve:快捷键 2025-09-06 14:36:07 +08:00
CJKmkp 375aec6f6c improve:按钮显示 2025-09-06 14:24:18 +08:00
CJKmkp 12ec527dcd improve:浮动栏 2025-09-06 14:19:33 +08:00
CJKmkp 52d95173d7 improve:PPT模块及浮动栏 2025-09-06 14:18:09 +08:00
CJKmkp 67e3d51106 improve:窗口置顶 2025-09-06 13:58:49 +08:00
CJKmkp 9081aea926 improve:墨迹渐隐 2025-09-06 13:48:44 +08:00
CJKmkp 02c6caf465 fix:issue #153 2025-09-06 13:30:32 +08:00
CJKmkp 1a9ddf1969 improve:墨迹渐隐 2025-09-06 13:16:10 +08:00
CJKmkp 41bcae9d05 fix:issue #153 2025-09-06 12:38:57 +08:00
CJKmkp 28109a0c97 improve:PPT联动 2025-09-06 12:09:06 +08:00
CJKmkp d8825f0a73 fix:issue #154 2025-09-06 10:20:28 +08:00
CJKmkp 82c045a243 fix:issue #152 2025-09-06 10:17:12 +08:00
CJKmkp 4d11f69282 fix:issue #151 2025-09-06 10:11:19 +08:00
CJKmkp 664e4ea048 add:新设置 2025-08-31 17:11:04 +08:00
CJKmkp 6d21c5e24e add:新设置 2025-08-31 17:07:40 +08:00
CJKmkp ba21b6884d add:新设置 2025-08-31 16:25:52 +08:00
CJKmkp 60065aab3e add:新设置 2025-08-31 16:24:57 +08:00
CJKmkp 4d978936ac add:新设置 2025-08-31 16:17:21 +08:00
CJKmkp 4ea585633b add:新设置 2025-08-31 16:06:36 +08:00
CJKmkp dee0bd7f4d add:新设置 2025-08-31 15:58:58 +08:00
CJKmkp edb206ffa5 add:新设置 2025-08-31 15:35:28 +08:00
CJKmkp 21932056f3 improve:插件 2025-08-31 13:14:05 +08:00
CJK_mkp 0c3809b789 Merge pull request #147 from InkCanvasForClass/beta
ICC CE 1.7.9.1
2025-08-31 12:39:00 +08:00
CJKmkp d5ae596ad6 更新版本号 2025-08-31 12:37:49 +08:00
CJKmkp 42db7e60f2 improve:截图 2025-08-31 12:32:15 +08:00
CJKmkp 85827820a4 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-08-31 12:17:13 +08:00
CJKmkp fad2571edb fix:合并冲突 2025-08-31 12:16:29 +08:00
PrefacedCorg c53b6de68c Update ScreenshotSelectorWindow.xaml.cs 2025-08-31 12:14:46 +08:00
PrefacedCorg c86eb12168 Update YesOrNoNotificationWindow.xaml 2025-08-31 12:07:59 +08:00
CJKmkp fb31f8df11 优化代码 2025-08-31 12:05:26 +08:00
CJKmkp 147b4fc5ed improve:文件关联 2025-08-31 12:03:58 +08:00
CJKmkp cdcb2d2af0 add:文件关联 2025-08-31 11:49:00 +08:00
PrefacedCorg ff086e497c 代码清理 2025-08-31 11:43:52 +08:00
PrefacedCorg a2b711da05 改了个图标 2025-08-31 11:41:17 +08:00
CJKmkp 40ecfbefa3 improve:截图 2025-08-31 10:47:33 +08:00
CJK_mkp 2097a1250b Merge pull request #146 from InkCanvasForClass/beta
ICC CE 1.7.9.0
2025-08-31 10:24:44 +08:00
CJK_mkp 22025f9b00 Merge pull request #145 from InkCanvasForClass/beta
ICC CE 1.7.9.0
2025-08-31 10:22:57 +08:00
CJKmkp 80c1badba5 更新版本号 2025-08-31 10:08:49 +08:00
CJKmkp be308ba280 improve:截图 2025-08-31 10:06:11 +08:00
CJKmkp fa7f3d44e4 improve:截图及浮动栏 2025-08-31 09:59:49 +08:00
CJKmkp 9bb00489fe improve:截图及浮动栏及代码优化 2025-08-31 09:54:13 +08:00
CJKmkp 33948c604c improve:图片插入 2025-08-31 09:20:35 +08:00
CJKmkp b8fe5bbd66 improve:图片插入 2025-08-31 09:18:30 +08:00
CJKmkp 3fcc01c253 improve:图片插入 2025-08-31 09:15:53 +08:00
CJKmkp 658d48c17b improve:图片插入 2025-08-31 09:12:30 +08:00
CJKmkp 8d76c014c8 优化代码 2025-08-31 09:09:22 +08:00
CJKmkp 26cd125534 优化代码 2025-08-31 09:00:32 +08:00
CJKmkp 1bc23af61a 优化代码 2025-08-31 08:51:50 +08:00
CJKmkp a2aa6b48a8 improve:用户体验分级及图片插入 2025-08-31 08:04:46 +08:00
CJKmkp 5784c974f7 优化代码 2025-08-31 07:56:26 +08:00
CJKmkp 80503dc42e 优化代码 2025-08-31 07:54:43 +08:00
PrefacedCorg d76195f7ae fix:二级菜单下不去 2025-08-31 01:55:17 +08:00
PrefacedCorg 2fe482b802 让警告没那么多
应该不会出问题的吧
2025-08-31 01:47:00 +08:00
PrefacedCorg 16283f4643 添加空值检查 2025-08-31 01:00:59 +08:00
CJKmkp cd7a801400 还未完成的选择工具栏 2025-08-31 00:39:40 +08:00
CJKmkp f6d8558d07 improve:自动更新 2025-08-31 00:33:41 +08:00
CJKmkp de20b506f0 improve:用户体验分级 2025-08-31 00:25:22 +08:00
CJKmkp ea1d52292e improve:快捷键 2025-08-30 23:53:26 +08:00
CJKmkp 463e506ca3 improve:快捷键 2025-08-30 23:28:28 +08:00
CJKmkp 3bae64a2c7 improve:插入图片improve:插入图片 2025-08-30 23:19:53 +08:00
CJKmkp 5d13c8b543 improve:插入图片 2025-08-30 23:14:00 +08:00
CJKmkp c6c0789794 improve:自动更新 2025-08-30 23:10:02 +08:00
CJKmkp d7df39290f improve:插入图片及墨迹平滑 2025-08-30 22:59:12 +08:00
CJKmkp b64cefad46 improve:插入图片及墨迹平滑 2025-08-30 22:54:16 +08:00
CJKmkp 4690ab3c30 improve:插入图片 2025-08-30 22:14:39 +08:00
CJKmkp b61c7490b3 improve:截图 2025-08-30 21:23:36 +08:00
CJKmkp 9e511d29a6 delete:图片插入选中和移动相关 2025-08-30 21:18:34 +08:00
CJKmkp a9cdc36967 improve:长按翻页 2025-08-30 20:59:50 +08:00
CJKmkp 96c51cd7ff 优化代码 2025-08-30 19:40:14 +08:00
CJKmkp 636769c0ef fix:插入图片子面板折叠问题 2025-08-30 18:59:32 +08:00
CJKmkp 7beb2a3cc5 更新版本号 2025-08-30 18:49:31 +08:00
CJKmkp 52d318054c fix:部分情况高光错位 2025-08-30 18:48:46 +08:00
CJKmkp 5ee2ad6f3d improve:浮动栏动态调整 2025-08-30 18:36:20 +08:00
CJKmkp 097d414d0d fix:隐藏按钮导致的高光错位 2025-08-30 18:33:57 +08:00
CJK_mkp e0ce119374 Merge pull request #144 from InkCanvasForClass/beta
ICC CE 1.7.8.3
2025-08-30 18:24:09 +08:00
CJKmkp da9a5242cb 上传日志 2025-08-30 18:22:37 +08:00
CJKmkp 0211b38be9 更新版本号 2025-08-30 18:12:43 +08:00
CJKmkp 512e9b21cd improve:issue #143 2025-08-30 18:10:55 +08:00
CJKmkp 9181ad55ae fix:部分窗口无法点击 2025-08-30 16:25:10 +08:00
CJKmkp a1cb9e1b28 improve:自动更新 2025-08-30 16:10:20 +08:00
CJKmkp ea09a3e798 improve:自动更新 2025-08-30 15:49:37 +08:00
CJKmkp 21fa146f6f improve:自动更新 2025-08-30 15:28:51 +08:00
CJKmkp 1a3130041f improve:自动更新 2025-08-30 14:56:59 +08:00
CJKmkp 7095a5890c fix:issue #133 2025-08-30 14:53:50 +08:00
CJKmkp 4a86d1aa05 improve:自动更新 2025-08-30 14:11:39 +08:00
CJKmkp 19fe7223fb improve:快捷键 2025-08-30 11:34:29 +08:00
CJKmkp 8867fde3f2 fix:issue #141 2025-08-30 11:29:16 +08:00
CJKmkp 588d1822b1 fix:issue #141 2025-08-30 11:20:53 +08:00
CJK_mkp d2b3b38d9e fix:issue #141 2025-08-28 22:13:18 +08:00
CJK_mkp 206ae3d9ed fix:issue #141 2025-08-28 21:02:47 +08:00
CJK_mkp a67b7a2fd0 fix:issue #141 2025-08-28 20:13:43 +08:00
CJK_mkp f5a08b225c fix:issue #141 2025-08-28 18:35:17 +08:00
CJK_mkp c807f97c29 fix:issue #141 2025-08-28 18:32:47 +08:00
CJK_mkp 8382413137 fix:issue #141 2025-08-28 18:31:59 +08:00
CJK_mkp c6acafd3a6 fix:issue #141 2025-08-28 18:28:17 +08:00
CJK_mkp 9d43056361 fix:issue #141 2025-08-28 18:25:04 +08:00
CJK_mkp 52f8e24954 fix:issue #141 2025-08-28 18:24:12 +08:00
CJK_mkp dac05fec84 fix:issue #141 2025-08-28 18:20:44 +08:00
CJK_mkp cb4ed77572 fix:issue #141 2025-08-28 18:17:29 +08:00
CJK_mkp 3427cbdc2e fix:issue #141 2025-08-28 18:11:26 +08:00
CJK_mkp ec0fb0c3cf fix:issue #141 2025-08-28 18:10:52 +08:00
CJK_mkp 357983179c fix:issue #141 2025-08-28 18:07:39 +08:00
CJK_mkp 78b66c141e fix:issue #141 2025-08-28 18:05:20 +08:00
CJK_mkp 3ac88bb400 fix:issue #141 2025-08-28 18:02:44 +08:00
CJK_mkp 27493b857c 撤销更改 2025-08-28 17:41:29 +08:00
CJK_mkp e5a976abcf fix:issue #141 2025-08-28 17:37:08 +08:00
CJK_mkp 9e89bf5303 Merge pull request #142 from InkCanvasForClass/all-contributors/add-Jursin
docs: add Jursin as a contributor for design
2025-08-28 09:01:25 +08:00
allcontributors[bot] 54eb330711 docs: update .all-contributorsrc 2025-08-28 01:01:08 +00:00
allcontributors[bot] e9014c6f5d docs: update README.md 2025-08-28 01:01:07 +00:00
CJK_mkp 620bcf6fab fix:issue #140 2025-08-27 17:43:04 +08:00
CJK_mkp c03bbd9e13 fix:issue #140 2025-08-27 17:41:58 +08:00
CJK_mkp b428b4ec5b 回滚等待重构截图功能 2025-08-27 15:57:06 +08:00
CJK_mkp 876cc116ea fix:issue #140 2025-08-27 12:23:05 +08:00
CJK_mkp 0ba00c08ef fix:更新日志显示问题 2025-08-25 09:53:01 +08:00
CJK_mkp d41ecd9d55 fix:修正错误地址 2025-08-25 09:00:45 +08:00
CJK_mkp 35d351ce6b Merge pull request #138 from InkCanvasForClass/beta
ICC CE 1.7.8.0
2025-08-24 18:02:28 +08:00
CJKmkp 5d3af58361 fix:浮动栏动画异常 2025-08-24 18:01:04 +08:00
CJKmkp 8554b92f42 improve:外部点名 2025-08-24 17:26:26 +08:00
CJKmkp 76363b4263 更新版本号 2025-08-24 16:29:51 +08:00
CJKmkp b4250b9161 fix:开关状态显示错误 2025-08-24 16:24:29 +08:00
CJKmkp 441f40886f fix:issue #133 2025-08-24 16:09:14 +08:00
CJKmkp e8e8ad5d63 更新版本号 2025-08-24 12:13:55 +08:00
CJKmkp 454078ec82 add:更多插件支持 2025-08-24 12:02:29 +08:00
CJK_mkp 63f29c2686 Merge pull request #137 from InkCanvasForClass/beta
ICC CE 1.7.7.7
2025-08-24 11:58:06 +08:00
CJKmkp 280445f613 fix:浮动栏动画异常 2025-08-24 11:47:39 +08:00
CJKmkp d7f6433b53 更新版本号 2025-08-24 11:09:41 +08:00
CJKmkp 22da4cc408 fix:无焦点无法翻页 2025-08-24 11:08:13 +08:00
CJKmkp 4e185ef584 improve:长按翻页 2025-08-24 10:39:24 +08:00
CJKmkp fd9b4b4ba6 improve:墨迹渐隐开关 2025-08-24 10:17:21 +08:00
CJKmkp 2f79bbcb0f fix:错误的唤起URL 2025-08-24 09:46:52 +08:00
CJKmkp 2c70d243df improve:墨迹渐隐开关 2025-08-24 09:33:27 +08:00
CJKmkp 7710b77255 improve:快捷键和墨迹渐隐 2025-08-24 09:17:58 +08:00
CJKmkp d1eed23399 improve:快捷键 2025-08-24 09:09:48 +08:00
CJKmkp ff9ce4df44 imrpove:快捷键 2025-08-24 08:45:53 +08:00
CJKmkp b7c52842f2 improve:快捷键 2025-08-24 02:30:38 +08:00
CJKmkp 365459f649 improve:快捷键 2025-08-24 02:27:57 +08:00
CJKmkp e6354f724f 优化日志 2025-08-24 02:03:34 +08:00
CJKmkp 14eedca939 fix:时间戳问题 2025-08-24 01:46:48 +08:00
CJKmkp 83529cfe09 add:issue #135 #136 2025-08-24 00:05:26 +08:00
CJKmkp bd4e1c1810 更新版本号 2025-08-23 23:32:44 +08:00
CJKmkp 5665fcc823 add:issue #131 2025-08-23 23:13:39 +08:00
CJKmkp 710a9014dd improve:issue #112 2025-08-23 21:39:00 +08:00
CJKmkp 108c6b2b17 improve:使用数据保存 2025-08-23 20:24:48 +08:00
CJKmkp 70735943c3 add:长按翻页 2025-08-23 19:43:09 +08:00
CJKmkp d01a24f879 add:底部翻页控件预览 2025-08-23 19:32:13 +08:00
CJKmkp ec2d5043ff add:底部PPT翻页按钮调节 2025-08-23 19:27:30 +08:00
CJKmkp 15082c2c52 fix:墨迹出现在不对应的PPT文档上 2025-08-23 19:16:19 +08:00
CJKmkp 44278d68b4 fix:翻页控件不合理的显示时机 2025-08-23 19:10:53 +08:00
CJKmkp 40eeb9db66 fix:退出放映模式后浮动栏异常问题 2025-08-23 19:04:46 +08:00
CJKmkp a5eb1dfca7 fix:开启部分功能后手势面板显示异常 2025-08-23 18:58:57 +08:00
CJKmkp 8719677f11 fix:开启部分功能后手势面板显示异常 2025-08-23 18:55:36 +08:00
CJKmkp 8f01b6c5fe fix:切换时荧光笔状态异常 2025-08-23 18:51:16 +08:00
CJKmkp 9e63a3f49b fix:快捷调色板不支持荧光笔 2025-08-23 18:43:47 +08:00
CJKmkp 84edb7bbe6 fix:快捷调色板不支持荧光笔 2025-08-23 18:39:12 +08:00
CJKmkp c86ce00a17 fix:快捷调色板不支持荧光笔 2025-08-23 18:29:24 +08:00
CJKmkp aba6c18a25 fix:issue #133 2025-08-23 18:24:24 +08:00
2,2,3-三甲基戊烷 62c81e6d44 Merge pull request #134 from InkCanvasForClass/all-contributors/add-PrefacedCorg 2025-08-23 10:44:32 +08:00
allcontributors[bot] 68ea323855 docs: update .all-contributorsrc 2025-08-23 02:44:15 +00:00
allcontributors[bot] f94d81ad20 docs: update README.md 2025-08-23 02:44:14 +00:00
CJK_mkp 063f8b1751 优化日志 2025-08-19 18:04:45 +08:00
CJK_mkp 35cfd26ece fix:退出不完全时更新 2025-08-19 17:52:02 +08:00
CJK_mkp f331cb1b4d fix:issue #110 2025-08-19 17:41:42 +08:00
CJK_mkp 502e205071 fix:关机时不保存使用数据 2025-08-18 18:01:23 +08:00
CJK_mkp 975b563b8d fix:关机时不保存使用数据 2025-08-18 17:57:56 +08:00
CJK_mkp cc88630859 fix:关机时不保存使用数据 2025-08-18 17:54:41 +08:00
CJK_mkp cce0b930ec fix:关机时不保存使用数据 2025-08-18 17:49:33 +08:00
CJK_mkp 723c825df2 fix:关机时不保存使用数据 2025-08-18 17:42:36 +08:00
CJK_mkp 5b4c966354 优化日志 2025-08-13 22:03:18 +08:00
CJK_mkp 69023f98d9 优化日志 2025-08-13 22:00:53 +08:00
CJK_mkp dce5722e96 优化日志 2025-08-13 21:59:35 +08:00
254 changed files with 46134 additions and 7264 deletions
+20 -10
View File
@@ -19,15 +19,6 @@
"code"
]
},
{
"login": "Hydro11451",
"name": "Hydrogen",
"avatar_url": "https://avatars.githubusercontent.com/u/214308559?v=4",
"profile": "http://hydro11451.qzz.io",
"contributions": [
"code"
]
},
{
"login": "CreeperAWA",
"name": "CreeperAWA",
@@ -86,7 +77,26 @@
"avatar_url": "https://avatars.githubusercontent.com/u/129855423?v=4",
"profile": "https://github.com/PrefacedCorg",
"contributions": [
"code"
"code",
"design"
]
},
{
"login": "Jursin",
"name": "Jursin",
"avatar_url": "https://avatars.githubusercontent.com/u/127487914?v=4",
"profile": "http://blog.jursin.top",
"contributions": [
"design"
]
},
{
"login": "Tayasui-rainnya",
"name": "tayasui rainnya!",
"avatar_url": "https://avatars.githubusercontent.com/u/156585442?v=4",
"profile": "https://github.com/Tayasui-rainnya",
"contributions": [
"design"
]
}
]
+2 -2
View File
@@ -39,14 +39,14 @@ body:
2.
3.
validations:
required: false
required: true
- type: textarea
id: expected
attributes:
label: 期望结果 | Expected Behavior
description: 你期望的正确行为或结果 | What did you expect to happen?
validations:
required: false
required: true
- type: textarea
id: extra
attributes:
+1 -1
View File
@@ -20,7 +20,7 @@ body:
label: 需求动机 | Motivation
description: 为什么需要这个功能?| Why do you need this feature?
validations:
required: false
required: true
- type: textarea
id: design
attributes:
+2 -2
View File
@@ -26,10 +26,10 @@ jobs:
- name: Build the Solution
run: |
msbuild -t:restore /p:GitFlow="Github Action"
msbuild /p:platform="Any CPU" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj"
msbuild /p:platform="AnyCPU" /p:configuration="Debug" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj"
- name: Upload to artifact
uses: actions/upload-artifact@v4.5.0
with:
name: InkCanvasForClass
path: "Ink Canvas/bin/Any CPU/Release/net472/"
path: "Ink Canvas/bin/Debug/net472"
+311
View File
@@ -0,0 +1,311 @@
name: Pre-release and Changelog
on:
workflow_dispatch:
inputs:
version_type:
description: 'Version bump type'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
- build
prerelease:
description: 'Create as pre-release'
required: true
default: true
type: boolean
jobs:
prerelease:
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta'
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0 # 获取所有历史记录用于生成changelog
- name: Setup MSbuild
uses: microsoft/setup-msbuild@v2
- name: Setup NuGet
uses: NuGet/setup-nuget@v2.0.1
- name: Restore NuGet Packages
run: nuget restore "Ink Canvas.sln"
- name: Get current version from Git tag
id: get_version
run: |
# 获取最新的tag
$latestTag = git describe --tags --abbrev=0 2>$null
if ($latestTag) {
# 移除v前缀(如果有的话)
$version = $latestTag -replace "^v", ""
echo "Found latest tag: $latestTag"
} else {
# 如果没有tag,使用默认版本
$version = "1.0.0.0"
echo "No tags found, using default version"
}
echo "current_version=$version" >> $env:GITHUB_OUTPUT
echo "Current version: $version"
- name: Calculate new version
id: calc_version
run: |
$currentVersion = "${{ steps.get_version.outputs.current_version }}"
$versionParts = $currentVersion.Split('.')
# 确保版本号格式正确(支持4部分)
if ($versionParts.Length -ge 3) {
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
$build = [int]$versionParts[3]
} else {
# 如果版本号格式不正确,使用默认值
$major = 1
$minor = 0
$patch = 0
$build = 0
}
$versionType = "${{ github.event.inputs.version_type }}"
switch ($versionType) {
"major" {
$major++
$minor = 0
$patch = 0
$build = 0
}
"minor" {
$minor++
$patch = 0
$build = 0
}
"patch" {
$patch++
$build = 0
}
"build" {
$build++
}
}
# 生成新版本号(4位格式,如1.7.18.0)
$newVersion = "$major.$minor.$patch.$build"
echo "new_version=$newVersion" >> $env:GITHUB_OUTPUT
echo "New version: $newVersion"
- name: Generate Changelog
id: changelog
run: |
# 获取上次tag到现在的所有commit
$lastTag = git describe --tags --abbrev=0 2>$null
if ($lastTag) {
$commits = git log --pretty=format:"%h|%s|%an|%ad" --date=short "$lastTag..HEAD"
} else {
$commits = git log --pretty=format:"%h|%s|%an|%ad" --date=short
}
# 初始化分类数组
$fixes = @()
$improvements = @()
$additions = @()
$deletions = @()
$versionChanges = @()
$others = @()
# 解析每个commit
foreach ($commit in $commits) {
if ($commit -match "^([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)$") {
$hash = $matches[1]
$message = $matches[2]
$author = $matches[3]
$date = $matches[4]
$commitInfo = @{
Hash = $hash
Message = $message
Author = $author
Date = $date
}
# 根据commit消息分类
if ($message -match "^(fix|修复)") {
$fixes += $commitInfo
} elseif ($message -match "^(improve|改进|优化)") {
$improvements += $commitInfo
} elseif ($message -match "^(add|新增|添加)") {
$additions += $commitInfo
} elseif ($message -match "^(delete|删除|移除)") {
$deletions += $commitInfo
} elseif ($message -match "(版本|version|更新版本号)") {
$versionChanges += $commitInfo
} else {
$others += $commitInfo
}
}
}
# 生成changelog内容
$version = "${{ steps.calc_version.outputs.new_version }}"
$changelog = "# ICC CE $version 更新日志`n`n## 修复 (Fixes)"
if ($fixes.Count -gt 0) {
foreach ($fix in $fixes) {
$changelog += "`n- $($fix.Message) ($($fix.Author), $($fix.Date))"
}
} else {
$changelog += "`n- 无"
}
$changelog += "`n`n## 改进 (Improvements)"
if ($improvements.Count -gt 0) {
foreach ($improvement in $improvements) {
$changelog += "`n- $($improvement.Message) ($($improvement.Author), $($improvement.Date))"
}
} else {
$changelog += "`n- 无"
}
$changelog += "`n`n## 新增功能 (New Features)"
if ($additions.Count -gt 0) {
foreach ($addition in $additions) {
$changelog += "`n- $($addition.Message) ($($addition.Author), $($addition.Date))"
}
} else {
$changelog += "`n- 无"
}
$changelog += "`n`n## 删除功能 (Removed Features)"
if ($deletions.Count -gt 0) {
foreach ($deletion in $deletions) {
$changelog += "`n- $($deletion.Message) ($($deletion.Author), $($deletion.Date))"
}
} else {
$changelog += "`n- 无"
}
$changelog += "`n`n## 版本更新 (Version Updates)"
if ($versionChanges.Count -gt 0) {
foreach ($versionChange in $versionChanges) {
$changelog += "`n- $($versionChange.Message) ($($versionChange.Author), $($versionChange.Date))"
}
} else {
$changelog += "`n- 无"
}
$changelog += "`n`n## 其他更改 (Other Changes)"
if ($others.Count -gt 0) {
foreach ($other in $others) {
$changelog += "`n- $($other.Message) ($($other.Author), $($other.Date))"
}
} else {
$changelog += "`n- 无"
}
$changelog += "`n`n---`n*此更新日志由GitHub Actions自动生成*"
# 保存changelog到文件
$changelog | Out-File -FilePath "CHANGELOG_${{ steps.calc_version.outputs.new_version }}.md" -Encoding UTF8
# 输出changelog内容到步骤输出
echo "changelog<<EOF" >> $env:GITHUB_OUTPUT
echo $changelog >> $env:GITHUB_OUTPUT
echo "EOF" >> $env:GITHUB_OUTPUT
echo "Changelog generated successfully"
- name: Display version info
run: |
echo "Current version: ${{ steps.get_version.outputs.current_version }}"
echo "New version: ${{ steps.calc_version.outputs.new_version }}"
echo "Note: Version will not be automatically updated in repository files"
- name: Build the Solution
run: |
msbuild -t:restore /p:GitFlow="Github Action"
msbuild /p:platform="AnyCPU" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj"
- name: Create Release Archive
run: |
$version = "${{ steps.calc_version.outputs.new_version }}"
$archiveName = "InkCanvasForClass.CE.$version.zip"
# 创建发布目录
New-Item -ItemType Directory -Path "release" -Force
# 复制发布文件
Copy-Item "Ink Canvas\bin\Release\net472\*" "release\" -Recurse -Force
# 创建压缩包
Compress-Archive -Path "release\*" -DestinationPath $archiveName -Force
echo "archive_name=$archiveName" >> $env:GITHUB_OUTPUT
- name: Upload Release Assets
uses: actions/upload-artifact@v4.5.0
with:
name: release-files-${{ steps.calc_version.outputs.new_version }}
path: |
InkCanvasForClass.CE.${{ steps.calc_version.outputs.new_version }}.zip
CHANGELOG_${{ steps.calc_version.outputs.new_version }}.md
- name: Prepare Release Info
run: |
$version = "${{ steps.calc_version.outputs.new_version }}"
echo "Preparing release for version: $version"
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.calc_version.outputs.new_version }}
name: ICC CE ${{ steps.calc_version.outputs.new_version }}
body: |
${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: ${{ github.event.inputs.prerelease }}
files: |
InkCanvasForClass.CE.${{ steps.calc_version.outputs.new_version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate UpdateLog preview
run: |
$version = "${{ steps.calc_version.outputs.new_version }}"
$changelogFile = "CHANGELOG_$version.md"
# 读取生成的changelog
$changelogContent = Get-Content $changelogFile -Raw
# 生成预览内容
$previewContent = "ICC CE $version 更新日志`n" + $changelogContent
echo "UpdateLog preview generated (not written to file):"
echo $previewContent
- name: Display Summary
run: |
echo "=== Release Summary ==="
echo "Version: ${{ steps.calc_version.outputs.new_version }}"
echo "Pre-release: ${{ github.event.inputs.prerelease }}"
echo "Changelog: Generated and attached to release"
echo "Archive: ICC_CE_${{ steps.calc_version.outputs.new_version }}.zip"
echo ""
echo "Note: No repository files were modified"
echo "You can manually update version numbers and changelog as needed"
+427 -3
View File
@@ -1,4 +1,428 @@
obj/
bin/
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
*.env
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
[Dd]ebug/x64/
[Dd]ebugPublic/x64/
[Rr]elease/x64/
[Rr]eleases/x64/
bin/x64/
obj/x64/
[Dd]ebug/x86/
[Dd]ebugPublic/x86/
[Rr]elease/x86/
[Rr]eleases/x86/
bin/x86/
obj/x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
[Aa][Rr][Mm]64[Ee][Cc]/
bld/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Build results on 'Bin' directories
**/[Bb]in/*
# Uncomment if you have tasks that rely on *.refresh files to move binaries
# (https://github.com/github/gitignore/pull/3736)
#!**/[Bb]in/*.refresh
# Visual Studio 2015/2017 cache/options directory
.vs/
/Ink Canvas/obj
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*.trx
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Approval Tests result files
*.received.*
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.idb
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
**/.paket/paket.exe
paket-files/
# FAKE - F# Make
**/.fake/
# CodeRush personal settings
**/.cr/personal
# Python Tools for Visual Studio (PTVS)
**/__pycache__/
*.pyc
# Cake - Uncomment if you are using it
#tools/**
#!tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
MSBuild_Logs/
# AWS SAM Build and Temporary Artifacts folder
.aws-sam
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
**/.mfractor/
# Local History for Visual Studio
**/.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
**/.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
+1 -1
View File
@@ -1 +1 @@
1.7.7.0
1.7.18.0
-4
View File
@@ -1,4 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/PencilsConfiguration/ActualSeverity/@EntryValue">WARNING</s:String>
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\MSBuild.exe</s:String>
<s:Int64 x:Key="/Default/Environment/Hierarchy/Build/BuildTool/MsbuildVersion/@EntryValue">1114112</s:Int64></wpf:ResourceDictionary>
+4 -5
View File
@@ -4,13 +4,13 @@
xmlns:local="clr-namespace:Ink_Canvas"
xmlns:tb="http://www.hardcodet.net/taskbar"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
StartupUri="MainWindow.xaml">
>
<Application.Resources>
<ResourceDictionary>
<Style TargetType="ui:ScrollViewerEx">
<EventSetter Event="PreviewMouseWheel" Handler="ScrollViewer_PreviewMouseWheel"/>
</Style>
<ContextMenu Opened="SysTrayMenu_Opened" x:Shared="false" x:Key="SysTrayMenu" Padding="6" ui:ThemeManager.RequestedTheme="Light">
<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>
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
@@ -32,7 +32,7 @@
</MenuItem.Icon>
</MenuItem>
<Separator Margin="0,3" />
<MenuItem>
<MenuItem Name="DisableAllHotkeysMenuItem" Click="DisableAllHotkeysMenuItem_Clicked">
<MenuItem.Header>
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="禁用所有快捷键" />
@@ -232,12 +232,11 @@
ContextMenu="{StaticResource SysTrayMenu}"
IconSource="/Resources/icc.ico"/>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources RequestedTheme="Light"/>
<ui:ThemeResources/>
<ui:XamlControlsResources />
<ResourceDictionary Source="Resources/SeewoImageDictionary.xaml"/>
<ResourceDictionary Source="Resources/DrawShapeImageDictionary.xaml"/>
<ResourceDictionary Source="Resources/IconImageDictionary.xaml"/>
<ResourceDictionary Source="Resources/Styles/Light.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
+612 -615
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -10,7 +10,7 @@ using System.Windows;
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("CJK_mkp")]
[assembly: AssemblyProduct("InkCanvasForClass")]
[assembly: AssemblyCopyright("Copyright © HARKOTEK Studio 2024")]
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@@ -49,5 +49,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.7.5")]
[assembly: AssemblyFileVersion("1.7.7.5")]
[assembly: AssemblyVersion("1.7.18.2")]
[assembly: AssemblyFileVersion("1.7.18.2")]
@@ -0,0 +1,86 @@
<UserControl x:Class="Ink_Canvas.Controls.QuickDrawFloatingButtonControl"
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"
Width="65" Height="45">
<UserControl.Resources>
<ResourceDictionary>
<!-- 悬浮按钮资源 -->
<SolidColorBrush x:Key="QuickDrawFloatingButtonBackground" Color="#80000000"/>
<SolidColorBrush x:Key="QuickDrawFloatingButtonBorderBrush" Color="#40000000"/>
<SolidColorBrush x:Key="QuickDrawFloatingButtonIconForeground" Color="White"/>
</ResourceDictionary>
</UserControl.Resources>
<Border Background="{DynamicResource QuickDrawFloatingButtonBackground}"
CornerRadius="8"
BorderThickness="1"
BorderBrush="{DynamicResource QuickDrawFloatingButtonBorderBrush}">
<Border.Effect>
<DropShadowEffect Color="Black" Direction="315" ShadowDepth="3" Opacity="0.3" BlurRadius="5"/>
</Border.Effect>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="22"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 拖动区域 -->
<Border Grid.Column="0"
MouseLeftButtonDown="DragArea_MouseLeftButtonDown"
MouseMove="DragArea_MouseMove"
MouseLeftButtonUp="DragArea_MouseLeftButtonUp"
Cursor="SizeAll"
Background="Transparent">
<Grid VerticalAlignment="Center" Height="14" IsHitTestVisible="False">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 三个白色横线 -->
<Border Grid.Row="0" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
<Border Grid.Row="2" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
<Border Grid.Row="4" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
</Grid>
</Border>
<!-- 半透明分割线 -->
<Rectangle Grid.Column="1" Width="1" Fill="#20FFFFFF" Margin="0,8,0,8"/>
<!-- 按钮区域 -->
<Border Grid.Column="2"
MouseLeftButtonDown="FloatingButton_Click"
Cursor="Hand"
Background="Transparent">
<Grid IsHitTestVisible="False">
<Path Data="M5 7C5 8.06087 5.42143 9.07828 6.17157 9.82843C6.92172 10.5786 7.93913 11 9 11C10.0609 11 11.0783 10.5786 11.8284 9.82843C12.5786 9.07828 13 8.06087 13 7C13 5.93913 12.5786 4.92172 11.8284 4.17157C11.0783 3.42143 10.0609 3 9 3C7.93913 3 6.92172 3.42143 6.17157 4.17157C5.42143 4.92172 5 5.93913 5 7Z M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21 M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88 M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15"
Stroke="{DynamicResource QuickDrawFloatingButtonIconForeground}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False"/>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,148 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using HorizontalAlignment = System.Windows.HorizontalAlignment;
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
using VerticalAlignment = System.Windows.VerticalAlignment;
namespace Ink_Canvas.Controls
{
/// <summary>
/// 快抽悬浮按钮控件
/// </summary>
public partial class QuickDrawFloatingButtonControl : UserControl
{
private bool _isDragging = false;
private Point _dragStartPoint;
private Point _controlStartPoint;
public QuickDrawFloatingButtonControl()
{
InitializeComponent();
}
/// <summary>
/// 快抽按钮点击事件
/// </summary>
private void FloatingButton_Click(object sender, MouseButtonEventArgs e)
{
try
{
// 如果正在拖动,不触发点击事件
if (_isDragging) return;
// 打开快抽窗口
var quickDrawWindow = new QuickDrawWindow();
quickDrawWindow.ShowDialog();
}
catch (Exception ex)
{
Helpers.LogHelper.WriteLogToFile($"打开快抽窗口失败: {ex.Message}", Helpers.LogHelper.LogType.Error);
}
}
/// <summary>
/// 拖动区域鼠标按下事件
/// </summary>
private void DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isDragging = false;
// 记录鼠标在屏幕上的初始位置
_dragStartPoint = this.PointToScreen(e.GetPosition(this));
// 记录控件的初始位置
var parent = this.Parent as FrameworkElement;
if (parent != null)
{
var transform = this.TransformToVisual(parent);
var currentPos = transform.Transform(new Point(0, 0));
_controlStartPoint = currentPos;
}
else
{
var currentMargin = this.Margin;
_controlStartPoint = new Point(
double.IsNaN(currentMargin.Left) ? 0 : currentMargin.Left,
double.IsNaN(currentMargin.Top) ? 0 : currentMargin.Top);
}
((UIElement)sender).CaptureMouse();
e.Handled = true;
}
/// <summary>
/// 拖动区域鼠标移动事件
/// </summary>
private void DragArea_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && ((UIElement)sender).IsMouseCaptured)
{
// 获取鼠标在屏幕上的当前位置
Point currentScreenPoint = this.PointToScreen(e.GetPosition(this));
Vector diff = currentScreenPoint - _dragStartPoint;
if (!_isDragging && (Math.Abs(diff.X) > 3 || Math.Abs(diff.Y) > 3))
{
_isDragging = true;
// 切换到绝对定位模式
this.HorizontalAlignment = HorizontalAlignment.Left;
this.VerticalAlignment = VerticalAlignment.Top;
}
if (_isDragging)
{
// 计算新位置
var parent = this.Parent as FrameworkElement;
if (parent != null)
{
// 计算屏幕坐标相对于父容器的位置
var parentPoint = parent.PointFromScreen(currentScreenPoint);
var startParentPoint = parent.PointFromScreen(_dragStartPoint);
// 计算相对于初始位置的偏移
double offsetX = parentPoint.X - startParentPoint.X;
double offsetY = parentPoint.Y - startParentPoint.Y;
// 新位置 = 初始位置 + 偏移
double newLeft = _controlStartPoint.X + offsetX;
double newTop = _controlStartPoint.Y + offsetY;
// 限制在父容器范围内
newLeft = Math.Max(0, Math.Min(newLeft, parent.ActualWidth - this.ActualWidth));
newTop = Math.Max(0, Math.Min(newTop, parent.ActualHeight - this.ActualHeight));
// 更新Margin
this.Margin = new Thickness(newLeft, newTop, 0, 0);
}
}
}
}
/// <summary>
/// 拖动区域鼠标释放事件
/// </summary>
private void DragArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (((UIElement)sender).IsMouseCaptured)
{
((UIElement)sender).ReleaseMouseCapture();
}
if (_isDragging)
{
Dispatcher.BeginInvoke(new Action(() => { _isDragging = false; }),
DispatcherPriority.Background);
}
else
{
_isDragging = false;
}
e.Handled = true;
}
}
}
-176
View File
@@ -1,176 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX86Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinArm64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCompression" type="xs:boolean">
<xs:annotation>
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCleanup" type="xs:boolean">
<xs:annotation>
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableEventSubscription" type="xs:boolean">
<xs:annotation>
<xs:documentation>The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinX86Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinX64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinArm64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>
+550 -31
View File
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Threading;
@@ -11,7 +12,7 @@ using System.Windows.Threading;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 异步硬件加速墨迹平滑处理器
/// 改进的异步硬件加速墨迹平滑处理器,使用优化的三次贝塞尔曲线拟合
/// </summary>
public class AsyncAdvancedBezierSmoothing
{
@@ -26,11 +27,13 @@ namespace Ink_Canvas.Helpers
_processingTasks = new ConcurrentDictionary<Stroke, CancellationTokenSource>();
}
public double SmoothingStrength { get; set; } = 0.3; // 大幅降低强度
public double ResampleInterval { get; set; } = 3.0; // 大幅增加间隔减少点数
public int InterpolationSteps { get; set; } = 8; // 从4增加到8,提高插值步数
public double SmoothingStrength { get; set; } = 0.4; // 适中的平滑强度
public double ResampleInterval { get; set; } = 2.5; // 适中的重采样间隔
public int InterpolationSteps { get; set; } = 12; // 增加插值步数提高精度
public bool UseHardwareAcceleration { get; set; } = true;
public int MaxConcurrentTasks { get; set; } = Environment.ProcessorCount;
public bool UseAdaptiveInterpolation { get; set; } = true; // 自适应插值
public double CurveTension { get; set; } = 0.3; // 曲线张力参数
/// <summary>
/// 异步平滑笔画
@@ -89,15 +92,26 @@ namespace Ink_Canvas.Helpers
cancellationToken.ThrowIfCancellationRequested();
// 简化处理:只进行轻度平滑,避免点数爆炸
var smoothedPoints = ApplyLightSmoothing(originalPoints);
// 使用改进的贝塞尔曲线拟合
var smoothedPoints = ApplyImprovedBezierSmoothing(originalPoints);
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 原始点数={originalPoints.Length}, 平滑后点数={smoothedPoints.Length}");
cancellationToken.ThrowIfCancellationRequested();
// 确保点数不会过多
if (smoothedPoints.Length > originalPoints.Length * 2)
// 放宽点数限制
if (smoothedPoints.Length > originalPoints.Length * 3.0)
{
// 如果点数增加太多,回退到原始笔画
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 点数过多,进行重采样");
// 如果点数增加太多,进行重采样
smoothedPoints = ResampleEquidistantOptimized(smoothedPoints, ResampleInterval);
}
// 进一步放宽最终检查
if (smoothedPoints.Length > originalPoints.Length * 2.5)
{
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 重采样后点数仍然过多,返回原始笔画");
// 如果仍然太多点,使用原始笔画
return stroke;
}
@@ -107,38 +121,404 @@ namespace Ink_Canvas.Helpers
DrawingAttributes = stroke.DrawingAttributes.Clone()
};
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 成功创建平滑笔画");
return smoothedStroke;
}
/// <summary>
/// 轻度平滑处理,避免点数爆炸
/// 改进的贝塞尔曲线平滑处理
/// </summary>
private StylusPoint[] ApplyLightSmoothing(StylusPoint[] points)
private StylusPoint[] ApplyImprovedBezierSmoothing(StylusPoint[] points)
{
if (points.Length < 3) return points;
if (points.Length < 6) return points; // 5次贝塞尔需要6个点
var result = new List<StylusPoint>();
result.Add(points[0]); // 保持第一个点
// 简单的3点平均平滑
for (int i = 1; i < points.Length - 1; i++)
// 添加第一个点
result.Add(points[0]);
// 使用5次贝塞尔曲线,每次移动1个点确保连续性
for (int i = 0; i < points.Length - 5; i++)
{
var prev = points[i - 1];
var curr = points[i];
var next = points[i + 1];
var p0 = points[i];
var p1 = points[i + 1];
var p2 = points[i + 2];
var p3 = points[i + 3];
var p4 = points[i + 4];
var p5 = points[i + 5];
// 3点平均
double x = (prev.X + curr.X + next.X) / 3.0;
double y = (prev.Y + curr.Y + next.Y) / 3.0;
float pressure = (prev.PressureFactor + curr.PressureFactor + next.PressureFactor) / 3.0f;
// 计算5次贝塞尔的控制点
var controlPoints = CalculateQuinticControlPoints(p0, p1, p2, p3, p4, p5);
result.Add(new StylusPoint(x, y, Math.Max(pressure, 0.1f)));
// 生成插值点
if (i == 0)
{
// 第一个窗口:生成更多插值点
for (int j = 1; j <= 4; j++)
{
double t = (double)j / 5;
var bezierPoint = CalculateQuinticBezierPoint(p0, controlPoints, p5, t);
result.Add(bezierPoint);
}
}
else
{
// 后续窗口:只生成最后一个插值点,避免重复
double t = 4.0 / 5.0; // 只取最后一个插值点
var bezierPoint = CalculateQuinticBezierPoint(p0, controlPoints, p5, t);
result.Add(bezierPoint);
}
}
result.Add(points[points.Length - 1]); // 保持最后一个点
// 添加最后一个点
result.Add(points[points.Length - 1]);
System.Diagnostics.Debug.WriteLine($"ApplyImprovedBezierSmoothing: 原始点数={points.Length}, 生成点数={result.Count}");
// 使用更宽松的去重
return RemoveDuplicatePointsLoose(result.ToArray());
}
/// <summary>
/// 5次贝塞尔曲线控制点计算
/// </summary>
private (Point cp1, Point cp2, Point cp3, Point cp4) CalculateQuinticControlPoints(
StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3, StylusPoint p4, StylusPoint p5)
{
// 计算控制点距离(基于相邻点距离)
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
double dist2 = Math.Sqrt((p2.X - p1.X) * (p2.X - p1.X) + (p2.Y - p1.Y) * (p2.Y - p1.Y));
double dist3 = Math.Sqrt((p4.X - p3.X) * (p4.X - p3.X) + (p4.Y - p3.Y) * (p4.Y - p3.Y));
double dist4 = Math.Sqrt((p5.X - p4.X) * (p5.X - p4.X) + (p5.Y - p4.Y) * (p5.Y - p4.Y));
// 使用更小的控制点距离,产生超平滑的曲线
double controlDist1 = dist1 * 0.15;
double controlDist2 = dist2 * 0.15;
double controlDist3 = dist3 * 0.15;
double controlDist4 = dist4 * 0.15;
// 计算控制点方向 - 使用更平滑的方向计算
double dir1X = p2.X - p0.X;
double dir1Y = p2.Y - p0.Y;
double dir2X = p3.X - p1.X;
double dir2Y = p3.Y - p1.Y;
double dir3X = p4.X - p2.X;
double dir3Y = p4.Y - p2.Y;
double dir4X = p5.X - p3.X;
double dir4Y = p5.Y - p3.Y;
// 归一化方向
NormalizeVector(ref dir1X, ref dir1Y);
NormalizeVector(ref dir2X, ref dir2Y);
NormalizeVector(ref dir3X, ref dir3Y);
NormalizeVector(ref dir4X, ref dir4Y);
// 计算控制点
var cp1 = new Point(p1.X + dir1X * controlDist1, p1.Y + dir1Y * controlDist1);
var cp2 = new Point(p2.X + dir2X * controlDist2, p2.Y + dir2Y * controlDist2);
var cp3 = new Point(p3.X - dir3X * controlDist3, p3.Y - dir3Y * controlDist3);
var cp4 = new Point(p4.X - dir4X * controlDist4, p4.Y - dir4Y * controlDist4);
return (cp1, cp2, cp3, cp4);
}
/// <summary>
/// 归一化向量
/// </summary>
private void NormalizeVector(ref double x, ref double y)
{
double length = Math.Sqrt(x * x + y * y);
if (length > 0)
{
x /= length;
y /= length;
}
}
/// <summary>
/// 5次贝塞尔曲线点计算
/// </summary>
private StylusPoint CalculateQuinticBezierPoint(StylusPoint p0, (Point cp1, Point cp2, Point cp3, Point cp4) controlPoints, StylusPoint p5, double t)
{
double oneMinusT = 1 - t;
double oneMinusT2 = oneMinusT * oneMinusT;
double oneMinusT3 = oneMinusT2 * oneMinusT;
double oneMinusT4 = oneMinusT3 * oneMinusT;
double oneMinusT5 = oneMinusT4 * oneMinusT;
double t2 = t * t;
double t3 = t2 * t;
double t4 = t3 * t;
double t5 = t4 * t;
// 5次贝塞尔曲线公式
double x = oneMinusT5 * p0.X +
5 * oneMinusT4 * t * controlPoints.cp1.X +
10 * oneMinusT3 * t2 * controlPoints.cp2.X +
10 * oneMinusT2 * t3 * controlPoints.cp3.X +
5 * oneMinusT * t4 * controlPoints.cp4.X +
t5 * p5.X;
double y = oneMinusT5 * p0.Y +
5 * oneMinusT4 * t * controlPoints.cp1.Y +
10 * oneMinusT3 * t2 * controlPoints.cp2.Y +
10 * oneMinusT2 * t3 * controlPoints.cp3.Y +
5 * oneMinusT * t4 * controlPoints.cp4.Y +
t5 * p5.Y;
// 压力插值 - 使用线性插值
float pressure = (float)((1 - t) * p0.PressureFactor + t * p5.PressureFactor);
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
}
/// <summary>
/// 简化的控制点计算
/// </summary>
private (Point cp1, Point cp2) CalculateSimpleControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
// 计算控制点距离(基于线段长度)
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
double dist2 = Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
// 使用更小的控制点距离,产生更平滑的曲线
double controlDist1 = dist1 * 0.2; // 进一步减少控制点影响
double controlDist2 = dist2 * 0.2;
// 计算控制点方向 - 使用更平滑的方向计算
double dir1X = p2.X - p0.X; // 使用更远的点计算方向
double dir1Y = p2.Y - p0.Y;
double dir2X = p3.X - p1.X;
double dir2Y = p3.Y - p1.Y;
// 归一化方向
double len1 = Math.Sqrt(dir1X * dir1X + dir1Y * dir1Y);
double len2 = Math.Sqrt(dir2X * dir2X + dir2Y * dir2Y);
if (len1 > 0)
{
dir1X /= len1;
dir1Y /= len1;
}
if (len2 > 0)
{
dir2X /= len2;
dir2Y /= len2;
}
// 计算控制点
var cp1 = new Point(
p1.X + dir1X * controlDist1,
p1.Y + dir1Y * controlDist1
);
var cp2 = new Point(
p2.X - dir2X * controlDist2,
p2.Y - dir2Y * controlDist2
);
return (cp1, cp2);
}
/// <summary>
/// 宽松的去重算法
/// </summary>
private StylusPoint[] RemoveDuplicatePointsLoose(StylusPoint[] points)
{
if (points.Length < 2) return points;
var result = new List<StylusPoint>();
result.Add(points[0]);
double minDistance = 0.1; // 非常小的距离阈值,几乎不去重
for (int i = 1; i < points.Length; i++)
{
var lastPoint = result[result.Count - 1];
var currentPoint = points[i];
// 计算距离
double distance = Math.Sqrt(
(currentPoint.X - lastPoint.X) * (currentPoint.X - lastPoint.X) +
(currentPoint.Y - lastPoint.Y) * (currentPoint.Y - lastPoint.Y));
// 如果距离足够大,添加这个点
if (distance >= minDistance)
{
result.Add(currentPoint);
}
}
System.Diagnostics.Debug.WriteLine($"RemoveDuplicatePointsLoose: 输入点数={points.Length}, 输出点数={result.Count}");
return result.ToArray();
}
/// <summary>
/// 计算贝塞尔曲线上的点
/// </summary>
private StylusPoint CalculateBezierPoint(StylusPoint p0, Point cp1, Point cp2, StylusPoint p3, double t)
{
double x = Math.Pow(1 - t, 3) * p0.X +
3 * Math.Pow(1 - t, 2) * t * cp1.X +
3 * (1 - t) * Math.Pow(t, 2) * cp2.X +
Math.Pow(t, 3) * p3.X;
double y = Math.Pow(1 - t, 3) * p0.Y +
3 * Math.Pow(1 - t, 2) * t * cp1.Y +
3 * (1 - t) * Math.Pow(t, 2) * cp2.Y +
Math.Pow(t, 3) * p3.Y;
// 压力插值
float pressure = (float)(Math.Pow(1 - t, 3) * p0.PressureFactor +
3 * Math.Pow(1 - t, 2) * t * p0.PressureFactor +
3 * (1 - t) * Math.Pow(t, 2) * p3.PressureFactor +
Math.Pow(t, 3) * p3.PressureFactor);
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
}
/// <summary>
/// 计算改进的控制点
/// </summary>
private (Point cp1, Point cp2) CalculateImprovedControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
// 计算切线方向
var tangent1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
var tangent2 = new Vector(p3.X - p2.X, p3.Y - p2.Y);
// 归一化切线
if (tangent1.Length > 0) tangent1.Normalize();
if (tangent2.Length > 0) tangent2.Normalize();
// 计算控制点距离(基于点间距离)
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
double dist2 = Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
double controlDist1 = dist1 * CurveTension;
double controlDist2 = dist2 * CurveTension;
// 计算控制点
var cp1 = new Point(
p1.X + tangent1.X * controlDist1,
p1.Y + tangent1.Y * controlDist1
);
var cp2 = new Point(
p2.X - tangent2.X * controlDist2,
p2.Y - tangent2.Y * controlDist2
);
return (cp1, cp2);
}
/// <summary>
/// 自适应插值步数计算
/// </summary>
private int CalculateAdaptiveSteps(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
// 基于曲线长度和复杂度计算步数
double totalLength = 0;
totalLength += Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
totalLength += Math.Sqrt((p2.X - p1.X) * (p2.X - p1.X) + (p2.Y - p1.Y) * (p2.Y - p1.Y));
totalLength += Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
// 计算曲率(简化版本)
double curvature = CalculateCurvature(p0, p1, p2, p3);
// 基于长度和曲率计算步数
int baseSteps = Math.Max(8, Math.Min(20, (int)(totalLength / 10)));
int curvatureSteps = (int)(curvature * 10);
return Math.Max(InterpolationSteps, Math.Min(24, baseSteps + curvatureSteps));
}
/// <summary>
/// 计算曲率(简化版本)
/// </summary>
private double CalculateCurvature(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
// 计算三个向量的角度变化
var v1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
var v2 = new Vector(p2.X - p1.X, p2.Y - p1.Y);
var v3 = new Vector(p3.X - p2.X, p3.Y - p2.Y);
if (v1.Length == 0 || v2.Length == 0 || v3.Length == 0) return 0;
v1.Normalize();
v2.Normalize();
v3.Normalize();
// 计算角度变化
double angle1 = Math.Acos(Math.Max(-1, Math.Min(1, Vector.Multiply(v1, v2))));
double angle2 = Math.Acos(Math.Max(-1, Math.Min(1, Vector.Multiply(v2, v3))));
return (angle1 + angle2) / Math.PI; // 归一化到0-1
}
/// <summary>
/// 去除重复和过近的点
/// </summary>
private StylusPoint[] RemoveDuplicatePoints(StylusPoint[] points)
{
if (points.Length < 2) return points;
var result = new List<StylusPoint>();
result.Add(points[0]);
double minDistance = 0.3; // 进一步减少最小距离阈值,保留更多平滑点
for (int i = 1; i < points.Length; i++)
{
var lastPoint = result[result.Count - 1];
var currentPoint = points[i];
// 计算距离
double distance = Math.Sqrt(
(currentPoint.X - lastPoint.X) * (currentPoint.X - lastPoint.X) +
(currentPoint.Y - lastPoint.Y) * (currentPoint.Y - lastPoint.Y));
// 如果距离足够大,添加这个点
if (distance >= minDistance)
{
result.Add(currentPoint);
}
}
System.Diagnostics.Debug.WriteLine($"RemoveDuplicatePoints: 输入点数={points.Length}, 输出点数={result.Count}");
return result.ToArray();
}
/// <summary>
/// 使用控制点的三次贝塞尔曲线计算
/// </summary>
private StylusPoint CubicBezierWithControlPoints((Point cp1, Point cp2) controlPoints, double t, StylusPoint p0, StylusPoint p3)
{
var p1 = controlPoints.cp1;
var p2 = controlPoints.cp2;
double u = 1 - t;
double tt = t * t;
double uu = u * u;
double uuu = uu * u;
double ttt = tt * t;
// 预计算系数
double c0 = uuu;
double c1 = 3 * uu * t;
double c2 = 3 * u * tt;
double c3 = ttt;
double x = c0 * p0.X + c1 * p1.X + c2 * p2.X + c3 * p3.X;
double y = c0 * p0.Y + c1 * p1.Y + c2 * p2.Y + c3 * p3.Y;
// 插值压力值
float pressure = (float)(p0.PressureFactor * u + p3.PressureFactor * t);
pressure = Math.Max(pressure, 0.1f);
return new StylusPoint(x, y, pressure);
}
/// <summary>
/// 硬件加速的向量化指数平滑
/// </summary>
@@ -352,23 +732,31 @@ namespace Ink_Canvas.Helpers
/// </summary>
public class AdvancedBezierSmoothing
{
public double SmoothingStrength { get; set; } = 0.3;
public double ResampleInterval { get; set; } = 3.0;
public int InterpolationSteps { get; set; } = 8;
public double SmoothingStrength { get; set; } = 0.6;
public double ResampleInterval { get; set; } = 2.0;
public int InterpolationSteps { get; set; } = 12;
public Stroke SmoothStroke(Stroke stroke)
{
if (stroke == null || stroke.StylusPoints.Count < 3)
{
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 笔画点数不足,跳过平滑 (点数: {stroke?.StylusPoints.Count ?? 0})");
return stroke;
}
var originalPoints = stroke.StylusPoints.ToList();
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 开始平滑处理,原始点数: {stroke.StylusPoints.Count}");
// 简化处理:只进行轻度平滑
var smoothedPoints = ApplyLightExponentialSmoothing(originalPoints, 0.2); // 很轻的平滑
var originalPoints = stroke.StylusPoints.ToArray();
// 使用真正的贝塞尔曲线平滑
var smoothedPoints = ApplyCubicBezierSmoothing(originalPoints);
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 平滑完成,平滑后点数: {smoothedPoints.Length}");
// 检查点数是否合理
if (smoothedPoints.Count > originalPoints.Count * 1.5)
if (smoothedPoints.Length > originalPoints.Length * 10.0)
{
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 点数增加过多,返回原始笔画 (原始:{originalPoints.Length}, 平滑后:{smoothedPoints.Length})");
return stroke; // 如果点数增加太多,返回原始笔画
}
@@ -376,9 +764,140 @@ namespace Ink_Canvas.Helpers
{
DrawingAttributes = stroke.DrawingAttributes.Clone()
};
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 创建平滑笔画成功");
return smoothedStroke;
}
/// <summary>
/// 三次贝塞尔曲线平滑
/// </summary>
private StylusPoint[] ApplyCubicBezierSmoothing(StylusPoint[] points)
{
if (points.Length < 4) return points;
var result = new List<StylusPoint>();
result.Add(points[0]);
// 使用更保守的窗口大小和插值
int windowSize = Math.Min(4, points.Length);
int stepSize = Math.Max(1, points.Length / 10); // 根据点数动态调整步长
for (int i = 0; i <= points.Length - windowSize; i += stepSize)
{
if (i + windowSize - 1 >= points.Length) break;
var p0 = points[i];
var p1 = points[Math.Min(i + 1, points.Length - 1)];
var p2 = points[Math.Min(i + 2, points.Length - 1)];
var p3 = points[Math.Min(i + windowSize - 1, points.Length - 1)];
// 计算控制点
var controlPoints = CalculateControlPoints(p0, p1, p2, p3);
// 只生成2-3个插值点
int steps = 2;
// 生成贝塞尔曲线点
for (int j = 1; j <= steps; j++)
{
double t = (double)j / steps;
var bezierPoint = CalculateBezierPoint(p0, controlPoints.cp1, controlPoints.cp2, p3, t);
result.Add(bezierPoint);
}
}
result.Add(points[points.Length - 1]);
// 去重处理
return RemoveDuplicatePoints(result.ToArray());
}
/// <summary>
/// 去除重复点
/// </summary>
private StylusPoint[] RemoveDuplicatePoints(StylusPoint[] points)
{
if (points.Length <= 1) return points;
var result = new List<StylusPoint> { points[0] };
double minDistance = 1.0; // 最小距离阈值
for (int i = 1; i < points.Length; i++)
{
var lastPoint = result[result.Count - 1];
var currentPoint = points[i];
double distance = Math.Sqrt(Math.Pow(currentPoint.X - lastPoint.X, 2) +
Math.Pow(currentPoint.Y - lastPoint.Y, 2));
if (distance > minDistance)
{
result.Add(currentPoint);
}
}
return result.ToArray();
}
/// <summary>
/// 计算控制点
/// </summary>
private (Point cp1, Point cp2) CalculateControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
// 计算切线方向
var tangent1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
var tangent2 = new Vector(p3.X - p2.X, p3.Y - p2.Y);
// 归一化切线
if (tangent1.Length > 0) tangent1.Normalize();
if (tangent2.Length > 0) tangent2.Normalize();
// 计算控制点距离
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
double dist2 = Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
double controlDist1 = dist1 * SmoothingStrength;
double controlDist2 = dist2 * SmoothingStrength;
// 计算控制点
var cp1 = new Point(
p1.X + tangent1.X * controlDist1,
p1.Y + tangent1.Y * controlDist1
);
var cp2 = new Point(
p2.X - tangent2.X * controlDist2,
p2.Y - tangent2.Y * controlDist2
);
return (cp1, cp2);
}
/// <summary>
/// 计算贝塞尔曲线上的点
/// </summary>
private StylusPoint CalculateBezierPoint(StylusPoint p0, Point cp1, Point cp2, StylusPoint p3, double t)
{
double x = Math.Pow(1 - t, 3) * p0.X +
3 * Math.Pow(1 - t, 2) * t * cp1.X +
3 * (1 - t) * Math.Pow(t, 2) * cp2.X +
Math.Pow(t, 3) * p3.X;
double y = Math.Pow(1 - t, 3) * p0.Y +
3 * Math.Pow(1 - t, 2) * t * cp1.Y +
3 * (1 - t) * Math.Pow(t, 2) * cp2.Y +
Math.Pow(t, 3) * p3.Y;
// 压力插值
float pressure = (float)(Math.Pow(1 - t, 3) * p0.PressureFactor +
3 * Math.Pow(1 - t, 2) * t * p0.PressureFactor +
3 * (1 - t) * Math.Pow(t, 2) * p3.PressureFactor +
Math.Pow(t, 3) * p3.PressureFactor);
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
}
/// <summary>
/// 轻度指数平滑
/// </summary>
+215
View File
@@ -0,0 +1,215 @@
using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 自动备份管理器
/// 负责管理配置文件的自动备份功能
/// </summary>
public static class AutoBackupManager
{
private static readonly string BackupDir = Path.Combine(App.RootPath, "Backups");
private static readonly string SettingsFile = Path.Combine(App.RootPath, "Configs", "Settings.json");
private static readonly string BackupPrefix = "Settings_AutoBackup_";
/// <summary>
/// 检查是否需要执行自动备份
/// </summary>
/// <param name="settings">设置对象</param>
/// <returns>如果需要备份返回true,否则返回false</returns>
public static bool ShouldPerformAutoBackup(Settings settings)
{
try
{
// 如果自动备份功能未启用,不执行备份
if (!settings.Advanced.IsAutoBackupEnabled)
{
return false;
}
// 如果从未备份过,需要创建首次备份
if (settings.Advanced.LastAutoBackupTime == DateTime.MinValue)
{
return true;
}
// 检查是否已超过备份间隔
var daysSinceLastBackup = (DateTime.Now - settings.Advanced.LastAutoBackupTime).TotalDays;
return daysSinceLastBackup >= settings.Advanced.AutoBackupIntervalDays;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"检查自动备份条件时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 执行自动备份
/// </summary>
/// <param name="settings">设置对象</param>
/// <returns>备份是否成功</returns>
public static bool PerformAutoBackup(Settings settings)
{
try
{
// 确保备份目录存在
if (!Directory.Exists(BackupDir))
{
Directory.CreateDirectory(BackupDir);
}
// 检查主配置文件是否存在
if (!File.Exists(SettingsFile))
{
LogHelper.WriteLogToFile("主配置文件不存在,跳过自动备份", LogHelper.LogType.Warning);
return false;
}
// 创建备份文件名(使用当前日期时间)
string backupFileName = $"{BackupPrefix}{DateTime.Now:yyyyMMdd_HHmmss}.json";
string backupPath = Path.Combine(BackupDir, backupFileName);
// 复制主配置文件到备份位置
File.Copy(SettingsFile, backupPath, true);
// 更新最后备份时间
settings.Advanced.LastAutoBackupTime = DateTime.Now;
MainWindow.SaveSettingsToFile();
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"执行自动备份时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 尝试从备份恢复配置文件
/// </summary>
/// <returns>恢复是否成功</returns>
public static bool TryRestoreFromBackup()
{
try
{
// 确保备份目录存在
if (!Directory.Exists(BackupDir))
{
LogHelper.WriteLogToFile("备份目录不存在,无法从备份恢复", LogHelper.LogType.Warning);
return false;
}
// 查找最新的备份文件
var backupFiles = Directory.GetFiles(BackupDir, $"{BackupPrefix}*.json")
.OrderByDescending(f => File.GetCreationTime(f))
.ToArray();
if (backupFiles.Length == 0)
{
LogHelper.WriteLogToFile("没有找到可用的备份文件", LogHelper.LogType.Warning);
return false;
}
// 尝试使用最新的备份文件
string latestBackup = backupFiles[0];
// 验证备份文件是否有效
try
{
string backupJson = File.ReadAllText(latestBackup);
var testSettings = JsonConvert.DeserializeObject<Settings>(backupJson);
if (testSettings == null)
{
LogHelper.WriteLogToFile("备份文件内容无效,无法恢复", LogHelper.LogType.Error);
return false;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"备份文件验证失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
// 备份当前损坏的配置文件(如果存在)
if (File.Exists(SettingsFile))
{
string corruptedBackup = Path.Combine(BackupDir, $"Settings_Corrupted_{DateTime.Now:yyyyMMdd_HHmmss}.json");
File.Copy(SettingsFile, corruptedBackup, true);
}
// 从备份恢复配置文件
File.Copy(latestBackup, SettingsFile, true);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从备份恢复配置文件时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 清理过期的备份文件
/// 保留最近30天的备份文件
/// </summary>
public static void CleanupOldBackups()
{
try
{
if (!Directory.Exists(BackupDir))
{
return;
}
var cutoffDate = DateTime.Now.AddDays(-30);
var backupFiles = Directory.GetFiles(BackupDir, $"{BackupPrefix}*.json");
int deletedCount = 0;
foreach (var file in backupFiles)
{
if (File.GetCreationTime(file) < cutoffDate)
{
File.Delete(file);
deletedCount++;
}
}
if (deletedCount > 0)
{
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理过期备份文件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 初始化自动备份功能
/// 在应用程序启动时调用
/// </summary>
/// <param name="settings">设置对象</param>
public static void Initialize(Settings settings)
{
try
{
// 检查是否需要执行自动备份
if (ShouldPerformAutoBackup(settings))
{
PerformAutoBackup(settings);
}
// 清理过期备份
CleanupOldBackups();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化自动备份功能时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
+681 -129
View File
@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
@@ -12,7 +12,6 @@ using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@@ -28,6 +27,7 @@ namespace Ink_Canvas.Helpers
private static readonly string updatesFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "AutoUpdate");
private static string statusFilePath;
// 线路组结构体(包含版本、下载、日志地址)
public class UpdateLineGroup
{
@@ -67,11 +67,41 @@ namespace Ink_Canvas.Helpers
{
GroupName = "智教联盟",
DownloadUrlFormat = "https://get.smart-teach.cn/d/Ningbo-S3/shared/jiangling/community/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "inkeys",
DownloadUrlFormat = "https://iccce.inkeys.top/Release/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "gh-proxy",
VersionUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "hk.gh-proxy",
VersionUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://hk.gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "cdn.gh-proxy",
VersionUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://cdn.gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "edgeone.gh-proxy",
VersionUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://edgeone.gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
}
}
},
@@ -102,11 +132,41 @@ namespace Ink_Canvas.Helpers
{
GroupName = "智教联盟",
DownloadUrlFormat = "https://get.smart-teach.cn/d/Ningbo-S3/shared/jiangling/community-beta/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "inkeys",
DownloadUrlFormat = "https://iccce.inkeys.top/Beta/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "gh-proxy",
VersionUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "hk.gh-proxy",
VersionUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://hk.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "cdn.gh-proxy",
VersionUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://cdn.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "edgeone.gh-proxy",
VersionUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://edgeone.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
}
}
}
@@ -184,14 +244,45 @@ namespace Ink_Canvas.Helpers
foreach (var group in groups)
{
// 跳过"智教联盟"和"inkeys"线路组,不参与延迟检测和排序
string testUrl = null;
if (group.GroupName == "智教联盟" || group.GroupName == "inkeys")
{
LogHelper.WriteLogToFile($"AutoUpdate | 跳过{group.GroupName}线路组延迟检测");
try
{
if (!string.IsNullOrEmpty(group.DownloadUrlFormat))
{
testUrl = group.DownloadUrlFormat.Replace("{0}", "test");
}
}
catch
{
testUrl = null;
}
}
else
{
testUrl = group.VersionUrl;
}
if (string.IsNullOrEmpty(testUrl))
{
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 缺少可用测速地址,跳过", LogHelper.LogType.Warning);
continue;
}
LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({group.VersionUrl})");
var delay = await GetUrlDelay(group.VersionUrl);
LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({testUrl})");
long delay;
if (group.GroupName == "智教联盟" || group.GroupName == "inkeys")
{
delay = await GetDownloadUrlDelay(testUrl);
}
else
{
delay = await GetUrlDelay(testUrl);
}
if (delay >= 0)
{
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 延迟: {delay}ms");
@@ -209,20 +300,12 @@ namespace Ink_Canvas.Helpers
.Select(x => x.group)
.ToList();
// 将"智教联盟"线路组插入到最前面(如果存在)
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
if (zhiJiaoGroup != null)
{
orderedGroups.Insert(0, zhiJiaoGroup);
LogHelper.WriteLogToFile("AutoUpdate | 智教联盟线路组已插入到首位");
}
// 将"inkeys"线路组插入到第二位(如果存在)
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
var inkeysGroup = orderedGroups.FirstOrDefault(g => g.GroupName == "inkeys");
if (inkeysGroup != null)
{
orderedGroups.Insert(1, inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | inkeys线路组已插入到第二位");
orderedGroups.Remove(inkeysGroup);
orderedGroups.Insert(0, inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | inkeys线路组已默认优先");
}
if (orderedGroups.Count > 0)
@@ -241,6 +324,47 @@ namespace Ink_Canvas.Helpers
return orderedGroups;
}
private static async Task<long> GetDownloadUrlDelay(string url)
{
try
{
var osVersion = Environment.OSVersion;
bool isWindows7 = osVersion.Version.Major == 6 && osVersion.Version.Minor == 1;
if (isWindows7)
{
using (var handler = new HttpClientHandler())
{
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
using (var client = new HttpClient(handler))
{
client.Timeout = TimeSpan.FromSeconds(5);
var sw = Stopwatch.StartNew();
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));
sw.Stop();
return sw.ElapsedMilliseconds;
}
}
}
else
{
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(5);
var sw = Stopwatch.StartNew();
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));
sw.Stop();
return sw.ElapsedMilliseconds;
}
}
}
catch
{
return -1;
}
}
// 获取远程版本号
private static async Task<string> GetRemoteVersion(string fileUrl)
{
@@ -409,6 +533,7 @@ namespace Ink_Canvas.Helpers
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
LogHelper.WriteLogToFile("AutoUpdate | 使用GitHub API调用");
var response = await client.GetStringAsync(apiUrl);
var releases = JArray.Parse(response);
@@ -450,6 +575,7 @@ namespace Ink_Canvas.Helpers
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
LogHelper.WriteLogToFile("AutoUpdate | 使用GitHub API调用");
var response = await client.GetStringAsync(apiUrl);
var json = JObject.Parse(response);
string version = json["tag_name"]?.ToString();
@@ -507,7 +633,7 @@ namespace Ink_Canvas.Helpers
// 尝试获取当前版本的发布时间
DateTime? currentVersionReleaseTime = await GetVersionReleaseTime(localVersion, channel);
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(apiVersion, releaseTime, true, currentVersionReleaseTime); // 明确标记为自动更新
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(apiVersion, releaseTime, true, currentVersionReleaseTime, localVersion); // 明确标记为自动更新
if (!shouldPush)
{
var priority = DeviceIdentifier.GetUpdatePriority();
@@ -563,7 +689,7 @@ namespace Ink_Canvas.Helpers
// 尝试获取当前版本的发布时间
DateTime? currentVersionReleaseTime = await GetVersionReleaseTime(localVersion, channel);
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(remoteVersion, DateTime.Now, true, currentVersionReleaseTime); // 明确标记为自动更新
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(remoteVersion, DateTime.Now, true, currentVersionReleaseTime, localVersion); // 明确标记为自动更新
if (!shouldPush)
{
var priority = DeviceIdentifier.GetUpdatePriority();
@@ -665,23 +791,13 @@ namespace Ink_Canvas.Helpers
SaveDownloadStatus(false);
// 优先尝试“智教联盟”线路组
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
// 优先尝试"inkeys"线路组
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
if (zhiJiaoGroup != null || inkeysGroup != null)
if (inkeysGroup != null)
{
var priorityGroups = new List<UpdateLineGroup>();
if (zhiJiaoGroup != null)
{
priorityGroups.Add(zhiJiaoGroup);
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试智教联盟线路组");
}
if (inkeysGroup != null)
{
priorityGroups.Add(inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试inkeys线路组");
}
groups = priorityGroups.Concat(groups.Where(g => g.GroupName != "智教联盟" && g.GroupName != "inkeys")).ToList();
groups.Remove(inkeysGroup);
groups.Insert(0, inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试inkeys线路组");
}
// 依次尝试每个线路组
@@ -1130,7 +1246,7 @@ namespace Ink_Canvas.Helpers
}
}
// 安装新版本应用
// 安装新版本应用 - 优化版本,不使用命令行
public static void InstallNewVersionApp(string version, bool isInSilence)
{
try
@@ -1183,112 +1299,109 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | ZIP文件大小: {fileInfo.Length} 字节");
string currentAppDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
int currentProcessId = Process.GetCurrentProcess().Id;
string appPath = Assembly.GetExecutingAssembly().Location;
int currentProcessId = Process.GetCurrentProcess().Id;
LogHelper.WriteLogToFile($"AutoUpdate | 当前应用程序目录: {currentAppDir}");
LogHelper.WriteLogToFile($"AutoUpdate | 当前进程ID: {currentProcessId}");
LogHelper.WriteLogToFile($"AutoUpdate | 静默更新模式: {isInSilence}");
string batchFilePath = Path.Combine(Path.GetTempPath(), "UpdateICC_" + Guid.NewGuid().ToString().Substring(0, 8) + ".bat");
LogHelper.WriteLogToFile($"AutoUpdate | 创建更新批处理文件: {batchFilePath}");
StringBuilder batchContent = new StringBuilder();
batchContent.AppendLine("@echo off");
batchContent.AppendLine("echo Set objShell = CreateObject(\"WScript.Shell\") > \"%temp%\\hideme.vbs\"");
batchContent.AppendLine("echo objShell.Run \"cmd /c \"\"\" ^& WScript.Arguments(0) ^& \"\"\"\", 0, True >> \"%temp%\\hideme.vbs\"");
batchContent.AppendLine("echo Wscript.Sleep 100 >> \"%temp%\\hideme.vbs\"");
string updateBatPath = Path.Combine(Path.GetTempPath(), "ICCUpdate_" + Guid.NewGuid().ToString().Substring(0, 8) + ".bat");
batchContent.AppendLine($"echo @echo off > \"{updateBatPath}\"");
batchContent.AppendLine($"echo set PROC_ID={currentProcessId} >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo :CHECK_PROCESS >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo tasklist /fi \"PID eq %PROC_ID%\" ^| find \"%PROC_ID%\" ^> nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if %%ERRORLEVEL%% == 0 ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo timeout /t 1 /nobreak ^> nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo goto CHECK_PROCESS >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Application closed, starting update process... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo timeout /t 2 /nobreak ^> nul >> \"{updateBatPath}\"");
// 创建解压目录
string extractPath = Path.Combine(updatesFolderPath, $"Extract_{version}");
batchContent.AppendLine($"echo echo Extracting update files... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if exist \"{extractPath}\" rd /s /q \"{extractPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo mkdir \"{extractPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo powershell -command \"Expand-Archive -Path '{zipFilePath.Replace("'", "''")}' -DestinationPath '{extractPath.Replace("'", "''")}' -Force\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if %%ERRORLEVEL%% neq 0 ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo goto ERROR_EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Copying updated files to application directory... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo xcopy /s /y /e \"{extractPath}\\*\" \"{currentAppDir}\\\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if %%ERRORLEVEL%% neq 0 ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo goto ERROR_EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Cleaning up temporary files... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if exist \"{extractPath}\" rd /s /q \"{extractPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if exist \"{zipFilePath}\" del /f /q \"{zipFilePath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Update completed successfully! >> \"{updateBatPath}\"");
if (isInSilence)
if (Directory.Exists(extractPath))
{
batchContent.AppendLine($"echo echo 自动启动应用程序... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo start \"\" \"{appPath}\" >> \"{updateBatPath}\"");
}
else
{
batchContent.AppendLine($"echo taskkill /F /IM \"InkCanvasForClass.exe\" >nul 2>nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo :: 检查应用程序是否已经在运行 >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo tasklist /FI \"IMAGENAME eq InkCanvasForClass.exe\" | find /i \"InkCanvasForClass.exe\" > nul >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo if %%ERRORLEVEL%% neq 0 ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo 启动应用程序... >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo start \"\" \"{appPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) else ( >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo 应用程序已经在运行,不再重复启动 >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo ) >> \"{updateBatPath}\"");
try
{
Directory.Delete(extractPath, true);
LogHelper.WriteLogToFile($"AutoUpdate | 清理已存在的解压目录: {extractPath}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 清理解压目录失败: {ex.Message}", LogHelper.LogType.Warning);
}
}
batchContent.AppendLine($"echo exit /b 0 >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo goto EXIT >> \"{updateBatPath}\"");
if (isInSilence)
try
{
batchContent.AppendLine($"echo :ERROR_EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo echo Update failed! >> \"%temp%\\icc_update_error.log\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo exit /b 1 >> \"{updateBatPath}\"");
Directory.CreateDirectory(extractPath);
LogHelper.WriteLogToFile($"AutoUpdate | 创建解压目录: {extractPath}");
}
else
catch (Exception ex)
{
batchContent.AppendLine($"echo :ERROR_EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo start \"\" cmd /c \"echo Update failed! ^& pause\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo exit /b 1 >> \"{updateBatPath}\"");
LogHelper.WriteLogToFile($"AutoUpdate | 创建解压目录失败: {ex.Message}", LogHelper.LogType.Error);
return;
}
batchContent.AppendLine($"echo :EXIT >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo del \"{updateBatPath}\" >> \"{updateBatPath}\"");
batchContent.AppendLine($"echo exit >> \"{updateBatPath}\"");
batchContent.AppendLine($"wscript \"%temp%\\hideme.vbs\" \"{updateBatPath}\"");
batchContent.AppendLine("del \"%temp%\\hideme.vbs\"");
batchContent.AppendLine("exit");
File.WriteAllText(batchFilePath, batchContent.ToString());
LogHelper.WriteLogToFile("AutoUpdate | 创建更新批处理文件完成");
Process.Start(new ProcessStartInfo
// 解压ZIP文件
try
{
FileName = batchFilePath,
CreateNoWindow = true,
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
});
LogHelper.WriteLogToFile($"AutoUpdate | 开始解压ZIP文件到: {extractPath}");
ZipFile.ExtractToDirectory(zipFilePath, extractPath);
LogHelper.WriteLogToFile("AutoUpdate | ZIP文件解压完成");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 解压ZIP文件失败: {ex.Message}", LogHelper.LogType.Error);
return;
}
LogHelper.WriteLogToFile("AutoUpdate | 启动更新批处理进程(隐藏窗口)");
// 查找解压后的主程序文件
string newAppPath = null;
string[] possibleExeNames = { "InkCanvasForClass.exe", "Ink Canvas.exe", "InkCanvas.exe" };
foreach (string exeName in possibleExeNames)
{
string testPath = Path.Combine(extractPath, exeName);
if (File.Exists(testPath))
{
newAppPath = testPath;
LogHelper.WriteLogToFile($"AutoUpdate | 找到新版本主程序: {newAppPath}");
break;
}
}
if (string.IsNullOrEmpty(newAppPath))
{
LogHelper.WriteLogToFile("AutoUpdate | 在解压目录中未找到主程序文件", LogHelper.LogType.Error);
return;
}
// 启动新版本进程
try
{
LogHelper.WriteLogToFile($"AutoUpdate | 准备启动新版本进程: {newAppPath}");
// 启动新版本进程(以更新模式)
string arguments = $"--update-mode --old-process-id={currentProcessId} --extract-path=\"{extractPath}\" --target-path=\"{currentAppDir}\" --is-silence={isInSilence}";
LogHelper.WriteLogToFile($"AutoUpdate | 启动新进程的命令行: {newAppPath} {arguments}");
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = newAppPath,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = false
};
Process.Start(startInfo);
LogHelper.WriteLogToFile("AutoUpdate | 新版本进程启动命令已执行");
// 等待一小段时间确保新进程启动
Thread.Sleep(2000);
// 关闭当前旧软件进程
LogHelper.WriteLogToFile("AutoUpdate | 关闭当前旧软件进程");
App.IsAppExitByUser = true;
Application.Current.Dispatcher.Invoke(() =>
{
Application.Current.Shutdown();
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 启动新版本进程时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
catch (Exception ex)
{
@@ -1300,6 +1413,445 @@ namespace Ink_Canvas.Helpers
}
}
// 处理更新模式的启动参数
public static bool HandleUpdateModeStartup(string[] args)
{
try
{
// 检查是否以更新模式启动
if (args.Contains("--update-mode"))
{
LogHelper.WriteLogToFile("AutoUpdate | 检测到更新模式启动");
// 解析命令行参数
int oldProcessId = -1;
string extractPath = null;
string targetPath = null;
bool isSilence = false;
// 记录所有参数用于调试
LogHelper.WriteLogToFile($"AutoUpdate | 接收到的命令行参数: {string.Join(" ", args)}");
for (int i = 0; i < args.Length; i++)
{
string arg = args[i];
LogHelper.WriteLogToFile($"AutoUpdate | 处理参数 {i}: {arg}");
if (arg.StartsWith("--old-process-id="))
{
string processIdStr = arg.Substring("--old-process-id=".Length);
if (int.TryParse(processIdStr, out int pid))
{
oldProcessId = pid;
LogHelper.WriteLogToFile($"AutoUpdate | 解析到老进程ID: {oldProcessId}");
}
}
else if (arg.StartsWith("--extract-path="))
{
extractPath = arg.Substring("--extract-path=".Length).Trim('"');
LogHelper.WriteLogToFile($"AutoUpdate | 解析到解压路径: {extractPath}");
}
else if (arg.StartsWith("--target-path="))
{
targetPath = arg.Substring("--target-path=".Length).Trim('"');
LogHelper.WriteLogToFile($"AutoUpdate | 解析到目标路径: {targetPath}");
}
else if (arg.StartsWith("--is-silence="))
{
string silenceStr = arg.Substring("--is-silence=".Length);
if (bool.TryParse(silenceStr, out bool silence))
{
isSilence = silence;
LogHelper.WriteLogToFile($"AutoUpdate | 解析到静默模式: {isSilence}");
}
}
}
LogHelper.WriteLogToFile($"AutoUpdate | 更新参数 - 老进程ID: {oldProcessId}, 解压路径: {extractPath}, 目标路径: {targetPath}, 静默模式: {isSilence}");
if (oldProcessId > 0 && !string.IsNullOrEmpty(extractPath) && !string.IsNullOrEmpty(targetPath))
{
LogHelper.WriteLogToFile("AutoUpdate | 参数验证通过,启动更新任务");
// 启动更新任务
Task.Run(async () => await PerformUpdate(oldProcessId, extractPath, targetPath, isSilence));
return true; // 返回true表示是更新模式
}
LogHelper.WriteLogToFile($"AutoUpdate | 参数验证失败 - 老进程ID: {oldProcessId}, 解压路径: {extractPath}, 目标路径: {targetPath}", LogHelper.LogType.Error);
return false;
}
return false; // 返回false表示不是更新模式
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 处理更新模式启动时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
// 执行实际的更新操作
private static async Task PerformUpdate(int oldProcessId, string extractPath, string targetPath, bool isSilence)
{
try
{
LogHelper.WriteLogToFile("AutoUpdate | 开始执行更新操作");
// 等待老进程完全退出
LogHelper.WriteLogToFile($"AutoUpdate | 等待老进程 {oldProcessId} 退出");
int waitCount = 0;
const int maxWaitCount = 30; // 最多等待30秒
while (waitCount < maxWaitCount)
{
try
{
Process oldProcess = Process.GetProcessById(oldProcessId);
if (oldProcess.HasExited)
{
LogHelper.WriteLogToFile("AutoUpdate | 老进程已退出");
break;
}
LogHelper.WriteLogToFile($"AutoUpdate | 老进程仍在运行,等待中... ({waitCount + 1}/{maxWaitCount})");
Thread.Sleep(1000);
waitCount++;
}
catch (ArgumentException)
{
// 进程不存在,说明已经退出
LogHelper.WriteLogToFile("AutoUpdate | 老进程已退出(进程不存在)");
break;
}
}
if (waitCount >= maxWaitCount)
{
LogHelper.WriteLogToFile("AutoUpdate | 等待老进程退出超时,尝试强制结束", LogHelper.LogType.Warning);
try
{
Process oldProcess = Process.GetProcessById(oldProcessId);
oldProcess.Kill();
Thread.Sleep(2000); // 等待进程完全结束
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 强制结束老进程失败: {ex.Message}", LogHelper.LogType.Warning);
}
}
// 确保目标目录存在
if (!Directory.Exists(targetPath))
{
Directory.CreateDirectory(targetPath);
LogHelper.WriteLogToFile($"AutoUpdate | 创建目标目录: {targetPath}");
}
// 复制文件到目标目录
LogHelper.WriteLogToFile($"AutoUpdate | 开始复制文件从 {extractPath} 到 {targetPath}");
try
{
// 使用递归复制方法,支持重试机制
bool copySuccess = await CopyDirectoryWithRetryAsync(extractPath, targetPath);
if (copySuccess)
{
LogHelper.WriteLogToFile("AutoUpdate | 文件复制完成");
}
else
{
LogHelper.WriteLogToFile("AutoUpdate | 文件复制失败,部分文件可能无法覆盖", LogHelper.LogType.Error);
if (!isSilence)
{
MessageBox.Show("更新失败:部分文件无法覆盖,可能是文件正在使用中。\n请关闭所有相关程序后重试。", "更新失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
return;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 文件复制失败: {ex.Message}", LogHelper.LogType.Error);
if (!isSilence)
{
MessageBox.Show($"更新失败:文件复制时出错\n{ex.Message}", "更新失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
return;
}
// 清理临时文件
try
{
LogHelper.WriteLogToFile("AutoUpdate | 清理临时文件");
// 删除解压目录
if (Directory.Exists(extractPath))
{
Directory.Delete(extractPath, true);
LogHelper.WriteLogToFile($"AutoUpdate | 删除解压目录: {extractPath}");
}
// 删除ZIP文件
string zipFile = Path.Combine(updatesFolderPath, "InkCanvasForClass.CE.*.zip");
string[] zipFiles = Directory.GetFiles(updatesFolderPath, "InkCanvasForClass.CE.*.zip");
foreach (string zip in zipFiles)
{
try
{
File.Delete(zip);
LogHelper.WriteLogToFile($"AutoUpdate | 删除ZIP文件: {zip}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 删除ZIP文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 清理临时文件时出错: {ex.Message}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("AutoUpdate | 更新操作完成");
// 启动更新后的应用程序
string newAppPath = Path.Combine(targetPath, "InkCanvasForClass.exe");
if (File.Exists(newAppPath))
{
try
{
LogHelper.WriteLogToFile($"AutoUpdate | 准备启动更新后的应用程序: {newAppPath}");
// 获取当前更新进程ID
int currentUpdateProcessId = Process.GetCurrentProcess().Id;
LogHelper.WriteLogToFile($"AutoUpdate | 当前更新进程ID: {currentUpdateProcessId}");
// 创建一个临时标记文件,用于新进程检测更新状态
string updateMarkerFile = Path.Combine(targetPath, "update_in_progress.tmp");
File.WriteAllText(updateMarkerFile, currentUpdateProcessId.ToString());
LogHelper.WriteLogToFile($"AutoUpdate | 创建更新标记文件: {updateMarkerFile}");
// 启动更新后的应用程序(标记为最终应用,不受相同进程影响)
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = newAppPath,
Arguments = "--final-app --skip-mutex-check",
WorkingDirectory = targetPath,
UseShellExecute = false
};
Process newProcess = Process.Start(startInfo);
LogHelper.WriteLogToFile($"AutoUpdate | 最终应用程序启动成功,PID: {newProcess?.Id},已标记为最终应用");
// 等待一小段时间确保最终应用程序启动
Thread.Sleep(2000);
// 结束当前更新进程
LogHelper.WriteLogToFile("AutoUpdate | 更新流程完成,结束更新进程");
// 强制结束当前更新进程
try
{
LogHelper.WriteLogToFile("AutoUpdate | 强制结束更新进程");
// 标记为应用主动退出,避免看门狗重启
App.IsAppExitByUser = true;
// 写入退出信号文件,确保看门狗不会重启
try
{
string watchdogExitSignalFile = Path.Combine(Path.GetTempPath(), "icc_watchdog_exit_" + Process.GetCurrentProcess().Id + ".flag");
File.WriteAllText(watchdogExitSignalFile, "exit");
LogHelper.WriteLogToFile("AutoUpdate | 已写入看门狗退出信号文件");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 写入看门狗退出信号文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
Environment.Exit(0);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 结束当前更新进程失败: {ex.Message}", LogHelper.LogType.Error);
Environment.Exit(0);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 启动更新后的应用程序失败: {ex.Message}", LogHelper.LogType.Error);
if (!isSilence)
{
MessageBox.Show($"更新完成,但启动应用程序失败:{ex.Message}\n请手动启动应用程序。", "启动失败", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | 更新后的应用程序文件不存在: {newAppPath}", LogHelper.LogType.Error);
if (!isSilence)
{
MessageBox.Show($"更新完成,但未找到应用程序文件:{newAppPath}\n请检查更新是否成功。", "文件缺失", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 执行更新操作时出错: {ex.Message}", LogHelper.LogType.Error);
if (!isSilence)
{
MessageBox.Show($"更新失败:{ex.Message}", "更新失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
// 异步复制目录的辅助方法(带重试机制)
private static async Task<bool> CopyDirectoryWithRetryAsync(string sourceDir, string destinationDir)
{
var dir = new DirectoryInfo(sourceDir);
DirectoryInfo[] dirs = dir.GetDirectories();
bool allFilesCopied = true;
// 如果目标目录不存在,则创建它
if (!Directory.Exists(destinationDir))
{
Directory.CreateDirectory(destinationDir);
}
// 定义需要覆盖的文件列表(仅覆盖主程序和配置文件)
string[] filesToOverwrite = { "InkCanvasForClass.exe", "InkCanvasForClass.exe.config" };
// 复制文件
foreach (FileInfo file in dir.GetFiles())
{
// 只覆盖指定的文件,跳过其他文件
if (!filesToOverwrite.Contains(file.Name))
{
LogHelper.WriteLogToFile($"AutoUpdate | 跳过文件(不在覆盖列表中): {file.Name}");
continue;
}
string targetFilePath = Path.Combine(destinationDir, file.Name);
bool fileCopied = false;
LogHelper.WriteLogToFile($"AutoUpdate | 开始覆盖文件: {file.Name}");
// 重试机制,最多重试3次
for (int retry = 0; retry < 3; retry++)
{
try
{
// 如果目标文件存在,先尝试删除
if (File.Exists(targetFilePath))
{
try
{
File.Delete(targetFilePath);
}
catch (IOException)
{
// 文件可能正在使用,等待一下再重试
if (retry < 2)
{
Thread.Sleep(1000);
continue;
}
}
}
await Task.Run(() => file.CopyTo(targetFilePath));
fileCopied = true;
LogHelper.WriteLogToFile($"AutoUpdate | 文件覆盖成功: {file.Name}");
break;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 复制文件失败 (重试 {retry + 1}/3) {file.FullName} -> {targetFilePath}: {ex.Message}", LogHelper.LogType.Warning);
if (retry < 2)
{
Thread.Sleep(1000); // 等待1秒后重试
}
}
}
if (!fileCopied)
{
allFilesCopied = false;
LogHelper.WriteLogToFile($"AutoUpdate | 文件复制最终失败: {file.FullName}", LogHelper.LogType.Error);
}
}
// 递归复制子目录
foreach (DirectoryInfo subDir in dirs)
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
bool subDirCopied = await CopyDirectoryWithRetryAsync(subDir.FullName, newDestinationDir);
if (!subDirCopied)
{
allFilesCopied = false;
}
}
return allFilesCopied;
}
// 异步复制目录的辅助方法(原版本,保留兼容性)
private static async Task CopyDirectoryAsync(string sourceDir, string destinationDir)
{
var dir = new DirectoryInfo(sourceDir);
DirectoryInfo[] dirs = dir.GetDirectories();
// 如果目标目录不存在,则创建它
if (!Directory.Exists(destinationDir))
{
Directory.CreateDirectory(destinationDir);
}
// 定义需要覆盖的文件列表(仅覆盖主程序和配置文件)
string[] filesToOverwrite = { "InkCanvasForClass.exe", "InkCanvasForClass.exe.config" };
// 复制文件
foreach (FileInfo file in dir.GetFiles())
{
// 只覆盖指定的文件,跳过其他文件
if (!filesToOverwrite.Contains(file.Name))
{
LogHelper.WriteLogToFile($"AutoUpdate | 跳过文件(不在覆盖列表中): {file.Name}");
continue;
}
string targetFilePath = Path.Combine(destinationDir, file.Name);
try
{
LogHelper.WriteLogToFile($"AutoUpdate | 开始覆盖文件: {file.Name}");
// 如果目标文件存在且正在使用,先删除
if (File.Exists(targetFilePath))
{
File.Delete(targetFilePath);
}
await Task.Run(() => file.CopyTo(targetFilePath));
LogHelper.WriteLogToFile($"AutoUpdate | 文件覆盖成功: {file.Name}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 复制文件失败 {file.FullName} -> {targetFilePath}: {ex.Message}", LogHelper.LogType.Warning);
// 继续复制其他文件,不中断整个过程
}
}
// 递归复制子目录
foreach (DirectoryInfo subDir in dirs)
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
await CopyDirectoryAsync(subDir.FullName, newDestinationDir);
}
}
// 获取远程内容的通用方法
public static async Task<string> GetRemoteContent(string fileUrl)
{
@@ -1449,8 +2001,8 @@ namespace Ink_Canvas.Helpers
return false;
}
// 执行安装,静默模式
InstallNewVersionApp(remoteVersion, false);
// 执行安装,静默模式
InstallNewVersionApp(remoteVersion, true);
App.IsAppExitByUser = true;
Application.Current.Dispatcher.Invoke(() =>
{
@@ -1477,6 +2029,7 @@ namespace Ink_Canvas.Helpers
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
LogHelper.WriteLogToFile("AutoUpdate | 使用GitHub API调用");
var response = await client.GetStringAsync(apiUrl);
var arr = JArray.Parse(response);
foreach (var item in arr)
@@ -1579,7 +2132,7 @@ namespace Ink_Canvas.Helpers
return false;
}
LogHelper.WriteLogToFile($"AutoUpdate | 手动安装版本: {version}");
InstallNewVersionApp(version, false);
InstallNewVersionApp(version, true);
App.IsAppExitByUser = true;
Application.Current.Dispatcher.Invoke(() =>
{
@@ -1630,4 +2183,3 @@ namespace Ink_Canvas.Helpers
}
}
}
+424
View File
@@ -0,0 +1,424 @@
using AForge.Video;
using AForge.Video.DirectShow;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers
{
public class CameraService : IDisposable
{
private VideoCaptureDevice _videoSource;
private bool _isCapturing;
private Bitmap _currentFrame;
private readonly object _frameLock = new object();
private Dispatcher _dispatcher;
// 新增属性
private int _rotationAngle = 0; // 0=0度,1=90度,2=180度,3=270度
private int _resolutionWidth = 640;
private int _resolutionHeight = 480;
public event EventHandler<Bitmap> FrameReceived;
public event EventHandler<string> ErrorOccurred;
public bool IsCapturing => _isCapturing;
public List<FilterInfo> AvailableCameras { get; private set; }
public FilterInfo CurrentCamera { get; private set; }
// 新增属性
public int RotationAngle
{
get => _rotationAngle;
set => _rotationAngle = Math.Max(0, Math.Min(3, value));
}
public int ResolutionWidth
{
get => _resolutionWidth;
set => _resolutionWidth = Math.Max(320, Math.Min(1920, value));
}
public int ResolutionHeight
{
get => _resolutionHeight;
set => _resolutionHeight = Math.Max(240, Math.Min(1080, value));
}
public CameraService()
{
_dispatcher = Dispatcher.CurrentDispatcher;
AvailableCameras = new List<FilterInfo>();
RefreshCameraList();
}
public CameraService(int rotationAngle, int resolutionWidth, int resolutionHeight)
{
_dispatcher = Dispatcher.CurrentDispatcher;
AvailableCameras = new List<FilterInfo>();
_rotationAngle = rotationAngle;
_resolutionWidth = resolutionWidth;
_resolutionHeight = resolutionHeight;
RefreshCameraList();
}
/// <summary>
/// 刷新可用摄像头列表
/// </summary>
public void RefreshCameraList()
{
try
{
AvailableCameras.Clear();
var videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
foreach (FilterInfo device in videoDevices)
{
AvailableCameras.Add(device);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"刷新摄像头列表失败: {ex.Message}", LogHelper.LogType.Error);
ErrorOccurred?.Invoke(this, $"刷新摄像头列表失败: {ex.Message}");
}
}
/// <summary>
/// 开始摄像头预览
/// </summary>
/// <param name="cameraIndex">摄像头索引</param>
public bool StartPreview(int cameraIndex = 0)
{
try
{
if (AvailableCameras.Count == 0)
{
RefreshCameraList();
if (AvailableCameras.Count == 0)
{
ErrorOccurred?.Invoke(this, "未找到可用的摄像头设备");
return false;
}
}
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
{
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
return false;
}
// 停止当前预览
StopPreview();
CurrentCamera = AvailableCameras[cameraIndex];
_videoSource = new VideoCaptureDevice(CurrentCamera.MonikerString);
// 设置视频源事件处理
_videoSource.NewFrame += VideoSource_NewFrame;
// 启动视频源
_videoSource.Start();
_isCapturing = true;
LogHelper.WriteLogToFile($"开始摄像头预览: {CurrentCamera.Name}");
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
ErrorOccurred?.Invoke(this, $"启动摄像头预览失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 停止摄像头预览
/// </summary>
public void StopPreview()
{
try
{
if (_videoSource != null && _videoSource.IsRunning)
{
_videoSource.SignalToStop();
_videoSource.WaitForStop();
_videoSource.NewFrame -= VideoSource_NewFrame;
_videoSource = null;
}
_isCapturing = false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"停止摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 切换到指定摄像头
/// </summary>
/// <param name="cameraIndex">摄像头索引</param>
public bool SwitchCamera(int cameraIndex)
{
try
{
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
{
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
return false;
}
return StartPreview(cameraIndex);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"切换摄像头失败: {ex.Message}", LogHelper.LogType.Error);
ErrorOccurred?.Invoke(this, $"切换摄像头失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 获取当前帧的BitmapSource(WPF格式),直接返回可用的WPF位图
/// </summary>
public BitmapSource GetCurrentFrameAsBitmapSource()
{
lock (_frameLock)
{
if (_currentFrame == null)
return null;
try
{
// 验证位图有效性
if (_currentFrame.Width <= 0 || _currentFrame.Height <= 0)
return null;
// 使用更安全的方法转换位图
var bitmapData = _currentFrame.LockBits(
new Rectangle(0, 0, _currentFrame.Width, _currentFrame.Height),
ImageLockMode.ReadOnly,
_currentFrame.PixelFormat);
try
{
// 根据像素格式选择合适的WPF像素格式
System.Windows.Media.PixelFormat wpfPixelFormat;
switch (_currentFrame.PixelFormat)
{
case PixelFormat.Format24bppRgb:
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
break;
case PixelFormat.Format32bppArgb:
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgra32;
break;
case PixelFormat.Format32bppRgb:
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr32;
break;
default:
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
break;
}
var bitmapSource = BitmapSource.Create(
bitmapData.Width,
bitmapData.Height,
_currentFrame.HorizontalResolution,
_currentFrame.VerticalResolution,
wpfPixelFormat,
null,
bitmapData.Scan0,
bitmapData.Stride * bitmapData.Height,
bitmapData.Stride);
bitmapSource.Freeze();
return bitmapSource;
}
finally
{
_currentFrame.UnlockBits(bitmapData);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"转换帧为BitmapSource失败: {ex.Message}", LogHelper.LogType.Error);
return null;
}
}
}
/// <summary>
/// 视频源新帧事件处理
/// </summary>
private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
{
try
{
lock (_frameLock)
{
// 释放之前的帧
_currentFrame?.Dispose();
// 创建新的位图,避免Clone的问题
var sourceFrame = eventArgs.Frame;
if (sourceFrame != null)
{
try
{
var width = sourceFrame.Width;
var height = sourceFrame.Height;
if (width > 0 && height > 0)
{
// 应用旋转
Bitmap rotatedFrame = ApplyRotation(sourceFrame);
int targetWidth = _resolutionWidth;
int targetHeight = _resolutionHeight;
if (_rotationAngle == 1 || _rotationAngle == 3)
{
targetWidth = _resolutionHeight;
targetHeight = _resolutionWidth;
}
_currentFrame = ResizeImageWithAspectRatio(rotatedFrame, targetWidth, targetHeight);
rotatedFrame?.Dispose();
}
else
{
_currentFrame = null;
}
}
catch (Exception frameEx)
{
LogHelper.WriteLogToFile($"处理源帧失败: {frameEx.Message}", LogHelper.LogType.Error);
_currentFrame = null;
}
}
else
{
_currentFrame = null;
}
}
// 在UI线程中触发事件
_dispatcher.BeginInvoke(new Action(() =>
{
FrameReceived?.Invoke(this, _currentFrame);
}));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理新帧失败: {ex.Message}", LogHelper.LogType.Error);
ErrorOccurred?.Invoke(this, $"处理新帧失败: {ex.Message}");
}
}
/// <summary>
/// 获取摄像头名称列表
/// </summary>
public List<string> GetCameraNames()
{
return AvailableCameras.Select(camera => camera.Name).ToList();
}
/// <summary>
/// 检查是否有可用摄像头
/// </summary>
public bool HasAvailableCameras()
{
if (AvailableCameras.Count == 0)
{
RefreshCameraList();
}
return AvailableCameras.Count > 0;
}
/// <summary>
/// 应用旋转到图像
/// </summary>
private Bitmap ApplyRotation(Bitmap source)
{
if (_rotationAngle == 0)
return new Bitmap(source);
var rotationType = RotateFlipType.RotateNoneFlipNone;
switch (_rotationAngle)
{
case 1: rotationType = RotateFlipType.Rotate90FlipNone; break;
case 2: rotationType = RotateFlipType.Rotate180FlipNone; break;
case 3: rotationType = RotateFlipType.Rotate270FlipNone; break;
}
var rotated = new Bitmap(source);
rotated.RotateFlip(rotationType);
return rotated;
}
/// <summary>
/// 调整图像大小
/// </summary>
private Bitmap ResizeImageWithAspectRatio(Bitmap source, int targetWidth, int targetHeight)
{
if (source.Width == targetWidth && source.Height == targetHeight)
return new Bitmap(source);
double scaleX = (double)targetWidth / source.Width;
double scaleY = (double)targetHeight / source.Height;
double scale = Math.Min(scaleX, scaleY);
// 计算实际尺寸
int actualWidth = (int)(source.Width * scale);
int actualHeight = (int)(source.Height * scale);
var resized = new Bitmap(actualWidth, actualHeight, PixelFormat.Format24bppRgb);
using (var graphics = Graphics.FromImage(resized))
{
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
graphics.DrawImage(source, 0, 0, actualWidth, actualHeight);
}
return resized;
}
/// <summary>
/// 调整图像大小
/// </summary>
private Bitmap ResizeImage(Bitmap source, int width, int height)
{
if (source.Width == width && source.Height == height)
return new Bitmap(source);
var resized = new Bitmap(width, height, PixelFormat.Format24bppRgb);
using (var graphics = Graphics.FromImage(resized))
{
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
graphics.DrawImage(source, 0, 0, width, height);
}
return resized;
}
public void Dispose()
{
StopPreview();
lock (_frameLock)
{
_currentFrame?.Dispose();
}
}
}
}
+23
View File
@@ -112,4 +112,27 @@ namespace Ink_Canvas.Converter
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); }
}
public class InverseBooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((bool)value)
{
return Visibility.Collapsed;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((bool)value)
{
return Visibility.Collapsed;
}
return Visibility.Visible;
}
}
}
File diff suppressed because it is too large Load Diff
+455
View File
@@ -0,0 +1,455 @@
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass API 客户端,用于与服务端通信
/// </summary>
public class DlassApiClient : IDisposable
{
private const string DEFAULT_BASE_URL = "https://dlass.tech";
private readonly string _appId;
private readonly string _appSecret;
private readonly string _baseUrl;
private HttpClient _httpClient;
private string _accessToken;
private DateTime _tokenExpiresAt;
private string _userToken;
/// <summary>
/// 初始化 Dlass API 客户端
/// </summary>
/// <param name="appId">应用ID</param>
/// <param name="appSecret">应用密钥</param>
/// <param name="baseUrl">API基础URL,如果为空则使用默认URL</param>
/// <param name="userToken">用户Token,如果提供则优先使用用户token而不是App Secret</param>
public DlassApiClient(string appId, string appSecret, string baseUrl = null, string userToken = null)
{
_appId = appId ?? throw new ArgumentNullException(nameof(appId));
_appSecret = appSecret ?? throw new ArgumentNullException(nameof(appSecret));
_userToken = userToken;
_baseUrl = baseUrl ?? DEFAULT_BASE_URL;
_baseUrl = _baseUrl.TrimEnd('/');
if (!_baseUrl.StartsWith("http://") && !_baseUrl.StartsWith("https://"))
{
_baseUrl = "https://" + _baseUrl;
}
_httpClient = new HttpClient
{
BaseAddress = new Uri(_baseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
_httpClient.DefaultRequestHeaders.Add("User-Agent", "InkCanvas/1.0");
}
/// <summary>
/// 获取访问令牌(Access Token
/// </summary>
public async Task<string> GetAccessTokenAsync()
{
if (!string.IsNullOrEmpty(_userToken))
{
return _userToken;
}
if (!string.IsNullOrEmpty(_accessToken) && DateTime.Now < _tokenExpiresAt.AddMinutes(-5))
{
return _accessToken;
}
try
{
var requestData = new
{
app_id = _appId,
app_secret = _appSecret,
grant_type = "client_credentials"
};
var json = JsonConvert.SerializeObject(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/oauth/token", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
_accessToken = tokenResponse.AccessToken;
_tokenExpiresAt = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn ?? 3600);
return _accessToken;
}
else
{
throw new Exception($"获取Access Token失败: {response.StatusCode}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"获取Access Token时网络错误: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception("获取Access Token时请求超时", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"获取Access Token时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送GET请求
/// </summary>
public async Task<T> GetAsync<T>(string endpoint, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"请求超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送POST请求
/// </summary>
public async Task<T> PostAsync<T>(string endpoint, object data = null, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"请求超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送PUT请求
/// </summary>
public async Task<T> PutAsync<T>(string endpoint, object data = null, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"请求超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送DELETE请求
/// </summary>
public async Task<bool> DeleteAsync(string endpoint, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return true;
}
else
{
return false;
}
}
catch (HttpRequestException)
{
return false;
}
catch (TaskCanceledException)
{
return false;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 上传笔记文件
/// </summary>
/// <param name="endpoint">上传端点</param>
/// <param name="filePath">文件路径</param>
/// <param name="boardId">白板ID</param>
/// <param name="secretKey">白板密钥</param>
/// <param name="title">笔记标题(可选)</param>
/// <param name="description">笔记描述(可选)</param>
/// <param name="tags">笔记标签(可选)</param>
public async Task<T> UploadNoteAsync<T>(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null)
{
try
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"文件不存在: {filePath}");
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
// 设置白板认证头
request.Headers.Add("X-Board-ID", boardId);
request.Headers.Add("X-Secret-Key", secretKey);
// 创建multipart/form-data内容
var content = new MultipartFormDataContent();
// 添加文件
var fileContent = new ByteArrayContent(File.ReadAllBytes(filePath));
var fileName = Path.GetFileName(filePath);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(fileContent, "file", fileName);
// 添加可选参数
if (!string.IsNullOrEmpty(title))
{
content.Add(new StringContent(title), "title");
}
if (!string.IsNullOrEmpty(description))
{
content.Add(new StringContent(description), "description");
}
if (!string.IsNullOrEmpty(tags))
{
content.Add(new StringContent(tags), "tags");
}
request.Content = content;
var response = await _httpClient.SendAsync(request);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(responseContent))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(responseContent);
}
else
{
throw new Exception($"上传文件失败: {response.StatusCode} - {responseContent}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"上传文件时网络错误: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"上传文件超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"上传文件时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_httpClient?.Dispose();
}
#region
/// <summary>
/// Token响应模型
/// </summary>
private class TokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int? ExpiresIn { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
}
#endregion
}
}
+757
View File
@@ -0,0 +1,757 @@
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass笔记自动上传辅助类
/// </summary>
public class DlassNoteUploader
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
private const int BATCH_SIZE = 10; // 批量上传大小
private const int MAX_RETRY_COUNT = 3; // 最大重试次数
private const string QUEUE_FILE_NAME = "DlassUploadQueue.json";
/// <summary>
/// 上传队列项
/// </summary>
private class UploadQueueItemData
{
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("retry_count")]
public int RetryCount { get; set; }
[JsonProperty("added_time")]
public DateTime AddedTime { get; set; }
}
/// <summary>
/// 上传队列项
/// </summary>
private class UploadQueueItem
{
public string FilePath { get; set; }
public int RetryCount { get; set; }
}
/// <summary>
/// 上传队列
/// </summary>
private static readonly ConcurrentQueue<UploadQueueItem> _uploadQueue = new ConcurrentQueue<UploadQueueItem>();
/// <summary>
/// 队列处理锁,防止并发处理
/// </summary>
private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 队列保存锁,防止并发保存
/// </summary>
private static readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 是否已初始化队列
/// </summary>
private static bool _isQueueInitialized = false;
/// <summary>
/// 获取队列文件路径
/// </summary>
private static string GetQueueFilePath()
{
var configsDir = Path.Combine(App.RootPath, "Configs");
if (!Directory.Exists(configsDir))
{
Directory.CreateDirectory(configsDir);
}
return Path.Combine(configsDir, QUEUE_FILE_NAME);
}
/// <summary>
/// 初始化上传队列
/// </summary>
public static void InitializeQueue()
{
if (_isQueueInitialized)
{
return;
}
try
{
var queueFilePath = GetQueueFilePath();
if (!File.Exists(queueFilePath))
{
_isQueueInitialized = true;
return;
}
var jsonContent = File.ReadAllText(queueFilePath);
if (string.IsNullOrWhiteSpace(jsonContent))
{
_isQueueInitialized = true;
return;
}
var queueData = JsonConvert.DeserializeObject<List<UploadQueueItemData>>(jsonContent);
if (queueData == null || queueData.Count == 0)
{
_isQueueInitialized = true;
return;
}
int restoredCount = 0;
int skippedCount = 0;
foreach (var item in queueData)
{
// 验证文件是否存在
if (!File.Exists(item.FilePath))
{
skippedCount++;
continue;
}
// 验证文件格式和大小
var fileExtension = Path.GetExtension(item.FilePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip")
{
skippedCount++;
continue;
}
try
{
var fileInfo = new FileInfo(item.FilePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
skippedCount++;
continue;
}
}
catch
{
skippedCount++;
continue;
}
// 恢复队列项
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = item.FilePath,
RetryCount = item.RetryCount
});
restoredCount++;
}
_isQueueInitialized = true;
if (restoredCount > 0)
{
LogHelper.WriteLogToFile($"已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
// 如果恢复了队列,触发处理
_ = ProcessUploadQueueAsync();
}
else if (skippedCount > 0)
{
LogHelper.WriteLogToFile($"队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
_isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试
}
}
/// <summary>
/// 保存队列到文件
/// </summary>
private static async Task SaveQueueToFileAsync()
{
if (!await _queueSaveLock.WaitAsync(1000)) // 最多等待1秒
{
return; // 如果无法获取锁,跳过保存(避免阻塞)
}
try
{
var queueData = new List<UploadQueueItemData>();
// 将队列转换为可序列化的格式
foreach (var item in _uploadQueue)
{
queueData.Add(new UploadQueueItemData
{
FilePath = item.FilePath,
RetryCount = item.RetryCount,
AddedTime = DateTime.Now
});
}
var queueFilePath = GetQueueFilePath();
// 如果队列为空,清空文件
if (queueData.Count == 0)
{
ClearQueueFile();
return;
}
var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented);
// 使用临时文件写入,然后替换,确保原子性
var tempFilePath = queueFilePath + ".tmp";
File.WriteAllText(tempFilePath, jsonContent);
// 如果原文件存在,先删除
if (File.Exists(queueFilePath))
{
File.Delete(queueFilePath);
}
// 重命名临时文件
File.Move(tempFilePath, queueFilePath);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
}
finally
{
_queueSaveLock.Release();
}
}
/// <summary>
/// 清空队列文件
/// </summary>
private static void ClearQueueFile()
{
try
{
var queueFilePath = GetQueueFilePath();
if (File.Exists(queueFilePath))
{
File.WriteAllText(queueFilePath, "[]");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 上传笔记响应模型
/// </summary>
public class UploadNoteResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("note_id")]
public int? NoteId { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("file_url")]
public string FileUrl { get; set; }
}
/// <summary>
/// 白板信息模型(用于查找白板)
/// </summary>
private class WhiteboardInfo
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("board_id")]
public string BoardId { get; set; }
[JsonProperty("secret_key")]
public string SecretKey { get; set; }
[JsonProperty("class_name")]
public string ClassName { get; set; }
[JsonProperty("class_id")]
public int ClassId { get; set; }
}
/// <summary>
/// 认证响应模型
/// </summary>
private class AuthWithTokenResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("whiteboards")]
public List<WhiteboardInfo> Whiteboards { get; set; }
}
/// <summary>
/// 异步上传笔记文件到Dlass(支持PNG、ICSTK和ZIP格式)
/// </summary>
/// <param name="filePath">文件路径(支持PNG、ICSTK和ZIP</param>
/// <returns>是否成功加入队列(不等待实际上传完成)</returns>
public static async Task<bool> UploadNoteFileAsync(string filePath)
{
try
{
// 检查是否启用自动上传
if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true)
{
return false;
}
// 基本验证
if (!File.Exists(filePath))
{
LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
return false;
}
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip")
{
return false;
}
var fileInfo = new FileInfo(filePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
return false;
}
// 获取上传延迟时间(分钟)
var delayMinutes = MainWindow.Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
// 如果设置了延迟时间,在后台任务中等待后再加入队列
if (delayMinutes > 0)
{
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
EnqueueFile(filePath);
});
}
else
{
EnqueueFile(filePath);
}
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 将文件加入上传队列
/// </summary>
private static void EnqueueFile(string filePath, int retryCount = 0)
{
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = filePath,
RetryCount = retryCount
});
// 异步保存队列到文件
_ = Task.Run(async () => await SaveQueueToFileAsync());
// 如果队列达到批量大小,触发批量上传
if (_uploadQueue.Count >= BATCH_SIZE)
{
_ = ProcessUploadQueueAsync();
}
}
/// <summary>
/// 处理上传队列,批量上传文件
/// </summary>
private static async Task ProcessUploadQueueAsync()
{
// 使用信号量防止并发处理
if (!await _queueProcessingLock.WaitAsync(0))
{
return; // 已有处理任务在运行
}
try
{
var filesToUpload = new List<UploadQueueItem>();
// 从队列中取出最多BATCH_SIZE个文件
while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
{
// 再次检查文件是否存在
if (File.Exists(item.FilePath))
{
filesToUpload.Add(item);
}
}
if (filesToUpload.Count == 0)
{
return;
}
// 获取共享的白板信息(同一批次的所有文件共享认证信息)
WhiteboardInfo sharedWhiteboard = null;
string apiBaseUrl = null;
string userToken = null;
try
{
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
userToken = MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
{
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
// 获取白板信息(只获取一次,所有文件共享)
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
sharedWhiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (sharedWhiteboard == null || string.IsNullOrEmpty(sharedWhiteboard.BoardId) || string.IsNullOrEmpty(sharedWhiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
// 并发上传所有文件(共享白板信息),并处理失败重试
var uploadTasks = filesToUpload.Select(async item =>
{
try
{
var success = await UploadFileInternalAsync(item.FilePath, sharedWhiteboard, apiBaseUrl, userToken);
if (!success)
{
// 检查是否是可重试的错误
if (IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1);
}
else
{
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
}
return success;
}
catch (Exception ex)
{
// 检查是否是可重试的错误(超时、网络错误等)
var errorMessage = ex.Message.ToLower();
bool isRetryable = errorMessage.Contains("超时") ||
errorMessage.Contains("timeout") ||
errorMessage.Contains("网络错误") ||
errorMessage.Contains("network");
if (isRetryable && IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1);
}
else
{
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
return false;
}
});
await Task.WhenAll(uploadTasks);
// 上传完成后保存队列状态
await SaveQueueToFileAsync();
// 如果队列达到批量大小,继续处理
if (_uploadQueue.Count >= BATCH_SIZE)
{
_ = ProcessUploadQueueAsync();
}
}
finally
{
_queueProcessingLock.Release();
}
}
/// <summary>
/// 内部上传方法,执行实际上传操作
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="whiteboard">白板信息(如果为null则重新获取)</param>
/// <param name="apiBaseUrl">API基础URL(如果为null则从设置获取)</param>
/// <param name="userToken">用户Token(如果为null则从设置获取)</param>
private static async Task<bool> UploadFileInternalAsync(string filePath, WhiteboardInfo whiteboard = null, string apiBaseUrl = null, string userToken = null)
{
try
{
// 再次检查文件是否存在(可能在队列等待时被删除)
if (!File.Exists(filePath))
{
return false;
}
// 检查文件扩展名
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip")
{
return false;
}
// 检查文件大小(最大10MB,ZIP文件可能更大,允许50MB)
var fileInfo = new FileInfo(filePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
return false;
}
// 如果白板信息未提供,则重新获取
if (whiteboard == null)
{
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
return false;
}
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
{
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
return false;
}
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
// 创建API客户端并获取白板信息
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
return false;
}
// 查找匹配班级的白板
whiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
return false;
}
}
}
// 获取API基础URL和用户Token(如果未提供)
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
// 准备上传参数
var fileName = Path.GetFileNameWithoutExtension(filePath);
var title = fileName;
string fileType;
string tags;
if (fileExtension == ".zip")
{
fileType = "多页面墨迹压缩包";
tags = "自动上传,多页面,zip,压缩包";
}
else if (fileExtension == ".icstk")
{
fileType = "墨迹文件";
tags = "自动上传,墨迹,icstk";
}
else
{
fileType = "笔记";
tags = "自动上传,笔记,png";
}
var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
// 创建API客户端并上传文件
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var uploadResult = await apiClient.UploadNoteAsync<UploadNoteResponse>(
"/api/whiteboard/upload_note",
filePath,
whiteboard.BoardId,
whiteboard.SecretKey,
title,
description,
tags);
if (uploadResult != null && uploadResult.Success)
{
LogHelper.WriteLogToFile($"笔记上传成功:{fileName} -> {uploadResult.FileUrl}", LogHelper.LogType.Event);
return true;
}
else
{
LogHelper.WriteLogToFile($"上传失败:服务器响应失败 - {uploadResult?.Message ?? ""}", LogHelper.LogType.Error);
return false;
}
}
}
catch (Exception ex)
{
// 记录错误信息,抛出异常以便调用方判断是否可重试
LogHelper.WriteLogToFile($"上传笔记时出错: {ex.Message}", LogHelper.LogType.Error);
throw;
}
}
/// <summary>
/// 判断错误是否可重试(超时、网络错误等)
/// </summary>
private static bool IsRetryableError(string filePath)
{
// 检查文件是否存在
if (!File.Exists(filePath))
{
return false; // 文件不存在,不可重试
}
// 检查文件扩展名
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip")
{
return false; // 文件格式错误,不可重试
}
// 检查文件大小
try
{
var fileInfo = new FileInfo(filePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
return false; // 文件过大,不可重试
}
}
catch
{
return false; // 无法读取文件信息,不可重试
}
// 其他错误(超时、网络错误等)可以重试
return true;
}
}
}
@@ -0,0 +1,589 @@
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Threading;
using System.Windows;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 文件关联管理器,用于注册和处理.icstk文件的关联
/// </summary>
public static class FileAssociationManager
{
private const string FileExtension = ".icstk";
private const string FileTypeName = "InkCanvasStrokesFile";
private const string AppName = "Ink Canvas";
private const string AppDescription = "Ink Canvas Strokes File";
// IPC相关常量
private const string IpcMutexName = "InkCanvasFileAssociationIpc";
private const string IpcEventName = "InkCanvasFileAssociationEvent";
private const string IpcFilePrefix = "InkCanvasFileAssociation_";
private const string IpcBoardModePrefix = "InkCanvasBoardMode_";
private const string IpcShowModePrefix = "InkCanvasShowMode_";
private const int IpcTimeout = 5000; // 5秒超时
/// <summary>
/// 注册.icstk文件关联
/// </summary>
public static bool RegisterFileAssociation()
{
try
{
string exePath = Process.GetCurrentProcess().MainModule.FileName;
// 注册文件类型
using (RegistryKey fileTypeKey = Registry.ClassesRoot.CreateSubKey(FileTypeName))
{
fileTypeKey.SetValue("", AppDescription);
fileTypeKey.SetValue("FriendlyTypeName", AppDescription);
// 设置默认图标
using (RegistryKey defaultIconKey = fileTypeKey.CreateSubKey("DefaultIcon"))
{
defaultIconKey.SetValue("", $"\"{exePath}\",0");
}
// 设置打开命令
using (RegistryKey shellKey = fileTypeKey.CreateSubKey("shell"))
using (RegistryKey openKey = shellKey.CreateSubKey("open"))
using (RegistryKey commandKey = openKey.CreateSubKey("command"))
{
commandKey.SetValue("", $"\"{exePath}\" \"%1\"");
}
}
// 注册文件扩展名
using (RegistryKey extensionKey = Registry.ClassesRoot.CreateSubKey(FileExtension))
{
extensionKey.SetValue("", FileTypeName);
}
// 刷新系统文件关联缓存
RefreshSystemFileAssociations();
LogHelper.WriteLogToFile($"成功注册{FileExtension}文件关联", LogHelper.LogType.Event);
return true;
}
catch (SecurityException ex)
{
LogHelper.WriteLogToFile($"注册文件关联时权限不足: {ex.Message}", LogHelper.LogType.Error);
return false;
}
catch (UnauthorizedAccessException ex)
{
LogHelper.WriteLogToFile($"注册文件关联时访问被拒绝: {ex.Message}", LogHelper.LogType.Error);
return false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"注册文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 注销.icstk文件关联
/// </summary>
public static bool UnregisterFileAssociation()
{
try
{
// 删除文件扩展名关联
Registry.ClassesRoot.DeleteSubKeyTree(FileExtension, false);
// 删除文件类型定义
Registry.ClassesRoot.DeleteSubKeyTree(FileTypeName, false);
// 刷新系统文件关联缓存
RefreshSystemFileAssociations();
LogHelper.WriteLogToFile($"成功注销{FileExtension}文件关联", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"注销文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 检查文件关联是否已注册
/// </summary>
public static bool IsFileAssociationRegistered()
{
try
{
using (RegistryKey extensionKey = Registry.ClassesRoot.OpenSubKey(FileExtension))
{
if (extensionKey == null) return false;
string fileType = extensionKey.GetValue("") as string;
if (string.IsNullOrEmpty(fileType)) return false;
using (RegistryKey fileTypeKey = Registry.ClassesRoot.OpenSubKey(fileType))
{
if (fileTypeKey == null) return false;
using (RegistryKey shellKey = fileTypeKey.OpenSubKey("shell\\open\\command"))
{
if (shellKey == null) return false;
string command = shellKey.GetValue("") as string;
if (string.IsNullOrEmpty(command)) return false;
// 检查命令是否指向当前应用程序
string currentExePath = Process.GetCurrentProcess().MainModule.FileName;
return command.Contains(currentExePath);
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"检查文件关联状态时出错: {ex.Message}", LogHelper.LogType.Error);
}
return false;
}
/// <summary>
/// 显示文件关联状态
/// </summary>
public static void ShowFileAssociationStatus()
{
bool isRegistered = IsFileAssociationRegistered();
LogHelper.WriteLogToFile($"{FileExtension}文件关联状态: {(isRegistered ? "" : "")}", LogHelper.LogType.Event);
}
/// <summary>
/// 刷新系统文件关联缓存
/// </summary>
private static void RefreshSystemFileAssociations()
{
try
{
// 通知系统文件关联已更改
SHChangeNotify(0x08000000, 0, IntPtr.Zero, IntPtr.Zero);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"刷新文件关联缓存时出错: {ex.Message}", LogHelper.LogType.Warning);
}
}
/// <summary>
/// 处理命令行参数中的文件路径
/// </summary>
/// <param name="args">命令行参数</param>
/// <returns>找到的.icstk文件路径,如果没有找到则返回null</returns>
public static string GetIcstkFileFromArgs(string[] args)
{
if (args == null || args.Length == 0) return null;
foreach (string arg in args)
{
if (string.IsNullOrEmpty(arg)) continue;
// 检查是否为.icstk文件
if (Path.GetExtension(arg).Equals(FileExtension, StringComparison.OrdinalIgnoreCase))
{
// 检查文件是否存在
if (File.Exists(arg))
{
LogHelper.WriteLogToFile($"从命令行参数中找到.icstk文件: {arg}", LogHelper.LogType.Event);
return arg;
}
else
{
LogHelper.WriteLogToFile($"命令行参数中的.icstk文件不存在: {arg}", LogHelper.LogType.Warning);
}
}
}
return null;
}
/// <summary>
/// 尝试通过IPC将文件路径发送给已运行的实例
/// </summary>
/// <param name="filePath">要打开的文件路径</param>
/// <returns>是否成功发送</returns>
public static bool TrySendFileToExistingInstance(string filePath)
{
try
{
LogHelper.WriteLogToFile($"尝试通过IPC发送文件路径给已运行实例: {filePath}", LogHelper.LogType.Event);
// 创建IPC文件
string tempDir = Path.GetTempPath();
string ipcFileName = IpcFilePrefix + Guid.NewGuid().ToString("N") + ".tmp";
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
// 写入文件路径到IPC文件
File.WriteAllText(ipcFilePath, filePath, Encoding.UTF8);
// 创建事件通知已运行实例
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
{
ipcEvent.Set();
}
// 等待一段时间让已运行实例处理文件
Thread.Sleep(1000);
// 清理IPC文件
try
{
if (File.Exists(ipcFilePath))
{
File.Delete(ipcFilePath);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("IPC文件路径发送完成", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"通过IPC发送文件路径失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 尝试通过IPC将白板模式命令发送给已运行的实例
/// </summary>
/// <returns>是否成功发送</returns>
public static bool TrySendBoardModeCommandToExistingInstance()
{
try
{
LogHelper.WriteLogToFile("尝试通过IPC发送白板模式命令给已运行实例", LogHelper.LogType.Event);
// 创建IPC文件
string tempDir = Path.GetTempPath();
string ipcFileName = IpcBoardModePrefix + Guid.NewGuid().ToString("N") + ".tmp";
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
// 写入白板模式命令到IPC文件
File.WriteAllText(ipcFilePath, "BOARD_MODE", Encoding.UTF8);
// 创建事件通知已运行实例
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
{
ipcEvent.Set();
}
// 等待一段时间让已运行实例处理命令
Thread.Sleep(1000);
// 清理IPC文件
try
{
if (File.Exists(ipcFilePath))
{
File.Delete(ipcFilePath);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("IPC白板模式命令发送完成", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"通过IPC发送白板模式命令失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 尝试通过IPC将展开浮动栏命令发送给已运行的实例
/// </summary>
/// <returns>是否成功发送</returns>
public static bool TrySendShowModeCommandToExistingInstance()
{
try
{
LogHelper.WriteLogToFile("尝试通过IPC发送展开浮动栏命令给已运行实例", LogHelper.LogType.Event);
// 创建IPC文件
string tempDir = Path.GetTempPath();
string ipcFileName = IpcShowModePrefix + Guid.NewGuid().ToString("N") + ".tmp";
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
// 写入展开浮动栏命令到IPC文件
File.WriteAllText(ipcFilePath, "SHOW_MODE", Encoding.UTF8);
// 创建事件通知已运行实例
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
{
ipcEvent.Set();
}
// 等待一段时间让已运行实例处理命令
Thread.Sleep(1000);
// 清理IPC文件
try
{
if (File.Exists(ipcFilePath))
{
File.Delete(ipcFilePath);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("IPC展开浮动栏命令发送完成", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"通过IPC发送展开浮动栏命令失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 启动IPC监听器,等待其他实例发送文件路径
/// </summary>
public static void StartIpcListener()
{
try
{
Thread ipcThread = new Thread(() =>
{
try
{
LogHelper.WriteLogToFile("启动IPC监听器", LogHelper.LogType.Event);
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
{
while (true)
{
// 等待IPC事件
if (ipcEvent.WaitOne(IpcTimeout))
{
// 处理IPC文件
ProcessIpcFiles();
// 重置事件
ipcEvent.Reset();
}
// 检查应用是否还在运行
if (Application.Current == null || Application.Current.Dispatcher == null)
{
break;
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"IPC监听器出错: {ex.Message}", LogHelper.LogType.Error);
}
});
ipcThread.IsBackground = true;
ipcThread.Start();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动IPC监听器失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 处理IPC文件
/// </summary>
private static void ProcessIpcFiles()
{
try
{
string tempDir = Path.GetTempPath();
// 处理文件路径IPC文件
string[] ipcFiles = Directory.GetFiles(tempDir, IpcFilePrefix + "*.tmp");
foreach (string ipcFile in ipcFiles)
{
try
{
// 读取文件路径
string filePath = File.ReadAllText(ipcFile, Encoding.UTF8);
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
{
LogHelper.WriteLogToFile($"IPC接收到文件路径: {filePath}", LogHelper.LogType.Event);
// 在UI线程中处理文件打开
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
try
{
// 获取主窗口并打开文件
if (Application.Current.MainWindow is MainWindow mainWindow)
{
mainWindow.OpenSingleStrokeFile(filePath);
mainWindow.ShowNotification($"已加载墨迹文件: {Path.GetFileName(filePath)}");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"IPC处理文件打开失败: {ex.Message}", LogHelper.LogType.Error);
}
}));
}
// 删除IPC文件
File.Delete(ipcFile);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
// 尝试删除损坏的IPC文件
try
{
if (File.Exists(ipcFile))
{
File.Delete(ipcFile);
}
}
catch { }
}
}
// 处理白板模式命令IPC文件
string[] boardModeFiles = Directory.GetFiles(tempDir, IpcBoardModePrefix + "*.tmp");
foreach (string ipcFile in boardModeFiles)
{
try
{
// 读取命令内容
string command = File.ReadAllText(ipcFile, Encoding.UTF8);
if (command == "BOARD_MODE")
{
LogHelper.WriteLogToFile("IPC接收到白板模式命令", LogHelper.LogType.Event);
// 在UI线程中处理白板模式切换
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
try
{
// 获取主窗口并切换到白板模式
if (Application.Current.MainWindow is MainWindow mainWindow)
{
mainWindow.SwitchToBoardMode();
mainWindow.ShowNotification("已切换到白板模式");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"IPC处理白板模式切换失败: {ex.Message}", LogHelper.LogType.Error);
}
}));
}
// 删除IPC文件
File.Delete(ipcFile);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理白板模式IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
// 尝试删除损坏的IPC文件
try
{
if (File.Exists(ipcFile))
{
File.Delete(ipcFile);
}
}
catch { }
}
}
// 处理展开浮动栏命令IPC文件
string[] showModeFiles = Directory.GetFiles(tempDir, IpcShowModePrefix + "*.tmp");
foreach (string ipcFile in showModeFiles)
{
try
{
// 读取命令内容
string command = File.ReadAllText(ipcFile, Encoding.UTF8);
if (command == "SHOW_MODE")
{
LogHelper.WriteLogToFile("IPC接收到展开浮动栏命令", LogHelper.LogType.Event);
// 在UI线程中处理展开浮动栏
Application.Current.Dispatcher.BeginInvoke(new Action(async () =>
{
try
{
// 获取主窗口并展开浮动栏
if (Application.Current.MainWindow is MainWindow mainWindow)
{
// 如果当前处于收纳模式,则展开浮动栏
if (mainWindow.isFloatingBarFolded)
{
await mainWindow.UnFoldFloatingBar(new object());
}
mainWindow.ShowNotification("已退出收纳模式并恢复浮动栏");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"IPC处理展开浮动栏失败: {ex.Message}", LogHelper.LogType.Error);
}
}));
}
// 删除IPC文件
File.Delete(ipcFile);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理展开浮动栏IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
// 尝试删除损坏的IPC文件
try
{
if (File.Exists(ipcFile))
{
File.Delete(ipcFile);
}
}
catch { }
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理IPC文件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
[DllImport("shell32.dll")]
private static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,398 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ink_Canvas
{
/// <summary>
/// 悬浮窗拦截管理器
/// </summary>
public class FloatingWindowInterceptorManager : IDisposable
{
#region
private FloatingWindowInterceptor _interceptor;
private bool _isInitialized;
private bool _disposed;
private FloatingWindowInterceptorSettings _settings;
#endregion
#region
public event EventHandler<FloatingWindowInterceptor.WindowInterceptedEventArgs> WindowIntercepted;
public event EventHandler<FloatingWindowInterceptor.WindowRestoredEventArgs> WindowRestored;
#endregion
#region
public bool IsEnabled => _interceptor != null && _settings != null && _settings.IsEnabled;
public bool IsRunning => _interceptor != null && _interceptor.IsRunning;
#endregion
#region
/// <summary>
/// 初始化拦截器
/// </summary>
public void Initialize(FloatingWindowInterceptorSettings settings)
{
if (_isInitialized) return;
try
{
_settings = settings ?? new FloatingWindowInterceptorSettings();
_interceptor = new FloatingWindowInterceptor();
// 订阅事件
_interceptor.WindowIntercepted += OnWindowIntercepted;
_interceptor.WindowRestored += OnWindowRestored;
// 应用配置
ApplySettings();
_isInitialized = true;
// 如果设置了自动启动,则启动拦截器
if (_settings.AutoStart && _settings.IsEnabled)
{
Start();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 启动拦截器
/// </summary>
public void Start()
{
if (!_isInitialized || _settings == null) return;
if (_interceptor == null) return;
try
{
_interceptor.Start(_settings.ScanIntervalMs);
LogHelper.WriteLogToFile("悬浮窗拦截器已启动", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 停止拦截器
/// </summary>
public void Stop()
{
if (_interceptor == null) return;
try
{
_interceptor.Stop();
LogHelper.WriteLogToFile("悬浮窗拦截器已停止", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"停止悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 设置拦截规则
/// </summary>
public void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
{
if (_interceptor == null || _settings == null) return;
try
{
_interceptor.SetInterceptRule(type, enabled);
// 更新设置
var ruleName = type.ToString();
if (_settings.InterceptRules.ContainsKey(ruleName))
{
_settings.InterceptRules[ruleName] = enabled;
}
// 获取规则信息以处理父子关系
var rule = _interceptor.GetInterceptRule(type);
if (rule != null)
{
// 如果是父规则,更新所有子规则的设置
if (rule.ChildTypes.Count > 0)
{
foreach (var childType in rule.ChildTypes)
{
var childRuleName = childType.ToString();
if (_settings.InterceptRules.ContainsKey(childRuleName))
{
_settings.InterceptRules[childRuleName] = enabled;
}
}
}
// 如果是子规则,更新父规则的设置
else if (rule.ParentType.HasValue)
{
var parentRule = _interceptor.GetInterceptRule(rule.ParentType.Value);
if (parentRule != null)
{
var parentRuleName = rule.ParentType.Value.ToString();
if (_settings.InterceptRules.ContainsKey(parentRuleName))
{
_settings.InterceptRules[parentRuleName] = parentRule.IsEnabled;
}
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"设置拦截规则失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 获取拦截规则
/// </summary>
public FloatingWindowInterceptor.InterceptRule GetInterceptRule(FloatingWindowInterceptor.InterceptType type)
{
return _interceptor?.GetInterceptRule(type);
}
/// <summary>
/// 获取所有拦截规则
/// </summary>
public Dictionary<FloatingWindowInterceptor.InterceptType, FloatingWindowInterceptor.InterceptRule> GetAllRules()
{
return _interceptor?.GetAllRules() ?? new Dictionary<FloatingWindowInterceptor.InterceptType, FloatingWindowInterceptor.InterceptRule>();
}
/// <summary>
/// 手动扫描一次
/// </summary>
public void ScanOnce()
{
if (_interceptor == null) return;
try
{
_interceptor.ScanOnce();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"手动扫描失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 恢复所有被拦截的窗口
/// </summary>
public void RestoreAllWindows()
{
if (_interceptor == null) return;
try
{
_interceptor.RestoreAllWindows();
LogHelper.WriteLogToFile("已恢复所有被拦截的窗口", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复窗口失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 应用设置
/// </summary>
public void ApplySettings()
{
if (_interceptor == null || _settings == null) return;
try
{
// 应用拦截规则设置
foreach (var kvp in _settings.InterceptRules)
{
if (Enum.TryParse<FloatingWindowInterceptor.InterceptType>(kvp.Key, out var type))
{
_interceptor.SetInterceptRule(type, kvp.Value);
}
}
// 如果启用了拦截器,则启动
if (_settings.IsEnabled && !IsRunning)
{
Start();
}
// 如果禁用了拦截器,则停止
else if (!_settings.IsEnabled && IsRunning)
{
Stop();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用设置失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 更新扫描间隔
/// </summary>
public void UpdateScanInterval(int intervalMs)
{
if (_interceptor == null || _settings == null) return;
try
{
_settings.ScanIntervalMs = intervalMs;
// 如果正在运行,重启以应用新间隔
if (IsRunning)
{
Stop();
Start();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新扫描间隔失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 获取拦截统计信息
/// </summary>
public InterceptStatistics GetStatistics()
{
if (_interceptor == null || _settings == null) return new InterceptStatistics();
try
{
var rules = GetAllRules();
var enabledRules = rules.Count(r => r.Value.IsEnabled);
var totalRules = rules.Count;
return new InterceptStatistics
{
TotalRules = totalRules,
EnabledRules = enabledRules,
IsRunning = IsRunning,
ScanIntervalMs = _settings.ScanIntervalMs
};
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取统计信息失败: {ex.Message}", LogHelper.LogType.Error);
return new InterceptStatistics();
}
}
#endregion
#region
private void OnWindowIntercepted(object sender, FloatingWindowInterceptor.WindowInterceptedEventArgs e)
{
try
{
// 记录日志
LogHelper.WriteLogToFile($"拦截窗口: {e.WindowTitle} ({e.InterceptType})", LogHelper.LogType.Event);
// 显示通知(如果启用)
if (_settings != null && _settings.ShowNotifications)
{
ShowNotification($"已拦截悬浮窗: {e.Rule.Description}");
}
// 触发事件
WindowIntercepted?.Invoke(this, e);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理窗口拦截事件失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void OnWindowRestored(object sender, FloatingWindowInterceptor.WindowRestoredEventArgs e)
{
try
{
// 记录日志
LogHelper.WriteLogToFile($"恢复窗口: {e.InterceptType}", LogHelper.LogType.Event);
// 触发事件
WindowRestored?.Invoke(this, e);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理窗口恢复事件失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ShowNotification(string message)
{
try
{
// 这里可以集成系统通知或自定义通知
// 暂时使用调试输出
System.Diagnostics.Debug.WriteLine($"通知: {message}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"显示通知失败: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region
public class InterceptStatistics
{
public int TotalRules { get; set; }
public int EnabledRules { get; set; }
public bool IsRunning { get; set; }
public int ScanIntervalMs { get; set; }
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
try
{
Stop();
_interceptor?.Dispose();
_interceptor = null;
_isInitialized = false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
}
finally
{
_disposed = true;
}
}
#endregion
}
}
File diff suppressed because it is too large Load Diff
@@ -16,7 +16,6 @@ namespace Ink_Canvas.Helpers
{
private readonly RenderTargetBitmap _renderTarget;
private readonly DrawingVisual _drawingVisual;
private readonly DrawingContext _drawingContext;
private bool _isInitialized;
public HardwareAcceleratedInkProcessor(int width = 1920, int height = 1080)
@@ -191,67 +190,10 @@ namespace Ink_Canvas.Helpers
/// </summary>
public void Dispose()
{
_drawingContext?.Close();
_renderTarget?.Clear();
_isInitialized = false;
}
}
/// <summary>
/// 质量配置枚举
/// </summary>
public enum InkSmoothingQuality
{
HighPerformance = 0, // 高性能低质量
Balanced = 1, // 平衡
HighQuality = 2 // 高质量低性能
}
/// <summary>
/// 墨迹平滑配置
/// </summary>
public class InkSmoothingConfig
{
public InkSmoothingQuality Quality { get; set; } = InkSmoothingQuality.HighQuality;
public bool UseHardwareAcceleration { get; set; } = true;
public bool UseAsyncProcessing { get; set; } = true;
public int MaxConcurrentTasks { get; set; } = Environment.ProcessorCount;
public double SmoothingStrength { get; set; } = 0.8; // 高质量模式的平滑强度
public double ResampleInterval { get; set; } = 0.8; // 高质量模式的重采样间隔
public int InterpolationSteps { get; set; } = 64; // 高质量模式的插值步数
public static InkSmoothingConfig FromSettings()
{
return new InkSmoothingConfig
{
Quality = (InkSmoothingQuality)MainWindow.Settings.Canvas.InkSmoothingQuality,
UseHardwareAcceleration = MainWindow.Settings.Canvas.UseHardwareAcceleration,
UseAsyncProcessing = MainWindow.Settings.Canvas.UseAsyncInkSmoothing,
MaxConcurrentTasks = MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks > 0 ?
MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks : Environment.ProcessorCount
};
}
public void ApplyQualitySettings()
{
switch (Quality)
{
case InkSmoothingQuality.HighPerformance:
SmoothingStrength = 0.4;
ResampleInterval = 2.0;
InterpolationSteps = 16;
break;
case InkSmoothingQuality.Balanced:
SmoothingStrength = 0.6;
ResampleInterval = 1.2;
InterpolationSteps = 32;
break;
case InkSmoothingQuality.HighQuality:
SmoothingStrength = 0.8;
ResampleInterval = 0.8;
InterpolationSteps = 64;
break;
}
}
}
}
@@ -0,0 +1,325 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 改进的三次贝塞尔曲线平滑算法
/// </summary>
public class ImprovedBezierSmoothing
{
private readonly InkSmoothingConfig _config;
public ImprovedBezierSmoothing(InkSmoothingConfig config = null)
{
_config = config ?? new InkSmoothingConfig();
}
/// <summary>
/// 使用改进的贝塞尔曲线算法平滑笔画
/// </summary>
public Stroke SmoothStroke(Stroke originalStroke)
{
if (originalStroke == null || originalStroke.StylusPoints.Count < 3)
return originalStroke;
var originalPoints = originalStroke.StylusPoints.ToArray();
// 预处理:去除噪声点
var cleanedPoints = RemoveNoisePoints(originalPoints);
// 使用改进的贝塞尔曲线拟合
var smoothedPoints = ApplyCubicBezierSmoothing(cleanedPoints);
// 后处理:重采样和优化
var finalPoints = PostProcessPoints(smoothedPoints);
return new Stroke(new StylusPointCollection(finalPoints))
{
DrawingAttributes = originalStroke.DrawingAttributes.Clone()
};
}
/// <summary>
/// 去除噪声点
/// </summary>
private StylusPoint[] RemoveNoisePoints(StylusPoint[] points)
{
if (points.Length < 3) return points;
var result = new List<StylusPoint> { points[0] };
double minDistance = _config.ResampleInterval * 0.5;
for (int i = 1; i < points.Length - 1; i++)
{
var prev = result[result.Count - 1];
var curr = points[i];
var next = points[i + 1];
// 计算到前一个点的距离
double distToPrev = Math.Sqrt((curr.X - prev.X) * (curr.X - prev.X) +
(curr.Y - prev.Y) * (curr.Y - prev.Y));
// 如果距离太近,跳过这个点
if (distToPrev < minDistance)
continue;
// 检查是否为异常点(与前后点形成锐角)
if (IsOutlierPoint(prev, curr, next))
continue;
result.Add(curr);
}
result.Add(points[points.Length - 1]);
return result.ToArray();
}
/// <summary>
/// 检查是否为异常点
/// </summary>
private bool IsOutlierPoint(StylusPoint prev, StylusPoint curr, StylusPoint next)
{
var v1 = new Vector(curr.X - prev.X, curr.Y - prev.Y);
var v2 = new Vector(next.X - curr.X, next.Y - curr.Y);
if (v1.Length == 0 || v2.Length == 0) return false;
v1.Normalize();
v2.Normalize();
double dotProduct = Vector.Multiply(v1, v2);
double angle = Math.Acos(Math.Max(-1, Math.Min(1, dotProduct)));
// 如果角度小于30度,认为是异常点
return angle < Math.PI / 6;
}
/// <summary>
/// 应用三次贝塞尔曲线平滑
/// </summary>
private StylusPoint[] ApplyCubicBezierSmoothing(StylusPoint[] points)
{
if (points.Length < 4) return points;
var result = new List<StylusPoint>();
result.Add(points[0]);
// 使用滑动窗口进行贝塞尔曲线拟合
for (int i = 0; i <= points.Length - 4; i++)
{
var p0 = points[i];
var p1 = points[i + 1];
var p2 = points[i + 2];
var p3 = points[i + 3];
// 计算控制点
var controlPoints = CalculateOptimalControlPoints(p0, p1, p2, p3);
// 计算插值步数
int steps = CalculateInterpolationSteps(p0, p1, p2, p3);
// 生成贝塞尔曲线点
for (int j = 1; j <= steps; j++)
{
double t = (double)j / steps;
var bezierPoint = CalculateBezierPoint(p0, controlPoints.cp1, controlPoints.cp2, p3, t);
result.Add(bezierPoint);
}
}
result.Add(points[points.Length - 1]);
return result.ToArray();
}
/// <summary>
/// 计算最优控制点
/// </summary>
private (Point cp1, Point cp2) CalculateOptimalControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
// 计算切线方向
var tangent1 = CalculateTangent(p0, p1, p2);
var tangent2 = CalculateTangent(p1, p2, p3);
// 计算控制点距离
double dist1 = CalculateDistance(p0, p1);
double dist2 = CalculateDistance(p2, p3);
double controlDist1 = dist1 * _config.CurveTension;
double controlDist2 = dist2 * _config.CurveTension;
// 计算控制点
var cp1 = new Point(
p1.X + tangent1.X * controlDist1,
p1.Y + tangent1.Y * controlDist1
);
var cp2 = new Point(
p2.X - tangent2.X * controlDist2,
p2.Y - tangent2.Y * controlDist2
);
return (cp1, cp2);
}
/// <summary>
/// 计算切线方向
/// </summary>
private Vector CalculateTangent(StylusPoint p0, StylusPoint p1, StylusPoint p2)
{
var v1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
var v2 = new Vector(p2.X - p1.X, p2.Y - p1.Y);
// 如果向量长度为零,返回零向量
if (v1.Length == 0 || v2.Length == 0)
return new Vector(0, 0);
v1.Normalize();
v2.Normalize();
// 返回平均方向
var tangent = (v1 + v2) / 2;
if (tangent.Length > 0)
tangent.Normalize();
return tangent;
}
/// <summary>
/// 计算两点间距离
/// </summary>
private double CalculateDistance(StylusPoint p1, StylusPoint p2)
{
double dx = p2.X - p1.X;
double dy = p2.Y - p1.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
/// <summary>
/// 计算插值步数
/// </summary>
private int CalculateInterpolationSteps(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
if (!_config.UseAdaptiveInterpolation)
return _config.InterpolationSteps;
// 计算曲线长度
double totalLength = CalculateDistance(p0, p1) + CalculateDistance(p1, p2) + CalculateDistance(p2, p3);
// 计算曲率
double curvature = CalculateCurvature(p0, p1, p2, p3);
// 基于长度和曲率计算步数
int baseSteps = Math.Max(8, Math.Min(20, (int)(totalLength / 10)));
int curvatureSteps = (int)(curvature * 15);
return Math.Max(_config.InterpolationSteps, Math.Min(30, baseSteps + curvatureSteps));
}
/// <summary>
/// 计算曲率
/// </summary>
private double CalculateCurvature(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
var v1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
var v2 = new Vector(p2.X - p1.X, p2.Y - p1.Y);
var v3 = new Vector(p3.X - p2.X, p3.Y - p2.Y);
if (v1.Length == 0 || v2.Length == 0 || v3.Length == 0) return 0;
v1.Normalize();
v2.Normalize();
v3.Normalize();
// 计算角度变化
double angle1 = Math.Acos(Math.Max(-1, Math.Min(1, Vector.Multiply(v1, v2))));
double angle2 = Math.Acos(Math.Max(-1, Math.Min(1, Vector.Multiply(v2, v3))));
return (angle1 + angle2) / Math.PI; // 归一化到0-1
}
/// <summary>
/// 计算贝塞尔曲线上的点
/// </summary>
private StylusPoint CalculateBezierPoint(StylusPoint p0, Point cp1, Point cp2, StylusPoint p3, double t)
{
double u = 1 - t;
double tt = t * t;
double uu = u * u;
double uuu = uu * u;
double ttt = tt * t;
// 预计算系数
double c0 = uuu;
double c1 = 3 * uu * t;
double c2 = 3 * u * tt;
double c3 = ttt;
double x = c0 * p0.X + c1 * cp1.X + c2 * cp2.X + c3 * p3.X;
double y = c0 * p0.Y + c1 * cp1.Y + c2 * cp2.Y + c3 * p3.Y;
// 插值压力值
float pressure = (float)(p0.PressureFactor * u + p3.PressureFactor * t);
pressure = Math.Max(pressure, 0.1f);
return new StylusPoint(x, y, pressure);
}
/// <summary>
/// 后处理点集
/// </summary>
private StylusPoint[] PostProcessPoints(StylusPoint[] points)
{
if (points.Length == 0) return points;
// 如果点数过多,进行重采样
if (points.Length > _config.MaxPointsPerStroke)
{
return ResamplePoints(points, _config.ResampleInterval);
}
return points;
}
/// <summary>
/// 重采样点集
/// </summary>
private StylusPoint[] ResamplePoints(StylusPoint[] points, double interval)
{
var result = new List<StylusPoint> { points[0] };
double accumulated = 0;
for (int i = 1; i < points.Length; i++)
{
var prev = result[result.Count - 1];
var curr = points[i];
double dx = curr.X - prev.X;
double dy = curr.Y - prev.Y;
double dist = Math.Sqrt(dx * dx + dy * dy);
if (dist + accumulated >= interval)
{
double t = (interval - accumulated) / dist;
double x = prev.X + t * dx;
double y = prev.Y + t * dy;
float pressure = (float)(prev.PressureFactor * (1 - t) + curr.PressureFactor * t);
pressure = Math.Max(pressure, 0.1f);
result.Add(new StylusPoint(x, y, pressure));
accumulated = 0;
i--; // 重新处理当前点
}
else
{
accumulated += dist;
}
}
return result.ToArray();
}
}
}
+897
View File
@@ -0,0 +1,897 @@
using System;
using System.Collections.Generic;
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.Effects;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 墨迹渐隐管理器 - 管理墨迹的渐隐动画和状态
/// </summary>
public class InkFadeManager
{
#region Properties
/// <summary>
/// 是否启用墨迹渐隐功能
/// </summary>
public bool IsEnabled { get; set; }
/// <summary>
/// 墨迹渐隐时间(毫秒)
/// </summary>
public int FadeTime { get; set; } = 3000;
/// <summary>
/// 渐隐动画持续时间(毫秒)
/// </summary>
public int AnimationDuration { get; set; } = 1000;
#endregion
#region Private Fields
private readonly MainWindow _mainWindow;
private readonly Dispatcher _dispatcher;
private readonly Dictionary<Stroke, DispatcherTimer> _fadeTimers;
private readonly Dictionary<Stroke, UIElement> _strokeVisuals;
private readonly Dictionary<Stroke, Point> _strokeStartPoints;
private readonly Dictionary<Stroke, Point> _strokeEndPoints;
#endregion
#region Constructor
public InkFadeManager(MainWindow mainWindow)
{
_mainWindow = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow));
_dispatcher = _mainWindow.Dispatcher;
_fadeTimers = new Dictionary<Stroke, DispatcherTimer>();
_strokeVisuals = new Dictionary<Stroke, UIElement>();
_strokeStartPoints = new Dictionary<Stroke, Point>();
_strokeEndPoints = new Dictionary<Stroke, Point>();
}
#endregion
#region Public Methods
/// <summary>
/// 添加需要渐隐的墨迹
/// </summary>
/// <param name="stroke">墨迹对象</param>
/// <param name="startPoint">落笔点</param>
/// <param name="endPoint">抬笔点</param>
public void AddFadingStroke(Stroke stroke, Point startPoint, Point endPoint)
{
if (!IsEnabled || stroke == null)
{
return;
}
try
{
// 确保主窗口的InkCanvas保持Ink编辑模式,防止墨迹渐隐时切换到鼠标模式
if (_mainWindow.inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
{
_mainWindow.inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
// 记录墨迹的起点和终点
_strokeStartPoints[stroke] = startPoint;
_strokeEndPoints[stroke] = endPoint;
// 创建墨迹的视觉元素(湿墨迹状态)
var strokeVisual = CreateStrokeVisual(stroke);
if (strokeVisual == null) return;
_strokeVisuals[stroke] = strokeVisual;
// 创建定时器,在指定时间后开始渐隐动画
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(FadeTime)
};
timer.Tick += (sender, e) =>
{
StartFadeAnimation(stroke);
timer.Stop();
_fadeTimers.Remove(stroke);
};
_fadeTimers[stroke] = timer;
timer.Start();
// 将视觉元素添加到画布上
_dispatcher.InvokeAsync(() =>
{
try
{
if (_mainWindow.inkCanvas != null)
{
// 将墨迹添加到 inkCanvas 的父容器中,而不是 inkCanvas.Children
// 这样可以避免坐标系统问题
var parent = _mainWindow.inkCanvas.Parent as Panel;
if (parent != null)
{
parent.Children.Add(strokeVisual);
}
else
{
// 如果无法获取父容器,则添加到 inkCanvas.Children
_mainWindow.inkCanvas.Children.Add(strokeVisual);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"添加墨迹视觉元素到画布失败: {ex}", LogHelper.LogType.Error);
}
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"添加渐隐墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 移除墨迹
/// </summary>
/// <param name="stroke">要移除的墨迹</param>
public void RemoveStroke(Stroke stroke)
{
if (stroke == null) return;
try
{
if (_fadeTimers.TryGetValue(stroke, out var timer))
{
timer.Stop();
_fadeTimers.Remove(stroke);
}
if (_strokeVisuals.TryGetValue(stroke, out var visual))
{
_dispatcher.InvokeAsync(() =>
{
try
{
// 从父容器中移除墨迹
var parent = _mainWindow.inkCanvas?.Parent as Panel;
if (parent != null && parent.Children.Contains(visual))
{
parent.Children.Remove(visual);
}
else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(visual))
{
_mainWindow.inkCanvas.Children.Remove(visual);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从画布移除墨迹视觉元素失败: {ex}", LogHelper.LogType.Error);
}
});
_strokeVisuals.Remove(stroke);
}
_strokeStartPoints.Remove(stroke);
_strokeEndPoints.Remove(stroke);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"移除渐隐墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 清除所有渐隐墨迹
/// </summary>
public void ClearAllFadingStrokes()
{
try
{
foreach (var timer in _fadeTimers.Values)
{
timer.Stop();
}
_fadeTimers.Clear();
_dispatcher.InvokeAsync(() =>
{
try
{
if (_mainWindow.inkCanvas != null)
{
var parent = _mainWindow.inkCanvas.Parent as Panel;
foreach (var visual in _strokeVisuals.Values)
{
if (parent != null && parent.Children.Contains(visual))
{
parent.Children.Remove(visual);
}
else if (_mainWindow.inkCanvas.Children.Contains(visual))
{
_mainWindow.inkCanvas.Children.Remove(visual);
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清除所有墨迹视觉元素失败: {ex}", LogHelper.LogType.Error);
}
});
_strokeVisuals.Clear();
_strokeStartPoints.Clear();
_strokeEndPoints.Clear();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清除所有渐隐墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 更新渐隐时间设置
/// </summary>
/// <param name="fadeTime">新的渐隐时间(毫秒)</param>
public void UpdateFadeTime(int fadeTime)
{
FadeTime = fadeTime;
foreach (var kvp in _fadeTimers)
{
var stroke = kvp.Key;
var timer = kvp.Value;
timer.Stop();
timer.Interval = TimeSpan.FromMilliseconds(FadeTime);
timer.Start();
}
}
/// <summary>
/// 启用墨迹渐隐功能
/// </summary>
public void Enable()
{
IsEnabled = true;
LogHelper.WriteLogToFile("墨迹渐隐功能已启用");
}
/// <summary>
/// 禁用墨迹渐隐功能
/// </summary>
public void Disable()
{
IsEnabled = false;
LogHelper.WriteLogToFile("墨迹渐隐功能已禁用");
}
#endregion
#region Private Methods
/// <summary>
/// 创建墨迹的视觉元素
/// </summary>
/// <param name="stroke">墨迹对象</param>
/// <returns>视觉元素</returns>
private UIElement CreateStrokeVisual(Stroke stroke)
{
try
{
// 创建路径几何,使用墨迹的实际位置
var geometry = stroke.GetGeometry();
if (geometry == null)
{
return null;
}
// 获取绘画属性
var drawingAttribs = stroke.DrawingAttributes;
// 创建路径元素,确保使用正确的绘画属性
var path = new Path
{
Data = geometry,
Stroke = new SolidColorBrush(drawingAttribs.Color),
StrokeThickness = drawingAttribs.Width, // 使用原始墨迹的粗细
StrokeStartLineCap = PenLineCap.Round,
StrokeEndLineCap = PenLineCap.Round,
StrokeLineJoin = PenLineJoin.Round,
Fill = drawingAttribs.IsHighlighter ? new SolidColorBrush(drawingAttribs.Color) : null, // 高亮笔需要填充
Opacity = 0.95, // 初始透明度更高,显得更自然
// 优化渲染质量
UseLayoutRounding = false,
SnapsToDevicePixels = false
};
// 如果是高亮笔,调整透明度和混合模式
if (drawingAttribs.IsHighlighter)
{
path.Opacity = 0.4; // 高亮笔初始透明度更低,更符合荧光笔特性
// 为高亮笔添加特殊的混合效果
// 使用更柔和的笔触样式
path.StrokeStartLineCap = PenLineCap.Flat;
path.StrokeEndLineCap = PenLineCap.Flat;
path.StrokeLineJoin = PenLineJoin.Miter;
// 高亮笔通常需要更宽的笔触来覆盖下面的内容
if (drawingAttribs.Width < 20)
{
path.StrokeThickness = Math.Max(drawingAttribs.Width * 1.5, 20);
}
// 为高亮笔添加轻微的模糊效果,使渐隐更加自然
path.Effect = new BlurEffect
{
Radius = 0.5, // 轻微的模糊效果
KernelType = KernelType.Gaussian
};
}
// 不设置任何变换,保持墨迹原有粗细
var bounds = geometry.Bounds;
// 设置墨迹的初始位置
System.Windows.Controls.Canvas.SetLeft(path, bounds.Left);
System.Windows.Controls.Canvas.SetTop(path, bounds.Top);
return path;
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// 开始渐隐动画
/// </summary>
/// <param name="stroke">要渐隐的墨迹</param>
private void StartFadeAnimation(Stroke stroke)
{
if (!_strokeVisuals.TryGetValue(stroke, out var visual)) return;
try
{
_dispatcher.InvokeAsync(() =>
{
// 获取当前透明度和判断是否为高亮笔
var currentOpacity = visual.Opacity;
var isHighlighter = stroke.DrawingAttributes.IsHighlighter;
// 根据墨迹类型选择不同的动画效果
if (isHighlighter)
{
StartHighlighterFadeAnimation(visual, stroke, currentOpacity);
}
else
{
StartNormalStrokeFadeAnimation(visual, stroke, currentOpacity);
}
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"开始渐隐动画失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 开始普通墨迹的渐隐动画
/// </summary>
private void StartNormalStrokeFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity)
{
try
{
StartProgressiveFadeAnimation(visual, stroke, currentOpacity, AnimationDuration);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"开始普通墨迹渐隐动画失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 统一渐隐动画 - 整个墨迹作为一个整体进行渐隐,与擦除效果一致
/// </summary>
private void StartUnifiedFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration)
{
try
{
// 创建透明度动画,模拟擦除时的效果
var fadeAnimation = new DoubleAnimation
{
From = currentOpacity,
To = 0.0,
Duration = TimeSpan.FromMilliseconds(duration),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut }
};
// 如果是高亮笔,添加轻微的缩放效果,使渐隐更加自然
if (stroke.DrawingAttributes.IsHighlighter)
{
// 创建轻微的缩放动画,模拟墨迹"蒸发"的效果
var scaleAnimation = new DoubleAnimation
{
From = 1.0,
To = 0.95, // 轻微缩小,增加自然感
Duration = TimeSpan.FromMilliseconds(duration),
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn }
};
// 创建缩放变换
var scaleTransform = new ScaleTransform();
visual.RenderTransform = scaleTransform;
visual.RenderTransformOrigin = new Point(0.5, 0.5);
// 应用缩放动画
scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation);
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation);
}
// 添加动画完成事件
fadeAnimation.Completed += (sender, e) => OnAnimationCompleted(visual, stroke);
// 应用透明度动画
visual.BeginAnimation(UIElement.OpacityProperty, fadeAnimation);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"统一渐隐动画失败: {ex}", LogHelper.LogType.Error);
OnAnimationCompleted(visual, stroke);
}
}
/// <summary>
/// 开始高亮笔的渐隐动画
/// </summary>
private void StartHighlighterFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity)
{
try
{
// 高亮笔使用统一的渐隐动画,与擦除效果一致
StartUnifiedFadeAnimation(visual, stroke, currentOpacity, (int)(AnimationDuration * 1.2));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"开始高亮笔渐隐动画失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 渐进式渐隐动画 - 从起点到终点逐渐消失
/// </summary>
private void StartProgressiveFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration)
{
try
{
// 确保所有墨迹都能显示动画,包括短墨迹
if (stroke.StylusPoints.Count < 2)
{
// 只有1个点的墨迹也使用分段动画,确保视觉效果
CreateSegmentedStroke(visual, stroke, currentOpacity, duration);
return;
}
// 将墨迹分段并创建多个 Path
CreateSegmentedStroke(visual, stroke, currentOpacity, duration);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"渐进式渐隐动画失败: {ex}", LogHelper.LogType.Error);
// 失败时回退到简单动画
StartSimpleFadeAnimation(visual, stroke, currentOpacity, duration);
}
}
/// <summary>
/// 创建分段墨迹并开始渐进消失
/// </summary>
private void CreateSegmentedStroke(UIElement originalVisual, Stroke stroke, double opacity, int duration)
{
try
{
var stylusPoints = stroke.StylusPoints;
var totalPoints = stylusPoints.Count;
// 分段算法 - 确保所有墨迹都有足够的动画效果
var strokeLength = CalculateStrokeLength(stylusPoints);
var segmentCount = CalculateOptimalSegmentCount(totalPoints, strokeLength);
// 强制最小分段数量,确保短墨迹也有动画效果
segmentCount = Math.Max(segmentCount, 4);
var pointsPerSegment = Math.Max(1, totalPoints / segmentCount);
// 隐藏原始视觉元素
originalVisual.Visibility = Visibility.Hidden;
var segments = new List<UIElement>();
var parent = _mainWindow.inkCanvas?.Parent as Panel;
if (parent == null)
{
// 如果父容器不是Panel,直接使用InkCanvas
parent = null; // 稍后会检查并使用InkCanvas.Children
}
// 创建各个分段 - 确保短墨迹也能正确分段
for (int i = 0; i < segmentCount; i++)
{
var startIndex = i * pointsPerSegment;
var endIndex = (i == segmentCount - 1) ? totalPoints - 1 : (i + 1) * pointsPerSegment;
// 确保有足够的点来创建分段,对于短墨迹特殊处理
if (endIndex <= startIndex && totalPoints > 1)
{
// 短墨迹:每个点作为一个分段
startIndex = i;
endIndex = Math.Min(i + 1, totalPoints - 1);
}
// 为每个分段添加重叠,确保连接处平滑
var overlap = Math.Max(1, pointsPerSegment / 6); // 15%的重叠,平衡平滑与速度
var actualStartIndex = Math.Max(0, startIndex - overlap);
var actualEndIndex = Math.Min(totalPoints - 1, endIndex + overlap);
var segment = CreateStrokeSegment(stroke, actualStartIndex, actualEndIndex, opacity);
if (segment != null)
{
segments.Add(segment);
if (parent != null)
{
parent.Children.Add(segment);
}
else if (_mainWindow.inkCanvas != null)
{
_mainWindow.inkCanvas.Children.Add(segment);
}
}
}
// 开始分段渐隐动画
StartSegmentedFadeAnimation(segments, stroke, originalVisual, duration);
}
catch (Exception)
{
StartSimpleFadeAnimation(originalVisual, stroke, opacity, duration);
}
}
/// <summary>
/// 创建墨迹分段
/// </summary>
private UIElement CreateStrokeSegment(Stroke originalStroke, int startIndex, int endIndex, double opacity)
{
try
{
// 创建分段的 StylusPoint 集合
var segmentPoints = new StylusPointCollection();
for (int i = startIndex; i <= endIndex && i < originalStroke.StylusPoints.Count; i++)
{
segmentPoints.Add(originalStroke.StylusPoints[i]);
}
if (segmentPoints.Count < 2) return null;
// 创建分段墨迹
var segmentStroke = new Stroke(segmentPoints)
{
DrawingAttributes = originalStroke.DrawingAttributes.Clone()
};
// 创建分段的视觉元素
var geometry = segmentStroke.GetGeometry();
if (geometry == null) return null;
var drawingAttribs = segmentStroke.DrawingAttributes;
var path = new Path
{
Data = geometry,
Stroke = new SolidColorBrush(drawingAttribs.Color),
StrokeThickness = drawingAttribs.Width,
StrokeStartLineCap = drawingAttribs.IsHighlighter ? PenLineCap.Flat : PenLineCap.Round,
StrokeEndLineCap = drawingAttribs.IsHighlighter ? PenLineCap.Flat : PenLineCap.Round,
StrokeLineJoin = drawingAttribs.IsHighlighter ? PenLineJoin.Miter : PenLineJoin.Round,
Fill = drawingAttribs.IsHighlighter ? new SolidColorBrush(drawingAttribs.Color) : null,
Opacity = opacity,
UseLayoutRounding = false,
SnapsToDevicePixels = false
};
// 设置位置
var bounds = geometry.Bounds;
System.Windows.Controls.Canvas.SetLeft(path, bounds.Left);
System.Windows.Controls.Canvas.SetTop(path, bounds.Top);
return path;
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// 开始分段渐隐动画
/// </summary>
private void StartSegmentedFadeAnimation(List<UIElement> segments, Stroke originalStroke, UIElement originalVisual, int totalDuration)
{
try
{
// 动画时序算法
var segmentDuration = CalculateOptimalSegmentDuration(totalDuration, segments.Count);
var animationCurve = CreateAppleStyleAnimationCurve(segments.Count, totalDuration);
// 跟踪动画完成状态
var completedSegments = new HashSet<UIElement>();
var totalSegments = segments.Count;
// 渐隐效果 - 使用自然的动画曲线
for (int i = 0; i < segments.Count; i++)
{
var segment = segments[i];
// 使用预计算的动画曲线获取延迟时间
var delay = animationCurve[i];
// 使用定时器延迟启动每个分段的动画
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(delay)
};
int segmentIndex = i; // 捕获当前索引
timer.Tick += (sender, e) =>
{
StartSingleSegmentFadeAnimation(segment, segmentDuration, () =>
{
// 动画完成回调
lock (completedSegments)
{
completedSegments.Add(segment);
// 检查是否所有分段都完成了
if (completedSegments.Count >= totalSegments)
{
CleanupSegmentedAnimation(segments, originalStroke, originalVisual);
}
}
});
timer.Stop();
};
timer.Start();
}
// 设置一个安全超时定时器,防止无限等待
var safetyTimeout = totalDuration + (segments.Count * segmentDuration) + 1200; // 额外1.2秒缓冲,确保动画完整
var safetyTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(safetyTimeout)
};
safetyTimer.Tick += (sender, e) =>
{
CleanupSegmentedAnimation(segments, originalStroke, originalVisual);
safetyTimer.Stop();
};
safetyTimer.Start();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"分段渐隐动画失败: {ex}", LogHelper.LogType.Error);
CleanupSegmentedAnimation(segments, originalStroke, originalVisual);
}
}
/// <summary>
/// 单个分段的渐隐动画
/// </summary>
private void StartSingleSegmentFadeAnimation(UIElement segment, int duration, Action onCompleted = null)
{
try
{
// 只使用透明度动画,保持墨迹原有粗细
var fadeAnimation = new DoubleAnimation
{
From = segment.Opacity,
To = 0.0,
Duration = TimeSpan.FromMilliseconds(duration),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut } // 更平滑的缓动
};
// 添加动画完成事件
if (onCompleted != null)
{
fadeAnimation.Completed += (sender, e) =>
{
onCompleted?.Invoke();
};
}
// 只应用透明度动画,不改变墨迹大小
segment.BeginAnimation(UIElement.OpacityProperty, fadeAnimation);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"单个分段渐隐动画失败: {ex}", LogHelper.LogType.Error);
// 即使失败也要调用完成回调
onCompleted?.Invoke();
}
}
/// <summary>
/// 清理分段动画
/// </summary>
private void CleanupSegmentedAnimation(List<UIElement> segments, Stroke originalStroke, UIElement originalVisual)
{
try
{
// 移除所有分段
var parent = _mainWindow.inkCanvas?.Parent as Panel;
foreach (var segment in segments)
{
if (parent != null && parent.Children.Contains(segment))
{
parent.Children.Remove(segment);
}
else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(segment))
{
_mainWindow.inkCanvas.Children.Remove(segment);
}
}
// 清理原始墨迹
OnAnimationCompleted(originalVisual, originalStroke);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理分段动画失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 简单渐隐动画(备用方案)
/// </summary>
private void StartSimpleFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration)
{
try
{
var fadeAnimation = new DoubleAnimation
{
From = currentOpacity,
To = 0.0,
Duration = TimeSpan.FromMilliseconds(duration),
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn }
};
fadeAnimation.Completed += (sender, e) => OnAnimationCompleted(visual, stroke);
visual.BeginAnimation(UIElement.OpacityProperty, fadeAnimation);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"简单渐隐动画失败: {ex}", LogHelper.LogType.Error);
OnAnimationCompleted(visual, stroke);
}
}
/// <summary>
/// 计算墨迹的实际长度
/// </summary>
private double CalculateStrokeLength(StylusPointCollection points)
{
if (points.Count < 2) return 0;
double totalLength = 0;
for (int i = 1; i < points.Count; i++)
{
var p1 = points[i - 1].ToPoint();
var p2 = points[i].ToPoint();
totalLength += Math.Sqrt(Math.Pow(p2.X - p1.X, 2) + Math.Pow(p2.Y - p1.Y, 2));
}
return totalLength;
}
/// <summary>
/// 根据墨迹特性计算最优分段数量 - 平衡速度与完整性
/// </summary>
private int CalculateOptimalSegmentCount(int pointCount, double strokeLength)
{
// 平衡速度与完整性,确保动画效果的同时提高速度
const double PIXELS_PER_SEGMENT = 12.0; // 每段适中长度,平衡效果与速度
const int MIN_SEGMENTS = 5; // 适当的最小分段数,确保动画效果
const int MAX_SEGMENTS = 100; // 适中的最大分段数,平衡性能与效果
// 根据长度计算基础分段数
var lengthBasedSegments = Math.Max(MIN_SEGMENTS, (int)(strokeLength / PIXELS_PER_SEGMENT));
// 根据点密度调整,平衡效果与速度
var density = pointCount > 0 ? strokeLength / pointCount : 1;
var densityFactor = Math.Max(0.4, Math.Min(2.5, density / 1.8));
var finalSegments = (int)(lengthBasedSegments * densityFactor);
// 对于短墨迹,确保至少有4个分段
if (pointCount <= 5)
{
finalSegments = Math.Max(finalSegments, 4);
}
// 限制在合理范围内
return Math.Min(MAX_SEGMENTS, Math.Max(MIN_SEGMENTS, finalSegments));
}
/// <summary>
/// 计算最优的单段动画持续时间 - 平衡速度与完整性
/// </summary>
private int CalculateOptimalSegmentDuration(int totalDuration, int segmentCount)
{
// 平衡速度与动画完整性
var baseDuration = totalDuration / Math.Max(segmentCount, 1);
var minDuration = 150; // 每段最少150ms,确保动画完整显示
var maxDuration = 500; // 每段最多500ms,平衡速度与完整性
return Math.Max(minDuration, Math.Min(maxDuration, baseDuration));
}
/// <summary>
/// 创建优化的动画时间曲线 - 平衡速度与完整性
/// </summary>
private int[] CreateAppleStyleAnimationCurve(int segmentCount, int totalDuration)
{
var curve = new int[segmentCount];
// 平衡速度与完整性,确保动画有足够时间播放
var availableTime = totalDuration * 0.6; // 使用60%的总时间,给动画留足够缓冲
var delayBetweenSegments = Math.Max(60, availableTime / Math.Max(segmentCount, 1));
for (int i = 0; i < segmentCount; i++)
{
// 线性延迟,确保每个分段都有足够时间
curve[i] = (int)(i * delayBetweenSegments);
}
return curve;
}
/// <summary>
/// 动画完成后的统一处理
/// </summary>
private void OnAnimationCompleted(UIElement visual, Stroke stroke)
{
try
{
// 从父容器中移除墨迹
var parent = _mainWindow.inkCanvas?.Parent as Panel;
if (parent != null && parent.Children.Contains(visual))
{
parent.Children.Remove(visual);
}
else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(visual))
{
_mainWindow.inkCanvas.Children.Remove(visual);
}
RemoveStroke(stroke);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"渐隐动画完成后清理墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
#endregion
}
}
+164
View File
@@ -0,0 +1,164 @@
using System;
using System.Diagnostics;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 墨迹平滑配置类
/// </summary>
public class InkSmoothingConfig
{
// 基本平滑参数
public double SmoothingStrength { get; set; } = 0.4;
public double ResampleInterval { get; set; } = 2.5;
public int InterpolationSteps { get; set; } = 12;
// 贝塞尔曲线参数
public bool UseAdaptiveInterpolation { get; set; } = true;
public double CurveTension { get; set; } = 0.3;
public double MinCurvatureThreshold { get; set; } = 0.1;
public double MaxCurvatureThreshold { get; set; } = 0.8;
// 性能参数
public bool UseHardwareAcceleration { get; set; } = true;
public bool UseAsyncProcessing { get; set; } = true;
public int MaxConcurrentTasks { get; set; } = Environment.ProcessorCount;
public int MaxPointsPerStroke { get; set; } = 10000;
// 质量设置
public SmoothingQuality Quality { get; set; } = SmoothingQuality.Balanced;
public enum SmoothingQuality
{
Performance, // 性能优先
Balanced, // 平衡
Quality // 质量优先
}
// 兼容性枚举
public enum InkSmoothingQuality
{
HighPerformance = 0, // 高性能低质量
Balanced = 1, // 平衡
HighQuality = 2 // 高质量低性能
}
/// <summary>
/// 从设置中加载配置
/// </summary>
public static InkSmoothingConfig FromSettings()
{
var config = new InkSmoothingConfig();
try
{
// 尝试从MainWindow.Settings加载配置(兼容性)
if (MainWindow.Settings?.Canvas != null)
{
config.Quality = (SmoothingQuality)MainWindow.Settings.Canvas.InkSmoothingQuality;
config.UseHardwareAcceleration = MainWindow.Settings.Canvas.UseHardwareAcceleration;
config.UseAsyncProcessing = MainWindow.Settings.Canvas.UseAsyncInkSmoothing;
config.MaxConcurrentTasks = MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks > 0 ?
MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks : Environment.ProcessorCount;
}
}
catch (Exception ex)
{
Debug.WriteLine($"加载平滑配置失败: {ex.Message}");
}
return config;
}
/// <summary>
/// 应用质量设置
/// </summary>
public void ApplyQualitySettings()
{
// 保存用户设置的异步处理偏好
bool userAsyncPreference = UseAsyncProcessing;
switch (Quality)
{
case SmoothingQuality.Performance:
SmoothingStrength = 0.15;
ResampleInterval = 5.0;
InterpolationSteps = 4;
UseAdaptiveInterpolation = false;
CurveTension = 0.15;
MaxConcurrentTasks = Math.Max(1, Environment.ProcessorCount / 2);
UseHardwareAcceleration = true;
UseAsyncProcessing = userAsyncPreference;
break;
case SmoothingQuality.Balanced:
SmoothingStrength = 0.3;
ResampleInterval = 3.0;
InterpolationSteps = 8;
UseAdaptiveInterpolation = true;
CurveTension = 0.25;
MaxConcurrentTasks = Environment.ProcessorCount;
UseHardwareAcceleration = true;
UseAsyncProcessing = userAsyncPreference;
break;
case SmoothingQuality.Quality:
SmoothingStrength = 0.5;
ResampleInterval = 2.0;
InterpolationSteps = 15;
UseAdaptiveInterpolation = true;
CurveTension = 0.35;
MaxConcurrentTasks = Environment.ProcessorCount;
UseHardwareAcceleration = true;
UseAsyncProcessing = userAsyncPreference;
break;
}
}
/// <summary>
/// 保存配置到设置
/// </summary>
public void SaveToSettings()
{
try
{
// 尝试保存到MainWindow.Settings(兼容性)
if (MainWindow.Settings?.Canvas != null)
{
MainWindow.Settings.Canvas.InkSmoothingQuality = (int)Quality;
MainWindow.Settings.Canvas.UseHardwareAcceleration = UseHardwareAcceleration;
MainWindow.Settings.Canvas.UseAsyncInkSmoothing = UseAsyncProcessing;
MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks = MaxConcurrentTasks;
}
}
catch (Exception ex)
{
Debug.WriteLine($"保存平滑配置失败: {ex.Message}");
}
}
/// <summary>
/// 验证配置参数
/// </summary>
public bool Validate()
{
return SmoothingStrength >= 0.0 && SmoothingStrength <= 1.0 &&
ResampleInterval > 0.0 &&
InterpolationSteps > 0 && InterpolationSteps <= 50 &&
CurveTension >= 0.0 && CurveTension <= 1.0 &&
MaxConcurrentTasks > 0 &&
MaxPointsPerStroke > 0;
}
/// <summary>
/// 获取配置摘要
/// </summary>
public string GetSummary()
{
return $"质量: {Quality}, 强度: {SmoothingStrength:F2}, 间隔: {ResampleInterval:F1}, " +
$"步数: {InterpolationSteps}, 自适应: {UseAdaptiveInterpolation}, " +
$"张力: {CurveTension:F2}, 硬件加速: {UseHardwareAcceleration}";
}
}
}
+3 -3
View File
@@ -197,7 +197,7 @@ namespace Ink_Canvas.Helpers
if (processorCount >= 4 && isHardwareAccelerated)
{
// 降低高质量模式的门槛,4核以上且支持硬件加速就使用高质量
config.Quality = InkSmoothingQuality.HighQuality;
config.Quality = (InkSmoothingConfig.SmoothingQuality)InkSmoothingConfig.InkSmoothingQuality.HighQuality;
config.UseHardwareAcceleration = true;
config.UseAsyncProcessing = true;
config.MaxConcurrentTasks = Math.Min(processorCount, 8);
@@ -205,7 +205,7 @@ namespace Ink_Canvas.Helpers
else if (processorCount >= 2)
{
// 2核以上使用平衡模式
config.Quality = InkSmoothingQuality.Balanced;
config.Quality = (InkSmoothingConfig.SmoothingQuality)InkSmoothingConfig.InkSmoothingQuality.Balanced;
config.UseHardwareAcceleration = isHardwareAccelerated;
config.UseAsyncProcessing = true;
config.MaxConcurrentTasks = Math.Min(processorCount, 4);
@@ -213,7 +213,7 @@ namespace Ink_Canvas.Helpers
else
{
// 单核或性能较低的设备使用高性能模式
config.Quality = InkSmoothingQuality.HighPerformance;
config.Quality = (InkSmoothingConfig.SmoothingQuality)InkSmoothingConfig.InkSmoothingQuality.HighPerformance;
config.UseHardwareAcceleration = false;
config.UseAsyncProcessing = false;
config.MaxConcurrentTasks = 1;
+134 -15
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
@@ -8,27 +9,53 @@ namespace Ink_Canvas.Helpers
{
public class VisualCanvas : FrameworkElement
{
private readonly List<DrawingVisual> _visuals = new List<DrawingVisual>();
protected override Visual GetVisualChild(int index)
{
return Visual;
if (index < 0 || index >= _visuals.Count)
throw new ArgumentOutOfRangeException(nameof(index));
return _visuals[index];
}
protected override int VisualChildrenCount => 1;
protected override int VisualChildrenCount => _visuals.Count;
public VisualCanvas(DrawingVisual visual)
public VisualCanvas()
{
Visual = visual;
CacheMode = new BitmapCache();
RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.LowQuality);
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
RenderOptions.SetCachingHint(this, CachingHint.Cache);
}
public void AddVisual(DrawingVisual visual)
{
if (visual == null) return;
_visuals.Add(visual);
AddVisualChild(visual);
}
public void Clear()
{
foreach (var visual in _visuals)
{
RemoveVisualChild(visual);
}
_visuals.Clear();
}
public DrawingVisual Visual { get; }
public IReadOnlyList<DrawingVisual> Visuals => _visuals;
}
/// <summary>
/// 用于显示笔迹的类
/// 用于显示笔迹的类
/// </summary>
public class StrokeVisual : DrawingVisual
public class StrokeVisual
{
private int _lastDrawnPointCount = 0;
private const int INCREMENTAL_DRAW_THRESHOLD = 2;
private VisualCanvas _visualCanvas;
/// <summary>
/// 创建显示笔迹的类
/// </summary>
@@ -43,7 +70,7 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// 创建显示笔迹的类
/// 创建显示笔迹的类
/// </summary>
/// <param name="drawingAttributes"></param>
public StrokeVisual(DrawingAttributes drawingAttributes)
@@ -52,12 +79,20 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// 设置或获取显示的笔迹
/// 设置或获取显示的笔迹
/// </summary>
public Stroke Stroke { set; get; }
/// <summary>
/// 在笔迹中添加点
/// 设置关联的VisualCanvas
/// </summary>
public void SetVisualCanvas(VisualCanvas visualCanvas)
{
_visualCanvas = visualCanvas;
}
/// <summary>
/// 在笔迹中添加点
/// </summary>
/// <param name="point"></param>
public void Add(StylusPoint point)
@@ -74,18 +109,102 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// 重新画出笔迹
/// 绘制点段到新的DrawingVisual
/// </summary>
private void DrawSegmentToNewVisual(int startIndex, int endIndex)
{
if (Stroke == null || Stroke.StylusPoints.Count == 0 || _visualCanvas == null) return;
if (startIndex >= endIndex || startIndex < 0 || endIndex > Stroke.StylusPoints.Count) return;
var points = Stroke.StylusPoints;
var drawingAttributes = Stroke.DrawingAttributes;
// 创建新的DrawingVisual用于绘制这个点段
var segmentVisual = new DrawingVisual();
RenderOptions.SetBitmapScalingMode(segmentVisual, BitmapScalingMode.LowQuality);
RenderOptions.SetEdgeMode(segmentVisual, EdgeMode.Aliased);
RenderOptions.SetCachingHint(segmentVisual, CachingHint.Cache);
using (var dc = segmentVisual.RenderOpen())
{
var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), drawingAttributes.Width);
pen.StartLineCap = PenLineCap.Round;
pen.EndLineCap = PenLineCap.Round;
pen.LineJoin = PenLineJoin.Round;
// 绘制指定范围内的点段
if (endIndex - startIndex >= 2)
{
// 多个点,绘制线段
for (int i = startIndex; i < endIndex - 1 && i < points.Count - 1; i++)
{
var startPoint = new Point(points[i].X, points[i].Y);
var endPoint = new Point(points[i + 1].X, points[i + 1].Y);
dc.DrawLine(pen, startPoint, endPoint);
}
}
else if (endIndex - startIndex == 1 && startIndex < points.Count)
{
// 只有一个点,绘制圆点
var brush = new SolidColorBrush(drawingAttributes.Color);
var point = points[startIndex];
dc.DrawEllipse(brush, null, new Point(point.X, point.Y),
drawingAttributes.Width / 2, drawingAttributes.Height / 2);
}
}
// 将新的DrawingVisual添加到VisualCanvas中
_visualCanvas.AddVisual(segmentVisual);
}
/// <summary>
/// 重新画出笔迹
/// </summary>
public void Redraw()
{
try
if (Stroke == null || _visualCanvas == null) return;
var currentPointCount = Stroke.StylusPoints.Count;
if (currentPointCount == 0) return;
// 计算新增的点数
int newPointCount = currentPointCount - _lastDrawnPointCount;
// 如果新增点数达到阈值,才进行增量绘制
if (newPointCount >= INCREMENTAL_DRAW_THRESHOLD || _lastDrawnPointCount == 0)
{
using (var dc = RenderOpen())
try
{
Stroke.Draw(dc);
if (_lastDrawnPointCount == 0)
{
// 首次绘制:绘制所有点
DrawSegmentToNewVisual(0, currentPointCount);
_lastDrawnPointCount = currentPointCount;
}
else
{
// 从上次绘制的最后一个点开始
int startIndex = Math.Max(0, _lastDrawnPointCount - 1);
DrawSegmentToNewVisual(startIndex, currentPointCount);
_lastDrawnPointCount = currentPointCount;
}
}
catch { }
}
catch { }
}
/// <summary>
/// 强制重绘
/// </summary>
public void ForceRedraw()
{
if (_visualCanvas != null)
{
_visualCanvas.Clear();
}
_lastDrawnPointCount = 0;
Redraw();
}
private readonly DrawingAttributes _drawingAttributes;
+304 -27
View File
@@ -1,6 +1,7 @@
using Microsoft.Office.Interop.PowerPoint;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Ink;
@@ -23,12 +24,23 @@ namespace Ink_Canvas.Helpers
private int _maxSlides = 100;
private string _currentPresentationId = "";
private readonly object _lockObject = new object();
private bool _disposed = false;
private bool _disposed;
// 墨迹锁定机制,防止翻页时的墨迹冲突
private DateTime _inkLockUntil = DateTime.MinValue;
private int _lockedSlideIndex = -1;
private const int InkLockMilliseconds = 500;
// 添加快速切换保护机制
private DateTime _lastSwitchTime = DateTime.MinValue;
private int _lastSwitchSlideIndex = -1;
private const int MinSwitchIntervalMs = 100; // 最小切换间隔100毫秒
// 内存管理相关字段
private long _totalMemoryUsage = 0;
private const long MaxMemoryUsageBytes = 100 * 1024 * 1024; // 100MB限制
private DateTime _lastMemoryCleanup = DateTime.MinValue;
private const int MemoryCleanupIntervalMinutes = 5; // 5分钟清理一次
#endregion
#region Constructor
@@ -55,11 +67,31 @@ namespace Ink_Canvas.Helpers
{
try
{
// 完全清理之前的墨迹状态
ClearAllStrokes();
// 重置墨迹锁定状态
_inkLockUntil = DateTime.MinValue;
_lockedSlideIndex = -1;
// 生成演示文稿唯一标识符
_currentPresentationId = GeneratePresentationId(presentation);
// 重新初始化内存流数组
var slideCount = presentation.Slides.Count;
int slideCount = 0;
try
{
slideCount = presentation.Slides.Count;
}
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr == 0x80048010)
{
return;
}
throw;
}
_memoryStreams = new MemoryStream[slideCount + 2];
// 如果启用自动保存,尝试加载已保存的墨迹
@@ -68,7 +100,6 @@ namespace Ink_Canvas.Helpers
LoadSavedStrokes();
}
LogHelper.WriteLogToFile($"已初始化演示文稿墨迹管理: {presentation.Name}, 幻灯片数量: {slideCount}", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
@@ -88,24 +119,39 @@ namespace Ink_Canvas.Helpers
{
try
{
// 检查墨迹锁定
// 检查墨迹锁定
if (!CanWriteInk(slideIndex))
{
LogHelper.WriteLogToFile($"墨迹写入被锁定,当前页:{slideIndex},锁定页:{_lockedSlideIndex}", LogHelper.LogType.Warning);
if (DateTime.Now < _inkLockUntil)
{
}
return;
}
if (slideIndex < _memoryStreams.Length)
{
// 先释放旧的内存流,防止内存泄漏
try
{
_memoryStreams[slideIndex]?.Dispose();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放旧内存流失败: {ex}", LogHelper.LogType.Warning);
}
// 创建新的内存流
var ms = new MemoryStream();
strokes.Save(ms);
ms.Position = 0;
// 释放旧的内存流
_memoryStreams[slideIndex]?.Dispose();
_memoryStreams[slideIndex] = ms;
LogHelper.WriteLogToFile($"已保存第{slideIndex}页墨迹,大小: {ms.Length} bytes", LogHelper.LogType.Trace);
if (ms.Length > 0)
{
}
// 检查内存使用情况
CheckAndPerformMemoryCleanup();
}
}
catch (Exception ex)
@@ -115,6 +161,45 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 强制保存指定页面的墨迹
/// </summary>
public void ForceSaveSlideStrokes(int slideIndex, StrokeCollection strokes)
{
if (slideIndex <= 0 || strokes == null) return;
lock (_lockObject)
{
try
{
if (slideIndex < _memoryStreams.Length)
{
// 先释放旧的内存流,防止内存泄漏
try
{
_memoryStreams[slideIndex]?.Dispose();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放旧内存流失败: {ex}", LogHelper.LogType.Warning);
}
// 创建新的内存流
var ms = new MemoryStream();
strokes.Save(ms);
ms.Position = 0;
_memoryStreams[slideIndex] = ms;
LogHelper.WriteLogToFile($"已强制保存第{slideIndex}页墨迹,大小: {ms.Length} bytes", LogHelper.LogType.Trace);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"强制保存第{slideIndex}页墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 加载指定页面的墨迹
/// </summary>
@@ -130,7 +215,6 @@ namespace Ink_Canvas.Helpers
{
_memoryStreams[slideIndex].Position = 0;
var strokes = new StrokeCollection(_memoryStreams[slideIndex]);
LogHelper.WriteLogToFile($"已加载第{slideIndex}页墨迹,笔画数量: {strokes.Count}", LogHelper.LogType.Trace);
return strokes;
}
}
@@ -152,17 +236,31 @@ namespace Ink_Canvas.Helpers
{
try
{
// 如果有当前墨迹,先保存
if (currentStrokes != null && currentStrokes.Count > 0)
// 检查快速切换保护
var now = DateTime.Now;
if (now - _lastSwitchTime < TimeSpan.FromMilliseconds(MinSwitchIntervalMs) &&
_lastSwitchSlideIndex == slideIndex)
{
SaveCurrentSlideStrokes(_lockedSlideIndex > 0 ? _lockedSlideIndex : slideIndex, currentStrokes);
LogHelper.WriteLogToFile($"快速切换保护:忽略重复的页面切换请求 {slideIndex}", LogHelper.LogType.Warning);
return LoadSlideStrokes(slideIndex);
}
// 设置墨迹锁定
LockInkForSlide(slideIndex);
// 加载新页面的墨迹
return LoadSlideStrokes(slideIndex);
var newStrokes = LoadSlideStrokes(slideIndex);
// 更新切换记录
_lastSwitchTime = now;
_lastSwitchSlideIndex = slideIndex;
if (newStrokes.Count > 0)
{
}
return newStrokes;
}
catch (Exception ex)
{
@@ -175,7 +273,9 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 保存所有墨迹到文件
/// </summary>
public void SaveAllStrokesToFile(Presentation presentation)
/// <param name="presentation">演示文稿对象</param>
/// <param name="currentSlideIndex">当前播放的页码,如果提供则使用此值保存位置,否则使用_lockedSlideIndex</param>
public void SaveAllStrokesToFile(Presentation presentation, int currentSlideIndex = -1)
{
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return;
@@ -192,7 +292,18 @@ namespace Ink_Canvas.Helpers
// 保存位置信息
try
{
File.WriteAllText(Path.Combine(folderPath, "Position"), _lockedSlideIndex.ToString());
// 优先使用传入的当前页码,否则使用锁定的页码
int positionToSave = currentSlideIndex > 0 ? currentSlideIndex : _lockedSlideIndex;
// 如果都没有有效值,尝试使用最后切换的页码
if (positionToSave <= 0 && _lastSwitchSlideIndex > 0)
{
positionToSave = _lastSwitchSlideIndex;
}
if (positionToSave > 0)
{
File.WriteAllText(Path.Combine(folderPath, "Position"), positionToSave.ToString());
}
}
catch (Exception ex)
{
@@ -201,7 +312,23 @@ namespace Ink_Canvas.Helpers
// 保存所有页面的墨迹
int savedCount = 0;
for (int i = 1; i <= presentation.Slides.Count && i < _memoryStreams.Length; i++)
int slideCount = 0;
try
{
slideCount = presentation.Slides.Count;
}
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr == 0x80048010)
{
return;
}
throw;
}
for (int i = 1; i <= slideCount && i < _memoryStreams.Length; i++)
{
if (_memoryStreams[i] != null)
{
@@ -217,7 +344,6 @@ namespace Ink_Canvas.Helpers
File.WriteAllBytes(filePath, srcBuf);
savedCount++;
LogHelper.WriteLogToFile($"已保存第{i}页墨迹,大小: {byteLength} bytes", LogHelper.LogType.Trace);
}
else
{
@@ -236,7 +362,6 @@ namespace Ink_Canvas.Helpers
}
}
LogHelper.WriteLogToFile($"已保存{savedCount}页墨迹到文件", LogHelper.LogType.Event);
}
catch (Exception ex)
{
@@ -283,7 +408,6 @@ namespace Ink_Canvas.Helpers
}
}
LogHelper.WriteLogToFile($"已从文件加载{loadedCount}页墨迹", LogHelper.LogType.Event);
}
catch (Exception ex)
{
@@ -301,13 +425,30 @@ namespace Ink_Canvas.Helpers
{
try
{
for (int i = 0; i < _memoryStreams.Length; i++)
// 安全释放所有内存流
if (_memoryStreams != null)
{
_memoryStreams[i]?.Dispose();
_memoryStreams[i] = null;
for (int i = 0; i < _memoryStreams.Length; i++)
{
try
{
_memoryStreams[i]?.Dispose();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放内存流{i}失败: {ex}", LogHelper.LogType.Warning);
}
finally
{
_memoryStreams[i] = null;
}
}
// 重新初始化数组
_memoryStreams = new MemoryStream[_maxSlides + 2];
}
CurrentStrokes.Clear();
CurrentStrokes?.Clear();
LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace);
}
catch (Exception ex)
@@ -331,9 +472,145 @@ namespace Ink_Canvas.Helpers
/// </summary>
public bool CanWriteInk(int currentSlideIndex)
{
if (DateTime.Now < _inkLockUntil) return false;
if (currentSlideIndex != _lockedSlideIndex && _lockedSlideIndex > 0) return false;
return true;
// 如果锁定时间已过,允许写入
if (DateTime.Now >= _inkLockUntil)
{
return true;
}
// 如果当前页面与锁定页面相同,允许写入(用户在当前页面绘制)
if (currentSlideIndex == _lockedSlideIndex)
{
return true;
}
// 如果当前页面不是锁定页面,但锁定时间很短(小于50ms),允许写入
// 这样可以确保旧页面的墨迹能够及时保存
if (DateTime.Now - (_inkLockUntil.AddMilliseconds(-InkLockMilliseconds)) < TimeSpan.FromMilliseconds(50))
{
return true;
}
// 只有在快速切换且页面不同时才锁定
return false;
}
/// <summary>
/// 重置墨迹锁定状态
/// </summary>
public void ResetLockState()
{
lock (_lockObject)
{
_inkLockUntil = DateTime.MinValue;
_lockedSlideIndex = -1;
_lastSwitchTime = DateTime.MinValue;
_lastSwitchSlideIndex = -1;
}
}
/// <summary>
/// 检查并执行内存清理
/// </summary>
private void CheckAndPerformMemoryCleanup()
{
try
{
var now = DateTime.Now;
// 检查是否需要执行内存清理
if (now - _lastMemoryCleanup < TimeSpan.FromMinutes(MemoryCleanupIntervalMinutes))
{
return;
}
// 计算当前内存使用量
long currentMemoryUsage = 0;
if (_memoryStreams != null)
{
for (int i = 0; i < _memoryStreams.Length; i++)
{
if (_memoryStreams[i] != null)
{
currentMemoryUsage += _memoryStreams[i].Length;
}
}
}
_totalMemoryUsage = currentMemoryUsage;
// 如果内存使用量超过限制,执行清理
if (currentMemoryUsage > MaxMemoryUsageBytes)
{
LogHelper.WriteLogToFile($"内存使用量超限 ({currentMemoryUsage / 1024 / 1024}MB),开始清理", LogHelper.LogType.Warning);
// 清理非当前页面的墨迹
CleanupInactiveSlideStrokes();
_lastMemoryCleanup = now;
LogHelper.WriteLogToFile($"内存清理完成,当前使用量: {_totalMemoryUsage / 1024 / 1024}MB", LogHelper.LogType.Trace);
}
else
{
_lastMemoryCleanup = now;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"内存清理检查失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 清理非活跃页面的墨迹
/// </summary>
private void CleanupInactiveSlideStrokes()
{
try
{
if (_memoryStreams == null) return;
int cleanedCount = 0;
long freedMemory = 0;
for (int i = 0; i < _memoryStreams.Length; i++)
{
// 保留当前锁定页面和最近访问的页面
if (i == _lockedSlideIndex || i == _lastSwitchSlideIndex)
{
continue;
}
if (_memoryStreams[i] != null)
{
long memorySize = _memoryStreams[i].Length;
try
{
_memoryStreams[i].Dispose();
freedMemory += memorySize;
cleanedCount++;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理页面{i}墨迹失败: {ex}", LogHelper.LogType.Warning);
}
finally
{
_memoryStreams[i] = null;
}
}
}
if (cleanedCount > 0)
{
LogHelper.WriteLogToFile($"已清理{cleanedCount}个页面的墨迹,释放内存: {freedMemory / 1024}KB", LogHelper.LogType.Trace);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理非活跃页面墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
#endregion
+300 -54
View File
@@ -1,4 +1,5 @@
using Microsoft.Office.Interop.PowerPoint;
using Microsoft.Office.Core;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -25,6 +26,7 @@ namespace Ink_Canvas.Helpers
public event Action<Presentation> PresentationOpen;
public event Action<Presentation> PresentationClose;
public event Action<bool> PPTConnectionChanged;
public event Action<bool> SlideShowStateChanged;
#endregion
#region Properties
@@ -69,7 +71,31 @@ namespace Ink_Canvas.Helpers
try
{
if (PPTApplication == null || !Marshal.IsComObject(PPTApplication)) return false;
return PPTApplication.SlideShowWindows?.Count > 0;
// 检查是否有放映窗口
var slideShowWindows = PPTApplication.SlideShowWindows;
if (slideShowWindows == null || slideShowWindows.Count == 0) return false;
// 验证放映窗口是否真正有效
try
{
var slideShowWindow = slideShowWindows[1];
if (slideShowWindow == null) return false;
// 尝试访问放映窗口的属性来验证其有效性
var _ = slideShowWindow.View;
return true;
}
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr == 0x8001010E || hr == 0x80004005)
{
// COM对象已失效,触发断开连接
DisconnectFromPPT();
}
return false;
}
}
catch (COMException comEx)
{
@@ -79,10 +105,12 @@ namespace Ink_Canvas.Helpers
// COM对象已失效,触发断开连接
DisconnectFromPPT();
}
LogHelper.WriteLogToFile($"检查PPT放映状态失败: {comEx.Message} (HR: 0x{hr:X8})", LogHelper.LogType.Warning);
return false;
}
catch
catch (Exception ex)
{
LogHelper.WriteLogToFile($"检查PPT放映状态时发生意外错误: {ex}", LogHelper.LogType.Warning);
return false;
}
}
@@ -92,6 +120,7 @@ namespace Ink_Canvas.Helpers
#region Private Fields
private Timer _connectionCheckTimer;
private Timer _slideShowStateCheckTimer;
private Timer _wpsProcessCheckTimer;
private Process _wpsProcess;
private bool _hasWpsProcessId;
@@ -99,8 +128,9 @@ namespace Ink_Canvas.Helpers
private int _wpsProcessCheckCount;
private WpsWindowInfo _lastForegroundWpsWindow;
private DateTime _lastWindowCheckTime = DateTime.MinValue;
private bool _lastSlideShowState;
private readonly object _lockObject = new object();
private bool _disposed = false;
private bool _disposed;
#endregion
#region Constructor & Initialization
@@ -114,6 +144,10 @@ namespace Ink_Canvas.Helpers
_connectionCheckTimer = new Timer(500);
_connectionCheckTimer.Elapsed += OnConnectionCheckTimerElapsed;
_connectionCheckTimer.AutoReset = true;
_slideShowStateCheckTimer = new Timer(1000);
_slideShowStateCheckTimer.Elapsed += OnSlideShowStateCheckTimerElapsed;
_slideShowStateCheckTimer.AutoReset = true;
}
public void StartMonitoring()
@@ -121,6 +155,7 @@ namespace Ink_Canvas.Helpers
if (!_disposed)
{
_connectionCheckTimer?.Start();
_slideShowStateCheckTimer?.Start();
LogHelper.WriteLogToFile("PPT监控已启动", LogHelper.LogType.Trace);
}
}
@@ -128,6 +163,7 @@ namespace Ink_Canvas.Helpers
public void StopMonitoring()
{
_connectionCheckTimer?.Stop();
_slideShowStateCheckTimer?.Stop();
DisconnectFromPPT();
LogHelper.WriteLogToFile("PPT监控已停止", LogHelper.LogType.Trace);
}
@@ -146,6 +182,18 @@ namespace Ink_Canvas.Helpers
}
}
private void OnSlideShowStateCheckTimerElapsed(object sender, ElapsedEventArgs e)
{
try
{
CheckSlideShowState();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PPT放映状态检查失败: {ex}", LogHelper.LogType.Error);
}
}
private void CheckAndConnectToPPT()
{
lock (_lockObject)
@@ -187,6 +235,29 @@ namespace Ink_Canvas.Helpers
}
}
private void CheckSlideShowState()
{
try
{
if (!IsConnected) return;
var currentSlideShowState = IsInSlideShow;
if (currentSlideShowState != _lastSlideShowState)
{
_lastSlideShowState = currentSlideShowState;
SlideShowStateChanged?.Invoke(currentSlideShowState);
if (!currentSlideShowState)
{
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"检查PPT放映状态异常: {ex}", LogHelper.LogType.Error);
}
}
private Microsoft.Office.Interop.PowerPoint.Application TryConnectToPowerPoint()
{
try
@@ -205,16 +276,6 @@ namespace Ink_Canvas.Helpers
catch (COMException ex)
{
var hr = (uint)ex.HResult;
// 忽略常见的PowerPoint连接错误:
// 0x800401E3: 操作无法使用
// 0x80004005: 未指定错误
// 0x800706B5: RPC服务器不可用
// 0x8001010E: 应用程序调用一个已为另一线程整理的接口
// 0x800401F3: 无效的类字符串(PowerPoint未安装或COM组件未注册)
if (hr != 0x800401E3 && hr != 0x80004005 && hr != 0x800706B5 && hr != 0x8001010E && hr != 0x800401F3)
{
LogHelper.WriteLogToFile($"连接PowerPoint失败: {ex}", LogHelper.LogType.Warning);
}
return null;
}
catch (InvalidCastException)
@@ -222,9 +283,8 @@ namespace Ink_Canvas.Helpers
// COM对象类型转换失败
return null;
}
catch (Exception ex)
catch (Exception)
{
LogHelper.WriteLogToFile($"连接PowerPoint时发生意外错误: {ex}", LogHelper.LogType.Warning);
return null;
}
}
@@ -295,7 +355,7 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"PPT事件注册失败: {ex}", LogHelper.LogType.Error);
throw; // 重新抛出异常,让外层处理
}
}, DispatcherPriority.Normal, System.Threading.CancellationToken.None, TimeSpan.FromSeconds(2));
}, DispatcherPriority.Normal, CancellationToken.None, TimeSpan.FromSeconds(2));
// 获取当前演示文稿信息
UpdateCurrentPresentationInfo();
@@ -350,27 +410,32 @@ namespace Ink_Canvas.Helpers
}
catch (COMException comEx)
{
// COM对象已经被释放或在错误的线程中,忽略这些错误
var hr = (uint)comEx.HResult;
if (hr != 0x8001010E && hr != 0x80004005 && hr != 0x800706B5)
{
LogHelper.WriteLogToFile($"取消PPT事件注册COM异常: {comEx}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile($"取消PPT事件注册时COM异常: {comEx.Message} (HR: 0x{hr:X8})", LogHelper.LogType.Warning);
}
catch (InvalidCastException)
{
// COM对象类型转换失败,通常是因为对象已经被释放
LogHelper.WriteLogToFile("PPT COM对象已被释放,跳过事件注册取消", LogHelper.LogType.Trace);
}
}, DispatcherPriority.Normal, System.Threading.CancellationToken.None, TimeSpan.FromSeconds(1));
catch (Exception ex)
{
LogHelper.WriteLogToFile($"取消PPT事件注册时发生异常: {ex}", LogHelper.LogType.Warning);
}
}, DispatcherPriority.Normal, CancellationToken.None, TimeSpan.FromSeconds(1));
}
}
catch (Exception ex)
{
// 记录但不抛出异常,确保清理过程能够继续
LogHelper.WriteLogToFile($"取消PPT事件注册失败: {ex.GetType().Name} - {ex.Message}", LogHelper.LogType.Warning);
LogHelper.WriteLogToFile($"取消PPT事件注册失败: {ex}", LogHelper.LogType.Warning);
}
// 安全释放COM对象
SafeReleaseComObject(CurrentSlide, "CurrentSlide");
SafeReleaseComObject(CurrentSlides, "CurrentSlides");
SafeReleaseComObject(CurrentPresentation, "CurrentPresentation");
SafeReleaseComObject(PPTApplication, "PPTApplication");
// 清理引用
PPTApplication = null;
CurrentPresentation = null;
@@ -393,6 +458,30 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 安全释放COM对象
/// </summary>
private void SafeReleaseComObject(object comObject, string objectName)
{
try
{
if (comObject != null && Marshal.IsComObject(comObject))
{
int refCount = Marshal.ReleaseComObject(comObject);
LogHelper.WriteLogToFile($"已释放COM对象 {objectName},引用计数: {refCount}", LogHelper.LogType.Trace);
}
}
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
LogHelper.WriteLogToFile($"释放COM对象 {objectName} 时COM异常: {comEx.Message} (HR: 0x{hr:X8})", LogHelper.LogType.Warning);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放COM对象 {objectName} 时发生异常: {ex}", LogHelper.LogType.Warning);
}
}
private void UpdateCurrentPresentationInfo()
{
try
@@ -407,7 +496,29 @@ namespace Ink_Canvas.Helpers
{
CurrentPresentation = activePresentation;
CurrentSlides = CurrentPresentation.Slides;
SlidesCount = CurrentSlides.Count;
// 验证页数读取是否成功
try
{
var slideCount = CurrentSlides.Count;
if (slideCount > 0)
{
SlidesCount = slideCount;
}
else
{
// 页数为0,可能是空演示文稿或读取失败
SlidesCount = 0;
LogHelper.WriteLogToFile("PPT演示文稿页数为0,可能为空演示文稿", LogHelper.LogType.Warning);
}
}
catch (COMException comEx)
{
// 页数读取失败
var hr = (uint)comEx.HResult;
SlidesCount = 0;
LogHelper.WriteLogToFile($"读取PPT页数失败: {comEx.Message} (HR: 0x{hr:X8})", LogHelper.LogType.Warning);
}
// 获取当前幻灯片
try
@@ -508,7 +619,6 @@ namespace Ink_Canvas.Helpers
try
{
PresentationClose?.Invoke(pres);
LogHelper.WriteLogToFile($"演示文稿已关闭: {pres?.Name}", LogHelper.LogType.Event);
// 重新启动连接检查
_connectionCheckTimer?.Start();
@@ -525,7 +635,6 @@ namespace Ink_Canvas.Helpers
{
UpdateCurrentPresentationInfo();
SlideShowBegin?.Invoke(wn);
LogHelper.WriteLogToFile("幻灯片放映开始", LogHelper.LogType.Event);
}
catch (Exception ex)
{
@@ -539,7 +648,6 @@ namespace Ink_Canvas.Helpers
{
UpdateCurrentPresentationInfo();
SlideShowNextSlide?.Invoke(wn);
LogHelper.WriteLogToFile($"幻灯片切换到第{wn?.View?.CurrentShowPosition}页", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
@@ -558,7 +666,6 @@ namespace Ink_Canvas.Helpers
}
SlideShowEnd?.Invoke(pres);
LogHelper.WriteLogToFile("幻灯片放映结束", LogHelper.LogType.Event);
}
catch (Exception ex)
{
@@ -572,14 +679,25 @@ namespace Ink_Canvas.Helpers
{
try
{
if (!IsConnected || !IsInSlideShow || PPTApplication == null) return false;
if (!IsConnected || PPTApplication == null) return false;
if (!Marshal.IsComObject(PPTApplication)) return false;
var slideShowWindow = PPTApplication.SlideShowWindows[1];
if (slideShowWindow?.View != null)
if (IsInSlideShow && PPTApplication.SlideShowWindows.Count >= 1)
{
slideShowWindow.View.GotoSlide(slideNumber);
return true;
var slideShowWindow = PPTApplication.SlideShowWindows[1];
if (slideShowWindow?.View != null)
{
slideShowWindow.View.GotoSlide(slideNumber);
return true;
}
}
else if (CurrentPresentation != null)
{
if (CurrentPresentation.Windows?.Count >= 1)
{
CurrentPresentation.Windows[1].View.GotoSlide(slideNumber);
return true;
}
}
return false;
}
@@ -601,7 +719,7 @@ namespace Ink_Canvas.Helpers
}
}
public bool TryNavigateNext()
public bool TryNavigateNext(bool skipAnimations = false)
{
try
{
@@ -612,8 +730,54 @@ namespace Ink_Canvas.Helpers
if (slideShowWindow?.View != null)
{
slideShowWindow.Activate();
slideShowWindow.View.Next();
return true;
var view = slideShowWindow.View;
var currentPosition = 0;
var totalSlides = 0;
try
{
currentPosition = view.CurrentShowPosition;
totalSlides = slideShowWindow.Presentation?.Slides?.Count ?? 0;
}
catch
{
}
if (skipAnimations && currentPosition > 0 && totalSlides > 0 && currentPosition < totalSlides)
{
try
{
view.GotoSlide(currentPosition + 1, MsoTriState.msoFalse);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"跳过转场动画跳转到下一页失败: {ex}", LogHelper.LogType.Warning);
try
{
view.Next();
return true;
}
catch
{
return false;
}
}
}
else
{
try
{
view.Next();
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"调用下一页失败: {ex}", LogHelper.LogType.Warning);
return false;
}
}
}
return false;
}
@@ -730,14 +894,47 @@ namespace Ink_Canvas.Helpers
}
}
public int GetCurrentSlideNumber()
/// <summary>
/// 获取当前活跃的演示文稿(用于多窗口墨迹分离)
/// </summary>
public Presentation GetCurrentActivePresentation()
{
try
{
if (!IsConnected || !IsInSlideShow || PPTApplication == null) return 0;
if (!Marshal.IsComObject(PPTApplication)) return 0;
if (!IsConnected || PPTApplication == null) return null;
if (!Marshal.IsComObject(PPTApplication)) return null;
return PPTApplication.SlideShowWindows[1]?.View?.CurrentShowPosition ?? 0;
// 如果在放映模式,获取放映窗口的演示文稿
if (IsInSlideShow && PPTApplication.SlideShowWindows.Count > 0)
{
try
{
var slideShowWindow = PPTApplication.SlideShowWindows[1];
if (slideShowWindow?.View != null)
{
return (Presentation)slideShowWindow.View.Slide.Parent;
}
}
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr == 0x80048240) // Integer out of range
{
// 放映窗口已不存在,返回null
return null;
}
throw; // 重新抛出其他COM异常
}
}
// 如果不在放映模式,获取活动窗口的演示文稿
if (PPTApplication.ActiveWindow?.Presentation != null)
{
return PPTApplication.ActiveWindow.Presentation;
}
// 如果没有活动窗口,返回当前演示文稿
return CurrentPresentation;
}
catch (COMException comEx)
{
@@ -747,12 +944,62 @@ namespace Ink_Canvas.Helpers
// COM对象已失效,触发断开连接
DisconnectFromPPT();
}
LogHelper.WriteLogToFile($"获取当前幻灯片编号失败: {comEx.Message}", LogHelper.LogType.Warning);
return 0;
LogHelper.WriteLogToFile($"获取当前活跃演示文稿失败: {comEx.Message}", LogHelper.LogType.Warning);
return CurrentPresentation;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取当前幻灯片编号失败: {ex}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"获取当前活跃演示文稿失败: {ex}", LogHelper.LogType.Error);
return CurrentPresentation;
}
}
/// <summary>
/// 获取当前幻灯片编号
/// </summary>
public int GetCurrentSlideNumber()
{
try
{
if (!IsConnected || PPTApplication == null) return 0;
if (!Marshal.IsComObject(PPTApplication)) return 0;
// 如果在放映模式,获取放映窗口的当前幻灯片编号
if (IsInSlideShow && PPTApplication.SlideShowWindows.Count > 0)
{
var slideShowWindow = PPTApplication.SlideShowWindows[1];
if (slideShowWindow?.View != null)
{
return slideShowWindow.View.CurrentShowPosition;
}
}
// 如果不在放映模式,获取活动窗口的当前幻灯片编号
if (PPTApplication.ActiveWindow?.Selection?.SlideRange?.SlideNumber > 0)
{
return PPTApplication.ActiveWindow.Selection.SlideRange.SlideNumber;
}
// 如果CurrentSlide存在,尝试获取其编号
if (CurrentSlide != null && Marshal.IsComObject(CurrentSlide))
{
return CurrentSlide.SlideNumber;
}
return 0;
}
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr == 0x8001010E || hr == 0x80004005)
{
// COM对象已失效,触发断开连接
DisconnectFromPPT();
}
return 0;
}
catch (Exception)
{
return 0;
}
}
@@ -843,11 +1090,9 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile("成功显示幻灯片导航(PowerPoint模式)", LogHelper.LogType.Event);
return true;
}
else
{
LogHelper.WriteLogToFile("SlideNavigation对象为空,可能是WPS不支持此功能", LogHelper.LogType.Warning);
return false;
}
LogHelper.WriteLogToFile("SlideNavigation对象为空,可能是WPS不支持此功能", LogHelper.LogType.Warning);
return false;
}
catch (COMException comEx)
{
@@ -949,7 +1194,7 @@ namespace Ink_Canvas.Helpers
_wpsProcessCheckTimer.Dispose();
}
// 优化:增加检查间隔到2秒,减少性能开销
// 增加检查间隔到2秒,减少性能开销
_wpsProcessCheckTimer = new Timer(2000);
_wpsProcessCheckTimer.Elapsed += OnWpsProcessCheckTimerElapsed;
_wpsProcessCheckTimer.Start();
@@ -983,7 +1228,7 @@ namespace Ink_Canvas.Helpers
}
// 检查前台WPS窗口是否存在(优化版)
// 检查前台WPS窗口是否存在
bool isForegroundWpsWindowActive = IsForegroundWpsWindowStillActiveOptimized();
if (isForegroundWpsWindowActive)
@@ -995,7 +1240,7 @@ namespace Ink_Canvas.Helpers
return;
}
// 优化:多重验证确保准确性
// 多重验证确保准确性
if (!PerformMultipleWpsWindowChecks())
{
LogHelper.WriteLogToFile("多重验证显示WPS窗口仍然存在,跳过查杀", LogHelper.LogType.Trace);
@@ -1016,7 +1261,7 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// 优化版的前台WPS窗口检测,减少性能开销
/// 前台WPS窗口检测
/// </summary>
private bool IsForegroundWpsWindowStillActiveOptimized()
{
@@ -1042,7 +1287,7 @@ namespace Ink_Canvas.Helpers
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"优化版WPS窗口检测失败: {ex}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"WPS窗口检测失败: {ex}", LogHelper.LogType.Error);
return false;
}
}
@@ -1627,6 +1872,7 @@ namespace Ink_Canvas.Helpers
StopWpsProcessCheckTimer();
_connectionCheckTimer?.Dispose();
_slideShowStateCheckTimer?.Dispose();
_wpsProcessCheckTimer?.Dispose();
_disposed = true;
+88 -3
View File
@@ -1,6 +1,7 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
@@ -18,7 +19,10 @@ namespace Ink_Canvas.Helpers
public int PPTBButtonsOption { get; set; } = 121;
public int PPTLSButtonPosition { get; set; } = 0;
public int PPTRSButtonPosition { get; set; } = 0;
public int PPTLBButtonPosition { get; set; } = 0;
public int PPTRBButtonPosition { get; set; } = 0;
public bool EnablePPTButtonPageClickable { get; set; } = true;
public bool EnablePPTButtonLongPressPageTurn { get; set; } = true;
#endregion
#region Private Fields
@@ -78,20 +82,56 @@ 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 = "/ ?";
}
UpdateNavigationPanelsVisibility();
UpdateNavigationButtonStyles();
if (MainWindow.Settings.Advanced.IsEnableAvoidFullScreenHelper)
{
// 设置为画板模式,允许全屏操作
AvoidFullScreenHelper.SetBoardMode(true);
_dispatcher.BeginInvoke(new Action(() =>
{
MainWindow.MoveWindow(new WindowInteropHelper(_mainWindow).Handle, 0, 0,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height, true);
}), DispatcherPriority.ApplicationIdle);
_mainWindow.isFullScreenApplied = true; // 标记已应用全屏处理
}
}
else
{
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Visible;
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Collapsed;
HideAllNavigationPanels();
if (MainWindow.Settings.Advanced.IsEnableAvoidFullScreenHelper)
{
// 恢复为非画板模式,重新启用全屏限制
AvoidFullScreenHelper.SetBoardMode(false);
_dispatcher.BeginInvoke(new Action(() =>
{
// 退出PPT放映模式,恢复到工作区域大小
var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
MainWindow.MoveWindow(new WindowInteropHelper(_mainWindow).Handle,
workingArea.X, workingArea.Y,
workingArea.Width, workingArea.Height, true);
}), DispatcherPriority.ApplicationIdle);
_mainWindow.isFullScreenApplied = false; // 标记全屏处理已还原
}
}
}
catch (Exception ex)
@@ -110,8 +150,18 @@ namespace Ink_Canvas.Helpers
{
try
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
// 只有在页数有效时才更新页码显示
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
}
else
{
// 页数无效时清空页码显示
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
}
}
catch (Exception ex)
{
@@ -120,6 +170,28 @@ namespace Ink_Canvas.Helpers
});
}
/// <summary>
/// 处理PPT放映状态变化
/// </summary>
public void OnSlideShowStateChanged(bool isInSlideShow)
{
_dispatcher.InvokeAsync(() =>
{
try
{
if (!isInSlideShow)
{
// 如果不在放映模式,隐藏所有导航面板
HideAllNavigationPanels();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理PPT放映状态变化失败: {ex}", LogHelper.LogType.Error);
}
});
}
/// <summary>
/// 更新导航面板显示状态
/// </summary>
@@ -130,7 +202,16 @@ namespace Ink_Canvas.Helpers
try
{
// 检查是否应该显示PPT按钮
bool shouldShowButtons = ShowPPTButton && _mainWindow.BtnPPTSlideShowEnd.Visibility == Visibility.Visible;
// 不仅要检查按钮设置,还要确保确实在PPT放映模式下且页数有效
bool isInSlideShow = _mainWindow.PPTManager?.IsInSlideShow == true;
int slidesCount = _mainWindow.PPTManager?.SlidesCount ?? 0;
bool hasValidPageCount = slidesCount > 0;
bool shouldShowButtons = ShowPPTButton &&
_mainWindow.BtnPPTSlideShowEnd.Visibility == Visibility.Visible &&
isInSlideShow &&
hasValidPageCount &&
!MainWindow.Settings.Automation.IsAutoFoldInPPTSlideShow;
if (!shouldShowButtons)
{
@@ -142,6 +223,10 @@ namespace Ink_Canvas.Helpers
_mainWindow.LeftSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTLSButtonPosition * 2);
_mainWindow.RightSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTRSButtonPosition * 2);
// 设置底部按钮水平位置
_mainWindow.LeftBottomPanelForPPTNavigation.Margin = new Thickness(6 + PPTLBButtonPosition, 0, 0, 6);
_mainWindow.RightBottomPanelForPPTNavigation.Margin = new Thickness(0, 0, 6 + PPTRBButtonPosition, 6);
// 根据显示选项设置面板可见性
var displayOption = PPTButtonsDisplayOption.ToString();
if (displayOption.Length >= 4)
@@ -0,0 +1,92 @@
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();
}
}
}
@@ -0,0 +1,241 @@
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
}
}
@@ -0,0 +1,296 @@
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
}
}
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,214 @@
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
}
}
@@ -0,0 +1,38 @@
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
}
}
@@ -0,0 +1,152 @@
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
}
}
@@ -0,0 +1,273 @@
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);
}
}
}
}
+2 -4
View File
@@ -20,8 +20,8 @@ namespace Ink_Canvas.Helpers.Plugins
public class PluginManager
{
private static readonly string PluginsDirectory = Path.Combine(App.RootPath, "Plugins");
private static readonly string PluginConfigFile = Path.Combine(App.RootPath, "PluginConfig.json");
private static readonly string PluginConfigBackupFile = Path.Combine(App.RootPath, "PluginConfig.json.bak");
private static readonly string PluginConfigFile = Path.Combine(App.RootPath, "Configs", "PluginConfig.json");
private static readonly string PluginConfigBackupFile = Path.Combine(App.RootPath, "Configs", "PluginConfig.json.bak");
private static PluginManager _instance;
private static SemaphoreSlim _configLock = new SemaphoreSlim(1, 1);
@@ -79,8 +79,6 @@ namespace Ink_Canvas.Helpers.Plugins
Directory.CreateDirectory(PluginsDirectory);
}
// 加载插件配置
LoadConfig();
// 初始化自动保存计时器(3秒)
_autoSaveTimer = new Timer(3000);
@@ -0,0 +1,509 @@
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
}
}
+156
View File
@@ -0,0 +1,156 @@
using System;
using System.Drawing;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Interop;
using Point = System.Windows.Point;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 屏幕检测帮助类 - 用于检测窗口所在的屏幕和屏幕信息
/// </summary>
public static class ScreenDetectionHelper
{
/// <summary>
/// 获取窗口所在的屏幕
/// </summary>
/// <param name="window">要检测的窗口</param>
/// <returns>窗口所在的屏幕,如果无法检测则返回主屏幕</returns>
public static Screen GetWindowScreen(Window window)
{
try
{
if (window == null)
return Screen.PrimaryScreen;
// 获取窗口的句柄
var hwndSource = PresentationSource.FromVisual(window) as HwndSource;
if (hwndSource == null)
return Screen.PrimaryScreen;
// 获取窗口在屏幕上的位置
var windowRect = GetWindowScreenBounds(window);
// 查找与窗口重叠最多的屏幕
Screen targetScreen = null;
double maxIntersection = 0;
foreach (var screen in Screen.AllScreens)
{
var intersection = Rectangle.Intersect(windowRect, screen.Bounds);
if (intersection.Width * intersection.Height > maxIntersection)
{
maxIntersection = intersection.Width * intersection.Height;
targetScreen = screen;
}
}
return targetScreen ?? Screen.PrimaryScreen;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"检测窗口屏幕时出错: {ex.Message}", LogHelper.LogType.Warning);
return Screen.PrimaryScreen;
}
}
/// <summary>
/// 获取窗口在屏幕坐标系中的边界
/// </summary>
/// <param name="window">要检测的窗口</param>
/// <returns>窗口的屏幕边界</returns>
private static Rectangle GetWindowScreenBounds(Window window)
{
try
{
// 获取窗口左上角在屏幕上的位置
var topLeft = window.PointToScreen(new Point(0, 0));
// 获取窗口右下角在屏幕上的位置
var bottomRight = window.PointToScreen(new Point(window.ActualWidth, window.ActualHeight));
return new Rectangle(
(int)topLeft.X,
(int)topLeft.Y,
(int)(bottomRight.X - topLeft.X),
(int)(bottomRight.Y - topLeft.Y));
}
catch
{
// 如果无法获取精确位置,返回窗口的Left和Top
return new Rectangle(
(int)window.Left,
(int)window.Top,
(int)window.Width,
(int)window.Height);
}
}
/// <summary>
/// 检查是否有多个屏幕
/// </summary>
/// <returns>如果有多个屏幕返回true,否则返回false</returns>
public static bool HasMultipleScreens()
{
try
{
return Screen.AllScreens.Length > 1;
}
catch
{
return false;
}
}
/// <summary>
/// 获取主屏幕
/// </summary>
/// <returns>主屏幕</returns>
public static Screen GetPrimaryScreen()
{
try
{
return Screen.PrimaryScreen;
}
catch
{
return null;
}
}
/// <summary>
/// 获取所有屏幕信息
/// </summary>
/// <returns>所有屏幕的数组</returns>
public static Screen[] GetAllScreens()
{
try
{
return Screen.AllScreens;
}
catch
{
return new Screen[] { Screen.PrimaryScreen };
}
}
/// <summary>
/// 检查窗口是否在主屏幕上
/// </summary>
/// <param name="window">要检查的窗口</param>
/// <returns>如果窗口在主屏幕上返回true,否则返回false</returns>
public static bool IsWindowOnPrimaryScreen(Window window)
{
try
{
var windowScreen = GetWindowScreen(window);
return windowScreen == Screen.PrimaryScreen;
}
catch
{
return true; // 出错时假设在主屏幕
}
}
}
}
+165
View File
@@ -0,0 +1,165 @@
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);
}
}
}
}
+263
View File
@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 窗口Z-Order管理器,用于管理窗口的层级顺序
/// 在无焦点模式下,确保后打开的窗口能够置顶于先打开的窗口
/// </summary>
public static class WindowZOrderManager
{
#region Win32 API
private const int GWL_EXSTYLE = -20;
private const int WS_EX_TOPMOST = 0x00000008;
private const int WS_EX_NOACTIVATE = 0x08000000;
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint SWP_SHOWWINDOW = 0x0040;
private const uint SWP_NOOWNERZORDER = 0x0200;
[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 int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll")]
private static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentProcessId();
#endregion
// 窗口层级管理
private static readonly List<WindowInfo> _windowStack = new List<WindowInfo>();
private static readonly object _lockObject = new object();
/// <summary>
/// 窗口信息类
/// </summary>
private class WindowInfo
{
public IntPtr Handle { get; set; }
public Window Window { get; set; }
public DateTime CreatedTime { get; set; }
public bool IsTopmost { get; set; }
public bool IsNoFocusMode { get; set; }
}
/// <summary>
/// 注册窗口到Z-Order管理器
/// </summary>
/// <param name="window">要注册的窗口</param>
/// <param name="isTopmost">是否置顶</param>
/// <param name="isNoFocusMode">是否无焦点模式</param>
public static void RegisterWindow(Window window, bool isTopmost = false, bool isNoFocusMode = false)
{
if (window == null) return;
lock (_lockObject)
{
var hwnd = new WindowInteropHelper(window).Handle;
if (hwnd == IntPtr.Zero) return;
// 移除已存在的记录
_windowStack.RemoveAll(w => w.Handle == hwnd);
// 添加新记录
var windowInfo = new WindowInfo
{
Handle = hwnd,
Window = window,
CreatedTime = DateTime.Now,
IsTopmost = isTopmost,
IsNoFocusMode = isNoFocusMode
};
_windowStack.Add(windowInfo);
// 应用Z-Order
ApplyZOrder();
}
}
/// <summary>
/// 从Z-Order管理器中移除窗口
/// </summary>
/// <param name="window">要移除的窗口</param>
public static void UnregisterWindow(Window window)
{
if (window == null) return;
lock (_lockObject)
{
var hwnd = new WindowInteropHelper(window).Handle;
_windowStack.RemoveAll(w => w.Handle == hwnd);
ApplyZOrder();
}
}
/// <summary>
/// 设置窗口为置顶状态
/// </summary>
/// <param name="window">要置顶的窗口</param>
/// <param name="isTopmost">是否置顶</param>
public static void SetWindowTopmost(Window window, bool isTopmost)
{
if (window == null) return;
lock (_lockObject)
{
var windowInfo = _windowStack.FirstOrDefault(w => w.Window == window);
if (windowInfo != null)
{
windowInfo.IsTopmost = isTopmost;
ApplyZOrder();
}
}
}
/// <summary>
/// 将窗口移到最顶层
/// </summary>
/// <param name="window">要移到最顶层的窗口</param>
public static void BringToTop(Window window)
{
if (window == null) return;
try
{
var hwnd = new WindowInteropHelper(window).Handle;
if (hwnd == IntPtr.Zero) return;
// 使用更直接的方法:先激活窗口,再置顶
window.Activate();
window.Focus();
// 设置窗口为置顶
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
// 确保窗口样式正确
int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
// 再次确保置顶
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"BringToTop失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 应用Z-Order排序
/// </summary>
private static void ApplyZOrder()
{
// 简化逻辑:直接设置所有窗口为置顶,让Windows系统自然处理层级
foreach (var windowInfo in _windowStack.ToList())
{
if (windowInfo.IsTopmost && IsWindow(windowInfo.Handle) && IsWindowVisible(windowInfo.Handle) && !IsIconic(windowInfo.Handle))
{
// 设置窗口为置顶
SetWindowPos(windowInfo.Handle, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
// 确保窗口样式正确
int exStyle = GetWindowLong(windowInfo.Handle, GWL_EXSTYLE);
if ((exStyle & WS_EX_TOPMOST) == 0)
{
SetWindowLong(windowInfo.Handle, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
}
}
}
}
/// <summary>
/// 检查是否有子窗口在前景
/// </summary>
/// <returns>如果有子窗口在前景返回true</returns>
public static bool HasChildWindowInForeground()
{
lock (_lockObject)
{
var foregroundWindow = GetForegroundWindow();
if (foregroundWindow == IntPtr.Zero) return false;
return _windowStack.Any(w => w.Handle == foregroundWindow);
}
}
/// <summary>
/// 清理无效的窗口记录
/// </summary>
public static void CleanupInvalidWindows()
{
lock (_lockObject)
{
_windowStack.RemoveAll(w => !IsWindow(w.Handle) || !IsWindowVisible(w.Handle));
}
}
/// <summary>
/// 获取当前注册的窗口数量
/// </summary>
/// <returns>窗口数量</returns>
public static int GetWindowCount()
{
lock (_lockObject)
{
return _windowStack.Count;
}
}
/// <summary>
/// 强制刷新所有窗口的置顶状态
/// </summary>
public static void ForceRefreshAllWindows()
{
lock (_lockObject)
{
foreach (var windowInfo in _windowStack.ToList())
{
if (windowInfo.IsTopmost && IsWindow(windowInfo.Handle))
{
// 强制设置窗口为置顶
SetWindowPos(windowInfo.Handle, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
// 确保窗口样式正确
int exStyle = GetWindowLong(windowInfo.Handle, GWL_EXSTYLE);
SetWindowLong(windowInfo.Handle, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
}
}
}
}
}
}
@@ -0,0 +1,62 @@
using Hardcodet.Wpf.TaskbarNotification;
using Microsoft.Toolkit.Uwp.Notifications;
using System;
using System.Windows;
namespace Ink_Canvas.Helpers
{
internal static class WindowsNotificationHelper
{
private const string APP_ID = "InkCanvasForClass.CE";
public static void ShowNewVersionToast(string version)
{
try
{
var os = Environment.OSVersion.Version;
if (os.Major == 6 && os.Minor == 1)
{
ShowBalloonForWin7(version);
}
else
{
ShowToastForModernWindows(version);
}
}
catch
{
}
}
private static void ShowBalloonForWin7(string version)
{
Application.Current?.Dispatcher.Invoke(() =>
{
try
{
var taskbar = Application.Current.Resources["TaskbarTrayIcon"] as TaskbarIcon;
if (taskbar == null) return;
taskbar.Visibility = Visibility.Visible;
taskbar.ShowBalloonTip(
"InkCanvasForClass CE",
$"有新版本:{version}",
BalloonIcon.Info);
}
catch
{
}
});
}
private static void ShowToastForModernWindows(string version)
{
new ToastContentBuilder()
.AddText("InkCanvasForClass CE")
.AddText($"有新版本:{version}")
.Show();
}
}
}
+96
View File
@@ -0,0 +1,96 @@
{
"Version": "1.0",
"LastModified": "2025-01-28T15:30:00",
"Hotkeys": [
{
"Name": "Undo",
"Key": "Z",
"Modifiers": "Control"
},
{
"Name": "Redo",
"Key": "Y",
"Modifiers": "Control"
},
{
"Name": "Clear",
"Key": "E",
"Modifiers": "Control"
},
{
"Name": "Paste",
"Key": "V",
"Modifiers": "Control"
},
{
"Name": "SelectTool",
"Key": "S",
"Modifiers": "Alt"
},
{
"Name": "DrawTool",
"Key": "D",
"Modifiers": "Alt"
},
{
"Name": "EraserTool",
"Key": "E",
"Modifiers": "Alt"
},
{
"Name": "BlackboardTool",
"Key": "B",
"Modifiers": "Alt"
},
{
"Name": "QuitDrawTool",
"Key": "Q",
"Modifiers": "Alt"
},
{
"Name": "Pen1",
"Key": "D1",
"Modifiers": "Alt"
},
{
"Name": "Pen2",
"Key": "D2",
"Modifiers": "Alt"
},
{
"Name": "Pen3",
"Key": "D3",
"Modifiers": "Alt"
},
{
"Name": "Pen4",
"Key": "D4",
"Modifiers": "Alt"
},
{
"Name": "Pen5",
"Key": "D5",
"Modifiers": "Alt"
},
{
"Name": "DrawLine",
"Key": "L",
"Modifiers": "Alt"
},
{
"Name": "Screenshot",
"Key": "C",
"Modifiers": "Alt"
},
{
"Name": "Hide",
"Key": "V",
"Modifiers": "Alt"
},
{
"Name": "Exit",
"Key": "Escape",
"Modifiers": "None"
}
]
}
+86 -3
View File
@@ -128,6 +128,7 @@
<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" />
@@ -152,11 +153,14 @@
<PackageReference Include="MdXaml" Version="1.27.0" />
<PackageReference Include="Microsoft.Office.Interop.PowerPoint" Version="15.0.4420.1018" />
<PackageReference Include="MicrosoftOfficeCore" Version="15.0.0" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="OSVersionExt" Version="3.0.0" />
<PackageReference Include="AForge.Video" Version="2.2.5" />
<PackageReference Include="AForge.Video.DirectShow" Version="2.2.5" />
</ItemGroup>
<ItemGroup>
<COMReference Include="IWshRuntimeLibrary">
@@ -189,11 +193,14 @@
</ItemGroup>
<ItemGroup>
<None Include="Resources\TimerDownNotice.wav" />
<None Include="Resources\ProgressiveAudio.wav" />
</ItemGroup>
<ItemGroup>
<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" />
@@ -205,6 +212,7 @@
<Resource Include="Resources\DeveloperAvatars\RaspberryKan.jpg" />
<Resource Include="Resources\DeveloperAvatars\wwei.png" />
<Resource Include="Resources\DeveloperAvatars\yuwenhui2020.png" />
<Resource Include="Resources\DeveloperAvatars\CJKmkp.jpg" />
<Resource Include="Resources\icc.ico" />
<Resource Include="Resources\Icons-png\AdmoxBooth.png" />
<Resource Include="Resources\Icons-png\AdmoxWhiteboard.png" />
@@ -219,6 +227,10 @@
<Resource Include="Resources\Icons-png\icc-transparent-dark.png" />
<Resource Include="Resources\Icons-png\icc-transparent.png" />
<Resource Include="Resources\Icons-png\icc.png" />
<Resource Include="Resources\Icons-png\icc-dark.png" />
<Resource Include="Resources\Icons-png\icc-noshadow.png" />
<Resource Include="Resources\Icons-png\icc-sharpdark.png" />
<Resource Include="Resources\Icons-png\icc-transparent-light-small.png" />
<Resource Include="Resources\Icons-png\InkCanvas.png" />
<Resource Include="Resources\Icons-png\kuanciya.png" />
<Resource Include="Resources\Icons-png\kuandogeyuanliangwo.png" />
@@ -235,6 +247,10 @@
<Resource Include="Resources\Icons-png\undo.png" />
<Resource Include="Resources\Icons-png\minimize.png" />
<Resource Include="Resources\Icons-png\penUpright.png" />
<Resource Include="Resources\new-icons\multi-touch_white.png" />
<Resource Include="Resources\new-icons\hand-move_white.png" />
<Resource Include="Resources\new-icons\zoom_white.png" />
<Resource Include="Resources\new-icons\rotate_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\twoFingelMove-Blue.png" />
@@ -251,6 +267,7 @@
<Resource Include="Resources\DeveloperAvatars\kengwang.png" />
<Resource Include="Resources\DeveloperAvatars\STBBRD.png" />
<Resource Include="Resources\DeveloperAvatars\WXRIW.png" />
<Resource Include="Resources\DeveloperAvatars\PrefacedCorg.jpg" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\down.png" />
@@ -283,7 +300,9 @@
<Resource Include="Resources\Icons-Fluent\ic_fluent_arrow_circle_left_24_regular.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_arrow_circle_right_24_regular.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_weather_moon_24_regular.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_weather_moon_24_regular_white.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_weather_sunny_24_regular.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_weather_sunny_24_regular_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-Fluent\ic_fluent_signature_24_regular.png" />
@@ -318,6 +337,7 @@
<ItemGroup>
<Resource Include="Resources\Icons-Fluent\ic_fluent_arrow_rotate_clockwise_24_regular.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_scale_fit_24_regular.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_scale_fit_24_regular_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-Fluent\ic_fluent_scales_24_regular.png" />
@@ -335,9 +355,6 @@
<ItemGroup>
<Resource Include="Resources\Icons-Fluent\ic_fluent_clock_24_regular.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-Fluent\ic_fluent_book_question_mark_24_regular.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-Fluent\ic_fluent_keyboard_24_regular.png" />
</ItemGroup>
@@ -405,6 +422,7 @@
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\new-icons\gesture.png" />
<Resource Include="Resources\new-icons\gesture_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\new-icons\gesture-enabled.png" />
@@ -452,6 +470,37 @@
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\cube.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\arrow_white.png" />
<Resource Include="Resources\Icons-png\geo-icons\dashed-line_white.png" />
<Resource Include="Resources\Icons-png\geo-icons\dotted-line_white.png" />
<Resource Include="Resources\Icons-png\geo-icons\line_white.png" />
<Resource Include="Resources\Icons-png\geo-icons\paralle-lines_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\centered-square_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\centered-circle_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\centered-circle-dashed_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\centered-oval_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\square_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\cylinder_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\cone_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\geo-icons\cube_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\EasiNote.png" />
<Resource Include="Resources\Icons-png\EasiNote3C.png" />
@@ -488,6 +537,19 @@
<Resource Include="Resources\new-icons\unfold-chevron.png" />
<Resource Include="Resources\new-icons\zoom.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-Fluent\ic_fluent_person_money_24_regular-light.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_people_money_24_regular-light.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_timer_24_regular-light.png" />
<Resource Include="Resources\new-icons\blackboard-light.png" />
<Resource Include="Resources\new-icons\end-slides-show-light.png" />
<Resource Include="Resources\new-icons\eye-light.png" />
<Resource Include="Resources\new-icons\chevron-left-light.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-Fluent\ic_fluent_keyboard_24_regular_white.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_control_button_24_regular_white.png" />
</ItemGroup>
<ItemGroup>
<Compile Remove="AssemblyInfo.cs" />
</ItemGroup>
@@ -514,6 +576,10 @@
<None Remove="Resources\Icons-png\icc-transparent-dark.png" />
<None Remove="Resources\Icons-png\icc-transparent.png" />
<None Remove="Resources\Icons-png\icc.png" />
<None Remove="Resources\Icons-png\icc-dark.png" />
<None Remove="Resources\Icons-png\icc-noshadow.png" />
<None Remove="Resources\Icons-png\icc-sharpdark.png" />
<None Remove="Resources\Icons-png\icc-transparent-light-small.png" />
<None Remove="Resources\Icons-png\idt.png" />
<None Remove="Resources\Icons-png\InkCanvas.png" />
<None Remove="Resources\Icons-png\kuanciya.png" />
@@ -550,14 +616,31 @@
<ItemGroup>
<Resource Include="Resources\Icons-png\idt.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Fonts\LXGWWenKaiTC-Regular.ttf" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons-png\HiteAnnotation.png" />
<Resource Include="Resources\Icons-png\AiClass.png" />
<Resource Include="Resources\Icons-png\天喻教育云.png" />
<Resource Include="Resources\Icons-png\畅言智慧课堂.png" />
<Resource Include="Resources\PresentationExample\bottombar-dark.png" />
<Resource Include="Resources\PresentationExample\bottombar-white.png" />
<Resource Include="Resources\PresentationExample\page.jpg" />
<Resource Include="Resources\PresentationExample\sidebar-dark.png" />
<Resource Include="Resources\PresentationExample\sidebar-white.png" />
<Resource Include="Resources\PresentationExample\toolbar.png" />
<Resource Include="Resources\Startup-animation\ICC Spring.png" />
<Resource Include="Resources\Startup-animation\ICC Summer.png" />
<Resource Include="Resources\Startup-animation\ICC Autumn.png" />
<Resource Include="Resources\Startup-animation\ICC Winter.png" />
<Resource Include="Resources\Startup-animation\ICC Horse.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_copy_24_regular_white.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_copy_add_24_regular_white.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_flip_horizontal_24_regular_white.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_flip_vertical_24_regular_white.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_edit_24_regular_white.png" />
<Resource Include="Resources\Icons-Fluent\ic_fluent_delete_24_regular_white.png" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Settings.Designer.cs">
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_LastSelectedProfileId>D:\vs\ica\Ink Canvas\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
</PropertyGroup>
</Project>
+1515 -524
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+59 -12
View File
@@ -103,7 +103,6 @@ namespace Ink_Canvas
HideSubPanels("cursor");
SidePannelMarginAnimation(-10);
});
isFloatingBarChangingHideMode = false;
}
private async void LeftUnFoldButtonDisplayQuickPanel_MouseUp(object sender, MouseButtonEventArgs e)
@@ -259,24 +258,72 @@ namespace Ink_Canvas
PenIcon_Click(null, null);
}
if (StackPanelPPTControls.Visibility == Visibility.Visible)
// 只有在PPT放映模式下且页数有效时才显示翻页按钮
if (StackPanelPPTControls.Visibility == Visibility.Visible &&
BtnPPTSlideShowEnd.Visibility == Visibility.Visible &&
PPTManager?.IsInSlideShow == true &&
PPTManager?.SlidesCount > 0)
{
var dops = Settings.PowerPointSettings.PPTButtonsDisplayOption.ToString();
var dopsc = dops.ToCharArray();
if (dopsc[0] == '2' && isDisplayingOrHidingBlackboard == false) AnimationsHelper.ShowWithFadeIn(LeftBottomPanelForPPTNavigation);
if (dopsc[1] == '2' && isDisplayingOrHidingBlackboard == false) AnimationsHelper.ShowWithFadeIn(RightBottomPanelForPPTNavigation);
if (dopsc[2] == '2' && isDisplayingOrHidingBlackboard == false) AnimationsHelper.ShowWithFadeIn(LeftSidePanelForPPTNavigation);
if (dopsc[3] == '2' && isDisplayingOrHidingBlackboard == false) AnimationsHelper.ShowWithFadeIn(RightSidePanelForPPTNavigation);
if (dopsc[0] == '2' && !isDisplayingOrHidingBlackboard) AnimationsHelper.ShowWithFadeIn(LeftBottomPanelForPPTNavigation);
if (dopsc[1] == '2' && !isDisplayingOrHidingBlackboard) AnimationsHelper.ShowWithFadeIn(RightBottomPanelForPPTNavigation);
if (dopsc[2] == '2' && !isDisplayingOrHidingBlackboard) AnimationsHelper.ShowWithFadeIn(LeftSidePanelForPPTNavigation);
if (dopsc[3] == '2' && !isDisplayingOrHidingBlackboard) AnimationsHelper.ShowWithFadeIn(RightSidePanelForPPTNavigation);
}
else
{
// 如果条件不满足,确保隐藏翻页按钮
LeftBottomPanelForPPTNavigation.Visibility = Visibility.Collapsed;
RightBottomPanelForPPTNavigation.Visibility = Visibility.Collapsed;
LeftSidePanelForPPTNavigation.Visibility = Visibility.Collapsed;
RightSidePanelForPPTNavigation.Visibility = Visibility.Collapsed;
}
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible)
ViewboxFloatingBarMarginAnimation(60);
else
ViewboxFloatingBarMarginAnimation(100, true);
// 新只在屏幕模式下显示浮动栏
if (currentMode == 0)
{
// 强制更新布局以确保ActualWidth正确
ViewboxFloatingBar.UpdateLayout();
// 等待一小段时间让布局完全更新
Task.Delay(50);
// 再次强制更新布局
ViewboxFloatingBar.UpdateLayout();
// 强制重新测量和排列
ViewboxFloatingBar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
ViewboxFloatingBar.Arrange(new Rect(ViewboxFloatingBar.DesiredSize));
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible)
ViewboxFloatingBarMarginAnimation(60);
else
ViewboxFloatingBarMarginAnimation(100, true);
}
SidePannelMarginAnimation(-50, !unfoldFloatingBarByUser);
});
isFloatingBarChangingHideMode = false;
await Dispatcher.InvokeAsync(async () =>
{
try
{
// 等待UI完全更新
await Task.Delay(100);
// 获取当前选中的模式并重新设置高光位置
string selectedToolMode = GetCurrentSelectedMode();
if (!string.IsNullOrEmpty(selectedToolMode))
{
SetFloatingBarHighlightPosition(selectedToolMode);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"浮动栏展开后重新设置按钮高亮状态失败: {ex.Message}", LogHelper.LogType.Error);
}
});
}
private async void SidePannelMarginAnimation(int MarginFromEdge, bool isNoAnimation = false) // Possible value: -50, -10
@@ -315,4 +362,4 @@ namespace Ink_Canvas
isFloatingBarChangingHideMode = false;
}
}
}
}
+464 -3
View File
@@ -1,24 +1,45 @@
using iNKORE.UI.WPF.Modern;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Application = System.Windows.Application;
using ui = iNKORE.UI.WPF.Modern.Controls;
namespace Ink_Canvas
{
public partial class MainWindow : Window
{
private Color FloatBarForegroundColor = Color.FromRgb(102, 102, 102);
private Color FloatBarForegroundColor;
private void SetTheme(string theme)
private void SetTheme(string theme, bool autoSwitchIcon = false)
{
// 清理现有的主题资源
var resourcesToRemove = new List<ResourceDictionary>();
foreach (var dict in Application.Current.Resources.MergedDictionaries)
{
if (dict.Source != null &&
(dict.Source.ToString().Contains("Light.xaml") ||
dict.Source.ToString().Contains("Dark.xaml")))
{
resourcesToRemove.Add(dict);
}
}
foreach (var dict in resourcesToRemove)
{
Application.Current.Resources.MergedDictionaries.Remove(dict);
}
if (theme == "Light")
{
var rd1 = new ResourceDictionary
{ Source = new Uri("Resources/Styles/Light.xaml", UriKind.Relative) };
Application.Current.Resources.MergedDictionaries.Add(rd1);
// 在主题资源之后添加其他资源
var rd2 = new ResourceDictionary
{ Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative) };
Application.Current.Resources.MergedDictionaries.Add(rd2);
@@ -33,13 +54,39 @@ namespace Ink_Canvas
ThemeManager.SetRequestedTheme(window, ElementTheme.Light);
FloatBarForegroundColor = (Color)Application.Current.FindResource("FloatBarForegroundColor");
InitializeFloatBarForegroundColor();
// 刷新快速面板图标
RefreshQuickPanelIcons();
// 刷新墨迹选中栏图标
RefreshStrokeSelectionIcons();
// 刷新图片选中栏图标
RefreshImageSelectionIcons();
// 刷新手势按钮图标
RefreshGestureButtonIcon();
RefreshFloatingBarHighlightColors();
if (autoSwitchIcon)
{
AutoSwitchFloatingBarIconForTheme("Light");
}
// 强制刷新UI
window.InvalidateVisual();
// 通知其他窗口刷新主题
RefreshOtherWindowsTheme();
}
else if (theme == "Dark")
{
var rd1 = new ResourceDictionary { Source = new Uri("Resources/Styles/Dark.xaml", UriKind.Relative) };
Application.Current.Resources.MergedDictionaries.Add(rd1);
// 在主题资源之后添加其他资源
var rd2 = new ResourceDictionary
{ Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative) };
Application.Current.Resources.MergedDictionaries.Add(rd2);
@@ -54,7 +101,198 @@ namespace Ink_Canvas
ThemeManager.SetRequestedTheme(window, ElementTheme.Dark);
InitializeFloatBarForegroundColor();
// 刷新快速面板图标
RefreshQuickPanelIcons();
// 刷新墨迹选中栏图标
RefreshStrokeSelectionIcons();
// 刷新图片选中栏图标
RefreshImageSelectionIcons();
// 刷新手势按钮图标
RefreshGestureButtonIcon();
RefreshFloatingBarHighlightColors();
if (autoSwitchIcon)
{
AutoSwitchFloatingBarIconForTheme("Dark");
}
// 强制刷新UI
window.InvalidateVisual();
// 通知其他窗口刷新主题
RefreshOtherWindowsTheme();
}
}
/// <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();
}
}
catch (Exception)
{
}
}
/// <summary>
/// 刷新浮动栏高光条颜色
/// </summary>
private void RefreshFloatingBarHighlightColors()
{
try
{
if (FloatingbarSelectionBG != null && FloatingbarSelectionBG.Visibility == Visibility.Visible)
{
// 根据主题设置高光颜色
Color highlightBackgroundColor;
Color highlightBarColor;
bool isDarkTheme = Settings.Appearance.Theme == 1 ||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
if (isDarkTheme)
{
highlightBackgroundColor = Color.FromArgb(21, 102, 204, 255);
highlightBarColor = Color.FromRgb(102, 204, 255);
}
else
{
highlightBackgroundColor = Color.FromArgb(21, 59, 130, 246);
highlightBarColor = Color.FromRgb(37, 99, 235);
}
// 设置高光背景颜色
FloatingbarSelectionBG.Background = new SolidColorBrush(highlightBackgroundColor);
if (FloatingbarSelectionBG.Child is System.Windows.Controls.Canvas canvas && canvas.Children.Count > 0)
{
var firstChild = canvas.Children[0];
if (firstChild is Border innerBorder)
{
innerBorder.Background = new SolidColorBrush(highlightBarColor);
}
}
}
}
catch (Exception)
{
}
}
/// <summary>
/// 刷新浮动工具栏按钮颜色
/// </summary>
private void RefreshFloatingBarButtonColors()
{
try
{
// 根据主题选择高光颜色
Color selectedColor;
bool isDarkTheme = Settings.Appearance.Theme == 1 ||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
if (isDarkTheme)
{
selectedColor = Color.FromRgb(102, 204, 255);
}
else
{
selectedColor = Color.FromRgb(30, 58, 138);
}
// 根据当前模式设置按钮颜色
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);
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);
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);
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);
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);
break;
}
}
catch (Exception)
{
}
}
@@ -91,5 +329,228 @@ namespace Ink_Canvas
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;
}
UpdateFloatingBarIcon();
UpdateFloatingBarIconComboBox();
}
catch (Exception)
{
}
}
/// <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);
}
}
}
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);
}
}
}
catch (Exception)
{
// 忽略异常,确保主题切换不会因为图标刷新失败而中断
}
}
/// <summary>
/// 递归刷新图片选中栏内的图标
/// </summary>
private void RefreshImageSelectionIconsRecursive(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)
{
// 递归处理子面板
RefreshImageSelectionIconsRecursive(childPanel);
}
else if (child is Border border && border.Child is System.Windows.Controls.Panel borderPanel)
{
// 处理Border内的面板
RefreshImageSelectionIconsRecursive(borderPanel);
}
else if (child is Grid grid)
{
// 处理Grid内的子元素
foreach (var gridChild in grid.Children)
{
if (gridChild is Image gridImage)
{
gridImage.InvalidateVisual();
}
}
}
}
}
catch (Exception)
{
// 忽略异常
}
}
/// <summary>
/// 刷新手势按钮图标
/// </summary>
private void RefreshGestureButtonIcon()
{
try
{
// 调用手势按钮颜色和图标更新方法,该方法会根据当前主题和手势状态设置正确的图标
CheckEnableTwoFingerGestureBtnColorPrompt();
}
catch (Exception)
{
}
}
/// <summary>
/// 刷新其他窗口的主题
/// </summary>
private void RefreshOtherWindowsTheme()
{
try
{
// 刷新所有打开的窗口
foreach (Window window in Application.Current.Windows)
{
if (window is CountdownTimerWindow timerWindow)
{
timerWindow.RefreshTheme();
}
else if (window is RandWindow randWindow)
{
randWindow.RefreshTheme();
}
else if (window is OperatingGuideWindow operatingGuideWindow)
{
operatingGuideWindow.RefreshTheme();
}
}
// 刷新计时器控件
if (TimerControl != null)
{
TimerControl.RefreshTheme();
}
if (MinimizedTimerControl != null)
{
MinimizedTimerControl.RefreshTheme();
}
}
catch (Exception)
{
}
}
}
}
+151 -34
View File
@@ -15,10 +15,10 @@ namespace Ink_Canvas
private StrokeCollection[] strokeCollections = new StrokeCollection[101];
private bool[] whiteboadLastModeIsRedo = new bool[101];
private StrokeCollection lastTouchDownStrokeCollection = new StrokeCollection();
private int CurrentWhiteboardIndex = 1;
private int WhiteboardTotalCount = 1;
private TimeMachineHistory[][] TimeMachineHistories = new TimeMachineHistory[101][]; //最多99页,0用来存储非白板时的墨迹以便还原
private TimeMachineHistory[][] TimeMachineHistories = new TimeMachineHistory[101][];
private bool[] savedMultiTouchModeStates = new bool[101];
// 保存每页白板图片信息
private void SaveStrokes(bool isBackupMain = false)
@@ -97,6 +97,8 @@ namespace Ink_Canvas
{
var timeMachineHistory = timeMachine.ExportTimeMachineHistory();
TimeMachineHistories[0] = timeMachineHistory;
// 保存多指书写模式状态
savedMultiTouchModeStates[0] = isInMultiTouchMode;
timeMachine.ClearStrokeHistory();
@@ -105,6 +107,8 @@ namespace Ink_Canvas
{
var timeMachineHistory = timeMachine.ExportTimeMachineHistory();
TimeMachineHistories[CurrentWhiteboardIndex] = timeMachineHistory;
// 保存多指书写模式状态
savedMultiTouchModeStates[CurrentWhiteboardIndex] = isInMultiTouchMode;
timeMachine.ClearStrokeHistory();
@@ -116,21 +120,41 @@ namespace Ink_Canvas
_currentCommitType = CommitReason.ClearingCanvas;
if (isErasedByCode) _currentCommitType = CommitReason.CodeInput;
// 取消任何UI元素的选择,隐藏拉伸控件
DeselectUIElement();
// 只清除笔画,不清除图片元素
// 图片元素的清除由调用方决定
inkCanvas.Strokes.Clear();
// 执行内存清理
PerformLightweightMemoryCleanup();
_currentCommitType = CommitReason.UserInput;
}
/// <summary>
/// 执行内存清理
/// </summary>
private void PerformLightweightMemoryCleanup()
{
Task.Run(() =>
{
GC.Collect();
});
}
// 恢复每页白板图片信息
private void RestoreStrokes(bool isBackupMain = false)
{
try
{
// 隐藏图片选择工具栏
if (currentSelectedElement != null)
{
// 保存当前编辑模式
var previousEditingMode = inkCanvas.EditingMode;
UnselectElement(currentSelectedElement);
// 恢复编辑模式
inkCanvas.EditingMode = previousEditingMode;
currentSelectedElement = null;
}
var targetIndex = isBackupMain ? 0 : CurrentWhiteboardIndex;
// 先清空当前画布的墨迹
@@ -151,19 +175,19 @@ namespace Ink_Canvas
{
timeMachine.ImportTimeMachineHistory(TimeMachineHistories[0]);
foreach (var item in TimeMachineHistories[0]) ApplyHistoryToCanvas(item);
// 恢复多指书写模式状态
RestoreMultiTouchModeState(0);
}
else
{
timeMachine.ImportTimeMachineHistory(TimeMachineHistories[CurrentWhiteboardIndex]);
// 通过时间机器历史恢复所有内容(墨迹和图片)
foreach (var item in TimeMachineHistories[CurrentWhiteboardIndex]) ApplyHistoryToCanvas(item);
// 恢复多指书写模式状态
RestoreMultiTouchModeState(CurrentWhiteboardIndex);
}
// 确保选中状态被清除,因为我们切换了页面
if (selectedUIElement != null)
{
DeselectUIElement();
}
}
catch
{
@@ -171,6 +195,39 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 恢复多指书写模式状态
/// </summary>
private void RestoreMultiTouchModeState(int pageIndex)
{
try
{
// 检查是否保存了多指书写模式状态
if (savedMultiTouchModeStates[pageIndex])
{
// 更新UI状态
if (ToggleSwitchEnableMultiTouchMode != null)
{
ToggleSwitchEnableMultiTouchMode.IsOn = true;
}
LogHelper.WriteLogToFile($"恢复多指书写模式状态 - 页面索引: {pageIndex}", LogHelper.LogType.Info);
}
else
{
// 更新UI状态
if (ToggleSwitchEnableMultiTouchMode != null)
{
ToggleSwitchEnableMultiTouchMode.IsOn = false;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复多指书写模式状态失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private async void BtnWhiteBoardPageIndex_Click(object sender, EventArgs e)
{
if (sender == BtnLeftPageListWB)
@@ -185,9 +242,12 @@ namespace Ink_Canvas
RefreshBlackBoardSidePageListView();
AnimationsHelper.ShowWithSlideFromBottomAndFade(BoardBorderLeftPageListView);
await Task.Delay(1);
ScrollViewToVerticalTop(
(ListViewItem)BlackBoardLeftSidePageListView.ItemContainerGenerator.ContainerFromIndex(
CurrentWhiteboardIndex - 1), BlackBoardLeftSidePageListScrollViewer);
var leftContainer = BlackBoardLeftSidePageListView.ItemContainerGenerator.ContainerFromIndex(
CurrentWhiteboardIndex - 1) as ListViewItem;
if (leftContainer != null)
{
ScrollViewToVerticalTop(leftContainer, BlackBoardLeftSidePageListScrollViewer);
}
}
}
else if (sender == BtnRightPageListWB)
@@ -202,9 +262,12 @@ namespace Ink_Canvas
RefreshBlackBoardSidePageListView();
AnimationsHelper.ShowWithSlideFromBottomAndFade(BoardBorderRightPageListView);
await Task.Delay(1);
ScrollViewToVerticalTop(
(ListViewItem)BlackBoardRightSidePageListView.ItemContainerGenerator.ContainerFromIndex(
CurrentWhiteboardIndex - 1), BlackBoardRightSidePageListScrollViewer);
var rightContainer = BlackBoardRightSidePageListView.ItemContainerGenerator.ContainerFromIndex(
CurrentWhiteboardIndex - 1) as ListViewItem;
if (rightContainer != null)
{
ScrollViewToVerticalTop(rightContainer, BlackBoardRightSidePageListScrollViewer);
}
}
}
@@ -214,8 +277,16 @@ namespace Ink_Canvas
{
if (CurrentWhiteboardIndex <= 1) return;
// 取消任何UI元素的选择
DeselectUIElement();
// 隐藏图片选择工具栏
if (currentSelectedElement != null)
{
// 保存当前编辑模式
var previousEditingMode = inkCanvas.EditingMode;
UnselectElement(currentSelectedElement);
// 恢复编辑模式
inkCanvas.EditingMode = previousEditingMode;
currentSelectedElement = null;
}
SaveStrokes();
@@ -235,13 +306,21 @@ namespace Ink_Canvas
inkCanvas.Strokes.Count > Settings.Automation.MinimumAutomationStrokeNumber) SaveScreenShot(true);
if (CurrentWhiteboardIndex >= WhiteboardTotalCount)
{
// 在最后一页时,点击新页面按钮直接新增一页
// 在最后一页时,点击"新页面"按钮直接新增一页
BtnWhiteBoardAdd_Click(sender, e);
return;
}
// 取消任何UI元素的选择
DeselectUIElement();
// 隐藏图片选择工具栏
if (currentSelectedElement != null)
{
// 保存当前编辑模式
var previousEditingMode = inkCanvas.EditingMode;
UnselectElement(currentSelectedElement);
// 恢复编辑模式
inkCanvas.EditingMode = previousEditingMode;
currentSelectedElement = null;
}
SaveStrokes();
@@ -259,8 +338,16 @@ namespace Ink_Canvas
if (Settings.Automation.IsAutoSaveStrokesAtClear &&
inkCanvas.Strokes.Count > Settings.Automation.MinimumAutomationStrokeNumber) SaveScreenShot(true);
// 取消任何UI元素的选择
DeselectUIElement();
// 隐藏图片选择工具栏
if (currentSelectedElement != null)
{
// 保存当前编辑模式
var previousEditingMode = inkCanvas.EditingMode;
UnselectElement(currentSelectedElement);
// 恢复编辑模式
inkCanvas.EditingMode = previousEditingMode;
currentSelectedElement = null;
}
SaveStrokes();
ClearStrokes(true);
@@ -290,6 +377,17 @@ namespace Ink_Canvas
private void BtnWhiteBoardDelete_Click(object sender, RoutedEventArgs e)
{
// 隐藏图片选择工具栏
if (currentSelectedElement != null)
{
// 保存当前编辑模式
var previousEditingMode = inkCanvas.EditingMode;
UnselectElement(currentSelectedElement);
// 恢复编辑模式
inkCanvas.EditingMode = previousEditingMode;
currentSelectedElement = null;
}
ClearStrokes(true);
if (CurrentWhiteboardIndex != WhiteboardTotalCount)
@@ -319,13 +417,25 @@ namespace Ink_Canvas
BtnLeftWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
BtnRightWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
// 始终允许点击“下一页/新页面”按钮(除非已达最大页数)
BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage;
if (isLastPage)
{
BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage;
}
else
{
BtnWhiteBoardSwitchNext.IsEnabled = true;
}
// 保持按钮常亮(高亮)
BtnLeftWhiteBoardSwitchNextGeometry.Brush = new SolidColorBrush(Color.FromArgb(255, 24, 24, 27));
// 获取主题颜色资源
var iconForegroundBrush = Application.Current.FindResource("IconForeground") as SolidColorBrush;
// 设置下一页按钮颜色
if (iconForegroundBrush != null)
{
BtnLeftWhiteBoardSwitchNextGeometry.Brush = iconForegroundBrush;
BtnRightWhiteBoardSwitchNextGeometry.Brush = iconForegroundBrush;
}
BtnLeftWhiteBoardSwitchNextLabel.Opacity = 1;
BtnRightWhiteBoardSwitchNextGeometry.Brush = new SolidColorBrush(Color.FromArgb(255, 24, 24, 27));
BtnRightWhiteBoardSwitchNextLabel.Opacity = 1;
BtnWhiteBoardSwitchPrevious.IsEnabled = true;
@@ -333,16 +443,23 @@ namespace Ink_Canvas
if (CurrentWhiteboardIndex == 1)
{
BtnWhiteBoardSwitchPrevious.IsEnabled = false;
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = new SolidColorBrush(Color.FromArgb(127, 24, 24, 27));
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;
}
BtnLeftWhiteBoardSwitchPreviousLabel.Opacity = 0.5;
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = new SolidColorBrush(Color.FromArgb(127, 24, 24, 27));
BtnRightWhiteBoardSwitchPreviousLabel.Opacity = 0.5;
}
else
{
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = new SolidColorBrush(Color.FromArgb(255, 24, 24, 27));
if (iconForegroundBrush != null)
{
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = iconForegroundBrush;
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = iconForegroundBrush;
}
BtnLeftWhiteBoardSwitchPreviousLabel.Opacity = 1;
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = new SolidColorBrush(Color.FromArgb(255, 24, 24, 27));
BtnRightWhiteBoardSwitchPreviousLabel.Opacity = 1;
}
+25 -21
View File
@@ -1,4 +1,4 @@
using Ink_Canvas.Helpers;
using Ink_Canvas.Helpers;
using System;
using System.Diagnostics;
using System.Windows;
@@ -61,7 +61,7 @@ namespace Ink_Canvas
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
// 设置为白板默认背景色
Color defaultWhiteboardColor = Color.FromRgb(234, 235, 237);
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
if (currentMode == 1) // 白板模式
{
@@ -135,7 +135,7 @@ namespace Ink_Canvas
{
Name = "BackgroundPalette",
Visibility = Visibility.Collapsed,
Background = new SolidColorBrush(Colors.White),
Background = (SolidColorBrush)Application.Current.FindResource("SettingsPageBackground"),
Opacity = 1,
BorderBrush = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)),
BorderThickness = new Thickness(1),
@@ -166,7 +166,7 @@ namespace Ink_Canvas
var titleText = new TextBlock
{
Text = "背景设置",
Foreground = new SolidColorBrush(Colors.White),
Foreground = (SolidColorBrush)Application.Current.FindResource("FloatBarForeground"),
Padding = new Thickness(0, 5, 0, 0),
FontSize = 11,
FontWeight = FontWeights.Bold,
@@ -198,7 +198,7 @@ namespace Ink_Canvas
var modeTitle = new TextBlock
{
Text = "白板模式",
Foreground = new SolidColorBrush(Color.FromRgb(0x17, 0x25, 0x54)),
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground"),
FontSize = 10,
FontWeight = FontWeights.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
@@ -233,7 +233,7 @@ namespace Ink_Canvas
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
// 设置为白板默认背景色
Color defaultWhiteboardColor = Color.FromRgb(234, 235, 237);
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
if (currentMode == 1) // 白板模式
{
@@ -319,7 +319,7 @@ namespace Ink_Canvas
var separator = new Border
{
Height = 1,
Background = new SolidColorBrush(Color.FromRgb(0xd4, 0xd4, 0xd8)),
Background = (SolidColorBrush)Application.Current.FindResource("SettingsPageBorderBrush"),
Margin = new Thickness(0, 12, 0, 12)
};
contentPanel.Children.Add(separator);
@@ -328,7 +328,7 @@ namespace Ink_Canvas
var colorTitle = new TextBlock
{
Text = "背景颜色",
Foreground = new SolidColorBrush(Color.FromRgb(0x17, 0x25, 0x54)),
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground"),
FontSize = 10,
FontWeight = FontWeights.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
@@ -342,7 +342,7 @@ namespace Ink_Canvas
Width = 100,
Height = 40,
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xd4, 0xd4, 0xd8)),
BorderBrush = (SolidColorBrush)Application.Current.FindResource("SettingsPageBorderBrush"),
Background = new SolidColorBrush(Colors.White),
CornerRadius = new CornerRadius(4),
Margin = new Thickness(0, 0, 0, 10),
@@ -378,7 +378,7 @@ namespace Ink_Canvas
// 先创建所有滑块控件
// 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 };
var rLabel = new TextBlock { Text = "R:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
var rSlider = new Slider
{
Minimum = 0,
@@ -393,12 +393,13 @@ namespace Ink_Canvas
Text = currentBackgroundColor.R.ToString(),
Width = 30,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Right
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 };
var gLabel = new TextBlock { Text = "G:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
var gSlider = new Slider
{
Minimum = 0,
@@ -413,12 +414,13 @@ namespace Ink_Canvas
Text = currentBackgroundColor.G.ToString(),
Width = 30,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Right
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 };
var bLabel = new TextBlock { Text = "B:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
var bSlider = new Slider
{
Minimum = 0,
@@ -433,7 +435,8 @@ namespace Ink_Canvas
Text = currentBackgroundColor.B.ToString(),
Width = 30,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Right
TextAlignment = TextAlignment.Right,
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
};
// 现在添加事件处理程序
@@ -704,7 +707,8 @@ namespace Ink_Canvas
forceEraser = false;
forcePointEraser = false;
drawingShapeMode = 0;
inkCanvas.EditingMode = InkCanvasEditingMode.Select;
// 使用集中化的工具模式切换方法
SetCurrentToolMode(InkCanvasEditingMode.Select);
SetCursorBasedOnEditingMode(inkCanvas);
}
@@ -715,16 +719,16 @@ namespace Ink_Canvas
//}
//else {
// 禁用高级橡皮擦系统
DisableAdvancedEraserSystem();
DisableEraserOverlay();
forceEraser = true;
forcePointEraser = false;
inkCanvas.EraserShape = new EllipseStylusShape(5, 5);
inkCanvas.EditingMode = InkCanvasEditingMode.EraseByStroke;
// 使用集中化的工具模式切换方法
SetCurrentToolMode(InkCanvasEditingMode.EraseByStroke);
drawingShapeMode = 0;
// 修复:切换到线擦时,确保重置笔的状态
penType = 0;
drawingAttributes.IsHighlighter = false;
drawingAttributes.StylusTip = StylusTip.Ellipse;
@@ -764,7 +768,7 @@ namespace Ink_Canvas
{
PenIcon_Click(null, null);
SymbolIconDelete_MouseUp(null, null);
if (Settings.Canvas.ClearCanvasAndClearTimeMachine == false) timeMachine.ClearStrokeHistory();
if (!Settings.Canvas.ClearCanvasAndClearTimeMachine) timeMachine.ClearStrokeHistory();
// 根据设置决定是否清空图片
if (Settings.Canvas.ClearCanvasAlsoClearImages)
@@ -853,4 +857,4 @@ namespace Ink_Canvas
}
}
}
}
}
+79 -19
View File
@@ -3,15 +3,24 @@ using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Forms;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Clipboard = System.Windows.Clipboard;
using ContextMenu = System.Windows.Controls.ContextMenu;
using Cursors = System.Windows.Input.Cursors;
using MenuItem = System.Windows.Controls.MenuItem;
namespace Ink_Canvas
{
public partial class MainWindow : Window
{
private bool isClipboardMonitoringEnabled = false;
private BitmapSource lastClipboardImage = null;
private bool isClipboardMonitoringEnabled;
private BitmapSource lastClipboardImage;
// 初始化剪贴板监控
private void InitializeClipboardMonitoring()
@@ -90,7 +99,7 @@ namespace Ink_Canvas
// 显示菜单
contextMenu.IsOpen = true;
contextMenu.PlacementTarget = inkCanvas;
contextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
contextMenu.Placement = PlacementMode.MousePoint;
}
catch (Exception ex)
{
@@ -122,36 +131,87 @@ namespace Ink_Canvas
Source = clipboardImage,
Width = clipboardImage.PixelWidth,
Height = clipboardImage.PixelHeight,
Stretch = System.Windows.Media.Stretch.Fill
Stretch = Stretch.Fill
};
// 生成唯一名称
string timestamp = "img_clipboard_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
image.Name = timestamp;
// 设置位置
if (position.HasValue)
// 初始化TransformGroup
if (image is FrameworkElement element)
{
// 在指定位置居中显示
InkCanvas.SetLeft(image, position.Value.X - image.Width / 2);
InkCanvas.SetTop(image, position.Value.Y - image.Height / 2);
var transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform(1, 1));
transformGroup.Children.Add(new TranslateTransform(0, 0));
transformGroup.Children.Add(new RotateTransform(0));
element.RenderTransform = transformGroup;
}
else
// 设置图片属性,避免被InkCanvas选择系统处理
image.IsHitTestVisible = true;
image.Focusable = false;
// 初始化InkCanvas选择设置
if (inkCanvas != null)
{
// 使用与文件选择相同的居中和缩放逻辑
CenterAndScaleElement(image);
// 清除当前选择,避免显示控制点
inkCanvas.Select(new StrokeCollection());
// 设置编辑模式为非选择模式,这样可以保持图片的交互功能
// 同时通过图片的IsHitTestVisible和Focusable属性来避免InkCanvas选择系统的干扰
inkCanvas.EditingMode = InkCanvasEditingMode.None;
}
// 添加到画布
inkCanvas.Children.Add(image);
// 添加鼠标事件处理
image.MouseDown += UIElement_MouseDown;
image.IsManipulationEnabled = true;
// 等待图片加载完成后再进行居中处理
image.Loaded += (sender, e) =>
{
// 确保在UI线程中执行
Dispatcher.BeginInvoke(new Action(() =>
{
// 先进行缩放居中处理
CenterAndScaleElement(image);
// 如果有指定位置,调整到指定位置
if (position.HasValue)
{
// 在指定位置居中显示
InkCanvas.SetLeft(image, position.Value.X - image.Width / 2);
InkCanvas.SetTop(image, position.Value.Y - image.Height / 2);
}
// 绑定事件处理器
if (image is FrameworkElement elementForEvents)
{
// 鼠标事件
elementForEvents.MouseLeftButtonDown += Element_MouseLeftButtonDown;
elementForEvents.MouseLeftButtonUp += Element_MouseLeftButtonUp;
elementForEvents.MouseMove += Element_MouseMove;
elementForEvents.MouseWheel += Element_MouseWheel;
// 触摸事件
elementForEvents.TouchDown += Element_TouchDown;
elementForEvents.TouchUp += Element_TouchUp;
elementForEvents.IsManipulationEnabled = true;
elementForEvents.ManipulationDelta += Element_ManipulationDelta;
elementForEvents.ManipulationCompleted += Element_ManipulationCompleted;
// 设置光标
elementForEvents.Cursor = Cursors.Hand;
}
}), DispatcherPriority.Loaded);
};
// 提交到历史记录
timeMachine.CommitElementInsertHistory(image);
// 插入图片后切换到选择模式并刷新浮动栏高光显示
SetCurrentToolMode(InkCanvasEditingMode.Select);
UpdateCurrentToolMode("select");
HideSubPanels("select");
ShowNotification("图片已从剪贴板粘贴");
}
catch (Exception ex)
@@ -185,7 +245,7 @@ namespace Ink_Canvas
}
// 处理全局粘贴快捷键
private async void HandleGlobalPaste(object sender, ExecutedRoutedEventArgs e)
internal async void HandleGlobalPaste(object sender, ExecutedRoutedEventArgs e)
{
try
{
@@ -226,13 +286,13 @@ namespace Ink_Canvas
{
public static event Action ClipboardUpdate;
private static System.Windows.Forms.Timer clipboardTimer;
private static Timer clipboardTimer;
private static string lastClipboardText = "";
private static bool lastHadImage = false;
private static bool lastHadImage;
static ClipboardNotification()
{
clipboardTimer = new System.Windows.Forms.Timer();
clipboardTimer = new Timer();
clipboardTimer.Interval = 500; // 每500ms检查一次
clipboardTimer.Tick += CheckClipboard;
clipboardTimer.Start();
+15 -3
View File
@@ -18,7 +18,10 @@ namespace Ink_Canvas
private void ColorSwitchCheck()
{
HideSubPanels("color");
if (penType != 1)
{
HideSubPanels("color");
}
if (GridTransparencyFakeBackground.Background == Brushes.Transparent)
{
if (currentMode == 1)
@@ -28,6 +31,11 @@ namespace Ink_Canvas
AnimationsHelper.HideWithSlideAndFade(BlackboardLeftSide);
AnimationsHelper.HideWithSlideAndFade(BlackboardCenterSide);
AnimationsHelper.HideWithSlideAndFade(BlackboardRightSide);
// 在PPT模式下隐藏手势面板和手势按钮
AnimationsHelper.HideWithSlideAndFade(TwoFingerGestureBorder);
AnimationsHelper.HideWithSlideAndFade(BoardTwoFingerGestureBorder);
EnableTwoFingerGestureBorder.Visibility = Visibility.Collapsed;
}
BtnHideInkCanvas_Click(BtnHideInkCanvas, null);
@@ -59,7 +67,8 @@ namespace Ink_Canvas
{
inkCanvas.IsManipulationEnabled = true;
drawingShapeMode = 0;
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
// 使用集中化的工具模式切换方法
SetCurrentToolMode(InkCanvasEditingMode.Ink);
CancelSingleFingerDragMode();
CheckColorTheme();
}
@@ -417,7 +426,7 @@ namespace Ink_Canvas
}
// 更新快捷调色盘选择指示器
if (penType == 0)
if (penType == 0)
{
UpdateQuickColorPaletteIndicator(inkCanvas.DefaultDrawingAttributes.Color);
}
@@ -595,6 +604,9 @@ namespace Ink_Canvas
drawingAttributes.Height = Settings.Canvas.HighlighterWidth;
drawingAttributes.StylusTip = StylusTip.Rectangle;
drawingAttributes.IsHighlighter = true;
// 确保荧光笔模式切换后正确更新颜色和快捷调色板指示器
ColorSwitchCheck();
}
private void BtnColorBlack_Click(object sender, RoutedEventArgs e)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+43 -33
View File
@@ -1,36 +1,46 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DrawingGroup x:Key="EraserDrawingGroup" ClipGeometry="M0,0 V56 H38 V0 H0 Z">
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M38,56z M0,0z M0,4C0,1.79086,1.79086,0,4,0L34,0C36.2091,0,38,1.79086,38,4L38,52C38,54.2091,36.2091,56,34,56L4,56C1.79086,56,0,54.2091,0,52L0,4z" />
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M38,56z M0,0z M34,1L4,1C2.34315,1,1,2.34315,1,4L1,52C1,53.6569,2.34315,55,4,55L34,55C35.6569,55,37,53.6569,37,52L37,4C37,2.34315,35.6569,1,34,1z M4,0C1.79086,0,0,1.79086,0,4L0,52C0,54.2091,1.79086,56,4,56L34,56C36.2091,56,38,54.2091,38,52L38,4C38,1.79086,36.2091,0,34,0L4,0z" />
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M12,19.5C12,18.1193,13.1193,17,14.5,17L14.5,17C15.8807,17,17,18.1193,17,19.5L17,36.5C17,37.8807,15.8807,39,14.5,39L14.5,39C13.1193,39,12,37.8807,12,36.5L12,19.5z" />
<GeometryDrawing Geometry="F0 M38,56z M0,0z M11.5,19.5C11.5,17.8431 12.8431,16.5 14.5,16.5 16.1569,16.5 17.5,17.8431 17.5,19.5L17.5,36.5C17.5,38.1569 16.1569,39.5 14.5,39.5 12.8431,39.5 11.5,38.1569 11.5,36.5L11.5,19.5z M14.5,17.5C13.3954,17.5,12.5,18.3954,12.5,19.5L12.5,36.5C12.5,37.6046 13.3954,38.5 14.5,38.5 15.6046,38.5 16.5,37.6046 16.5,36.5L16.5,19.5C16.5,18.3954,15.6046,17.5,14.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
<GeometryDrawing Geometry="F0 M38,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup>
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 矩形橡皮擦图像资源 -->
<DrawingImage x:Key="RectangleEraserImageSource">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V56 H38 V0 H0 Z">
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M38,56z M0,0z M0,4C0,1.79086,1.79086,0,4,0L34,0C36.2091,0,38,1.79086,38,4L38,52C38,54.2091,36.2091,56,34,56L4,56C1.79086,56,0,54.2091,0,52L0,4z" />
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M38,56z M0,0z M34,1L4,1C2.34315,1,1,2.34315,1,4L1,52C1,53.6569,2.34315,55,4,55L34,55C35.6569,55,37,53.6569,37,52L37,4C37,2.34315,35.6569,1,34,1z M4,0C1.79086,0,0,1.79086,0,4L0,52C0,54.2091,1.79086,56,4,56L34,56C36.2091,56,38,54.2091,38,52L38,4C38,1.79086,36.2091,0,34,0L4,0z" />
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M12,19.5C12,18.1193,13.1193,17,14.5,17L14.5,17C15.8807,17,17,18.1193,17,19.5L17,36.5C17,37.8807,15.8807,39,14.5,39L14.5,39C13.1193,39,12,37.8807,12,36.5L12,19.5z" />
<GeometryDrawing Geometry="F0 M38,56z M0,0z M11.5,19.5C11.5,17.8431 12.8431,16.5 14.5,16.5 16.1569,16.5 17.5,17.8431 17.5,19.5L17.5,36.5C17.5,38.1569 16.1569,39.5 14.5,39.5 12.8431,39.5 11.5,38.1569 11.5,36.5L11.5,19.5z M14.5,17.5C13.3954,17.5,12.5,18.3954,12.5,19.5L12.5,36.5C12.5,37.6046 13.3954,38.5 14.5,38.5 15.6046,38.5 16.5,37.6046 16.5,36.5L16.5,19.5C16.5,18.3954,15.6046,17.5,14.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
<GeometryDrawing Geometry="F0 M38,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingGroup x:Key="EraserCircleDrawingGroup" ClipGeometry="M0,0 V56 H56 V0 H0 Z">
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M56,56z M0,0z M0,28C0,12.536,12.536,0,28,0L28,0C43.464,0,56,12.536,56,28L56,28C56,43.464,43.464,56,28,56L28,56C12.536,56,0,43.464,0,28L0,28z" />
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M56,56z M0,0z M1,28C1,42.9117 13.0883,55 28,55 42.9117,55 55,42.9117 55,28 55,13.0883 42.9117,1 28,1 13.0883,1 1,13.0883 1,28z M28,0C12.536,0 0,12.536 0,28 0,43.464 12.536,56 28,56 43.464,56 56,43.464 56,28 56,12.536 43.464,0 28,0z" />
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
<GeometryDrawing Geometry="F0 M56,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M30,19.5C30,18.1193,31.1193,17,32.5,17L32.5,17C33.8807,17,35,18.1193,35,19.5L35,36.5C35,37.8807,33.8807,39,32.5,39L32.5,39C31.1193,39,30,37.8807,30,36.5L30,19.5z" />
<GeometryDrawing Geometry="F0 M56,56z M0,0z M29.5,19.5C29.5,17.8431 30.8431,16.5 32.5,16.5 34.1569,16.5 35.5,17.8431 35.5,19.5L35.5,36.5C35.5,38.1569 34.1569,39.5 32.5,39.5 30.8431,39.5 29.5,38.1569 29.5,36.5L29.5,19.5z M32.5,17.5C31.3954,17.5,30.5,18.3954,30.5,19.5L30.5,36.5C30.5,37.6046 31.3954,38.5 32.5,38.5 33.6046,38.5 34.5,37.6046 34.5,36.5L34.5,19.5C34.5,18.3954,33.6046,17.5,32.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup>
<!-- 圆形橡皮擦图像资源 -->
<DrawingImage x:Key="EllipseEraserImageSource">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V56 H56 V0 H0 Z">
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M56,56z M0,0z M0,28C0,12.536,12.536,0,28,0L28,0C43.464,0,56,12.536,56,28L56,28C56,43.464,43.464,56,28,56L28,56C12.536,56,0,43.464,0,28L0,28z" />
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M56,56z M0,0z M1,28C1,42.9117 13.0883,55 28,55 42.9117,55 55,42.9117 55,28 55,13.0883 42.9117,1 28,1 13.0883,1 1,13.0883 1,28z M28,0C12.536,0 0,12.536 0,28 0,43.464 12.536,56 28,56 43.464,56 56,43.464 56,28 56,12.536 43.464,0 28,0z" />
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
<GeometryDrawing Geometry="F0 M56,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M30,19.5C30,18.1193,31.1193,17,32.5,17L32.5,17C33.8807,17,35,18.1193,35,19.5L35,36.5C35,37.8807,33.8807,39,32.5,39L32.5,39C31.1193,39,30,37.8807,30,36.5L30,19.5z" />
<GeometryDrawing Geometry="F0 M56,56z M0,0z M29.5,19.5C29.5,17.8431 30.8431,16.5 32.5,16.5 34.1569,16.5 35.5,17.8431 35.5,19.5L35.5,36.5C35.5,38.1569 34.1569,39.5 32.5,39.5 30.8431,39.5 29.5,38.1569 29.5,36.5L29.5,19.5z M32.5,17.5C31.3954,17.5,30.5,18.3954,30.5,19.5L30.5,36.5C30.5,37.6046 31.3954,38.5 32.5,38.5 33.6046,38.5 34.5,37.6046 34.5,36.5L34.5,19.5C34.5,18.3954,33.6046,17.5,32.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</ResourceDictionary>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,358 @@
using Ink_Canvas.Helpers;
using iNKORE.UI.WPF.Modern.Controls;
using System;
using System.Linq;
using System.Windows;
namespace Ink_Canvas
{
public partial class MainWindow : Window
{
#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)
{
LogHelper.WriteLogToFile($"初始化悬浮窗拦截管理器失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <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();
}));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理窗口拦截事件失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 窗口被恢复事件处理
/// </summary>
private void OnFloatingWindowRestored(object sender, FloatingWindowInterceptor.WindowRestoredEventArgs e)
{
try
{
// 在UI线程中更新状态
Dispatcher.BeginInvoke(new Action(() =>
{
UpdateFloatingWindowInterceptorUI();
}));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理窗口恢复事件失败: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region
/// <summary>
/// 主开关切换事件
/// </summary>
private void ToggleSwitchFloatingWindowInterceptorEnabled_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
try
{
Settings.Automation.FloatingWindowInterceptor.IsEnabled = ToggleSwitchFloatingWindowInterceptorEnabled.IsOn;
if (_floatingWindowInterceptorManager != null)
{
if (Settings.Automation.FloatingWindowInterceptor.IsEnabled)
{
_floatingWindowInterceptorManager.Start();
}
else
{
_floatingWindowInterceptorManager.Stop();
}
}
UpdateFloatingWindowInterceptorUI();
SaveSettingsToFile();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"切换悬浮窗拦截主开关失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 希沃白板3拦截开关
/// </summary>
private void ToggleSwitchSeewoWhiteboard3Floating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard3Floating, ToggleSwitchSeewoWhiteboard3Floating.IsOn);
}
/// <summary>
/// 希沃白板5拦截开关
/// </summary>
private void ToggleSwitchSeewoWhiteboard5Floating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5Floating, ToggleSwitchSeewoWhiteboard5Floating.IsOn);
}
/// <summary>
/// 希沃白板5C拦截开关
/// </summary>
private void ToggleSwitchSeewoWhiteboard5CFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5CFloating, ToggleSwitchSeewoWhiteboard5CFloating.IsOn);
}
/// <summary>
/// 希沃品课侧栏拦截开关
/// </summary>
private void ToggleSwitchSeewoPincoSideBarFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoSideBarFloating, ToggleSwitchSeewoPincoSideBarFloating.IsOn);
}
/// <summary>
/// 希沃品课画笔拦截开关
/// </summary>
private void ToggleSwitchSeewoPincoDrawingFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoDrawingFloating, ToggleSwitchSeewoPincoDrawingFloating.IsOn);
}
/// <summary>
/// 希沃PPT小工具拦截开关
/// </summary>
private void ToggleSwitchSeewoPPTFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPPTFloating, ToggleSwitchSeewoPPTFloating.IsOn);
}
/// <summary>
/// AiClass拦截开关
/// </summary>
private void ToggleSwitchAiClassFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.AiClassFloating, ToggleSwitchAiClassFloating.IsOn);
}
/// <summary>
/// 鸿合屏幕书写拦截开关
/// </summary>
private void ToggleSwitchHiteAnnotationFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.HiteAnnotationFloating, ToggleSwitchHiteAnnotationFloating.IsOn);
}
/// <summary>
/// 畅言智慧课堂拦截开关
/// </summary>
private void ToggleSwitchChangYanFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanFloating, ToggleSwitchChangYanFloating.IsOn);
}
/// <summary>
/// 畅言PPT拦截开关
/// </summary>
private void ToggleSwitchChangYanPptFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanPptFloating, ToggleSwitchChangYanPptFloating.IsOn);
}
/// <summary>
/// 天喻教育云拦截开关
/// </summary>
private void ToggleSwitchIntelligentClassFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.IntelligentClassFloating, ToggleSwitchIntelligentClassFloating.IsOn);
}
/// <summary>
/// 希沃桌面画笔拦截开关
/// </summary>
private void ToggleSwitchSeewoDesktopAnnotationFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopAnnotationFloating, ToggleSwitchSeewoDesktopAnnotationFloating.IsOn);
}
/// <summary>
/// 希沃桌面侧栏拦截开关
/// </summary>
private void ToggleSwitchSeewoDesktopSideBarFloating_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopSideBarFloating, ToggleSwitchSeewoDesktopSideBarFloating.IsOn);
}
/// <summary>
/// 设置拦截规则
/// </summary>
private void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
{
try
{
if (_floatingWindowInterceptorManager != null)
{
_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)
{
var childRuleName = childType.ToString();
if (Settings.Automation.FloatingWindowInterceptor.InterceptRules.ContainsKey(childRuleName))
{
Settings.Automation.FloatingWindowInterceptor.InterceptRules[childRuleName] = enabled;
}
}
}
// 如果是子规则,更新父规则的设置
else if (rule.ParentType.HasValue)
{
var parentRule = _floatingWindowInterceptorManager?.GetInterceptRule(rule.ParentType.Value);
if (parentRule != null)
{
var parentRuleName = rule.ParentType.Value.ToString();
if (Settings.Automation.FloatingWindowInterceptor.InterceptRules.ContainsKey(parentRuleName))
{
Settings.Automation.FloatingWindowInterceptor.InterceptRules[parentRuleName] = parentRule.IsEnabled;
}
}
}
}
// 更新UI显示
UpdateFloatingWindowInterceptorUI();
SaveSettingsToFile();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"设置拦截规则失败: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
}
}
+28 -20
View File
@@ -7,31 +7,31 @@ namespace Ink_Canvas
{
private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (StackPanelPPTControls.Visibility != Visibility.Visible || currentMode != 0) return;
if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return;
if (e.Delta >= 120)
BtnPPTSlidesUp_Click(BtnPPTSlidesUp, null);
else if (e.Delta <= -120) BtnPPTSlidesDown_Click(BtnPPTSlidesDown, null);
{
BtnPPTSlidesUp_Click(null, null);
}
else if (e.Delta <= -120)
{
BtnPPTSlidesDown_Click(null, null);
}
}
private void Main_Grid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (StackPanelPPTControls.Visibility != Visibility.Visible || currentMode != 0) return;
if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return;
if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N ||
e.Key == Key.Space) BtnPPTSlidesDown_Click(BtnPPTSlidesDown, null);
if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N || e.Key == Key.Space)
{
BtnPPTSlidesDown_Click(null, null);
}
if (e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Left || e.Key == Key.P)
BtnPPTSlidesUp_Click(BtnPPTSlidesUp, null);
{
BtnPPTSlidesUp_Click(null, null);
}
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape) KeyExit(null, null);
}
private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
private void HotKey_Undo(object sender, ExecutedRoutedEventArgs e)
{
@@ -57,7 +57,7 @@ namespace Ink_Canvas
}
private void KeyExit(object sender, ExecutedRoutedEventArgs e)
internal void KeyExit(object sender, ExecutedRoutedEventArgs e)
{
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible) BtnPPTSlideShowEnd_Click(BtnPPTSlideShowEnd, null);
}
@@ -67,10 +67,18 @@ namespace Ink_Canvas
PenIcon_Click(lastBorderMouseDownObject, null);
}
private void KeyChangeToQuitDrawTool(object sender, ExecutedRoutedEventArgs e)
internal void KeyChangeToQuitDrawTool(object sender, ExecutedRoutedEventArgs e)
{
if (currentMode != 0) ImageBlackboard_MouseUp(lastBorderMouseDownObject, null);
CursorIcon_Click(lastBorderMouseDownObject, null);
if (currentMode != 0)
{
// 在白板模式下,alt+q 退出白板模式
ImageBlackboard_MouseUp(lastBorderMouseDownObject, null);
}
else
{
// 在非白板模式下,alt+q 切换到鼠标模式
CursorIcon_Click(lastBorderMouseDownObject, null);
}
}
private void KeyChangeToSelect(object sender, ExecutedRoutedEventArgs e)
+34
View File
@@ -5,6 +5,9 @@
public static string LinedCursorIcon =
"F1 M24,24z M0,0z M5.72106,15.9716L3.71327,3.00395C3.6389,2.6693 3.65747,2.41831 3.76902,2.25099 3.88057,2.08366 4.0479,2 4.271,2 4.4941,2 4.71711,2.07437 4.94021,2.2231 6.72502,3.39438 9.28149,5.10481 12.6094,7.3544 15.677,9.45526 18.1125,11.1285 19.9159,12.3742 20.1204,12.5229 20.2505,12.6995 20.3062,12.904 20.362,13.1085 20.3249,13.2944 20.1947,13.4618 20.0832,13.6105 19.8973,13.6849 19.637,13.6849L13.3902,13.6849 17.6291,19.7365C17.722,19.8666 17.75,20.0153 17.7128,20.1827 17.6942,20.3314 17.6198,20.4522 17.4897,20.5452L15.5654,21.8838C15.4353,21.9768 15.2865,22.0139 15.1192,21.9953 14.9704,21.9582 14.8496,21.8745 14.7566,21.7444L10.2389,15.2745 7.58956,19.9038C7.45942,20.1269 7.30144,20.2756 7.11552,20.35 6.92961,20.4058 6.75292,20.3872 6.5856,20.2942 6.43686,20.2013 6.34392,20.0339 6.30673,19.7922L6.00007,17.8959C5.88852,17.0779,5.79543,16.4364,5.72106,15.9716z";
public static string SolidCursorIcon =
"F1 M24,24z M0,0z M5.72106,15.9716L3.71327,3.00395C3.6389,2.6693 3.65747,2.41831 3.76902,2.25099 3.88057,2.08366 4.0479,2 4.271,2 4.4941,2 4.71711,2.07437 4.94021,2.2231 6.72502,3.39438 9.28149,5.10481 12.6094,7.3544 15.677,9.45526 18.1125,11.1285 19.9159,12.3742 20.1204,12.5229 20.2505,12.6995 20.3062,12.904 20.362,13.1085 20.3249,13.2944 20.1947,13.4618 20.0832,13.6105 19.8973,13.6849 19.637,13.6849L13.3902,13.6849 17.6291,19.7365C17.722,19.8666 17.75,20.0153 17.7128,20.1827 17.6942,20.3314 17.6198,20.4522 17.4897,20.5452L15.5654,21.8838C15.4353,21.9768 15.2865,22.0139 15.1192,21.9953 14.9704,21.9582 14.8496,21.8745 14.7566,21.7444L10.2389,15.2745 7.58956,19.9038C7.45942,20.1269 7.30144,20.2756 7.11552,20.35 6.92961,20.4058 6.75292,20.3872 6.5856,20.2942 6.43686,20.2013 6.34392,20.0339 6.30673,19.7922L6.00007,17.8959C5.88852,17.0779,5.79543,16.4364,5.72106,15.9716z";
public static string LinedPenIcon =
"F1 M24,24z M0,0z M16.996,2.34419L21.6823,7.00397C21.8941,7.23343 22,7.49819 22,7.79825 22,8.09831 21.8941,8.35425 21.6823,8.56606L10.8271,19.4212 4.57877,13.1994 15.4339,2.34419C15.6457,2.11473 15.9017,2 16.2018,2 16.5195,2 16.7842,2.11473 16.996,2.34419z M9.63571,20.5862L9.50333,20.6391 2.6725,21.9894C2.47834,22.0247 2.31066,21.9718 2.16946,21.8306 2.02825,21.707 1.97529,21.5481 2.01059,21.354L3.38736,14.5232C3.38736,14.4879,3.40502,14.4349,3.44032,14.3643L9.63571,20.5862z";
@@ -37,5 +40,36 @@
public static string EnabledGestureIconBadgeCheck =
"M22.74,18.2234C22.74,20.8888 20.5793,23.0494 17.914,23.0494 15.2487,23.0494 13.088,20.8888 13.088,18.2234 13.088,15.5581 15.2487,13.3975 17.914,13.3975 20.5793,13.3975 22.74,15.5581 22.74,18.2234z M21.1673,15.8009C21.4651,16.0889,21.473,16.5637,21.1851,16.8614L17.5425,20.6282C17.4012,20.7743 17.2066,20.8568 17.0034,20.8568 16.8001,20.8568 16.6055,20.7743 16.4642,20.6282L14.6429,18.7448C14.355,18.447 14.3629,17.9722 14.6607,17.6843 14.9585,17.3963 15.4333,17.4043 15.7212,17.7021L17.0034,19.0279 20.1068,15.8187C20.3947,15.5209,20.8695,15.513,21.1673,15.8009z";
// 老版浮动栏按钮图标
public static string LegacyLinedCursorIcon =
"F0 M24,24z M0,0z M3.85151,2.7073C3.52422,2.57095 3.147,2.64558 2.89629,2.89629 2.64558,3.147 2.57095,3.52422 2.7073,3.85151L9.7773,20.8215C9.91729,21.1575 10.2507,21.3718 10.6145,21.3595 10.9783,21.3473 11.2965,21.1111 11.4135,20.7664L13.4711,14.7085 18.8963,20.1337C19.238,20.4754 19.792,20.4754 20.1337,20.1337 20.4754,19.792 20.4754,19.238 20.1337,18.8963L14.7085,13.4711 20.7664,11.4135C21.1111,11.2965 21.3473,10.9783 21.3595,10.6145 21.3718,10.2507 21.1575,9.91729 20.8215,9.7773L3.85151,2.7073z M10.5017,18.0097L5.13984,5.13984 18.0097,10.5017 12.8136,12.2665C12.5561,12.3539,12.3539,12.5561,12.2665,12.8136L10.5017,18.0097z";
public static string LegacySolidCursorIcon =
"F0 M24,24z M0,0z M2.89629,2.89629C3.147,2.64558,3.52422,2.57095,3.85151,2.7073L20.8215,9.7773C21.1575,9.91729 21.3718,10.2507 21.3595,10.6145 21.3473,10.9783 21.1111,11.2965 20.7664,11.4135L14.7085,13.4711 20.1337,18.8963C20.4754,19.238 20.4754,19.792 20.1337,20.1337 19.792,20.4754 19.238,20.4754 18.8963,20.1337L13.4711,14.7085 11.4135,20.7664C11.2965,21.1111 10.9783,21.3473 10.6145,21.3595 10.2507,21.3718 9.91729,21.1575 9.7773,20.8215L2.7073,3.85151C2.57095,3.52422,2.64558,3.147,2.89629,2.89629z";
public static string LegacyLinedPenIcon =
"F0 M24,24z M0,0z M18.7033,4.39761C18.4948,4.31644 18.2714,4.27922 18.0473,4.28846 17.8233,4.29771 17.6038,4.3532 17.403,4.4512 17.2022,4.54919 17.0246,4.68744 16.8813,4.8568 16.8665,4.87422 16.8511,4.89102 16.8349,4.90716L15.7108,6.03131 17.9591,8.27962 19.0832,7.15546C19.1021,7.13662 19.1218,7.11869 19.1424,7.10176 19.3143,6.96037 19.4543,6.7853 19.5537,6.58793 19.6531,6.39058 19.7099,6.1751 19.7207,5.95519 19.7314,5.73528 19.6959,5.51545 19.6163,5.30962 19.5367,5.10378 19.4147,4.91625 19.2576,4.75914 19.1004,4.60201 18.9117,4.47877 18.7033,4.39761z M16.7944,9.44428L14.5461,7.19597 5.47079,16.2713 4.62767,19.3627 7.7191,18.5196 16.7944,9.44428z M13.9636,5.44913L4.15148,15.2613C4.05014,15.3626,3.977,15.4886,3.93929,15.6269L2.65942,20.3198C2.58166,20.6049 2.66264,20.9098 2.87161,21.1188 3.08059,21.3277 3.38551,21.4087 3.67063,21.331L8.36347,20.0511C8.50174,20.0134,8.62777,19.9402,8.72911,19.8389L20.2217,8.34636C20.5551,8.06468 20.8283,7.71873 21.0247,7.3289 21.2275,6.92628 21.3437,6.48586 21.3658,6.03572 21.3878,5.58559 21.3151,5.13594 21.1525,4.71552 20.99,4.29512 20.7411,3.91338 20.4222,3.59447 20.1033,3.27558 19.7214,3.0265 19.3009,2.86277 18.8804,2.69905 18.4304,2.62417 17.9794,2.64278 17.5285,2.66139 17.0862,2.77308 16.6807,2.97095 16.2862,3.16344 15.9348,3.43348 15.6478,3.76494L13.9636,5.44913z";
public static string LegacySolidPenIcon =
"F1 M24,24z M0,0z M19.3332,2.85933C18.9193,2.69814 18.4762,2.62442 18.0322,2.64274 17.5882,2.66106 17.1527,2.77103 16.7535,2.96583 16.3643,3.15575 16.0177,3.42232 15.7349,3.74956L14.5672,4.91725 19.0731,9.4231 20.2373,8.25888C20.5666,7.98121 20.8364,7.63993 21.0302,7.25528 21.2298,6.85899 21.3442,6.42551 21.3659,5.98249 21.3876,5.53947 21.3161,5.09692 21.1561,4.68313 20.996,4.26934 20.7511,3.89359 20.4372,3.57966 20.1232,3.26574 19.7472,3.02052 19.3332,2.85933z M18.0085,10.4877L13.5026,5.98183 4.14128,15.3432C4.04864,15.4358,3.98179,15.551,3.94732,15.6774L2.65684,20.4091C2.58577,20.6698 2.65979,20.9485 2.8508,21.1395 3.04182,21.3305 3.32054,21.4045 3.58117,21.3335L8.3129,20.043C8.43929,20.0085,8.5545,19.9417,8.64713,19.849L18.0085,10.4877z";
public static string LegacyLinedEraserStrokeIcon =
"F0 M25,24z M0,0z M7.32029,21.36L13.0098,21.36 13.0122,21.36 21.5471,21.36C21.989,21.36 22.3473,21.0017 22.3473,20.5598 22.3473,20.1179 21.989,19.7596 21.5471,19.7596L14.9429,19.7596 21.4352,13.2673C22.7372,12.0786,22.6872,10.1353,21.449,8.89707L16.1515,3.59952C14.9628,2.29751,13.0195,2.34754,11.7813,3.58572L2.68992,12.6771C1.3879,13.8657,1.43793,15.8091,2.67611,17.0473L6.75447,21.1256C6.90453,21.2757,7.10807,21.36,7.32029,21.36z M14.9771,4.68685C14.4571,4.10907,13.5664,4.06392,12.9129,4.71737L6.55503,11.0753 13.9595,18.4797 20.3174,12.1218C20.3273,12.1119 20.3375,12.1022 20.3479,12.0929 20.9257,11.5729 20.9708,10.6822 20.3174,10.0287L15.006,4.71737C14.9961,4.70745,14.9864,4.69727,14.9771,4.68685z M12.8278,19.6114L5.42338,12.2069 3.80776,13.8225C3.79784,13.8324 3.78766,13.8421 3.77724,13.8515 3.19947,14.3715 3.15431,15.2622 3.80776,15.9156L7.65174,19.7596 12.6796,19.7596 12.8278,19.6114z";
public static string LegacySolidEraserStrokeIcon =
"F1 M24,24z M0,0z M11.6199,3.61372C12.8916,2.34202,14.8995,2.2837,16.1307,3.62964L21.3433,8.84225C22.615,10.1139,22.6733,12.1218,21.3274,13.353L15.1877,19.4927 5.46434,9.76928 11.6199,3.61372z M7.33167,21.36C7.08919,21.36 6.86831,21.2676 6.70232,21.116 6.69184,21.1064 6.68155,21.0966 6.67147,21.0865L2.65671,17.0718C1.385,15.8001,1.32668,13.7922,2.67262,12.561L4.14394,11.0897 12.5469,19.4927 21.3367,19.4927C21.8523,19.4927 22.2703,19.9107 22.2703,20.4263 22.2703,20.942 21.8523,21.36 21.3367,21.36L7.33167,21.36z";
public static string LegacyLinedEraserCircleIcon =
"F0 M25,24z M0,0z M2.47995,17.1206L6.56736,21.208C6.57733,21.218 6.58749,21.2277 6.59783,21.237 6.66429,21.2971 6.7405,21.3466 6.82381,21.3829L6.83712,21.3885C6.84698,21.3926 6.85693,21.3965 6.86698,21.4003 6.86818,21.4007 6.86937,21.4011 6.87057,21.4016 6.94576,21.4289 7.02412,21.4451 7.10303,21.45L7.12183,21.451 7.13076,21.4513 7.13345,21.4514 7.15549,21.4517 17.0847,21.4517C17.5973,22.3438 18.5597,22.9445 19.6624,22.9445 21.3031,22.9445 22.6332,21.6144 22.6332,19.9737 22.6332,18.3329 21.3031,17.0028 19.6624,17.0028 18.0839,17.0028 16.793,18.2338 16.6972,19.7882L14.8669,19.7882 21.3224,13.3327C22.6404,12.1289,22.5884,10.1619,21.3367,8.91021L16.0278,3.60138C14.8241,2.28336,12.8571,2.33535,11.6053,3.58706L2.49426,12.6981C1.17625,13.9019,1.22824,15.8689,2.47995,17.1206z M14.8072,4.7316C14.2984,4.16633,13.4255,4.11939,12.7816,4.76332L6.43063,11.1143 13.8094,18.4931 20.1604,12.1421C20.1707,12.1318 20.1813,12.1218 20.1921,12.112 20.7574,11.6033 20.8043,10.7304 20.1604,10.0865L14.8373,4.76332C14.8269,4.75301,14.8169,4.74243,14.8072,4.7316z M3.65621,13.8887C3.6459,13.899 3.63532,13.9091 3.62448,13.9188 3.05922,14.4276 3.01228,15.3004 3.65621,15.9444L7.50001,19.7882 12.752,19.7882 5.25437,12.2906 3.65621,13.8887z";
public static string LegacySolidEraserCircleIcon =
"F1 M24,24z M0,0z M15.0919,19.6686L21.4282,13.3322C22.7462,12.1285,22.6942,10.1616,21.4426,8.90993L16.134,3.60133C14.9303,2.28338,12.9633,2.33537,11.7117,3.58702L5.36097,9.93771 15.0919,19.6686z M6.67201,21.2053C6.82267,21.3569,7.03137,21.4508,7.26201,21.4508L17.1907,21.4508C17.7033,22.3429 18.6657,22.9437 19.7683,22.9437 21.409,22.9437 22.7391,21.6136 22.7391,19.9729 22.7391,18.3322 21.409,17.0022 19.7683,17.0022 18.19,17.0022 16.8991,18.2331 16.8033,19.7874L12.8583,19.7874 4.18476,11.1139 2.60098,12.6977C1.28303,13.9014,1.33502,15.8683,2.58667,17.12L6.67201,21.2053z";
public static string LegacyLinedLassoSelectIcon =
"F0 M24,24z M0,0z M14.4715,12.7092L14.4715,18.7882 15.8291,16.7546C15.9688,16.5453,16.2038,16.4196,16.4554,16.4196L19.0106,16.4196 14.4715,12.7092z M14.6618,10.9193C14.4981,10.784 14.2733,10.6788 14.0025,10.6788 13.9951,10.6788 13.9877,10.6789 13.9803,10.6791 13.7083,10.6872 13.4502,10.8008 13.2607,10.9961 13.0712,11.1913 12.9653,11.4526 12.9653,11.7246L12.9653,20.3314C12.9653,20.3403 12.9655,20.3491 12.9658,20.358 12.9734,20.5733 13.0468,20.7811 13.176,20.9534 13.3053,21.1258 13.4842,21.2544 13.6888,21.3219 13.765,21.3471 13.8447,21.36 13.925,21.36L14.0025,21.36C14.1661,21.36 14.3276,21.3218 14.474,21.2486 14.6204,21.1754 14.7477,21.0692 14.8459,20.9382 14.8542,20.9272 14.8622,20.9159 14.8698,20.9045L16.8582,17.9258 20.3145,17.9258C20.5287,17.9281 20.7384,17.8641 20.9149,17.7424 21.0941,17.6187 21.2299,17.4417 21.3029,17.2365 21.3759,17.0313 21.3825,16.8084 21.3217,16.5993 21.262,16.3936 21.14,16.2117 20.9729,16.0782L14.6618,10.9193z M8.14548,20.0044C7.70454,19.6737 7.34665,19.2448 7.10016,18.7519 6.94658,18.4447 6.83887,18.1179 6.7795,17.7818 7.131,17.6605 7.45404,17.4604 7.72196,17.1924 7.74959,17.1648 7.7765,17.1366 7.80267,17.1078 8.5567,17.4118 9.3392,17.6365 10.1444,17.7694 10.5548,17.8372 10.9424,17.5594 11.0101,17.149 11.0779,16.7387 10.8001,16.3511 10.3897,16.2833 9.72172,16.1731 9.06686,15.9883 8.42926,15.7362 8.44084,15.6393 8.44672,15.5413 8.44672,15.4427 8.44672,14.7865 8.18602,14.1571 7.72196,13.693 7.25791,13.229 6.62852,12.9682 5.97224,12.9682 5.65536,12.9682 5.34474,13.029 5.05598,13.1441 4.47073,12.3026 4.15196,11.303 4.14328,10.2756 4.14532,7.03688 7.49758,4.1462 11.9971,4.1462 16.4941,4.1462 19.8451,7.03371 19.8508,10.2703 19.8388,10.7807 19.7549,11.2869 19.6016,11.7739 19.4767,12.1706 19.697,12.5934 20.0938,12.7183 20.4905,12.8432 20.9134,12.6228 21.0383,12.2261 21.2351,11.6008 21.3424,10.9507 21.3568,10.2952L21.357,10.2786C21.357,5.91009 16.9982,2.64 11.9971,2.64 6.9959,2.64 2.63705,5.91009 2.63705,10.2786L2.6371,10.2845C2.6479,11.6579 3.08647,12.993 3.89074,14.1047 3.63615,14.5007 3.49777,14.9646 3.49777,15.4427 3.49777,16.099 3.75847,16.7284 4.22252,17.1924 4.51465,17.4846 4.87229,17.6961 5.26092,17.8128 5.33333,18.3726 5.49917,18.9179 5.75297,19.4255 6.10404,20.1276 6.61375,20.7383 7.24176,21.2093 7.5745,21.4589 8.04654,21.3915 8.2961,21.0587 8.54566,20.726 8.47822,20.2539 8.14548,20.0044z M5.97224,14.4745C5.71544,14.4745 5.46916,14.5765 5.28757,14.7581 5.10598,14.9396 5.00397,15.1859 5.00397,15.4427 5.00397,15.6995 5.10598,15.9458 5.28757,16.1274 5.46916,16.309 5.71544,16.411 5.97224,16.411 6.22904,16.411 6.47533,16.309 6.65692,16.1274 6.8385,15.9458 6.94052,15.6995 6.94052,15.4427 6.94052,15.1859 6.8385,14.9396 6.65692,14.7581 6.47533,14.5765 6.22904,14.4745 5.97224,14.4745z";
public static string LegacySolidLassoSelectIcon =
"F1 M24,24z M0,0z M14.6618,10.9193C14.4981,10.784 14.2733,10.6788 14.0025,10.6788 13.9951,10.6788 13.9877,10.6789 13.9803,10.6791 13.7083,10.6872 13.4502,10.8008 13.2607,10.9961 13.0712,11.1913 12.9653,11.4526 12.9653,11.7246L12.9653,20.3314C12.9653,20.3403 12.9655,20.3491 12.9658,20.358 12.9734,20.5733 13.0468,20.7811 13.176,20.9534 13.3053,21.1258 13.4842,21.2544 13.6888,21.3219 13.765,21.3471 13.8447,21.36 13.925,21.36L14.0025,21.36C14.1661,21.36 14.3276,21.3218 14.474,21.2486 14.6204,21.1754 14.7477,21.0692 14.8459,20.9382 14.8542,20.9272 14.8622,20.9159 14.8698,20.9045L16.8582,17.9258 20.3145,17.9258C20.5287,17.9281 20.7384,17.8641 20.9149,17.7424 21.0941,17.6187 21.2299,17.4417 21.3029,17.2365 21.3759,17.0313 21.3825,16.8084 21.3217,16.5993 21.262,16.3936 21.14,16.2117 20.9729,16.0782L14.6618,10.9193z M8.14548,20.0044C7.70454,19.6737 7.34665,19.2448 7.10016,18.7519 6.94658,18.4447 6.83888,18.1179 6.7795,17.7818 7.131,17.6605 7.45404,17.4604 7.72196,17.1924 7.74959,17.1648 7.77649,17.1366 7.80267,17.1078 8.5567,17.4118 9.3392,17.6365 10.1444,17.7694 10.5548,17.8372 10.9424,17.5594 11.0101,17.149 11.0779,16.7387 10.8001,16.3511 10.3897,16.2833 9.72172,16.1731 9.06686,15.9883 8.42926,15.7362 8.44084,15.6393 8.44672,15.5413 8.44672,15.4427 8.44672,14.7865 8.18602,14.1571 7.72196,13.693 7.25791,13.229 6.62852,12.9682 5.97224,12.9682 5.65536,12.9682 5.34474,13.029 5.05598,13.1441 4.47073,12.3026 4.15196,11.303 4.14328,10.2756 4.14532,7.03688 7.49758,4.1462 11.9971,4.1462 16.4941,4.1462 19.8451,7.03371 19.8508,10.2703 19.8388,10.7807 19.7549,11.2869 19.6016,11.7739 19.4767,12.1706 19.697,12.5934 20.0938,12.7183 20.4905,12.8432 20.9134,12.6228 21.0383,12.2261 21.2351,11.6008 21.3424,10.9507 21.3568,10.2952L21.357,10.2786C21.357,5.91009 16.9982,2.64 11.9971,2.64 6.9959,2.64 2.63705,5.91009 2.63705,10.2786L2.6371,10.2845C2.6479,11.6579 3.08647,12.993 3.89074,14.1047 3.63615,14.5007 3.49777,14.9646 3.49777,15.4427 3.49777,16.099 3.75847,16.7284 4.22252,17.1924 4.51465,17.4846 4.87229,17.6961 5.26092,17.8128 5.33333,18.3726 5.49917,18.9178 5.75297,19.4255 6.10404,20.1276 6.61375,20.7383 7.24176,21.2093 7.5745,21.4589 8.04654,21.3915 8.2961,21.0587 8.54566,20.726 8.47822,20.2539 8.14548,20.0044z";
}
}
+774
View File
@@ -0,0 +1,774 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Application = System.Windows.Application;
using Color = System.Drawing.Color;
using Cursors = System.Windows.Input.Cursors;
using Image = System.Windows.Controls.Image;
using PixelFormat = System.Drawing.Imaging.PixelFormat;
using Point = System.Windows.Point;
using Size = System.Drawing.Size;
namespace Ink_Canvas
{
// 截图结果结构体
public struct ScreenshotResult
{
public Rectangle Area;
public List<Point> Path;
public Bitmap CameraImage;
public BitmapSource CameraBitmapSource;
public ScreenshotResult(Rectangle area, List<Point> path = null, Bitmap cameraImage = null, BitmapSource cameraBitmapSource = null)
{
Area = area;
Path = path;
CameraImage = cameraImage;
CameraBitmapSource = cameraBitmapSource;
}
}
public partial class MainWindow : Window
{
// 截图并插入到画布
private async Task CaptureScreenshotAndInsert()
{
try
{
// 隐藏主窗口以避免截图包含窗口本身
var originalVisibility = Visibility;
Visibility = Visibility.Hidden;
// 等待窗口隐藏
await Task.Delay(200);
// 启动区域选择截图
var screenshotResult = await ShowScreenshotSelector();
// 恢复窗口显示
Visibility = originalVisibility;
if (screenshotResult.HasValue)
{
// 检查是否是摄像头截图
if (screenshotResult.Value.CameraBitmapSource != null)
{
// 摄像头截图(使用BitmapSource
await InsertBitmapSourceToCanvas(screenshotResult.Value.CameraBitmapSource);
}
else if (screenshotResult.Value.CameraImage != null)
{
// 摄像头截图(使用Bitmap
await InsertScreenshotToCanvas(screenshotResult.Value.CameraImage);
}
else if (screenshotResult.Value.Area.Width > 0 && screenshotResult.Value.Area.Height > 0)
{
// 屏幕截图
using (var originalBitmap = CaptureScreenArea(screenshotResult.Value.Area))
{
if (originalBitmap != null)
{
Bitmap finalBitmap = originalBitmap;
bool needDisposeFinalBitmap = false;
try
{
// 如果有路径信息,应用形状遮罩
if (screenshotResult.Value.Path != null && screenshotResult.Value.Path.Count > 0)
{
finalBitmap = ApplyShapeMask(originalBitmap, screenshotResult.Value.Path, screenshotResult.Value.Area);
needDisposeFinalBitmap = true; // 标记需要释放新创建的位图
}
// 将截图转换为WPF Image并插入到画布
await InsertScreenshotToCanvas(finalBitmap);
}
finally
{
// 如果创建了新的位图,需要释放它
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
{
finalBitmap.Dispose();
}
}
}
}
}
}
else
{
ShowNotification("截图已取消");
}
}
catch (Exception ex)
{
ShowNotification($"截图失败: {ex.Message}");
Visibility = Visibility.Visible;
}
}
// 直接全屏截图并插入到画布
private async Task CaptureFullScreenAndInsert()
{
try
{
// 隐藏主窗口以避免截图包含窗口本身
var originalVisibility = Visibility;
Visibility = Visibility.Hidden;
// 等待窗口隐藏
await Task.Delay(200);
// 获取虚拟屏幕边界
var virtualScreen = SystemInformation.VirtualScreen;
var fullScreenArea = new Rectangle(virtualScreen.X, virtualScreen.Y, virtualScreen.Width, virtualScreen.Height);
// 截取全屏
using (var fullScreenBitmap = CaptureScreenArea(fullScreenArea))
{
if (fullScreenBitmap != null)
{
// 将截图转换为WPF Image并插入到画布
await InsertScreenshotToCanvas(fullScreenBitmap);
}
else
{
ShowNotification("全屏截图失败");
}
}
// 恢复窗口显示
Visibility = originalVisibility;
}
catch (Exception ex)
{
ShowNotification($"全屏截图失败: {ex.Message}");
Visibility = Visibility.Visible;
}
}
// 显示截图区域选择器
private async Task<ScreenshotResult?> ShowScreenshotSelector()
{
ScreenshotResult? result = null;
try
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
var selectorWindow = new ScreenshotSelectorWindow();
if (selectorWindow.ShowDialog() == true)
{
// 检查是否是摄像头截图
if (selectorWindow.CameraBitmapSource != null)
{
result = new ScreenshotResult(
Rectangle.Empty, // 摄像头截图不需要区域
null, // 摄像头截图不需要路径
null, // 不再使用Bitmap
selectorWindow.CameraBitmapSource // 摄像头BitmapSource
);
}
else if (selectorWindow.CameraImage != null)
{
result = new ScreenshotResult(
Rectangle.Empty, // 摄像头截图不需要区域
null, // 摄像头截图不需要路径
selectorWindow.CameraImage // 摄像头图像
);
}
else
{
result = new ScreenshotResult(
selectorWindow.SelectedArea.Value,
selectorWindow.SelectedPath
);
}
}
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"显示截图选择器失败: {ex.Message}", LogHelper.LogType.Error);
}
return result;
}
// 截取指定屏幕区域
private Bitmap CaptureScreenArea(Rectangle area)
{
try
{
// 确保区域在有效范围内
var virtualScreen = SystemInformation.VirtualScreen;
// 调整区域边界,确保不超出屏幕范围
int x = Math.Max(area.X, virtualScreen.X);
int y = Math.Max(area.Y, virtualScreen.Y);
int right = Math.Min(area.Right, virtualScreen.Right);
int bottom = Math.Min(area.Bottom, virtualScreen.Bottom);
int width = Math.Max(1, right - x);
int height = Math.Max(1, bottom - y);
// 创建支持透明度的位图
var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
// 设置高质量渲染
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.CompositingMode = CompositingMode.SourceOver;
// 截取屏幕区域
graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy);
}
return bitmap;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"截取屏幕区域失败: {ex.Message}", LogHelper.LogType.Error);
return null;
}
}
// 将截图插入到画布
private async Task InsertScreenshotToCanvas(Bitmap bitmap)
{
try
{
// 验证位图有效性
if (bitmap == null || bitmap.Width <= 0 || bitmap.Height <= 0)
{
ShowNotification("无效的截图");
return;
}
// 将Bitmap转换为WPF BitmapSource
var bitmapSource = ConvertBitmapToBitmapSource(bitmap);
if (bitmapSource == null)
{
ShowNotification("转换截图失败");
return;
}
// 创建WPF Image控件
var image = new Image
{
Source = bitmapSource,
Stretch = Stretch.Uniform
};
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
// 生成唯一名称
string timestamp = "screenshot_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
image.Name = timestamp;
// 初始化TransformGroup
InitializeScreenshotTransform(image);
// 设置截图属性,避免被InkCanvas选择系统处理
image.IsHitTestVisible = true;
image.Focusable = false;
// 初始化InkCanvas选择设置
InitializeInkCanvasSelectionSettings();
// 等待图片加载完成后再进行居中处理
image.Loaded += (sender, e) =>
{
// 确保在UI线程中执行
Dispatcher.BeginInvoke(new Action(() =>
{
CenterAndScaleScreenshot(image);
// 绑定事件处理器
BindScreenshotEvents(image);
}), DispatcherPriority.Loaded);
};
// 添加到画布
inkCanvas.Children.Add(image);
// 提交历史记录
timeMachine.CommitElementInsertHistory(image);
// 插入图片后切换到选择模式并刷新浮动栏高光显示
SetCurrentToolMode(InkCanvasEditingMode.Select);
UpdateCurrentToolMode("select");
HideSubPanels("select");
ShowNotification("截图已插入到画布");
}
catch (Exception ex)
{
ShowNotification($"插入截图失败: {ex.Message}");
LogHelper.WriteLogToFile($"插入截图失败: {ex.Message}", LogHelper.LogType.Error);
}
finally
{
bitmap?.Dispose();
}
}
// 将BitmapSource插入到画布(用于摄像头截图)
private async Task InsertBitmapSourceToCanvas(BitmapSource bitmapSource)
{
try
{
// 创建WPF Image控件
var image = new Image
{
Source = bitmapSource,
Stretch = Stretch.Uniform
};
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
// 生成唯一名称
string timestamp = "camera_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
image.Name = timestamp;
// 初始化TransformGroup
InitializeScreenshotTransform(image);
// 设置截图属性,避免被InkCanvas选择系统处理
image.IsHitTestVisible = true;
image.Focusable = false;
// 初始化InkCanvas选择设置
InitializeInkCanvasSelectionSettings();
// 等待图片加载完成后再进行居中处理
image.Loaded += (sender, e) =>
{
// 确保在UI线程中执行
Dispatcher.BeginInvoke(new Action(() =>
{
CenterAndScaleScreenshot(image);
// 绑定事件处理器
BindScreenshotEvents(image);
}), DispatcherPriority.Loaded);
};
// 添加到画布
inkCanvas.Children.Add(image);
// 提交历史记录
timeMachine.CommitElementInsertHistory(image);
// 插入图片后切换到选择模式并刷新浮动栏高光显示
SetCurrentToolMode(InkCanvasEditingMode.Select);
UpdateCurrentToolMode("select");
HideSubPanels("select");
ShowNotification("摄像头截图已插入到画布");
}
catch (Exception ex)
{
ShowNotification($"插入摄像头截图失败: {ex.Message}");
LogHelper.WriteLogToFile($"插入摄像头截图失败: {ex.Message}", LogHelper.LogType.Error);
}
}
// 初始化截图的TransformGroup
private void InitializeScreenshotTransform(Image image)
{
var transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform(1, 1));
transformGroup.Children.Add(new TranslateTransform(0, 0));
transformGroup.Children.Add(new RotateTransform(0));
image.RenderTransform = transformGroup;
}
// 绑定截图事件处理器
private void BindScreenshotEvents(Image image)
{
// 鼠标事件
image.MouseLeftButtonDown += Element_MouseLeftButtonDown;
image.MouseLeftButtonUp += Element_MouseLeftButtonUp;
image.MouseMove += Element_MouseMove;
image.MouseWheel += Element_MouseWheel;
// 触摸事件
image.TouchDown += Element_TouchDown;
image.TouchUp += Element_TouchUp;
image.IsManipulationEnabled = true;
image.ManipulationDelta += Element_ManipulationDelta;
image.ManipulationCompleted += Element_ManipulationCompleted;
// 设置光标
image.Cursor = Cursors.Hand;
// 禁用InkCanvas对截图的选择处理
image.IsHitTestVisible = true;
image.Focusable = false;
}
// 专门为截图优化的居中缩放方法
private void CenterAndScaleScreenshot(Image image)
{
try
{
// 确保图片已加载
if (image.Source == null || image.ActualWidth == 0 || image.ActualHeight == 0)
{
return;
}
// 获取画布的实际尺寸
double canvasWidth = inkCanvas.ActualWidth;
double canvasHeight = inkCanvas.ActualHeight;
// 如果画布尺寸为0,使用窗口尺寸作为备选
if (canvasWidth <= 0 || canvasHeight <= 0)
{
canvasWidth = ActualWidth;
canvasHeight = ActualHeight;
}
// 如果仍然为0,使用屏幕尺寸
if (canvasWidth <= 0 || canvasHeight <= 0)
{
canvasWidth = SystemParameters.PrimaryScreenWidth;
canvasHeight = SystemParameters.PrimaryScreenHeight;
}
// 计算最大允许尺寸(画布的80%
double maxWidth = canvasWidth * 0.8;
double maxHeight = canvasHeight * 0.8;
// 获取图片的原始尺寸
double originalWidth = image.Source.Width;
double originalHeight = image.Source.Height;
// 计算缩放比例
double scaleX = maxWidth / originalWidth;
double scaleY = maxHeight / originalHeight;
double scale = Math.Min(scaleX, scaleY);
// 如果图片本身比最大尺寸小,不进行缩放
if (scale > 1.0)
{
scale = 1.0;
}
// 计算新的尺寸
double newWidth = originalWidth * scale;
double newHeight = originalHeight * scale;
// 设置图片尺寸
image.Width = newWidth;
image.Height = newHeight;
// 计算居中位置
double centerX = (canvasWidth - newWidth) / 2;
double centerY = (canvasHeight - newHeight) / 2;
// 确保位置不为负数
centerX = Math.Max(0, centerX);
centerY = Math.Max(0, centerY);
// 设置位置
InkCanvas.SetLeft(image, centerX);
InkCanvas.SetTop(image, centerY);
// 这样可以保持滚轮缩放和拖动功能
if (image.RenderTransform == null || image.RenderTransform == Transform.Identity)
{
// 只有在没有TransformGroup时才创建
InitializeScreenshotTransform(image);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"截图居中失败: {ex.Message}", LogHelper.LogType.Error);
// 如果居中失败,使用默认的居中方法作为备选
CenterAndScaleElement(image);
}
}
// 应用形状遮罩到截图
private Bitmap ApplyShapeMask(Bitmap bitmap, List<Point> path, Rectangle area)
{
try
{
// 验证路径参数
if (path == null || path.Count < 3)
{
LogHelper.WriteLogToFile("路径点数不足,无法应用形状遮罩", LogHelper.LogType.Warning);
return bitmap;
}
// 获取DPI缩放比例
var dpiScale = GetDpiScale();
var virtualScreen = SystemInformation.VirtualScreen;
// 创建结果位图,确保支持透明度
var resultBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb);
// 首先将整个位图设置为透明
using (var resultGraphics = Graphics.FromImage(resultBitmap))
{
// 清除位图,设置为完全透明
resultGraphics.Clear(Color.Transparent);
// 设置高质量渲染
resultGraphics.SmoothingMode = SmoothingMode.AntiAlias;
resultGraphics.CompositingQuality = CompositingQuality.HighQuality;
resultGraphics.CompositingMode = CompositingMode.SourceOver;
// 创建路径
using (var pathGraphics = new GraphicsPath())
{
// 转换WPF坐标到GDI+坐标,考虑DPI缩放和屏幕偏移
var points = new PointF[path.Count];
for (int i = 0; i < path.Count; i++)
{
// 将WPF坐标转换为实际屏幕坐标,然后相对于截图区域计算偏移
double screenX = (path[i].X * dpiScale) + virtualScreen.Left;
double screenY = (path[i].Y * dpiScale) + virtualScreen.Top;
// 计算相对于截图区域的坐标
float relativeX = (float)(screenX - area.X);
float relativeY = (float)(screenY - area.Y);
// 确保坐标在有效范围内
relativeX = Math.Max(0, Math.Min(relativeX, bitmap.Width - 1));
relativeY = Math.Max(0, Math.Min(relativeY, bitmap.Height - 1));
points[i] = new PointF(relativeX, relativeY);
}
// 添加路径 - 使用FillMode.Winding确保路径正确填充
pathGraphics.FillMode = FillMode.Winding;
pathGraphics.AddPolygon(points);
// 验证路径是否有效
if (!pathGraphics.IsVisible(0, 0) && pathGraphics.GetBounds().Width > 0 && pathGraphics.GetBounds().Height > 0)
{
// 设置裁剪区域为路径内部
resultGraphics.SetClip(pathGraphics);
// 在裁剪区域内绘制原始图像
resultGraphics.DrawImage(bitmap, 0, 0);
// 重置裁剪区域,确保后续操作不受影响
resultGraphics.ResetClip();
}
else
{
LogHelper.WriteLogToFile("生成的路径无效,返回透明图像", LogHelper.LogType.Warning);
// 如果路径无效,返回透明图像
return resultBitmap;
}
}
}
return resultBitmap;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用形状遮罩失败: {ex.Message}", LogHelper.LogType.Error);
return bitmap;
}
}
// 将System.Drawing.Bitmap转换为WPF BitmapSource
private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap)
{
try
{
// 验证位图有效性
if (bitmap == null)
return null;
// 验证位图尺寸
if (bitmap.Width <= 0 || bitmap.Height <= 0)
return null;
// 使用更安全的方法转换位图
var bitmapData = bitmap.LockBits(
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadOnly,
bitmap.PixelFormat);
try
{
// 根据像素格式选择合适的WPF像素格式
System.Windows.Media.PixelFormat wpfPixelFormat;
switch (bitmap.PixelFormat)
{
case PixelFormat.Format24bppRgb:
wpfPixelFormat = PixelFormats.Bgr24;
break;
case PixelFormat.Format32bppArgb:
wpfPixelFormat = PixelFormats.Bgra32;
break;
case PixelFormat.Format32bppRgb:
wpfPixelFormat = PixelFormats.Bgr32;
break;
default:
wpfPixelFormat = PixelFormats.Bgr24;
break;
}
var bitmapSource = BitmapSource.Create(
bitmapData.Width,
bitmapData.Height,
bitmap.HorizontalResolution,
bitmap.VerticalResolution,
wpfPixelFormat,
null,
bitmapData.Scan0,
bitmapData.Stride * bitmapData.Height,
bitmapData.Stride);
bitmapSource.Freeze();
return bitmapSource;
}
finally
{
bitmap.UnlockBits(bitmapData);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"转换位图失败: {ex.Message}", LogHelper.LogType.Error);
// 尝试使用备用方法:内存流转换
try
{
return ConvertBitmapToBitmapSourceFallback(bitmap);
}
catch (Exception fallbackEx)
{
LogHelper.WriteLogToFile($"备用转换方法也失败: {fallbackEx.Message}", LogHelper.LogType.Error);
// 最后尝试:使用最简单的转换方法
try
{
return ConvertBitmapToBitmapSourceSimple(bitmap);
}
catch (Exception simpleEx)
{
LogHelper.WriteLogToFile($"简单转换方法也失败: {simpleEx.Message}", LogHelper.LogType.Error);
throw;
}
}
}
}
// 备用的位图转换方法(使用内存流)
private BitmapSource ConvertBitmapToBitmapSourceFallback(Bitmap bitmap)
{
try
{
// 验证位图有效性
if (bitmap == null || bitmap.Width <= 0 || bitmap.Height <= 0)
return null;
// 创建一个新的位图,确保格式正确
using (var convertedBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb))
{
using (var graphics = Graphics.FromImage(convertedBitmap))
{
graphics.DrawImage(bitmap, 0, 0);
}
using (var memory = new MemoryStream())
{
convertedBitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = memory;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"备用转换方法失败: {ex.Message}", LogHelper.LogType.Error);
throw;
}
}
// 最简单的位图转换方法
private BitmapSource ConvertBitmapToBitmapSourceSimple(Bitmap bitmap)
{
try
{
if (bitmap == null)
return null;
// 使用最基础的方法:直接保存为PNG然后加载
var tempFile = Path.GetTempFileName() + ".png";
try
{
bitmap.Save(tempFile, ImageFormat.Png);
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.UriSource = new Uri(tempFile);
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
finally
{
// 清理临时文件
try
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
catch (Exception deleteEx)
{
LogHelper.WriteLogToFile($"删除临时文件失败: {deleteEx.Message}", LogHelper.LogType.Warning);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"简单转换方法失败: {ex.Message}", LogHelper.LogType.Error);
throw;
}
}
// 获取DPI缩放比例
private double GetDpiScale()
{
var source = PresentationSource.FromVisual(this);
if (source?.CompositionTarget != null)
{
return source.CompositionTarget.TransformToDevice.M11;
}
return 1.0; // 默认DPI
}
}
}
File diff suppressed because it is too large Load Diff
+32 -5
View File
@@ -68,9 +68,20 @@ namespace Ink_Canvas
public static void ScrollViewToVerticalTop(FrameworkElement element, ScrollViewer scrollViewer)
{
if (element == null || scrollViewer == null)
{
return;
}
var scrollViewerOffset = scrollViewer.VerticalOffset;
var point = new Point(0, scrollViewerOffset);
var tarPos = element.TransformToVisual(scrollViewer).Transform(point);
var transform = element.TransformToVisual(scrollViewer);
if (transform == null)
{
return;
}
var tarPos = transform.Transform(point);
scrollViewer.ScrollToVerticalOffset(tarPos.Y);
}
@@ -86,8 +97,16 @@ namespace Ink_Canvas
// 只有当选择的页面与当前页面不同时才进行切换
if (index + 1 != CurrentWhiteboardIndex)
{
// 取消任何UI元素的选择(只在真正切换页面时)
DeselectUIElement();
// 隐藏图片选择工具栏
if (currentSelectedElement != null)
{
// 保存当前编辑模式
var previousEditingMode = inkCanvas.EditingMode;
UnselectElement(currentSelectedElement);
// 恢复编辑模式
inkCanvas.EditingMode = previousEditingMode;
currentSelectedElement = null;
}
SaveStrokes();
ClearStrokes(true);
@@ -111,8 +130,16 @@ namespace Ink_Canvas
// 只有当选择的页面与当前页面不同时才进行切换
if (index + 1 != CurrentWhiteboardIndex)
{
// 取消任何UI元素的选择(只在真正切换页面时)
DeselectUIElement();
// 隐藏图片选择工具栏
if (currentSelectedElement != null)
{
// 保存当前编辑模式
var previousEditingMode = inkCanvas.EditingMode;
UnselectElement(currentSelectedElement);
// 恢复编辑模式
inkCanvas.EditingMode = previousEditingMode;
currentSelectedElement = null;
}
SaveStrokes();
ClearStrokes(true);
+88 -28
View File
@@ -2,14 +2,22 @@ using Ink_Canvas.Helpers;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Color = System.Drawing.Color;
using File = System.IO.File;
using Image = System.Windows.Controls.Image;
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
namespace Ink_Canvas
@@ -71,7 +79,7 @@ namespace Ink_Canvas
for (int i = 1; i <= totalSlides; i++)
{
var slideStrokes = _pptInkManager?.LoadSlideStrokes(i);
var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i);
if (slideStrokes != null && slideStrokes.Count > 0)
{
allPageStrokes.Add(slideStrokes);
@@ -125,6 +133,23 @@ namespace Ink_Canvas
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePathWithName);
}
catch (Exception)
{
}
});
// 保存元素信息
var elementInfos = new List<CanvasElementInfo>();
foreach (var child in inkCanvas.Children)
@@ -150,7 +175,7 @@ namespace Ink_Canvas
catch (Exception ex)
{
ShowNotification("墨迹保存失败");
LogHelper.WriteLogToFile("墨迹保存失败 | " + ex.ToString(), LogHelper.LogType.Error);
LogHelper.WriteLogToFile("墨迹保存失败 | " + ex, LogHelper.LogType.Error);
}
}
@@ -191,7 +216,7 @@ namespace Ink_Canvas
// 保存元数据信息
string metadataFile = Path.Combine(tempDir, "metadata.txt");
using (var writer = new StreamWriter(metadataFile, false, System.Text.Encoding.UTF8))
using (var writer = new StreamWriter(metadataFile, false, Encoding.UTF8))
{
writer.WriteLine($"保存时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
writer.WriteLine($"总页数: {allPageStrokes.Count}");
@@ -219,7 +244,25 @@ namespace Ink_Canvas
File.Delete(zipFileName);
// 使用System.IO.Compression.FileSystem来创建ZIP
System.IO.Compression.ZipFile.CreateFromDirectory(tempDir, zipFileName);
ZipFile.CreateFromDirectory(tempDir, zipFileName);
// 异步上传ZIP文件到Dlass
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName);
}
catch (Exception)
{
}
});
if (newNotice) ShowNotification($"多页面墨迹成功保存至压缩包 {zipFileName}");
}
@@ -233,13 +276,13 @@ namespace Ink_Canvas
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理临时目录失败: {ex.ToString()}", LogHelper.LogType.Warning);
LogHelper.WriteLogToFile($"清理临时目录失败: {ex}", LogHelper.LogType.Warning);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存多页面墨迹压缩包失败: {ex.ToString()}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"保存多页面墨迹压缩包失败: {ex}", LogHelper.LogType.Error);
throw;
}
}
@@ -250,16 +293,16 @@ namespace Ink_Canvas
private void SaveSinglePageStrokesAsImage(string savePathWithName, bool newNotice)
{
// 全页面保存模式 - 保存整个墨迹页面的图像
var bitmap = new System.Drawing.Bitmap(
(int)System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
(int)System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height);
var bitmap = new Bitmap(
Screen.PrimaryScreen.Bounds.Width,
Screen.PrimaryScreen.Bounds.Height);
using (var g = System.Drawing.Graphics.FromImage(bitmap))
using (var g = Graphics.FromImage(bitmap))
{
// 创建黑色或透明背景
System.Drawing.Color bgColor = Settings.Canvas.UsingWhiteboard
? System.Drawing.Color.White
: System.Drawing.Color.FromArgb(22, 41, 36); // 黑板背景色
Color bgColor = Settings.Canvas.UsingWhiteboard
? Color.White
: Color.FromArgb(22, 41, 36); // 黑板背景色
g.Clear(bgColor);
// 将InkCanvas墨迹渲染到Visual
@@ -287,7 +330,7 @@ namespace Ink_Canvas
{
encoder.Save(ms);
ms.Seek(0, SeekOrigin.Begin);
var imgBitmap = new System.Drawing.Bitmap(ms);
var imgBitmap = new Bitmap(ms);
// 将生成的墨迹图像绘制到屏幕截图上
// 居中绘制,确保墨迹位于屏幕中央
@@ -297,12 +340,29 @@ namespace Ink_Canvas
// 保存为PNG
string imagePathWithName = Path.ChangeExtension(savePathWithName, "png");
bitmap.Save(imagePathWithName, System.Drawing.Imaging.ImageFormat.Png);
bitmap.Save(imagePathWithName, ImageFormat.Png);
// 仍然保存墨迹文件以兼容旧版本
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(imagePathWithName);
}
catch (Exception)
{
}
});
}
}
@@ -337,7 +397,7 @@ namespace Ink_Canvas
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存页面图像失败: {ex.ToString()}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"保存页面图像失败: {ex}", LogHelper.LogType.Error);
throw;
}
}
@@ -376,7 +436,7 @@ namespace Ink_Canvas
catch (Exception ex)
{
ShowNotification("墨迹打开失败");
LogHelper.WriteLogToFile($"墨迹打开失败: {ex.ToString()}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"墨迹打开失败: {ex}", LogHelper.LogType.Error);
}
}
@@ -394,7 +454,7 @@ namespace Ink_Canvas
try
{
// 解压ZIP文件
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempDir);
ZipFile.ExtractToDirectory(zipFilePath, tempDir);
// 读取元数据文件
string metadataFile = Path.Combine(tempDir, "metadata.txt");
@@ -447,13 +507,13 @@ namespace Ink_Canvas
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理临时目录失败: {ex.ToString()}", LogHelper.LogType.Warning);
LogHelper.WriteLogToFile($"清理临时目录失败: {ex}", LogHelper.LogType.Warning);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"打开ICC压缩包失败: {ex.ToString()}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"打开ICC压缩包失败: {ex}", LogHelper.LogType.Error);
throw;
}
}
@@ -465,7 +525,7 @@ namespace Ink_Canvas
{
var metadata = new Dictionary<string, string>();
using (var reader = new StreamReader(metadataPath, System.Text.Encoding.UTF8))
using (var reader = new StreamReader(metadataPath, Encoding.UTF8))
{
string line;
while ((line = reader.ReadLine()) != null)
@@ -521,7 +581,7 @@ namespace Ink_Canvas
timeMachine.ClearStrokeHistory();
// 重置PPT墨迹存储
_pptInkManager?.ClearAllStrokes();
_singlePPTInkManager?.ClearAllStrokes();
// 读取所有页面的墨迹文件
var files = Directory.GetFiles(tempDir, "page_*.icstk");
@@ -535,7 +595,7 @@ namespace Ink_Canvas
var strokes = new StrokeCollection(fs);
if (strokes.Count > 0)
{
_pptInkManager?.SaveCurrentSlideStrokes(pageNumber, strokes);
_singlePPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes);
}
}
}
@@ -545,7 +605,7 @@ namespace Ink_Canvas
if (_pptManager?.IsInSlideShow == true)
{
int currentSlide = _pptManager.GetCurrentSlideNumber();
var currentStrokes = _pptInkManager?.LoadSlideStrokes(currentSlide);
var currentStrokes = _singlePPTInkManager?.LoadSlideStrokes(currentSlide);
if (currentStrokes != null && currentStrokes.Count > 0)
{
inkCanvas.Strokes.Add(currentStrokes);
@@ -556,7 +616,7 @@ namespace Ink_Canvas
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复PPT墨迹失败: {ex.ToString()}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"恢复PPT墨迹失败: {ex}", LogHelper.LogType.Error);
throw;
}
}
@@ -609,7 +669,7 @@ namespace Ink_Canvas
{
// 创建历史记录
var history = new TimeMachineHistory(strokes, TimeMachineHistoryType.UserInput, false);
TimeMachineHistories[pageNumber] = new TimeMachineHistory[] { history };
TimeMachineHistories[pageNumber] = new[] { history };
}
}
}
@@ -628,7 +688,7 @@ namespace Ink_Canvas
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复白板墨迹失败: {ex.ToString()}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"恢复白板墨迹失败: {ex}", LogHelper.LogType.Error);
throw;
}
}
@@ -636,7 +696,7 @@ namespace Ink_Canvas
/// <summary>
/// 打开单个墨迹文件
/// </summary>
private void OpenSingleStrokeFile(string filePath)
public void OpenSingleStrokeFile(string filePath)
{
var fileStreamHasNoStroke = false;
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
+21 -331
View File
@@ -1,6 +1,4 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
@@ -8,26 +6,9 @@ using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Media.Imaging;
using Application = System.Windows.Application;
using Clipboard = System.Windows.Clipboard;
using Size = System.Drawing.Size;
namespace Ink_Canvas
{
// 截图结果结构体
public struct ScreenshotResult
{
public System.Drawing.Rectangle Area;
public List<System.Windows.Point> Path;
public ScreenshotResult(System.Drawing.Rectangle area, List<System.Windows.Point> path = null)
{
Area = area;
Path = path;
}
}
public partial class MainWindow : Window
{
private void SaveScreenShot(bool isHideNotification, string fileName = null)
@@ -42,7 +23,7 @@ namespace Ink_Canvas
SaveInkCanvasStrokes(false);
}
private void SaveScreenShotToDesktop()
internal void SaveScreenShotToDesktop()
{
var desktopPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
@@ -63,10 +44,10 @@ namespace Ink_Canvas
using (var memoryGraphics = Graphics.FromImage(bitmap))
{
// 设置高质量渲染
memoryGraphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
memoryGraphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
memoryGraphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
memoryGraphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;
memoryGraphics.CompositingQuality = CompositingQuality.HighQuality;
memoryGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
memoryGraphics.SmoothingMode = SmoothingMode.HighQuality;
memoryGraphics.CompositingMode = CompositingMode.SourceOver;
memoryGraphics.CopyFromScreen(rc.X, rc.Y, 0, 0, rc.Size, CopyPixelOperation.SourceCopy);
@@ -85,6 +66,22 @@ namespace Ink_Canvas
{
ShowNotification($"截图成功保存至 {savePath}");
}
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePath);
}
catch (Exception)
{
}
});
}
// 获取日期文件夹路径
@@ -120,312 +117,5 @@ namespace Ink_Canvas
screenshotsFolder,
$"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.png");
}
// 截图并复制到剪贴板
private async Task CaptureScreenshotToClipboard()
{
try
{
// 隐藏主窗口以避免截图包含窗口本身
var originalVisibility = this.Visibility;
this.Visibility = Visibility.Hidden;
// 等待窗口隐藏
await Task.Delay(200);
// 启动区域选择截图
var screenshotResult = await ShowScreenshotSelector();
// 恢复窗口显示
this.Visibility = originalVisibility;
if (screenshotResult.HasValue && screenshotResult.Value.Area.Width > 0 && screenshotResult.Value.Area.Height > 0)
{
// 截取选定区域
using (var originalBitmap = CaptureScreenArea(screenshotResult.Value.Area))
{
if (originalBitmap != null)
{
Bitmap finalBitmap = originalBitmap;
bool needDisposeFinalBitmap = false;
try
{
// 如果有路径信息,应用形状遮罩
if (screenshotResult.Value.Path != null && screenshotResult.Value.Path.Count > 0)
{
finalBitmap = ApplyShapeMask(originalBitmap, screenshotResult.Value.Path, screenshotResult.Value.Area);
needDisposeFinalBitmap = true; // 标记需要释放新创建的位图
}
// 将截图复制到剪贴板
CopyBitmapToClipboard(finalBitmap);
// 等待窗口完全显示后自动粘贴
await Task.Delay(100);
await AutoPasteScreenshot();
}
finally
{
// 如果创建了新的位图,需要释放它
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
{
finalBitmap.Dispose();
}
}
}
}
}
else
{
ShowNotification("截图已取消");
}
}
catch (Exception ex)
{
ShowNotification($"截图失败: {ex.Message}");
this.Visibility = Visibility.Visible;
}
}
// 显示截图区域选择器
private async Task<ScreenshotResult?> ShowScreenshotSelector()
{
ScreenshotResult? result = null;
try
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
var selectorWindow = new ScreenshotSelectorWindow();
if (selectorWindow.ShowDialog() == true)
{
result = new ScreenshotResult(
selectorWindow.SelectedArea.Value,
selectorWindow.SelectedPath
);
}
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"显示截图选择器失败: {ex.Message}", LogHelper.LogType.Error);
}
return result;
}
// 截取指定屏幕区域
private Bitmap CaptureScreenArea(System.Drawing.Rectangle area)
{
try
{
// 确保区域在有效范围内
var virtualScreen = SystemInformation.VirtualScreen;
// 调整区域边界,确保不超出屏幕范围
int x = Math.Max(area.X, virtualScreen.X);
int y = Math.Max(area.Y, virtualScreen.Y);
int right = Math.Min(area.Right, virtualScreen.Right);
int bottom = Math.Min(area.Bottom, virtualScreen.Bottom);
int width = Math.Max(1, right - x);
int height = Math.Max(1, bottom - y);
// 创建支持透明度的位图
var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
// 设置高质量渲染
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;
// 截取屏幕区域
graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy);
}
LogHelper.WriteLogToFile($"成功截取区域: X={x}, Y={y}, Width={width}, Height={height}", LogHelper.LogType.Info);
return bitmap;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"截取屏幕区域失败: {ex.Message}", LogHelper.LogType.Error);
return null;
}
}
// 自动粘贴截图到画布
private async Task AutoPasteScreenshot()
{
try
{
// 只在白板模式下自动粘贴
if (currentMode == 1)
{
await PasteImageFromClipboard();
ShowNotification("截图已自动插入到画布");
}
else
{
ShowNotification("截图已复制到剪贴板,可在白板模式下粘贴");
}
}
catch (Exception ex)
{
ShowNotification($"自动粘贴截图失败: {ex.Message}");
LogHelper.WriteLogToFile($"自动粘贴截图失败: {ex.Message}", LogHelper.LogType.Error);
}
}
// 将Bitmap复制到剪贴板
private void CopyBitmapToClipboard(Bitmap bitmap)
{
try
{
// 将System.Drawing.Bitmap转换为WPF BitmapSource
var bitmapSource = ConvertBitmapToBitmapSource(bitmap);
// 复制到剪贴板
Clipboard.SetImage(bitmapSource);
}
catch (Exception ex)
{
ShowNotification($"复制到剪贴板失败: {ex.Message}");
}
}
// 应用形状遮罩到截图
private Bitmap ApplyShapeMask(Bitmap bitmap, List<System.Windows.Point> path, System.Drawing.Rectangle area)
{
try
{
// 验证路径参数
if (path == null || path.Count < 3)
{
LogHelper.WriteLogToFile("路径点数不足,无法应用形状遮罩", LogHelper.LogType.Warning);
return bitmap;
}
// 获取DPI缩放比例
var dpiScale = GetDpiScale();
var virtualScreen = SystemInformation.VirtualScreen;
// 创建结果位图,确保支持透明度
var resultBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb);
// 首先将整个位图设置为透明
using (var resultGraphics = Graphics.FromImage(resultBitmap))
{
// 清除位图,设置为完全透明
resultGraphics.Clear(System.Drawing.Color.Transparent);
// 设置高质量渲染
resultGraphics.SmoothingMode = SmoothingMode.AntiAlias;
resultGraphics.CompositingQuality = CompositingQuality.HighQuality;
resultGraphics.CompositingMode = CompositingMode.SourceOver;
// 创建路径
using (var pathGraphics = new GraphicsPath())
{
// 转换WPF坐标到GDI+坐标,考虑DPI缩放和屏幕偏移
var points = new PointF[path.Count];
for (int i = 0; i < path.Count; i++)
{
// 将WPF坐标转换为实际屏幕坐标,然后相对于截图区域计算偏移
double screenX = (path[i].X * dpiScale) + virtualScreen.Left;
double screenY = (path[i].Y * dpiScale) + virtualScreen.Top;
// 计算相对于截图区域的坐标
float relativeX = (float)(screenX - area.X);
float relativeY = (float)(screenY - area.Y);
// 确保坐标在有效范围内
relativeX = Math.Max(0, Math.Min(relativeX, bitmap.Width - 1));
relativeY = Math.Max(0, Math.Min(relativeY, bitmap.Height - 1));
points[i] = new PointF(relativeX, relativeY);
}
// 添加路径 - 使用FillMode.Winding确保路径正确填充
pathGraphics.FillMode = FillMode.Winding;
pathGraphics.AddPolygon(points);
// 验证路径是否有效
if (!pathGraphics.IsVisible(0, 0) && pathGraphics.GetBounds().Width > 0 && pathGraphics.GetBounds().Height > 0)
{
// 设置裁剪区域为路径内部
resultGraphics.SetClip(pathGraphics);
// 在裁剪区域内绘制原始图像
resultGraphics.DrawImage(bitmap, 0, 0);
// 重置裁剪区域,确保后续操作不受影响
resultGraphics.ResetClip();
}
else
{
LogHelper.WriteLogToFile("生成的路径无效,返回原始图像", LogHelper.LogType.Warning);
// 如果路径无效,返回透明图像
return resultBitmap;
}
}
}
LogHelper.WriteLogToFile($"成功应用形状遮罩,路径点数: {path.Count}", LogHelper.LogType.Info);
return resultBitmap;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用形状遮罩失败: {ex.Message}", LogHelper.LogType.Error);
// 返回完全透明的图像而不是原始图像
var transparentBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(transparentBitmap))
{
g.Clear(System.Drawing.Color.Transparent);
}
return transparentBitmap;
}
}
// 获取DPI缩放比例
private double GetDpiScale()
{
var source = PresentationSource.FromVisual(this);
if (source?.CompositionTarget != null)
{
return source.CompositionTarget.TransformToDevice.M11;
}
return 1.0; // 默认DPI
}
// 将System.Drawing.Bitmap转换为WPF BitmapSource
private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap)
{
try
{
using (var memory = new MemoryStream())
{
// 使用PNG格式保存,确保透明度信息不丢失
bitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"转换位图失败: {ex.Message}", LogHelper.LogType.Error);
throw;
}
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+403 -20
View File
@@ -1,8 +1,10 @@
using Hardcodet.Wpf.TaskbarNotification;
using Hardcodet.Wpf.TaskbarNotification;
using Ink_Canvas.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OSVersionExtension;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
@@ -10,8 +12,10 @@ using System.Windows.Ink;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using File = System.IO.File;
using OperatingSystem = OSVersionExtension.OperatingSystem;
using WinForms = System.Windows.Forms;
namespace Ink_Canvas
{
@@ -28,12 +32,103 @@ namespace Ink_Canvas
{
string text = File.ReadAllText(App.RootPath + settingsFileName);
Settings = JsonConvert.DeserializeObject<Settings>(text);
if (Settings != null)
{
CleanupObsoleteSettings(text);
}
// 验证设置是否成功加载
if (Settings == null)
{
LogHelper.WriteLogToFile("配置文件解析失败,尝试从备份恢复", LogHelper.LogType.Warning);
if (AutoBackupManager.TryRestoreFromBackup())
{
// 重新尝试加载
text = File.ReadAllText(App.RootPath + settingsFileName);
Settings = JsonConvert.DeserializeObject<Settings>(text);
if (Settings != null)
{
// 清理过期配置项
CleanupObsoleteSettings(text);
}
}
// 如果仍然失败,使用默认设置
if (Settings == null)
{
LogHelper.WriteLogToFile("从备份恢复失败,使用默认设置", LogHelper.LogType.Warning);
BtnResetToSuggestion_Click(null, null);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"配置文件加载失败: {ex.Message}", LogHelper.LogType.Error);
// 尝试从备份恢复
LogHelper.WriteLogToFile("尝试从备份恢复配置文件", LogHelper.LogType.Warning);
if (AutoBackupManager.TryRestoreFromBackup())
{
try
{
string text = File.ReadAllText(App.RootPath + settingsFileName);
Settings = JsonConvert.DeserializeObject<Settings>(text);
if (Settings != null)
{
// 清理过期配置项
CleanupObsoleteSettings(text);
}
}
catch (Exception restoreEx)
{
LogHelper.WriteLogToFile($"从备份恢复后重新加载失败: {restoreEx.Message}", LogHelper.LogType.Error);
BtnResetToSuggestion_Click(null, null);
}
}
// 如果仍然失败,使用默认设置
if (Settings == null)
{
LogHelper.WriteLogToFile("从备份恢复失败,使用默认设置", LogHelper.LogType.Warning);
BtnResetToSuggestion_Click(null, null);
}
}
catch { }
}
else
{
BtnResetToSuggestion_Click(null, null);
LogHelper.WriteLogToFile("配置文件不存在,尝试从备份恢复", LogHelper.LogType.Warning);
if (AutoBackupManager.TryRestoreFromBackup())
{
try
{
string text = File.ReadAllText(App.RootPath + settingsFileName);
Settings = JsonConvert.DeserializeObject<Settings>(text);
if (Settings != null)
{
// 清理过期配置项
CleanupObsoleteSettings(text);
}
}
catch (Exception restoreEx)
{
LogHelper.WriteLogToFile($"从备份恢复后加载失败: {restoreEx.Message}", LogHelper.LogType.Error);
BtnResetToSuggestion_Click(null, null);
}
}
else
{
// 备份恢复失败(备份目录不存在等),使用默认设置
LogHelper.WriteLogToFile("备份恢复失败,使用默认设置", LogHelper.LogType.Warning);
BtnResetToSuggestion_Click(null, null);
}
// 如果仍然失败,使用默认设置
if (Settings == null)
{
LogHelper.WriteLogToFile("从备份恢复失败,使用默认设置", LogHelper.LogType.Warning);
BtnResetToSuggestion_Click(null, null);
}
}
}
catch (Exception ex)
@@ -54,10 +149,15 @@ namespace Ink_Canvas
{
ToggleSwitchRunAtStartup.IsOn = true;
}
else
{
ToggleSwitchRunAtStartup.IsOn = false;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile(ex.ToString(), LogHelper.LogType.Error);
ToggleSwitchRunAtStartup.IsOn = false;
}
if (Settings.Startup != null)
@@ -70,7 +170,7 @@ namespace Ink_Canvas
Settings.Automation.AutoDelSavedFilesDaysThreshold);
}
if (Settings.Startup.IsFoldAtStartup)
if (Settings.Startup.IsFoldAtStartup && !App.StartWithBoardMode)
{
FoldFloatingBar_MouseUp(Fold_Icon, null);
}
@@ -108,7 +208,7 @@ namespace Ink_Canvas
}
}
// ToggleSwitchIsAutoUpdateWithSilence.Visibility = Settings.Startup.IsAutoUpdate ? Visibility.Visible : Visibility.Collapsed;
ToggleSwitchIsAutoUpdateWithSilence.Visibility = Settings.Startup.IsAutoUpdate ? Visibility.Visible : Visibility.Collapsed;
if (Settings.Startup.IsAutoUpdateWithSilence)
{
ToggleSwitchIsAutoUpdateWithSilence.IsOn = true;
@@ -141,6 +241,10 @@ namespace Ink_Canvas
else
{
Settings.Startup = new Startup();
Settings.Startup.IsEnableNibMode = false; // 默认关闭笔尖模式
ToggleSwitchEnableNibMode.IsOn = false; // 默认关闭笔尖模式
BoardToggleSwitchEnableNibMode.IsOn = false; // 默认关闭笔尖模式
BoundsWidth = Settings.Advanced.FingerModeBoundsWidth; // 使用手指模式边界宽度
}
// 恢复崩溃后操作设置
@@ -236,10 +340,17 @@ namespace Ink_Canvas
break;
}
// 设置主题下拉框
ComboBoxTheme.SelectedIndex = Settings.Appearance.Theme;
ComboBoxChickenSoupSource.SelectedIndex = Settings.Appearance.ChickenSoupSource;
ToggleSwitchEnableQuickPanel.IsOn = Settings.Appearance.IsShowQuickPanel;
ToggleSwitchEnableSplashScreen.IsOn = Settings.Appearance.EnableSplashScreen;
ComboBoxSplashScreenStyle.SelectedIndex = Settings.Appearance.SplashScreenStyle;
ToggleSwitchEnableTrayIcon.IsOn = Settings.Appearance.EnableTrayIcon;
ICCTrayIconExampleImage.Visibility =
Settings.Appearance.EnableTrayIcon ? Visibility.Visible : Visibility.Collapsed;
@@ -248,6 +359,10 @@ namespace Ink_Canvas
ViewboxFloatingBar.Opacity = Settings.Appearance.ViewboxFloatingBarOpacityValue;
// 初始化浮动栏透明度滑块值
ViewboxFloatingBarOpacityValueSlider.Value = Settings.Appearance.ViewboxFloatingBarOpacityValue;
ViewboxFloatingBarOpacityInPPTValueSlider.Value = Settings.Appearance.ViewboxFloatingBarOpacityInPPTValue;
if (Settings.Appearance.EnableViewboxBlackBoardScaleTransform) // 画板 UI 缩放 80%
{
//ViewboxBlackboardLeftSideScaleTransform.ScaleX = 0.8;
@@ -307,24 +422,28 @@ namespace Ink_Canvas
Settings.Appearance.EnableChickenSoupInWhiteboardMode;
// 浮动栏按钮显示控制开关初始化
ToggleSwitchShowShapeButton.IsOn = Settings.Appearance.IsShowShapeButton;
ToggleSwitchShowUndoButton.IsOn = Settings.Appearance.IsShowUndoButton;
ToggleSwitchShowRedoButton.IsOn = Settings.Appearance.IsShowRedoButton;
ToggleSwitchShowClearButton.IsOn = Settings.Appearance.IsShowClearButton;
ToggleSwitchShowWhiteboardButton.IsOn = Settings.Appearance.IsShowWhiteboardButton;
ToggleSwitchShowHideButton.IsOn = Settings.Appearance.IsShowHideButton;
ToggleSwitchShowQuickColorPalette.IsOn = Settings.Appearance.IsShowQuickColorPalette;
ToggleSwitchShowLassoSelectButton.IsOn = Settings.Appearance.IsShowLassoSelectButton;
ToggleSwitchShowClearAndMouseButton.IsOn = Settings.Appearance.IsShowClearAndMouseButton;
CheckBoxUseLegacyFloatingBarUI.IsChecked = Settings.Appearance.UseLegacyFloatingBarUI;
CheckBoxShowShapeButton.IsChecked = Settings.Appearance.IsShowShapeButton;
CheckBoxShowUndoButton.IsChecked = Settings.Appearance.IsShowUndoButton;
CheckBoxShowRedoButton.IsChecked = Settings.Appearance.IsShowRedoButton;
CheckBoxShowClearButton.IsChecked = Settings.Appearance.IsShowClearButton;
CheckBoxShowWhiteboardButton.IsChecked = Settings.Appearance.IsShowWhiteboardButton;
CheckBoxShowHideButton.IsChecked = Settings.Appearance.IsShowHideButton;
CheckBoxShowQuickColorPalette.IsChecked = Settings.Appearance.IsShowQuickColorPalette;
CheckBoxShowLassoSelectButton.IsChecked = Settings.Appearance.IsShowLassoSelectButton;
CheckBoxShowClearAndMouseButton.IsChecked = Settings.Appearance.IsShowClearAndMouseButton;
ComboBoxEraserDisplayOption.SelectedIndex = Settings.Appearance.EraserDisplayOption;
ComboBoxQuickColorPaletteDisplayMode.SelectedIndex = Settings.Appearance.QuickColorPaletteDisplayMode;
// 初始化快捷调色盘指示器
UpdateQuickColorPaletteIndicator(inkCanvas.DefaultDrawingAttributes.Color);
// 应用浮动栏按钮可见性设置
UpdateFloatingBarButtonsVisibility();
// 更新浮动栏图标
UpdateFloatingBarIcons();
SystemEvents_UserPreferenceChanged(null, null);
}
else
@@ -367,6 +486,9 @@ namespace Ink_Canvas
ToggleSwitchEnablePPTButtonPageClickable.IsOn =
Settings.PowerPointSettings.EnablePPTButtonPageClickable;
ToggleSwitchEnablePPTButtonLongPressPageTurn.IsOn =
Settings.PowerPointSettings.EnablePPTButtonLongPressPageTurn;
var dops = Settings.PowerPointSettings.PPTButtonsDisplayOption.ToString();
var dopsc = dops.ToCharArray();
if ((dopsc[0] == '1' || dopsc[0] == '2') && (dopsc[1] == '1' || dopsc[1] == '2') &&
@@ -427,6 +549,10 @@ namespace Ink_Canvas
PPTButtonRightPositionValueSlider.Value = Settings.PowerPointSettings.PPTRSButtonPosition;
PPTButtonLBPositionValueSlider.Value = Settings.PowerPointSettings.PPTLBButtonPosition;
PPTButtonRBPositionValueSlider.Value = Settings.PowerPointSettings.PPTRBButtonPosition;
UpdatePPTBtnSlidersStatus();
UpdatePPTBtnPreview();
@@ -439,6 +565,8 @@ namespace Ink_Canvas
ToggleSwitchSupportWPS.IsOn = Settings.PowerPointSettings.IsSupportWPS;
ToggleSwitchPowerPointEnhancement.IsOn = Settings.PowerPointSettings.EnablePowerPointEnhancement;
ToggleSwitchAutoSaveScreenShotInPowerPoint.IsOn =
Settings.PowerPointSettings.IsAutoSaveScreenShotInPowerPoint;
ToggleSwitchEnableWppProcessKill.IsOn = Settings.PowerPointSettings.EnableWppProcessKill;
@@ -477,7 +605,7 @@ namespace Ink_Canvas
ToggleSwitchEnableTwoFingerTranslate.IsOn = false;
BoardToggleSwitchEnableTwoFingerTranslate.IsOn = false;
Settings.Gesture.IsEnableTwoFingerTranslate = false;
if (!isInMultiTouchMode) ToggleSwitchEnableMultiTouchMode.IsOn = true;
// if (!isInMultiTouchMode) ToggleSwitchEnableMultiTouchMode.IsOn = true;
}
else
{
@@ -539,6 +667,7 @@ namespace Ink_Canvas
// 初始化屏蔽压感开关状态
ToggleSwitchDisablePressure.IsOn = Settings.Canvas.DisablePressure;
inkCanvas.DefaultDrawingAttributes.IgnorePressure = Settings.Canvas.DisablePressure;
ComboBoxPenStyle.SelectedIndex = Settings.Canvas.InkStyle;
BoardComboBoxPenStyle.SelectedIndex = Settings.Canvas.InkStyle;
@@ -667,6 +796,7 @@ namespace Ink_Canvas
if (Settings.Canvas != null)
{
ToggleSwitchEnablePalmEraser.IsOn = Settings.Canvas.EnablePalmEraser;
ComboBoxPalmEraserSensitivity.SelectedIndex = Settings.Canvas.PalmEraserSensitivity;
}
// Advanced
@@ -678,6 +808,7 @@ namespace Ink_Canvas
ToggleSwitchIsLogEnabled.IsOn = Settings.Advanced.IsLogEnabled;
ToggleSwitchIsSaveLogByDate.IsOn = Settings.Advanced.IsSaveLogByDate;
ToggleSwitchIsSecondConfimeWhenShutdownApp.IsOn = Settings.Advanced.IsSecondConfirmWhenShutdownApp;
ToggleSwitchWindowMode.IsOn = Settings.Advanced.WindowMode;
ToggleSwitchIsSpecialScreen.IsOn = Settings.Advanced.IsSpecialScreen;
ToggleSwitchIsQuadIR.IsOn = Settings.Advanced.IsQuadIR;
ToggleSwitchEraserBindTouchMultiplier.IsOn = Settings.Advanced.EraserBindTouchMultiplier;
@@ -688,6 +819,17 @@ namespace Ink_Canvas
ToggleSwitchIsEnableDPIChangeDetection.IsOn = Settings.Advanced.IsEnableDPIChangeDetection;
ToggleSwitchIsEnableAvoidFullScreenHelper.IsOn = Settings.Advanced.IsEnableAvoidFullScreenHelper;
ToggleSwitchIsAutoBackupBeforeUpdate.IsOn = Settings.Advanced.IsAutoBackupBeforeUpdate;
ToggleSwitchIsAutoBackupEnabled.IsOn = Settings.Advanced.IsAutoBackupEnabled;
// 设置备份间隔下拉框
foreach (ComboBoxItem item in ComboBoxAutoBackupInterval.Items)
{
if (item.Tag != null && int.TryParse(item.Tag.ToString(), out int interval) && interval == Settings.Advanced.AutoBackupIntervalDays)
{
ComboBoxAutoBackupInterval.SelectedItem = item;
break;
}
}
if (Settings.Advanced.IsEnableFullScreenHelper)
{
FullScreenHelper.MarkFullscreenWindowTaskbarList(new WindowInteropHelper(this).Handle, true);
@@ -695,6 +837,14 @@ namespace Ink_Canvas
if (Settings.Advanced.IsEnableAvoidFullScreenHelper)
{
AvoidFullScreenHelper.StartAvoidFullScreen(this);
Dispatcher.BeginInvoke(new Action(() =>
{
if (isLoaded)
{
MoveWindow(new WindowInteropHelper(this).Handle, 0, 0,
WinForms.Screen.PrimaryScreen.Bounds.Width, WinForms.Screen.PrimaryScreen.Bounds.Height, true);
}
}), DispatcherPriority.ApplicationIdle);
}
if (Settings.Advanced.IsEnableEdgeGestureUtil)
{
@@ -740,10 +890,36 @@ namespace Ink_Canvas
RandWindowOnceCloseLatencySlider.Value = Settings.RandSettings.RandWindowOnceCloseLatency;
RandWindowOnceMaxStudentsSlider.Value = Settings.RandSettings.RandWindowOnceMaxStudents;
ToggleSwitchShowRandomAndSingleDraw.IsOn = Settings.RandSettings.ShowRandomAndSingleDraw;
ToggleSwitchDirectCallCiRand.IsOn = Settings.RandSettings.DirectCallCiRand;
ToggleSwitchEnableQuickDraw.IsOn = Settings.RandSettings.EnableQuickDraw;
ToggleSwitchExternalCaller.IsOn = Settings.RandSettings.DirectCallCiRand;
ComboBoxExternalCallerType.SelectedIndex = Settings.RandSettings.ExternalCallerType;
RandomDrawPanel.Visibility = Settings.RandSettings.ShowRandomAndSingleDraw ? Visibility.Visible : Visibility.Collapsed;
SingleDrawPanel.Visibility = Settings.RandSettings.ShowRandomAndSingleDraw ? Visibility.Visible : Visibility.Collapsed;
// 计时器设置
ToggleSwitchUseLegacyTimerUI.IsOn = Settings.RandSettings.UseLegacyTimerUI;
ToggleSwitchUseNewStyleUI.IsOn = Settings.RandSettings.UseNewStyleUI;
ToggleSwitchEnableOvertimeCountUp.IsOn = Settings.RandSettings.EnableOvertimeCountUp;
// 新点名UI设置
ToggleSwitchUseNewRollCallUI.IsOn = Settings.RandSettings.UseNewRollCallUI;
ToggleSwitchEnableMLAvoidance.IsOn = Settings.RandSettings.EnableMLAvoidance;
MLAvoidanceHistorySlider.Value = Settings.RandSettings.MLAvoidanceHistoryCount;
MLAvoidanceWeightSlider.Value = Settings.RandSettings.MLAvoidanceWeight;
bool canEnableRedText = Settings.RandSettings.EnableOvertimeCountUp && Settings.RandSettings.EnableOvertimeRedText;
ToggleSwitchEnableOvertimeRedText.IsOn = canEnableRedText;
if (!canEnableRedText)
{
Settings.RandSettings.EnableOvertimeRedText = false;
}
TimerVolumeSlider.Value = Settings.RandSettings.TimerVolume;
// 渐进提醒设置
ToggleSwitchEnableProgressiveReminder.IsOn = Settings.RandSettings.EnableProgressiveReminder;
ProgressiveReminderVolumeSlider.Value = Settings.RandSettings.ProgressiveReminderVolume;
// 加载自定义点名背景
UpdatePickNameBackgroundsInComboBox();
@@ -760,7 +936,44 @@ namespace Ink_Canvas
ToggleSwitchDisplayRandWindowNamesInputBtn.IsOn = Settings.RandSettings.DisplayRandWindowNamesInputBtn;
RandWindowOnceCloseLatencySlider.Value = Settings.RandSettings.RandWindowOnceCloseLatency;
RandWindowOnceMaxStudentsSlider.Value = Settings.RandSettings.RandWindowOnceMaxStudents;
ToggleSwitchDirectCallCiRand.IsOn = Settings.RandSettings.DirectCallCiRand;
ToggleSwitchEnableQuickDraw.IsOn = Settings.RandSettings.EnableQuickDraw;
ToggleSwitchExternalCaller.IsOn = Settings.RandSettings.DirectCallCiRand;
ComboBoxExternalCallerType.SelectedIndex = Settings.RandSettings.ExternalCallerType;
ToggleSwitchUseLegacyTimerUI.IsOn = Settings.RandSettings.UseLegacyTimerUI;
ToggleSwitchUseNewStyleUI.IsOn = Settings.RandSettings.UseNewStyleUI;
ToggleSwitchEnableOvertimeCountUp.IsOn = Settings.RandSettings.EnableOvertimeCountUp;
bool canEnableRedText = Settings.RandSettings.EnableOvertimeCountUp && Settings.RandSettings.EnableOvertimeRedText;
ToggleSwitchEnableOvertimeRedText.IsOn = canEnableRedText;
if (!canEnableRedText)
{
Settings.RandSettings.EnableOvertimeRedText = false;
}
TimerVolumeSlider.Value = Settings.RandSettings.TimerVolume;
// 渐进提醒设置
ToggleSwitchEnableProgressiveReminder.IsOn = Settings.RandSettings.EnableProgressiveReminder;
ProgressiveReminderVolumeSlider.Value = Settings.RandSettings.ProgressiveReminderVolume;
}
// ModeSettings
if (Settings.ModeSettings != null)
{
ToggleSwitchMode.IsOn = Settings.ModeSettings.IsPPTOnlyMode;
// 根据加载的配置状态执行相应的窗口显示/隐藏逻辑
if (isStartup && Settings.ModeSettings.IsPPTOnlyMode)
{
// 启动时如果是仅PPT模式,隐藏主窗口
Hide();
LogHelper.WriteLogToFile("启动时检测到仅PPT模式,主窗口已隐藏", LogHelper.LogType.Event);
}
}
else
{
Settings.ModeSettings = new ModeSettings();
ToggleSwitchMode.IsOn = false;
}
// Automation
@@ -814,6 +1027,8 @@ namespace Ink_Canvas
ToggleSwitchAutoFoldAfterPPTSlideShow.IsOn = Settings.Automation.IsAutoFoldAfterPPTSlideShow;
ToggleSwitchKeepFoldAfterSoftwareExit.IsOn = Settings.Automation.KeepFoldAfterSoftwareExit;
if (Settings.Automation.IsAutoKillEasiNote || Settings.Automation.IsAutoKillPptService ||
Settings.Automation.IsAutoKillHiteAnnotation || Settings.Automation.IsAutoKillInkCanvas
|| Settings.Automation.IsAutoKillICA || Settings.Automation.IsAutoKillIDT ||
@@ -852,6 +1067,23 @@ namespace Ink_Canvas
ToggleSwitchSaveFullPageStrokes.IsOn = Settings.Automation.IsSaveFullPageStrokes;
// 加载定时保存墨迹设置
ToggleSwitchEnableAutoSaveStrokes.IsOn = Settings.Automation.IsEnableAutoSaveStrokes;
// 初始化保存间隔下拉框
if (ComboBoxAutoSaveStrokesInterval != null)
{
int intervalMinutes = Settings.Automation.AutoSaveStrokesIntervalMinutes;
if (intervalMinutes < 1) intervalMinutes = 5; // 默认5分钟
foreach (System.Windows.Controls.ComboBoxItem item in ComboBoxAutoSaveStrokesInterval.Items)
{
if (item.Tag != null && int.TryParse(item.Tag.ToString(), out int tagValue) && tagValue == intervalMinutes)
{
ComboBoxAutoSaveStrokesInterval.SelectedItem = item;
break;
}
}
}
SideControlMinimumAutomationSlider.Value = Settings.Automation.MinimumAutomationStrokeNumber;
AutoSavedStrokesLocation.Text = Settings.Automation.AutoSavedStrokesLocation;
@@ -861,6 +1093,9 @@ namespace Ink_Canvas
// 加载退出收纳模式自动切换至批注模式设置
ToggleSwitchAutoEnterAnnotationModeWhenExitFoldMode.IsOn = Settings.Automation.IsAutoEnterAnnotationModeWhenExitFoldMode;
// 加载退出白板时自动收纳设置
ToggleSwitchAutoFoldWhenExitWhiteboard.IsOn = Settings.Automation.IsAutoFoldWhenExitWhiteboard;
}
else
{
@@ -876,6 +1111,154 @@ namespace Ink_Canvas
{
ViewboxFloatingBarMarginAnimation(100, true);
}
// 加载墨迹渐隐设置
LoadInkFadeSettings();
}
/// <summary>
/// 加载墨迹渐隐设置
/// </summary>
private void LoadInkFadeSettings()
{
try
{
// 同步设置面板中的开关状态
if (ToggleSwitchEnableInkFade != null)
{
ToggleSwitchEnableInkFade.IsOn = Settings.Canvas.EnableInkFade;
}
// 同步批注子面板中的开关状态
if (ToggleSwitchInkFadeInPanel != null)
{
ToggleSwitchInkFadeInPanel.IsOn = Settings.Canvas.EnableInkFade;
}
// 同步普通画笔面板中的开关状态
if (ToggleSwitchInkFadeInPanel2 != null)
{
ToggleSwitchInkFadeInPanel2.IsOn = Settings.Canvas.EnableInkFade;
}
// 同步滑块值
if (InkFadeTimeSlider != null)
{
InkFadeTimeSlider.Value = Settings.Canvas.InkFadeTime;
}
// 同步墨迹渐隐管理器的状态
if (_inkFadeManager != null)
{
_inkFadeManager.IsEnabled = Settings.Canvas.EnableInkFade;
_inkFadeManager.UpdateFadeTime(Settings.Canvas.InkFadeTime);
}
LogHelper.WriteLogToFile("墨迹渐隐设置已加载", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载墨迹渐隐设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <param name="userConfigJson">用户配置的JSON字符串</param>
private void CleanupObsoleteSettings(string userConfigJson)
{
try
{
// 创建默认配置对象
Settings defaultSettings = new Settings();
// 将默认配置和用户配置都序列化为JObject
JObject defaultConfigObj = JObject.FromObject(defaultSettings);
JObject userConfigObj = JObject.Parse(userConfigJson);
// 记录是否有清理操作
bool hasChanges = false;
// 递归比较并删除用户配置中多余的键
RemoveObsoleteProperties(userConfigObj, defaultConfigObj, ref hasChanges);
// 如果有清理操作,重新反序列化并保存
if (hasChanges)
{
string cleanedJson = userConfigObj.ToString(Formatting.Indented);
Settings = JsonConvert.DeserializeObject<Settings>(cleanedJson);
SaveSettingsToFile();
LogHelper.WriteLogToFile("已清理过期配置项", LogHelper.LogType.Event);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理过期配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <param name="userObj">用户配置的JObject</param>
/// <param name="defaultObj">默认配置的JObject</param>
/// <param name="hasChanges">是否有变更的引用标志</param>
private void RemoveObsoleteProperties(JObject userObj, JObject defaultObj, ref bool hasChanges)
{
if (userObj == null || defaultObj == null)
return;
// 获取需要删除的键列表(避免在遍历时修改集合)
List<string> keysToRemove = new List<string>();
foreach (var property in userObj.Properties())
{
string propertyName = property.Name;
// 如果默认配置中不存在该属性,标记为删除
if (!defaultObj.ContainsKey(propertyName))
{
keysToRemove.Add(propertyName);
continue;
}
// 如果两个属性都是对象类型,递归比较
JToken userValue = property.Value;
JToken defaultValue = defaultObj[propertyName];
if (userValue != null && defaultValue != null)
{
if (userValue.Type == JTokenType.Object && defaultValue.Type == JTokenType.Object)
{
RemoveObsoleteProperties(userValue as JObject, defaultValue as JObject, ref hasChanges);
}
// 处理数组中的对象(如自定义图标列表等)
else if (userValue.Type == JTokenType.Array && defaultValue.Type == JTokenType.Array)
{
JArray userArray = userValue as JArray;
JArray defaultArray = defaultValue as JArray;
if (userArray != null && defaultArray != null && userArray.Count > 0 && defaultArray.Count > 0)
{
// 如果数组元素是对象,比较第一个元素的属性结构
if (userArray[0].Type == JTokenType.Object && defaultArray[0].Type == JTokenType.Object)
{
for (int i = 0; i < userArray.Count; i++)
{
if (userArray[i] is JObject userItemObj && defaultArray[0] is JObject defaultItemObj)
{
RemoveObsoleteProperties(userItemObj, defaultItemObj, ref hasChanges);
}
}
}
}
}
}
}
// 删除标记的键
foreach (string key in keysToRemove)
{
userObj.Remove(key);
hasChanges = true;
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,7 @@ using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Point = System.Windows.Point;
namespace Ink_Canvas
@@ -17,21 +18,19 @@ namespace Ink_Canvas
{
private StrokeCollection newStrokes = new StrokeCollection();
private List<Circle> circles = new List<Circle>();
private const double LINE_STRAIGHTEN_THRESHOLD = 0.20; // 默认灵敏度阈值,与UI默认值对应
private const double LINE_STRAIGHTEN_THRESHOLD = 0.20;
// 矩形参考线系统
private List<RectangleGuideLine> rectangleGuideLines = new List<RectangleGuideLine>();
private const double RECTANGLE_ENDPOINT_THRESHOLD = 30.0; // 端点相交判断阈值
private const double RECTANGLE_ANGLE_THRESHOLD = 15.0; // 角度判断阈值(度)
private const double RECTANGLE_ENDPOINT_THRESHOLD = 30.0;
private const double RECTANGLE_ANGLE_THRESHOLD = 15.0;
// 矩形参考线数据结构
private class RectangleGuideLine
{
public Stroke OriginalStroke { get; set; }
public Point StartPoint { get; set; }
public Point EndPoint { get; set; }
public DateTime CreatedTime { get; set; }
public double Angle { get; set; } // 直线角度(弧度)
public double Angle { get; set; }
public bool IsHorizontal { get; set; }
public bool IsVertical { get; set; }
@@ -56,17 +55,60 @@ namespace Ink_Canvas
private void inkCanvas_StrokeCollected(object sender, InkCanvasStrokeCollectedEventArgs e)
{
// 检查是否启用墨迹渐隐功能
if (Settings.Canvas.EnableInkFade)
{
// 获取墨迹的起点和终点
var startPoint = e.Stroke.StylusPoints.Count > 0 ? e.Stroke.StylusPoints[0].ToPoint() : new Point();
var endPoint = e.Stroke.StylusPoints.Count > 0 ? e.Stroke.StylusPoints[e.Stroke.StylusPoints.Count - 1].ToPoint() : new Point();
if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
// 添加到墨迹渐隐管理器
if (_inkFadeManager != null)
{
_inkFadeManager.AddFadingStroke(e.Stroke, startPoint, endPoint);
}
else
{
LogHelper.WriteLogToFile("StrokeCollected: 墨迹渐隐管理器为空,无法添加墨迹", LogHelper.LogType.Error);
}
Dispatcher.BeginInvoke(new Action(() =>
{
try
{
if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
if (inkCanvas.Strokes.Contains(e.Stroke))
{
inkCanvas.Strokes.Remove(e.Stroke);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"延迟移除墨迹时出错: {ex}", LogHelper.LogType.Error);
}
}), DispatcherPriority.Background);
return;
}
// 标记是否进行了直线拉直
bool wasStraightened = false;
// 禁用原有的FitToCurve,使用新的高级贝塞尔曲线平滑
if (Settings.Canvas.FitToCurve) drawingAttributes.FitToCurve = false;
try
{
inkCanvas.Opacity = 1;
// 应用屏蔽压感功能 - 如果启用,所有笔画都使用统一粗细
if (Settings.Canvas.DisablePressure)
{
var uniformPoints = new StylusPointCollection();
@@ -77,13 +119,11 @@ namespace Ink_Canvas
}
e.Stroke.StylusPoints = uniformPoints;
}
// 应用压感触屏模式 - 如果启用并且检测到触屏输入
else if (Settings.Canvas.EnablePressureTouchMode)
{
bool isTouchInput = true;
foreach (StylusPoint point in e.Stroke.StylusPoints)
{
// 检测是否为压感笔输入(压感笔的PressureFactor不等于0.5或0
if ((point.PressureFactor > 0.501 || point.PressureFactor < 0.5) && point.PressureFactor != 0)
{
isTouchInput = false;
@@ -91,7 +131,6 @@ namespace Ink_Canvas
}
}
// 如果是触屏输入,则应用模拟压感
if (isTouchInput)
{
switch (Settings.Canvas.InkStyle)
@@ -184,39 +223,30 @@ namespace Ink_Canvas
// 检查是否启用了直线自动拉直功能
if (Settings.Canvas.AutoStraightenLine && IsPotentialStraightLine(e.Stroke))
{
// Get start and end points of the stroke
Point startPoint = e.Stroke.StylusPoints[0].ToPoint();
Point endPoint = e.Stroke.StylusPoints[e.Stroke.StylusPoints.Count - 1].ToPoint();
Point endpoint1, endpoint2;
bool shouldStraighten = TryGetStraightLineEndpoints(e.Stroke, out endpoint1, out endpoint2);
// 先完成所有直线判定,再考虑端点吸附
// 读取实际的灵敏度设置值
double sensitivity = Settings.InkToShape.LineStraightenSensitivity;
Debug.WriteLine($"当前灵敏度值: {sensitivity}");
// 判断是否应该拉直线条
bool shouldStraighten = ShouldStraightenLine(e.Stroke);
// 输出一些调试信息,帮助理解灵敏度设置的效果
Debug.WriteLine($"LineStraightenSensitivity: {Settings.InkToShape.LineStraightenSensitivity}, ShouldStraighten: {shouldStraighten}");
// 只有当确定要拉直线条时,才检查端点吸附
if (shouldStraighten && Settings.Canvas.LineEndpointSnapping)
{
// 只有在启用了形状识别(矩形或三角形)时才执行端点吸附
if (Settings.InkToShape.IsInkToShapeRectangle || Settings.InkToShape.IsInkToShapeTriangle)
{
Point[] snappedPoints = GetSnappedEndpoints(startPoint, endPoint);
if (snappedPoints != null)
{
startPoint = snappedPoints[0];
endPoint = snappedPoints[1];
}
}
}
// 如果确定要拉直,则创建直线
if (shouldStraighten)
{
Point startPoint = endpoint1;
Point endPoint = endpoint2;
// 只有当确定要拉直线条时,才检查端点吸附
if (Settings.Canvas.LineEndpointSnapping)
{
// 只有在启用了形状识别(矩形或三角形)时才执行端点吸附
if (Settings.InkToShape.IsInkToShapeRectangle || Settings.InkToShape.IsInkToShapeTriangle)
{
Point[] snappedPoints = GetSnappedEndpoints(startPoint, endPoint);
if (snappedPoints != null)
{
startPoint = snappedPoints[0];
endPoint = snappedPoints[1];
}
}
}
// 创建直线
StylusPointCollection straightLinePoints = CreateStraightLine(startPoint, endPoint);
Stroke straightStroke = new Stroke(straightLinePoints)
{
@@ -690,16 +720,22 @@ namespace Ink_Canvas
catch { }
// 应用高级贝塞尔曲线平滑(仅在未进行直线拉直时)
Debug.WriteLine($"墨迹平滑检查: UseAdvancedBezierSmoothing={Settings.Canvas.UseAdvancedBezierSmoothing}, wasStraightened={wasStraightened}");
Debug.WriteLine($"异步平滑设置: UseAsyncInkSmoothing={Settings.Canvas.UseAsyncInkSmoothing}, _inkSmoothingManager={_inkSmoothingManager != null}");
if (Settings.Canvas.UseAdvancedBezierSmoothing && !wasStraightened)
{
try
{
Debug.WriteLine($"开始墨迹平滑处理: 原始点数={e.Stroke.StylusPoints.Count}, 直线拉直={wasStraightened}");
// 检查原始笔画是否仍然存在于画布中
if (inkCanvas.Strokes.Contains(e.Stroke))
{
// 使用新的异步墨迹平滑管理器
if (Settings.Canvas.UseAsyncInkSmoothing && _inkSmoothingManager != null)
{
Debug.WriteLine("使用异步墨迹平滑");
// 异步处理
_ = ProcessStrokeAsync(e.Stroke);
}
@@ -719,6 +755,10 @@ namespace Ink_Canvas
}
}
}
else
{
Debug.WriteLine("原始笔画不在画布中,跳过平滑处理");
}
}
catch (Exception ex)
{
@@ -739,17 +779,27 @@ namespace Ink_Canvas
{
try
{
Debug.WriteLine($"异步平滑开始: 原始点数={originalStroke.StylusPoints.Count}");
await _inkSmoothingManager.SmoothStrokeAsync(originalStroke, (original, smoothed) =>
{
Debug.WriteLine($"异步平滑完成: 原始点数={original.StylusPoints.Count}, 平滑后点数={smoothed.StylusPoints.Count}");
Debug.WriteLine($"墨迹比较: smoothed != original = {smoothed != original}");
Debug.WriteLine($"画布包含原始墨迹: {inkCanvas.Strokes.Contains(original)}");
// 在UI线程上执行笔画替换
if (inkCanvas.Strokes.Contains(original) && smoothed != original)
{
Debug.WriteLine("异步替换原始笔画为平滑后的笔画");
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
inkCanvas.Strokes.Remove(original);
inkCanvas.Strokes.Add(smoothed);
_currentCommitType = CommitReason.UserInput;
}
else
{
Debug.WriteLine($"异步平滑后的笔画与原始笔画相同,未进行替换 (contains={inkCanvas.Strokes.Contains(original)}, different={smoothed != original})");
}
});
}
catch (Exception ex)
@@ -785,65 +835,69 @@ namespace Ink_Canvas
// 获取用户设置的灵敏度值,确保使用正确的设置
double sensitivity = Settings.InkToShape.LineStraightenSensitivity;
// 输出当前灵敏度值(调试用)
// 输出当前灵敏度值
Debug.WriteLine($"IsPotentialStraightLine - sensitivity: {sensitivity}, length: {lineLength}");
// 根据灵敏度调整快速检查阈值
double quickThreshold;
// 如果灵敏度超过1.0,使用更宽松的快速检查标准
if (sensitivity > 1.0)
{
// 高灵敏度模式 - 使用更宽松的阈值
quickThreshold = Math.Min(0.2 + (sensitivity - 1.0) * 0.3, 0.5); // 映射到0.2-0.5范围
}
else
{
// 常规灵敏度模式
quickThreshold = Math.Min(sensitivity * 1.5, 0.20);
}
// 将灵敏度转换为阈值:灵敏度0.05-2.0映射到阈值0.01-0.4
double quickThreshold = Math.Max(0.01, sensitivity * 0.2); // 确保最小阈值为0.01
Debug.WriteLine($"使用快速检查阈值: {quickThreshold}");
// 快速检查:计算几个关键点与直线的距离
if (stroke.StylusPoints.Count >= 10)
{
// 取中点和1/4、3/4位置的点,快速检查偏差
int quarterIdx = stroke.StylusPoints.Count / 4;
int midIdx = stroke.StylusPoints.Count / 2;
int threeQuarterIdx = quarterIdx * 3;
List<Point> checkPoints;
Point quarterPoint = stroke.StylusPoints[quarterIdx].ToPoint();
Point midPoint = stroke.StylusPoints[midIdx].ToPoint();
Point threeQuarterPoint = stroke.StylusPoints[threeQuarterIdx].ToPoint();
double quarterDeviation = DistanceFromLineToPoint(start, end, quarterPoint);
double midDeviation = DistanceFromLineToPoint(start, end, midPoint);
double threeQuarterDeviation = DistanceFromLineToPoint(start, end, threeQuarterPoint);
// 使用相对偏差:偏差与线长的比例,并使用灵敏度进行调整
double quickRelativeThreshold = lineLength * quickThreshold;
// 记录检测到的偏差(调试用)
Debug.WriteLine($"Deviations: q={quarterDeviation}, m={midDeviation}, tq={threeQuarterDeviation}, threshold={quickRelativeThreshold}");
// 如果灵敏度超过1.5,则即使有一个点满足条件也认为可能是直线
if (sensitivity > 1.5)
// 使用采样点进行更准确的判断
if (Settings.Canvas.HighPrecisionLineStraighten)
{
// 超高灵敏度模式:只要有一个关键点偏差小,就认为可能是直线
if (quarterDeviation <= quickRelativeThreshold ||
midDeviation <= quickRelativeThreshold ||
threeQuarterDeviation <= quickRelativeThreshold)
{
return true;
}
var allPoints = stroke.StylusPoints.Select(p => p.ToPoint()).ToList();
checkPoints = SamplePointsByDistance(allPoints, 10.0);
Debug.WriteLine($"高精度模式快速检查:原始点数={allPoints.Count}, 采样点数={checkPoints.Count}");
}
else
{
// 常规判断:如果任一点偏离太大,直接排除
if (quarterDeviation > quickRelativeThreshold ||
midDeviation > quickRelativeThreshold ||
threeQuarterDeviation > quickRelativeThreshold)
// 取中点和1/4、3/4位置的点
int quarterIdx = stroke.StylusPoints.Count / 4;
int midIdx = stroke.StylusPoints.Count / 2;
int threeQuarterIdx = quarterIdx * 3;
checkPoints = new List<Point>
{
stroke.StylusPoints[quarterIdx].ToPoint(),
stroke.StylusPoints[midIdx].ToPoint(),
stroke.StylusPoints[threeQuarterIdx].ToPoint()
};
}
// 计算所有检查点与直线的平均偏差
double totalDeviation = 0;
double maxDeviation = 0;
int validPointCount = 0;
foreach (Point checkPoint in checkPoints)
{
double deviation = DistanceFromLineToPoint(start, end, checkPoint);
totalDeviation += deviation;
maxDeviation = Math.Max(maxDeviation, deviation);
validPointCount++;
}
if (validPointCount > 0)
{
double avgDeviation = totalDeviation / validPointCount;
// 使用相对偏差:偏差与线长的比例,并使用灵敏度进行调整
double quickRelativeThreshold = lineLength * quickThreshold;
// 使用平均偏差和最大偏差的综合判断
double deviationThreshold = Settings.Canvas.HighPrecisionLineStraighten
? Math.Max(avgDeviation, maxDeviation * 0.7) // 高精度模式更严格
: maxDeviation;
// 记录检测到的偏差
Debug.WriteLine($"Deviations: avg={avgDeviation:F2}, max={maxDeviation:F2}, threshold={quickRelativeThreshold:F2}, highPrecision={Settings.Canvas.HighPrecisionLineStraighten}");
if (deviationThreshold > quickRelativeThreshold)
{
return false;
}
@@ -854,7 +908,7 @@ namespace Ink_Canvas
}
/// <summary>
/// 检查墨迹是否为复杂形状(如一团墨迹、涂鸦等)
/// 检查墨迹是否为复杂形状
/// </summary>
private bool IsComplexShape(Stroke stroke)
{
@@ -1130,303 +1184,160 @@ namespace Ink_Canvas
lineEnd.X * lineStart.Y - lineEnd.Y * lineStart.X) / lineLength;
}
// New method: Determines if a stroke should be straightened into a line
private bool TryGetStraightLineEndpoints(Stroke stroke, out Point endpoint1, out Point endpoint2)
{
endpoint1 = new Point();
endpoint2 = new Point();
var points = stroke.StylusPoints.Select(p => p.ToPoint()).ToList();
if (points.Count < 10)
{
return false;
}
List<Point> workingPoints = points;
if (Settings.Canvas.HighPrecisionLineStraighten)
{
workingPoints = SamplePointsByDistance(points, 10.0);
Debug.WriteLine($"高精度模式:原始点数={points.Count}, 采样后点数={workingPoints.Count}");
}
// 使用总最小二乘法(TLS/PCA)进行直线拟合
int n = workingPoints.Count - 8;
if (n < 1)
{
// 如果采样后点数太少,回退到原始方法
n = points.Count - 8;
workingPoints = points;
}
List<Point> filteredPoints = new List<Point>();
// 收集过滤后的点(跳过前 4 个和后 4 个点,用于计算直线方向)
int skipCount = Math.Min(4, n / 2); // 确保跳过数量不超过一半
for (int i = skipCount; i < n + skipCount && i < workingPoints.Count; i++)
{
filteredPoints.Add(workingPoints[i]);
}
// 计算中心点(使用过滤后的点)
double centerX = 0, centerY = 0;
foreach (Point p in filteredPoints)
{
centerX += p.X;
centerY += p.Y;
}
centerX /= filteredPoints.Count;
centerY /= filteredPoints.Count;
// 计算协方差矩阵(使用过滤后的点)
double covXX = 0, covYY = 0, covXY = 0;
foreach (Point p in filteredPoints)
{
double dx = p.X - centerX;
double dy = p.Y - centerY;
covXX += dx * dx;
covYY += dy * dy;
covXY += dx * dy;
}
// 计算特征值和特征向量
double trace = covXX + covYY;
double determinant = covXX * covYY - covXY * covXY;
double discriminant = Math.Sqrt(trace * trace - 4 * determinant);
double eigenvalue1 = (trace + discriminant) / 2;
double eigenvalue2 = (trace - discriminant) / 2;
// 最大特征值对应的特征向量即为直线方向
double directionX, directionY;
if (Math.Abs(covXY) > 1e-10)
{
directionX = covXY;
directionY = eigenvalue1 - covXX;
// 归一化
double length = Math.Sqrt(directionX * directionX + directionY * directionY);
directionX /= length;
directionY /= length;
}
else
{
// 如果协方差为 0,则是水平或垂直直线
directionX = (covXX >= covYY) ? 1 : 0;
directionY = (covXX >= covYY) ? 0 : 1;
}
// 计算解释方差比例(拟合优度)
double totalVariance = eigenvalue1 + eigenvalue2;
double explainedVarianceRatio = (totalVariance > 1e-10) ?
Math.Max(eigenvalue1, eigenvalue2) / totalVariance : 1d;
// 使用所有点计算端点
double minProjection = double.MaxValue;
double maxProjection = double.MinValue;
// 计算所有点在直线方向上的投影
List<Point> pointsForProjection = Settings.Canvas.HighPrecisionLineStraighten ? workingPoints : points;
foreach (Point p in pointsForProjection)
{
// 相对于过滤点中心的投影
double projection = (p.X - centerX) * directionX + (p.Y - centerY) * directionY;
minProjection = Math.Min(minProjection, projection);
maxProjection = Math.Max(maxProjection, projection);
}
// 计算端点坐标
endpoint1 = new Point(
centerX + minProjection * directionX,
centerY + minProjection * directionY
);
endpoint2 = new Point(
centerX + maxProjection * directionX,
centerY + maxProjection * directionY
);
// 使用解释方差比例作为判断条件
double threshold = 0.998 + Settings.InkToShape.LineNormalizationThreshold / 500;
return explainedVarianceRatio > threshold;
}
// New method: Determines if a stroke should be straightened into a line
private bool ShouldStraightenLine(Stroke stroke)
{
// 分辨率自适应阈值
Point start = stroke.StylusPoints.First().ToPoint();
Point end = stroke.StylusPoints.Last().ToPoint();
double maxDeviation = 0;
double lineLength = GetDistance(start, end);
// 分辨率自适应阈值
double adaptiveThreshold = Settings.Canvas.AutoStraightenLineThreshold * GetResolutionScale();
// 如果线条太短,不进行拉直处理,使用自适应阈值
// 如果线条太短,不进行拉直处理
if (lineLength < adaptiveThreshold)
{
Debug.WriteLine($"线条太短: {lineLength} < {adaptiveThreshold}");
return false;
}
// 新增:再次检查复杂度(双重保险)
// 检查复杂度
if (IsComplexShape(stroke))
{
Debug.WriteLine("拒绝拉直:检测到复杂形状");
return false;
}
// 新增:检查线条的直线度评分
double straightnessScore = CalculateStraightnessScore(stroke);
double minStraightnessThreshold = 0.7; // 最低直线度要求
Point endpoint1, endpoint2;
bool shouldStraighten = TryGetStraightLineEndpoints(stroke, out endpoint1, out endpoint2);
if (straightnessScore < minStraightnessThreshold)
if (shouldStraighten)
{
Debug.WriteLine($"拒绝拉直:直线度评分过低 {straightnessScore:F3} < {minStraightnessThreshold}");
return false;
}
// 获取用户设置的灵敏度值,确保使用正确的值进行后续判断
double sensitivity = Settings.InkToShape.LineStraightenSensitivity;
// 输出详细的调试信息
Debug.WriteLine($"ShouldStraightenLine - sensitivity: {sensitivity}, length: {lineLength}");
// 临时:显示调试消息框
// MessageBox.Show($"灵敏度值: {sensitivity}", "调试信息");
// 计算点与直线的偏差
double totalDeviation = 0;
int pointCount = 0;
// 检查是否启用了高精度直线拉直
bool useHighPrecision = Settings.Canvas.HighPrecisionLineStraighten;
if (useHighPrecision)
{
Debug.WriteLine("使用高精度直线拉直模式");
// 高精度模式:每隔10像素取一个计数点
double strokeLength = 0;
double sampleInterval = 10.0; // 10像素间隔
// 计算笔画的总长度,用于后续采样
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point p1 = stroke.StylusPoints[i - 1].ToPoint();
Point p2 = stroke.StylusPoints[i].ToPoint();
strokeLength += GetDistance(p1, p2);
}
// 如果笔画太短,直接使用所有点
if (strokeLength < sampleInterval * 5)
{
foreach (StylusPoint sp in stroke.StylusPoints)
{
Point p = sp.ToPoint();
double deviation = DistanceFromLineToPoint(start, end, p);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
}
}
else
{
// 使用等距采样点
double currentLength = 0;
double nextSampleAt = 0;
// 总是包含起点
Point lastPoint = start;
double deviation = DistanceFromLineToPoint(start, end, lastPoint);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
// 采样中间点
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point currentPoint = stroke.StylusPoints[i].ToPoint();
double segmentLength = GetDistance(lastPoint, currentPoint);
// 如果这段线段跨越了下一个采样点
while (currentLength + segmentLength >= nextSampleAt)
{
// 计算采样点在线段上的位置
double t = (nextSampleAt - currentLength) / segmentLength;
Point samplePoint = new Point(
lastPoint.X + t * (currentPoint.X - lastPoint.X),
lastPoint.Y + t * (currentPoint.Y - lastPoint.Y)
);
// 计算采样点的偏差
deviation = DistanceFromLineToPoint(start, end, samplePoint);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
// 设置下一个采样点位置
nextSampleAt += sampleInterval;
// 防止无限循环
if (nextSampleAt > strokeLength) break;
}
currentLength += segmentLength;
lastPoint = currentPoint;
}
// 总是包含终点
deviation = DistanceFromLineToPoint(start, end, end);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
}
Debug.WriteLine($"接受拉直:判断为直线,解释方差比例满足阈值");
}
else
{
// 原始模式:使用所有点
foreach (StylusPoint sp in stroke.StylusPoints)
{
Point p = sp.ToPoint();
double deviation = DistanceFromLineToPoint(start, end, p);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
}
Debug.WriteLine($"拒绝拉直:判断不满足直线条件");
}
// 计算平均偏差
double avgDeviation = totalDeviation / pointCount;
// 更详细的调试信息
Debug.WriteLine($"Max deviation: {maxDeviation}, Avg: {avgDeviation}, Threshold: {sensitivity * lineLength}, Points: {pointCount}");
// 支持更广泛的灵敏度范围 (0.05-2.0)
// 如果灵敏度高于1.0,使用更宽松的判断标准
if (sensitivity > 1.0)
{
// 高灵敏度模式 - 允许更大的偏差
double adjustedSensitivity = 0.5 + (sensitivity - 1.0) * 1.5; // 映射到0.5-2.0范围
// 只判断平均偏差和相对偏差
if (maxDeviation / lineLength < adjustedSensitivity && avgDeviation < lineLength * 0.1 * adjustedSensitivity)
{
Debug.WriteLine("接受拉直 (高灵敏度模式)");
return true;
}
Debug.WriteLine("拒绝拉直 (高灵敏度模式)");
return false;
}
// 否则使用常规判断标准
// 检查点分布的一致性 - 如果有些点偏离很大而其他点很接近直线,表明线条有明显弯曲
double deviationVariance = 0;
// 使用相同的高精度/原始模式来计算方差
if (useHighPrecision)
{
// 高精度模式:重新采样计算方差
double strokeLength = 0;
double sampleInterval = 10.0; // 10像素间隔
// 计算笔画的总长度,用于后续采样
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point p1 = stroke.StylusPoints[i - 1].ToPoint();
Point p2 = stroke.StylusPoints[i].ToPoint();
strokeLength += GetDistance(p1, p2);
}
// 如果笔画太短,直接使用所有点
if (strokeLength < sampleInterval * 5)
{
foreach (StylusPoint sp in stroke.StylusPoints)
{
Point p = sp.ToPoint();
double deviation = DistanceFromLineToPoint(start, end, p);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
}
}
else
{
// 使用等距采样点
double currentLength = 0;
double nextSampleAt = 0;
Point lastPoint = start;
// 起点方差
double deviation = DistanceFromLineToPoint(start, end, lastPoint);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
// 采样中间点
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point currentPoint = stroke.StylusPoints[i].ToPoint();
double segmentLength = GetDistance(lastPoint, currentPoint);
// 如果这段线段跨越了下一个采样点
while (currentLength + segmentLength >= nextSampleAt)
{
// 计算采样点在线段上的位置
double t = (nextSampleAt - currentLength) / segmentLength;
Point samplePoint = new Point(
lastPoint.X + t * (currentPoint.X - lastPoint.X),
lastPoint.Y + t * (currentPoint.Y - lastPoint.Y)
);
// 计算采样点的方差
deviation = DistanceFromLineToPoint(start, end, samplePoint);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
// 设置下一个采样点位置
nextSampleAt += sampleInterval;
// 防止无限循环
if (nextSampleAt > strokeLength) break;
}
currentLength += segmentLength;
lastPoint = currentPoint;
}
// 终点方差
deviation = DistanceFromLineToPoint(start, end, end);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
}
}
else
{
// 原始模式:使用所有点计算方差
foreach (StylusPoint sp in stroke.StylusPoints)
{
Point p = sp.ToPoint();
double deviation = DistanceFromLineToPoint(start, end, p);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
}
}
deviationVariance /= pointCount;
// 输出更多调试信息
Debug.WriteLine($"Deviation variance: {deviationVariance}, Threshold: {sensitivity * lineLength * 0.05}");
// 如果最大偏差超过线长的阈值比例,或者偏差方差较大(表示不均匀弯曲),则不拉直
// 灵敏度越大,容许的偏差越大,更容易将线条识别为直线
if ((maxDeviation / lineLength) > sensitivity)
{
Debug.WriteLine("拒绝拉直:最大偏差过大");
return false;
}
// 如果偏差方差大,说明线条弯曲不均匀
// 灵敏度越大,容许的偏差方差越大
if (deviationVariance > (sensitivity * lineLength * 0.05))
{
Debug.WriteLine("拒绝拉直:偏差方差过大");
return false;
}
// 检查中点偏离情况 - 针对弧形线条特别有效
if (stroke.StylusPoints.Count > 10)
{
int midIndex = stroke.StylusPoints.Count / 2;
Point midPoint = stroke.StylusPoints[midIndex].ToPoint();
double midDeviation = DistanceFromLineToPoint(start, end, midPoint);
// 输出中点偏差信息
Debug.WriteLine($"Mid deviation: {midDeviation}, Threshold: {lineLength * sensitivity * 0.8}");
// 如果中点偏离过大,不拉直
// 使用灵敏度作为判断基准,灵敏度越大,容许的中点偏离越大
if (midDeviation > (lineLength * sensitivity * 0.8))
{
Debug.WriteLine("拒绝拉直:中点偏差过大");
return false;
}
}
Debug.WriteLine($"接受拉直:直线度评分 = {straightnessScore:F3}");
return true;
return shouldStraighten;
}
/// <summary>
@@ -1576,6 +1487,43 @@ namespace Ink_Canvas
return points;
}
/// <summary>
/// 高精度模式
/// </summary>
private List<Point> SamplePointsByDistance(List<Point> points, double sampleInterval = 10.0)
{
if (points == null || points.Count < 2)
return points;
List<Point> sampledPoints = new List<Point>();
sampledPoints.Add(points[0]); // 总是包含起点
double accumulatedDistance = 0;
Point lastSampledPoint = points[0];
for (int i = 1; i < points.Count; i++)
{
double segmentDistance = GetDistance(lastSampledPoint, points[i]);
accumulatedDistance += segmentDistance;
// 当累积距离达到采样间隔时,添加当前点
if (accumulatedDistance >= sampleInterval)
{
sampledPoints.Add(points[i]);
lastSampledPoint = points[i];
accumulatedDistance = 0; // 重置累积距离
}
}
// 总是包含终点(如果还没有包含)
if (sampledPoints.Count == 0 || GetDistance(sampledPoints.Last(), points.Last()) > 1.0)
{
sampledPoints.Add(points.Last());
}
return sampledPoints;
}
// New method: Gets distance from point to a line defined by two points
private double DistanceFromLineToPoint(Point lineStart, Point lineEnd, Point point)
{
@@ -1590,11 +1538,66 @@ namespace Ink_Canvas
return distance;
}
/// <summary>
/// 判断一个 stroke 是否是直线(排除虚线和点线)
/// </summary>
/// <param name="stroke">要检查的 stroke</param>
/// <returns>如果是直线返回 true,否则返回 false</returns>
private bool IsStraightLine(Stroke stroke)
{
if (stroke == null || stroke.StylusPoints.Count == 0)
return false;
int pointCount = stroke.StylusPoints.Count;
if (pointCount == 1)
return false;
// 最简单的直线:只有2个点
if (pointCount == 2)
{
Point p1 = stroke.StylusPoints[0].ToPoint();
Point p2 = stroke.StylusPoints[1].ToPoint();
double lineLength = GetDistance(p1, p2);
if (lineLength < 10)
return false;
return true;
}
if (pointCount > 3)
return false;
// 对于3个点的情况,检查它们是否基本在一条直线上
if (pointCount == 3)
{
Point p1 = stroke.StylusPoints[0].ToPoint();
Point p2 = stroke.StylusPoints[1].ToPoint();
Point p3 = stroke.StylusPoints[2].ToPoint();
double totalLength = GetDistance(p1, p3);
if (totalLength < 10)
return false;
// 计算点到直线的距离
// 使用 p1 和 p3 作为直线端点,检查 p2 是否在这条直线上
double distance = DistanceFromLineToPoint(p1, p3, p2);
// 如果点到直线的距离相对于线段长度很小,认为是直线
// 使用相对误差阈值(比如 1%
if (totalLength > 0 && distance / totalLength < 0.01)
return true;
return false;
}
return false;
}
// New method: Attempts to snap endpoints to existing stroke endpoints
private Point[] GetSnappedEndpoints(Point start, Point end)
{
// 如果端点吸附功能关闭,直接返回null
// 这里不再返回原始点,因为调用此方法的地方会判断返回值是否为null
if (!Settings.Canvas.LineEndpointSnapping)
return null;
@@ -1611,6 +1614,10 @@ namespace Ink_Canvas
{
if (stroke.StylusPoints.Count == 0) continue;
// 只对直线进行端点吸附,跳过虚线和点线
if (!IsStraightLine(stroke))
continue;
// Get stroke endpoints
Point strokeStart = stroke.StylusPoints.First().ToPoint();
Point strokeEnd = stroke.StylusPoints.Last().ToPoint();
+22 -10
View File
@@ -190,21 +190,33 @@ namespace Ink_Canvas
{
if (item.InsertedElement is Image img)
{
img.MouseDown -= UIElement_MouseDown;
img.MouseDown += UIElement_MouseDown;
img.IsManipulationEnabled = true;
// 检查图片是否有位置信息,如果没有则应用居中
double left = InkCanvas.GetLeft(img);
double top = InkCanvas.GetTop(img);
// 重新应用CenterAndScaleElement变换
CenterAndScaleElement(img);
if (double.IsNaN(left) || double.IsNaN(top))
{
// 图片没有位置信息,应用居中
CenterAndScaleElement(img);
}
// 重新绑定事件处理器
BindElementEvents(img);
}
else if (item.InsertedElement is MediaElement media)
{
media.MouseDown -= UIElement_MouseDown;
media.MouseDown += UIElement_MouseDown;
media.IsManipulationEnabled = true;
// 检查媒体元素是否有位置信息,如果没有则应用居中
double left = InkCanvas.GetLeft(media);
double top = InkCanvas.GetTop(media);
// 重新应用CenterAndScaleElement变换
CenterAndScaleElement(media);
if (double.IsNaN(left) || double.IsNaN(top))
{
// 媒体元素没有位置信息,应用居中
CenterAndScaleElement(media);
}
// 重新绑定事件处理器
BindElementEvents(media);
}
}
}
+417 -25
View File
@@ -1,4 +1,4 @@
using Ink_Canvas.Helpers;
using Ink_Canvas.Helpers;
using System;
using System.ComponentModel;
using System.Diagnostics;
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
namespace Ink_Canvas
{
@@ -61,12 +62,22 @@ namespace Ink_Canvas
private Timer timerCheckAutoFold = new Timer();
private string AvailableLatestVersion;
private Timer timerCheckAutoUpdateWithSilence = new Timer();
private Timer timerCheckAutoUpdateRetry = new Timer();
private bool isHidingSubPanelsWhenInking; // 避免书写时触发二次关闭二级菜单导致动画不连续
private int updateCheckRetryCount = 0;
private const int MAX_UPDATE_CHECK_RETRIES = 6;
private Timer timerDisplayTime = new Timer();
private Timer timerDisplayDate = new Timer();
private Timer timerNtpSync = new Timer();
private TimeViewModel nowTimeVM = new TimeViewModel();
private DateTime cachedNetworkTime = DateTime.Now;
private DateTime lastNtpSyncTime = DateTime.MinValue;
private string lastDisplayedTime = "";
private bool useNetworkTime = false;
private TimeSpan networkTimeOffset = TimeSpan.Zero;
private DateTime lastLocalTime = DateTime.Now; // 记录上次的本地时间,用于检测时间跳跃
private bool isNtpSyncing = false; // 防止重复NTP同步的标志
private async Task<DateTime> GetNetworkTimeAsync()
{
@@ -79,7 +90,7 @@ namespace Ink_Canvas
var ipEndPoint = new IPEndPoint(addresses[0], 123);
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
{
socket.ReceiveTimeout = 2000;
socket.ReceiveTimeout = 5000;
socket.Connect(ipEndPoint);
await Task.Factory.FromAsync(socket.BeginSend(ntpData, 0, ntpData.Length, SocketFlags.None, null, socket), socket.EndSend);
await Task.Factory.FromAsync(socket.BeginReceive(ntpData, 0, ntpData.Length, SocketFlags.None, null, socket), socket.EndReceive);
@@ -91,7 +102,7 @@ namespace Ink_Canvas
var networkDateTime = (new DateTime(1900, 1, 1)).AddMilliseconds((long)milliseconds);
return networkDateTime.ToLocalTime();
}
catch
catch (Exception)
{
return DateTime.Now;
}
@@ -109,28 +120,189 @@ namespace Ink_Canvas
timerCheckAutoFold.Interval = 500;
timerCheckAutoUpdateWithSilence.Elapsed += timerCheckAutoUpdateWithSilence_Elapsed;
timerCheckAutoUpdateWithSilence.Interval = 1000 * 60 * 10;
timerCheckAutoUpdateRetry.Elapsed += timerCheckAutoUpdateRetry_Elapsed;
timerCheckAutoUpdateRetry.Interval = 1000 * 60 * 10;
WaterMarkTime.DataContext = nowTimeVM;
WaterMarkDate.DataContext = nowTimeVM;
timerDisplayTime.Elapsed += async (s, e) => await TimerDisplayTime_ElapsedAsync();
timerDisplayTime.Elapsed += TimerDisplayTime_Elapsed;
timerDisplayTime.Interval = 1000;
timerDisplayTime.Start();
timerDisplayDate.Elapsed += TimerDisplayDate_Elapsed;
timerDisplayDate.Interval = 1000 * 60 * 60 * 1;
timerDisplayDate.Start();
timerNtpSync.Elapsed += async (s, e) => await TimerNtpSync_ElapsedAsync();
timerNtpSync.Interval = 1000 * 60 * 60 * 2; // 每2小时同步一次
timerNtpSync.Start();
timerKillProcess.Start();
nowTimeVM.nowDate = DateTime.Now.ToString("yyyy'年'MM'月'dd'日' dddd");
nowTimeVM.nowTime = DateTime.Now.ToString("tt hh'时'mm'分'ss'秒'");
// 程序启动时立即进行一次NTP同步
Task.Run(async () =>
{
try
{
await TimerNtpSync_ElapsedAsync();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"程序启动时NTP同步失败: {ex.Message}", LogHelper.LogType.Error);
}
});
// 初始化定时保存墨迹定时器
InitAutoSaveStrokesTimer();
}
// 修改TimerDisplayTime_ElapsedAsync方法中的时间格式
private async Task TimerDisplayTime_ElapsedAsync()
// 初始化定时保存墨迹定时器
private void InitAutoSaveStrokesTimer()
{
DateTime now = await GetNetworkTimeAsync();
// 只更新时间,日期由原有逻辑定时更新即可
Dispatcher.Invoke(() =>
if (autoSaveStrokesTimer == null)
{
nowTimeVM.nowTime = now.ToString("tt hh'时'mm'分'ss'秒'");
});
autoSaveStrokesTimer = new DispatcherTimer();
autoSaveStrokesTimer.Tick += AutoSaveStrokesTimer_Tick;
}
// 根据设置更新定时器间隔和启动状态
UpdateAutoSaveStrokesTimer();
}
// 更新定时保存墨迹定时器状态
private void UpdateAutoSaveStrokesTimer()
{
if (autoSaveStrokesTimer == null) return;
autoSaveStrokesTimer.Stop();
if (Settings.Automation.IsEnableAutoSaveStrokes)
{
int intervalMinutes = Settings.Automation.AutoSaveStrokesIntervalMinutes;
if (intervalMinutes < 1) intervalMinutes = 1; // 最小间隔1分钟
autoSaveStrokesTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
autoSaveStrokesTimer.Start();
}
}
// 定时保存墨迹定时器事件处理
private void AutoSaveStrokesTimer_Tick(object sender, EventArgs e)
{
try
{
// 只有在画布可见且有墨迹时才保存
if (inkCanvas.Visibility == Visibility.Visible && inkCanvas.Strokes.Count > 0)
{
// 静默保存
SaveInkCanvasStrokes(false, false);
}
}
catch (Exception)
{
}
}
// NTP同步定时器事件处理
private async Task TimerNtpSync_ElapsedAsync()
{
// 防止重复同步
if (isNtpSyncing) return;
isNtpSyncing = true;
try
{
// 添加超时机制,最多等待10秒
var timeoutTask = Task.Delay(10000);
var ntpTask = GetNetworkTimeAsync();
var completedTask = await Task.WhenAny(ntpTask, timeoutTask);
if (completedTask == timeoutTask)
{
cachedNetworkTime = DateTime.Now;
lastNtpSyncTime = DateTime.Now;
useNetworkTime = false;
networkTimeOffset = TimeSpan.Zero;
return;
}
DateTime networkTime = await ntpTask;
DateTime localTime = DateTime.Now;
cachedNetworkTime = networkTime;
lastNtpSyncTime = localTime;
// 计算网络时间与本地时间的偏移量
networkTimeOffset = networkTime - localTime;
// 如果时间差超过3分钟,则使用网络时间
useNetworkTime = Math.Abs(networkTimeOffset.TotalMinutes) > 3.0;
}
catch (Exception ex)
{
// NTP同步失败时,保持使用本地时间
cachedNetworkTime = DateTime.Now;
lastNtpSyncTime = DateTime.Now;
useNetworkTime = false;
networkTimeOffset = TimeSpan.Zero;
LogHelper.WriteLogToFile($"NTP同步失败: {ex.Message}", LogHelper.LogType.Warning);
}
finally
{
isNtpSyncing = false;
}
}
// 优化后的时间显示方法,仅在NTP同步时计算网络时间偏移
private void TimerDisplayTime_Elapsed(object sender, ElapsedEventArgs e)
{
DateTime localTime = DateTime.Now;
DateTime displayTime = localTime; // 默认使用本地时间
// 检测系统时间是否发生重大跳跃(超过2分钟)
TimeSpan timeJump = localTime - lastLocalTime;
double timeJumpMinutes = Math.Abs(timeJump.TotalMinutes);
if (timeJumpMinutes > 3 && !isNtpSyncing)
{
// 系统时间发生重大变化(超过3分钟),立即触发NTP同步
// 使用异步方式触发NTP同步,避免阻塞主线程
Task.Run(async () =>
{
try
{
await TimerNtpSync_ElapsedAsync();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"时间跳跃触发的NTP同步失败: {ex.Message}", LogHelper.LogType.Error);
}
});
}
lastLocalTime = localTime;
// 如果启用网络时间且偏移量已计算,则应用偏移量
if (useNetworkTime && networkTimeOffset != TimeSpan.Zero)
{
displayTime = localTime + networkTimeOffset;
}
// 格式化时间字符串
string timeString = displayTime.ToString("tt hh'时'mm'分'ss'秒'");
// 只有当时间字符串发生变化时才更新UI,避免不必要的UI刷新
if (timeString != lastDisplayedTime)
{
lastDisplayedTime = timeString;
// 使用BeginInvoke异步更新UI,避免阻塞
Dispatcher.BeginInvoke(new Action(() =>
{
nowTimeVM.nowTime = timeString;
}));
}
}
// 修改TimerDisplayDate_Elapsed方法中的日期格式
@@ -157,6 +329,8 @@ namespace Ink_Canvas
{
var processes = Process.GetProcessesByName("EasiNote");
if (processes.Length > 0) arg += " /IM EasiNote.exe";
var seewoStartProcesses = Process.GetProcessesByName("SeewoStart");
if (seewoStartProcesses.Length > 0) arg += " /IM SeewoStart.exe";
}
if (Settings.Automation.IsAutoKillHiteAnnotation)
@@ -220,8 +394,18 @@ namespace Ink_Canvas
ShowNotification("“鸿合屏幕书写”已自动关闭");
if (Settings.Automation.IsAutoKillHiteAnnotation && Settings.Automation.IsAutoEnterAnnotationAfterKillHite)
{
// 自动进入批注状态
PenIcon_Click(null, null);
// 检查是否处于收纳状态,如果是则先展开浮动栏
if (isFloatingBarFolded)
{
// 先展开浮动栏,然后进入批注状态
// UnFoldFloatingBar 方法内部会根据设置自动进入批注模式
UnFoldFloatingBar(null);
}
else
{
// 如果已经展开,直接进入批注状态
PenIcon_Click(null, null);
}
}
});
}
@@ -274,6 +458,55 @@ namespace Ink_Canvas
private bool foldFloatingBarByUser, // 保持收纳操作不受自动收纳的控制
unfoldFloatingBarByUser; // 允许用户在希沃软件内进行展开操作
/// <summary>
/// 检测是否为批注窗口(窗口标题为空且高度小于500像素)
/// </summary>
/// <returns>如果是批注窗口返回true,否则返回false</returns>
private bool IsAnnotationWindow()
{
var windowTitle = ForegroundWindowInfo.WindowTitle();
var windowRect = ForegroundWindowInfo.WindowRect();
var windowProcessName = ForegroundWindowInfo.ProcessName();
// 检测希沃白板五的批注面板
// 希沃白板五的批注面板通常具有以下特征:
// 1. 窗口标题为空或包含特定关键词
// 2. 窗口高度较小(批注工具栏)
// 3. 窗口宽度适中(工具栏宽度)
if (windowProcessName == "BoardService" || windowProcessName == "seewoPincoTeacher")
{
// 检测希沃白板五的批注工具栏
// 批注工具栏通常高度在50-200像素之间,宽度在200-800像素之间
if (windowRect.Height >= 50 && windowRect.Height <= 200 &&
windowRect.Width >= 200 && windowRect.Width <= 800)
{
return true;
}
// 检测希沃白板五的二级菜单面板
// 二级菜单面板通常高度在100-400像素之间,宽度在150-400像素之间
if (windowRect.Height >= 100 && windowRect.Height <= 400 &&
windowRect.Width >= 150 && windowRect.Width <= 400)
{
return true;
}
}
// 检测鸿合软件的批注面板
if (windowProcessName == "HiteCamera" || windowProcessName == "HiteTouchPro" || windowProcessName == "HiteLightBoard")
{
// 鸿合软件的批注面板特征
if (windowRect.Height >= 50 && windowRect.Height <= 300 &&
windowRect.Width >= 200 && windowRect.Width <= 600)
{
return true;
}
}
// 原有的检测逻辑(保持向后兼容)
return windowTitle.Length == 0 && windowRect.Height < 500;
}
private void timerCheckAutoFold_Elapsed(object sender, ElapsedEventArgs e)
{
if (isFloatingBarChangingHideMode) return;
@@ -294,10 +527,22 @@ namespace Ink_Canvas
Trace.WriteLine(ForegroundWindowInfo.ProcessPath());
Trace.WriteLine(version);
Trace.WriteLine(prodName);
if (version.StartsWith("5.") && Settings.Automation.IsAutoFoldInEasiNote && (!(windowTitle.Length == 0 && ForegroundWindowInfo.WindowRect().Height < 500) ||
!Settings.Automation.IsAutoFoldInEasiNoteIgnoreDesktopAnno))
if (version.StartsWith("5.") && Settings.Automation.IsAutoFoldInEasiNote)
{ // EasiNote5
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
// 检查是否是桌面批注窗口
bool isAnnotationWindow = windowTitle.Length == 0 && ForegroundWindowInfo.WindowRect().Height < 500;
// 如果启用了忽略桌面批注窗口功能,且当前是批注窗口
if (Settings.Automation.IsAutoFoldInEasiNoteIgnoreDesktopAnno && isAnnotationWindow)
{
// 强制保持收纳状态
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
else if (!isAnnotationWindow)
{
// 非批注窗口时正常收纳
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
}
else if (version.StartsWith("3.") && Settings.Automation.IsAutoFoldInEasiNote3)
{ // EasiNote3
@@ -316,7 +561,17 @@ namespace Ink_Canvas
ForegroundWindowInfo.WindowRect().Height >= SystemParameters.WorkArea.Height - 16 &&
ForegroundWindowInfo.WindowRect().Width >= SystemParameters.WorkArea.Width - 16)
{
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
// 检测到批注窗口时保持收纳状态
if (IsAnnotationWindow())
{
// 批注窗口打开时,如果当前是展开状态则收纳
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
else
{
// 非批注窗口时正常处理
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
// EasiNote5C
}
else if (Settings.Automation.IsAutoFoldInEasiNote5C && windowProcessName == "EasiNote5C" &&
@@ -328,21 +583,51 @@ namespace Ink_Canvas
}
else if (Settings.Automation.IsAutoFoldInSeewoPincoTeacher && (windowProcessName == "BoardService" || windowProcessName == "seewoPincoTeacher"))
{
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
// 检测到希沃白板五的批注窗口时保持收纳状态
if (IsAnnotationWindow())
{
// 批注窗口打开时,如果当前是展开状态则收纳
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
else
{
// 非批注窗口时正常处理
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
// HiteCamera
}
else if (Settings.Automation.IsAutoFoldInHiteCamera && windowProcessName == "HiteCamera" &&
ForegroundWindowInfo.WindowRect().Height >= SystemParameters.WorkArea.Height - 16 &&
ForegroundWindowInfo.WindowRect().Width >= SystemParameters.WorkArea.Width - 16)
{
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
// 检测到批注窗口时保持收纳状态
if (IsAnnotationWindow())
{
// 批注窗口打开时,如果当前是展开状态则收纳
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
else
{
// 非批注窗口时正常处理
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
// HiteTouchPro
}
else if (Settings.Automation.IsAutoFoldInHiteTouchPro && windowProcessName == "HiteTouchPro" &&
ForegroundWindowInfo.WindowRect().Height >= SystemParameters.WorkArea.Height - 16 &&
ForegroundWindowInfo.WindowRect().Width >= SystemParameters.WorkArea.Width - 16)
{
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
// 检测到批注窗口时保持收纳状态
if (IsAnnotationWindow())
{
// 批注窗口打开时,如果当前是展开状态则收纳
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
else
{
// 非批注窗口时正常处理
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
// WxBoardMain
}
else if (Settings.Automation.IsAutoFoldInWxBoardMain && windowProcessName == "WxBoardMain" &&
@@ -369,7 +654,17 @@ namespace Ink_Canvas
ForegroundWindowInfo.WindowRect().Height >= SystemParameters.WorkArea.Height - 16 &&
ForegroundWindowInfo.WindowRect().Width >= SystemParameters.WorkArea.Width - 16)
{
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
// 检测到批注窗口时保持收纳状态
if (IsAnnotationWindow())
{
// 批注窗口打开时,如果当前是展开状态则收纳
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
else
{
// 非批注窗口时正常处理
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
}
// AdmoxWhiteboard
}
else if (Settings.Automation.IsAutoFoldInAdmoxWhiteboard && windowProcessName == "Amdox.WhiteBoard" &&
@@ -420,8 +715,18 @@ namespace Ink_Canvas
}
else
{
if (isFloatingBarFolded && !foldFloatingBarByUser) UnFoldFloatingBar_MouseUp(new object(), null);
unfoldFloatingBarByUser = false;
// 检查是否启用了软件退出后保持收纳模式
if (Settings.Automation.KeepFoldAfterSoftwareExit)
{
// 如果启用了保持收纳模式,则不自动展开浮动栏
unfoldFloatingBarByUser = false;
}
else
{
// 原有的逻辑:软件退出后自动展开浮动栏
if (isFloatingBarFolded && !foldFloatingBarByUser) UnFoldFloatingBar_MouseUp(new object(), null);
unfoldFloatingBarByUser = false;
}
}
}
catch { }
@@ -579,5 +884,92 @@ namespace Ink_Canvas
timerCheckAutoUpdateWithSilence.Start();
}
}
// 检查更新失败重试定时器事件处理
private async void timerCheckAutoUpdateRetry_Elapsed(object sender, ElapsedEventArgs e)
{
// 停止定时器,避免重复触发
timerCheckAutoUpdateRetry.Stop();
try
{
// 检查是否启用了自动更新
if (!Settings.Startup.IsAutoUpdate)
{
LogHelper.WriteLogToFile("AutoUpdate | Auto update is disabled, stopping retry timer");
return;
}
// 增加重试计数
updateCheckRetryCount++;
LogHelper.WriteLogToFile($"AutoUpdate | Retry check attempt {updateCheckRetryCount}/{MAX_UPDATE_CHECK_RETRIES}");
// 检查是否超过最大重试次数
if (updateCheckRetryCount > MAX_UPDATE_CHECK_RETRIES)
{
LogHelper.WriteLogToFile("AutoUpdate | Maximum retry attempts reached, stopping retry timer", LogHelper.LogType.Warning);
return;
}
// 执行更新检查
LogHelper.WriteLogToFile("AutoUpdate | Retrying update check after failure");
// 清除之前的更新状态
AvailableLatestVersion = null;
AvailableLatestLineGroup = null;
// 使用当前选择的更新通道检查更新
var (remoteVersion, lineGroup, apiReleaseNotes) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel);
AvailableLatestVersion = remoteVersion;
AvailableLatestLineGroup = lineGroup;
if (AvailableLatestVersion != null)
{
// 检查更新成功,重置重试计数
updateCheckRetryCount = 0;
LogHelper.WriteLogToFile($"AutoUpdate | Retry successful, found new version: {AvailableLatestVersion}");
// 停止重试定时器,因为已经找到了更新
return;
}
else
{
// 检查更新仍然失败,继续重试
LogHelper.WriteLogToFile($"AutoUpdate | Retry {updateCheckRetryCount} failed, will retry in 10 minutes");
// 重新启动定时器,10分钟后再次尝试
timerCheckAutoUpdateRetry.Start();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | Error in retry check: {ex.Message}", LogHelper.LogType.Error);
// 出错时也重新启动定时器,稍后再检查
if (updateCheckRetryCount <= MAX_UPDATE_CHECK_RETRIES)
{
timerCheckAutoUpdateRetry.Start();
}
}
}
// 重置更新检查重试状态
public void ResetUpdateCheckRetry()
{
try
{
// 停止重试定时器
timerCheckAutoUpdateRetry.Stop();
// 重置重试计数
updateCheckRetryCount = 0;
LogHelper.WriteLogToFile("AutoUpdate | Update check retry state reset");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | Error resetting retry state: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
}
+417 -261
View File
@@ -5,9 +5,9 @@ using System.Linq;
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.Imaging;
using Point = System.Windows.Point;
namespace Ink_Canvas
@@ -21,30 +21,138 @@ namespace Ink_Canvas
private bool isSingleFingerDragMode;
private Point centerPoint = new Point(0, 0);
private InkCanvasEditingMode lastInkCanvasEditingMode = InkCanvasEditingMode.Ink;
private DateTime lastTouchDownTime = DateTime.MinValue;
private const double MULTI_TOUCH_DELAY_MS = 100;
private bool isMultiTouchTimerActive = false;
/// <summary>
/// </summary>
/// 保存画布上的非笔画元素(如图片、媒体元素等)
/// </summary>
private List<UIElement> PreserveNonStrokeElements()
{
var preservedElements = new List<UIElement>();
// 遍历inkCanvas的所有子元素
// 遍历inkCanvas的所有子元素,创建副本而不是直接引用
for (int i = inkCanvas.Children.Count - 1; i >= 0; i--)
{
var child = inkCanvas.Children[i];
// 保存图片、媒体元素等非笔画相关的UI元素
if (child is Image || child is MediaElement ||
(child is Border border && border.Name != "AdvancedEraserOverlay"))
(child is Border border && border.Name != "EraserOverlayCanvas"))
{
preservedElements.Add(child);
// 创建元素的深拷贝,避免直接引用导致的问题
var clonedElement = CloneUIElement(child);
if (clonedElement != null)
{
preservedElements.Add(clonedElement);
}
}
}
return preservedElements;
}
/// <summary>
/// 克隆UI元素,创建深拷贝
/// </summary>
private UIElement CloneUIElement(UIElement originalElement)
{
try
{
if (originalElement is Image originalImage)
{
var clonedImage = new Image();
// 复制图片源
if (originalImage.Source is BitmapSource bitmapSource)
{
clonedImage.Source = bitmapSource;
}
// 复制属性
clonedImage.Width = originalImage.Width;
clonedImage.Height = originalImage.Height;
clonedImage.Stretch = originalImage.Stretch;
clonedImage.StretchDirection = originalImage.StretchDirection;
clonedImage.Name = originalImage.Name;
clonedImage.IsHitTestVisible = originalImage.IsHitTestVisible;
clonedImage.Focusable = originalImage.Focusable;
clonedImage.Cursor = originalImage.Cursor;
clonedImage.IsManipulationEnabled = originalImage.IsManipulationEnabled;
// 复制位置
InkCanvas.SetLeft(clonedImage, InkCanvas.GetLeft(originalImage));
InkCanvas.SetTop(clonedImage, InkCanvas.GetTop(originalImage));
// 复制变换
if (originalImage.RenderTransform != null)
{
clonedImage.RenderTransform = originalImage.RenderTransform.Clone();
}
return clonedImage;
}
else if (originalElement is MediaElement originalMedia)
{
var clonedMedia = new MediaElement();
// 复制媒体属性
clonedMedia.Source = originalMedia.Source;
clonedMedia.Width = originalMedia.Width;
clonedMedia.Height = originalMedia.Height;
clonedMedia.Name = originalMedia.Name;
clonedMedia.IsHitTestVisible = originalMedia.IsHitTestVisible;
clonedMedia.Focusable = originalMedia.Focusable;
// 复制位置
InkCanvas.SetLeft(clonedMedia, InkCanvas.GetLeft(originalMedia));
InkCanvas.SetTop(clonedMedia, InkCanvas.GetTop(originalMedia));
// 复制变换
if (originalMedia.RenderTransform != null)
{
clonedMedia.RenderTransform = originalMedia.RenderTransform.Clone();
}
return clonedMedia;
}
else if (originalElement is Border originalBorder)
{
var clonedBorder = new Border();
// 复制边框属性
clonedBorder.Width = originalBorder.Width;
clonedBorder.Height = originalBorder.Height;
clonedBorder.Name = originalBorder.Name;
clonedBorder.IsHitTestVisible = originalBorder.IsHitTestVisible;
clonedBorder.Focusable = originalBorder.Focusable;
clonedBorder.Background = originalBorder.Background;
clonedBorder.BorderBrush = originalBorder.BorderBrush;
clonedBorder.BorderThickness = originalBorder.BorderThickness;
clonedBorder.CornerRadius = originalBorder.CornerRadius;
// 复制位置
InkCanvas.SetLeft(clonedBorder, InkCanvas.GetLeft(originalBorder));
InkCanvas.SetTop(clonedBorder, InkCanvas.GetTop(originalBorder));
// 复制变换
if (originalBorder.RenderTransform != null)
{
clonedBorder.RenderTransform = originalBorder.RenderTransform.Clone();
}
return clonedBorder;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"克隆UI元素失败: {ex.Message}", LogHelper.LogType.Error);
}
return null;
}
/// <summary>
/// 恢复之前保存的非笔画元素到画布
/// </summary>
@@ -54,11 +162,15 @@ namespace Ink_Canvas
foreach (var element in preservedElements)
{
// 确保元素没有父容器再添加到inkCanvas
if (element is FrameworkElement fe && fe.Parent == null)
try
{
// 由于现在使用的是克隆的元素,不需要检查Parent属性
inkCanvas.Children.Add(element);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复非笔画元素失败: {ex.Message}", LogHelper.LogType.Error);
}
}
}
@@ -108,6 +220,18 @@ namespace Ink_Canvas
private void MainWindow_TouchDown(object sender, TouchEventArgs e)
{
// 检查触摸是否发生在浮动栏区域,如果是则允许事件传播到浮动栏按钮
var touchPoint = e.GetTouchPoint(this);
var floatingBarBounds = ViewboxFloatingBar.TransformToAncestor(this).TransformBounds(
new Rect(0, 0, ViewboxFloatingBar.ActualWidth, ViewboxFloatingBar.ActualHeight));
// 如果触摸发生在浮动栏区域,不阻止事件传播,让浮动栏按钮能够接收触摸事件
if (floatingBarBounds.Contains(touchPoint.Position))
{
// 不设置 ViewboxFloatingBar.IsHitTestVisible = false,让浮动栏按钮能够接收触摸事件
return;
}
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint
|| inkCanvas.EditingMode == InkCanvasEditingMode.EraseByStroke
|| inkCanvas.EditingMode == InkCanvasEditingMode.Select) return;
@@ -118,18 +242,23 @@ namespace Ink_Canvas
HideSubPanels(); // 书写时自动隐藏二级菜单
}
// 修复:几何绘制模式下完全禁止触摸轨迹收集
if (drawingShapeMode != 0)
{
// 确保几何绘制模式下不切换到Ink模式,避免触摸轨迹被收集
inkCanvas.EditingMode = InkCanvasEditingMode.None;
isTouchDown = true;
ViewboxFloatingBar.IsHitTestVisible = false;
BlackboardUIGridForInkReplay.IsHitTestVisible = false;
// 设置起始点
if (NeedUpdateIniP()) iniP = e.GetTouchPoint(inkCanvas).Position;
return;
}
// 只保留普通橡皮逻辑
TouchDownPointsList[e.TouchDevice.Id] = InkCanvasEditingMode.None;
inkCanvas.EraserShape = new EllipseStylusShape(50, 50);
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke)
{
inkCanvas.EditingMode = InkCanvasEditingMode.None;
@@ -138,25 +267,47 @@ namespace Ink_Canvas
private void MainWindow_StylusDown(object sender, StylusDownEventArgs e)
{
// 新增:根据是否为笔尾自动切换橡皮擦/画笔模式
// 检查手写笔点击是否发生在浮动栏区域,如果是则允许事件传播到浮动栏按钮
var stylusPoint = e.GetPosition(this);
var floatingBarBounds = ViewboxFloatingBar.TransformToAncestor(this).TransformBounds(
new Rect(0, 0, ViewboxFloatingBar.ActualWidth, ViewboxFloatingBar.ActualHeight));
// 如果手写笔点击发生在浮动栏区域,不阻止事件传播,让浮动栏按钮能够接收手写笔事件
if (floatingBarBounds.Contains(stylusPoint))
{
// 不设置 ViewboxFloatingBar.IsHitTestVisible = false,让浮动栏按钮能够接收手写笔事件
return;
}
// 根据是否为笔尾自动切换橡皮擦/画笔模式
if (e.StylusDevice.Inverted)
{
inkCanvas.EditingMode = InkCanvasEditingMode.EraseByPoint;
}
else
{
// 修复:几何绘制模式下完全禁止触摸轨迹收集
if (drawingShapeMode != 0)
{
// 确保几何绘制模式下不切换到Ink模式,避免触摸轨迹被收集
inkCanvas.EditingMode = InkCanvasEditingMode.None;
isTouchDown = true;
ViewboxFloatingBar.IsHitTestVisible = false;
BlackboardUIGridForInkReplay.IsHitTestVisible = false;
// 设置起始点
if (NeedUpdateIniP()) iniP = e.GetPosition(inkCanvas);
return;
}
// 修复:保持当前的线擦模式,不要强制切换到Ink模式
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
else
{
LogHelper.WriteLogToFile("保持当前线擦模式");
}
}
SetCursorBasedOnEditingMode(inkCanvas);
@@ -173,7 +324,7 @@ namespace Ink_Canvas
// 根据当前编辑模式设置不同的光标
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint)
{
inkCanvas.Cursor = Cursors.Cross;
inkCanvas.Cursor = Cursors.Arrow;
}
else if (inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
{
@@ -195,17 +346,68 @@ namespace Ink_Canvas
private async void MainWindow_StylusUp(object sender, StylusEventArgs e)
{
if (drawingShapeMode != 0)
{
// 重置触摸状态
isTouchDown = false;
ViewboxFloatingBar.IsHitTestVisible = true;
BlackboardUIGridForInkReplay.IsHitTestVisible = true;
// 对于双曲线等需要多步绘制的图形,手写笔抬起时应该进入下一步
if (drawingShapeMode == 24 || drawingShapeMode == 25)
{
if (drawMultiStepShapeCurrentStep == 0)
{
// 第一笔完成,进入第二笔
drawMultiStepShapeCurrentStep = 1;
}
else
{
// 第二笔完成,完成绘制
var mouseArgs = new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Left)
{
RoutedEvent = MouseLeftButtonUpEvent,
Source = inkCanvas
};
inkCanvas_MouseUp(inkCanvas, mouseArgs);
}
}
else
{
// 其他单步绘制的图形,手写笔抬起时完成绘制
var mouseArgs = new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Left)
{
RoutedEvent = MouseLeftButtonUpEvent,
Source = inkCanvas
};
inkCanvas_MouseUp(inkCanvas, mouseArgs);
}
return;
}
try
{
inkCanvas.Strokes.Add(GetStrokeVisual(e.StylusDevice.Id).Stroke);
await Task.Delay(5); // 避免渲染墨迹完成前预览墨迹被删除导致墨迹闪烁
inkCanvas.Children.Remove(GetVisualCanvas(e.StylusDevice.Id));
var stroke = GetStrokeVisual(e.StylusDevice.Id).Stroke;
inkCanvas_StrokeCollected(inkCanvas,
new InkCanvasStrokeCollectedEventArgs(GetStrokeVisual(e.StylusDevice.Id).Stroke));
if (stroke != null)
{
inkCanvas.Strokes.Add(stroke);
await Task.Delay(5);
inkCanvas.Children.Remove(GetVisualCanvas(e.StylusDevice.Id));
inkCanvas_StrokeCollected(inkCanvas,
new InkCanvasStrokeCollectedEventArgs(stroke));
}
else
{
await Task.Delay(5);
inkCanvas.Children.Remove(GetVisualCanvas(e.StylusDevice.Id));
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"MainWindow_StylusUp 出错: {ex}", LogHelper.LogType.Error);
Label.Content = ex.ToString();
}
@@ -240,6 +442,16 @@ namespace Ink_Canvas
{
try
{
if (drawingShapeMode != 0)
{
if (isTouchDown)
{
Point stylusPoint = e.GetPosition(inkCanvas);
MouseTouchMove(stylusPoint);
}
return;
}
if (GetTouchDownPointsList(e.StylusDevice.Id) != InkCanvasEditingMode.None) return;
try
{
@@ -270,8 +482,8 @@ namespace Ink_Canvas
var strokeVisual = new StrokeVisual(inkCanvas.DefaultDrawingAttributes.Clone());
StrokeVisualList[id] = strokeVisual;
StrokeVisualList[id] = strokeVisual;
var visualCanvas = new VisualCanvas(strokeVisual);
var visualCanvas = new VisualCanvas();
strokeVisual.SetVisualCanvas(visualCanvas);
VisualCanvasList[id] = visualCanvas;
inkCanvas.Children.Add(visualCanvas);
@@ -297,30 +509,48 @@ namespace Ink_Canvas
#endregion
private int lastTouchDownTime = 0, lastTouchUpTime = 0;
private Point iniP = new Point(0, 0);
private void Main_Grid_TouchDown(object sender, TouchEventArgs e)
{
// 检查触摸是否发生在浮动栏区域,如果是则允许事件传播到浮动栏按钮
var touchPoint = e.GetTouchPoint(this);
var floatingBarBounds = ViewboxFloatingBar.TransformToAncestor(this).TransformBounds(
new Rect(0, 0, ViewboxFloatingBar.ActualWidth, ViewboxFloatingBar.ActualHeight));
// 如果触摸发生在浮动栏区域,不阻止事件传播,让浮动栏按钮能够接收触摸事件
if (floatingBarBounds.Contains(touchPoint.Position))
{
// 不设置 ViewboxFloatingBar.IsHitTestVisible = false,让浮动栏按钮能够接收触摸事件
return;
}
SetCursorBasedOnEditingMode(inkCanvas);
inkCanvas.CaptureTouch(e.TouchDevice);
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint)
{
// 橡皮状态下只return,保证橡皮状态可保持
return;
}
if (drawingShapeMode != 0)
{
inkCanvas.EditingMode = InkCanvasEditingMode.None;
// 设置触摸状态,类似鼠标事件处理
isTouchDown = true;
ViewboxFloatingBar.IsHitTestVisible = false;
BlackboardUIGridForInkReplay.IsHitTestVisible = false;
// 设置起始点
if (NeedUpdateIniP()) iniP = e.GetTouchPoint(inkCanvas).Position;
return;
}
if (inkCanvas.EditingMode == InkCanvasEditingMode.Select)
{
// 套索选状态下只return,保证套索选可用
return;
}
// 修复:几何绘制模式下完全禁止触摸轨迹收集
if (drawingShapeMode != 0)
{
// 确保几何绘制模式下不切换到Ink模式,避免触摸轨迹被收集
inkCanvas.EditingMode = InkCanvasEditingMode.None;
dec.Add(e.TouchDevice.Id);
return;
}
if (inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
@@ -338,108 +568,32 @@ namespace Ink_Canvas
}
}
// 手掌擦相关变量
private bool isPalmEraserActive;
private InkCanvasEditingMode palmEraserLastEditingMode = InkCanvasEditingMode.Ink;
private bool palmEraserLastIsHighlighter;
private bool palmEraserWasEnabledBeforeMultiTouch;
public double GetTouchBoundWidth(TouchEventArgs e)
{
var args = e.GetTouchPoint(null).Bounds;
double value;
if (!Settings.Advanced.IsQuadIR) value = args.Width;
else value = Math.Sqrt(args.Width * args.Height); //四边红外
if (Settings.Advanced.IsSpecialScreen) value *= Settings.Advanced.TouchMultiplier;
return value;
}
private void inkCanvas_PreviewTouchDown(object sender, TouchEventArgs e)
{
// 橡皮状态下不做任何切换,直接return,保证橡皮可持续
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint
|| inkCanvas.EditingMode == InkCanvasEditingMode.EraseByStroke)
{
return;
}
// 修复:几何绘制模式下完全禁止触摸轨迹收集
if (drawingShapeMode != 0)
{
// 确保几何绘制模式下不切换到Ink模式,避免触摸轨迹收集
inkCanvas.EditingMode = InkCanvasEditingMode.None;
// 几何绘制模式下不记录触摸点,避免触摸轨迹被收集
SetCursorBasedOnEditingMode(inkCanvas);
inkCanvas.CaptureTouch(e.TouchDevice);
ViewboxFloatingBar.IsHitTestVisible = false;
BlackboardUIGridForInkReplay.IsHitTestVisible = false;
// 修复:几何绘制模式下,只记录几何绘制的起点,不记录触摸轨迹
if (dec.Count == 0)
{
var touchPoint = e.GetTouchPoint(inkCanvas);
// 对于双曲线绘制,第一笔时记录起点,第二笔时不更新起点
if (drawingShapeMode == 24 || drawingShapeMode == 25)
{
// 双曲线绘制:第一笔记录起点,第二笔保持第一笔的起点
if (drawMultiStepShapeCurrentStep == 0)
{
iniP = touchPoint.Position;
}
// 第二笔时不更新iniP,保持第一笔的起点
}
else
{
// 其他图形正常记录起点
iniP = touchPoint.Position;
}
lastTouchDownStrokeCollection = inkCanvas.Strokes.Clone();
}
dec.Add(e.TouchDevice.Id);
return;
}
// 非几何绘制模式下的正常触摸处理
SetCursorBasedOnEditingMode(inkCanvas);
inkCanvas.CaptureTouch(e.TouchDevice);
ViewboxFloatingBar.IsHitTestVisible = false;
BlackboardUIGridForInkReplay.IsHitTestVisible = false;
dec.Add(e.TouchDevice.Id);
// Palm Eraser 逻辑
if (Settings.Canvas.EnablePalmEraser && dec.Count >= 2 && !isPalmEraserActive)
{
var bounds = e.GetTouchPoint(inkCanvas).Bounds;
double palmThreshold = 40; // 触摸面积阈值,可根据实际调整
if (bounds.Width >= palmThreshold || bounds.Height >= palmThreshold)
//设备1个的时候,记录中心点
if (dec.Count == 1)
{
// 记录当前编辑模式和高光状态
palmEraserLastEditingMode = inkCanvas.EditingMode;
palmEraserLastIsHighlighter = drawingAttributes.IsHighlighter;
// 切换为橡皮擦
EraserIcon_Click(null, null);
isPalmEraserActive = true;
}
}
// 设备1个的时候,记录中心点
if (dec.Count == 1)
{
var touchPoint = e.GetTouchPoint(inkCanvas);
centerPoint = touchPoint.Position;
var touchPoint = e.GetTouchPoint(inkCanvas);
centerPoint = touchPoint.Position;
// 修复:只允许在此处赋值iniP,防止TouchMove等其他地方覆盖,保证几何绘制起点一致
if (drawingShapeMode != 0)
{
// 对于双曲线绘制,第一笔时记录起点,第二笔时不更新起点
if (drawingShapeMode == 24 || drawingShapeMode == 25)
{
// 双曲线绘制:第一笔记录起点,第二笔保持第一笔的起点
if (drawMultiStepShapeCurrentStep == 0)
{
iniP = touchPoint.Position;
}
// 第二笔时不更新iniP,保持第一笔的起点
}
else
{
// 其他图形正常记录起点
iniP = touchPoint.Position;
}
//记录第一根手指点击时的 StrokeCollection
lastTouchDownStrokeCollection = inkCanvas.Strokes.Clone();
}
// 记录第一根手指点击时的 StrokeCollection
lastTouchDownStrokeCollection = inkCanvas.Strokes.Clone();
}
//设备两个及两个以上,将画笔功能关闭
if (dec.Count > 1 || isSingleFingerDragMode || !Settings.Gesture.IsEnableTwoFingerGesture)
{
@@ -447,59 +601,57 @@ namespace Ink_Canvas
if (inkCanvas.EditingMode == InkCanvasEditingMode.None ||
inkCanvas.EditingMode == InkCanvasEditingMode.Select) return;
lastInkCanvasEditingMode = inkCanvas.EditingMode;
// 修复:几何绘制模式下禁止切回Ink
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& drawingShapeMode == 0)
{
inkCanvas.EditingMode = InkCanvasEditingMode.None;
}
inkCanvas.EditingMode = InkCanvasEditingMode.None;
}
}
private void inkCanvas_PreviewTouchMove(object sender, TouchEventArgs e)
{
}
private void inkCanvas_PreviewTouchUp(object sender, TouchEventArgs e)
{
// 橡皮状态下不做任何切换,直接return,保证橡皮可持续
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint && !isPalmEraserActive)
{
return;
}
inkCanvas.ReleaseAllTouchCaptures();
ViewboxFloatingBar.IsHitTestVisible = true;
BlackboardUIGridForInkReplay.IsHitTestVisible = true;
// Palm Eraser 逻辑:所有点抬起后恢复原编辑模式
//手势完成后切回之前的状态
if (dec.Count > 1)
if (inkCanvas.EditingMode == InkCanvasEditingMode.None)
inkCanvas.EditingMode = lastInkCanvasEditingMode;
dec.Remove(e.TouchDevice.Id);
if (isPalmEraserActive && dec.Count == 0)
// 重置多触控点定时器状态
if (dec.Count <= 1)
{
// 恢复高光状态
drawingAttributes.IsHighlighter = palmEraserLastIsHighlighter;
// 恢复编辑模式
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint)
isMultiTouchTimerActive = false;
}
if (dec.Count == 0)
{
isSingleFingerDragMode = false;
isWaitUntilNextTouchDown = false;
if (drawingShapeMode == 0
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& inkCanvas.EditingMode != InkCanvasEditingMode.Select
&& inkCanvas.EditingMode != InkCanvasEditingMode.None)
{
if (palmEraserLastEditingMode == InkCanvasEditingMode.Ink)
if (lastInkCanvasEditingMode != InkCanvasEditingMode.None)
{
PenIcon_Click(null, null);
}
else if (palmEraserLastEditingMode == InkCanvasEditingMode.Select)
{
SymbolIconSelect_MouseUp(null, null);
}
else
{
inkCanvas.EditingMode = palmEraserLastEditingMode;
inkCanvas.EditingMode = lastInkCanvasEditingMode;
}
}
isPalmEraserActive = false;
}
// 修复:几何绘制模式下,触摸抬手时应该正确处理,而不是简单模拟鼠标事件
if (drawingShapeMode != 0)
{
// 对于双曲线等需要多步绘制的图形,触摸抬手时应该进入下一步
isTouchDown = false;
ViewboxFloatingBar.IsHitTestVisible = true;
BlackboardUIGridForInkReplay.IsHitTestVisible = true;
if (drawingShapeMode == 24 || drawingShapeMode == 25)
{
// 双曲线绘制:触摸抬手时进入下一步,但不自动触发鼠标抬起事件
// 让用户继续绘制第二笔
if (drawMultiStepShapeCurrentStep == 0)
{
// 第一笔完成,进入第二笔
@@ -518,7 +670,6 @@ namespace Ink_Canvas
}
else
{
// 其他单步绘制的图形,触摸抬手时完成绘制
var mouseArgs = new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Left)
{
RoutedEvent = MouseLeftButtonUpEvent,
@@ -528,31 +679,6 @@ namespace Ink_Canvas
}
}
// 手势完成后切回之前的状态
if (drawingShapeMode == 0)
{
if (dec.Count > 1)
{
if (inkCanvas.EditingMode == InkCanvasEditingMode.None)
{
if (lastInkCanvasEditingMode != InkCanvasEditingMode.EraseByPoint)
{
inkCanvas.EditingMode = lastInkCanvasEditingMode;
}
}
}
else if (dec.Count == 0)
{
// 当所有触摸点都抬起时,确保正确恢复编辑模式
// 这对于从橡皮擦切换到笔后恢复多指手势功能很重要
if (inkCanvas.EditingMode == InkCanvasEditingMode.None &&
lastInkCanvasEditingMode != InkCanvasEditingMode.None &&
lastInkCanvasEditingMode != InkCanvasEditingMode.EraseByPoint)
{
inkCanvas.EditingMode = lastInkCanvasEditingMode;
}
}
}
inkCanvas.Opacity = 1;
if (dec.Count == 0)
@@ -574,30 +700,37 @@ namespace Ink_Canvas
private void Main_Grid_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
{
if (e.Manipulators.Count() != 0) return;
// 修复:几何绘制模式下不自动切换到Ink模式,避免触摸轨迹被收集
if (drawingShapeMode == 0
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke)
if (e.Manipulators.Count() == 0)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
// 修复:确保多指手势完成后正确更新lastInkCanvasEditingMode
lastInkCanvasEditingMode = InkCanvasEditingMode.Ink;
if (dec.Count > 0)
{
dec.Clear();
}
isSingleFingerDragMode = false;
if (drawingShapeMode == 0
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& inkCanvas.EditingMode != InkCanvasEditingMode.Select)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
lastInkCanvasEditingMode = InkCanvasEditingMode.Ink;
}
}
}
private void Main_Grid_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
// 手掌擦时禁止移动/缩放
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint)
return;
// 三指及以上禁止缩放
bool disableScale = dec.Count >= 3;
if (isInMultiTouchMode || !Settings.Gesture.IsEnableTwoFingerGesture) return;
if ((dec.Count >= 2 && (Settings.PowerPointSettings.IsEnableTwoFingerGestureInPresentationMode ||
StackPanelPPTControls.Visibility != Visibility.Visible ||
StackPanelPPTButtons.Visibility == Visibility.Collapsed)) ||
isSingleFingerDragMode)
bool hasMultipleManipulators = e.Manipulators.Count() >= 2;
bool shouldUseTwoFingerGesture = (dec.Count >= 2 && hasMultipleManipulators &&
(Settings.PowerPointSettings.IsEnableTwoFingerGestureInPresentationMode ||
StackPanelPPTControls.Visibility != Visibility.Visible ||
StackPanelPPTButtons.Visibility == Visibility.Collapsed)) ||
isSingleFingerDragMode;
if (shouldUseTwoFingerGesture)
{
var md = e.DeltaManipulation;
var trans = md.Translation; // 获得位移矢量
@@ -607,20 +740,23 @@ namespace Ink_Canvas
if (Settings.Gesture.IsEnableTwoFingerTranslate)
m.Translate(trans.X, trans.Y); // 移动
// 计算中心点(用于缩放和旋转)
var fe = e.Source as FrameworkElement;
var center = new Point(fe.ActualWidth / 2, fe.ActualHeight / 2);
center = m.Transform(center); // 转换为矩阵缩放和旋转的中心点
if (Settings.Gesture.IsEnableTwoFingerGestureTranslateOrRotation)
{
var rotate = md.Rotation; // 获得旋转角度
var scale = md.Scale; // 获得缩放倍数
// Find center of element and then transform to get current location of center
var fe = e.Source as FrameworkElement;
var center = new Point(fe.ActualWidth / 2, fe.ActualHeight / 2);
center = m.Transform(center); // 转换为矩阵缩放和旋转的中心点
if (Settings.Gesture.IsEnableTwoFingerRotation)
m.RotateAt(rotate, center.X, center.Y); // 旋转
if (Settings.Gesture.IsEnableTwoFingerZoom && !disableScale)
m.ScaleAt(scale.X, scale.Y, center.X, center.Y); // 缩放
}
if (Settings.Gesture.IsEnableTwoFingerZoom)
{
var scale = md.Scale; // 获得缩放倍数
m.ScaleAt(scale.X, scale.Y, center.X, center.Y); // 缩放
}
var strokes = inkCanvas.GetSelectedStrokes();
@@ -667,12 +803,15 @@ namespace Ink_Canvas
catch { }
}
;
// 同时变换画布上的图片元素
TransformCanvasImages(m);
}
else
{
foreach (var stroke in inkCanvas.Strokes) stroke.Transform(m, false);
;
// 同时变换画布上的图片元素
TransformCanvasImages(m);
}
foreach (var circle in circles)
@@ -690,68 +829,85 @@ namespace Ink_Canvas
}
}
// 退出多指书写模式,恢复InkCanvas的TouchDown事件绑定
private void ExitMultiTouchModeIfNeeded()
/// <summary>
/// 变换画布上的图片元素,使其与墨迹同步移动
/// </summary>
private void TransformCanvasImages(Matrix matrix)
{
if (isInMultiTouchMode)
try
{
inkCanvas.StylusDown -= MainWindow_StylusDown;
inkCanvas.StylusMove -= MainWindow_StylusMove;
inkCanvas.StylusUp -= MainWindow_StylusUp;
inkCanvas.TouchDown -= MainWindow_TouchDown;
inkCanvas.TouchDown += Main_Grid_TouchDown;
// 修复:几何绘制模式下不自动切换到Ink模式,避免触摸轨迹被收集
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& drawingShapeMode == 0)
// 遍历inkCanvas的所有子元素,找到图片元素
for (int i = inkCanvas.Children.Count - 1; i >= 0; i--)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
// 保存非笔画元素(如图片)
var preservedElements = PreserveNonStrokeElements();
inkCanvas.Children.Clear();
// 恢复非笔画元素
RestoreNonStrokeElements(preservedElements);
isInMultiTouchMode = false;
// 关闭多指书写时,恢复手掌擦开关
if (palmEraserWasEnabledBeforeMultiTouch)
{
Settings.Canvas.EnablePalmEraser = true;
if (ToggleSwitchEnablePalmEraser != null)
ToggleSwitchEnablePalmEraser.IsOn = true;
var child = inkCanvas.Children[i];
if (child is Image image)
{
// 应用矩阵变换到图片
ApplyMatrixTransformToImage(image, matrix);
}
else if (child is MediaElement mediaElement)
{
// 对媒体元素也应用变换
ApplyMatrixTransformToMediaElement(mediaElement, matrix);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"变换画布图片失败: {ex.Message}", LogHelper.LogType.Error);
}
}
// 进入多指书写模式,绑定Main_Grid_TouchDown
private void EnterMultiTouchModeIfNeeded()
/// <summary>
/// 对图片应用矩阵变换
/// </summary>
private void ApplyMatrixTransformToImage(Image image, Matrix matrix)
{
if (!isInMultiTouchMode)
try
{
inkCanvas.StylusDown += MainWindow_StylusDown;
inkCanvas.StylusMove += MainWindow_StylusMove;
inkCanvas.StylusUp += MainWindow_StylusUp;
inkCanvas.TouchDown += MainWindow_TouchDown;
inkCanvas.TouchDown -= Main_Grid_TouchDown;
// 修复:几何绘制模式下不自动切换到Ink模式,避免触摸轨迹被收集
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& drawingShapeMode == 0)
// 获取图片的RenderTransform,如果不存在则创建新的TransformGroup
TransformGroup transformGroup = image.RenderTransform as TransformGroup;
if (transformGroup == null)
{
inkCanvas.EditingMode = InkCanvasEditingMode.None;
transformGroup = new TransformGroup();
image.RenderTransform = transformGroup;
}
// 保存非笔画元素(如图片)
var preservedElements = PreserveNonStrokeElements();
inkCanvas.Children.Clear();
// 恢复非笔画元素
RestoreNonStrokeElements(preservedElements);
isInMultiTouchMode = true;
// 启用多指书写时,自动禁用手掌擦
palmEraserWasEnabledBeforeMultiTouch = Settings.Canvas.EnablePalmEraser;
Settings.Canvas.EnablePalmEraser = false;
if (ToggleSwitchEnablePalmEraser != null)
ToggleSwitchEnablePalmEraser.IsOn = false;
// 创建新的MatrixTransform并添加到变换组
var matrixTransform = new MatrixTransform(matrix);
transformGroup.Children.Add(matrixTransform);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用图片变换失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 对媒体元素应用矩阵变换
/// </summary>
private void ApplyMatrixTransformToMediaElement(MediaElement mediaElement, Matrix matrix)
{
try
{
// 获取媒体元素的RenderTransform,如果不存在则创建新的TransformGroup
TransformGroup transformGroup = mediaElement.RenderTransform as TransformGroup;
if (transformGroup == null)
{
transformGroup = new TransformGroup();
mediaElement.RenderTransform = transformGroup;
}
// 创建新的MatrixTransform并添加到变换组
var matrixTransform = new MatrixTransform(matrix);
transformGroup.Children.Add(matrixTransform);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用媒体元素变换失败: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
+75 -1
View File
@@ -30,6 +30,12 @@ namespace Ink_Canvas
var mainWin = (MainWindow)Current.MainWindow;
if (mainWin.IsLoaded)
{
// 在无焦点模式下,暂时取消主窗口置顶,让系统菜单能够正常显示
if (Ink_Canvas.MainWindow.Settings.Advanced.IsAlwaysOnTop && Ink_Canvas.MainWindow.Settings.Advanced.IsNoFocusMode)
{
mainWin.Topmost = false;
}
// 判斷是否在收納模式中
if (mainWin.isFloatingBarFolded)
{
@@ -57,6 +63,19 @@ namespace Ink_Canvas
}
}
private void SysTrayMenu_Closed(object sender, RoutedEventArgs e)
{
var mainWin = (MainWindow)Current.MainWindow;
if (mainWin.IsLoaded)
{
// 菜单关闭后,恢复主窗口的置顶状态
if (Ink_Canvas.MainWindow.Settings.Advanced.IsAlwaysOnTop && Ink_Canvas.MainWindow.Settings.Advanced.IsNoFocusMode)
{
mainWin.Topmost = true;
}
}
}
private void CloseAppTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e)
{
var mainWin = (MainWindow)Current.MainWindow;
@@ -84,7 +103,7 @@ namespace Ink_Canvas
startInfo.UseShellExecute = true;
// 启动进程但不等待
Process.Start(startInfo);
Process.Start(new ProcessStartInfo(exePath, "-delay 2000") { UseShellExecute = true });
}
catch (Exception ex)
{
@@ -182,5 +201,60 @@ namespace Ink_Canvas
}
}
private void DisableAllHotkeysMenuItem_Clicked(object sender, RoutedEventArgs e)
{
var mainWin = (MainWindow)Current.MainWindow;
if (mainWin.IsLoaded)
{
try
{
// 获取全局快捷键管理器
var hotkeyManagerField = typeof(MainWindow).GetField("_globalHotkeyManager",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var hotkeyManager = hotkeyManagerField?.GetValue(mainWin) as GlobalHotkeyManager;
if (hotkeyManager != null)
{
// 禁用所有快捷键
hotkeyManager.DisableHotkeyRegistration();
// 更新菜单项文本和状态
var menuItem = sender as MenuItem;
if (menuItem != null)
{
var headerPanel = menuItem.Header as SimpleStackPanel;
if (headerPanel != null)
{
var textBlock = headerPanel.Children[0] as TextBlock;
if (textBlock != null)
{
if (textBlock.Text == "禁用所有快捷键")
{
textBlock.Text = "启用所有快捷键";
LogHelper.WriteLogToFile("已禁用所有快捷键", LogHelper.LogType.Event);
}
else
{
textBlock.Text = "禁用所有快捷键";
// 重新启用快捷键
hotkeyManager.EnableHotkeyRegistration();
LogHelper.WriteLogToFile("已启用所有快捷键", LogHelper.LogType.Event);
}
}
}
}
}
else
{
LogHelper.WriteLogToFile("无法获取全局快捷键管理器", LogHelper.LogType.Error);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"禁用/启用快捷键时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
using System.Reflection;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows;
@@ -10,7 +10,7 @@ using System.Windows;
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("CJK_mkp")]
[assembly: AssemblyProduct("InkCanvasForClass")]
[assembly: AssemblyCopyright("© Copyright HARKOTEK Studio 2024-now")]
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@@ -49,5 +49,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.7.5")]
[assembly: AssemblyFileVersion("1.7.7.5")]
[assembly: AssemblyVersion("1.7.18.2")]
[assembly: AssemblyFileVersion("1.7.18.2")]
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>dist\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
</PropertyGroup>
</Project>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
</Project>
+9
View File
@@ -74,5 +74,14 @@ namespace Ink_Canvas.Properties {
return ResourceManager.GetStream("TimerDownNotice", resourceCulture);
}
}
/// <summary>
/// Looks up a localized resource of type System.IO.UnmanagedMemoryStream similar to System.IO.MemoryStream.
/// </summary>
internal static UnmanagedMemoryStream ProgressiveAudio {
get {
return ResourceManager.GetStream("ProgressiveAudio", resourceCulture);
}
}
}
}
+3
View File
@@ -121,4 +121,7 @@
<data name="TimerDownNotice" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\TimerDownNotice.wav;System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="ProgressiveAudio" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\ProgressiveAudio.wav;System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
</root>
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.
+101
View File
@@ -0,0 +1,101 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="IconBrush" Color="{DynamicResource IconForeground}"></SolidColorBrush>
<DrawingImage x:Key="CursorIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M22.7989,10.1653L1.14304,1.14304 10.1653,22.7989 12.8305,14.9518 19.6892,21.8105 21.8105,19.6892 14.9518,12.8305 22.7989,10.1653z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="PenIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M20.4786,1.42438C19.9985,1.23743 19.4847,1.15194 18.9698,1.17319 18.4549,1.19444 17.9499,1.32197 17.4869,1.54789 17.0368,1.76752 16.6358,2.07554 16.3083,2.45361L3.85516,14.9067 9.08243,20.134 21.5311,7.68529C21.9113,7.36382 22.223,6.96912 22.447,6.52438 22.6786,6.06462 22.8113,5.56167 22.8365,5.04763 22.8616,4.5336 22.7787,4.02012 22.593,3.54002 22.4073,3.05994 22.1232,2.62403 21.759,2.25988 21.3949,1.89574 20.9587,1.61132 20.4786,1.42438z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M7.28056,21.1605L2.8286,16.7086 1.15912,22.83 7.28056,21.1605z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="EraserIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M15.6314,20.7262L22.7921,13.5655C24.3494,12.141,24.2819,9.81776,22.8105,8.34633L16.7793,2.31508C15.3547,0.757753,13.0315,0.825236,11.5601,2.29666L4.38099,9.47574 15.6314,20.7262z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M14.2172,22.1404L2.96677,10.89 1.20761,12.6491C-0.34971,14.0737,-0.281711,16.3974,1.18971,17.8688L6.15089,22.83 13.5276,22.83 14.2172,22.1404z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="TrashBinIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M2.15454,7.07729L2.15454,5.1082 7.07727,5.1082 7.07727,4.12365C7.07727,3.30648 7.47109,2.57791 7.98305,2.0758 8.49501,1.56383 9.22358,1.17001 10.0309,1.17001L13.9691,1.17001C14.7863,1.17001 15.5148,1.56383 16.0169,2.0758 16.5289,2.58776 16.9227,3.31632 16.9227,4.12365L16.9227,5.1082 21.8454,5.1082 21.8454,7.07729 2.15454,7.07729z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F0 M24,24z M0,0z M4.12363,19.7779C4.12363,20.6148 4.51745,21.3729 5.02941,21.8947 5.54138,22.4165 6.26994,22.83 7.07727,22.83L16.9227,22.83C17.7399,22.83 18.4685,22.4165 18.9706,21.8947 19.4825,21.3729 19.8764,20.6148 19.8764,19.7779L19.8764,8.06183 4.12363,8.06183 4.12363,19.7779z M12.9845,11.0155L14.9536,11.0155 14.9536,18.8918 12.9845,18.8918 12.9845,11.0155z M9.04636,11.0155L11.0154,11.0155 11.0154,18.8918 9.04636,18.8918 9.04636,11.0155z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="LassoSelectIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M2.12162,2.12162C2.73093,1.51232,3.55732,1.17001,4.41901,1.17001L5.50201,1.17001 5.50201,3.33601 4.41901,3.33601C4.13178,3.33601 3.85632,3.45011 3.65321,3.65321 3.45011,3.85632 3.33601,4.13178 3.33601,4.41901L3.33601,5.50201 1.17001,5.50201 1.17001,4.41901C1.17001,3.55732,1.51232,2.73093,2.12162,2.12162z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M18.498,1.17001L19.581,1.17001C20.4427,1.17001 21.2691,1.51232 21.8784,2.12162 22.4877,2.73093 22.83,3.55732 22.83,4.41901L22.83,5.50201 20.664,5.50201 20.664,4.41901C20.664,4.13178 20.5499,3.85632 20.3468,3.65321 20.1437,3.45011 19.8682,3.33601 19.581,3.33601L18.498,3.33601 18.498,1.17001z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M3.33601,19.581L3.33601,18.498 1.17001,18.498 1.17001,19.581C1.17001,20.4427 1.51232,21.2691 2.12162,21.8784 2.73093,22.4877 3.55732,22.83 4.41901,22.83L5.50201,22.83 5.50201,20.664 4.41901,20.664C4.13178,20.664 3.85632,20.5499 3.65321,20.3468 3.45011,20.1437 3.33601,19.8682 3.33601,19.581z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M7.66801,1.17001L10.917,1.17001 10.917,3.33601 7.66801,3.33601 7.66801,1.17001z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M12,20.664L7.66801,20.664 7.66801,22.83 12,22.83 12,20.664z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M13.083,1.17001L16.332,1.17001 16.332,3.33601 13.083,3.33601 13.083,1.17001z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M3.33601,10.917L3.33601,7.66801 1.17001,7.66801 1.17001,10.917 3.33601,10.917z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M22.83,7.66801L22.83,12 20.664,12 20.664,7.66801 22.83,7.66801z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M3.33601,16.332L3.33601,13.083 1.17001,13.083 1.17001,16.332 3.33601,16.332z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M10.7469,10.747L22.83,15.781 18.4517,17.2681 22.2785,21.0949 21.0949,22.2785 17.2681,18.4517 15.781,22.83 10.7469,10.747z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="ShapesIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M17.8604,10.7597L12.0068,1.17001 6.13961,10.7597 17.8604,10.7597z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M10.9345,13.2403L1.34476,13.2403 1.34476,22.83 10.9345,22.83 10.9345,13.2403z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M17.8604,13.2403C15.2122,13.2403 13.0655,15.387 13.0655,18.0352 13.0655,20.6833 15.2122,22.83 17.8604,22.83 20.5085,22.83 22.6552,20.6833 22.6552,18.0352 22.6552,15.387 20.5085,13.2403 17.8604,13.2403z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="UndoIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M8.71408,16.8493L0.874451,9.00964 8.71408,1.17001 8.71408,7.42358 15.7239,7.42358C16.7074,7.42358 17.6791,7.62744 18.583,8.02124 19.4866,8.41493 20.3023,8.98966 20.9857,9.70849 21.6689,10.4271 22.2069,11.276 22.5726,12.2047 22.9383,13.1333 23.1256,14.126 23.1256,15.1268 23.1256,16.1276 22.9383,17.1203 22.5726,18.0489 22.2069,18.9776 21.6689,19.8264 20.9857,20.5451 20.3023,21.2639 19.4866,21.8387 18.583,22.2324 17.6791,22.6262 16.7074,22.83 15.7239,22.83L10.437,22.83 10.437,19.6579 15.7239,19.6579C16.2679,19.6579 16.8086,19.5453 17.3159,19.3243 17.8235,19.1031 18.29,18.7767 18.6867,18.3594 19.0835,17.942 19.4023,17.4422 19.6211,16.8866 19.8399,16.3308 19.9534,15.7326 19.9534,15.1268 19.9534,14.5209 19.8399,13.9227 19.6211,13.367 19.4023,12.8114 19.0835,12.3115 18.6867,11.8941 18.29,11.4769 17.8235,11.1505 17.3159,10.9293 16.8086,10.7083 16.2679,10.5957 15.7239,10.5957L8.71408,10.5957 8.71408,16.8493z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="RedoIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M15.2859,16.8493L23.1255,9.00964 15.2859,1.17001 15.2859,7.42358 8.27607,7.42358C7.29262,7.42358 6.32086,7.62744 5.41703,8.02124 4.51341,8.41493 3.69773,8.98966 3.01434,9.70849 2.33111,10.4271 1.79312,11.276 1.42741,12.2047 1.06174,13.1333 0.874422,14.126 0.874422,15.1268 0.874422,16.1276 1.06174,17.1203 1.42741,18.0489 1.79312,18.9776 2.33111,19.8264 3.01434,20.5451 3.69773,21.2639 4.51341,21.8387 5.41703,22.2324 6.32086,22.6262 7.29262,22.83 8.27607,22.83L13.563,22.83 13.563,19.6579 8.27607,19.6579C7.7321,19.6579 7.19139,19.5453 6.68406,19.3243 6.17652,19.1031 5.70999,18.7767 5.31333,18.3594 4.91651,17.942 4.59775,17.4422 4.37894,16.8866 4.1601,16.3308 4.04656,15.7326 4.04656,15.1268 4.04656,14.5209 4.1601,13.9227 4.37894,13.367 4.59775,12.8114 4.91651,12.3115 5.31333,11.8941 5.70999,11.4769 6.17652,11.1505 6.68406,10.9293 7.19139,10.7083 7.7321,10.5957 8.27607,10.5957L15.2859,10.5957 15.2859,16.8493z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="MoreToolsIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M3.336,1.17001C2.13975,1.17001,1.17,2.13976,1.17,3.33601L1.17,8.75101C1.17,9.94726,2.13975,10.917,3.336,10.917L8.751,10.917C9.94725,10.917,10.917,9.94726,10.917,8.75101L10.917,3.33601C10.917,2.13976,9.94725,1.17001,8.751,1.17001L3.336,1.17001z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M15.249,1.17001C14.0527,1.17001,13.083,2.13976,13.083,3.33601L13.083,8.75101C13.083,9.94726,14.0527,10.917,15.249,10.917L20.664,10.917C21.8602,10.917,22.83,9.94726,22.83,8.75101L22.83,3.33601C22.83,2.13976,21.8602,1.17001,20.664,1.17001L15.249,1.17001z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M3.336,13.083C2.13975,13.083,1.17,14.0528,1.17,15.249L1.17,20.664C1.17,21.8603,2.13975,22.83,3.336,22.83L8.751,22.83C9.94725,22.83,10.917,21.8603,10.917,20.664L10.917,15.249C10.917,14.0528,9.94725,13.083,8.751,13.083L3.336,13.083z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M15.249,13.083C14.0527,13.083,13.083,14.0528,13.083,15.249L13.083,20.664C13.083,21.8603,14.0527,22.83,15.249,22.83L20.664,22.83C21.8602,22.83,22.83,21.8603,22.83,20.664L22.83,15.249C22.83,14.0528,21.8602,13.083,20.664,13.083L15.249,13.083z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="GestureIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F0 M24,24z M0,0z M7.82154,10.0753L7.82154,3.74613C7.82154,3.06604 8.08946,2.40655 8.57377,1.92224 9.05808,1.43793 9.70726,1.17001 10.3977,1.17001 11.0881,1.17001 11.7372,1.43793 12.2216,1.92224 12.7059,2.40655 12.9738,3.05573 12.9738,3.74613L12.9738,6.37308C13.1415,6.33947 13.3139,6.32225 13.489,6.32225 14.1794,6.32225 14.8286,6.59016 15.3129,7.07447 15.4484,7.21001 15.567,7.35845 15.6675,7.5171 15.9551,7.40916 16.2634,7.35269 16.5803,7.35269 17.2707,7.35269 17.9199,7.62061 18.4042,8.10492 18.5461,8.24683 18.6695,8.4029 18.7729,8.57001 19.6856,8.26338 20.7674,8.45871 21.4647,9.15599 21.949,9.6403 22.2169,10.2998 22.2169,10.9799L22.2169,15.6169C22.2169,17.5438 21.4647,19.3574 20.1045,20.7176 18.7443,22.0778 16.9307,22.83 15.0038,22.83L13.149,22.83 13.1799,22.8094 12.8398,22.8094C11.7682,22.7579 10.7068,22.4694 9.75878,21.9541 8.70773,21.3874 7.81124,20.563 7.15175,19.5738L6.94566,19.2647C6.60562,18.7494 5.49273,16.8019 3.52458,13.3087 3.19484,12.7213 3.1021,12.0412 3.27727,11.3818 3.45245,10.7326 3.86463,10.1761 4.44168,9.83608 5.00842,9.49604 5.66791,9.35177 6.31709,9.43421 6.86548,9.50385 7.39181,9.7279 7.82154,10.0753z M10.037,3.38547C10.1297,3.28243 10.2637,3.23091 10.3977,3.23091 10.5316,3.23091 10.6656,3.29273 10.7583,3.38547 10.8614,3.47821 10.9129,3.61217 10.9129,3.74613L10.9129,11.4745C10.9129,12.0412 11.3766,12.5049 11.9433,12.5049 12.5101,12.5049 12.9738,12.0412 12.9738,11.4745L12.9738,8.89836C12.9738,8.7644 13.0356,8.63045 13.1283,8.53771 13.2211,8.43466 13.355,8.38314 13.489,8.38314 13.623,8.38314 13.7569,8.44497 13.8497,8.53771 13.9527,8.63045 14.0042,8.7644 14.0042,8.89836L14.0042,11.4745C14.0042,12.0412 14.4679,12.5049 15.0347,12.5049 15.6014,12.5049 16.0651,12.0412 16.0651,11.4745L16.0651,9.92881C16.0651,9.79485 16.1269,9.66089 16.2197,9.56815 16.3124,9.46511 16.4464,9.41359 16.5803,9.41359 16.7143,9.41359 16.8483,9.47541 16.941,9.56815 17.044,9.66089 17.0956,9.79485 17.0956,9.92881L17.0956,10.5869C17.0752,10.7163 17.0646,10.8477 17.0646,10.9799 17.0646,11.0661 17.0754,11.1499 17.0956,11.2301L17.0956,11.4745C17.0956,12.0412 17.5593,12.5049 18.126,12.5049 18.6928,12.5049 19.1565,12.0412 19.1565,11.4745L19.1565,10.8128C19.1834,10.7399 19.2266,10.6727 19.2801,10.6192 19.4759,10.4234 19.8159,10.4234 20.0117,10.6192 20.1148,10.712 20.1663,10.8459 20.1663,10.9799L20.1663,15.6169C20.1663,16.9977 19.6408,18.296 18.6618,19.2647 17.6829,20.2333 16.3949,20.7691 15.0141,20.7691L13.1593,20.7691C12.3143,20.7691 11.4796,20.5527 10.7274,20.1509 9.98548,19.749 9.3363,19.1616 8.8726,18.4506L8.66651,18.1415C8.35738,17.6675 7.23419,15.7096 5.31756,12.2989 5.24543,12.1752 5.23512,12.0412 5.26604,11.9073 5.30725,11.7733 5.38969,11.6703 5.50304,11.5981 5.66791,11.4951 5.874,11.4539 6.06978,11.4745 6.26557,11.5054 6.45105,11.5878 6.59531,11.7321L8.11007,13.2469C8.49419,13.631 9.10425,13.648 9.50833,13.2978 9.73651,13.1084 9.88244,12.8229 9.88244,12.5049L9.88244,3.74613C9.88244,3.61217,9.94426,3.47821,10.037,3.38547z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M2.99905,6.31195L1.78313,4.65293 2.61779,4.04497C3.46275,3.4267,4.37985,2.89087,5.33817,2.46838L6.27587,2.0459 7.12084,3.93162 6.18313,4.3541C5.35878,4.72506,4.56533,5.17846,3.83372,5.71429L2.99905,6.32225 2.99905,6.31195z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M18.2806,5.20935L19.1565,5.75549 20.259,4.01404 19.3832,3.4679C18.1157,2.67446,16.7452,2.0768,15.3026,1.68523L14.303,1.41731 13.7672,3.40607 14.7667,3.67399C16.0033,4.00373,17.1883,4.51895,18.2806,5.20935z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="EyeOffIconV2">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F0 M24,24z M0,0z M22.8347,21.5006L2.50417,1.17001 1.16525,2.50893 5.45737,6.80104C3.85257,8.10197 2.53265,9.72576 1.64954,11.5964 1.53559,11.8433 1.52609,12.1282 1.63055,12.3751L1.63055,12.3941C1.63055,12.4131 1.64954,12.4321 1.65904,12.4606 1.68752,12.5175 1.72551,12.5935 1.77299,12.698 1.87744,12.8974 2.01988,13.1822 2.21929,13.5146 2.61812,14.1793 3.21635,15.0719 4.04249,15.9645 5.69477,17.7497 8.30612,19.5919 11.981,19.5919 13.7282,19.5919 15.4375,19.1551 16.9568,18.31L21.4768,22.83 22.8158,21.4911 22.8347,21.5006z M11.9905,15.8791C12.5033,15.8696 13.0066,15.7556 13.4719,15.5467 13.6428,15.4707 13.7852,15.3758 13.9372,15.2808L8.72394,10.0676C8.62898,10.2195 8.53402,10.3715 8.45805,10.5329 8.24915,10.9982 8.1257,11.5015 8.1257,12.0143 8.1162,12.527 8.21116,13.0303 8.40108,13.5051 8.591,13.9799 8.87587,14.4072 9.23671,14.768 9.59755,15.1289 10.0249,15.4138 10.4997,15.6037 10.9745,15.7936 11.4777,15.8791 11.9905,15.8791z" />
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="F1 M24,24z M0,0z M10.9063,6.37657C11.2749,6.33269 11.6452,6.30726 12,6.30726 14.974,6.30726 17.1134,7.78595 18.5447,9.32737 19.2601,10.0978 19.7852,10.8712 20.1309,11.4519 20.2587,11.6665 20.3611,11.8535 20.4389,12.0025 20.0809,12.6966 19.6562,13.3505 19.1703,13.9543L18.5749,14.694 20.0544,15.8848 20.6498,15.145C21.3248,14.3064 21.8966,13.387 22.3556,12.4078 22.4706,12.1625 22.475,11.8789 22.3683,11.6299L22.3669,11.6266 22.364,11.6201 22.3551,11.5998C22.3477,11.5833 22.3375,11.5605 22.3243,11.5319 22.2979,11.4748 22.2598,11.3946 22.2098,11.2945 22.1097,11.0944 21.9615,10.8142 21.7628,10.4805 21.3666,9.81482 20.7641,8.92644 19.9364,8.03508 18.2816,6.25295 15.6731,4.4081 12,4.4081 11.5572,4.4081 11.1108,4.43965 10.6818,4.49072L9.73885,4.60297 9.96336,6.48883 10.9063,6.37657z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</ResourceDictionary>
+63
View File
@@ -0,0 +1,63 @@
using System.Windows;
using System.Windows.Media;
namespace Ink_Canvas.Resources.ICCConfiguration
{
public enum InitialPositionTypes
{
TopLeft, TopRight, BottomLeft, BottomRight, TopCenter, BottomCenter, Custom
}
public enum ElementCornerRadiusTypes
{
SuperEllipse, Circle, Custom, None
}
public class NearSnapAreaSize
{
public double[] TopLeft { get; set; } = { 24, 24 };
public double[] TopRight { get; set; } = { 24, 24 };
public double[] BottomLeft { get; set; } = { 24, 24 };
public double[] BottomRight { get; set; } = { 24, 24 };
public double TopCenter { get; set; } = 24;
public double BottomCenter { get; set; } = 24;
}
public class ICCFloatingBarConfiguration
{
public bool SemiTransparent { get; set; } = false;
public bool NearSnap { get; set; } = true;
public InitialPositionTypes InitialPosition { get; set; } = InitialPositionTypes.BottomCenter;
public Point InitialPositionPoint { get; set; } = new Point(0, 0);
public double ElementCornerRadiusValue = 0;
public ElementCornerRadiusTypes ElementCornerRadiusType { get; set; } = ElementCornerRadiusTypes.SuperEllipse;
public bool ParallaxEffect { get; set; } = true;
public bool MiniMode { get; set; } = false;
public Color ClearButtonColor { get; set; } = Color.FromRgb(224, 27, 36);
public Color ClearButtonPressColor { get; set; } = Color.FromRgb(254, 226, 226);
public Color ToolButtonSelectedBgColor { get; set; } = Color.FromRgb(37, 99, 235);
public double MovingLimitationNoSnap { get; set; } = 12;
public double MovingLimitationSnapped { get; set; } = 24;
public NearSnapAreaSize NearSnapAreaSize { get; set; } = new NearSnapAreaSize()
{
TopLeft = new double[] { 24, 24 },
TopRight = new double[] { 24, 24 },
BottomLeft = new double[] { 24, 24 },
BottomRight = new double[] { 24, 24 },
};
public string[] ToolBarItemsInCursorMode { get; set; } = new string[] {
"Cursor", "Pen", "Clear", "Separator", "Whiteboard", "Gesture", "Menu", "Fold"
};
public string[] ToolBarItemsInMiniMode { get; set; } = new string[] {
"Cursor", "Pen", "Clear"
};
public string[] ToolBarItemsInAnnotationMode { get; set; } = new string[] {
"Cursor", "Pen", "Clear", "Separator", "Eraser", "ShapeDrawing", "Select", "Separator", "Undo", "Redo", "Separator", "Whiteboard", "Gesture", "Menu", "Fold"
};
}
public class ICCConfiguration
{
public ICCFloatingBarConfiguration FloatingBar { get; set; } = new ICCFloatingBarConfiguration();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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