Compare commits
545 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 664ec925e9 | |||
| 3806454b18 | |||
| bea92f3483 | |||
| 133542d7fa | |||
| 7a363f7f79 | |||
| 2322efcc00 | |||
| 3cc830b37b | |||
| 7a1a9be3ab | |||
| 082c9ed005 | |||
| dfd327d7fc | |||
| 3354850216 | |||
| d330d0bf26 | |||
| 13b9b597ce | |||
| 96491f3bf5 | |||
| 4f99f11a33 | |||
| 134c90508d | |||
| 863829bb41 | |||
| 9802ab92e0 | |||
| b7eebeecb4 | |||
| a406778d35 | |||
| c9cef8ca86 | |||
| c099870352 | |||
| ad921dcfe7 | |||
| 0bdf8ea81b | |||
| 079c3cf1a6 | |||
| e29b4f3ff3 | |||
| b67476ae19 | |||
| 2e4b841e7e | |||
| 1dd49c6d93 | |||
| a948c0d7fb | |||
| 62e79ff5b3 | |||
| 6300a06a44 | |||
| a80c4e709f | |||
| 08f7afc3f1 | |||
| 1e65cee4ad | |||
| bbe262df7e | |||
| 7d0de8b5a3 | |||
| 244da46ac2 | |||
| bd6a4bf298 | |||
| ef6f3adc3f | |||
| 2047fb3a3a | |||
| 57594d7e4d | |||
| ba60aa6eb0 | |||
| 11c7c0f83f | |||
| 1108433b99 | |||
| 3ac4802d44 | |||
| 884d015b88 | |||
| 69ed0fc1ec | |||
| 4a9d492571 | |||
| 8a9a4c8b0f | |||
| 2ce6088e74 | |||
| 0ad74d9f7f | |||
| c76021194a | |||
| 5c3056aea0 | |||
| 7deb5b1aae | |||
| a7bd58a6bd | |||
| f690612fae | |||
| 59d1c4b4ab | |||
| ccd9398a53 | |||
| 1db9ed8169 | |||
| 7fbef51237 | |||
| efe3308325 | |||
| 4c874fa50b | |||
| 1704ad37d2 | |||
| a019c27c8b | |||
| 5119652ca5 | |||
| 1fb0ea29a3 | |||
| 1ca9fbd023 | |||
| 9eb70de1bc | |||
| fccfca8890 | |||
| 06e12e2899 | |||
| 559822686c | |||
| c46ac64a9c | |||
| ea7233bc1b | |||
| a6bc7552f4 | |||
| 7b04f18d4e | |||
| 99f0e25acf | |||
| 7b7ce4320b | |||
| 1023ef87f7 | |||
| 84365130df | |||
| 4097e6d20c | |||
| 940a8024ac | |||
| 15196b39c3 | |||
| 81864d4947 | |||
| 73a4a044ee | |||
| 8155aac25d | |||
| 6e8d1f5d9e | |||
| 30852376dc | |||
| ad986043aa | |||
| 17c020284d | |||
| 1d280a3a35 | |||
| 8394c6d6d6 | |||
| e2da207965 | |||
| 6f00a332c1 | |||
| 1c8bdcf352 | |||
| 31fe3c858e | |||
| c06be8f502 | |||
| 2d51eb73b5 | |||
| 30bc0b03e5 | |||
| dd9cb1825e | |||
| 6d5c4b4e08 | |||
| 3d0a960337 | |||
| 705f4fd155 | |||
| ac98a76797 | |||
| 13396f418b | |||
| e70696cd6f | |||
| d8bbee8c76 | |||
| 3e701718d3 | |||
| 656863a7d0 | |||
| 3e3db27296 | |||
| 86cdb231d0 | |||
| 5d6c9bb76b | |||
| d02f1e99c8 | |||
| 26b6de9149 | |||
| 2b3d1c11de | |||
| 51e3377b38 | |||
| 99ec2d7609 | |||
| 469dbd1497 | |||
| b1513ca587 | |||
| b1e384e52d | |||
| 14cb2e836b | |||
| 1c50edd8be | |||
| 87570e16ce | |||
| abf4bf0254 | |||
| 092fd1c3ee | |||
| c1105df271 | |||
| 8292b2ef25 | |||
| d032293a0d | |||
| d038beb07a | |||
| edac024ad9 | |||
| dc422a2ccc | |||
| bcb7002b22 | |||
| 1a585d353f | |||
| b16ec37df3 | |||
| d2abadd69b | |||
| 432cc3825e | |||
| 9ef764ffa1 | |||
| c2ca1c9702 | |||
| 07de74b775 | |||
| a7d0f022dc | |||
| 2e03cb1db1 | |||
| 7f82312b96 | |||
| 10ba2db0f1 | |||
| 8c99492518 | |||
| b5b2d97786 | |||
| 3d601189f1 | |||
| 681b674eb2 | |||
| c838b9d077 | |||
| 703e710006 | |||
| 3ab1c59a05 | |||
| 45dc3cb537 | |||
| 2f8c368eef | |||
| 78302ca426 | |||
| 0675a369c3 | |||
| 85f92ca9a5 | |||
| f252eb068a | |||
| 8c65075823 | |||
| 7d78f677b8 | |||
| 41e9e9cc04 | |||
| fefd8424b7 | |||
| 60bdc64730 | |||
| 7b6b2a30e6 | |||
| 9b55eff11d | |||
| 07a03d3e60 | |||
| cf3063f3e4 | |||
| 1d46818eec | |||
| ec3a826e01 | |||
| 97d5b12732 | |||
| e0cb877162 | |||
| 698478d826 | |||
| f722f3516b | |||
| 27a9f965a9 | |||
| a95effabdd | |||
| 08eb9d892d | |||
| d7530d0c4c | |||
| 967243e705 | |||
| 47eac7e70e | |||
| f35075de9d | |||
| b983cbe094 | |||
| 3ea6da4790 | |||
| b658bf4fd0 | |||
| 4aa12638f1 | |||
| 54e9e3b10d | |||
| 12a13bb97b | |||
| 8b35926b2d | |||
| 1d71f809cc | |||
| a9fefadec9 | |||
| bdf4b7e495 | |||
| 12c8384347 | |||
| 272782ba4c | |||
| a75d7876e2 | |||
| 890f3e890c | |||
| 1c2e513c34 | |||
| ed239929f3 | |||
| 2d65224389 | |||
| 418810d542 | |||
| 4a2d015b97 | |||
| 15cb1aa5f2 | |||
| 7226187ac6 | |||
| 6de2e49047 | |||
| f0b31c15a6 | |||
| 80decf5656 | |||
| 376790330d | |||
| 200317a0f5 | |||
| 92a405f833 | |||
| 2a3e759890 | |||
| 46e0b04dd1 | |||
| 88a8d8fa4b | |||
| bf5c4ab188 | |||
| 14d314e0a2 | |||
| 324d514f10 | |||
| e11e1989ad | |||
| a5b99c25ed | |||
| 3c908feb95 | |||
| 840eca88c8 | |||
| fa7caf3592 | |||
| 654e15f845 | |||
| 7a81344aea | |||
| 803d267687 | |||
| c8e4d18364 | |||
| aeecca1260 | |||
| 49aaa2d58b | |||
| dd22b119b7 | |||
| 42984ea5fa | |||
| ce0147091a | |||
| 884abf5b0e | |||
| 5cc1c7093a | |||
| 87aae93f4b | |||
| 77002b59b2 | |||
| 5601f72d59 | |||
| afcc8050ce | |||
| 17c043668c | |||
| 2495fc7b26 | |||
| 61404ff852 | |||
| a62ae3c6e0 | |||
| 28f65b3790 | |||
| ce4b83dbe0 | |||
| 7a24faece1 | |||
| 22555b835b | |||
| b2648fecac | |||
| 15a6799d7b | |||
| 13b24aeb7c | |||
| 674ce28420 | |||
| 60c07c3738 | |||
| 2b7f3c1f73 | |||
| 04ff617e3c | |||
| 5ee247d423 | |||
| 0bd2c4eff7 | |||
| 0408729a1d | |||
| 61e9a456af | |||
| 93f89e7a41 | |||
| bb26c9ce55 | |||
| bd2107378b | |||
| 0b0714c166 | |||
| 696ed84f4c | |||
| c8a467af9d | |||
| 8f6383cbe6 | |||
| 25e64dd6f8 | |||
| 745bc65379 | |||
| a4868215f2 | |||
| 8e2d8045c0 | |||
| 5b290ef5c0 | |||
| b68a50431f | |||
| 4f8c7d66e1 | |||
| 48bf15457d | |||
| d85bea7872 | |||
| 02fe33da5a | |||
| 3f17cc705b | |||
| 8078d0f137 | |||
| 51ed642c35 | |||
| 842e1c0d6c | |||
| f76cfe648f | |||
| 1b66473ae0 | |||
| f0bae76b78 | |||
| ff58675069 | |||
| b3cb53b482 | |||
| 4eb9773398 | |||
| 748ab0fff2 | |||
| ae22162020 | |||
| 7390e59ab0 | |||
| 57e0331c58 | |||
| 862374bec4 | |||
| 266c5c0dc8 | |||
| 9fee9a1d6a | |||
| f65341d906 | |||
| c11ca7a3f7 | |||
| b7f3a38826 | |||
| 6ebe50e71b | |||
| 6ce2aea000 | |||
| 60c341927f | |||
| ed03d40ff3 | |||
| 6c616f2b6b | |||
| 84f026f1dd | |||
| 89c9c0d9ef | |||
| 9d631cc980 | |||
| 786d4fc719 | |||
| 9c68a5350b | |||
| e26bf83bb0 | |||
| 52583a6092 | |||
| de9056739b | |||
| 9c611698c7 | |||
| a6357abc15 | |||
| b83facb14e | |||
| 2dc16c13bc | |||
| a46f8b36a0 | |||
| c0453ea1fc | |||
| 41b8f9c962 | |||
| e69175e5c4 | |||
| 76babf4dd3 | |||
| 5d6f53dc58 | |||
| 72a49b7bf2 | |||
| c4230a15c9 | |||
| 84167bbcfc | |||
| 7f154ba1db | |||
| 7abb7e2ef1 | |||
| 04d21ac890 | |||
| a6992874f8 | |||
| 1b2db81dde | |||
| 17e6d23650 | |||
| 2aa1ba537f | |||
| 981ca7629e | |||
| 1aaef3d554 | |||
| 2fd83a4a80 | |||
| fc89dce7c2 | |||
| 62b85a4bbd | |||
| 927d85ea68 | |||
| 89ccf700c3 | |||
| b3c29f2e27 | |||
| 06af63a10a | |||
| 50742e5e4d | |||
| 570c701b93 | |||
| a1c4d53d7c | |||
| 3250b81a23 | |||
| fb914734c8 | |||
| be6eb73671 | |||
| 221a0f8e85 | |||
| d5e5ec8c46 | |||
| b10215aec9 | |||
| ea20f84d91 | |||
| 008e843b39 | |||
| 172dd4f81b | |||
| 4697fb4664 | |||
| c9e6ba972b | |||
| 758f414302 | |||
| 0fb5c04deb | |||
| 47885685fe | |||
| 8e87dddcd2 | |||
| 4649649cf3 | |||
| 68a74be279 | |||
| 483e0757b7 | |||
| 9abce33257 | |||
| 337d4d7288 | |||
| bde0680f81 | |||
| ec579288a8 | |||
| e2c222a156 | |||
| 23de7e3575 | |||
| 18a8f41bf3 | |||
| 63585911a7 | |||
| acafdcc991 | |||
| 9d14f16fe2 | |||
| 89eb93aa67 | |||
| fbfac18ca0 | |||
| 78b2f94bae | |||
| 89f0a401ef | |||
| 0776071454 | |||
| 56fb29fe15 | |||
| d7ed3884d6 | |||
| ba673ccf41 | |||
| 44c1071d49 | |||
| 7789240f64 | |||
| 7eee02dc94 | |||
| 965957aa1b | |||
| 65a3917f62 | |||
| 6ba4a57bbc | |||
| 4e13817509 | |||
| a8330fa2e9 | |||
| be0d444db9 | |||
| ecf3c1ad04 | |||
| 6bf439f493 | |||
| 94b52941af | |||
| a80fb33880 | |||
| 30e4c35165 | |||
| 9bd1214567 | |||
| c28a2bd792 | |||
| 45a7e586c7 | |||
| 5903bab81f | |||
| 295f8f56e3 | |||
| 4cc8af7ff0 | |||
| bca2dde497 | |||
| a8a72159f0 | |||
| 8ea7cb6ac2 | |||
| 2b721b111b | |||
| d7e080a579 | |||
| 6429c242e4 | |||
| aa7b593b65 | |||
| fabac7e2bb | |||
| 9b5ee56a09 | |||
| c78d3d74c2 | |||
| b1459901d0 | |||
| aba56ac340 | |||
| 3d4e1872f1 | |||
| 7a4d33b7da | |||
| afb65eb908 | |||
| df5daeeb75 | |||
| 792779e5b2 | |||
| 3313a0a182 | |||
| 88dd53302f | |||
| e972adfbcb | |||
| ab6425e0d9 | |||
| a81ee5b3db | |||
| fae08a5285 | |||
| eeb4a25d7a | |||
| fdf07180dd | |||
| 6387a6fcda | |||
| a6e400629a | |||
| 8172b7c776 | |||
| ca4d2ac4a2 | |||
| abd9f850eb | |||
| 3aef6f5e05 | |||
| 2f957374e3 | |||
| 3f8cabc7e0 | |||
| 73d1dc8f48 | |||
| 2bfb78a257 | |||
| 783e5c43a5 | |||
| d7e8330016 | |||
| 80d3836e9e | |||
| c26d1c348e | |||
| 95c307cc0b | |||
| 6e7299e445 | |||
| 01fa047591 | |||
| 8c741c1fb7 | |||
| b0bfc8d5ed | |||
| ad8d8f94ff | |||
| c99bd2bb63 | |||
| 468df7dad7 | |||
| 3c2e4a0990 | |||
| 87b717f6a9 | |||
| 51dc45988e | |||
| 2f8b986f1f | |||
| 7f01e7acb6 | |||
| d011d2ba8a | |||
| a922654c17 | |||
| e70a486362 | |||
| 1683bc8418 | |||
| b6368fb0e4 | |||
| ddfa9c2676 | |||
| f3ddd5a11a | |||
| 39e8b2359e | |||
| 4fb73c155b | |||
| 3287603d0a | |||
| 1abb317054 | |||
| 719e37c26b | |||
| 45d2f99fd7 | |||
| f75fdded98 | |||
| 22a6d87771 | |||
| 2479da4bbc | |||
| c22424d798 | |||
| bbb30b7c25 | |||
| 71cbe12cee | |||
| 011effa047 | |||
| b1648dd702 | |||
| 81621cb9d0 | |||
| 3f460d7a5c | |||
| 6fe34c1250 | |||
| d5cac938c2 | |||
| 6222fabdd4 | |||
| 8ed4f25499 | |||
| 3afd4641cd | |||
| e9fde97453 | |||
| 441f8b6e26 | |||
| 8042b917a0 | |||
| 343e7281fe | |||
| c64e6a4554 | |||
| 8190bf275c | |||
| e792f2637d | |||
| 3cd26323dc | |||
| 40e1c4d467 | |||
| 86c22d373a | |||
| cb7a76efc5 | |||
| 545425c4d3 | |||
| eb1aaa10e4 | |||
| daf0db312b | |||
| 8b2bc2f064 | |||
| 2e343cbbf9 | |||
| a75f0470bc | |||
| 287d31a3a9 | |||
| 430fff0515 | |||
| fbbb7b8ad7 | |||
| 54b74d7411 | |||
| 82dba31b2a | |||
| 38d7e782e0 | |||
| 05e5ceeb43 | |||
| fcbbad71d2 | |||
| 6f9161439f | |||
| aa0c4fb841 | |||
| cff50d1f81 | |||
| b8581b6368 | |||
| 094f1223d1 | |||
| 6802476afa | |||
| a0539dce9b | |||
| bf2b8fec35 | |||
| 082c9a03ec | |||
| 4ccdd862ba | |||
| e5a20ed0fc | |||
| a72022704e | |||
| 2acc7ada30 | |||
| e61882c331 | |||
| 61c145689a | |||
| 40ea9664a7 | |||
| bccd2d0f3e | |||
| b918809dca | |||
| e7c2e92879 | |||
| cf03c921a7 | |||
| 2c45c839b1 | |||
| 83f5fc58d1 | |||
| 1a267f1e5a | |||
| c3fd5551d8 | |||
| 1baa74bb69 | |||
| 261ecefb17 | |||
| d81d8f7c5d | |||
| de1af12157 | |||
| 803cbbdee9 | |||
| 008477d5fa | |||
| 7f0d29ebd2 | |||
| 11bf8cffb2 | |||
| 87b9ebc7e1 | |||
| b89d27411b | |||
| 24c37f1d3e | |||
| 58b0a0a3be | |||
| ed58873a82 | |||
| a8dcbd4af0 | |||
| 4b2f29442a | |||
| dfab0d7ddf | |||
| ce1998b701 | |||
| 92c631d6ce | |||
| 01009f9e35 | |||
| 74eca093da | |||
| e7d89e65b2 | |||
| 24b2bffe8e | |||
| d2906476c8 | |||
| 2b31a355ae | |||
| b602048186 | |||
| 4fb7031060 | |||
| 4ef77c2e72 | |||
| 72ba1a9f58 |
+44
-2
@@ -16,7 +16,8 @@
|
||||
"contributions": [
|
||||
"maintenance",
|
||||
"doc",
|
||||
"code"
|
||||
"code",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -36,7 +37,10 @@
|
||||
"contributions": [
|
||||
"blog",
|
||||
"doc",
|
||||
"design"
|
||||
"design",
|
||||
"test",
|
||||
"tutorial",
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -98,6 +102,44 @@
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "doudou0720",
|
||||
"name": "doudou0720",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/98651603?v=4",
|
||||
"profile": "https://github.com/doudou0720",
|
||||
"contributions": [
|
||||
"code",
|
||||
"blog"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "PANDAJSR",
|
||||
"name": "PANDAJSR",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/170189561?v=4",
|
||||
"profile": "https://github.com/PANDAJSR",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "LiuYan-xwx",
|
||||
"name": "流焰xwx",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/66517348?v=4",
|
||||
"profile": "http://lyxwx.top",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Super-Yyt",
|
||||
"name": "Super-Yyt",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/206630707?v=4",
|
||||
"profile": "https://github.com/Super-Yyt",
|
||||
"contributions": [
|
||||
"infra",
|
||||
"blog"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet",
|
||||
"postCreateCommand": "dotnet restore",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-dotnettools.csharp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
name: Bug 报告 | Bug Report
|
||||
description: 反馈软件缺陷或异常 | Report a bug to help us improve
|
||||
labels: [bug]
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢你的反馈!请详细填写以下内容,便于我们定位问题。
|
||||
Thank you for your feedback! Please fill out the following information to help us locate the issue.
|
||||
|
||||
在报告问题之前,请确保你的软件已经更新到最新Beta版本,否则我们可能会无条件直接关闭该Issue,感谢配合!
|
||||
Before reporting the issue, please make sure your software has been updated to the latest Beta version. Otherwise, we may unconditionally close this Issue without any further notice. Thank you for your cooperation!
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
@@ -33,7 +36,7 @@ body:
|
||||
id: steps
|
||||
attributes:
|
||||
label: 复现步骤 | Steps to Reproduce
|
||||
description: 如何复现该问题?如有必要可附截图/录屏 | How to reproduce this bug? Screenshots/recordings if needed
|
||||
description: 如何复现该问题?如有必要可附截图/录屏/触发该问题的文件 | How to reproduce this bug? Screenshots/recordings/specific files if needed
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
@@ -51,6 +54,13 @@ body:
|
||||
id: extra
|
||||
attributes:
|
||||
label: 其他补充信息 | Additional Info
|
||||
description: 其他相关信息(如日志、配置、特殊环境等)| Any other context, logs, configs, special environment, etc.
|
||||
description: 其他相关信息(如日志文件、崩溃日志文件、配置文件、特殊环境等)| Any other context, logs, crash logs, configs, special environment, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: upload
|
||||
id: files
|
||||
attributes:
|
||||
label: 上传有关文件 | Upload relevant files
|
||||
description: "你可以在此处上传相关文件 | You can upload relevant files here."
|
||||
validations:
|
||||
required: false
|
||||
+8
-1
@@ -1,6 +1,6 @@
|
||||
name: 功能请求 | Feature Request
|
||||
description: 提出你对本项目的功能建议 | Suggest an idea for this project
|
||||
labels: [enhancement]
|
||||
type: Feature
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -35,3 +35,10 @@ body:
|
||||
description: 其他补充说明或建议 | Any other context or suggestions
|
||||
validations:
|
||||
required: false
|
||||
- type: upload
|
||||
id: files
|
||||
attributes:
|
||||
label: 上传有关文件 | Upload relevant files
|
||||
description: "你可以在此处上传相关文件 | You can upload relevant files here."
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: (Markdown Template) Bug 报告 | Bug Report
|
||||
about: 反馈软件缺陷或异常 | Report a bug to help us improve
|
||||
title: "[Version x.x.x] <your title>"
|
||||
type: Bug
|
||||
---
|
||||
|
||||
<!---请注意,你正在使用Markdown格式的Issue模板,如果你删除该模板的框架、更改问题的tag/类型/受理人或者不按照规范填写,你的Issue可能被直接关闭,如果你对Markdown不熟悉,请使用位于该选项下方的反馈入口继续反馈,感谢配合!-->
|
||||
|
||||
<!---感谢你的反馈!请详细填写以下内容,便于我们定位问题。-->
|
||||
<!---Thank you for your feedback! Please fill out the following information to help us locate the issue.-->
|
||||
|
||||
<!---在报告问题之前,请确保你的软件已经更新到最新Beta版本,否则我们可能会无条件直接关闭该Issue,感谢配合!-->
|
||||
<!---Before reporting the issue, please make sure your software has been updated to the latest Beta version. Otherwise, we may unconditionally close this Issue without any further notice. Thank you for your cooperation!-->
|
||||
|
||||
### 软件版本 | App Version (必填 | Required)
|
||||
<!---可在设置中的"关于"界面查看 | You can find it on the "About" interface in the settings-->
|
||||
<!---例如 v1.2.3 | e.g. v1.2.3-->
|
||||
|
||||
### 操作系统及版本 | OS & Version (必填 | Required)
|
||||
<!---例如 Windows 10 22H2 64位 | e.g. Windows 10 22H2 64bit-->
|
||||
|
||||
### 问题描述 | Description (必填 | Required)
|
||||
<!---简要描述遇到的问题 | Briefly describe the problem-->
|
||||
|
||||
### 复现步骤 | Steps to Reproduce (必填 | Required)
|
||||
<!---如何复现该问题?如有必要可附截图/录屏/触发该问题的文件 | How to reproduce this bug? Screenshots/recordings/specific files if needed-->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
### 期望结果 | Expected Behavior (必填 | Required)
|
||||
<!---你期望的正确行为或结果 | What did you expect to happen?-->
|
||||
|
||||
### 其他补充信息 | Additional Info (可选 | Optional)
|
||||
<!---其他相关信息(如日志文件、崩溃日志文件、配置文件、特殊环境等)| Any other context, logs, crash logs, configs, special environment, etc.-->
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: (Markdown Template) 功能请求 | Feature Request
|
||||
about: 提出你对本项目的功能建议 | Suggest an idea for this project
|
||||
title: "[Feature Request] "
|
||||
type: Feature
|
||||
---
|
||||
|
||||
<!---请注意,你正在使用Markdown格式的Issue模板,如果你删除该模板的框架、更改问题的tag/类型/受理人或者不按照规范填写,你的Issue可能被直接关闭,如果你对Markdown不熟悉,请使用位于该选项下方的反馈入口继续反馈,感谢配合!-->
|
||||
|
||||
<!---感谢你的建议!请详细描述你的需求。-->
|
||||
<!---Thank you for your suggestion! Please describe your needs in detail.-->
|
||||
|
||||
### 功能描述 | Description (必填 | Required)
|
||||
<!---请描述你希望添加的功能 | Describe the feature you want-->
|
||||
|
||||
### 需求动机 | Motivation (必填 | Required)
|
||||
<!---为什么需要这个功能?| Why do you need this feature?-->
|
||||
|
||||
### 期望设计 | Expected Design (可选 | Optional)
|
||||
<!---描述或画出你期望的界面或交互 | Describe or sketch the expected UI/UX-->
|
||||
|
||||
### 其他补充信息 | Additional Info (可选 | Optional)
|
||||
<!---其他补充说明或建议 | Any other context or suggestions-->
|
||||
@@ -1,35 +1,105 @@
|
||||
name: .NET Build
|
||||
name: .NET Build & Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main,beta ]
|
||||
branches: [ main, beta ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
types: [opened, synchronize, reopened ]
|
||||
branches: [ main, beta ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.head_ref || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
build-and-package:
|
||||
name: Build & Package
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup MSbuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Setup NuGet
|
||||
uses: NuGet/setup-nuget@v2.0.1
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Restore NuGet Packages
|
||||
run: nuget restore "Ink Canvas.sln"
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
cache: true
|
||||
cache-dependency-path: '**/packages.lock.json'
|
||||
|
||||
- name: Restore Package
|
||||
run: dotnet restore "Ink Canvas.sln" --locked-mode
|
||||
|
||||
- name: Build the Solution
|
||||
env:
|
||||
DLASS_SENTRY_DSN: ${{ secrets.DLASS_SENTRY_DSN }}
|
||||
run: msbuild /p:platform="AnyCPU" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
|
||||
- name: Build the Solution
|
||||
run: |
|
||||
msbuild -t:restore /p:GitFlow="Github Action"
|
||||
msbuild /p:platform="AnyCPU" /p:configuration="Debug" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj"
|
||||
- name: Check if exe file is generated
|
||||
id: check-exe
|
||||
run: |
|
||||
$exePath = "Ink Canvas\bin\Debug\net472\InkCanvasForClass.exe"
|
||||
|
||||
if (Test-Path $exePath) {
|
||||
echo "build_success=true" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "build_success=false" >> $env:GITHUB_OUTPUT
|
||||
|
||||
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
- name: Upload to artifact
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: InkCanvasForClass
|
||||
path: "Ink Canvas/bin/Debug/net472"
|
||||
- name: Create Package (if build succeeded)
|
||||
id: create-archive
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
env:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||
run: |
|
||||
$shortSha = $env:GITHUB_SHA.Substring(0, 7)
|
||||
$version = "debug-$shortSha-$env:GITHUB_RUN_NUMBER"
|
||||
echo "archive_name=$version" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Artifact (if build succeeded)
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: InkCanvasForClass.CE.debug
|
||||
path: "Ink Canvas/bin/Debug/net472/*"
|
||||
|
||||
- name: Create Summary
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo "# Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then
|
||||
echo "## ✅ Build Successful" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Download Artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.zip)\)" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "## ❌ Build Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
+614
-270
@@ -1,6 +1,9 @@
|
||||
name: Pre-release and Changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_type:
|
||||
@@ -12,291 +15,632 @@ on:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- build
|
||||
prerelease:
|
||||
description: 'Create as pre-release'
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
draft:
|
||||
description: 'Create as draft release'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prerelease:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
prepare:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
tag_name: ${{ steps.get_tag.outputs.tag_name }}
|
||||
version: ${{ steps.get_tag.outputs.version }}
|
||||
is_prerelease: ${{ steps.release_type.outputs.is_prerelease }}
|
||||
changelog: ${{ steps.read_changelog.outputs.changelog }}
|
||||
|
||||
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"
|
||||
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('.')
|
||||
|
||||
# 确保版本号格式正确(至少3部分)
|
||||
if ($versionParts.Length -ge 3) {
|
||||
$major = [int]$versionParts[0]
|
||||
$minor = [int]$versionParts[1]
|
||||
$patch = [int]$versionParts[2]
|
||||
} else {
|
||||
# 如果版本号格式不正确,使用默认值
|
||||
$major = 1
|
||||
$minor = 0
|
||||
$patch = 0
|
||||
}
|
||||
|
||||
$versionType = "${{ github.event.inputs.version_type }}"
|
||||
|
||||
switch ($versionType) {
|
||||
"major" {
|
||||
$major++
|
||||
$minor = 0
|
||||
$patch = 0
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
# ========== 获取当前版本 ==========
|
||||
- name: Get current version from Git tag
|
||||
id: get_version
|
||||
run: |
|
||||
# 获取最新的tag
|
||||
$latestTag = git describe --tags --abbrev=0 2>$null
|
||||
if ($latestTag) {
|
||||
$version = $latestTag
|
||||
echo "Found latest tag: $latestTag"
|
||||
} else {
|
||||
# 如果没有tag,使用默认值
|
||||
$version = "1.0.0.0"
|
||||
echo "No tag found, using default version: $version"
|
||||
}
|
||||
"minor" {
|
||||
$minor++
|
||||
$patch = 0
|
||||
}
|
||||
"patch" {
|
||||
$patch++
|
||||
}
|
||||
}
|
||||
|
||||
# 生成新版本号(保持3位格式,如1.7.13)
|
||||
$newVersion = "$major.$minor.$patch"
|
||||
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]
|
||||
echo "current_version=$version" >> $env:GITHUB_OUTPUT
|
||||
echo "Current version: $version"
|
||||
|
||||
# ========== 处理版本号和标签名 ==========
|
||||
- name: Get tag name and version
|
||||
id: get_tag
|
||||
run: |
|
||||
if ("${{ github.event_name }}" -eq "push") {
|
||||
# 从 push tag 事件获取原始标签名
|
||||
$tagName = "${{ github.ref }}".Replace("refs/tags/", "")
|
||||
$cleanVersion = $tagName
|
||||
|
||||
$commitInfo = @{
|
||||
Hash = $hash
|
||||
Message = $message
|
||||
Author = $author
|
||||
Date = $date
|
||||
}
|
||||
echo "tag_name=$tagName" >> $env:GITHUB_OUTPUT
|
||||
echo "version=$cleanVersion" >> $env:GITHUB_OUTPUT
|
||||
echo "Using pushed tag: $tagName, version: $cleanVersion"
|
||||
} else {
|
||||
# 从 workflow_dispatch 计算新版本(4位格式)
|
||||
$currentVersion = "${{ steps.get_version.outputs.current_version }}"
|
||||
$versionParts = $currentVersion.Split('.')
|
||||
|
||||
# 根据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
|
||||
# 确保版本号格式正确(至少4部分)
|
||||
if ($versionParts.Length -ge 4) {
|
||||
$major = [int]$versionParts[0]
|
||||
$minor = [int]$versionParts[1]
|
||||
$patch = [int]$versionParts[2]
|
||||
$build = [int]$versionParts[3]
|
||||
} else {
|
||||
$others += $commitInfo
|
||||
# 如果版本号格式不正确,补充为4位
|
||||
if ($versionParts.Length -ge 3) {
|
||||
$major = [int]$versionParts[0]
|
||||
$minor = [int]$versionParts[1]
|
||||
$patch = [int]$versionParts[2]
|
||||
$build = 0
|
||||
} else {
|
||||
# 如果版本号格式不正确,抛出错误
|
||||
echo "Error: Invalid version format. Expected format: x.y.z.w (e.g., 1.7.18.0)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$versionType = "${{ github.event.inputs.version_type }}"
|
||||
$isPrerelease = "${{ github.event.inputs.prerelease }}" -eq "true"
|
||||
|
||||
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"
|
||||
|
||||
# 根据是否为预发布决定版本号最后一位
|
||||
# 如果是预发布,确保最后一位不为0(使用1)
|
||||
if ($isPrerelease -and $build -eq 0) {
|
||||
$build = 1
|
||||
$newVersion = "$major.$minor.$patch.$build"
|
||||
}
|
||||
$tagName = $newVersion
|
||||
|
||||
echo "tag_name=$tagName" >> $env:GITHUB_OUTPUT
|
||||
echo "version=$newVersion" >> $env:GITHUB_OUTPUT
|
||||
echo "New tag: $tagName, version: $newVersion"
|
||||
}
|
||||
}
|
||||
|
||||
# 生成changelog内容
|
||||
$version = "${{ steps.calc_version.outputs.new_version }}"
|
||||
$changelog = "# ICC CE $version.0 更新日志`n`n## 修复 (Fixes)"
|
||||
|
||||
if ($fixes.Count -gt 0) {
|
||||
foreach ($fix in $fixes) {
|
||||
$changelog += "`n- $($fix.Message) ($($fix.Author), $($fix.Date))"
|
||||
|
||||
- name: Determine release type
|
||||
id: release_type
|
||||
run: |
|
||||
if ("${{ github.event_name }}" -eq "push") {
|
||||
# 根据版本号最后一位确定是否为预发布版本
|
||||
# 最后一位为0表示正式版本,非0表示预发布版本
|
||||
$version = "${{ steps.get_tag.outputs.version }}"
|
||||
$versionParts = $version.Split('.')
|
||||
if ($versionParts.Length -ge 4) {
|
||||
$build = [int]$versionParts[3]
|
||||
if ($build -eq 0) {
|
||||
echo "is_prerelease=false" >> $env:GITHUB_OUTPUT
|
||||
echo "This is a release"
|
||||
} else {
|
||||
echo "is_prerelease=true" >> $env:GITHUB_OUTPUT
|
||||
echo "This is a pre-release (beta)"
|
||||
}
|
||||
} else {
|
||||
echo "is_prerelease=false" >> $env:GITHUB_OUTPUT
|
||||
echo "This is a release (invalid version format)"
|
||||
}
|
||||
} else {
|
||||
# workflow_dispatch 方式
|
||||
echo "is_prerelease=${{ github.event.inputs.prerelease }}" >> $env:GITHUB_OUTPUT
|
||||
}
|
||||
} 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.0.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 }}.0.zip
|
||||
CHANGELOG_${{ steps.calc_version.outputs.new_version }}.md
|
||||
|
||||
# ========== 使用 git-cliff 生成变更日志 ==========
|
||||
- name: Generate changelog with git-cliff (for pushed tag)
|
||||
if: github.event_name == 'push'
|
||||
id: git_cliff_tag
|
||||
uses: orhun/git-cliff-action@v4
|
||||
with:
|
||||
config: build/cliff.toml # 使用项目build目录的 cliff.toml 配置
|
||||
args: --latest --tag ${{ steps.get_tag.outputs.tag_name }} --output CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate changelog with git-cliff (for workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
id: git_cliff_unreleased
|
||||
uses: orhun/git-cliff-action@v4
|
||||
with:
|
||||
config: build/cliff.toml
|
||||
args: --unreleased --tag ${{ steps.get_tag.outputs.tag_name }} --output CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Read changelog content
|
||||
id: read_changelog
|
||||
run: |
|
||||
$changelogContent = Get-Content -Path CHANGELOG.md -Raw
|
||||
echo "changelog<<EOF" >> $env:GITHUB_OUTPUT
|
||||
echo $changelogContent >> $env:GITHUB_OUTPUT
|
||||
echo "EOF" >> $env:GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
if: success()
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
archive_name: ${{ steps.create_archive.outputs.archive_name }}
|
||||
zip_size: ${{ steps.calculate_size.outputs.zip_size }}
|
||||
installer_size: ${{ steps.calculate_installer_size.outputs.installer_size }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
cache: true
|
||||
cache-dependency-path: '**/packages.lock.json'
|
||||
|
||||
- name: Restore Package
|
||||
run: dotnet restore "Ink Canvas.sln" --locked-mode
|
||||
|
||||
- name: Build the Solution (Release)
|
||||
env:
|
||||
DLASS_SENTRY_DSN: ${{ secrets.DLASS_SENTRY_DSN }}
|
||||
run: |
|
||||
msbuild /p:platform="AnyCPU" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
|
||||
- name: Check if exe file is generated
|
||||
id: check-exe
|
||||
run: |
|
||||
$exePath = "Ink Canvas/bin/Release/net472/InkCanvasForClass.exe"
|
||||
|
||||
- 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 }}.0
|
||||
name: ICC CE ${{ steps.calc_version.outputs.new_version }}.0
|
||||
body: |
|
||||
${{ steps.changelog.outputs.changelog }}
|
||||
draft: false
|
||||
prerelease: ${{ github.event.inputs.prerelease }}
|
||||
files: |
|
||||
InkCanvasForClass.CE.${{ steps.calc_version.outputs.new_version }}.0.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.0 更新日志`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"
|
||||
if (Test-Path $exePath) {
|
||||
echo "build_success=true" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "build_success=false" >> $env:GITHUB_OUTPUT
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Install Inno Setup Unofficial Language Files
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
# 创建临时目录用于下载文件
|
||||
New-Item -ItemType Directory -Path "temp_lang" -Force
|
||||
|
||||
# 下载英语英国版语言文件
|
||||
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/refs/heads/main/Files/Languages/Unofficial/EnglishBritish.isl" -OutFile "temp_lang/EnglishBritish.isl"
|
||||
|
||||
# 下载简体中文版语言文件
|
||||
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/refs/heads/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "temp_lang/ChineseSimplified.isl"
|
||||
|
||||
# 将文件移动到 Inno Setup 的语言目录
|
||||
Move-Item -Path "temp_lang/EnglishBritish.isl" -Destination "C:/Program Files (x86)/Inno Setup 6/Languages/EnglishBritish.isl" -Force
|
||||
Move-Item -Path "temp_lang/ChineseSimplified.isl" -Destination "C:/Program Files (x86)/Inno Setup 6/Languages/ChineseSimplified.isl" -Force
|
||||
|
||||
# 清理临时目录
|
||||
Remove-Item -Path "temp_lang" -Recurse -Force
|
||||
|
||||
- name: Create Release Archive
|
||||
id: create_archive
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.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: Prepare Inno Setup script
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
|
||||
# 更新 ISS 文件中的版本信息
|
||||
$issPath = "build/InkCanvasForClass CE.iss"
|
||||
$issContent = Get-Content -Path $issPath -Raw
|
||||
|
||||
# 替换版本信息
|
||||
$issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`""
|
||||
|
||||
# 替换源文件路径为相对路径(考虑到ISS文件在build目录下,需要返回上级目录)
|
||||
$issContent = $issContent -replace 'Source: ".*\\{#MyAppExeName}";', 'Source: "../release/{#MyAppExeName}";'
|
||||
$issContent = $issContent -replace 'Source: ".*\\InkCanvasForClass.exe.config";', 'Source: "../release/InkCanvasForClass.exe.config";'
|
||||
|
||||
# 更新输出目录为当前目录
|
||||
$issContent = $issContent -replace 'OutputDir=.*', 'OutputDir=.'
|
||||
|
||||
# 更新默认安装目录
|
||||
$issContent = $issContent -replace 'DefaultDirName=.*', 'DefaultDirName={autopf}/{#MyAppName}'
|
||||
|
||||
# 更新许可证文件路径为相对路径(考虑到ISS文件在build目录下,需要返回上级目录)
|
||||
$issContent = $issContent -replace 'LicenseFile=.*', 'LicenseFile=../LICENSE'
|
||||
|
||||
# 保存修改后的 ISS 文件
|
||||
$issContent | Set-Content -Path $issPath -Encoding UTF8
|
||||
|
||||
- name: Build MSI installer with Inno Setup
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
uses: Minionguyjpro/Inno-Setup-Action@v1.2.7
|
||||
with:
|
||||
path: build/InkCanvasForClass CE.iss
|
||||
options: /O.
|
||||
|
||||
- name: Rename installer file
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$setupFile = "InkCanvasForClass CE Setup.exe"
|
||||
$newSetupName = "InkCanvasForClass.CE.$version.Setup.exe"
|
||||
|
||||
if (Test-Path $setupFile) {
|
||||
Rename-Item -Path $setupFile -NewName $newSetupName
|
||||
} else {
|
||||
Write-Error "Setup file not found: $setupFile"
|
||||
}
|
||||
|
||||
- name: Calculate archive size
|
||||
id: calculate_size
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$archiveName = "InkCanvasForClass.CE.$version.zip"
|
||||
|
||||
# 获取文件大小(字节)
|
||||
$fileSize = (Get-Item $archiveName).Length
|
||||
|
||||
echo "zip_size=$fileSize" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Calculate installer size
|
||||
id: calculate_installer_size
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$installerName = "InkCanvasForClass.CE.$version.Setup.exe"
|
||||
|
||||
if (Test-Path $installerName) {
|
||||
# 获取文件大小(字节)
|
||||
$fileSize = (Get-Item $installerName).Length
|
||||
|
||||
echo "installer_size=$fileSize" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
Write-Error "Installer file not found: $installerName"
|
||||
}
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
path: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
|
||||
- name: Create Build Summary
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo "# Release Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then
|
||||
echo "## ✅ Release Build Successful" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Release Type:** ${{ needs.prepare.outputs.is_prerelease == 'true' && 'Pre-release' || 'Release' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ -n "${{ steps.calculate_size.outputs.zip_size }}" ]; then
|
||||
echo "**Archive Size:** ${{ steps.calculate_size.outputs.zip_size }} bytes" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [ -n "${{ steps.calculate_installer_size.outputs.installer_size }}" ]; then
|
||||
echo "**Installer Size:** ${{ steps.calculate_installer_size.outputs.installer_size }} bytes" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
else
|
||||
echo "## ❌ Release Build Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
sign:
|
||||
needs: [prepare, build]
|
||||
if: success()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Sign release artifacts with sigstore-python
|
||||
uses: sigstore/gh-action-sigstore-python@v3.2.0
|
||||
with:
|
||||
inputs: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
release-signing-artifacts: true
|
||||
upload-signing-artifacts: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Signed Artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: signed-files-${{ needs.prepare.outputs.version }}
|
||||
path: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe.sigstore.json
|
||||
|
||||
release:
|
||||
needs: [prepare, build, sign]
|
||||
if: success()
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
enhanced_changelog: ${{steps.enhanced_changelog.outputs.enhanced_changelog}}
|
||||
|
||||
steps:
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Download Signed Artifacts (if exists)
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: signed-files-${{ needs.prepare.outputs.version }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create enhanced changelog with file table
|
||||
id: enhanced_changelog
|
||||
run: |
|
||||
version="${{ needs.prepare.outputs.version }}"
|
||||
|
||||
# 读取git-cliff生成的changelog内容
|
||||
originalChangelog="${{ needs.prepare.outputs.changelog }}"
|
||||
|
||||
# 检查是否为预发布版本,如果是则添加警告提示
|
||||
if [ "${{ needs.prepare.outputs.is_prerelease }}" = "true" ]; then
|
||||
warningText=$'\n> [!CAUTION]\n'
|
||||
warningText+=$'> **注意:此版本为预览或测试版**\n'
|
||||
warningText+=$'> \n'
|
||||
warningText+=$'> 请注意,这是一个预览/测试版本,使用时可能出现BUG,常规用户建议使用预览版或正式版\n\n'
|
||||
originalChangelog="${warningText}${originalChangelog}"
|
||||
fi
|
||||
|
||||
# 构建文件信息表格
|
||||
fileTable=$'\n## 文件信息 (File Information)\n'
|
||||
fileTable+=$'| 文件名 | 大小 |\n'
|
||||
fileTable+=$'|--------|------|\n'
|
||||
|
||||
# ZIP 文件信息
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"
|
||||
fileTable+=$'.zip | ${{ needs.build.outputs.zip_size }} bytes |\n'
|
||||
|
||||
# 安装包文件信息
|
||||
installerSize="${{ needs.build.outputs.installer_size }}"
|
||||
if [ -n "$installerSize" ]; then
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe | '"$installerSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
# 检查是否有签名文件
|
||||
if [ -f "InkCanvasForClass.CE.$version.zip.sigstore.json" ]; then
|
||||
sigstoreSize=$(stat -c%s "InkCanvasForClass.CE.$version.zip.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
# 检查安装程序签名文件
|
||||
if [ -f "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json" ]; then
|
||||
sigstoreSize=$(stat -c%s "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
fileTable+=$'\n*文件大小信息由GitHub Actions自动生成*\n'
|
||||
|
||||
# 将表格附加到原始changelog
|
||||
enhancedChangelog="${originalChangelog}${fileTable}"
|
||||
|
||||
# 输出增强版changelog内容
|
||||
echo "enhanced_changelog<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$enhancedChangelog" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Enhanced changelog created with file information table"
|
||||
|
||||
- name: Display Release Info
|
||||
run: |
|
||||
echo "=== Creating Release ==="
|
||||
echo "Version: ${{ needs.prepare.outputs.version }}"
|
||||
echo "Tag: ${{ needs.prepare.outputs.tag_name }}"
|
||||
echo "Pre-release: ${{ needs.prepare.outputs.is_prerelease }}"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.prepare.outputs.tag_name }}
|
||||
name: ICC CE ${{ needs.prepare.outputs.version }}
|
||||
body: |
|
||||
${{ steps.enhanced_changelog.outputs.enhanced_changelog }}
|
||||
draft: ${{ github.event.inputs.draft || false }}
|
||||
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
|
||||
files: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe.sigstore.json
|
||||
fail_on_unmatched_files: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
post_release:
|
||||
needs: [prepare, release]
|
||||
if: success()
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Get beta token
|
||||
uses: octo-sts/action@main
|
||||
id: octo-sts-beta
|
||||
with:
|
||||
scope: InkCanvasForClass/community-beta
|
||||
identity: repo-sync
|
||||
|
||||
- name: Get download token
|
||||
uses: octo-sts/action@main
|
||||
id: octo-sts-downloads
|
||||
with:
|
||||
scope: InkCanvasForClass/downloads
|
||||
identity: repo-sync
|
||||
|
||||
- name: Sync downloads repos(Universal)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.octo-sts-downloads.outputs.token }}
|
||||
run: |
|
||||
set -e
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
REPO_DIR=$(mktemp -d)
|
||||
git clone --depth 1 --filter=blob:none --branch main https://x-access-token:${{ steps.octo-sts-downloads.outputs.token }}@github.com/InkCanvasForClass/downloads.git $REPO_DIR
|
||||
|
||||
cd $REPO_DIR
|
||||
IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}"
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION.zip"
|
||||
|
||||
if [ "$IS_PRERELEASE" == "true" ]; then
|
||||
mkdir -p Beta
|
||||
cp "$ZIP_FILE" Beta/
|
||||
git add Beta/InkCanvasForClass.CE.$VERSION.zip
|
||||
git commit -m "Add $VERSION PreRelease"
|
||||
else
|
||||
mkdir -p Release Beta
|
||||
cp "$ZIP_FILE" Release/
|
||||
cp "$ZIP_FILE" Beta/
|
||||
git add Release/InkCanvasForClass.CE.$VERSION.zip Beta/InkCanvasForClass.CE.$VERSION.zip
|
||||
git commit -m "Add $VERSION Release"
|
||||
fi
|
||||
git push origin main
|
||||
|
||||
- name: Update AutomaticUpdateVersionControl in beta repo
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }}
|
||||
run: |
|
||||
CONTENT=$(echo -n "${{ needs.prepare.outputs.version }}" | base64 -w0)
|
||||
|
||||
SHA=$(gh api /repos/InkCanvasForClass/community-beta/contents/AutomaticUpdateVersionControl.txt --jq '.sha' 2>/dev/null || echo "")
|
||||
|
||||
gh api \
|
||||
--method PUT \
|
||||
/repos/InkCanvasForClass/community-beta/contents/AutomaticUpdateVersionControl.txt \
|
||||
-f message="Update AutomaticUpdateVersionControl.txt" \
|
||||
-f content="$CONTENT" \
|
||||
-f branch="main" \
|
||||
${SHA:+-f sha="$SHA"}
|
||||
|
||||
- name: Create GitHub Release on beta repo
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.prepare.outputs.tag_name }}
|
||||
name: ICC CE ${{ needs.prepare.outputs.version }}
|
||||
body: |
|
||||
${{ needs.release.outputs.enhanced_changelog }}
|
||||
draft: false
|
||||
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
|
||||
files: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
fail_on_unmatched_files: false
|
||||
repository: "InkCanvasForClass/community-beta"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }}
|
||||
|
||||
- name: Update community repo AutomaticUpdateVersionControl
|
||||
if: ${{needs.prepare.outputs.is_prerelease == 'false'}}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
CONTENT=$(echo -n "${{ needs.prepare.outputs.version }}" | base64 -w0)
|
||||
|
||||
SHA=$(gh api /repos/InkCanvasForClass/community/contents/AutomaticUpdateVersionControl.txt --jq '.sha' 2>/dev/null || echo "")
|
||||
|
||||
gh api \
|
||||
--method PUT \
|
||||
/repos/InkCanvasForClass/community/contents/AutomaticUpdateVersionControl.txt \
|
||||
-f message="Update AutomaticUpdateVersionControl.txt" \
|
||||
-f content="$CONTENT" \
|
||||
-f branch="beta" \
|
||||
${SHA:+-f sha="$SHA"}
|
||||
|
||||
+5
-1
@@ -425,4 +425,8 @@ FodyWeavers.xsd
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
*.msp
|
||||
|
||||
# Telemetry DSN configuration file (contains sensitive information)
|
||||
telemetry_dsn.txt
|
||||
**/telemetry_dsn.txt
|
||||
@@ -1 +1 @@
|
||||
1.7.17.0
|
||||
1.7.18.0
|
||||
|
||||
+25
-24
@@ -4,6 +4,7 @@
|
||||
xmlns:local="clr-namespace:Ink_Canvas"
|
||||
xmlns:tb="http://www.hardcodet.net/taskbar"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
|
||||
>
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
@@ -13,9 +14,9 @@
|
||||
<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">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock Name="HideICCMainWindowTrayIconMenuItemHeaderText" FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="隐藏ICC主窗口" />
|
||||
</ui:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="28" Height="28" Margin="-2">
|
||||
@@ -34,9 +35,9 @@
|
||||
<Separator Margin="0,3" />
|
||||
<MenuItem Name="DisableAllHotkeysMenuItem" Click="DisableAllHotkeysMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="禁用所有快捷键" />
|
||||
</ui:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="28" Height="28" Margin="-2">
|
||||
@@ -54,9 +55,9 @@
|
||||
</MenuItem>
|
||||
<MenuItem Name="ForceFullScreenTrayIconMenuItem" Click="ForceFullScreenTrayIconMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="强制全屏化" />
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
|
||||
</Border>
|
||||
@@ -64,8 +65,8 @@
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="F" />
|
||||
</Border>
|
||||
</ui:SimpleStackPanel>
|
||||
</ui:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="28" Height="28" Margin="-2">
|
||||
@@ -87,9 +88,9 @@
|
||||
</MenuItem>
|
||||
<MenuItem Name="FoldFloatingBarTrayIconMenuItem" Click="FoldFloatingBarTrayIconMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock Name="FoldFloatingBarTrayIconMenuItemHeaderText" FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="切换为收纳模式" />
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
|
||||
</Border>
|
||||
@@ -97,8 +98,8 @@
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="S" />
|
||||
</Border>
|
||||
</ui:SimpleStackPanel>
|
||||
</ui:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Grid>
|
||||
@@ -132,9 +133,9 @@
|
||||
</MenuItem>
|
||||
<MenuItem Name="ResetFloatingBarPositionTrayIconMenuItem" Click="ResetFloatingBarPositionTrayIconMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="重置工具栏位置" />
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
|
||||
</Border>
|
||||
@@ -142,8 +143,8 @@
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="T" />
|
||||
</Border>
|
||||
</ui:SimpleStackPanel>
|
||||
</ui:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="28" Height="28" Margin="-2">
|
||||
@@ -163,9 +164,9 @@
|
||||
<Separator Margin="0,3" />
|
||||
<MenuItem Name="RestartAppTrayIconMenuItem" Click="RestartAppTrayIconMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center" Foreground="#2563eb" Text="重启软件" />
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
|
||||
</Border>
|
||||
@@ -173,8 +174,8 @@
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="R" />
|
||||
</Border>
|
||||
</ui:SimpleStackPanel>
|
||||
</ui:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="24" Height="24">
|
||||
@@ -195,9 +196,9 @@
|
||||
</MenuItem>
|
||||
<MenuItem Name="CloseAppTrayIconMenuItem" Click="CloseAppTrayIconMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center" Foreground="#dc2626" Text="退出软件" />
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
|
||||
</Border>
|
||||
@@ -205,8 +206,8 @@
|
||||
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
|
||||
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="Q" />
|
||||
</Border>
|
||||
</ui:SimpleStackPanel>
|
||||
</ui:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="24" Height="24">
|
||||
|
||||
+388
-51
@@ -18,8 +18,10 @@ using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using Application = System.Windows.Application;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
using Ink_Canvas.Properties;
|
||||
using SplashScreen = Ink_Canvas.Windows.SplashScreen;
|
||||
using Timer = System.Threading.Timer;
|
||||
using Sentry;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -31,7 +33,7 @@ namespace Ink_Canvas
|
||||
Mutex mutex;
|
||||
|
||||
public static string[] StartArgs;
|
||||
public static string RootPath = Environment.GetEnvironmentVariable("APPDATA") + "\\Ink Canvas\\";
|
||||
public static string RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
|
||||
|
||||
// 新增:标记是否通过--board参数启动
|
||||
public static bool StartWithBoardMode = false;
|
||||
@@ -41,12 +43,16 @@ namespace Ink_Canvas
|
||||
public static Process watchdogProcess;
|
||||
// 新增:标记是否为软件内主动退出
|
||||
public static bool IsAppExitByUser;
|
||||
// 新增:标记是否正在触发安装更新(用于跳过某些交互确认)
|
||||
public static bool IsUpdateInstalling;
|
||||
// 新增:标记是否启用了UIA置顶功能
|
||||
public static bool IsUIAccessTopMostEnabled;
|
||||
// 新增:标记是否正在显示 OOBE(首次启动向导),看门狗在此期间不判定为卡死/假死
|
||||
public static bool IsOobeShowing;
|
||||
// 新增:退出信号文件路径
|
||||
private static string watchdogExitSignalFile = Path.Combine(Path.GetTempPath(), "icc_watchdog_exit_" + Process.GetCurrentProcess().Id + ".flag");
|
||||
// 新增:崩溃日志文件路径
|
||||
private static string crashLogFile = Path.Combine(Environment.GetEnvironmentVariable("APPDATA"), "Ink Canvas", "crash_logs");
|
||||
private static string crashLogFile = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "Crashes");
|
||||
// 新增:进程ID
|
||||
private static int currentProcessId = Process.GetCurrentProcess().Id;
|
||||
// 新增:应用启动时间
|
||||
@@ -59,8 +65,39 @@ namespace Ink_Canvas
|
||||
private static SplashScreen _splashScreen;
|
||||
private static bool _isSplashScreenShown = false;
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern int SetCurrentProcessExplicitAppUserModelID(string appId);
|
||||
|
||||
public App()
|
||||
{
|
||||
try
|
||||
{
|
||||
SetCurrentProcessExplicitAppUserModelID("InkCanvasForClass.CE");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var dsn = GetDlassTelemetryDsn();
|
||||
if (!string.IsNullOrWhiteSpace(dsn))
|
||||
{
|
||||
SentrySdk.Init(options =>
|
||||
{
|
||||
options.Dsn = dsn;
|
||||
options.Debug = false;
|
||||
options.SendDefaultPii = true;
|
||||
options.TracesSampleRate = 1.0;
|
||||
options.IsGlobalModeEnabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化 Dlass 遥测失败: {ex}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
// 配置TLS协议以支持Windows 7
|
||||
ConfigureTlsForWindows7();
|
||||
|
||||
@@ -107,7 +144,6 @@ namespace Ink_Canvas
|
||||
|
||||
if (isWindows7)
|
||||
{
|
||||
LogHelper.WriteLogToFile("检测到Windows 7系统,配置TLS协议支持");
|
||||
|
||||
// 启用所有TLS版本以支持Windows 7
|
||||
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
|
||||
@@ -117,17 +153,14 @@ namespace Ink_Canvas
|
||||
ServicePointManager.Expect100Continue = false;
|
||||
ServicePointManager.UseNagleAlgorithm = false;
|
||||
|
||||
LogHelper.WriteLogToFile("TLS协议配置完成,已启用TLS 1.2/1.1/1.0支持");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 对于更新的Windows版本,不进行任何TLS配置,使用系统默认设置
|
||||
LogHelper.WriteLogToFile($"检测到Windows版本: {osVersion.VersionString},使用系统默认TLS配置");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"配置TLS协议时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +379,25 @@ namespace Ink_Canvas
|
||||
try
|
||||
{
|
||||
var exception = e.ExceptionObject as Exception;
|
||||
|
||||
if (exception is InvalidOperationException invalidOpEx)
|
||||
{
|
||||
string exceptionMessage = invalidOpEx.Message ?? "";
|
||||
string exceptionStackTrace = invalidOpEx.StackTrace ?? "";
|
||||
|
||||
if (exceptionMessage.Contains("调用线程无法访问此对象") ||
|
||||
exceptionMessage.Contains("because another thread owns it") ||
|
||||
exceptionStackTrace.Contains("DynamicRenderer") ||
|
||||
exceptionStackTrace.Contains("CompositionTarget.get_RootVisual"))
|
||||
{
|
||||
LogHelper.WriteLogToFile(
|
||||
$"检测到DynamicRenderer线程访问异常: {invalidOpEx.Message}",
|
||||
LogHelper.LogType.Warning
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
string errorMessage = exception?.ToString() ?? "未知异常";
|
||||
lastErrorMessage = errorMessage;
|
||||
|
||||
@@ -361,12 +413,15 @@ namespace Ink_Canvas
|
||||
// 尝试在最后时刻记录错误
|
||||
try
|
||||
{
|
||||
string timeStr = (appStartTime != default(DateTime) && appStartTime != DateTime.MinValue)
|
||||
? appStartTime.ToString("yyyy-MM-dd-HH-mm-ss")
|
||||
: DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss");
|
||||
File.AppendAllText(
|
||||
Path.Combine(crashLogFile, $"critical_error_{DateTime.Now:yyyyMMdd_HHmmss}.log"),
|
||||
Path.Combine(crashLogFile, $"Crash_{timeStr}.txt"),
|
||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 记录未处理异常时发生错误: {ex.Message}\r\n"
|
||||
);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,6 +475,7 @@ namespace Ink_Canvas
|
||||
LogHelper.WriteLogToFile("启动画面对象创建成功,准备显示...");
|
||||
_splashScreen.Show();
|
||||
_isSplashScreenShown = true;
|
||||
splashScreenStartTime = DateTime.Now;
|
||||
LogHelper.WriteLogToFile("启动画面已显示");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -489,6 +545,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLaunchByFileOrUri(string[] args)
|
||||
{
|
||||
if (args == null || args.Length == 0) return false;
|
||||
foreach (string a in args)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(a)) continue;
|
||||
string t = a.Trim();
|
||||
if (t.StartsWith("icc:", StringComparison.OrdinalIgnoreCase)) return true;
|
||||
if (Path.GetExtension(t).Equals(".icstk", StringComparison.OrdinalIgnoreCase)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 记录崩溃日志
|
||||
private static void WriteCrashLog(string message)
|
||||
{
|
||||
@@ -500,7 +569,10 @@ namespace Ink_Canvas
|
||||
Directory.CreateDirectory(crashLogFile);
|
||||
}
|
||||
|
||||
string logFileName = Path.Combine(crashLogFile, $"crash_{DateTime.Now:yyyyMMdd}.log");
|
||||
string appStartTimeStr = (appStartTime != default(DateTime) && appStartTime != DateTime.MinValue)
|
||||
? appStartTime.ToString("yyyy-MM-dd-HH-mm-ss")
|
||||
: DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss");
|
||||
string logFileName = Path.Combine(crashLogFile, $"Crash_{appStartTimeStr}.txt");
|
||||
|
||||
// 收集系统状态信息
|
||||
string memoryUsage = (Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024)) + " MB";
|
||||
@@ -518,7 +590,7 @@ namespace Ink_Canvas
|
||||
// 同时记录到主日志
|
||||
LogHelper.WriteLogToFile(message, LogHelper.LogType.Error);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
// 增加字段保存崩溃后操作设置
|
||||
@@ -535,7 +607,7 @@ namespace Ink_Canvas
|
||||
var json = File.ReadAllText(settingsPath);
|
||||
dynamic obj = JsonConvert.DeserializeObject(json);
|
||||
int crashAction = 0;
|
||||
try { crashAction = (int)(obj["startup"]["crashAction"] ?? 0); } catch { }
|
||||
try { crashAction = (int)(obj["startup"]["crashAction"] ?? 0); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
CrashAction = (CrashActionType)crashAction;
|
||||
}
|
||||
// 从主窗口同步
|
||||
@@ -544,12 +616,37 @@ namespace Ink_Canvas
|
||||
CrashAction = (CrashActionType)Ink_Canvas.MainWindow.Settings.Startup.CrashAction;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
Ink_Canvas.MainWindow.ShowNewMessage("抱歉,出现未预期的异常,可能导致 InkCanvasForClass 运行不稳定。\n建议保存墨迹后重启应用。");
|
||||
// 检查是否是DynamicRenderer线程访问UI对象的已知问题
|
||||
if (e.Exception is InvalidOperationException invalidOpEx)
|
||||
{
|
||||
string exceptionMessage = invalidOpEx.Message ?? "";
|
||||
string exceptionStackTrace = invalidOpEx.StackTrace ?? "";
|
||||
|
||||
// 检查是否是DynamicRenderer相关的线程访问问题
|
||||
if (exceptionMessage.Contains("调用线程无法访问此对象") ||
|
||||
exceptionMessage.Contains("because another thread owns it") ||
|
||||
exceptionStackTrace.Contains("DynamicRenderer") ||
|
||||
exceptionStackTrace.Contains("CompositionTarget.get_RootVisual"))
|
||||
{
|
||||
// 这是WPF InkCanvas的已知问题,DynamicRenderer的后台线程尝试访问UI对象
|
||||
// 这个异常不会影响应用程序功能,可以安全地忽略
|
||||
LogHelper.WriteLogToFile(
|
||||
$"检测到DynamicRenderer线程访问异常(已安全处理): {invalidOpEx.Message}",
|
||||
LogHelper.LogType.Warning
|
||||
);
|
||||
|
||||
// 标记为已处理,不显示错误消息,不触发重启
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Ink_Canvas.MainWindow.ShowNewMessage(Strings.GetString("Msg_UnexpectedError"));
|
||||
LogHelper.NewLog(e.Exception.ToString());
|
||||
|
||||
// 记录到崩溃日志
|
||||
@@ -565,7 +662,7 @@ namespace Ink_Canvas
|
||||
StartupCount.Increment();
|
||||
if (StartupCount.GetCount() >= 5)
|
||||
{
|
||||
MessageBox.Show("检测到程序已连续重启5次,已停止自动重启。请联系开发者或检查系统环境。", "重启次数过多", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show(Strings.GetString("Msg_RestartLimit"), Strings.GetString("Msg_RestartLimitTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
StartupCount.Reset();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
@@ -574,7 +671,7 @@ namespace Ink_Canvas
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
Process.Start(exePath);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
Environment.Exit(1);
|
||||
}
|
||||
// CrashActionType.NoAction 时不做处理
|
||||
@@ -582,16 +679,28 @@ namespace Ink_Canvas
|
||||
|
||||
private TaskbarIcon _taskbar;
|
||||
|
||||
/// <summary>
|
||||
/// 处理应用启动流程:根据命令行与设置显示启动画面、初始化组件与遥测、处理更新相关逻辑、单实例检查并在必要时通过 IPC 与已运行实例通信,最终创建并显示主窗口并启动文件关联与 IPC 监听器。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件的发送者(通常为 Application 对象)。</param>
|
||||
/// <param name="e">启动事件参数;其 Args 可包含控制启动流程的标志,例如:
|
||||
/// - "--final-app":表示这是更新后的最终应用启动(会清理更新标记等)
|
||||
/// - "--update-mode":表示以更新模式启动(跳过主窗口显示)
|
||||
/// - "--board":直接进入白板模式
|
||||
/// - "--show":退出收纳模式并恢复浮动栏
|
||||
/// - "--skip-mutex-check":跳过单实例互斥检查
|
||||
/// - "-m":允许多实例启动
|
||||
/// 另外也可能包含以 "icc:" 开头的 URI 参数或 .icstk 文件路径用于启动时的 IPC 交互。</param>
|
||||
async void App_Startup(object sender, StartupEventArgs e)
|
||||
{
|
||||
// 初始化应用启动时间
|
||||
appStartTime = DateTime.Now;
|
||||
appStartupStartTime = DateTime.Now;
|
||||
|
||||
// 根据设置决定是否显示启动画面
|
||||
if (ShouldShowSplashScreen())
|
||||
if (ShouldShowSplashScreen() && !IsLaunchByFileOrUri(e.Args))
|
||||
{
|
||||
ShowSplashScreen();
|
||||
SetSplashMessage("正在启动 Ink Canvas...");
|
||||
SetSplashMessage(Strings.GetString("Splash_Starting"));
|
||||
SetSplashProgress(20);
|
||||
await Task.Delay(500);
|
||||
|
||||
@@ -599,7 +708,7 @@ namespace Ink_Canvas
|
||||
Application.Current.Dispatcher.Invoke(() => { }, DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
System.Threading.Thread.Sleep(500);
|
||||
await Task.Delay(500);
|
||||
RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
|
||||
|
||||
LogHelper.NewLog(string.Format("Ink Canvas Starting (Version: {0})", Assembly.GetExecutingAssembly().GetName().Version));
|
||||
@@ -670,6 +779,21 @@ namespace Ink_Canvas
|
||||
await Task.Delay(500);
|
||||
}
|
||||
DeviceIdentifier.RecordAppLaunch();
|
||||
try
|
||||
{
|
||||
var systemVersion = DeviceIdentifier.GetSystemVersion();
|
||||
if (!string.IsNullOrWhiteSpace(systemVersion))
|
||||
{
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.SetTag("system_version", systemVersion);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"App | 初始化系统版本遥测标签失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
LogHelper.WriteLogToFile($"App | 设备ID: {DeviceIdentifier.GetDeviceId()}");
|
||||
LogHelper.WriteLogToFile($"App | 使用频率: {DeviceIdentifier.GetUsageFrequency()}");
|
||||
LogHelper.WriteLogToFile($"App | 更新优先级: {DeviceIdentifier.GetUpdatePriority()}");
|
||||
@@ -706,6 +830,20 @@ namespace Ink_Canvas
|
||||
{
|
||||
LogHelper.WriteLogToFile($"App | 清理更新标记文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(3000);
|
||||
LogHelper.WriteLogToFile("App | 最终应用启动,删除AutoUpdate文件夹");
|
||||
AutoUpdateHelper.DeleteUpdatesFolder();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"App | 删除AutoUpdate文件夹失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果不是最终应用启动,才检查更新标记文件
|
||||
@@ -797,7 +935,7 @@ namespace Ink_Canvas
|
||||
LogHelper.WriteLogToFile("App | 清理损坏的更新标记文件");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,6 +995,22 @@ namespace Ink_Canvas
|
||||
LogHelper.WriteLogToFile("通过IPC发送展开浮动栏命令失败", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
// 检查是否有URI参数
|
||||
else if (e.Args.Any(a => a.StartsWith("icc:", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
string uriArg = e.Args.FirstOrDefault(a => a.StartsWith("icc:", StringComparison.OrdinalIgnoreCase));
|
||||
LogHelper.WriteLogToFile($"检测到已运行实例且有URI参数: {uriArg}", LogHelper.LogType.Event);
|
||||
|
||||
// 尝试通过IPC发送URI命令给已运行实例
|
||||
if (FileAssociationManager.TrySendUriCommandToExistingInstance(uriArg))
|
||||
{
|
||||
LogHelper.WriteLogToFile("URI命令已通过IPC发送给已运行实例", LogHelper.LogType.Event);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile("通过IPC发送URI命令失败", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile("检测到已运行实例,但无文件参数", LogHelper.LogType.Event);
|
||||
@@ -874,7 +1028,7 @@ namespace Ink_Canvas
|
||||
watchdogProcess.Kill();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
@@ -902,7 +1056,7 @@ namespace Ink_Canvas
|
||||
mutex = new Mutex(true, mutexName, out bool tempRet);
|
||||
|
||||
// 额外等待一小段时间确保更新进程完全退出
|
||||
Thread.Sleep(1000);
|
||||
await Task.Delay(1000);
|
||||
LogHelper.WriteLogToFile("App | 特殊模式等待完成,继续启动");
|
||||
}
|
||||
|
||||
@@ -923,6 +1077,18 @@ namespace Ink_Canvas
|
||||
// 主窗口加载完成后关闭启动画面
|
||||
mainWindow.Loaded += (s, args) =>
|
||||
{
|
||||
isStartupComplete = true;
|
||||
startupCompleteHeartbeat = DateTime.Now;
|
||||
if (_isSplashScreenShown && splashScreenStartTime != DateTime.MinValue)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动完成心跳已记录,启动画面显示时长: {(startupCompleteHeartbeat - splashScreenStartTime).TotalSeconds:F2}秒");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动完成心跳已记录");
|
||||
}
|
||||
LogHelper.WriteLogToFile($"启动时长: {(startupCompleteHeartbeat - appStartupStartTime).TotalSeconds:F2}秒");
|
||||
|
||||
if (_isSplashScreenShown)
|
||||
{
|
||||
SetSplashMessage("完成初始化...");
|
||||
@@ -945,6 +1111,21 @@ namespace Ink_Canvas
|
||||
|
||||
mainWindow.Show();
|
||||
|
||||
// 处理启动时的URI参数
|
||||
string startupUriArg = e.Args.FirstOrDefault(a => a.StartsWith("icc:", StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrEmpty(startupUriArg))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"App | 处理启动URI参数: {startupUriArg}", LogHelper.LogType.Event);
|
||||
// 延迟一点执行,确保窗口初始化完成
|
||||
Task.Delay(1000).ContinueWith(_ =>
|
||||
{
|
||||
mainWindow.Dispatcher.Invoke(() =>
|
||||
{
|
||||
mainWindow.HandleUriCommand(startupUriArg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 注册.icstk文件关联
|
||||
try
|
||||
{
|
||||
@@ -968,6 +1149,17 @@ namespace Ink_Canvas
|
||||
LogHelper.WriteLogToFile($"启动IPC监听器时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
// 初始化上传帮助类
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("初始化上传帮助类");
|
||||
Helpers.UploadHelper.Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化上传帮助类时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||||
@@ -983,9 +1175,9 @@ namespace Ink_Canvas
|
||||
SenderScrollViewer.ScrollToVerticalOffset(SenderScrollViewer.VerticalOffset - e.Delta * 10 * SystemInformation.MouseWheelScrollLines / (double)120);
|
||||
e.Handled = true;
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
// 用于设置崩溃后操作类型
|
||||
@@ -996,37 +1188,105 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
// 心跳相关
|
||||
private static Timer heartbeatTimer;
|
||||
private static DispatcherTimer heartbeatTimer;
|
||||
private static DateTime lastHeartbeat = DateTime.Now;
|
||||
private static Timer watchdogTimer;
|
||||
private static bool isStartupComplete = false;
|
||||
private static DateTime startupCompleteHeartbeat = DateTime.MinValue;
|
||||
private static DateTime splashScreenStartTime = DateTime.MinValue;
|
||||
private static DateTime appStartupStartTime = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// 启动并管理应用的心跳与守护检查定时器,监测启动阶段与主线程是否无响应,并在符合配置的情况下尝试静默重启应用。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 启动一个每秒更新心跳时间戳的调度定时器和一个每3秒运行的守护定时器。
|
||||
/// - 守护定时器在首次运行的启动阶段若检测到超过两分钟未完成启动,会根据 CrashAction 配置尝试静默重启。
|
||||
/// - 在启动完成后若检测到主线程超过10秒无响应,会根据 CrashAction 配置尝试静默重启。
|
||||
/// - 对连续重启次数有保护:若重启计数达到或超过5次,会弹出提示并停止自动重启(重置重启计数并退出进程)。
|
||||
/// - 在 OOBE(首次引导)展示期间不执行守护检查。
|
||||
/// - 该方法会产生外部可观察的副作用:可能启动新进程并调用 Environment.Exit 终止当前进程,或显示消息框。
|
||||
/// </remarks>
|
||||
private void StartHeartbeatMonitor()
|
||||
{
|
||||
// 主线程定时更新心跳
|
||||
heartbeatTimer = new Timer(_ => lastHeartbeat = DateTime.Now, null, 0, 1000);
|
||||
// 辅助线程检测心跳超时
|
||||
heartbeatTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
heartbeatTimer.Tick += (_, __) => lastHeartbeat = DateTime.Now;
|
||||
heartbeatTimer.Start();
|
||||
|
||||
watchdogTimer = new Timer(_ =>
|
||||
{
|
||||
if ((DateTime.Now - lastHeartbeat).TotalSeconds > 10)
|
||||
if (IsOobeShowing)
|
||||
return;
|
||||
|
||||
if (!isStartupComplete && appStartupStartTime != DateTime.MinValue)
|
||||
{
|
||||
LogHelper.NewLog("检测到主线程无响应,自动重启。");
|
||||
SyncCrashActionFromSettings(); // 新增:心跳检测时同步最新设置
|
||||
if (CrashAction == CrashActionType.SilentRestart)
|
||||
DateTime startTime = _isSplashScreenShown && splashScreenStartTime != DateTime.MinValue
|
||||
? splashScreenStartTime
|
||||
: appStartupStartTime;
|
||||
TimeSpan elapsedSinceStart = DateTime.Now - startTime;
|
||||
if (elapsedSinceStart.TotalMinutes >= 2)
|
||||
{
|
||||
StartupCount.Increment();
|
||||
if (StartupCount.GetCount() >= 5)
|
||||
string timeType = _isSplashScreenShown ? "启动画面已显示" : "应用启动开始";
|
||||
LogHelper.WriteLogToFile($"检测到启动假死:{timeType}{elapsedSinceStart.TotalMinutes:F2}分钟,但未收到启动完成心跳,自动重启。", LogHelper.LogType.Error);
|
||||
SyncCrashActionFromSettings();
|
||||
if (CrashAction == CrashActionType.SilentRestart)
|
||||
{
|
||||
MessageBox.Show("检测到程序已连续重启5次,已停止自动重启。请联系开发者或检查系统环境。", "重启次数过多", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
StartupCount.Reset();
|
||||
StartupCount.Increment();
|
||||
if (StartupCount.GetCount() >= 5)
|
||||
{
|
||||
MessageBox.Show(Strings.GetString("Msg_RestartLimit"), Strings.GetString("Msg_RestartLimitTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
StartupCount.Reset();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
try
|
||||
{
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
Process.Start(exePath);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
Environment.Exit(1);
|
||||
}
|
||||
try
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isStartupComplete)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var sinceHeartbeat = now - lastHeartbeat;
|
||||
var sinceStartupComplete = startupCompleteHeartbeat == DateTime.MinValue
|
||||
? TimeSpan.Zero
|
||||
: now - startupCompleteHeartbeat;
|
||||
|
||||
if (sinceStartupComplete.TotalSeconds < 30)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sinceHeartbeat.TotalSeconds > 10)
|
||||
{
|
||||
LogHelper.NewLog("检测到主线程无响应,自动重启。");
|
||||
SyncCrashActionFromSettings();
|
||||
if (CrashAction == CrashActionType.SilentRestart)
|
||||
{
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
Process.Start(exePath);
|
||||
StartupCount.Increment();
|
||||
if (StartupCount.GetCount() >= 5)
|
||||
{
|
||||
MessageBox.Show(Strings.GetString("Msg_RestartLimit"), Strings.GetString("Msg_RestartLimitTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
StartupCount.Reset();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
try
|
||||
{
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
Process.Start(exePath);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
Environment.Exit(1);
|
||||
}
|
||||
catch { }
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
}, null, 0, 3000);
|
||||
@@ -1050,7 +1310,16 @@ namespace Ink_Canvas
|
||||
watchdogProcess = Process.Start(psi);
|
||||
}
|
||||
|
||||
// 看门狗主逻辑
|
||||
/// <summary>
|
||||
/// 作为守护进程监视指定的主进程,并在主进程异常退出时根据配置执行重启或退出操作。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该方法期望命令行参数格式为:"--watchdog <pid> <exitSignalFile>"(args[1..3])。
|
||||
/// - 每 2 秒检查一次指定的主进程是否仍在运行;同时检测退出信号文件,若存在则删除该文件并以代码 0 退出守护进程。
|
||||
/// - 当主进程退出时,会同步崩溃处理设置(SyncCrashActionFromSettings)。若启用了 UIA 顶层访问(IsUIAccessTopMostEnabled),守护进程直接退出。
|
||||
/// - 若崩溃动作为 SilentRestart,则增加启动计数并:当连续重启计数达到 5 次及以上时弹出错误对话框、重置计数并以代码 1 退出;否则启动新的主进程实例。
|
||||
/// 方法对内部异常静默处理,并在完成后确保进程退出。
|
||||
/// </remarks>
|
||||
public static void RunWatchdogIfNeeded()
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
@@ -1066,37 +1335,105 @@ namespace Ink_Canvas
|
||||
// 检查退出信号文件
|
||||
if (File.Exists(exitSignalFile))
|
||||
{
|
||||
try { File.Delete(exitSignalFile); } catch { }
|
||||
try { File.Delete(exitSignalFile); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
Environment.Exit(0);
|
||||
}
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
// 主进程异常退出,自动重启前判断崩溃后操作
|
||||
SyncCrashActionFromSettings(); // 同步设置
|
||||
|
||||
|
||||
if (IsUIAccessTopMostEnabled)
|
||||
{
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
|
||||
if (CrashAction == CrashActionType.SilentRestart)
|
||||
{
|
||||
StartupCount.Increment();
|
||||
if (StartupCount.GetCount() >= 5)
|
||||
{
|
||||
MessageBox.Show("检测到程序已连续重启5次,已停止自动重启。请联系开发者或检查系统环境。", "重启次数过多", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show(Strings.GetString("Msg_RestartLimit"), Strings.GetString("Msg_RestartLimitTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
StartupCount.Reset();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
Process.Start(exePath);
|
||||
else
|
||||
{
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
Process.Start(exePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetDlassTelemetryDsn()
|
||||
{
|
||||
try
|
||||
{
|
||||
var envDsn = Environment.GetEnvironmentVariable("DLASS_SENTRY_DSN");
|
||||
if (!string.IsNullOrWhiteSpace(envDsn))
|
||||
{
|
||||
return envDsn;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = "Ink_Canvas.telemetry_dsn.txt";
|
||||
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
|
||||
{
|
||||
if (stream != null)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(stream, System.Text.Encoding.UTF8))
|
||||
{
|
||||
string dsn = reader.ReadToEnd().Trim();
|
||||
if (!string.IsNullOrWhiteSpace(dsn))
|
||||
{
|
||||
return dsn;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从程序集资源读取遥测 DSN 失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
string assemblyLocation = Assembly.GetExecutingAssembly().Location;
|
||||
string currentDir = Path.GetDirectoryName(assemblyLocation);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
string dsnFilePath = Path.Combine(currentDir, "telemetry_dsn.txt");
|
||||
if (File.Exists(dsnFilePath))
|
||||
{
|
||||
string dsn = File.ReadAllText(dsnFilePath, System.Text.Encoding.UTF8).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(dsn))
|
||||
{
|
||||
return dsn;
|
||||
}
|
||||
}
|
||||
|
||||
DirectoryInfo parentDir = Directory.GetParent(currentDir);
|
||||
if (parentDir == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
currentDir = parentDir.FullName;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void App_Exit(object sender, ExitEventArgs e)
|
||||
{
|
||||
// 仅在软件内主动退出时关闭看门狗,并写入退出信号
|
||||
@@ -1135,7 +1472,7 @@ namespace Ink_Canvas
|
||||
{
|
||||
LogHelper.WriteLogToFile($"退出处理时发生错误: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using System.Windows;
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("CJK_mkp")]
|
||||
[assembly: AssemblyProduct("InkCanvasForClass")]
|
||||
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025")]
|
||||
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025-2026")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
@@ -19,14 +19,8 @@ using System.Windows;
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
//In order to begin building localizable applications, set
|
||||
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
|
||||
//inside a <PropertyGroup>. For example, if you are using US english
|
||||
//in your source files, set the <UICulture> to en-US. Then uncomment
|
||||
//the NeutralResourceLanguage attribute below. Update the "en-US" in
|
||||
//the line below to match the UICulture setting in the project file.
|
||||
|
||||
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
|
||||
// i18n: 默认/回退语言为简体中文,与 Strings.resx 默认文案一致。
|
||||
[assembly: System.Resources.NeutralResourcesLanguage("zh-CN", System.Resources.UltimateResourceFallbackLocation.MainAssembly)]
|
||||
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
@@ -49,5 +43,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.17.0")]
|
||||
[assembly: AssemblyFileVersion("1.7.17.0")]
|
||||
[assembly: AssemblyVersion("1.7.18.8")]
|
||||
[assembly: AssemblyFileVersion("1.7.18.8")]
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<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">
|
||||
|
||||
<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
@@ -73,7 +73,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
sb.Begin((FrameworkElement)element);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
public static void ShowWithSlideFromLeftAndFade(UIElement element, double duration = 0.25)
|
||||
@@ -113,7 +113,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
sb.Begin((FrameworkElement)element);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
public static void ShowWithScaleFromLeft(UIElement element, double duration = 0.2)
|
||||
@@ -156,7 +156,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
sb.Begin((FrameworkElement)element);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
public static void ShowWithScaleFromRight(UIElement element, double duration = 0.2)
|
||||
@@ -200,7 +200,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
sb.Begin((FrameworkElement)element);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
public static void HideWithSlideAndFade(UIElement element, double duration = 0.15)
|
||||
@@ -246,7 +246,7 @@ namespace Ink_Canvas.Helpers
|
||||
element.RenderTransform = new TranslateTransform();
|
||||
sb.Begin((FrameworkElement)element);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
public static void HideWithFadeOut(UIElement element, double duration = 0.15)
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Ink_Canvas.Helpers
|
||||
/// 检查是否需要执行自动备份
|
||||
/// </summary>
|
||||
/// <param name="settings">设置对象</param>
|
||||
/// <returns>如果需要备份返回true,否则返回false</returns>
|
||||
/// <returns>如果需要备份返回<see langword="true"/>,否则返回<see langword="false"/></returns>
|
||||
public static bool ShouldPerformAutoBackup(Settings settings)
|
||||
{
|
||||
try
|
||||
@@ -50,8 +50,11 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 执行自动备份
|
||||
/// </summary>
|
||||
/// <param name="settings">设置对象</param>
|
||||
/// <returns>备份是否成功</returns>
|
||||
/// <remarks>
|
||||
/// 为主配置文件创建一次自动备份并在成功后更新并保存设置中的最后备份时间。
|
||||
/// </remarks>
|
||||
/// <param name="settings">应用的设置对象;在成功备份后会更新 settings.Advanced.LastAutoBackupTime 并调用保存操作。</param>
|
||||
/// <returns><see langword="true"/> 表示备份成功,<see langword="false"/> 表示备份失败或被跳过。</returns>
|
||||
public static bool PerformAutoBackup(Settings settings)
|
||||
{
|
||||
try
|
||||
@@ -59,7 +62,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 确保备份目录存在
|
||||
if (!Directory.Exists(BackupDir))
|
||||
{
|
||||
Directory.CreateDirectory(BackupDir);
|
||||
ProcessProtectionManager.WithWriteAccess(BackupDir, () => Directory.CreateDirectory(BackupDir));
|
||||
}
|
||||
|
||||
// 检查主配置文件是否存在
|
||||
@@ -74,7 +77,7 @@ namespace Ink_Canvas.Helpers
|
||||
string backupPath = Path.Combine(BackupDir, backupFileName);
|
||||
|
||||
// 复制主配置文件到备份位置
|
||||
File.Copy(SettingsFile, backupPath, true);
|
||||
ProcessProtectionManager.WithWriteAccess(backupPath, () => File.Copy(SettingsFile, backupPath, true));
|
||||
|
||||
// 更新最后备份时间
|
||||
settings.Advanced.LastAutoBackupTime = DateTime.Now;
|
||||
@@ -91,7 +94,10 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 尝试从备份恢复配置文件
|
||||
/// </summary>
|
||||
/// <returns>恢复是否成功</returns>
|
||||
/// <remarks>
|
||||
/// 从最新可用的自动备份恢复主设置文件(Settings.json)。如果当前设置文件存在,会先将其复制到备份目录并加上时间戳作为“损坏”的备份副本,然后用最新备份覆盖原文件。
|
||||
/// </remarks>
|
||||
/// <returns><see langword="true"/> 如果恢复成功,<see langword="false"/> 否则。</returns>
|
||||
public static bool TryRestoreFromBackup()
|
||||
{
|
||||
try
|
||||
@@ -138,11 +144,11 @@ namespace Ink_Canvas.Helpers
|
||||
if (File.Exists(SettingsFile))
|
||||
{
|
||||
string corruptedBackup = Path.Combine(BackupDir, $"Settings_Corrupted_{DateTime.Now:yyyyMMdd_HHmmss}.json");
|
||||
File.Copy(SettingsFile, corruptedBackup, true);
|
||||
ProcessProtectionManager.WithWriteAccess(corruptedBackup, () => File.Copy(SettingsFile, corruptedBackup, true));
|
||||
}
|
||||
|
||||
// 从备份恢复配置文件
|
||||
File.Copy(latestBackup, SettingsFile, true);
|
||||
ProcessProtectionManager.WithWriteAccess(SettingsFile, () => File.Copy(latestBackup, SettingsFile, true));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -156,6 +162,10 @@ namespace Ink_Canvas.Helpers
|
||||
/// 清理过期的备份文件
|
||||
/// 保留最近30天的备份文件
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 删除备份目录中按“备份前缀”匹配且创建时间早于 30 天的自动备份文件(即自动备份文件的命名前缀),不会删除诸如 Settings_Corrupted_*.json 之类的其他备份或错误状态文件。
|
||||
/// 如果备份目录不存在则不执行任何操作;删除操作在受写入保护的上下文中执行,任何错误会被记录但不会抛出异常。
|
||||
/// </remarks>
|
||||
public static void CleanupOldBackups()
|
||||
{
|
||||
try
|
||||
@@ -173,7 +183,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
if (File.GetCreationTime(file) < cutoffDate)
|
||||
{
|
||||
File.Delete(file);
|
||||
ProcessProtectionManager.WithWriteAccess(file, () => File.Delete(file));
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically shrinks text to fit available width.
|
||||
/// Supports TextBlock and Label.
|
||||
/// Only shrinks, never enlarges above MaxFontSize.
|
||||
/// </summary>
|
||||
public static class AutoFontSizeHelper
|
||||
{
|
||||
public static readonly DependencyProperty IsEnabledProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"IsEnabled",
|
||||
typeof(bool),
|
||||
typeof(AutoFontSizeHelper),
|
||||
new PropertyMetadata(false, OnIsEnabledChanged));
|
||||
|
||||
public static void SetIsEnabled(DependencyObject element, bool value) => element.SetValue(IsEnabledProperty, value);
|
||||
public static bool GetIsEnabled(DependencyObject element) => (bool)element.GetValue(IsEnabledProperty);
|
||||
|
||||
public static readonly DependencyProperty MinFontSizeProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"MinFontSize",
|
||||
typeof(double),
|
||||
typeof(AutoFontSizeHelper),
|
||||
new PropertyMetadata(6d, OnSizingPropertyChanged));
|
||||
|
||||
public static void SetMinFontSize(DependencyObject element, double value) => element.SetValue(MinFontSizeProperty, value);
|
||||
public static double GetMinFontSize(DependencyObject element) => (double)element.GetValue(MinFontSizeProperty);
|
||||
|
||||
public static readonly DependencyProperty MaxFontSizeProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"MaxFontSize",
|
||||
typeof(double),
|
||||
typeof(AutoFontSizeHelper),
|
||||
new PropertyMetadata(double.NaN, OnSizingPropertyChanged));
|
||||
|
||||
public static void SetMaxFontSize(DependencyObject element, double value) => element.SetValue(MaxFontSizeProperty, value);
|
||||
public static double GetMaxFontSize(DependencyObject element) => (double)element.GetValue(MaxFontSizeProperty);
|
||||
|
||||
public static readonly DependencyProperty StepProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"Step",
|
||||
typeof(double),
|
||||
typeof(AutoFontSizeHelper),
|
||||
new PropertyMetadata(0.5d, OnSizingPropertyChanged));
|
||||
|
||||
public static void SetStep(DependencyObject element, double value) => element.SetValue(StepProperty, value);
|
||||
public static double GetStep(DependencyObject element) => (double)element.GetValue(StepProperty);
|
||||
|
||||
private static readonly DependencyProperty IsAdjustingProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"IsAdjusting",
|
||||
typeof(bool),
|
||||
typeof(AutoFontSizeHelper),
|
||||
new PropertyMetadata(false));
|
||||
|
||||
private static void SetIsAdjusting(DependencyObject element, bool value) => element.SetValue(IsAdjustingProperty, value);
|
||||
private static bool GetIsAdjusting(DependencyObject element) => (bool)element.GetValue(IsAdjustingProperty);
|
||||
|
||||
private static readonly DependencyProperty OriginalFontSizeProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"OriginalFontSize",
|
||||
typeof(double),
|
||||
typeof(AutoFontSizeHelper),
|
||||
new PropertyMetadata(double.NaN));
|
||||
|
||||
private static void SetOriginalFontSize(DependencyObject element, double value) => element.SetValue(OriginalFontSizeProperty, value);
|
||||
private static double GetOriginalFontSize(DependencyObject element) => (double)element.GetValue(OriginalFontSizeProperty);
|
||||
|
||||
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (!(d is FrameworkElement fe)) return;
|
||||
if (!(fe is TextBlock) && !(fe is Label)) return;
|
||||
|
||||
if ((bool)e.NewValue)
|
||||
{
|
||||
var originalFontSize = GetElementFontSize(fe);
|
||||
if (!double.IsNaN(originalFontSize) && originalFontSize > 0)
|
||||
{
|
||||
SetOriginalFontSize(fe, originalFontSize);
|
||||
}
|
||||
|
||||
fe.SizeChanged += Element_OnSizeChanged;
|
||||
fe.Loaded += Element_OnLoaded;
|
||||
fe.Unloaded += Element_OnUnloaded;
|
||||
TryHookContentChanged(fe, true);
|
||||
|
||||
fe.Dispatcher.BeginInvoke(new Action(() => TryAdjust(fe)), DispatcherPriority.Loaded);
|
||||
}
|
||||
else
|
||||
{
|
||||
fe.SizeChanged -= Element_OnSizeChanged;
|
||||
fe.Loaded -= Element_OnLoaded;
|
||||
fe.Unloaded -= Element_OnUnloaded;
|
||||
TryHookContentChanged(fe, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnSizingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is FrameworkElement fe && GetIsEnabled(fe))
|
||||
{
|
||||
fe.Dispatcher.BeginInvoke(new Action(() => TryAdjust(fe)), DispatcherPriority.Loaded);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Element_OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement fe)
|
||||
{
|
||||
fe.Dispatcher.BeginInvoke(new Action(() => TryAdjust(fe)), DispatcherPriority.Loaded);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Element_OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// No extra cleanup required here.
|
||||
}
|
||||
|
||||
private static void Element_OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement fe) TryAdjust(fe);
|
||||
}
|
||||
|
||||
private static void Element_OnTextChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement fe)
|
||||
{
|
||||
fe.Dispatcher.BeginInvoke(new Action(() => TryAdjust(fe)), DispatcherPriority.Loaded);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryHookContentChanged(FrameworkElement fe, bool add)
|
||||
{
|
||||
try
|
||||
{
|
||||
DependencyPropertyDescriptor dpd = null;
|
||||
if (fe is TextBlock)
|
||||
{
|
||||
dpd = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock));
|
||||
}
|
||||
else if (fe is Label)
|
||||
{
|
||||
dpd = DependencyPropertyDescriptor.FromProperty(ContentControl.ContentProperty, typeof(ContentControl));
|
||||
}
|
||||
|
||||
if (dpd == null) return;
|
||||
if (add) dpd.AddValueChanged(fe, Element_OnTextChanged);
|
||||
else dpd.RemoveValueChanged(fe, Element_OnTextChanged);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore descriptor issues in rare runtime cases.
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryAdjust(FrameworkElement fe)
|
||||
{
|
||||
if (fe == null) return;
|
||||
if (!GetIsEnabled(fe)) return;
|
||||
if (GetIsAdjusting(fe)) return;
|
||||
|
||||
var text = GetElementText(fe);
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
if (!ShouldAutoScaleForCurrentCulture(text))
|
||||
{
|
||||
RestoreOriginalFontSize(fe);
|
||||
return;
|
||||
}
|
||||
|
||||
var availableWidth = GetAvailableWidth(fe);
|
||||
if (double.IsNaN(availableWidth) || availableWidth <= 1) return;
|
||||
|
||||
var min = GetMinFontSize(fe);
|
||||
if (double.IsNaN(min) || min <= 0) min = 6d;
|
||||
|
||||
var step = GetStep(fe);
|
||||
if (double.IsNaN(step) || step < 0.1) step = 0.5d;
|
||||
|
||||
var current = GetElementFontSize(fe);
|
||||
if (double.IsNaN(current) || current <= 0) return;
|
||||
|
||||
var max = GetMaxFontSize(fe);
|
||||
if (double.IsNaN(max) || max <= 0) max = current;
|
||||
// Never enlarge: auto-fit should only reduce font size when needed.
|
||||
if (max > current) max = current;
|
||||
|
||||
var startFont = Math.Min(current, max);
|
||||
if (startFont < min) startFont = min;
|
||||
|
||||
SetIsAdjusting(fe, true);
|
||||
try
|
||||
{
|
||||
var font = startFont;
|
||||
var desired = MeasureTextWidth(fe, text, font);
|
||||
if (desired <= 0) return;
|
||||
|
||||
while (font > min && desired > availableWidth + 0.5)
|
||||
{
|
||||
font = Math.Max(min, font - step);
|
||||
desired = MeasureTextWidth(fe, text, font);
|
||||
if (desired <= 0) break;
|
||||
}
|
||||
|
||||
// Hard-fit fallback: when very narrow slots (e.g., 28px) still overflow at MinFontSize,
|
||||
// keep shrinking proportionally so text always fits in the available width.
|
||||
if (desired > availableWidth + 0.5)
|
||||
{
|
||||
var hardFont = font;
|
||||
for (var i = 0; i < 6 && desired > availableWidth + 0.5; i++)
|
||||
{
|
||||
var ratio = availableWidth / Math.Max(1.0, desired);
|
||||
hardFont = Math.Max(1.0, hardFont * ratio);
|
||||
desired = MeasureTextWidth(fe, text, hardFont);
|
||||
if (desired <= 0) break;
|
||||
}
|
||||
|
||||
font = hardFont;
|
||||
}
|
||||
|
||||
if (!double.IsNaN(font) && font > 0 && Math.Abs(current - font) > 0.01)
|
||||
{
|
||||
SetElementFontSize(fe, font);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetIsAdjusting(fe, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetElementText(FrameworkElement fe)
|
||||
{
|
||||
if (fe is TextBlock tb) return tb.Text;
|
||||
if (fe is Label label) return label.Content as string ?? label.Content?.ToString();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ShouldAutoScaleForCurrentCulture(string text)
|
||||
{
|
||||
// Requirement: auto-scale for English UI only, keep Chinese font size unchanged.
|
||||
var culture = CultureInfo.CurrentUICulture;
|
||||
var name = culture?.Name ?? string.Empty;
|
||||
if (name.StartsWith("en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: if actual rendered text is Latin-heavy, still auto-scale.
|
||||
// This avoids clipping when culture detection is out of sync.
|
||||
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||
foreach (var ch in text)
|
||||
{
|
||||
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void RestoreOriginalFontSize(FrameworkElement fe)
|
||||
{
|
||||
var original = GetOriginalFontSize(fe);
|
||||
if (double.IsNaN(original) || original <= 0) return;
|
||||
|
||||
var current = GetElementFontSize(fe);
|
||||
if (double.IsNaN(current) || current <= 0) return;
|
||||
|
||||
if (Math.Abs(current - original) > 0.01)
|
||||
{
|
||||
SetElementFontSize(fe, original);
|
||||
}
|
||||
}
|
||||
|
||||
private static double GetElementFontSize(FrameworkElement fe)
|
||||
{
|
||||
if (fe is TextBlock tb) return tb.FontSize;
|
||||
if (fe is Label label) return label.FontSize;
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
private static void SetElementFontSize(FrameworkElement fe, double value)
|
||||
{
|
||||
if (fe is TextBlock tb) tb.FontSize = value;
|
||||
else if (fe is Label label) label.FontSize = value;
|
||||
}
|
||||
|
||||
private static double GetAvailableWidth(FrameworkElement fe)
|
||||
{
|
||||
double width = double.PositiveInfinity;
|
||||
|
||||
// Explicit width on the element itself should be a hard cap.
|
||||
if (!double.IsNaN(fe.Width) && !double.IsInfinity(fe.Width) && fe.Width > 1)
|
||||
{
|
||||
width = Math.Min(width, fe.Width - fe.Margin.Left - fe.Margin.Right);
|
||||
}
|
||||
|
||||
if (!double.IsNaN(fe.MaxWidth) && !double.IsInfinity(fe.MaxWidth) && fe.MaxWidth > 1)
|
||||
{
|
||||
width = Math.Min(width, fe.MaxWidth - fe.Margin.Left - fe.Margin.Right);
|
||||
}
|
||||
|
||||
// Prefer the real layout slot first. This is usually the most accurate
|
||||
// "space actually assigned by layout" for the element.
|
||||
var slot = LayoutInformation.GetLayoutSlot(fe);
|
||||
if (!double.IsNaN(slot.Width) && !double.IsInfinity(slot.Width))
|
||||
{
|
||||
var slotWidth = slot.Width - fe.Margin.Left - fe.Margin.Right;
|
||||
if (slotWidth > 1) width = Math.Min(width, slotWidth);
|
||||
}
|
||||
|
||||
if (fe.ActualWidth > 1) width = Math.Min(width, fe.ActualWidth);
|
||||
|
||||
// Immediate parent may be a StackPanel that does not constrain width.
|
||||
// Walk a few ancestors and take the tightest finite width as fallback.
|
||||
DependencyObject ancestor = fe.Parent ?? VisualTreeHelper.GetParent(fe);
|
||||
var depth = 0;
|
||||
while (ancestor != null && depth < 8)
|
||||
{
|
||||
if (ancestor is FrameworkElement af && af.ActualWidth > 1)
|
||||
{
|
||||
var candidate = af.ActualWidth;
|
||||
|
||||
// If ancestor sets explicit width, treat it as a stronger cap.
|
||||
if (!double.IsNaN(af.Width) && !double.IsInfinity(af.Width) && af.Width > 1)
|
||||
{
|
||||
candidate = Math.Min(candidate, af.Width);
|
||||
}
|
||||
|
||||
if (!double.IsNaN(af.MaxWidth) && !double.IsInfinity(af.MaxWidth) && af.MaxWidth > 1)
|
||||
{
|
||||
candidate = Math.Min(candidate, af.MaxWidth);
|
||||
}
|
||||
|
||||
if (ancestor is Control ac)
|
||||
{
|
||||
candidate -= ac.Padding.Left + ac.Padding.Right;
|
||||
candidate -= ac.BorderThickness.Left + ac.BorderThickness.Right;
|
||||
}
|
||||
else if (ancestor is Border ab)
|
||||
{
|
||||
candidate -= ab.Padding.Left + ab.Padding.Right;
|
||||
candidate -= ab.BorderThickness.Left + ab.BorderThickness.Right;
|
||||
}
|
||||
|
||||
if (candidate > 1) width = Math.Min(width, candidate);
|
||||
}
|
||||
|
||||
ancestor = (ancestor as FrameworkElement)?.Parent ?? VisualTreeHelper.GetParent(ancestor);
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (double.IsInfinity(width) || double.IsNaN(width) || width <= 1) return -1;
|
||||
|
||||
// Keep width as inner text area.
|
||||
if (fe is Control control)
|
||||
{
|
||||
width -= control.Padding.Left + control.Padding.Right;
|
||||
width -= control.BorderThickness.Left + control.BorderThickness.Right;
|
||||
}
|
||||
else if (fe is Border border)
|
||||
{
|
||||
width -= border.Padding.Left + border.Padding.Right;
|
||||
width -= border.BorderThickness.Left + border.BorderThickness.Right;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
private static double MeasureTextWidth(FrameworkElement fe, string text, double fontSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dpi = VisualTreeHelper.GetDpi(fe);
|
||||
var culture = CultureInfo.CurrentUICulture;
|
||||
|
||||
if (fe.Language != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
culture = fe.Language.GetEquivalentCulture();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
var fontFamily = SystemFonts.MessageFontFamily;
|
||||
var fontStyle = FontStyles.Normal;
|
||||
var fontWeight = FontWeights.Normal;
|
||||
var fontStretch = FontStretches.Normal;
|
||||
Brush foreground = Brushes.Black;
|
||||
var flowDirection = FlowDirection.LeftToRight;
|
||||
|
||||
if (fe is TextBlock tb)
|
||||
{
|
||||
fontFamily = tb.FontFamily;
|
||||
fontStyle = tb.FontStyle;
|
||||
fontWeight = tb.FontWeight;
|
||||
fontStretch = tb.FontStretch;
|
||||
foreground = tb.Foreground;
|
||||
flowDirection = tb.FlowDirection;
|
||||
}
|
||||
else if (fe is Label label)
|
||||
{
|
||||
fontFamily = label.FontFamily;
|
||||
fontStyle = label.FontStyle;
|
||||
fontWeight = label.FontWeight;
|
||||
fontStretch = label.FontStretch;
|
||||
foreground = label.Foreground;
|
||||
flowDirection = label.FlowDirection;
|
||||
}
|
||||
|
||||
var typeface = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);
|
||||
var formatted = new FormattedText(
|
||||
text,
|
||||
culture,
|
||||
flowDirection,
|
||||
typeface,
|
||||
fontSize,
|
||||
foreground,
|
||||
dpi.PixelsPerDip);
|
||||
|
||||
return formatted.WidthIncludingTrailingWhitespace;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,99 @@ namespace Ink_Canvas.Helpers
|
||||
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"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ UpdateChannel.Preview, new List<UpdateLineGroup>
|
||||
{
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "GitHub主线",
|
||||
VersionUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "bgithub备用",
|
||||
VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "kkgithub线路",
|
||||
VersionUrl = "https://kkgithub.com/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://kkgithub.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://kkgithub.com/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
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"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -111,6 +204,34 @@ namespace Ink_Canvas.Helpers
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +288,7 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -188,14 +309,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");
|
||||
@@ -213,20 +365,12 @@ namespace Ink_Canvas.Helpers
|
||||
.Select(x => x.group)
|
||||
.ToList();
|
||||
|
||||
// 将"inkeys"线路组插入到最前面(如果存在)
|
||||
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
|
||||
var inkeysGroup = orderedGroups.FirstOrDefault(g => g.GroupName == "inkeys");
|
||||
if (inkeysGroup != null)
|
||||
{
|
||||
orderedGroups.Remove(inkeysGroup);
|
||||
orderedGroups.Insert(0, inkeysGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | inkeys线路组已插入到首位");
|
||||
}
|
||||
|
||||
// 将"智教联盟"线路组插入到第二位(如果存在)
|
||||
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
|
||||
if (zhiJiaoGroup != null)
|
||||
{
|
||||
orderedGroups.Insert(1, zhiJiaoGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 智教联盟线路组已插入到第二位");
|
||||
LogHelper.WriteLogToFile("AutoUpdate | inkeys线路组已默认优先");
|
||||
}
|
||||
|
||||
if (orderedGroups.Count > 0)
|
||||
@@ -245,6 +389,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)
|
||||
{
|
||||
@@ -407,7 +592,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
string apiUrl = channel == UpdateChannel.Beta
|
||||
string apiUrl = (channel == UpdateChannel.Beta || channel == UpdateChannel.Preview)
|
||||
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases"
|
||||
: "https://api.github.com/repos/InkCanvasForClass/community/releases";
|
||||
using (var client = new HttpClient())
|
||||
@@ -449,28 +634,58 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
string apiUrl = channel == UpdateChannel.Beta
|
||||
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases/latest"
|
||||
: "https://api.github.com/repos/InkCanvasForClass/community/releases/latest";
|
||||
using (var client = new HttpClient())
|
||||
if (channel == UpdateChannel.Beta)
|
||||
{
|
||||
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();
|
||||
string releaseNotes = json["body"]?.ToString();
|
||||
string downloadUrl = json["assets"]?.First?["browser_download_url"]?.ToString();
|
||||
|
||||
// 解析发布时间
|
||||
DateTime? releaseTime = null;
|
||||
if (json["published_at"] != null && DateTime.TryParse(json["published_at"].ToString(), out DateTime parsedTime))
|
||||
string apiUrl = "https://api.github.com/repos/InkCanvasForClass/community-beta/releases";
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
releaseTime = parsedTime;
|
||||
}
|
||||
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);
|
||||
|
||||
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
|
||||
return (version, downloadUrl, releaseNotes, releaseTime);
|
||||
if (releases.Count > 0)
|
||||
{
|
||||
var latestRelease = releases[0];
|
||||
string version = latestRelease["tag_name"]?.ToString();
|
||||
string releaseNotes = latestRelease["body"]?.ToString();
|
||||
string downloadUrl = latestRelease["assets"]?.First?["browser_download_url"]?.ToString();
|
||||
|
||||
DateTime? releaseTime = null;
|
||||
if (latestRelease["published_at"] != null && DateTime.TryParse(latestRelease["published_at"].ToString(), out DateTime parsedTime))
|
||||
{
|
||||
releaseTime = parsedTime;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
|
||||
return (version, downloadUrl, releaseNotes, releaseTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string apiUrl = channel == UpdateChannel.Preview
|
||||
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases/latest"
|
||||
: "https://api.github.com/repos/InkCanvasForClass/community/releases/latest";
|
||||
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();
|
||||
string releaseNotes = json["body"]?.ToString();
|
||||
string downloadUrl = json["assets"]?.First?["browser_download_url"]?.ToString();
|
||||
|
||||
DateTime? releaseTime = null;
|
||||
if (json["published_at"] != null && DateTime.TryParse(json["published_at"].ToString(), out DateTime parsedTime))
|
||||
{
|
||||
releaseTime = parsedTime;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
|
||||
return (version, downloadUrl, releaseNotes, releaseTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -671,23 +886,13 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
SaveDownloadStatus(false);
|
||||
|
||||
// 优先尝试"inkeys"线路组和"智教联盟"线路组
|
||||
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
|
||||
// 优先尝试"inkeys"线路组
|
||||
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
|
||||
if (inkeysGroup != null || zhiJiaoGroup != null)
|
||||
if (inkeysGroup != null)
|
||||
{
|
||||
var priorityGroups = new List<UpdateLineGroup>();
|
||||
if (inkeysGroup != null)
|
||||
{
|
||||
priorityGroups.Add(inkeysGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试inkeys线路组");
|
||||
}
|
||||
if (zhiJiaoGroup != null)
|
||||
{
|
||||
priorityGroups.Add(zhiJiaoGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试智教联盟线路组");
|
||||
}
|
||||
groups = priorityGroups.Concat(groups.Where(g => g.GroupName != "智教联盟" && g.GroupName != "inkeys")).ToList();
|
||||
groups.Remove(inkeysGroup);
|
||||
groups.Insert(0, inkeysGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试inkeys线路组");
|
||||
}
|
||||
|
||||
// 依次尝试每个线路组
|
||||
@@ -925,7 +1130,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 清理可能损坏的分块文件
|
||||
if (File.Exists(tempPath))
|
||||
{
|
||||
try { File.Delete(tempPath); } catch { }
|
||||
try { File.Delete(tempPath); } catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
|
||||
// 增加重试间隔,避免频繁重试
|
||||
@@ -1111,11 +1316,17 @@ namespace Ink_Canvas.Helpers
|
||||
return resp.Content.Headers.ContentLength.Value;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 保存下载状态
|
||||
/// <summary>
|
||||
/// 将下载完成状态写入预定义的状态文件以供后续检查。
|
||||
/// </summary>
|
||||
/// <param name="isSuccess">指示下载是否成功;将以字符串形式写入状态文件("True" 或 "False")。</param>
|
||||
/// <remarks>
|
||||
/// 如果状态文件路径为空则不执行任何操作;方法内部捕获异常并记录日志,不会向调用方抛出异常。
|
||||
/// </remarks>
|
||||
private static void SaveDownloadStatus(bool isSuccess)
|
||||
{
|
||||
try
|
||||
@@ -1136,11 +1347,32 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
// 安装新版本应用 - 优化版本,不使用命令行
|
||||
/// <summary>
|
||||
/// 安装指定版本的更新包并启动新版本进程以完成替换,然后退出当前应用程序。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该方法会临时将 App.IsUpdateInstalling 置为 true、尝试关闭进程保护(并在结束时还原)、在必要时备份当前设置、解压更新 ZIP、启动解压后的新可执行文件(以更新模式传递旧进程 ID、解压路径和目标路径等参数),并在新进程启动后关闭当前进程。方法会记录日志并在遇到错误时安全退出相应步骤,但不会抛出异常给调用方以外的上下文。</remarks>
|
||||
/// <param name="version">要安装的版本号,用于定位更新包文件名(例如 InkCanvasForClass.CE.{version}.zip)。</param>
|
||||
/// <param name="isInSilence">指示是否以静默模式启动新版本(影响传递给新进程的参数和可能的用户提示)。</param>
|
||||
public static void InstallNewVersionApp(string version, bool isInSilence)
|
||||
{
|
||||
bool wasProcessProtectionEnabled = false;
|
||||
try
|
||||
{
|
||||
wasProcessProtectionEnabled = ProcessProtectionManager.Enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
App.IsUpdateInstalling = true;
|
||||
if (wasProcessProtectionEnabled)
|
||||
{
|
||||
try { ProcessProtectionManager.SetEnabled(false); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
// 在更新前备份设置文件
|
||||
try
|
||||
{
|
||||
@@ -1226,7 +1458,7 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 开始解压ZIP文件到: {extractPath}");
|
||||
ZipFile.ExtractToDirectory(zipFilePath, extractPath);
|
||||
SafeZipExtractor.ExtractZipSafely(zipFilePath, extractPath, overwrite: true);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | ZIP文件解压完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1301,6 +1533,18 @@ namespace Ink_Canvas.Helpers
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 内部异常: {ex.InnerException.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 确保无论更新成功还是失败,都恢复标志位和进程保护状态
|
||||
App.IsUpdateInstalling = false;
|
||||
try
|
||||
{
|
||||
ProcessProtectionManager.SetEnabled(wasProcessProtectionEnabled);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理更新模式的启动参数
|
||||
@@ -1854,16 +2098,16 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
foreach (string file in Directory.GetFiles(updatesFolderPath, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
try { File.Delete(file); } catch { }
|
||||
try { File.Delete(file); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
foreach (string dir in Directory.GetDirectories(updatesFolderPath))
|
||||
{
|
||||
try { Directory.Delete(dir, true); } catch { }
|
||||
try { Directory.Delete(dir, true); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
try { Directory.Delete(updatesFolderPath, true); } catch { }
|
||||
try { Directory.Delete(updatesFolderPath, true); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
// 版本修复方法,强制下载并安装指定通道的最新版本
|
||||
@@ -1913,7 +2157,7 @@ namespace Ink_Canvas.Helpers
|
||||
var result = new List<(string, string, string)>();
|
||||
try
|
||||
{
|
||||
string apiUrl = channel == UpdateChannel.Beta
|
||||
string apiUrl = (channel == UpdateChannel.Beta || channel == UpdateChannel.Preview)
|
||||
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases"
|
||||
: "https://api.github.com/repos/InkCanvasForClass/community/releases";
|
||||
using (var client = new HttpClient())
|
||||
@@ -2072,4 +2316,4 @@ namespace Ink_Canvas.Helpers
|
||||
return currentTime >= StartTime || currentTime <= EndTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
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>
|
||||
/// 上传队列项数据(用于序列化)
|
||||
/// </summary>
|
||||
public 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>
|
||||
public class UploadQueueItem
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
public int RetryCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通用上传队列基类
|
||||
/// </summary>
|
||||
public abstract class BaseUploadQueue : IDisposable
|
||||
{
|
||||
protected const int BATCH_SIZE = 10; // 批量上传大小
|
||||
protected const int MAX_RETRY_COUNT = 3; // 最大重试次数
|
||||
|
||||
/// <summary>
|
||||
/// 上传队列
|
||||
/// </summary>
|
||||
protected readonly ConcurrentQueue<UploadQueueItem> _uploadQueue = new ConcurrentQueue<UploadQueueItem>();
|
||||
|
||||
/// <summary>
|
||||
/// 队列处理锁,防止并发处理
|
||||
/// </summary>
|
||||
protected readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// 队列保存锁,防止并发保存
|
||||
/// </summary>
|
||||
protected readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// 是否已初始化队列
|
||||
/// </summary>
|
||||
protected bool _isQueueInitialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已释放资源
|
||||
/// </summary>
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// 队列文件名
|
||||
/// </summary>
|
||||
protected abstract string QueueFileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 允许的文件扩展名
|
||||
/// </summary>
|
||||
protected virtual HashSet<string> AllowedExtensions => new HashSet<string> { ".png", ".icstk", ".xml", ".zip" };
|
||||
|
||||
/// <summary>
|
||||
/// 获取队列文件路径
|
||||
/// </summary>
|
||||
protected string GetQueueFilePath()
|
||||
{
|
||||
var configsDir = Path.Combine(App.RootPath, "Configs");
|
||||
if (!Directory.Exists(configsDir))
|
||||
{
|
||||
Directory.CreateDirectory(configsDir);
|
||||
}
|
||||
return Path.Combine(configsDir, QueueFileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大文件大小
|
||||
/// </summary>
|
||||
/// <param name="extension">文件扩展名</param>
|
||||
/// <returns>最大文件大小(字节)</returns>
|
||||
protected virtual long GetMaxFileSize(string extension)
|
||||
{
|
||||
return extension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化上传队列
|
||||
/// </summary>
|
||||
public 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;
|
||||
}
|
||||
|
||||
// 验证文件格式和大小
|
||||
if (!IsValidFile(item.FilePath))
|
||||
{
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 恢复队列项
|
||||
_uploadQueue.Enqueue(new UploadQueueItem
|
||||
{
|
||||
FilePath = item.FilePath,
|
||||
RetryCount = item.RetryCount
|
||||
});
|
||||
restoredCount++;
|
||||
}
|
||||
|
||||
_isQueueInitialized = true;
|
||||
|
||||
if (restoredCount > 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
|
||||
// 如果恢复了队列,触发处理
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessUploadQueueAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 恢复上传队列后处理时出错: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (skippedCount > 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
_isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存队列到文件
|
||||
/// </summary>
|
||||
protected async Task SaveQueueToFileAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await _queueSaveLock.WaitAsync(1000, cancellationToken)) // 最多等待1秒
|
||||
{
|
||||
return; // 如果无法获取锁,跳过保存(避免阻塞)
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var queueData = new List<UploadQueueItemData>();
|
||||
|
||||
// 将队列转换为可序列化的格式
|
||||
foreach (var item in _uploadQueue)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
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";
|
||||
ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
|
||||
{
|
||||
File.WriteAllText(tempFilePath, jsonContent);
|
||||
if (File.Exists(queueFilePath))
|
||||
File.Delete(queueFilePath);
|
||||
File.Move(tempFilePath, queueFilePath);
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 取消操作,静默处理
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_queueSaveLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空队列文件
|
||||
/// </summary>
|
||||
protected void ClearQueueFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
var queueFilePath = GetQueueFilePath();
|
||||
ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
|
||||
{
|
||||
if (File.Exists(queueFilePath))
|
||||
File.WriteAllText(queueFilePath, "[]");
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将文件加入上传队列
|
||||
/// </summary>
|
||||
protected void EnqueueFile(string filePath, int retryCount = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_uploadQueue.Enqueue(new UploadQueueItem
|
||||
{
|
||||
FilePath = filePath,
|
||||
RetryCount = retryCount
|
||||
});
|
||||
|
||||
// 异步保存队列到文件
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await SaveQueueToFileAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 取消操作,静默处理
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 保存上传队列时出错(后台任务): {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
// 触发队列处理
|
||||
_ = ProcessUploadQueueAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理上传队列,批量上传文件
|
||||
/// </summary>
|
||||
protected async Task ProcessUploadQueueAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 使用信号量防止并发处理
|
||||
if (!await _queueProcessingLock.WaitAsync(0, cancellationToken))
|
||||
{
|
||||
return; // 已有处理任务在运行
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var filesToUpload = new List<UploadQueueItem>();
|
||||
|
||||
// 从队列中取出最多BATCH_SIZE个文件
|
||||
int count = 0;
|
||||
while (count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 再次检查文件是否存在
|
||||
if (File.Exists(item.FilePath) && IsValidFile(item.FilePath))
|
||||
{
|
||||
filesToUpload.Add(item);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToUpload.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否启用
|
||||
if (!IsUploadEnabled())
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:上传未启用", LogHelper.LogType.Error);
|
||||
// 将文件重新加入队列
|
||||
foreach (var item in filesToUpload)
|
||||
{
|
||||
EnqueueFile(item.FilePath, item.RetryCount, cancellationToken);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发上传所有文件,并处理失败重试
|
||||
var uploadTasks = filesToUpload.Select(async item =>
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var success = await UploadFileInternalAsync(item.FilePath, cancellationToken);
|
||||
if (!success)
|
||||
{
|
||||
// 检查是否是可重试的错误
|
||||
if (IsRetryableError(item.FilePath))
|
||||
{
|
||||
// 检查重试次数
|
||||
if (item.RetryCount < MAX_RETRY_COUNT)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
|
||||
EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 取消操作,将文件重新加入队列
|
||||
EnqueueFile(item.FilePath, item.RetryCount, cancellationToken);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 检查是否是可重试的错误(超时、网络错误等)
|
||||
var errorMessage = ex.Message.ToLower();
|
||||
bool isRetryable = errorMessage.Contains("超时") ||
|
||||
errorMessage.Contains("timeout") ||
|
||||
errorMessage.Contains("网络错误") ||
|
||||
errorMessage.Contains("network") ||
|
||||
errorMessage.Contains("408") || // 请求超时
|
||||
errorMessage.Contains("423") || // 资源锁定
|
||||
errorMessage.Contains("429") || // 请求过多
|
||||
errorMessage.Contains("500") || // 服务器错误
|
||||
errorMessage.Contains("502") || // 网关错误
|
||||
errorMessage.Contains("503") || // 服务不可用
|
||||
errorMessage.Contains("504"); // 网关超时
|
||||
|
||||
if (isRetryable && IsRetryableError(item.FilePath))
|
||||
{
|
||||
// 检查重试次数
|
||||
if (item.RetryCount < MAX_RETRY_COUNT)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
|
||||
EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败(不可重试): {Path.GetFileName(item.FilePath)} - {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
await Task.WhenAll(uploadTasks).ConfigureAwait(false);
|
||||
|
||||
// 上传完成后保存队列状态
|
||||
await SaveQueueToFileAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 检查队列中是否还有文件,如果有就继续处理
|
||||
if (_uploadQueue.Count > 0)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessUploadQueueAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 继续处理上传队列时出错: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_queueProcessingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证文件是否有效
|
||||
/// </summary>
|
||||
protected virtual bool IsValidFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileExtension = Path.GetExtension(filePath).ToLower();
|
||||
if (!AllowedExtensions.Contains(fileExtension))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
long maxSize = GetMaxFileSize(fileExtension);
|
||||
if (fileInfo.Length > maxSize)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件过大({fileInfo.Length / 1024 / 1024:F2}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断错误是否可重试
|
||||
/// </summary>
|
||||
protected bool IsRetryableError(string filePath)
|
||||
{
|
||||
// 检查文件是否存在
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return false; // 文件不存在,不可重试
|
||||
}
|
||||
|
||||
// 检查文件是否有效
|
||||
if (!IsValidFile(filePath))
|
||||
{
|
||||
return false; // 文件无效,不可重试
|
||||
}
|
||||
|
||||
// 检查是否启用
|
||||
if (!IsUploadEnabled())
|
||||
{
|
||||
return false; // 上传未启用,不可重试
|
||||
}
|
||||
|
||||
// 其他错误(超时、网络错误等)可以重试
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查上传是否启用
|
||||
/// </summary>
|
||||
protected abstract bool IsUploadEnabled();
|
||||
|
||||
/// <summary>
|
||||
/// 内部上传方法,执行实际上传操作
|
||||
/// </summary>
|
||||
protected abstract Task<bool> UploadFileInternalAsync(string filePath, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 异步上传文件
|
||||
/// </summary>
|
||||
public async Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 检查是否启用
|
||||
if (!IsUploadEnabled())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 基本验证
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsValidFile(filePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 确保队列已初始化
|
||||
if (!_isQueueInitialized)
|
||||
{
|
||||
InitializeQueue();
|
||||
}
|
||||
|
||||
// 加入队列
|
||||
EnqueueFile(filePath, 0, cancellationToken);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传被取消: {Path.GetFileName(filePath)}", LogHelper.LogType.Event);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[{GetType().Name}] 加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
/// <param name="disposing">是否手动释放</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_queueProcessingLock?.Dispose();
|
||||
_queueSaveLock?.Dispose();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 析构函数
|
||||
/// </summary>
|
||||
~BaseUploadQueue()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,13 +40,13 @@ namespace Ink_Canvas.Helpers
|
||||
public int ResolutionWidth
|
||||
{
|
||||
get => _resolutionWidth;
|
||||
set => _resolutionWidth = Math.Max(320, Math.Min(1920, value));
|
||||
set => _resolutionWidth = Math.Max(320, Math.Min(3840, value));
|
||||
}
|
||||
|
||||
public int ResolutionHeight
|
||||
{
|
||||
get => _resolutionHeight;
|
||||
set => _resolutionHeight = Math.Max(240, Math.Min(1080, value));
|
||||
set => _resolutionHeight = Math.Max(240, Math.Min(2160, value));
|
||||
}
|
||||
|
||||
public CameraService()
|
||||
@@ -281,8 +281,16 @@ namespace Ink_Canvas.Helpers
|
||||
// 应用旋转
|
||||
Bitmap rotatedFrame = ApplyRotation(sourceFrame);
|
||||
|
||||
// 应用分辨率调整
|
||||
_currentFrame = ResizeImage(rotatedFrame, _resolutionWidth, _resolutionHeight);
|
||||
int targetWidth = _resolutionWidth;
|
||||
int targetHeight = _resolutionHeight;
|
||||
|
||||
if (_rotationAngle == 1 || _rotationAngle == 3)
|
||||
{
|
||||
targetWidth = _resolutionHeight;
|
||||
targetHeight = _resolutionWidth;
|
||||
}
|
||||
|
||||
_currentFrame = ResizeImageWithAspectRatio(rotatedFrame, targetWidth, targetHeight);
|
||||
|
||||
rotatedFrame?.Dispose();
|
||||
}
|
||||
@@ -357,6 +365,33 @@ namespace Ink_Canvas.Helpers
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public class ComPPTLinkManager : IPPTLinkManager
|
||||
{
|
||||
private readonly PPTManager _inner;
|
||||
|
||||
public ComPPTLinkManager()
|
||||
{
|
||||
_inner = new PPTManager();
|
||||
|
||||
_inner.SlideShowBegin += wn => SlideShowBegin?.Invoke(wn);
|
||||
_inner.SlideShowNextSlide += wn => SlideShowNextSlide?.Invoke(wn);
|
||||
_inner.SlideShowEnd += pres => SlideShowEnd?.Invoke(pres);
|
||||
_inner.PresentationOpen += pres => PresentationOpen?.Invoke(pres);
|
||||
_inner.PresentationClose += pres => PresentationClose?.Invoke(pres);
|
||||
_inner.PPTConnectionChanged += connected => PPTConnectionChanged?.Invoke(connected);
|
||||
_inner.SlideShowStateChanged += inSlideShow => SlideShowStateChanged?.Invoke(inSlideShow);
|
||||
}
|
||||
|
||||
#region IPPTLinkManager 事件
|
||||
public event Action<object> SlideShowBegin;
|
||||
public event Action<object> SlideShowNextSlide;
|
||||
public event Action<object> SlideShowEnd;
|
||||
public event Action<object> PresentationOpen;
|
||||
public event Action<object> PresentationClose;
|
||||
public event Action<bool> PPTConnectionChanged;
|
||||
public event Action<bool> SlideShowStateChanged;
|
||||
#endregion
|
||||
|
||||
#region IPPTLinkManager 属性
|
||||
public bool IsConnected => _inner.IsConnected;
|
||||
|
||||
public bool IsInSlideShow => _inner.IsInSlideShow;
|
||||
|
||||
public bool IsSupportWPS
|
||||
{
|
||||
get => _inner.IsSupportWPS;
|
||||
set => _inner.IsSupportWPS = value;
|
||||
}
|
||||
|
||||
public bool SkipAnimationsWhenNavigating
|
||||
{
|
||||
get => _inner.SkipAnimationsWhenNavigating;
|
||||
set => _inner.SkipAnimationsWhenNavigating = value;
|
||||
}
|
||||
|
||||
public int SlidesCount => _inner.SlidesCount;
|
||||
|
||||
public object PPTApplication => _inner.PPTApplication;
|
||||
#endregion
|
||||
|
||||
#region 生命周期管理
|
||||
/// <summary>
|
||||
/// 开始监控本地 PowerPoint 的连接与运行状态,并在状态变化时触发相应事件。
|
||||
/// </summary>
|
||||
public void StartMonitoring() => _inner.StartMonitoring();
|
||||
|
||||
/// <summary>
|
||||
/// 停止对 PowerPoint 的监控,断开当前连接并停止触发相关事件。
|
||||
/// </summary>
|
||||
public void StopMonitoring() => _inner.StopMonitoring();
|
||||
|
||||
/// <summary>
|
||||
/// 强制断开当前 COM PPT 连接并停止对其监控,同时写入事件日志。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 会向日志记录一条事件信息并调用内部管理器停止监控;该方法不会重新启动监控或重新初始化内部管理器实例。
|
||||
/// </remarks>
|
||||
public void ReloadConnection()
|
||||
{
|
||||
LogHelper.WriteLogToFile("COM PPT 执行热重载:强制断开并重新连接", LogHelper.LogType.Event);
|
||||
_inner.StopMonitoring();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 放映控制
|
||||
public bool TryStartSlideShow() => _inner.TryStartSlideShow();
|
||||
|
||||
public bool TryEndSlideShow() => _inner.TryEndSlideShow();
|
||||
#endregion
|
||||
|
||||
#region 导航控制
|
||||
public bool TryNavigateToSlide(int slideNumber) => _inner.TryNavigateToSlide(slideNumber);
|
||||
|
||||
public bool TryNavigateNext() => _inner.TryNavigateNext();
|
||||
|
||||
public bool TryNavigatePrevious() => _inner.TryNavigatePrevious();
|
||||
#endregion
|
||||
|
||||
#region 查询
|
||||
public int GetCurrentSlideNumber() => _inner.GetCurrentSlideNumber();
|
||||
|
||||
public string GetPresentationName() => _inner.GetPresentationName();
|
||||
|
||||
public bool TryShowSlideNavigation() => _inner.TryShowSlideNavigation();
|
||||
|
||||
public object GetCurrentActivePresentation() => _inner.GetCurrentActivePresentation();
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
_inner?.Dispose();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 提供多配置文件保存、切换与热重载支持。
|
||||
/// 方案保存在 Configs/Profiles 目录下,当前生效的配置仍为 Configs/Settings.json。
|
||||
/// </summary>
|
||||
public static class ConfigProfileManager
|
||||
{
|
||||
private static readonly string ProfilesDir = Path.Combine(App.RootPath, "Configs", "Profiles");
|
||||
private static readonly string SettingsFilePath = Path.Combine(App.RootPath, "Configs", "Settings.json");
|
||||
private const string ProfileExtension = ".json";
|
||||
|
||||
/// <summary>将配置文件名称转为安全文件名(去掉非法字符)。</summary>
|
||||
private static string ToSafeFileName(string profileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(profileName)) return "未命名";
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
var name = string.Join("_", profileName.Trim().Split(invalid, StringSplitOptions.RemoveEmptyEntries));
|
||||
return string.IsNullOrEmpty(name) ? "未命名" : name;
|
||||
}
|
||||
|
||||
/// <summary>确保配置文件目录存在。</summary>
|
||||
public static void EnsureProfilesDirectory()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(ProfilesDir))
|
||||
{
|
||||
ProcessProtectionManager.WithWriteAccess(ProfilesDir, () => Directory.CreateDirectory(ProfilesDir));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建配置文件目录失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取所有配置文件名称(不含扩展名),按名称排序。</summary>
|
||||
public static IReadOnlyList<string> ListProfileNames()
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureProfilesDirectory();
|
||||
if (!Directory.Exists(ProfilesDir)) return Array.Empty<string>();
|
||||
var files = Directory.GetFiles(ProfilesDir, "*" + ProfileExtension);
|
||||
return files
|
||||
.Select(f => Path.GetFileNameWithoutExtension(f))
|
||||
.Where(n => !string.IsNullOrEmpty(n))
|
||||
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"列举配置文件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取某配置文件对应的文件路径。</summary>
|
||||
public static string GetProfilePath(string profileName)
|
||||
{
|
||||
var safe = ToSafeFileName(profileName);
|
||||
return Path.Combine(ProfilesDir, safe + ProfileExtension);
|
||||
}
|
||||
|
||||
/// <summary>将当前配置的 JSON 内容保存为指定名称的配置文件。</summary>
|
||||
/// <param name="profileName">配置文件显示名称(会转为安全文件名)。</param>
|
||||
/// <param name="settingsJson">已序列化好的 Settings JSON 字符串。</param>
|
||||
/// <returns>成功返回 true。</returns>
|
||||
public static bool SaveAsProfile(string profileName, string settingsJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(settingsJson))
|
||||
{
|
||||
LogHelper.WriteLogToFile("配置文件保存失败:内容为空", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
EnsureProfilesDirectory();
|
||||
var path = GetProfilePath(profileName);
|
||||
ProcessProtectionManager.WithWriteAccess(path, () => File.WriteAllText(path, settingsJson));
|
||||
LogHelper.WriteLogToFile($"配置文件已保存: {ToSafeFileName(profileName)}", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存配置文件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>将指定配置文件应用到当前配置(覆盖 Configs/Settings.json),供主窗口随后热重载。</summary>
|
||||
/// <param name="profileName">配置文件名称(与 ListProfileNames 中一致,或与保存时使用的显示名一致)。</param>
|
||||
/// <returns>成功返回 true;文件不存在或复制失败返回 false。</returns>
|
||||
public static bool ApplyProfile(string profileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = GetProfilePath(profileName);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"配置文件文件不存在: {path}", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
var json = File.ReadAllText(path);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
LogHelper.WriteLogToFile("配置文件内容为空", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
// 可选:校验是否为合法 Settings JSON
|
||||
try
|
||||
{
|
||||
JsonConvert.DeserializeObject<Settings>(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"配置文件格式无效: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
var configsDir = Path.GetDirectoryName(SettingsFilePath);
|
||||
if (!string.IsNullOrEmpty(configsDir) && !Directory.Exists(configsDir))
|
||||
{
|
||||
ProcessProtectionManager.WithWriteAccess(configsDir, () => Directory.CreateDirectory(configsDir));
|
||||
}
|
||||
ProcessProtectionManager.WithWriteAccess(SettingsFilePath, () => File.WriteAllText(SettingsFilePath, json));
|
||||
LogHelper.WriteLogToFile($"已应用配置文件: {profileName}(请热重载以生效)", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用配置文件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>删除指定名称的配置文件。</summary>
|
||||
public static bool DeleteProfile(string profileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = GetProfilePath(profileName);
|
||||
if (!File.Exists(path)) return true;
|
||||
ProcessProtectionManager.WithWriteAccess(path, () => File.Delete(path));
|
||||
LogHelper.WriteLogToFile($"已删除配置文件: {profileName}", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"删除配置文件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,4 +135,21 @@ namespace Ink_Canvas.Converter
|
||||
return Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
public class RippleEffectTranslationConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is double d)
|
||||
{
|
||||
return -d / 2;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using OSVersionExtension;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -46,20 +47,40 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 尝试从主文件读取设备ID
|
||||
string deviceId = LoadDeviceIdFromFile(DeviceIdFilePath);
|
||||
if (!string.IsNullOrEmpty(deviceId))
|
||||
// 计算当前设备的硬件指纹
|
||||
string currentHardwareFingerprint = GenerateHardwareFingerprint();
|
||||
|
||||
// 1. 尝试从主文件读取设备ID及其硬件指纹
|
||||
var storedInfo = LoadDeviceIdFromFile(DeviceIdFilePath);
|
||||
if (storedInfo != null && !string.IsNullOrEmpty(storedInfo.DeviceId) && IsValidDeviceId(storedInfo.DeviceId))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 从主文件读取设备ID: {deviceId}");
|
||||
return deviceId;
|
||||
if (!string.IsNullOrEmpty(storedInfo.HardwareFingerprint))
|
||||
{
|
||||
if (storedInfo.HardwareFingerprint == currentHardwareFingerprint)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 从主文件读取设备ID且硬件信息一致: {storedInfo.DeviceId}");
|
||||
return storedInfo.DeviceId;
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("DeviceIdentifier | 检测到当前硬件信息与保存的设备ID不一致,将重新生成设备ID");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile("DeviceIdentifier | 检测到旧格式设备ID文件(无硬件信息),将基于当前硬件重新生成设备ID并升级文件格式");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 生成新的设备ID
|
||||
string newDeviceId = GenerateDeviceId();
|
||||
// 2. 基于当前硬件指纹生成新的设备ID
|
||||
string newDeviceId = GenerateDeviceIdFromFingerprint(currentHardwareFingerprint);
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 生成新设备ID: {newDeviceId}");
|
||||
|
||||
// 3. 保存到主文件
|
||||
SaveDeviceIdToFile(DeviceIdFilePath, newDeviceId);
|
||||
// 3. 保存到主文件(包含硬件指纹)
|
||||
var newInfo = new DeviceIdInfo
|
||||
{
|
||||
DeviceId = newDeviceId,
|
||||
HardwareFingerprint = currentHardwareFingerprint
|
||||
};
|
||||
SaveDeviceIdToFile(DeviceIdFilePath, newInfo);
|
||||
|
||||
return newDeviceId;
|
||||
}
|
||||
@@ -79,143 +100,9 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
// 收集硬件信息
|
||||
var hardwareInfo = new StringBuilder();
|
||||
|
||||
// 使用反射获取硬件信息,避免直接引用System.Management
|
||||
try
|
||||
{
|
||||
// 尝试加载System.Management程序集
|
||||
var assembly = Assembly.Load("System.Management");
|
||||
if (assembly != null)
|
||||
{
|
||||
// CPU信息
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT ProcessorId FROM Win32_Processor");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var processorId = indexer.GetValue(obj, new object[] { "ProcessorId" });
|
||||
hardwareInfo.Append(processorId?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 主板序列号
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BaseBoard");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
|
||||
hardwareInfo.Append(serialNumber?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch { }
|
||||
|
||||
// BIOS序列号
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BIOS");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
|
||||
hardwareInfo.Append(serialNumber?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 主硬盘序列号
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
|
||||
hardwareInfo.Append(serialNumber?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 如果硬件信息不足,添加系统信息
|
||||
if (hardwareInfo.Length < 10)
|
||||
{
|
||||
hardwareInfo.Append(Environment.MachineName);
|
||||
hardwareInfo.Append(Environment.UserName);
|
||||
hardwareInfo.Append(Environment.OSVersion);
|
||||
}
|
||||
|
||||
// 生成哈希
|
||||
using (var sha256 = SHA256.Create())
|
||||
{
|
||||
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(hardwareInfo.ToString()));
|
||||
string hashString = BitConverter.ToString(hashBytes).Replace("-", "");
|
||||
|
||||
// 取前25个字符,确保唯一性
|
||||
string deviceId = hashString.Substring(0, 25);
|
||||
|
||||
// 添加校验位(第25位)
|
||||
int checksum = 0;
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
checksum += Convert.ToInt32(deviceId[i]);
|
||||
}
|
||||
checksum %= 36; // 0-9, A-Z
|
||||
char checksumChar = checksum < 10 ? (char)(checksum + '0') : (char)(checksum - 10 + 'A');
|
||||
|
||||
return deviceId.Substring(0, 24) + checksumChar;
|
||||
}
|
||||
// 基于当前硬件指纹生成设备ID
|
||||
string hardwareFingerprint = GenerateHardwareFingerprint();
|
||||
return GenerateDeviceIdFromFingerprint(hardwareFingerprint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -224,6 +111,157 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成当前设备的硬件指纹字符串(用于生成和校验设备ID)
|
||||
/// </summary>
|
||||
private static string GenerateHardwareFingerprint()
|
||||
{
|
||||
// 收集硬件信息
|
||||
var hardwareInfo = new StringBuilder();
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.Load("System.Management");
|
||||
if (assembly != null)
|
||||
{
|
||||
// CPU信息
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT ProcessorId FROM Win32_Processor");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var processorId = indexer.GetValue(obj, new object[] { "ProcessorId" });
|
||||
hardwareInfo.Append(processorId?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
// 主板序列号
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BaseBoard");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
|
||||
hardwareInfo.Append(serialNumber?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
// BIOS序列号
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BIOS");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
|
||||
hardwareInfo.Append(serialNumber?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
// 主硬盘序列号
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
|
||||
hardwareInfo.Append(serialNumber?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (hardwareInfo.Length < 10)
|
||||
{
|
||||
hardwareInfo.Append(Environment.MachineName);
|
||||
hardwareInfo.Append(Environment.UserName);
|
||||
hardwareInfo.Append(Environment.OSVersion);
|
||||
}
|
||||
|
||||
return hardwareInfo.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于硬件指纹生成25字符的设备ID
|
||||
/// </summary>
|
||||
private static string GenerateDeviceIdFromFingerprint(string hardwareFingerprint)
|
||||
{
|
||||
// 生成哈希
|
||||
using (var sha256 = SHA256.Create())
|
||||
{
|
||||
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(hardwareFingerprint ?? string.Empty));
|
||||
string hashString = BitConverter.ToString(hashBytes).Replace("-", "");
|
||||
|
||||
// 取前25个字符,确保唯一性
|
||||
string deviceId = hashString.Substring(0, 25);
|
||||
|
||||
// 添加校验位(第25位)
|
||||
int checksum = 0;
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
checksum += Convert.ToInt32(deviceId[i]);
|
||||
}
|
||||
checksum %= 36; // 0-9, A-Z
|
||||
char checksumChar = checksum < 10 ? (char)(checksum + '0') : (char)(checksum - 10 + 'A');
|
||||
|
||||
return deviceId.Substring(0, 24) + checksumChar;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成备用设备ID(基于时间戳)
|
||||
/// </summary>
|
||||
@@ -282,16 +320,33 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 从文件加载设备ID
|
||||
/// </summary>
|
||||
private static string LoadDeviceIdFromFile(string filePath)
|
||||
private static DeviceIdInfo LoadDeviceIdFromFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
string content = File.ReadAllText(filePath).Trim();
|
||||
|
||||
try
|
||||
{
|
||||
var info = JsonConvert.DeserializeObject<DeviceIdInfo>(content);
|
||||
if (info != null && !string.IsNullOrEmpty(info.DeviceId) && IsValidDeviceId(info.DeviceId))
|
||||
{
|
||||
return info;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (IsValidDeviceId(content))
|
||||
{
|
||||
return content;
|
||||
return new DeviceIdInfo
|
||||
{
|
||||
DeviceId = content,
|
||||
HardwareFingerprint = null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,7 +360,12 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 保存设备ID到文件
|
||||
/// </summary>
|
||||
private static void SaveDeviceIdToFile(string filePath, string deviceId)
|
||||
/// <remarks>
|
||||
/// 将设备标识信息以格式化的 JSON 写入指定文件,并确保目标目录存在;在失败时记录错误但不抛出异常。
|
||||
/// </remarks>
|
||||
/// <param name="filePath">目标文件的完整路径,用于保存设备标识信息。</param>
|
||||
/// <param name="info">要保存的设备标识信息对象(包含 DeviceId 和 可选的硬件指纹)。</param>
|
||||
private static void SaveDeviceIdToFile(string filePath, DeviceIdInfo info)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -313,10 +373,11 @@ namespace Ink_Canvas.Helpers
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
ProcessProtectionManager.WithWriteAccess(directory, () => Directory.CreateDirectory(directory));
|
||||
}
|
||||
|
||||
File.WriteAllText(filePath, deviceId);
|
||||
string json = JsonConvert.SerializeObject(info, Formatting.Indented);
|
||||
ProcessProtectionManager.WithWriteAccess(filePath, () => File.WriteAllText(filePath, json));
|
||||
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 设备ID已保存到: {filePath}");
|
||||
}
|
||||
@@ -326,6 +387,14 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private class DeviceIdInfo
|
||||
{
|
||||
[JsonProperty("deviceId")]
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
[JsonProperty("hardwareFingerprint")]
|
||||
public string HardwareFingerprint { get; set; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -342,7 +411,9 @@ namespace Ink_Canvas.Helpers
|
||||
[JsonProperty("launchCount")]
|
||||
public int LaunchCount { get; set; }
|
||||
|
||||
// 新的秒级精度字段
|
||||
[JsonProperty("systemVersion")]
|
||||
public string SystemVersion { get; set; }
|
||||
|
||||
[JsonProperty("totalUsageSeconds")]
|
||||
public long TotalUsageSeconds { get; set; }
|
||||
|
||||
@@ -363,6 +434,9 @@ namespace Ink_Canvas.Helpers
|
||||
[JsonProperty("lastModified")]
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
[JsonProperty("updateChannel")]
|
||||
public Ink_Canvas.UpdateChannel UpdateChannel { get; set; } = Ink_Canvas.UpdateChannel.Release;
|
||||
|
||||
// 每周统计数据(秒级精度)
|
||||
[JsonProperty("weeklyLaunchCount")]
|
||||
public int WeeklyLaunchCount { get; set; }
|
||||
@@ -501,6 +575,20 @@ namespace Ink_Canvas.Helpers
|
||||
// 记录每周启动次数
|
||||
stats.RecordWeeklyLaunch();
|
||||
|
||||
try
|
||||
{
|
||||
var osName = OSVersion.GetOperatingSystem();
|
||||
var osVersion = OSVersion.GetOSVersion();
|
||||
string versionText = osVersion != null
|
||||
? $"{osName} {osVersion.Version}"
|
||||
: osName.ToString();
|
||||
stats.SystemVersion = versionText;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 刷新系统版本信息失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
// 计算使用频率
|
||||
CalculateUsageFrequency(stats);
|
||||
|
||||
@@ -536,8 +624,6 @@ namespace Ink_Canvas.Helpers
|
||||
// 更新秒级精度数据
|
||||
stats.TotalUsageSeconds += sessionSeconds;
|
||||
|
||||
|
||||
|
||||
// 记录每周使用时长(秒级精度)
|
||||
stats.RecordWeeklyUsage(sessionSeconds);
|
||||
|
||||
@@ -545,7 +631,6 @@ namespace Ink_Canvas.Helpers
|
||||
if (stats.LaunchCount > 0)
|
||||
{
|
||||
stats.AverageSessionSeconds = (double)stats.TotalUsageSeconds / stats.LaunchCount;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +650,20 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetSystemVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stats = LoadUsageStats();
|
||||
return stats.SystemVersion;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 获取系统版本失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算使用频率和更新优先级(基于真实的每周统计数据)
|
||||
/// 通过多维度评分系统确定用户类型:高频(≥80分)、中频(40-79分)、低频(<40分)
|
||||
@@ -905,7 +1004,7 @@ namespace Ink_Canvas.Helpers
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
ProcessProtectionManager.WithWriteAccess(directory, () => Directory.CreateDirectory(directory));
|
||||
}
|
||||
|
||||
string json = JsonConvert.SerializeObject(stats, Formatting.Indented);
|
||||
@@ -929,7 +1028,7 @@ namespace Ink_Canvas.Helpers
|
||||
checksum.CopyTo(finalData, 0);
|
||||
encryptedData.CopyTo(finalData, checksum.Length);
|
||||
|
||||
File.WriteAllBytes(filePath, finalData);
|
||||
ProcessProtectionManager.WithWriteAccess(filePath, () => File.WriteAllBytes(filePath, finalData));
|
||||
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 加密使用统计已保存到: {filePath}");
|
||||
}
|
||||
@@ -940,6 +1039,40 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateUsageChannel(Ink_Canvas.UpdateChannel channel)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (fileLock)
|
||||
{
|
||||
var stats = LoadUsageStats();
|
||||
if (stats == null)
|
||||
{
|
||||
stats = new UsageStats
|
||||
{
|
||||
DeviceId = DeviceId,
|
||||
LastLaunchTime = DateTime.Now,
|
||||
LaunchCount = 0,
|
||||
TotalUsageSeconds = 0,
|
||||
AverageSessionSeconds = 0,
|
||||
LastUpdateCheck = DateTime.MinValue,
|
||||
UpdatePriority = UpdatePriority.Medium,
|
||||
UsageFrequency = UsageFrequency.Medium
|
||||
};
|
||||
}
|
||||
|
||||
stats.UpdateChannel = channel;
|
||||
SaveUsageStats(stats);
|
||||
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 更新使用统计中的通道信息: {channel}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 更新通道信息到使用统计失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录更新检查时间
|
||||
/// </summary>
|
||||
@@ -960,6 +1093,23 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取上次更新检查时间
|
||||
/// </summary>
|
||||
public static DateTime GetLastUpdateCheck()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stats = LoadUsageStats();
|
||||
return stats.LastUpdateCheck;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 获取上次更新检查时间失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 从备份文件恢复使用统计数据
|
||||
@@ -1092,15 +1242,13 @@ namespace Ink_Canvas.Helpers
|
||||
int versionDiff = CalculateVersionGenerationDifference(localVersion, updateVersion);
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 无法获取版本发布时间,使用版本号差异判断 - 本地版本: {localVersion}, 远程版本: {updateVersion}, 代数差异: {versionDiff}");
|
||||
|
||||
// 当版本号代数差异大于3时自动更新
|
||||
if (versionDiff > 3)
|
||||
if (versionDiff >= 1)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})大于3,自动更新");
|
||||
return true;
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=1,允许更新");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})不大于3,暂不更新");
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})<1,可能是相同版本或降级,暂不更新");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1476,4 +1624,3 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
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>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
public async Task<string> GetAccessTokenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_userToken))
|
||||
{
|
||||
return _userToken;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_accessToken) && DateTime.Now < _tokenExpiresAt.AddMinutes(-5))
|
||||
{
|
||||
return _accessToken;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
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, cancellationToken);
|
||||
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 (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"获取Access Token时网络错误: {httpEx.Message}", httpEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"获取Access Token时出错: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送GET请求
|
||||
/// </summary>
|
||||
/// <param name="endpoint">API端点</param>
|
||||
/// <param name="requireAuth">是否需要认证</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
public async Task<T> GetAsync<T>(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string token = null;
|
||||
if (requireAuth)
|
||||
{
|
||||
token = await GetAccessTokenAsync(cancellationToken);
|
||||
}
|
||||
|
||||
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, cancellationToken);
|
||||
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 (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送POST请求
|
||||
/// </summary>
|
||||
/// <param name="endpoint">API端点</param>
|
||||
/// <param name="data">请求数据</param>
|
||||
/// <param name="requireAuth">是否需要认证</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
public async Task<T> PostAsync<T>(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string token = null;
|
||||
if (requireAuth)
|
||||
{
|
||||
token = await GetAccessTokenAsync(cancellationToken);
|
||||
}
|
||||
|
||||
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, cancellationToken);
|
||||
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 (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送PUT请求
|
||||
/// </summary>
|
||||
/// <param name="endpoint">API端点</param>
|
||||
/// <param name="data">请求数据</param>
|
||||
/// <param name="requireAuth">是否需要认证</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
public async Task<T> PutAsync<T>(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string token = null;
|
||||
if (requireAuth)
|
||||
{
|
||||
token = await GetAccessTokenAsync(cancellationToken);
|
||||
}
|
||||
|
||||
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, cancellationToken);
|
||||
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 (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送DELETE请求
|
||||
/// </summary>
|
||||
/// <param name="endpoint">API端点</param>
|
||||
/// <param name="requireAuth">是否需要认证</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
public async Task<bool> DeleteAsync(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string token = null;
|
||||
if (requireAuth)
|
||||
{
|
||||
token = await GetAccessTokenAsync(cancellationToken);
|
||||
}
|
||||
|
||||
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, cancellationToken);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
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>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
public async Task<T> UploadNoteAsync<T>(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
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, cancellationToken);
|
||||
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 (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"上传文件时网络错误: {httpEx.Message}", httpEx);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
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 DlassUploadQueue : BaseUploadQueue
|
||||
{
|
||||
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
|
||||
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
|
||||
|
||||
/// <summary>
|
||||
/// 队列文件名
|
||||
/// </summary>
|
||||
protected override string QueueFileName => "DlassUploadQueue.json";
|
||||
|
||||
/// <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>
|
||||
/// 检查上传是否启用
|
||||
/// </summary>
|
||||
protected override bool IsUploadEnabled()
|
||||
{
|
||||
return MainWindow.Settings?.Dlass?.IsAutoUploadNotes == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部上传方法,执行实际上传操作
|
||||
/// </summary>
|
||||
protected override async Task<bool> UploadFileInternalAsync(string filePath, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 再次检查文件是否存在(可能在队列等待时被删除)
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取白板信息
|
||||
var whiteboard = await GetWhiteboardInfo(cancellationToken);
|
||||
if (whiteboard == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取API基础URL和用户Token
|
||||
var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
|
||||
var userToken = MainWindow.Settings?.Dlass?.UserToken;
|
||||
|
||||
// 准备上传参数
|
||||
var fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||
var fileExtension = Path.GetExtension(filePath).ToLower();
|
||||
var title = fileName;
|
||||
string fileType;
|
||||
string tags;
|
||||
if (fileExtension == ".zip")
|
||||
{
|
||||
fileType = "多页面墨迹压缩包";
|
||||
tags = "自动上传,多页面,zip,压缩包";
|
||||
}
|
||||
else if (fileExtension == ".icstk")
|
||||
{
|
||||
fileType = "墨迹文件";
|
||||
tags = "自动上传,墨迹,icstk";
|
||||
}
|
||||
else if (fileExtension == ".xml")
|
||||
{
|
||||
fileType = "XML文件";
|
||||
tags = "自动上传,xml";
|
||||
}
|
||||
else
|
||||
{
|
||||
fileType = "笔记";
|
||||
tags = "自动上传,笔记,png";
|
||||
}
|
||||
var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
|
||||
|
||||
// 创建API客户端并上传文件
|
||||
var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken);
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var uploadResult = await apiClient.UploadNoteAsync<UploadNoteResponse>(
|
||||
"/api/whiteboard/upload_note",
|
||||
filePath,
|
||||
whiteboard.BoardId,
|
||||
whiteboard.SecretKey,
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
cancellationToken);
|
||||
|
||||
if (uploadResult != null && uploadResult.Success)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
apiClient.Dispose();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取白板信息
|
||||
/// </summary>
|
||||
private async Task<WhiteboardInfo> GetWhiteboardInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
|
||||
if (string.IsNullOrEmpty(selectedClassName))
|
||||
{
|
||||
LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:未选择班级", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
var userToken = MainWindow.Settings?.Dlass?.UserToken;
|
||||
if (string.IsNullOrEmpty(userToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:未设置用户Token", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
|
||||
|
||||
// 创建API客户端并获取白板信息
|
||||
var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken);
|
||||
try
|
||||
{
|
||||
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, cancellationToken: cancellationToken);
|
||||
|
||||
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:无法获取白板信息", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查找匹配班级的白板
|
||||
var 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($"[DlassUploadQueue] 上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return whiteboard;
|
||||
}
|
||||
finally
|
||||
{
|
||||
apiClient.Dispose();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ namespace Ink_Canvas.Helpers
|
||||
private const string IpcFilePrefix = "InkCanvasFileAssociation_";
|
||||
private const string IpcBoardModePrefix = "InkCanvasBoardMode_";
|
||||
private const string IpcShowModePrefix = "InkCanvasShowMode_";
|
||||
private const string IpcUriCommandPrefix = "InkCanvasUriCommand_";
|
||||
private const int IpcTimeout = 5000; // 5秒超时
|
||||
|
||||
/// <summary>
|
||||
@@ -361,6 +362,57 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过IPC将URI命令发送给已运行的实例
|
||||
/// </summary>
|
||||
/// <param name="uri">URI命令</param>
|
||||
/// <returns>是否成功发送</returns>
|
||||
public static bool TrySendUriCommandToExistingInstance(string uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"尝试通过IPC发送URI命令给已运行实例: {uri}", LogHelper.LogType.Event);
|
||||
|
||||
// 创建IPC文件
|
||||
string tempDir = Path.GetTempPath();
|
||||
string ipcFileName = IpcUriCommandPrefix + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
|
||||
|
||||
// 写入URI命令到IPC文件
|
||||
File.WriteAllText(ipcFilePath, uri, 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 URI命令发送完成", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"通过IPC发送URI命令失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动IPC监听器,等待其他实例发送文件路径
|
||||
/// </summary>
|
||||
@@ -467,7 +519,7 @@ namespace Ink_Canvas.Helpers
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +570,7 @@ namespace Ink_Canvas.Helpers
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,7 +625,57 @@ namespace Ink_Canvas.Helpers
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
|
||||
// 处理URI命令IPC文件
|
||||
string[] uriCommandFiles = Directory.GetFiles(tempDir, IpcUriCommandPrefix + "*.tmp");
|
||||
foreach (string ipcFile in uriCommandFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取命令内容
|
||||
string uri = File.ReadAllText(ipcFile, Encoding.UTF8);
|
||||
|
||||
if (!string.IsNullOrEmpty(uri))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC接收到URI命令: {uri}", LogHelper.LogType.Event);
|
||||
|
||||
// 在UI线程中处理URI命令
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取主窗口并处理URI命令
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.HandleUriCommand(uri);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC处理URI命令失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 删除IPC文件
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理URI命令IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
|
||||
// 尝试删除损坏的IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFile))
|
||||
{
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,24 @@ namespace Ink_Canvas.Helpers
|
||||
public int Height => Bottom - Top;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MONITORINFO
|
||||
{
|
||||
public uint cbSize;
|
||||
public RECT rcMonitor;
|
||||
public RECT rcWork;
|
||||
public uint dwFlags;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromRect(ref RECT lprc, uint dwFlags);
|
||||
|
||||
public static string WindowTitle()
|
||||
{
|
||||
IntPtr foregroundWindowHandle = GetForegroundWindow();
|
||||
@@ -106,10 +124,28 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
public static double GetTaskbarHeight(Screen screen, double dpiScaleY)
|
||||
{
|
||||
// 获取工作区和屏幕高度的差值
|
||||
var workingArea = screen.WorkingArea;
|
||||
var bounds = screen.Bounds;
|
||||
int taskbarHeight = bounds.Height - workingArea.Height;
|
||||
// 创建RECT结构体表示屏幕边界
|
||||
RECT screenRect = new RECT
|
||||
{
|
||||
Left = screen.Bounds.Left,
|
||||
Top = screen.Bounds.Top,
|
||||
Right = screen.Bounds.Right,
|
||||
Bottom = screen.Bounds.Bottom
|
||||
};
|
||||
|
||||
// 获取屏幕句柄
|
||||
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
|
||||
IntPtr hMonitor = MonitorFromRect(ref screenRect, MONITOR_DEFAULTTONEAREST);
|
||||
|
||||
// 初始化MONITORINFO结构体
|
||||
MONITORINFO monitorInfo = new MONITORINFO();
|
||||
monitorInfo.cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFO));
|
||||
|
||||
// 获取监视器信息
|
||||
GetMonitorInfo(hMonitor, ref monitorInfo);
|
||||
|
||||
// 计算任务栏高度:monitorInfo.rcMonitor.bottom减去monitorInfo.rcWork.bottom的值
|
||||
int taskbarHeight = monitorInfo.rcMonitor.Bottom - monitorInfo.rcWork.Bottom;
|
||||
// 考虑 DPI 缩放
|
||||
return taskbarHeight / dpiScaleY;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,16 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
UnregisterHotkey(hotkeyName);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
HotkeyManager.Current.Remove(hotkeyName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// 创建快捷键信息
|
||||
var hotkeyInfo = new HotkeyInfo
|
||||
@@ -112,9 +122,8 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册全局快捷键 {hotkeyName} 失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -383,6 +392,13 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_registeredHotkeys.Count == 0)
|
||||
{
|
||||
if (ShouldEnableHotkeysBasedOnContext())
|
||||
{
|
||||
LoadHotkeysFromSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -438,6 +454,11 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
// 如果设置允许,则在鼠标模式下也启用快捷键
|
||||
EnableHotkeyRegistration();
|
||||
|
||||
if (_hotkeysShouldBeRegistered && _registeredHotkeys.Count == 0)
|
||||
{
|
||||
LoadHotkeysFromSettings();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -449,6 +470,11 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
// 非鼠标模式下启用快捷键
|
||||
EnableHotkeyRegistration();
|
||||
|
||||
if (_hotkeysShouldBeRegistered && _registeredHotkeys.Count == 0)
|
||||
{
|
||||
LoadHotkeysFromSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -186,11 +186,11 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放GPU资源
|
||||
/// 释放GPU相关资源标记
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_renderTarget?.Clear();
|
||||
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 哈希计算辅助类,用于路径/标识等短字符串的 MD5 前缀哈希。
|
||||
/// </summary>
|
||||
internal static class HashHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 对给定路径字符串计算 MD5 哈希,返回前 8 位十六进制字符串。
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径或任意字符串</param>
|
||||
/// <returns>8 位十六进制字符串;异常或空输入时返回 "error" 或 "unknown"</returns>
|
||||
public static string GetFileHash(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath)) return "unknown";
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(filePath));
|
||||
return BitConverter.ToString(hash).Replace("-", "").Substring(0, 8);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"计算文件哈希失败: {ex}", LogHelper.LogType.Error);
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public interface IPPTLinkManager : IDisposable
|
||||
{
|
||||
event Action<object> SlideShowBegin;
|
||||
event Action<object> SlideShowNextSlide;
|
||||
event Action<object> SlideShowEnd;
|
||||
event Action<object> PresentationOpen;
|
||||
event Action<object> PresentationClose;
|
||||
event Action<bool> PPTConnectionChanged;
|
||||
event Action<bool> SlideShowStateChanged;
|
||||
|
||||
bool IsConnected { get; }
|
||||
bool IsInSlideShow { get; }
|
||||
bool IsSupportWPS { get; set; }
|
||||
bool SkipAnimationsWhenNavigating { get; set; }
|
||||
int SlidesCount { get; }
|
||||
|
||||
object PPTApplication { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始监视与 PowerPoint 的连接以及幻灯片放映相关状态,并在状态变化时触发对应事件。
|
||||
/// </summary>
|
||||
void StartMonitoring();
|
||||
/// <summary>
|
||||
/// 停止监控 PowerPoint 的连接与事件,停止接收并处理与演示文稿和幻灯片放映相关的通知。
|
||||
/// </summary>
|
||||
void StopMonitoring();
|
||||
|
||||
/// <summary>
|
||||
/// 重新加载或重建与 PowerPoint 的连接。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 调用后实现应刷新内部连接与状态,必要时重建与 PowerPoint 的会话;此操作可能导致 IsConnected 变化并触发 PPTConnectionChanged 或其他相关事件(例如 SlideShowStateChanged)。
|
||||
/// </remarks>
|
||||
void ReloadConnection();
|
||||
|
||||
/// <summary>
|
||||
/// 尝试启动当前演示文稿的放映模式。
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> 如果放映已成功启动,<c>false</c> 否则。</returns>
|
||||
bool TryStartSlideShow();
|
||||
/// <summary>
|
||||
/// 尝试结束当前正在进行的幻灯片放映。
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> 如果放映已成功结束,<c>false</c> 否则。</returns>
|
||||
bool TryEndSlideShow();
|
||||
|
||||
// 导航控制
|
||||
bool TryNavigateToSlide(int slideNumber);
|
||||
bool TryNavigateNext();
|
||||
bool TryNavigatePrevious();
|
||||
|
||||
// 查询
|
||||
int GetCurrentSlideNumber();
|
||||
string GetPresentationName();
|
||||
bool TryShowSlideNavigation();
|
||||
object GetCurrentActivePresentation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using Ink_Canvas.Properties;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// i18n 本地化辅助:设置/获取当前 UI 语言,便于后续从配置切换语言。
|
||||
/// </summary>
|
||||
public static class LocalizationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前 UI 语言(如 "zh-CN", "en-US")。未设置时使用系统当前 UI 语言。
|
||||
/// </summary>
|
||||
public static CultureInfo CurrentCulture
|
||||
{
|
||||
get => Thread.CurrentThread.CurrentUICulture;
|
||||
set
|
||||
{
|
||||
if (value == null) return;
|
||||
Thread.CurrentThread.CurrentUICulture = value;
|
||||
Strings.Culture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定语言名称设置当前 UI 语言(如 "zh-CN", "en-US")。
|
||||
/// 若名称无效则保持当前语言不变。
|
||||
/// </summary>
|
||||
public static bool TrySetCulture(string cultureName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cultureName))
|
||||
{
|
||||
CurrentCulture = CultureInfo.InstalledUICulture;
|
||||
return true;
|
||||
}
|
||||
var culture = CultureInfo.GetCultureInfo(cultureName);
|
||||
CurrentCulture = culture;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串。优先使用强类型属性,未知键时用此方法。
|
||||
/// </summary>
|
||||
public static string GetString(string key)
|
||||
{
|
||||
return Strings.GetString(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
@@ -29,6 +29,11 @@ namespace Ink_Canvas.Helpers
|
||||
WriteLogToFile(msg, LogType.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将一条日志消息记录到应用的日志文件中(可能是单一日志文件或按启动时间存档的文件),同时在日志条目中包含时间戳、线程 ID 和调用者信息,并遵循应用的日志设置。
|
||||
/// </summary>
|
||||
/// <param name="str">要记录的日志文本消息。</param>
|
||||
/// <param name="logType">日志的类型/等级,用于在日志条目中标识(例如 Info、Error、Warning 等)。</param>
|
||||
public static void WriteLogToFile(string str, LogType logType = LogType.Info)
|
||||
{
|
||||
// 检查日志是否启用
|
||||
@@ -62,7 +67,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
if (!Directory.Exists(App.RootPath))
|
||||
{
|
||||
Directory.CreateDirectory(App.RootPath);
|
||||
ProcessProtectionManager.WithWriteAccess(App.RootPath, () => Directory.CreateDirectory(App.RootPath));
|
||||
}
|
||||
|
||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||
@@ -78,14 +83,30 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
string logLine = string.Format("{0} [T{1}] [{2}] [{3}] {4}", DateTime.Now.ToString("O"), threadId, strLogType, callerInfo, str);
|
||||
using (StreamWriter sw = new StreamWriter(file, true))
|
||||
ProcessProtectionManager.WithWriteAccess(file, () =>
|
||||
{
|
||||
sw.WriteLine(logLine);
|
||||
}
|
||||
using (StreamWriter sw = new StreamWriter(file, true))
|
||||
{
|
||||
sw.WriteLine(logLine);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[LogHelper] WriteLogToFile failed: {ex.Message}");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定日志文件夹的总大小,并在超过 MaxLogsFolderSizeBytes 时删除该文件夹下的所有文件并记录清理日志。
|
||||
/// </summary>
|
||||
/// <param name="logsPath">要检查和清理的日志文件夹路径。</param>
|
||||
/// <remarks>
|
||||
/// - 如果目录不存在则直接返回。
|
||||
/// - 当总大小超过 MaxLogsFolderSizeBytes 时,会尝试删除目录下的每个文件(单个删除失败将被忽略)。
|
||||
/// - 清理完成后会向该目录下的 Log_{AppStartTime}.txt 写入一条带有时间戳和 [Cleanup] 标签的记录。
|
||||
/// - 方法内部捕获并忽略所有异常以避免影响调用者流程。
|
||||
/// </remarks>
|
||||
private static void CheckAndCleanLogsFolder(string logsPath)
|
||||
{
|
||||
try
|
||||
@@ -111,18 +132,28 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
file.Delete();
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[LogHelper] Delete log file failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 记录清理操作
|
||||
string cleanupMessage = $"Logs folder exceeded size limit ({totalSize / 1024.0 / 1024.0:F2} MB > {MaxLogsFolderSizeBytes / 1024.0 / 1024.0:F2} MB). Folder cleaned.";
|
||||
using (StreamWriter sw = new StreamWriter(Path.Combine(logsPath, $"Log_{AppStartTime}.txt"), true))
|
||||
var logFile = Path.Combine(logsPath, $"Log_{AppStartTime}.txt");
|
||||
ProcessProtectionManager.WithWriteAccess(logFile, () =>
|
||||
{
|
||||
sw.WriteLine($"{DateTime.Now:O} [Cleanup] {cleanupMessage}");
|
||||
}
|
||||
using (StreamWriter sw = new StreamWriter(logFile, true))
|
||||
{
|
||||
sw.WriteLine($"{DateTime.Now:O} [Cleanup] {cleanupMessage}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[LogHelper] CheckAndCleanLogsFolder failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WriteLogToFile(string v, object warning)
|
||||
@@ -139,4 +170,4 @@ namespace Ink_Canvas.Helpers
|
||||
Warning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,813 +0,0 @@
|
||||
using Microsoft.Office.Interop.PowerPoint;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Windows.Ink;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 多PPT墨迹管理器 - 支持多个PPT窗口分别管理墨迹
|
||||
/// </summary>
|
||||
public class MultiPPTInkManager : IDisposable
|
||||
{
|
||||
#region Properties
|
||||
public bool IsAutoSaveEnabled { get; set; } = true;
|
||||
public string AutoSaveLocation { get; set; } = "";
|
||||
public PPTManager PPTManager { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
private readonly Dictionary<string, PPTInkManager> _presentationManagers;
|
||||
private readonly Dictionary<string, PresentationInfo> _presentationInfos;
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _disposed;
|
||||
private string _currentActivePresentationId = "";
|
||||
|
||||
// 墨迹备份机制
|
||||
private readonly Dictionary<string, Dictionary<int, StrokeCollection>> _strokeBackups;
|
||||
private DateTime _lastBackupTime = DateTime.MinValue;
|
||||
private const int BackupIntervalMinutes = 2; // 每2分钟备份一次
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public MultiPPTInkManager()
|
||||
{
|
||||
_presentationManagers = new Dictionary<string, PPTInkManager>();
|
||||
_presentationInfos = new Dictionary<string, PresentationInfo>();
|
||||
_strokeBackups = new Dictionary<string, Dictionary<int, StrokeCollection>>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// 初始化新的演示文稿
|
||||
/// </summary>
|
||||
public void InitializePresentation(Presentation presentation)
|
||||
{
|
||||
if (presentation == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var presentationId = GeneratePresentationId(presentation);
|
||||
|
||||
// 如果已存在该演示文稿的管理器,先清理
|
||||
if (_presentationManagers.ContainsKey(presentationId))
|
||||
{
|
||||
_presentationManagers[presentationId].Dispose();
|
||||
_presentationManagers.Remove(presentationId);
|
||||
}
|
||||
|
||||
// 创建新的墨迹管理器
|
||||
var inkManager = new PPTInkManager();
|
||||
inkManager.IsAutoSaveEnabled = IsAutoSaveEnabled;
|
||||
inkManager.AutoSaveLocation = AutoSaveLocation;
|
||||
inkManager.InitializePresentation(presentation);
|
||||
|
||||
// 保存管理器和演示文稿信息
|
||||
_presentationManagers[presentationId] = inkManager;
|
||||
_presentationInfos[presentationId] = new PresentationInfo
|
||||
{
|
||||
Id = presentationId,
|
||||
Name = presentation.Name,
|
||||
FullName = presentation.FullName,
|
||||
SlideCount = presentation.Slides.Count,
|
||||
CreatedTime = DateTime.Now,
|
||||
LastAccessTime = DateTime.Now
|
||||
};
|
||||
|
||||
// 设置为当前活跃的演示文稿
|
||||
_currentActivePresentationId = presentationId;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化多PPT墨迹管理失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定的演示文稿
|
||||
/// </summary>
|
||||
public bool SwitchToPresentation(Presentation presentation)
|
||||
{
|
||||
if (presentation == null) return false;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var presentationId = GeneratePresentationId(presentation);
|
||||
|
||||
if (_presentationManagers.ContainsKey(presentationId))
|
||||
{
|
||||
// 如果切换的是不同的演示文稿,先保存当前活跃演示文稿的墨迹
|
||||
if (!string.IsNullOrEmpty(_currentActivePresentationId) &&
|
||||
_currentActivePresentationId != presentationId)
|
||||
{
|
||||
var currentManager = GetCurrentManager();
|
||||
if (currentManager != null)
|
||||
{
|
||||
// 获取当前活跃的演示文稿并保存墨迹
|
||||
var currentPresentation = GetCurrentActivePresentation();
|
||||
if (currentPresentation != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
currentManager.SaveAllStrokesToFile(currentPresentation);
|
||||
LogHelper.WriteLogToFile($"已保存当前演示文稿墨迹: {currentPresentation.Name}", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存当前演示文稿墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_currentActivePresentationId = presentationId;
|
||||
|
||||
// 更新最后访问时间
|
||||
if (_presentationInfos.ContainsKey(presentationId))
|
||||
{
|
||||
_presentationInfos[presentationId].LastAccessTime = DateTime.Now;
|
||||
}
|
||||
|
||||
if (_currentActivePresentationId != presentationId)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"已切换到演示文稿: {presentation.Name}", LogHelper.LogType.Trace);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果不存在,尝试初始化
|
||||
InitializePresentation(presentation);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"切换到演示文稿失败: {ex}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前页面的墨迹
|
||||
/// </summary>
|
||||
public void SaveCurrentSlideStrokes(int slideIndex, StrokeCollection strokes)
|
||||
{
|
||||
if (slideIndex <= 0 || strokes == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manager = GetCurrentManager();
|
||||
if (manager != null)
|
||||
{
|
||||
// 保存到管理器
|
||||
manager.SaveCurrentSlideStrokes(slideIndex, strokes);
|
||||
|
||||
// 只有在保存成功后才创建备份
|
||||
if (!string.IsNullOrEmpty(_currentActivePresentationId))
|
||||
{
|
||||
CreateStrokeBackup(_currentActivePresentationId, slideIndex, strokes);
|
||||
}
|
||||
|
||||
// 检查是否需要执行定期备份
|
||||
CheckAndPerformBackup();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存当前页面墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制保存指定页面的墨迹(忽略锁定状态)
|
||||
/// </summary>
|
||||
public void ForceSaveSlideStrokes(int slideIndex, StrokeCollection strokes)
|
||||
{
|
||||
if (slideIndex <= 0 || strokes == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manager = GetCurrentManager();
|
||||
if (manager != null)
|
||||
{
|
||||
manager.ForceSaveSlideStrokes(slideIndex, strokes);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"强制保存页面墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载指定页面的墨迹
|
||||
/// </summary>
|
||||
public StrokeCollection LoadSlideStrokes(int slideIndex)
|
||||
{
|
||||
if (slideIndex <= 0) return new StrokeCollection();
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manager = GetCurrentManager();
|
||||
if (manager != null)
|
||||
{
|
||||
var strokes = manager.LoadSlideStrokes(slideIndex);
|
||||
|
||||
// 如果从管理器加载失败,尝试从备份恢复
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_currentActivePresentationId))
|
||||
{
|
||||
strokes = RestoreStrokeFromBackup(_currentActivePresentationId, slideIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return strokes ?? new StrokeCollection();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载页面墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
|
||||
// 尝试从备份恢复
|
||||
if (!string.IsNullOrEmpty(_currentActivePresentationId))
|
||||
{
|
||||
return RestoreStrokeFromBackup(_currentActivePresentationId, slideIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new StrokeCollection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定页面并加载墨迹
|
||||
/// </summary>
|
||||
public StrokeCollection SwitchToSlide(int slideIndex, StrokeCollection currentStrokes = null)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manager = GetCurrentManager();
|
||||
if (manager != null)
|
||||
{
|
||||
return manager.SwitchToSlide(slideIndex, currentStrokes);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"无法获取当前墨迹管理器,页面切换失败: {slideIndex}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"切换页面墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
return new StrokeCollection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存所有墨迹到文件
|
||||
/// </summary>
|
||||
public void SaveAllStrokesToFile(Presentation presentation)
|
||||
{
|
||||
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var presentationId = GeneratePresentationId(presentation);
|
||||
if (_presentationManagers.ContainsKey(presentationId))
|
||||
{
|
||||
_presentationManagers[presentationId].SaveAllStrokesToFile(presentation);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存所有墨迹到文件失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载已保存的墨迹
|
||||
/// </summary>
|
||||
public void LoadSavedStrokes(Presentation presentation)
|
||||
{
|
||||
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var presentationId = GeneratePresentationId(presentation);
|
||||
if (_presentationManagers.ContainsKey(presentationId))
|
||||
{
|
||||
_presentationManagers[presentationId].LoadSavedStrokes();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从文件加载墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除指定演示文稿的所有墨迹
|
||||
/// </summary>
|
||||
public void ClearPresentationStrokes(Presentation presentation)
|
||||
{
|
||||
if (presentation == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var presentationId = GeneratePresentationId(presentation);
|
||||
if (_presentationManagers.ContainsKey(presentationId))
|
||||
{
|
||||
_presentationManagers[presentationId].ClearAllStrokes();
|
||||
LogHelper.WriteLogToFile($"已清除演示文稿墨迹: {presentation.Name}", LogHelper.LogType.Trace);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清除演示文稿墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有演示文稿的墨迹
|
||||
/// </summary>
|
||||
public void ClearAllStrokes()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var manager in _presentationManagers.Values)
|
||||
{
|
||||
manager?.ClearAllStrokes();
|
||||
}
|
||||
LogHelper.WriteLogToFile("已清除所有演示文稿墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清除所有墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻页后锁定墨迹写入
|
||||
/// </summary>
|
||||
public void LockInkForSlide(int slideIndex)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manager = GetCurrentManager();
|
||||
if (manager != null)
|
||||
{
|
||||
manager.LockInkForSlide(slideIndex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"锁定墨迹写入失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以写入墨迹
|
||||
/// </summary>
|
||||
public bool CanWriteInk(int currentSlideIndex)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manager = GetCurrentManager();
|
||||
if (manager != null)
|
||||
{
|
||||
return manager.CanWriteInk(currentSlideIndex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"检查墨迹写入权限失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置当前演示文稿的墨迹锁定状态
|
||||
/// </summary>
|
||||
public void ResetCurrentPresentationLockState()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manager = GetCurrentManager();
|
||||
if (manager != null)
|
||||
{
|
||||
manager.ResetLockState();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"重置墨迹锁定状态失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除演示文稿管理器
|
||||
/// </summary>
|
||||
public void RemovePresentation(Presentation presentation)
|
||||
{
|
||||
if (presentation == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var presentationId = GeneratePresentationId(presentation);
|
||||
|
||||
if (_presentationManagers.ContainsKey(presentationId))
|
||||
{
|
||||
// 保存墨迹到文件
|
||||
_presentationManagers[presentationId].SaveAllStrokesToFile(presentation);
|
||||
|
||||
// 释放资源
|
||||
_presentationManagers[presentationId].Dispose();
|
||||
_presentationManagers.Remove(presentationId);
|
||||
}
|
||||
|
||||
if (_presentationInfos.ContainsKey(presentationId))
|
||||
{
|
||||
_presentationInfos.Remove(presentationId);
|
||||
}
|
||||
|
||||
// 如果移除的是当前活跃的演示文稿,重置活跃ID
|
||||
if (_currentActivePresentationId == presentationId)
|
||||
{
|
||||
_currentActivePresentationId = "";
|
||||
}
|
||||
|
||||
}
|
||||
catch (COMException comEx)
|
||||
{
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706BA || hr == 0x800706BE || hr == 0x80048010)
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前管理的演示文稿数量
|
||||
/// </summary>
|
||||
public int GetPresentationCount()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _presentationManagers.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有演示文稿信息
|
||||
/// </summary>
|
||||
public List<PresentationInfo> GetAllPresentationInfos()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _presentationInfos.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理长时间未访问的演示文稿管理器
|
||||
/// </summary>
|
||||
public void CleanupInactivePresentations(TimeSpan inactiveThreshold)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var inactiveIds = new List<string>();
|
||||
var cutoffTime = DateTime.Now - inactiveThreshold;
|
||||
|
||||
foreach (var info in _presentationInfos.Values)
|
||||
{
|
||||
if (info.LastAccessTime < cutoffTime && info.Id != _currentActivePresentationId)
|
||||
{
|
||||
inactiveIds.Add(info.Id);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var id in inactiveIds)
|
||||
{
|
||||
if (_presentationManagers.ContainsKey(id))
|
||||
{
|
||||
_presentationManagers[id].Dispose();
|
||||
_presentationManagers.Remove(id);
|
||||
}
|
||||
_presentationInfos.Remove(id);
|
||||
|
||||
// 清理备份数据
|
||||
if (_strokeBackups.ContainsKey(id))
|
||||
{
|
||||
_strokeBackups.Remove(id);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已清理非活跃演示文稿: {id}", LogHelper.LogType.Trace);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理非活跃演示文稿失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建墨迹备份
|
||||
/// </summary>
|
||||
private void CreateStrokeBackup(string presentationId, int slideIndex, StrokeCollection strokes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0) return;
|
||||
|
||||
if (!_strokeBackups.ContainsKey(presentationId))
|
||||
{
|
||||
_strokeBackups[presentationId] = new Dictionary<int, StrokeCollection>();
|
||||
}
|
||||
|
||||
// 释放旧的备份
|
||||
if (_strokeBackups[presentationId].ContainsKey(slideIndex))
|
||||
{
|
||||
_strokeBackups[presentationId][slideIndex] = null;
|
||||
}
|
||||
|
||||
// 创建新的备份
|
||||
_strokeBackups[presentationId][slideIndex] = strokes.Clone();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建墨迹备份失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从备份恢复墨迹
|
||||
/// </summary>
|
||||
private StrokeCollection RestoreStrokeFromBackup(string presentationId, int slideIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_strokeBackups.ContainsKey(presentationId) &&
|
||||
_strokeBackups[presentationId].ContainsKey(slideIndex))
|
||||
{
|
||||
var backup = _strokeBackups[presentationId][slideIndex];
|
||||
if (backup != null)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从备份恢复第{slideIndex}页墨迹", LogHelper.LogType.Trace);
|
||||
return backup.Clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从备份恢复墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
return new StrokeCollection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并执行定期备份
|
||||
/// </summary>
|
||||
private void CheckAndPerformBackup()
|
||||
{
|
||||
try
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
|
||||
// 检查是否需要执行备份
|
||||
if (now - _lastBackupTime < TimeSpan.FromMinutes(BackupIntervalMinutes))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 备份当前活跃演示文稿的所有墨迹
|
||||
if (!string.IsNullOrEmpty(_currentActivePresentationId) &&
|
||||
_presentationManagers.ContainsKey(_currentActivePresentationId))
|
||||
{
|
||||
var manager = _presentationManagers[_currentActivePresentationId];
|
||||
if (manager != null)
|
||||
{
|
||||
// 这里可以添加更详细的备份逻辑
|
||||
}
|
||||
}
|
||||
|
||||
_lastBackupTime = now;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"定期备份检查失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
private PPTInkManager GetCurrentManager()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_currentActivePresentationId) ||
|
||||
!_presentationManagers.ContainsKey(_currentActivePresentationId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _presentationManagers[_currentActivePresentationId];
|
||||
}
|
||||
|
||||
private Presentation GetCurrentActivePresentation()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 通过PPTManager获取当前活跃的演示文稿
|
||||
return PPTManager?.GetCurrentActivePresentation();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取当前活跃演示文稿失败: {ex}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string GeneratePresentationId(Presentation presentation)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查COM对象是否仍然有效
|
||||
if (presentation == null)
|
||||
{
|
||||
return $"invalid_{DateTime.Now.Ticks}";
|
||||
}
|
||||
|
||||
var presentationPath = presentation.FullName;
|
||||
var fileHash = GetFileHash(presentationPath);
|
||||
var processId = GetProcessId(presentation);
|
||||
return $"{presentation.Name}_{presentation.Slides.Count}_{fileHash}_{processId}";
|
||||
}
|
||||
catch (COMException comEx)
|
||||
{
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706BA || hr == 0x800706BE || hr == 0x80048010)
|
||||
{
|
||||
return $"disconnected_{DateTime.Now.Ticks}";
|
||||
}
|
||||
return $"error_{DateTime.Now.Ticks}";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return $"unknown_{DateTime.Now.Ticks}";
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFileHash(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath)) return "unknown";
|
||||
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(filePath));
|
||||
return BitConverter.ToString(hashBytes).Replace("-", "").Substring(0, 8);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 所有异常都静默处理,避免日志噪音
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
private string GetProcessId(Presentation presentation)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 尝试获取PowerPoint应用程序的进程ID
|
||||
if (presentation.Application != null)
|
||||
{
|
||||
// 通过COM对象获取进程信息
|
||||
var hwnd = presentation.Application.HWND;
|
||||
if (hwnd != 0)
|
||||
{
|
||||
return hwnd.ToString();
|
||||
}
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
catch (COMException comEx)
|
||||
{
|
||||
// COM对象已失效,这是正常情况,完全静默处理
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706BA || hr == 0x800706BE || hr == 0x80048010)
|
||||
{
|
||||
return "disconnected";
|
||||
}
|
||||
return "error";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Dispose
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
// 释放所有管理器
|
||||
foreach (var manager in _presentationManagers.Values)
|
||||
{
|
||||
manager?.Dispose();
|
||||
}
|
||||
_presentationManagers.Clear();
|
||||
_presentationInfos.Clear();
|
||||
|
||||
// 清理备份数据
|
||||
foreach (var backupDict in _strokeBackups.Values)
|
||||
{
|
||||
foreach (var backup in backupDict.Values)
|
||||
{
|
||||
backup?.Clear();
|
||||
}
|
||||
backupDict.Clear();
|
||||
}
|
||||
_strokeBackups.Clear();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示文稿信息
|
||||
/// </summary>
|
||||
public class PresentationInfo
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string FullName { get; set; }
|
||||
public int SlideCount { get; set; }
|
||||
public DateTime CreatedTime { get; set; }
|
||||
public DateTime LastAccessTime { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
@@ -8,30 +9,52 @@ 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.HighQuality);
|
||||
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 bool _needsRedraw = true;
|
||||
private int _lastPointCount = 0;
|
||||
private const int REDRAW_THRESHOLD = 3;
|
||||
private int _lastDrawnPointCount = 0;
|
||||
private const int INCREMENTAL_DRAW_THRESHOLD = 2;
|
||||
private VisualCanvas _visualCanvas;
|
||||
|
||||
/// <summary>
|
||||
/// 创建显示笔迹的类
|
||||
@@ -47,17 +70,12 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建显示笔迹的类
|
||||
/// 创建显示笔迹的类
|
||||
/// </summary>
|
||||
/// <param name="drawingAttributes"></param>
|
||||
public StrokeVisual(DrawingAttributes drawingAttributes)
|
||||
{
|
||||
_drawingAttributes = drawingAttributes;
|
||||
|
||||
// 启用硬件加速
|
||||
RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.HighQuality);
|
||||
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
|
||||
RenderOptions.SetCachingHint(this, CachingHint.Cache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,6 +83,14 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
public Stroke Stroke { set; get; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置关联的VisualCanvas
|
||||
/// </summary>
|
||||
public void SetVisualCanvas(VisualCanvas visualCanvas)
|
||||
{
|
||||
_visualCanvas = visualCanvas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在笔迹中添加点
|
||||
/// </summary>
|
||||
@@ -75,16 +101,61 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
var collection = new StylusPointCollection { point };
|
||||
Stroke = new Stroke(collection) { DrawingAttributes = _drawingAttributes };
|
||||
_lastPointCount = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Stroke.StylusPoints.Add(point);
|
||||
_lastPointCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.HighQuality);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 标记需要重绘
|
||||
_needsRedraw = true;
|
||||
// 将新的DrawingVisual添加到VisualCanvas中
|
||||
_visualCanvas.AddVisual(segmentVisual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,22 +163,35 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
public void Redraw()
|
||||
{
|
||||
if (!_needsRedraw || Stroke == null) return;
|
||||
if (Stroke == null || _visualCanvas == null) return;
|
||||
|
||||
if (_lastPointCount % REDRAW_THRESHOLD != 0 && _lastPointCount > REDRAW_THRESHOLD)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var currentPointCount = Stroke.StylusPoints.Count;
|
||||
if (currentPointCount == 0) return;
|
||||
|
||||
try
|
||||
// 计算新增的点数
|
||||
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;
|
||||
}
|
||||
}
|
||||
_needsRedraw = false;
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,15 +199,15 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
public void ForceRedraw()
|
||||
{
|
||||
_needsRedraw = true;
|
||||
if (_visualCanvas != null)
|
||||
{
|
||||
_visualCanvas.Clear();
|
||||
}
|
||||
_lastDrawnPointCount = 0;
|
||||
Redraw();
|
||||
}
|
||||
|
||||
private readonly DrawingAttributes _drawingAttributes;
|
||||
|
||||
public static implicit operator Stroke(StrokeVisual v)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+264
-319
@@ -1,15 +1,13 @@
|
||||
using Microsoft.Office.Interop.PowerPoint;
|
||||
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;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// PPT墨迹管理器 - 负责PPT中墨迹的保存、加载和同步
|
||||
/// PPT墨迹管理器 - 负责按幻灯片保存/加载墨迹、自动保存与内存管理。
|
||||
/// </summary>
|
||||
public class PPTInkManager : IDisposable
|
||||
{
|
||||
@@ -21,7 +19,8 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
#region Private Fields
|
||||
private MemoryStream[] _memoryStreams;
|
||||
private int _maxSlides = 100;
|
||||
private const int DefaultMaxSlides = 100;
|
||||
private int _maxSlides = DefaultMaxSlides;
|
||||
private string _currentPresentationId = "";
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _disposed;
|
||||
@@ -34,50 +33,70 @@ namespace Ink_Canvas.Helpers
|
||||
// 添加快速切换保护机制
|
||||
private DateTime _lastSwitchTime = DateTime.MinValue;
|
||||
private int _lastSwitchSlideIndex = -1;
|
||||
private const int MinSwitchIntervalMs = 100; // 最小切换间隔100毫秒
|
||||
private const int MinSwitchIntervalMs = 100;
|
||||
|
||||
// 内存管理相关字段
|
||||
private long _totalMemoryUsage = 0;
|
||||
private const long MaxMemoryUsageBytes = 100 * 1024 * 1024; // 100MB限制
|
||||
private long _totalMemoryUsage;
|
||||
private const long MaxMemoryUsageBytes = 100 * 1024 * 1024; // 100MB
|
||||
private DateTime _lastMemoryCleanup = DateTime.MinValue;
|
||||
private const int MemoryCleanupIntervalMinutes = 5; // 5分钟清理一次
|
||||
private const int MemoryCleanupIntervalMinutes = 5;
|
||||
|
||||
private const string StrokeFileExtension = ".icstk";
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// 初始化 PPTInkManager 实例并为内部内存流分配初始容量以跟踪默认最大幻灯片数加上备用槽位。
|
||||
/// </summary>
|
||||
public PPTInkManager()
|
||||
{
|
||||
InitializeMemoryStreams();
|
||||
InitializeMemoryStreams(DefaultMaxSlides + 2);
|
||||
}
|
||||
|
||||
private void InitializeMemoryStreams()
|
||||
/// <summary>
|
||||
/// 根据指定容量初始化用于存储每页墨迹的内存流数组。
|
||||
/// </summary>
|
||||
/// <param name="capacity">期望的数组容量;如果小于 2,则会使用 2 作为最小容量。</param>
|
||||
private void InitializeMemoryStreams(int capacity)
|
||||
{
|
||||
_memoryStreams = new MemoryStream[_maxSlides + 2];
|
||||
if (_memoryStreams != null)
|
||||
{
|
||||
for (int i = 0; i < _memoryStreams.Length; i++)
|
||||
{
|
||||
try { _memoryStreams[i]?.Dispose(); } catch (Exception ex) { LogHelper.WriteLogToFile($"InitializeMemoryStreams 释放内存流 {i} 失败: {ex}", LogHelper.LogType.Warning); }
|
||||
}
|
||||
}
|
||||
_memoryStreams = new MemoryStream[Math.Max(2, capacity)];
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// 初始化新的演示文稿
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 为新的或当前的演示文稿初始化墨迹管理器的内部状态。
|
||||
/// 方法会清除所有内存中的笔迹数据,重置墨迹写入锁与快速切换追踪,并根据演示文稿的幻灯片数量分配内部内存缓冲区。
|
||||
/// 如果已启用自动保存且设置了 <see cref="AutoSaveLocation"/>,则会尝试加载磁盘上的已保存墨迹文件。
|
||||
/// </remarks>
|
||||
/// <param name="presentation">要初始化的 PowerPoint Presentation 实例;为 null 时方法不执行任何操作并直接返回。</param>
|
||||
public void InitializePresentation(Presentation presentation)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (presentation == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 完全清理之前的墨迹状态
|
||||
ClearAllStrokes();
|
||||
|
||||
// 重置墨迹锁定状态
|
||||
ClearAllStrokesInternal();
|
||||
_inkLockUntil = DateTime.MinValue;
|
||||
_lockedSlideIndex = -1;
|
||||
_lastSwitchSlideIndex = -1;
|
||||
_lastSwitchTime = DateTime.MinValue;
|
||||
|
||||
// 生成演示文稿唯一标识符
|
||||
_currentPresentationId = GeneratePresentationId(presentation);
|
||||
|
||||
// 重新初始化内存流数组
|
||||
int slideCount = 0;
|
||||
try
|
||||
{
|
||||
@@ -85,21 +104,17 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
catch (COMException comEx)
|
||||
{
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr == 0x80048010)
|
||||
{
|
||||
return;
|
||||
}
|
||||
uint hr = (uint)comEx.HResult;
|
||||
if (hr == 0x80048010) return;
|
||||
throw;
|
||||
}
|
||||
_memoryStreams = new MemoryStream[slideCount + 2];
|
||||
|
||||
// 如果启用自动保存,尝试加载已保存的墨迹
|
||||
int capacity = slideCount + 2;
|
||||
_maxSlides = Math.Max(_maxSlides, slideCount);
|
||||
_memoryStreams = new MemoryStream[capacity];
|
||||
|
||||
if (IsAutoSaveEnabled && !string.IsNullOrEmpty(AutoSaveLocation))
|
||||
{
|
||||
LoadSavedStrokes();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -111,48 +126,25 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 保存当前页面的墨迹
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 将指定幻灯片的墨迹保存到内部内存缓存,并在必要时触发内存清理。
|
||||
/// </remarks>
|
||||
/// <param name="slideIndex">要保存的幻灯片索引(从 1 开始)。方法在索引小于或等于 0 时不执行任何操作。</param>
|
||||
/// <param name="strokes">要保存的墨迹集合;为 null 时方法不执行任何操作。</param>
|
||||
public void SaveCurrentSlideStrokes(int slideIndex, StrokeCollection strokes)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (slideIndex <= 0 || strokes == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查墨迹锁定
|
||||
if (!CanWriteInk(slideIndex))
|
||||
{
|
||||
if (DateTime.Now < _inkLockUntil)
|
||||
{
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!CanWriteInk(slideIndex)) return;
|
||||
if (slideIndex >= _memoryStreams.Length) 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] = ms;
|
||||
|
||||
if (ms.Length > 0)
|
||||
{
|
||||
}
|
||||
|
||||
// 检查内存使用情况
|
||||
CheckAndPerformMemoryCleanup();
|
||||
}
|
||||
ReplaceSlideStream(slideIndex, strokes);
|
||||
CheckAndPerformMemoryCleanup();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -162,36 +154,25 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制保存指定页面的墨迹
|
||||
/// 强制保存指定页墨迹到内存(不受锁定限制)。用于放映结束前保存当前画布到当前页。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 强制将指定幻灯片的墨迹保存到内部内存缓存,覆盖该幻灯片已有的墨迹数据。
|
||||
/// </remarks>
|
||||
/// <param name="slideIndex">要保存的幻灯片索引(从 1 开始)。</param>
|
||||
/// <param name="strokes">要保存的墨迹集合,不能为空。</param>
|
||||
public void ForceSaveSlideStrokes(int slideIndex, StrokeCollection strokes)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
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);
|
||||
}
|
||||
if (slideIndex >= _memoryStreams.Length) return;
|
||||
ReplaceSlideStream(slideIndex, strokes);
|
||||
LogHelper.WriteLogToFile($"已强制保存第{slideIndex}页墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -203,8 +184,14 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 加载指定页面的墨迹
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 加载并返回指定幻灯片的墨迹集合。
|
||||
/// </remarks>
|
||||
/// <param name="slideIndex">要加载的幻灯片索引(从1开始)。</param>
|
||||
/// <returns>包含指定幻灯片的墨迹的 StrokeCollection;如果该幻灯片没有已保存的墨迹或加载失败,则返回空的 StrokeCollection。</returns>
|
||||
public StrokeCollection LoadSlideStrokes(int slideIndex)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (slideIndex <= 0) return new StrokeCollection();
|
||||
|
||||
lock (_lockObject)
|
||||
@@ -214,8 +201,7 @@ namespace Ink_Canvas.Helpers
|
||||
if (slideIndex < _memoryStreams.Length && _memoryStreams[slideIndex] != null && _memoryStreams[slideIndex].Length > 0)
|
||||
{
|
||||
_memoryStreams[slideIndex].Position = 0;
|
||||
var strokes = new StrokeCollection(_memoryStreams[slideIndex]);
|
||||
return strokes;
|
||||
return new StrokeCollection(_memoryStreams[slideIndex]);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -230,36 +216,30 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 切换到指定页面并加载墨迹
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 切换到指定幻灯片并返回该幻灯片的已加载笔迹集合。
|
||||
/// </remarks>
|
||||
/// <param name="slideIndex">要切换到的幻灯片索引(从 1 开始)。</param>
|
||||
/// <param name="currentStrokes">可选的当前笔迹集合,用于在切换时提供当前画面状态。</param>
|
||||
/// <returns>`StrokeCollection`:指定幻灯片已加载的笔迹集合;若加载失败则返回一个空的 `StrokeCollection`。</returns>
|
||||
public StrokeCollection SwitchToSlide(int slideIndex, StrokeCollection currentStrokes = null)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查快速切换保护
|
||||
var now = DateTime.Now;
|
||||
if (now - _lastSwitchTime < TimeSpan.FromMilliseconds(MinSwitchIntervalMs) &&
|
||||
_lastSwitchSlideIndex == slideIndex)
|
||||
if (now - _lastSwitchTime < TimeSpan.FromMilliseconds(MinSwitchIntervalMs) && _lastSwitchSlideIndex == slideIndex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"快速切换保护:忽略重复的页面切换请求 {slideIndex}", LogHelper.LogType.Warning);
|
||||
LogHelper.WriteLogToFile($"快速切换保护:忽略重复请求 页{slideIndex}", LogHelper.LogType.Trace);
|
||||
return LoadSlideStrokes(slideIndex);
|
||||
}
|
||||
|
||||
|
||||
// 设置墨迹锁定
|
||||
LockInkForSlide(slideIndex);
|
||||
|
||||
// 加载新页面的墨迹
|
||||
var newStrokes = LoadSlideStrokes(slideIndex);
|
||||
|
||||
// 更新切换记录
|
||||
StrokeCollection newStrokes = LoadSlideStrokes(slideIndex);
|
||||
_lastSwitchTime = now;
|
||||
_lastSwitchSlideIndex = slideIndex;
|
||||
|
||||
if (newStrokes.Count > 0)
|
||||
{
|
||||
}
|
||||
|
||||
return newStrokes;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -273,82 +253,66 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 保存所有墨迹到文件
|
||||
/// </summary>
|
||||
public void SaveAllStrokesToFile(Presentation presentation)
|
||||
/// <remarks>
|
||||
/// 将内存中当前演示文稿的每页墨迹保存到磁盘,并根据情况写入当前播放位置文件。
|
||||
/// 仅在 IsAutoSaveEnabled 为真且 AutoSaveLocation 已设置时执行。会在演示文稿专属文件夹中写入按页编号的墨迹文件(带 `.icstk` 扩展名)和可选的 Position 文件。遇到特定 COM 错误(HRESULT 0x80048010)时会中止保存当前幻灯片计数读取而不抛出异常;单页保存失败会记录错误并继续处理其他页。
|
||||
/// </remarks>
|
||||
/// <param name="presentation">要保存墨迹的 PowerPoint 演示文稿对象。</param>
|
||||
/// <param name="currentSlideIndex">当前播放的页码;如果大于 0 则以此值写入 Position 文件,否则使用当前被锁定的页码或最后切换的页码作为保存位置。</param>
|
||||
public void SaveAllStrokesToFile(Presentation presentation, int currentSlideIndex = -1)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var folderPath = GetPresentationFolderPath();
|
||||
string folderPath = GetPresentationFolderPath();
|
||||
if (!Directory.Exists(folderPath))
|
||||
{
|
||||
Directory.CreateDirectory(folderPath);
|
||||
|
||||
int positionToSave = currentSlideIndex > 0 ? currentSlideIndex : (_lockedSlideIndex > 0 ? _lockedSlideIndex : _lastSwitchSlideIndex);
|
||||
if (positionToSave > 0)
|
||||
{
|
||||
try { File.WriteAllText(Path.Combine(folderPath, "Position"), positionToSave.ToString()); }
|
||||
catch (Exception ex) { LogHelper.WriteLogToFile($"保存 Position 失败: {ex}", LogHelper.LogType.Warning); }
|
||||
}
|
||||
|
||||
// 保存位置信息
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(folderPath, "Position"), _lockedSlideIndex.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存位置信息失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
// 保存所有页面的墨迹
|
||||
int savedCount = 0;
|
||||
int slideCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
slideCount = presentation.Slides.Count;
|
||||
}
|
||||
try { slideCount = presentation.Slides.Count; }
|
||||
catch (COMException comEx)
|
||||
{
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr == 0x80048010)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if ((uint)comEx.HResult == 0x80048010) return;
|
||||
throw;
|
||||
}
|
||||
|
||||
for (int i = 1; i <= slideCount && i < _memoryStreams.Length; i++)
|
||||
{
|
||||
if (_memoryStreams[i] != null)
|
||||
if (_memoryStreams[i] == null) continue;
|
||||
try
|
||||
{
|
||||
try
|
||||
if (_memoryStreams[i].Length > 8)
|
||||
{
|
||||
if (_memoryStreams[i].Length > 8)
|
||||
_memoryStreams[i].Position = 0;
|
||||
byte[] buf = new byte[_memoryStreams[i].Length];
|
||||
int read = _memoryStreams[i].Read(buf, 0, buf.Length);
|
||||
if (read > 0)
|
||||
{
|
||||
var srcBuf = new byte[_memoryStreams[i].Length];
|
||||
_memoryStreams[i].Position = 0;
|
||||
var byteLength = _memoryStreams[i].Read(srcBuf, 0, srcBuf.Length);
|
||||
|
||||
var filePath = Path.Combine(folderPath, i.ToString("0000") + ".icstk");
|
||||
File.WriteAllBytes(filePath, srcBuf);
|
||||
savedCount++;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// 删除空的墨迹文件
|
||||
var filePath = Path.Combine(folderPath, i.ToString("0000") + ".icstk");
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
string basePath = Path.Combine(folderPath, i.ToString("0000"));
|
||||
File.WriteAllBytes(basePath + StrokeFileExtension, buf);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存第{i}页墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
TryDeleteStrokeFile(folderPath, i);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存第{i}页墨迹到文件失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -360,41 +324,50 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 从文件加载已保存的墨迹
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 从自动保存目录加载已保存的幻灯片墨迹数据到内存流中,供后续显示和编辑使用。
|
||||
/// 仅在启用自动保存且已设置 AutoSaveLocation 时执行。函数获取当前演示文稿的自动保存文件夹,遍历以 <c>.icstk</c> 为扩展名的文件,
|
||||
/// 将文件名(去除扩展名)解析为幻灯片索引并在合法且文件大小大于 8 字节时加载到对应的内存流槽位。对单个文件的读取失败会记录错误并继续处理其他文件;
|
||||
/// 若成功加载则会记录已加载页数。方法在内部使用锁以保证线程安全。
|
||||
/// </remarks>
|
||||
public void LoadSavedStrokes()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation)) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var folderPath = GetPresentationFolderPath();
|
||||
string folderPath = GetPresentationFolderPath();
|
||||
if (!Directory.Exists(folderPath)) return;
|
||||
|
||||
var files = new DirectoryInfo(folderPath).GetFiles("*.icstk");
|
||||
var dir = new DirectoryInfo(folderPath);
|
||||
int loadedCount = 0;
|
||||
|
||||
foreach (var file in files)
|
||||
foreach (FileInfo file in dir.GetFiles("*" + StrokeFileExtension))
|
||||
{
|
||||
string nameWithoutExt = Path.GetFileNameWithoutExtension(file.Name);
|
||||
if (!int.TryParse(nameWithoutExt, out int slideIndex) || slideIndex <= 0) continue;
|
||||
if (slideIndex >= _memoryStreams.Length) continue;
|
||||
|
||||
try
|
||||
{
|
||||
if (int.TryParse(Path.GetFileNameWithoutExtension(file.Name), out int slideIndex))
|
||||
byte[] bytes = File.ReadAllBytes(file.FullName);
|
||||
if (bytes.Length > 8)
|
||||
{
|
||||
if (slideIndex > 0 && slideIndex < _memoryStreams.Length)
|
||||
{
|
||||
var fileBytes = File.ReadAllBytes(file.FullName);
|
||||
_memoryStreams[slideIndex] = new MemoryStream(fileBytes);
|
||||
_memoryStreams[slideIndex].Position = 0;
|
||||
loadedCount++;
|
||||
}
|
||||
_memoryStreams[slideIndex] = new MemoryStream(bytes);
|
||||
_memoryStreams[slideIndex].Position = 0;
|
||||
loadedCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载墨迹文件{file.Name}失败: {ex}", LogHelper.LogType.Error);
|
||||
LogHelper.WriteLogToFile($"加载墨迹文件 {file.Name} 失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedCount > 0)
|
||||
LogHelper.WriteLogToFile($"已从磁盘加载 {loadedCount} 页墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -406,87 +379,53 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 清除所有墨迹
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 清除并释放当前演示文稿所有幻灯片的墨迹数据和相关内存资源。
|
||||
/// 该方法在内部加锁以保证线程安全;会处置并清空所有内部存储的墨迹流、重建内部流数组并清空 CurrentStrokes。
|
||||
/// </remarks>
|
||||
public void ClearAllStrokes()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 安全释放所有内存流
|
||||
if (_memoryStreams != 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();
|
||||
LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清除墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
ClearAllStrokesInternal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻页后锁定墨迹写入
|
||||
/// 为指定幻灯片设置短时墨迹写入锁,防止在该时间窗口内对其他幻灯片进行写入操作。
|
||||
/// </summary>
|
||||
/// <param name="slideIndex">要上锁的幻灯片索引(大于 0)。锁从调用时刻开始,持续 InkLockMilliseconds 毫秒。</param>
|
||||
public void LockInkForSlide(int slideIndex)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_inkLockUntil = DateTime.Now.AddMilliseconds(InkLockMilliseconds);
|
||||
_lockedSlideIndex = slideIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以写入墨迹
|
||||
/// 确定在当前滑页上下文中是否允许写入墨迹(基于短期的墨迹写入锁与容差窗口)。
|
||||
/// </summary>
|
||||
/// <param name="currentSlideIndex">当前尝试写入墨迹的幻灯片索引(从 1 开始)。</param>
|
||||
/// <returns>`true` 如果允许写入墨迹(锁已过期、目标为被锁定的幻灯片,或处于短暂的容差窗口内),`false` 否则。</returns>
|
||||
public bool CanWriteInk(int currentSlideIndex)
|
||||
{
|
||||
// 如果锁定时间已过,允许写入
|
||||
if (DateTime.Now >= _inkLockUntil)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果当前页面与锁定页面相同,允许写入(用户在当前页面绘制)
|
||||
if (currentSlideIndex == _lockedSlideIndex)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果当前页面不是锁定页面,但锁定时间很短(小于50ms),允许写入
|
||||
// 这样可以确保旧页面的墨迹能够及时保存
|
||||
if (DateTime.Now - (_inkLockUntil.AddMilliseconds(-InkLockMilliseconds)) < TimeSpan.FromMilliseconds(50))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 只有在快速切换且页面不同时才锁定
|
||||
ThrowIfDisposed();
|
||||
if (DateTime.Now >= _inkLockUntil) return true;
|
||||
if (currentSlideIndex == _lockedSlideIndex) return true;
|
||||
if (DateTime.Now - (_inkLockUntil.AddMilliseconds(-InkLockMilliseconds)) < TimeSpan.FromMilliseconds(50)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置墨迹锁定状态
|
||||
/// 重置与墨迹书写和幻灯片切换相关的锁与跟踪状态为初始(未锁定)值。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 将内部的墨迹写入到期时间、当前被锁定的幻灯片索引、上次切换时间和上次切换的幻灯片索引均恢复为默认未设置状态。
|
||||
/// </remarks>
|
||||
public void ResetLockState()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
lock (_lockObject)
|
||||
{
|
||||
_inkLockUntil = DateTime.MinValue;
|
||||
@@ -496,51 +435,90 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// 检查并执行内存清理
|
||||
/// 释放并清除类内用于存储各页墨迹的所有内存流,清空当前画笔集合,并重置内部内存流数组容量为 _maxSlides + 2。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 会逐个释放已存在的 MemoryStream(忽略释放过程中的异常),并将对应槽位设为 null。
|
||||
/// - 会清空 CurrentStrokes 集合。
|
||||
/// - 会记录一条跟踪日志,指示已完成清除操作。
|
||||
/// </remarks>
|
||||
private void ClearAllStrokesInternal()
|
||||
{
|
||||
if (_memoryStreams != 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();
|
||||
LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用指定的笔迹集合替换内部存储中对应幻灯片索引的内存流:释放(并忽略释放错误)旧流,将 <paramref name="strokes"/> 序列化到新的 <see cref="MemoryStream"/> 并保存回内部数组。
|
||||
/// </summary>
|
||||
/// <param name="slideIndex">要替换的幻灯片索引(内部内存流数组的索引)。</param>
|
||||
/// <param name="strokes">要序列化并保存到内存流的笔迹集合。</param>
|
||||
private void ReplaceSlideStream(int slideIndex, StrokeCollection strokes)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定文件夹删除对应幻灯片的笔迹文件(按四位索引命名);如果文件不存在或删除失败则静默忽略错误。
|
||||
/// </summary>
|
||||
/// <param name="folderPath">存放笔迹文件的文件夹路径。</param>
|
||||
/// <param name="slideIndex">用于生成文件名的幻灯片索引(格式化为四位,例如 1 -> "0001")。</param>
|
||||
private void TryDeleteStrokeFile(string folderPath, int slideIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = Path.Combine(folderPath, slideIndex.ToString("0000") + StrokeFileExtension);
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查当前墨迹内存使用状况并在超过阈值时触发清理操作。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 会更新内部的内存使用统计并刷新上次清理时间;当总占用超过 MaxMemoryUsageBytes 时,会记录警告并调用 CleanupInactiveSlideStrokes 清理不活跃幻灯页的墨迹流。若检查或清理过程中发生异常,会记录错误日志。
|
||||
/// </remarks>
|
||||
private void CheckAndPerformMemoryCleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
if (now - _lastMemoryCleanup < TimeSpan.FromMinutes(MemoryCleanupIntervalMinutes)) return;
|
||||
|
||||
// 检查是否需要执行内存清理
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (_memoryStreams[i] != null) currentMemoryUsage += _memoryStreams[i].Length;
|
||||
}
|
||||
|
||||
_totalMemoryUsage = currentMemoryUsage;
|
||||
|
||||
// 如果内存使用量超过限制,执行清理
|
||||
if (currentMemoryUsage > MaxMemoryUsageBytes)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"内存使用量超限 ({currentMemoryUsage / 1024 / 1024}MB),开始清理", LogHelper.LogType.Warning);
|
||||
|
||||
// 清理非当前页面的墨迹
|
||||
LogHelper.WriteLogToFile($"墨迹内存超限 ({currentMemoryUsage / (1024 * 1024)}MB),执行清理", LogHelper.LogType.Warning);
|
||||
CleanupInactiveSlideStrokes();
|
||||
|
||||
_lastMemoryCleanup = now;
|
||||
LogHelper.WriteLogToFile($"内存清理完成,当前使用量: {_totalMemoryUsage / 1024 / 1024}MB", LogHelper.LogType.Trace);
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastMemoryCleanup = now;
|
||||
}
|
||||
_lastMemoryCleanup = now;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -549,111 +527,78 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理非活跃页面的墨迹
|
||||
/// 清理不活跃幻灯片的内存化墨迹数据以回收内存空间。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 将释放除当前锁定幻灯片与最近切换幻灯片之外的每页内存流(若存在),并将对应数组项设为 null;完成后若有释放,会记录已清理页数与释放的总大小(KB)。
|
||||
/// </remarks>
|
||||
private void CleanupInactiveSlideStrokes()
|
||||
{
|
||||
try
|
||||
if (_memoryStreams == null) return;
|
||||
int cleaned = 0;
|
||||
long freed = 0;
|
||||
for (int i = 0; i < _memoryStreams.Length; i++)
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 保留当前锁定页面和最近访问的页面
|
||||
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);
|
||||
long len = _memoryStreams[i].Length;
|
||||
try { _memoryStreams[i].Dispose(); freed += len; cleaned++; }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
finally { _memoryStreams[i] = null; }
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理非活跃页面墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
if (cleaned > 0)
|
||||
LogHelper.WriteLogToFile($"已清理 {cleaned} 页墨迹,释放 {freed / 1024}KB", LogHelper.LogType.Trace);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
/// <summary>
|
||||
/// 生成基于演示文稿名称、幻灯片数量和路径哈希的标识符字符串。
|
||||
/// </summary>
|
||||
/// <returns>由 `名称_幻灯片数_路径哈希` 组成的标识符;若生成失败则返回形如 `unknown_{ticks}` 的回退标识符。</returns>
|
||||
private string GeneratePresentationId(Presentation presentation)
|
||||
{
|
||||
try
|
||||
{
|
||||
var presentationPath = presentation.FullName;
|
||||
var fileHash = GetFileHash(presentationPath);
|
||||
return $"{presentation.Name}_{presentation.Slides.Count}_{fileHash}";
|
||||
string path = presentation.FullName;
|
||||
string hash = HashHelper.GetFileHash(path);
|
||||
return $"{presentation.Name}_{presentation.Slides.Count}_{hash}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"生成演示文稿ID失败: {ex}", LogHelper.LogType.Error);
|
||||
LogHelper.WriteLogToFile($"生成演示文稿 ID 失败: {ex}", LogHelper.LogType.Error);
|
||||
return $"unknown_{DateTime.Now.Ticks}";
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFileHash(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath)) return "unknown";
|
||||
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(filePath));
|
||||
return BitConverter.ToString(hashBytes).Replace("-", "").Substring(0, 8);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"计算文件哈希值失败: {ex}", LogHelper.LogType.Error);
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
private string GetPresentationFolderPath()
|
||||
{
|
||||
return Path.Combine(AutoSaveLocation, "Auto Saved - Presentations", _currentPresentationId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dispose
|
||||
|
||||
/// <summary>
|
||||
/// 释放 PPTInkManager 持有的资源并清除所有内存中的笔迹数据。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 调用后该实例将进入已释放状态,不应再被使用。方法为幂等且线程安全:如果已释放则立即返回,否则在同步区内清理资源并标记为已释放。
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
ClearAllStrokes();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
if (_disposed) return;
|
||||
lock (_lockObject) { ClearAllStrokesInternal(); }
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(PPTInkManager));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
+527
-296
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,522 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using System.Text;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public static class PPTROTConnectionHelper
|
||||
{
|
||||
#region Win32 API Declarations
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int CreateBindCtx(int reserved, out IBindCtx ppbc);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct RECT
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
private static readonly Guid PowerPointApplicationGuid = new Guid("91493441-5A91-11CF-8700-00AA0060263B");
|
||||
|
||||
private static readonly string[] PptLikeExtensions = new[]
|
||||
{
|
||||
".pptx", ".pptm", ".ppt",
|
||||
".ppsx", ".ppsm", ".pps",
|
||||
".potx", ".potm", ".pot",
|
||||
".dps", ".dpt"
|
||||
};
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
public static Microsoft.Office.Interop.PowerPoint.Application TryConnectViaROT(bool isSupportWPS = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
object bestApp = GetAnyActivePowerPoint(null, out int bestPriority, out _);
|
||||
|
||||
if (bestApp != null && bestPriority > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
Type appType = typeof(Microsoft.Office.Interop.PowerPoint.Application);
|
||||
Microsoft.Office.Interop.PowerPoint.Application pptApp = null;
|
||||
if (appType.IsInstanceOfType(bestApp))
|
||||
{
|
||||
pptApp = (Microsoft.Office.Interop.PowerPoint.Application)bestApp;
|
||||
}
|
||||
if (pptApp != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var nameObj = pptApp.GetType().InvokeMember("Name", BindingFlags.GetProperty, null, pptApp, null);
|
||||
SafeReleaseComObject(nameObj);
|
||||
return pptApp;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ROT 连接验证 Name 不可用(将依赖 SlideShowWindows): {ex.Message}", LogHelper.LogType.Warning);
|
||||
return pptApp;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SafeReleaseComObject(bestApp);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ROT 连接验证失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
SafeReleaseComObject(bestApp);
|
||||
}
|
||||
}
|
||||
else if (bestApp != null)
|
||||
{
|
||||
SafeReleaseComObject(bestApp);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("PowerPoint.Application");
|
||||
if (pptApp != null && Marshal.IsComObject(pptApp))
|
||||
{
|
||||
try
|
||||
{
|
||||
var _ = pptApp.Name;
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
}
|
||||
return pptApp;
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
|
||||
if (isSupportWPS)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("kwpp.Application");
|
||||
if (wpsApp != null && Marshal.IsComObject(wpsApp))
|
||||
{
|
||||
try
|
||||
{
|
||||
var _ = wpsApp.Name;
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
}
|
||||
return wpsApp;
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ROT 连接过程发生异常: {ex}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// 在系统的运行对象表(ROT)中查找并返回最合适的正在运行的 PowerPoint 应用实例。
|
||||
/// </summary>
|
||||
/// <param name="targetApp">可选的目标 PowerPoint COM 对象,用于优先比较;传入 null 表示不指定目标。</param>
|
||||
/// <param name="bestPriority">输出参数:返回找到的最佳实例的优先级(0 表示未找到或无活动演示)。</param>
|
||||
/// <param name="targetPriority">输出参数:返回与 <paramref name="targetApp"/> 对应实例的优先级(如果未提供或未命中则为 0)。</param>
|
||||
/// <returns>最合适的 PowerPoint 应用对象(通常为 COM Application 实例),若未找到则返回 null。</returns>
|
||||
public static object GetAnyActivePowerPoint(object targetApp, out int bestPriority, out int targetPriority)
|
||||
{
|
||||
IRunningObjectTable rot = null;
|
||||
IEnumMoniker enumMoniker = null;
|
||||
|
||||
object bestApp = null;
|
||||
bestPriority = 0;
|
||||
targetPriority = 0;
|
||||
int highestPriority = 0;
|
||||
|
||||
List<object> foundAppObjects = new List<object>();
|
||||
|
||||
try
|
||||
{
|
||||
int hr = GetRunningObjectTable(0, out rot);
|
||||
if (hr != 0 || rot == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("无法获取 Running Object Table", LogHelper.LogType.Warning);
|
||||
return null;
|
||||
}
|
||||
|
||||
rot.EnumRunning(out enumMoniker);
|
||||
if (enumMoniker == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("无法枚举 ROT 中的对象", LogHelper.LogType.Warning);
|
||||
return null;
|
||||
}
|
||||
|
||||
IMoniker[] moniker = new IMoniker[1];
|
||||
IntPtr fetched = IntPtr.Zero;
|
||||
|
||||
while (enumMoniker.Next(1, moniker, fetched) == 0)
|
||||
{
|
||||
IBindCtx bindCtx = null;
|
||||
object comObject = null;
|
||||
dynamic candidateApp = null;
|
||||
string displayName = "Unknown";
|
||||
dynamic activePres = null;
|
||||
dynamic ssWindow = null;
|
||||
bool keepAlive = false;
|
||||
|
||||
try
|
||||
{
|
||||
CreateBindCtx(0, out bindCtx);
|
||||
moniker[0].GetDisplayName(bindCtx, null, out displayName);
|
||||
|
||||
if (LooksLikePresentationFile(displayName) || displayName == "!{91493441-5A91-11CF-8700-00AA0060263B}")
|
||||
{
|
||||
rot.GetObject(moniker[0], out comObject);
|
||||
if (comObject != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
object appObj = comObject.GetType().InvokeMember("Application", BindingFlags.GetProperty, null, comObject, null);
|
||||
candidateApp = appObj;
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
bool isDuplicate = false;
|
||||
if (candidateApp != null)
|
||||
{
|
||||
foreach (var processedApp in foundAppObjects)
|
||||
{
|
||||
if (AreComObjectsEqual((object)candidateApp, processedApp))
|
||||
{
|
||||
isDuplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDuplicate)
|
||||
{
|
||||
foundAppObjects.Add(candidateApp);
|
||||
keepAlive = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateApp != null && !isDuplicate)
|
||||
{
|
||||
int currentPriority = 0;
|
||||
bool isTarget = false;
|
||||
|
||||
if (targetApp != null && AreComObjectsEqual((object)candidateApp, targetApp))
|
||||
{
|
||||
isTarget = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
activePres = candidateApp.ActivePresentation;
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
if (activePres != null)
|
||||
{
|
||||
currentPriority = 1;
|
||||
|
||||
try
|
||||
{
|
||||
ssWindow = activePres.SlideShowWindow;
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
if (ssWindow != null)
|
||||
{
|
||||
currentPriority = 2;
|
||||
|
||||
try
|
||||
{
|
||||
bool isActive = false;
|
||||
try
|
||||
{
|
||||
object val = ssWindow.Active;
|
||||
if (val is int && (int)val == -1) isActive = true;
|
||||
else if (val is bool && (bool)val == true) isActive = true;
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
currentPriority = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsSlideShowWindowActive(ssWindow))
|
||||
{
|
||||
currentPriority = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"计算优先级时出错: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
if (isTarget)
|
||||
{
|
||||
targetPriority = currentPriority;
|
||||
}
|
||||
|
||||
if (currentPriority > 0)
|
||||
{
|
||||
if (currentPriority > highestPriority)
|
||||
{
|
||||
highestPriority = currentPriority;
|
||||
SafeReleaseComObject(bestApp);
|
||||
bestApp = candidateApp;
|
||||
candidateApp = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ROT 枚举循环中出错: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SafeReleaseComObject(ssWindow);
|
||||
SafeReleaseComObject(activePres);
|
||||
|
||||
if (!keepAlive)
|
||||
{
|
||||
SafeReleaseComObject(candidateApp);
|
||||
}
|
||||
|
||||
CleanUpLoopObjects(bindCtx, moniker[0], comObject);
|
||||
}
|
||||
}
|
||||
|
||||
bestPriority = highestPriority;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ROT 扫描关键错误: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (foundAppObjects != null)
|
||||
{
|
||||
foreach (var cachedApp in foundAppObjects)
|
||||
{
|
||||
if (bestApp != null && ReferenceEquals(cachedApp, bestApp))
|
||||
continue;
|
||||
|
||||
SafeReleaseComObject(cachedApp);
|
||||
}
|
||||
foundAppObjects.Clear();
|
||||
}
|
||||
|
||||
if (enumMoniker != null) Marshal.ReleaseComObject(enumMoniker);
|
||||
if (rot != null) Marshal.ReleaseComObject(rot);
|
||||
}
|
||||
|
||||
return bestApp;
|
||||
}
|
||||
|
||||
public static bool AreComObjectsEqual(object o1, object o2)
|
||||
{
|
||||
if (o1 == null || o2 == null) return false;
|
||||
if (ReferenceEquals(o1, o2)) return true;
|
||||
|
||||
IntPtr pUnk1 = IntPtr.Zero;
|
||||
IntPtr pUnk2 = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
pUnk1 = Marshal.GetIUnknownForObject(o1);
|
||||
pUnk2 = Marshal.GetIUnknownForObject(o2);
|
||||
return pUnk1 == pUnk2;
|
||||
}
|
||||
catch { return false; }
|
||||
finally
|
||||
{
|
||||
if (pUnk1 != IntPtr.Zero) Marshal.Release(pUnk1);
|
||||
if (pUnk2 != IntPtr.Zero) Marshal.Release(pUnk2);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool LooksLikePresentationFile(string displayName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(displayName))
|
||||
return false;
|
||||
|
||||
string lower = displayName.ToLowerInvariant();
|
||||
foreach (var ext in PptLikeExtensions)
|
||||
{
|
||||
if (lower.Contains(ext))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsSlideShowWindowActive(object sswObj)
|
||||
{
|
||||
try
|
||||
{
|
||||
dynamic ssw = sswObj;
|
||||
|
||||
IntPtr foregroundHwnd = GetForegroundWindow();
|
||||
if (foregroundHwnd == IntPtr.Zero) return false;
|
||||
|
||||
uint fgPid;
|
||||
GetWindowThreadProcessId(foregroundHwnd, out fgPid);
|
||||
|
||||
IntPtr sswHwnd = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
sswHwnd = GetPptHwndFromSlideShowWindow(sswObj);
|
||||
}
|
||||
catch { return false; }
|
||||
if (sswHwnd == IntPtr.Zero) return false;
|
||||
|
||||
uint sswPid;
|
||||
GetWindowThreadProcessId(sswHwnd, out sswPid);
|
||||
|
||||
if (fgPid == sswPid) return true;
|
||||
|
||||
try
|
||||
{
|
||||
using (Process fgProc = Process.GetProcessById((int)fgPid))
|
||||
using (Process appProc = Process.GetProcessById((int)sswPid))
|
||||
{
|
||||
string fgName = fgProc.ProcessName.ToLower();
|
||||
string appName = appProc.ProcessName.ToLower();
|
||||
|
||||
if (fgName.StartsWith("wps") && appName.StartsWith("wpp"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr GetPptHwndFromSlideShowWindow(object pptSlideShowWindowObj)
|
||||
{
|
||||
IntPtr hwnd = IntPtr.Zero;
|
||||
if (pptSlideShowWindowObj == null) return IntPtr.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
Microsoft.Office.Interop.PowerPoint.SlideShowWindow slideWindow = (Microsoft.Office.Interop.PowerPoint.SlideShowWindow)pptSlideShowWindowObj;
|
||||
|
||||
int hwndVal = slideWindow.HWND;
|
||||
|
||||
hwnd = new IntPtr(hwndVal);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
return hwnd;
|
||||
}
|
||||
|
||||
public static void SafeReleaseComObject(object comObj)
|
||||
{
|
||||
if (comObj == null) return;
|
||||
|
||||
if (Marshal.IsComObject(comObj))
|
||||
{
|
||||
try
|
||||
{
|
||||
Marshal.ReleaseComObject(comObj);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
|
||||
private static void CleanUpLoopObjects(IBindCtx bindCtx, IMoniker moniker, object comObject)
|
||||
{
|
||||
if (comObject != null && Marshal.IsComObject(comObject))
|
||||
Marshal.ReleaseComObject(comObject);
|
||||
if (moniker != null)
|
||||
Marshal.ReleaseComObject(moniker);
|
||||
if (bindCtx != null)
|
||||
Marshal.ReleaseComObject(bindCtx);
|
||||
}
|
||||
|
||||
public static int GetSlideShowWindowsCount(Microsoft.Office.Interop.PowerPoint.Application pptApp)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (pptApp == null) return 0;
|
||||
return pptApp.SlideShowWindows.Count;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsValidSlideShowWindow(object pptSlideShowWindow)
|
||||
{
|
||||
if (pptSlideShowWindow == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
dynamic ssw = pptSlideShowWindow;
|
||||
var _ = ssw.Active;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ namespace Ink_Canvas.Helpers
|
||||
public int PPTRBButtonPosition { get; set; } = 0;
|
||||
public bool EnablePPTButtonPageClickable { get; set; } = true;
|
||||
public bool EnablePPTButtonLongPressPageTurn { get; set; } = true;
|
||||
public double PPTLSButtonOpacity { get; set; } = 0.5;
|
||||
public double PPTRSButtonOpacity { get; set; } = 0.5;
|
||||
public double PPTLBButtonOpacity { get; set; } = 0.5;
|
||||
public double PPTRBButtonOpacity { get; set; } = 0.5;
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
@@ -97,6 +101,8 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
UpdateNavigationPanelsVisibility();
|
||||
UpdateNavigationButtonStyles();
|
||||
_mainWindow.UpdatePPTTimeCapsuleVisibility();
|
||||
_mainWindow.UpdatePPTQuickPanelVisibility();
|
||||
if (MainWindow.Settings.Advanced.IsEnableAvoidFullScreenHelper)
|
||||
{
|
||||
// 设置为画板模式,允许全屏操作
|
||||
@@ -107,6 +113,8 @@ namespace Ink_Canvas.Helpers
|
||||
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
|
||||
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height, true);
|
||||
}), DispatcherPriority.ApplicationIdle);
|
||||
|
||||
_mainWindow.isFullScreenApplied = true; // 标记已应用全屏处理
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -114,6 +122,8 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Visible;
|
||||
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Collapsed;
|
||||
HideAllNavigationPanels();
|
||||
_mainWindow.UpdatePPTTimeCapsuleVisibility();
|
||||
_mainWindow.UpdatePPTQuickPanelVisibility();
|
||||
if (MainWindow.Settings.Advanced.IsEnableAvoidFullScreenHelper)
|
||||
{
|
||||
// 恢复为非画板模式,重新启用全屏限制
|
||||
@@ -127,6 +137,8 @@ namespace Ink_Canvas.Helpers
|
||||
workingArea.X, workingArea.Y,
|
||||
workingArea.Width, workingArea.Height, true);
|
||||
}), DispatcherPriority.ApplicationIdle);
|
||||
|
||||
_mainWindow.isFullScreenApplied = false; // 标记全屏处理已还原
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,10 +389,9 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow.PPTLSPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.PPTRSPageButton.Visibility = pageButtonVisibility;
|
||||
|
||||
// 透明度设置
|
||||
var opacity = options[1] == '2' ? 0.5 : 1.0;
|
||||
_mainWindow.PPTBtnLSBorder.Opacity = opacity;
|
||||
_mainWindow.PPTBtnRSBorder.Opacity = opacity;
|
||||
// 透明度设置 - 直接使用用户设置的透明度值
|
||||
_mainWindow.PPTBtnLSBorder.Opacity = PPTLSButtonOpacity;
|
||||
_mainWindow.PPTBtnRSBorder.Opacity = PPTRSButtonOpacity;
|
||||
|
||||
// 颜色主题
|
||||
bool isDarkTheme = options[2] == '2';
|
||||
@@ -406,10 +417,9 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow.PPTLBPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.PPTRBPageButton.Visibility = pageButtonVisibility;
|
||||
|
||||
// 透明度设置
|
||||
var opacity = options[1] == '2' ? 0.5 : 1.0;
|
||||
_mainWindow.PPTBtnLBBorder.Opacity = opacity;
|
||||
_mainWindow.PPTBtnRBBorder.Opacity = opacity;
|
||||
// 透明度设置 - 直接使用用户设置的透明度值
|
||||
_mainWindow.PPTBtnLBBorder.Opacity = PPTLBButtonOpacity;
|
||||
_mainWindow.PPTBtnRBBorder.Opacity = PPTRBButtonOpacity;
|
||||
|
||||
// 颜色主题
|
||||
bool isDarkTheme = options[2] == '2';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using iNKORE.UI.WPF.Controls;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
@@ -207,7 +207,7 @@ namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try { Close(); } catch { }
|
||||
try { Close(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
// 启动应用程序任务
|
||||
launchTask.Start();
|
||||
@@ -224,7 +224,7 @@ namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用按钮点击事件出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
try { IsClosing = true; Close(); } catch { }
|
||||
try { IsClosing = true; Close(); } catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
internal static class ProcessProtectionManager
|
||||
{
|
||||
private static readonly object _lock = new object();
|
||||
private static readonly Dictionary<string, FileStream> _lockedFiles = new Dictionary<string, FileStream>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, SafeFileHandle> _lockedDirs = new Dictionary<string, SafeFileHandle>(StringComparer.OrdinalIgnoreCase);
|
||||
private static bool _enabled;
|
||||
private static int _writeGate;
|
||||
private static readonly string[] _excludedSubDirectories = new[]
|
||||
{
|
||||
"Configs",
|
||||
"Saves",
|
||||
"Backups",
|
||||
"Logs",
|
||||
"AutoUpdate"
|
||||
};
|
||||
|
||||
public static bool Enabled
|
||||
{
|
||||
get { lock (_lock) return _enabled; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从应用设置读取 EnableProcessProtection 并相应地启用或禁用进程保护。
|
||||
/// </summary>
|
||||
public static void ApplyFromSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = MainWindow.Settings;
|
||||
var enabled = settings?.Security != null && settings.Security.EnableProcessProtection;
|
||||
SetEnabled(enabled);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ProcessProtectionManager.ApplyFromSettings 失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换进程保护的启用状态;在状态发生变化时触发相应的启用或禁用操作并保证线程安全。
|
||||
/// </summary>
|
||||
/// <param name="enabled">为 `true` 时启用进程保护,为 `false` 时禁用进程保护。</param>
|
||||
public static void SetEnabled(bool enabled)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_enabled == enabled) return;
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (enabled) Enable();
|
||||
else Disable();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ProcessProtectionManager.SetEnabled 后台执行失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在受进程保护的上下文中执行对指定目标的写入操作;在执行时会在必要情况下临时释放针对目标路径及其父目录的锁,执行完成后恢复这些锁。
|
||||
/// </summary>
|
||||
/// <param name="targetPath">目标文件或目录的路径,用于确定需要临时释放和随后恢复的锁。</param>
|
||||
/// <param name="action">执行写入的操作委托,不能为空。</param>
|
||||
/// <remarks>
|
||||
/// 如果 ProcessProtectionManager.Enabled 为 false,会直接执行 <paramref name="action"/>;
|
||||
/// 若在有限时间内无法获取写入门闩,会记录警告并降级为直接执行 <paramref name="action"/>。方法在内部处理异常,不会抛出异常给调用者。
|
||||
/// </remarks>
|
||||
public static void WithWriteAccess(string targetPath, Action action)
|
||||
{
|
||||
if (action == null) return;
|
||||
|
||||
if (!Enabled)
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
const int gateTimeoutMs = 10_000;
|
||||
if (!TryEnterWriteGate(gateTimeoutMs))
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ProcessProtectionManager.WithWriteAccess: 获取写入门闩超时({gateTimeoutMs}ms),将降级释放目标路径锁后执行写入。目标: {targetPath}",
|
||||
LogHelper.LogType.Warning);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[ProcessProtectionManager] 写日志失败: {ex.Message}");
|
||||
}
|
||||
|
||||
var normPath = NormalizePath(targetPath);
|
||||
var dirsChain = GetDirChainToRoot(normPath);
|
||||
Dictionary<string, SafeFileHandle> fallbackDirs = null;
|
||||
Dictionary<string, FileStream> fallbackFiles = null;
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
fallbackDirs = new Dictionary<string, SafeFileHandle>(StringComparer.OrdinalIgnoreCase);
|
||||
fallbackFiles = new Dictionary<string, FileStream>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var dir in dirsChain)
|
||||
{
|
||||
if (_lockedDirs.TryGetValue(dir, out var handle))
|
||||
{
|
||||
_lockedDirs.Remove(dir);
|
||||
fallbackDirs[dir] = handle;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(normPath) && File.Exists(normPath) && _lockedFiles.TryGetValue(normPath, out var fs))
|
||||
{
|
||||
_lockedFiles.Remove(normPath);
|
||||
fallbackFiles[normPath] = fs;
|
||||
}
|
||||
}
|
||||
|
||||
if (fallbackFiles != null)
|
||||
{
|
||||
foreach (var kv in fallbackFiles)
|
||||
{
|
||||
try { kv.Value.Dispose(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
if (fallbackDirs != null)
|
||||
{
|
||||
foreach (var kv in fallbackDirs)
|
||||
{
|
||||
try { kv.Value.Dispose(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
|
||||
action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
Enable(rescanRoot: false, rescanDirs: dirsChain);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var normalized = NormalizePath(targetPath);
|
||||
var dirsToToggle = GetDirChainToRoot(normalized);
|
||||
|
||||
Dictionary<string, SafeFileHandle> releasedDirs = null;
|
||||
Dictionary<string, FileStream> releasedFiles = null;
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
releasedDirs = new Dictionary<string, SafeFileHandle>(StringComparer.OrdinalIgnoreCase);
|
||||
releasedFiles = new Dictionary<string, FileStream>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var dir in dirsToToggle)
|
||||
{
|
||||
if (_lockedDirs.TryGetValue(dir, out var handle))
|
||||
{
|
||||
_lockedDirs.Remove(dir);
|
||||
releasedDirs[dir] = handle;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(normalized) && File.Exists(normalized) && _lockedFiles.TryGetValue(normalized, out var fs))
|
||||
{
|
||||
_lockedFiles.Remove(normalized);
|
||||
releasedFiles[normalized] = fs;
|
||||
}
|
||||
}
|
||||
|
||||
if (releasedFiles != null)
|
||||
{
|
||||
foreach (var kv in releasedFiles)
|
||||
{
|
||||
try { kv.Value.Dispose(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
if (releasedDirs != null)
|
||||
{
|
||||
foreach (var kv in releasedDirs)
|
||||
{
|
||||
try { kv.Value.Dispose(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
|
||||
action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
Enable(rescanRoot: false, rescanDirs: dirsToToggle);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _writeGate, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试在指定的毫秒数内获取写入门控(write gate)。
|
||||
/// </summary>
|
||||
/// <param name="timeoutMs">等待超时时间(毫秒)。小于或等于 0 时视为 1 毫秒。</param>
|
||||
/// <returns>`true` 如果在指定时间内成功获取到写入门控,`false` 否则。</returns>
|
||||
private static bool TryEnterWriteGate(int timeoutMs)
|
||||
{
|
||||
if (timeoutMs <= 0) timeoutMs = 1;
|
||||
|
||||
var start = Environment.TickCount;
|
||||
while (Interlocked.CompareExchange(ref _writeGate, 1, 0) != 0)
|
||||
{
|
||||
var elapsed = unchecked(Environment.TickCount - start);
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
|
||||
if (elapsed < 2000) Thread.Sleep(10);
|
||||
else Thread.Sleep(50);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用进程保护并对应用根路径进行完整重扫描以锁定需要保护的目录和文件。
|
||||
/// </summary>
|
||||
private static void Enable()
|
||||
{
|
||||
Enable(rescanRoot: true, rescanDirs: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在应用根目录或提供的路径集合上建立目录句柄和文件读取锁以启用进程保护。
|
||||
/// </summary>
|
||||
/// <param name="rescanRoot">为 true 时对 App.RootPath 进行递归扫描并锁定其下的目录与文件;为 false 时仅处理 <paramref name="rescanDirs"/> 指定的路径(若为 null 则不处理)。</param>
|
||||
/// <param name="rescanDirs">当 <paramref name="rescanRoot"/> 为 false 时,按项对存在的目录建立目录锁,对存在的文件建立文件锁;可为 null。</param>
|
||||
private static void Enable(bool rescanRoot, IEnumerable<string> rescanDirs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = App.RootPath;
|
||||
if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) return;
|
||||
root = NormalizePath(root);
|
||||
|
||||
if (rescanRoot)
|
||||
{
|
||||
LockDirectoryRecursive(root);
|
||||
}
|
||||
else if (rescanDirs != null)
|
||||
{
|
||||
foreach (var d in rescanDirs)
|
||||
{
|
||||
if (Directory.Exists(d))
|
||||
{
|
||||
LockDirectory(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rescanRoot)
|
||||
{
|
||||
LockFilesRecursive(root);
|
||||
}
|
||||
else if (rescanDirs != null)
|
||||
{
|
||||
foreach (var d in rescanDirs)
|
||||
{
|
||||
if (Directory.Exists(d))
|
||||
{
|
||||
LockFilesRecursive(d);
|
||||
}
|
||||
else if (File.Exists(d))
|
||||
{
|
||||
LockFile(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放并清除当前进程持有的所有文件和目录锁定句柄与流资源。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 在内部同步锁定下逐一 Dispose 已记录的 FileStream 和 SafeFileHandle,并清空对应的缓存字典;
|
||||
/// 释放过程中发生的异常会被忽略(吞掉)。
|
||||
/// </remarks>
|
||||
private static void Disable()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var kv in _lockedFiles)
|
||||
{
|
||||
try { kv.Value.Dispose(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
_lockedFiles.Clear();
|
||||
|
||||
foreach (var kv in _lockedDirs)
|
||||
{
|
||||
try { kv.Value.Dispose(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
_lockedDirs.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归地尝试为指定目录及其所有子目录建立并保持目录句柄锁定,跳过配置的排除目录。
|
||||
/// </summary>
|
||||
/// <param name="root">起始目录的路径;从此路径开始遍历并对符合条件的子目录尝试建立锁定。</param>
|
||||
/// <remarks>遇到的异常会被捕获并忽略,不会向调用方抛出。</remarks>
|
||||
private static void LockDirectoryRecursive(string root)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsExcludedPath(root))
|
||||
{
|
||||
LockDirectory(root);
|
||||
}
|
||||
foreach (var dir in Directory.GetDirectories(root, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (!IsExcludedPath(dir))
|
||||
{
|
||||
LockDirectory(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归扫描指定根目录下的所有文件,并为具有特定扩展名的文件建立读取锁定以防止被进程修改或替换。
|
||||
/// </summary>
|
||||
/// <param name="root">要开始扫描的根目录路径。</param>
|
||||
/// <remarks>
|
||||
/// 仅处理扩展名为 `.exe`, `.dll`, `.config`, `.manifest`, `.dat`, `.enc` 的文件;会跳过被 IsExcludedPath 判定为排除的路径。遇到任何 I/O 或访问错误时会静默忽略,不会抛出异常。</remarks>
|
||||
private static void LockFilesRecursive(string root)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(root, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (!IsExcludedPath(file))
|
||||
{
|
||||
var ext = Path.GetExtension(file);
|
||||
if (string.Equals(ext, ".exe", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".config", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".manifest", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".dat", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".enc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
LockFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以只读方式打开并保留指定文件的句柄,将其加入内部锁定缓存以减少该文件被外部修改或删除的可能性。
|
||||
/// </summary>
|
||||
/// <param name="filePath">要锁定的文件的路径(会被规范化为完整路径)。</param>
|
||||
private static void LockFile(string filePath)
|
||||
{
|
||||
filePath = NormalizePath(filePath);
|
||||
lock (_lock)
|
||||
{
|
||||
if (_lockedFiles.ContainsKey(filePath)) return;
|
||||
try
|
||||
{
|
||||
var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
_lockedFiles[filePath] = fs;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试为指定目录获取一个用于保持目录打开的句柄并将其保存为内部锁定记录;若目录已被记录则不作任何操作,发生错误时静默忽略。
|
||||
/// </summary>
|
||||
/// <param name="dirPath">要锁定的目录路径;调用时会对路径进行规范化(转换为完整路径并移除多余分隔符)。</param>
|
||||
private static void LockDirectory(string dirPath)
|
||||
{
|
||||
dirPath = NormalizePath(dirPath);
|
||||
lock (_lock)
|
||||
{
|
||||
if (_lockedDirs.ContainsKey(dirPath)) return;
|
||||
try
|
||||
{
|
||||
var handle = CreateDirectoryHandle(dirPath);
|
||||
if (handle != null && !handle.IsInvalid)
|
||||
{
|
||||
_lockedDirs[dirPath] = handle;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将路径标准化为不含末尾路径分隔符的绝对路径。
|
||||
/// </summary>
|
||||
/// <param name="p">要规范化的路径;如果为 null、空或仅空白,则返回原值。</param>
|
||||
/// <returns>规范化后的路径:在解析成功时返回去除末尾分隔符的绝对路径;在解析失败时返回原始输入。</returns>
|
||||
private static string NormalizePath(string p)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(p)) return p;
|
||||
return Path.GetFullPath(p.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建从指定路径向上直到应用根目录(App.RootPath)的目录链,并按从下到上的顺序返回已规范化的目录路径。
|
||||
/// </summary>
|
||||
/// <param name="path">起始路径,既可为文件路径也可为目录路径;若为文件则使用其所在目录作为起点。</param>
|
||||
/// <returns>包含起始目录及其各级父目录直到并包含应用根目录的列表;当根路径无效或未能匹配到根目录时返回空列表。</returns>
|
||||
private static List<string> GetDirChainToRoot(string path)
|
||||
{
|
||||
var list = new List<string>();
|
||||
try
|
||||
{
|
||||
var root = NormalizePath(App.RootPath);
|
||||
if (string.IsNullOrWhiteSpace(root)) return list;
|
||||
|
||||
string dir = Directory.Exists(path) ? NormalizePath(path) : NormalizePath(Path.GetDirectoryName(path));
|
||||
while (!string.IsNullOrWhiteSpace(dir))
|
||||
{
|
||||
if (!dir.StartsWith(root, StringComparison.OrdinalIgnoreCase)) break;
|
||||
list.Add(dir);
|
||||
if (string.Equals(dir, root, StringComparison.OrdinalIgnoreCase)) break;
|
||||
dir = NormalizePath(Path.GetDirectoryName(dir));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查给定路径是否位于应用根目录下的受排除子目录之一。
|
||||
/// </summary>
|
||||
/// <param name="path">要检查的文件或目录路径。</param>
|
||||
/// <returns>`true` 如果路径位于任何配置为排除的子目录下,`false` 否则。</returns>
|
||||
private static bool IsExcludedPath(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = NormalizePath(App.RootPath);
|
||||
if (string.IsNullOrWhiteSpace(root)) return false;
|
||||
path = NormalizePath(path);
|
||||
foreach (var name in _excludedSubDirectories)
|
||||
{
|
||||
var prefix = Path.Combine(root, name);
|
||||
if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定目录打开一个文件句柄,便于对该目录进行锁定或访问其元数据。
|
||||
/// </summary>
|
||||
/// <param name="dirPath">目标目录的完整路径。</param>
|
||||
/// <returns>表示已打开目录的 <see cref="SafeFileHandle"/>;若无法打开则返回无效的句柄,调用方应检查句柄有效性。</returns>
|
||||
private static SafeFileHandle CreateDirectoryHandle(string dirPath)
|
||||
{
|
||||
const uint GENERIC_READ = 0x80000000;
|
||||
const uint FILE_SHARE_READ = 0x00000001;
|
||||
const uint OPEN_EXISTING = 3;
|
||||
const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
||||
|
||||
return CreateFile(
|
||||
dirPath,
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ,
|
||||
IntPtr.Zero,
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS,
|
||||
IntPtr.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用原生 CreateFile API 打开或创建一个文件/目录并返回底层句柄的包装函数声明。
|
||||
/// </summary>
|
||||
/// <param name="lpFileName">要打开或创建的文件或目录的完整路径(UTF-16 编码)。</param>
|
||||
/// <param name="dwDesiredAccess">请求的访问权限位掩码(例如读取或写入访问)。</param>
|
||||
/// <param name="dwShareMode">共享模式位掩码,指定其他进程可以如何共享此文件句柄。</param>
|
||||
/// <param name="lpSecurityAttributes">指向安全属性结构的指针,或为 <see cref="IntPtr.Zero"/> 表示默认安全性。</param>
|
||||
/// <param name="dwCreationDisposition">指定如何处理已存在或不存在的文件(例如打开、创建或截断)。</param>
|
||||
/// <param name="dwFlagsAndAttributes">文件属性和标志位,用于控制文件或目录的特殊行为(例如备份语义)。</param>
|
||||
/// <param name="hTemplateFile">用于创建新文件时的模板句柄,通常为 <see cref="IntPtr.Zero"/>。</param>
|
||||
/// <returns>表示文件或目录句柄的 <see cref="Microsoft.Win32.SafeHandles.SafeFileHandle"/>;调用失败时返回无效的句柄(可通过检查句柄或调用 <see cref="System.Runtime.InteropServices.Marshal.GetLastWin32Error"/> 获取错误码)。</returns>
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern SafeFileHandle CreateFile(
|
||||
string lpFileName,
|
||||
uint dwDesiredAccess,
|
||||
uint dwShareMode,
|
||||
IntPtr lpSecurityAttributes,
|
||||
uint dwCreationDisposition,
|
||||
uint dwFlagsAndAttributes,
|
||||
IntPtr hTemplateFile);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public static class SafeZipExtractor
|
||||
{
|
||||
/// <param name="zipFilePath">ZIP 文件路径</param>
|
||||
/// <param name="extractPath">解压目标目录</param>
|
||||
/// <param name="overwrite">是否覆盖已存在文件</param>
|
||||
public static void ExtractZipSafely(string zipFilePath, string extractPath, bool overwrite = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(zipFilePath))
|
||||
throw new ArgumentNullException(nameof(zipFilePath));
|
||||
if (string.IsNullOrWhiteSpace(extractPath))
|
||||
throw new ArgumentNullException(nameof(extractPath));
|
||||
|
||||
var fullExtractPath = Path.GetFullPath(extractPath);
|
||||
Directory.CreateDirectory(fullExtractPath);
|
||||
|
||||
using (var zip = ZipFile.OpenRead(zipFilePath))
|
||||
{
|
||||
foreach (var entry in zip.Entries)
|
||||
{
|
||||
// 跳过空条目
|
||||
if (string.IsNullOrEmpty(entry.FullName))
|
||||
continue;
|
||||
|
||||
// 防止绝对路径和盘符前缀
|
||||
if (Path.IsPathRooted(entry.FullName))
|
||||
continue;
|
||||
|
||||
// 统一路径分隔符
|
||||
var normalized = entry.FullName.Replace('/', Path.DirectorySeparatorChar);
|
||||
|
||||
// 拒绝包含 .. 的路径,防止目录穿越
|
||||
if (normalized.Contains(".." + Path.DirectorySeparatorChar) ||
|
||||
normalized.StartsWith(".." + Path.DirectorySeparatorChar, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var destinationPath = Path.GetFullPath(
|
||||
Path.Combine(fullExtractPath, normalized));
|
||||
|
||||
// 再次确认仍然在目标目录下
|
||||
if (!destinationPath.StartsWith(fullExtractPath, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
// 目录条目
|
||||
if (entry.FullName.EndsWith("/", StringComparison.Ordinal) ||
|
||||
entry.FullName.EndsWith("\\", StringComparison.Ordinal))
|
||||
{
|
||||
Directory.CreateDirectory(destinationPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath) ?? fullExtractPath);
|
||||
|
||||
if (!overwrite && File.Exists(destinationPath))
|
||||
continue;
|
||||
|
||||
using (var input = entry.Open())
|
||||
using (var output = File.Create(destinationPath))
|
||||
{
|
||||
input.CopyTo(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
using iNKORE.UI.WPF.Controls;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
internal static class SecurityManager
|
||||
{
|
||||
private const int Pbkdf2Iterations = 120_000;
|
||||
private const int SaltSizeBytes = 16;
|
||||
private const int HashSizeBytes = 32;
|
||||
|
||||
/// <summary>
|
||||
/// 检查设置中是否启用了密码安全功能。
|
||||
/// </summary>
|
||||
/// <param name="settings">应用程序设置对象(可能为 null)。</param>
|
||||
/// <returns>`true` 当 settings 非 null 且其 Security 部分存在且已启用密码功能;`false` 否则。</returns>
|
||||
public static bool IsPasswordFeatureEnabled(Settings settings)
|
||||
=> settings?.Security != null && settings.Security.PasswordEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// 确定给定设置中是否已配置密码(存在非空的密码盐和密码哈希)。
|
||||
/// </summary>
|
||||
/// <param name="settings">应用的设置;为 null 或未包含 Security 部分时视为未配置密码。</param>
|
||||
/// <returns>`true` 如果设置包含非空的 PasswordSalt 和 PasswordHash,否则 `false`。</returns>
|
||||
public static bool HasPasswordConfigured(Settings settings)
|
||||
=> settings?.Security != null
|
||||
&& !string.IsNullOrWhiteSpace(settings.Security.PasswordSalt)
|
||||
&& !string.IsNullOrWhiteSpace(settings.Security.PasswordHash);
|
||||
|
||||
/// <summary>
|
||||
/// 确定在退出应用时是否需要输入密码。
|
||||
/// </summary>
|
||||
/// <param name="settings">应用配置;如果为 null,则视为未启用或未配置密码。</param>
|
||||
/// <returns>`true` 当密码功能已启用、已配置密码且设置要求在退出时需要密码,`false` 否则。</returns>
|
||||
public static bool IsPasswordRequiredForExit(Settings settings)
|
||||
=> IsPasswordFeatureEnabled(settings) && HasPasswordConfigured(settings) && settings.Security.RequirePasswordOnExit;
|
||||
|
||||
/// <summary>
|
||||
/// 确定在进入设置界面时是否需要输入密码。
|
||||
/// </summary>
|
||||
/// <param name="settings">应用配置;为 null 或未启用密码功能时视为未配置密码。</param>
|
||||
/// <returns>`true` 如果已启用密码功能、已配置密码且已设置为在进入设置时要求密码,`false` 否则。</returns>
|
||||
public static bool IsPasswordRequiredForEnterSettings(Settings settings)
|
||||
=> IsPasswordFeatureEnabled(settings) && HasPasswordConfigured(settings) && settings.Security.RequirePasswordOnEnterSettings;
|
||||
|
||||
/// <summary>
|
||||
/// 指示在重置配置时是否需要输入密码。
|
||||
/// </summary>
|
||||
/// <param name="settings">应用设置对象;如果为 null 或未启用密码功能,则视为不需要密码。</param>
|
||||
/// <returns>`true` 如果已启用密码功能、已有配置的密码且设置要求在重置配置时进行密码验证;`false` 否则。</returns>
|
||||
public static bool IsPasswordRequiredForResetConfig(Settings settings)
|
||||
=> IsPasswordFeatureEnabled(settings) && HasPasswordConfigured(settings) && settings.Security.RequirePasswordOnResetConfig;
|
||||
|
||||
/// <summary>
|
||||
/// 将提供的明文密码与 Settings 中存储的密码散列进行比对以验证密码是否正确。
|
||||
/// </summary>
|
||||
/// <param name="settings">包含存储的密码盐和哈希的设置对象(使用 Base64 编码的 PasswordSalt 和 PasswordHash)。</param>
|
||||
/// <param name="password">要验证的明文密码。</param>
|
||||
/// <returns>`true` 如果密码与存储的哈希匹配,`false` 否则(包括未配置密码、password 为 null 或在解析/派生过程中发生错误)。</returns>
|
||||
public static bool VerifyPassword(Settings settings, string password)
|
||||
{
|
||||
if (!HasPasswordConfigured(settings)) return false;
|
||||
if (password == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var salt = Convert.FromBase64String(settings.Security.PasswordSalt);
|
||||
var expected = Convert.FromBase64String(settings.Security.PasswordHash);
|
||||
|
||||
var actual = DeriveKey(password, salt, expected.Length);
|
||||
return FixedTimeEquals(actual, expected);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 如果已配置密码,显示一个对话框提示用户输入密码并验证;如果未配置密码则直接允许通过。
|
||||
/// </summary>
|
||||
/// <returns>`true` 如果未配置密码或用户确认并输入了正确的密码,`false` 如果用户取消或验证失败。</returns>
|
||||
public static async Task<bool> PromptAndVerifyAsync(Settings settings, Window owner, string title, string message)
|
||||
{
|
||||
if (!HasPasswordConfigured(settings)) return true;
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = title,
|
||||
PrimaryButtonText = "确定",
|
||||
SecondaryButtonText = "取消"
|
||||
};
|
||||
|
||||
var panel = new SimpleStackPanel
|
||||
{
|
||||
Spacing = 12,
|
||||
Margin = new Thickness(0, 10, 0, 0)
|
||||
};
|
||||
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Text = message,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
var passwordBox = new PasswordBox
|
||||
{
|
||||
Height = 32
|
||||
};
|
||||
|
||||
panel.Children.Add(textBlock);
|
||||
panel.Children.Add(passwordBox);
|
||||
dialog.Content = panel;
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
if (result != ContentDialogResult.Primary) return false;
|
||||
|
||||
return VerifyPassword(settings, passwordBox.Password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示一个对话框让用户输入并确认新密码,成功时返回该密码。
|
||||
/// </summary>
|
||||
/// <param name="owner">对话框的所属窗口(用于指定父窗口)。</param>
|
||||
/// <returns>用户输入的新密码;如果用户取消或输入无效(长度不足或两次不匹配),则返回 <c>null</c>。</returns>
|
||||
public static async Task<string> PromptSetNewPasswordAsync(Window owner)
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = "设置安全密码",
|
||||
PrimaryButtonText = "确定",
|
||||
SecondaryButtonText = "取消"
|
||||
};
|
||||
|
||||
var panel = new SimpleStackPanel
|
||||
{
|
||||
Spacing = 12,
|
||||
Margin = new Thickness(0, 10, 0, 0)
|
||||
};
|
||||
|
||||
var tipText = new TextBlock
|
||||
{
|
||||
Text = "请输入新密码",
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
var newPwdBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) };
|
||||
var confirmPwdBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) };
|
||||
|
||||
panel.Children.Add(tipText);
|
||||
panel.Children.Add(new TextBlock { Text = "新密码", Margin = new Thickness(0, 4, 0, 0) });
|
||||
panel.Children.Add(newPwdBox);
|
||||
panel.Children.Add(new TextBlock { Text = "确认新密码", Margin = new Thickness(0, 8, 0, 0) });
|
||||
panel.Children.Add(confirmPwdBox);
|
||||
dialog.Content = panel;
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
if (result != ContentDialogResult.Primary) return null;
|
||||
|
||||
var pwd = newPwdBox.Password ?? "";
|
||||
var confirm = confirmPwdBox.Password ?? "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pwd) || pwd.Length < 4)
|
||||
{
|
||||
MessageBox.Show("密码长度过短。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return null;
|
||||
}
|
||||
if (!string.Equals(pwd, confirm, StringComparison.Ordinal))
|
||||
{
|
||||
MessageBox.Show("两次输入的密码不一致。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return null;
|
||||
}
|
||||
|
||||
return pwd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹出对话框以更改已配置的安全密码;如果尚未配置密码则转而提示设置新密码。
|
||||
/// </summary>
|
||||
/// <param name="settings">应用配置对象,包含当前存储的密码信息。</param>
|
||||
/// <param name="owner">对话框的父窗口(用于定位/所有权)。</param>
|
||||
/// <returns>用户成功更改后返回新的密码字符串;当用户取消、验证失败或校验不通过时返回 <c>null</c>。</returns>
|
||||
public static async Task<string> PromptChangePasswordAsync(Settings settings, Window owner)
|
||||
{
|
||||
if (!HasPasswordConfigured(settings))
|
||||
{
|
||||
return await PromptSetNewPasswordAsync(owner);
|
||||
}
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = "修改安全密码",
|
||||
PrimaryButtonText = "确定",
|
||||
SecondaryButtonText = "取消"
|
||||
};
|
||||
|
||||
var panel = new SimpleStackPanel
|
||||
{
|
||||
Spacing = 12,
|
||||
Margin = new Thickness(0, 10, 0, 0)
|
||||
};
|
||||
|
||||
var tipText = new TextBlock
|
||||
{
|
||||
Text = "请输入当前密码,并设置新密码。",
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
var currentBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) };
|
||||
var newPwdBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) };
|
||||
var confirmPwdBox = new PasswordBox { Height = 32, Margin = new Thickness(0, 4, 0, 0) };
|
||||
|
||||
panel.Children.Add(tipText);
|
||||
panel.Children.Add(new TextBlock { Text = "当前密码", Margin = new Thickness(0, 4, 0, 0) });
|
||||
panel.Children.Add(currentBox);
|
||||
panel.Children.Add(new TextBlock { Text = "新密码", Margin = new Thickness(0, 8, 0, 0) });
|
||||
panel.Children.Add(newPwdBox);
|
||||
panel.Children.Add(new TextBlock { Text = "确认新密码", Margin = new Thickness(0, 8, 0, 0) });
|
||||
panel.Children.Add(confirmPwdBox);
|
||||
dialog.Content = panel;
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
if (result != ContentDialogResult.Primary) return null;
|
||||
|
||||
var current = currentBox.Password ?? "";
|
||||
var newPwd = newPwdBox.Password ?? "";
|
||||
var confirm = confirmPwdBox.Password ?? "";
|
||||
|
||||
if (!VerifyPassword(settings, current))
|
||||
{
|
||||
MessageBox.Show("当前密码错误。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newPwd) || newPwd.Length < 4)
|
||||
{
|
||||
MessageBox.Show("新密码长度过短。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return null;
|
||||
}
|
||||
if (!string.Equals(newPwd, confirm, StringComparison.Ordinal))
|
||||
{
|
||||
MessageBox.Show("两次输入的新密码不一致。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return null;
|
||||
}
|
||||
|
||||
return newPwd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定 Settings 生成并存储新的密码盐与哈希到 settings.Security 中。
|
||||
/// </summary>
|
||||
/// <param name="settings">要更新的设置对象;如果为 null 或其 Security 为 null 则不执行任何操作。</param>
|
||||
/// <param name="password">用于派生哈希的原始密码字符串。</param>
|
||||
public static void SetPassword(Settings settings, string password)
|
||||
{
|
||||
if (settings?.Security == null) return;
|
||||
|
||||
var salt = new byte[SaltSizeBytes];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(salt);
|
||||
}
|
||||
var hash = DeriveKey(password, salt, HashSizeBytes);
|
||||
|
||||
settings.Security.PasswordSalt = Convert.ToBase64String(salt);
|
||||
settings.Security.PasswordHash = Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除设置中存储的密码信息。
|
||||
/// </summary>
|
||||
/// <param name="settings">要更新的设置对象;将把其 Security.PasswordSalt 和 Security.PasswordHash 设为空字符串。若 <paramref name="settings"/> 为 null 或其 Security 为 null 则不执行任何操作。</param>
|
||||
public static void ClearPassword(Settings settings)
|
||||
{
|
||||
if (settings?.Security == null) return;
|
||||
settings.Security.PasswordSalt = "";
|
||||
settings.Security.PasswordHash = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 PBKDF2(Rfc2898)从给定的密码和盐派生指定长度的密钥字节。
|
||||
/// </summary>
|
||||
/// <param name="password">用于派生的密码字符串。</param>
|
||||
/// <param name="salt">用于派生的盐字节数组(不可为 null)。</param>
|
||||
/// <param name="keyBytes">要返回的密钥字节长度(以字节为单位)。</param>
|
||||
/// <returns>派生出的密钥字节数组,长度等于 <paramref name="keyBytes"/>。</returns>
|
||||
private static byte[] DeriveKey(string password, byte[] salt, int keyBytes)
|
||||
{
|
||||
// 注意:Rfc2898DeriveBytes 在 net472 默认 HMACSHA1
|
||||
using (var kdf = new Rfc2898DeriveBytes(password, salt, Pbkdf2Iterations))
|
||||
{
|
||||
return kdf.GetBytes(keyBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以固定时间方式比较两个字节数组的内容是否完全相同,防止基于时序的比对攻击。
|
||||
/// </summary>
|
||||
/// <param name="a">要比较的第一个字节数组。</param>
|
||||
/// <param name="b">要比较的第二个字节数组。</param>
|
||||
/// <returns>`true` 如果两个数组长度相同且所有字节相等,`false` 否则。</returns>
|
||||
private static bool FixedTimeEquals(byte[] a, byte[] b)
|
||||
{
|
||||
if (a == null || b == null) return false;
|
||||
if (a.Length != b.Length) return false;
|
||||
var diff = 0;
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
diff |= a[i] ^ b[i];
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
@@ -18,7 +19,7 @@ namespace Ink_Canvas.Helpers
|
||||
return count;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
File.WriteAllText(CountFilePath, count.ToString());
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ namespace Ink_Canvas.Helpers
|
||||
if (File.Exists(CountFilePath))
|
||||
File.Delete(CountFilePath);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
using Sentry;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
internal static class TelemetryUploader
|
||||
{
|
||||
private static readonly Regex EmailRegex = new Regex(
|
||||
@"(?i)\b[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}\b",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex PhoneRegex = new Regex(
|
||||
@"\b1[3-9]\d{9}\b",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex IPv4Regex = new Regex(
|
||||
@"\b(?:\d{1,3}\.){3}\d{1,3}\b",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex WindowsPathRegex = new Regex(
|
||||
@"\b[A-Za-z]:\\[^\s<>|]+\b",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex UncPathRegex = new Regex(
|
||||
@"\\\\[^\s]+",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex KeyValueSecretRegex = new Regex(
|
||||
@"(?i)(\b(?:access[_-]?token|refresh[_-]?token|token|password|passwd|pwd|secret|authorization)\b\s*[:=]\s*)([^\s,;]+)",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex JsonSecretRegex = new Regex(
|
||||
"(?i)(\"(?:access_token|refresh_token|token|password|passwd|pwd|secret|authorization)\"\\s*:\\s*\")([^\"]*)(\")",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex UrlSecretRegex = new Regex(
|
||||
@"(?i)([?&](?:access_token|token|password|pwd|secret)=)[^&\s]+",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public static Task UploadTelemetryIfNeededAsync()
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = MainWindow.Settings;
|
||||
if (settings == null || settings.Startup == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var level = settings.Startup.TelemetryUploadLevel;
|
||||
if (level == TelemetryUploadLevel.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string deviceId = DeviceIdentifier.GetDeviceId();
|
||||
if (string.IsNullOrWhiteSpace(deviceId) || deviceId.Length < 5)
|
||||
{
|
||||
LogHelper.WriteLogToFile("TelemetryUploader | 设备ID无效,取消遥测上传", LogHelper.LogType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic 和 Extended 均上传崩溃日志(脱敏)
|
||||
object crashFile = TryGetLatestSanitizedFile(
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Crashes"),
|
||||
"Crash_*.txt",
|
||||
"崩溃日志");
|
||||
|
||||
// Extended 额外上传运行日志(脱敏)
|
||||
object runtimeLogFile = null;
|
||||
if (level == TelemetryUploadLevel.Extended)
|
||||
{
|
||||
runtimeLogFile = TryGetLatestSanitizedFile(
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"),
|
||||
"Log_*.txt",
|
||||
"运行日志");
|
||||
}
|
||||
|
||||
var telemetryData = new
|
||||
{
|
||||
telemetry_level = level.ToString(),
|
||||
device_id = deviceId,
|
||||
update_channel = settings.Startup.UpdateChannel.ToString(),
|
||||
app_version = Assembly.GetExecutingAssembly().GetName().Version.ToString(),
|
||||
os_version = Environment.OSVersion.VersionString,
|
||||
has_crash_log = crashFile != null,
|
||||
has_runtime_log = runtimeLogFile != null
|
||||
};
|
||||
|
||||
// 通过 Sentry 上报一个包含遥测信息的事件
|
||||
string userName = Environment.UserName;
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.User = new SentryUser
|
||||
{
|
||||
Id = deviceId,
|
||||
Username = userName,
|
||||
Email = $"{userName}",
|
||||
IpAddress = "{{auto}}"
|
||||
};
|
||||
});
|
||||
|
||||
var evt = new SentryEvent
|
||||
{
|
||||
Message = "ICC CE Telemetry",
|
||||
Level = SentryLevel.Info
|
||||
};
|
||||
|
||||
evt.User = new SentryUser
|
||||
{
|
||||
Id = deviceId,
|
||||
Username = userName,
|
||||
Email = $"{userName}",
|
||||
IpAddress = "{{auto}}"
|
||||
};
|
||||
|
||||
evt.SetTag("telemetry_level", level.ToString());
|
||||
evt.SetTag("device_id", deviceId);
|
||||
evt.SetTag("update_channel", settings.Startup.UpdateChannel.ToString());
|
||||
evt.SetTag("app_version", Assembly.GetExecutingAssembly().GetName().Version.ToString());
|
||||
evt.SetTag("os_version", Environment.OSVersion.VersionString);
|
||||
evt.SetExtra("telemetry_data", telemetryData);
|
||||
|
||||
if (crashFile != null)
|
||||
{
|
||||
evt.SetExtra("crash_file", crashFile);
|
||||
}
|
||||
|
||||
if (runtimeLogFile != null)
|
||||
{
|
||||
evt.SetExtra("runtime_log_file", runtimeLogFile);
|
||||
}
|
||||
|
||||
SentrySdk.CaptureEvent(evt);
|
||||
LogHelper.WriteLogToFile("TelemetryUploader | 遥测数据已通过 Sentry 上报", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"TelemetryUploader | 遥测上传失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static object TryGetLatestSanitizedFile(string directory, string pattern, string fileType)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var latest = new DirectoryInfo(directory)
|
||||
.GetFiles(pattern)
|
||||
.OrderByDescending(file => file.LastWriteTime)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (latest == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string content = File.ReadAllText(latest.FullName);
|
||||
string sanitizedContent = SanitizeLogContent(content);
|
||||
|
||||
return new
|
||||
{
|
||||
file_type = fileType,
|
||||
file_name = latest.Name,
|
||||
last_write_time = latest.LastWriteTime.ToString("o"),
|
||||
content = sanitizedContent
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile(
|
||||
$"TelemetryUploader | 收集{fileType}失败: {ex.Message}",
|
||||
LogHelper.LogType.Warning);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string SanitizeLogContent(string content)
|
||||
{
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
string sanitized = content;
|
||||
sanitized = EmailRegex.Replace(sanitized, "[REDACTED_EMAIL]");
|
||||
sanitized = PhoneRegex.Replace(sanitized, "[REDACTED_PHONE]");
|
||||
sanitized = IPv4Regex.Replace(sanitized, "[REDACTED_IP]");
|
||||
sanitized = WindowsPathRegex.Replace(sanitized, "[REDACTED_PATH]");
|
||||
sanitized = UncPathRegex.Replace(sanitized, "[REDACTED_PATH]");
|
||||
sanitized = UrlSecretRegex.Replace(sanitized, "$1[REDACTED]");
|
||||
sanitized = KeyValueSecretRegex.Replace(sanitized, "$1[REDACTED]");
|
||||
sanitized = JsonSecretRegex.Replace(sanitized, "$1[REDACTED]$3");
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,11 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
public TimeMachineHistory Undo()
|
||||
{
|
||||
if (_currentIndex < 0 || _currentIndex >= _currentStrokeHistory.Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var item = _currentStrokeHistory[_currentIndex];
|
||||
item.StrokeHasBeenCleared = !item.StrokeHasBeenCleared;
|
||||
_currentIndex--;
|
||||
@@ -102,6 +107,11 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
public TimeMachineHistory Redo()
|
||||
{
|
||||
if (_currentStrokeHistory.Count == 0 || _currentIndex >= _currentStrokeHistory.Count - 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var item = _currentStrokeHistory[++_currentIndex];
|
||||
item.StrokeHasBeenCleared = !item.StrokeHasBeenCleared;
|
||||
NotifyUndoRedoState();
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 上传提供者接口
|
||||
/// </summary>
|
||||
public interface IUploadProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 提供者名称
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
bool IsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>是否上传成功</returns>
|
||||
Task<bool> UploadAsync(string filePath, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dlass上传提供者
|
||||
/// </summary>
|
||||
public class DlassUploadProvider : IUploadProvider
|
||||
{
|
||||
public static readonly DlassUploadQueue Queue = new DlassUploadQueue();
|
||||
|
||||
/// <summary>
|
||||
/// 提供者名称
|
||||
/// </summary>
|
||||
public string Name => "Dlass";
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool IsEnabled => MainWindow.Settings?.Upload?.EnabledProviders?.Contains(Name) ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>是否上传成功</returns>
|
||||
public async Task<bool> UploadAsync(string filePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Queue.UploadFileAsync(filePath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WebDav上传提供者
|
||||
/// </summary>
|
||||
public class WebDavUploadProvider : IUploadProvider
|
||||
{
|
||||
public static readonly WebDavUploadQueue Queue = new WebDavUploadQueue();
|
||||
|
||||
/// <summary>
|
||||
/// 提供者名称
|
||||
/// </summary>
|
||||
public string Name => "WebDav";
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool IsEnabled => MainWindow.Settings?.Upload?.EnabledProviders?.Contains(Name) ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>是否上传成功</returns>
|
||||
public async Task<bool> UploadAsync(string filePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Queue.UploadFileAsync(filePath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 上传帮助类
|
||||
/// </summary>
|
||||
public static class UploadHelper
|
||||
{
|
||||
private static readonly List<IUploadProvider> _providers = new List<IUploadProvider>();
|
||||
private static bool _initialized;
|
||||
private static readonly object s_sync = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 初始化上传帮助类
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
lock (s_sync)
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
// 注册默认上传提供者
|
||||
RegisterProviderInternal(new DlassUploadProvider());
|
||||
RegisterProviderInternal(new WebDavUploadProvider());
|
||||
|
||||
// 注册上传队列
|
||||
UploadQueueHelper.RegisterQueue(DlassUploadProvider.Queue);
|
||||
UploadQueueHelper.RegisterQueue(WebDavUploadProvider.Queue);
|
||||
|
||||
// 初始化所有上传队列
|
||||
UploadQueueHelper.InitializeAllQueues();
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册上传提供者
|
||||
/// </summary>
|
||||
/// <param name="provider">上传提供者</param>
|
||||
public static void RegisterProvider(IUploadProvider provider)
|
||||
{
|
||||
if (provider == null)
|
||||
return;
|
||||
|
||||
lock (s_sync)
|
||||
{
|
||||
RegisterProviderInternal(provider);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterProviderInternal(IUploadProvider provider)
|
||||
{
|
||||
if (provider != null)
|
||||
{
|
||||
bool providerExists = _providers.Any(p => p.GetType() == provider.GetType());
|
||||
if (!providerExists)
|
||||
{
|
||||
_providers.Add(provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件到所有启用的提供者
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>是否至少有一个提供者上传成功</returns>
|
||||
public static async Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
List<IUploadProvider> providersSnapshot;
|
||||
lock (s_sync)
|
||||
{
|
||||
providersSnapshot = new List<IUploadProvider>(_providers);
|
||||
}
|
||||
|
||||
bool anySuccess = false;
|
||||
|
||||
// 获取上传延迟时间
|
||||
int delayMinutes = MainWindow.Settings?.Upload?.UploadDelayMinutes ?? 0;
|
||||
|
||||
// 应用上传延迟
|
||||
if (delayMinutes > 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传延迟 {delayMinutes} 分钟", LogHelper.LogType.Event);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await Task.Delay(TimeSpan.FromMinutes(delayMinutes), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 上传前验证文件是否存在且可访问
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 检查文件是否可访问
|
||||
using (var fileStream = File.OpenRead(filePath))
|
||||
{
|
||||
// 文件可访问
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败:文件不可访问 - {filePath}, 原因: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var provider in providersSnapshot)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (provider.IsEnabled)
|
||||
{
|
||||
bool success = await provider.UploadAsync(filePath, cancellationToken).ConfigureAwait(false);
|
||||
if (success)
|
||||
{
|
||||
anySuccess = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传被取消: {provider.Name}", LogHelper.LogType.Event);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"使用 {provider.Name} 上传失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
return anySuccess;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有上传提供者
|
||||
/// </summary>
|
||||
/// <returns>上传提供者列表</returns>
|
||||
public static List<IUploadProvider> GetProviders()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
lock (s_sync)
|
||||
{
|
||||
return new List<IUploadProvider>(_providers);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有启用的上传提供者
|
||||
/// </summary>
|
||||
/// <returns>启用的上传提供者列表</returns>
|
||||
public static List<IUploadProvider> GetEnabledProviders()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
lock (s_sync)
|
||||
{
|
||||
return _providers.FindAll(p => p.IsEnabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 上传队列帮助类,提供统一的队列管理功能
|
||||
/// </summary>
|
||||
public static class UploadQueueHelper
|
||||
{
|
||||
private static readonly List<BaseUploadQueue> _queues = new List<BaseUploadQueue>();
|
||||
private static readonly object _syncLock = new object();
|
||||
private static volatile bool _initialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化所有上传队列
|
||||
/// </summary>
|
||||
public static void InitializeAllQueues()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
// 初始化所有注册的队列
|
||||
foreach (var queue in _queues)
|
||||
{
|
||||
try
|
||||
{
|
||||
queue.InitializeQueue();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[UploadQueueHelper] 初始化队列时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册上传队列
|
||||
/// </summary>
|
||||
/// <param name="queue">上传队列实例</param>
|
||||
public static void RegisterQueue(BaseUploadQueue queue)
|
||||
{
|
||||
if (queue == null)
|
||||
return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_queues.Contains(queue))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 先初始化队列,再添加到列表
|
||||
queue.InitializeQueue();
|
||||
_queues.Add(queue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"[UploadQueueHelper] 注册队列时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有注册的上传队列
|
||||
/// </summary>
|
||||
/// <returns>上传队列列表</returns>
|
||||
public static IReadOnlyList<BaseUploadQueue> GetAllQueues()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
return new List<BaseUploadQueue>(_queues).AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保所有队列都已初始化
|
||||
/// </summary>
|
||||
public static void EnsureQueuesInitialized()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
InitializeAllQueues();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public static class UriSchemeHelper
|
||||
{
|
||||
private const string SchemeName = "icc";
|
||||
private const string FriendlyName = "URL:Ink Canvas Protocol";
|
||||
|
||||
public static bool RegisterUriScheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
|
||||
// 使用 CurrentUser\Software\Classes 代替 ClassesRoot,无需管理员权限
|
||||
using (RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Classes\" + SchemeName))
|
||||
{
|
||||
key.SetValue("", FriendlyName);
|
||||
key.SetValue("URL Protocol", "");
|
||||
|
||||
using (RegistryKey defaultIconKey = key.CreateSubKey("DefaultIcon"))
|
||||
{
|
||||
// 修正引号转义
|
||||
defaultIconKey.SetValue("", "\"" + exePath + "\",1");
|
||||
}
|
||||
|
||||
using (RegistryKey shellKey = key.CreateSubKey("shell"))
|
||||
using (RegistryKey openKey = shellKey.CreateSubKey("open"))
|
||||
using (RegistryKey commandKey = openKey.CreateSubKey("command"))
|
||||
{
|
||||
// 修正引号转义
|
||||
commandKey.SetValue("", "\"" + exePath + "\" \"%1\"");
|
||||
}
|
||||
}
|
||||
LogHelper.WriteLogToFile($"成功注册URI Scheme: {SchemeName}://", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册URI Scheme失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool UnregisterUriScheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用 CurrentUser\Software\Classes
|
||||
Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\" + SchemeName, false);
|
||||
LogHelper.WriteLogToFile($"成功注销URI Scheme: {SchemeName}://", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注销URI Scheme失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsUriSchemeRegistered()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用 CurrentUser\Software\Classes
|
||||
using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Classes\" + SchemeName))
|
||||
{
|
||||
if (key == null) return false;
|
||||
// 修正反斜杠路径
|
||||
using (RegistryKey shellKey = key.OpenSubKey(@"shell\open\command"))
|
||||
{
|
||||
if (shellKey == null) return false;
|
||||
string command = shellKey.GetValue("") as string;
|
||||
if (string.IsNullOrEmpty(command)) return false;
|
||||
|
||||
// 提取第一个标记作为可执行文件路径(处理带引号的情况)
|
||||
string registeredExePath = "";
|
||||
if (command.StartsWith("\""))
|
||||
{
|
||||
int nextQuote = command.IndexOf("\"", 1);
|
||||
if (nextQuote > 1)
|
||||
{
|
||||
registeredExePath = command.Substring(1, nextQuote - 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int firstSpace = command.IndexOf(" ");
|
||||
registeredExePath = firstSpace > 0 ? command.Substring(0, firstSpace) : command;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(registeredExePath)) return false;
|
||||
|
||||
string currentExePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
|
||||
try
|
||||
{
|
||||
string normalizedRegisteredPath = System.IO.Path.GetFullPath(registeredExePath);
|
||||
string normalizedCurrentPath = System.IO.Path.GetFullPath(currentExePath);
|
||||
return string.Equals(normalizedRegisteredPath, normalizedCurrentPath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Equals(registeredExePath, currentExePath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// WebDAV上传队列
|
||||
/// </summary>
|
||||
public class WebDavUploadQueue : BaseUploadQueue
|
||||
{
|
||||
/// <summary>
|
||||
/// 队列文件名
|
||||
/// </summary>
|
||||
protected override string QueueFileName => "WebDavUploadQueue.json";
|
||||
|
||||
/// <summary>
|
||||
/// 检查上传是否启用
|
||||
/// </summary>
|
||||
protected override bool IsUploadEnabled()
|
||||
{
|
||||
return WebDavUploader.IsWebDavEnabled();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部上传方法,执行实际上传操作
|
||||
/// </summary>
|
||||
protected override async Task<bool> UploadFileInternalAsync(string filePath, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 再次检查文件是否存在(可能在队列等待时被删除)
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查WebDAV是否仍然启用
|
||||
if (!WebDavUploader.IsWebDavEnabled())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 调用WebDavUploader进行实际上传
|
||||
var success = await WebDavUploader.UploadFileAsync(filePath, cancellationToken);
|
||||
return success;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using WebDav;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// WebDav上传工具类
|
||||
/// </summary>
|
||||
public static class WebDavUploader
|
||||
{
|
||||
/// <summary>
|
||||
/// 上传文件到WebDav服务器
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>是否上传成功</returns>
|
||||
public static async Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取WebDav设置
|
||||
var webDavUrl = MainWindow.Settings?.Dlass?.WebDavUrl;
|
||||
var username = MainWindow.Settings?.Dlass?.WebDavUsername;
|
||||
var password = MainWindow.Settings?.Dlass?.WebDavPassword;
|
||||
var rootDirectory = MainWindow.Settings?.Dlass?.WebDavRootDirectory;
|
||||
|
||||
// 验证设置
|
||||
if (string.IsNullOrEmpty(webDavUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 构建完整的目标路径
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var targetPath = Path.Combine(rootDirectory ?? string.Empty, fileName).Replace("\\", "/");
|
||||
if (targetPath.StartsWith("/"))
|
||||
{
|
||||
targetPath = targetPath.Substring(1);
|
||||
}
|
||||
|
||||
// 创建WebDav客户端
|
||||
var clientParams = new WebDavClientParams
|
||||
{
|
||||
BaseAddress = new Uri(webDavUrl),
|
||||
Credentials = new NetworkCredential(username ?? string.Empty, password ?? string.Empty)
|
||||
};
|
||||
|
||||
using (var client = new WebDavClient(clientParams))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 先直接尝试上传文件
|
||||
using (var fileStream = File.OpenRead(filePath))
|
||||
{
|
||||
// 检查取消令牌
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var result = await client.PutFile(targetPath, fileStream);
|
||||
if (result.IsSuccessful)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 上传失败,尝试创建目录
|
||||
var directoryPath = Path.GetDirectoryName(targetPath);
|
||||
if (!string.IsNullOrEmpty(directoryPath))
|
||||
{
|
||||
await EnsureDirectoryExistsAsync(client, directoryPath, cancellationToken);
|
||||
|
||||
// 再次尝试上传文件
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (var retryStream = File.OpenRead(filePath))
|
||||
{
|
||||
var retryResult = await client.PutFile(targetPath, retryStream);
|
||||
return retryResult.IsSuccessful;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有目录路径,直接返回失败
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保WebDav目录存在
|
||||
/// </summary>
|
||||
/// <param name="client">WebDav客户端</param>
|
||||
/// <param name="directoryPath">目录路径</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
private static async Task EnsureDirectoryExistsAsync(IWebDavClient client, string directoryPath, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 分割路径并逐级创建目录
|
||||
var pathParts = directoryPath.Split('/');
|
||||
var currentPath = string.Empty;
|
||||
|
||||
foreach (var part in pathParts)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (string.IsNullOrEmpty(part))
|
||||
continue;
|
||||
|
||||
currentPath = Path.Combine(currentPath, part).Replace("\\", "/");
|
||||
|
||||
// 检查取消令牌
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 尝试创建目录
|
||||
await client.Mkcol(currentPath);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 静默处理目录创建错误
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查WebDAV是否已启用
|
||||
/// </summary>
|
||||
/// <returns>是否启用</returns>
|
||||
public static bool IsWebDavEnabled()
|
||||
{
|
||||
// 检查WebDav设置是否有效
|
||||
var webDavUrl = MainWindow.Settings?.Dlass?.WebDavUrl;
|
||||
if (string.IsNullOrEmpty(webDavUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试解析URL
|
||||
try
|
||||
{
|
||||
new Uri(webDavUrl);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,611 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩形结构体(用于窗口位置和大小)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct WindowRect
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
|
||||
public int Width => Right - Left;
|
||||
public int Height => Bottom - Top;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口信息结构
|
||||
/// </summary>
|
||||
public class WindowInfo
|
||||
{
|
||||
public IntPtr Handle { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string ClassName { get; set; }
|
||||
public string ProcessName { get; set; }
|
||||
public string ProcessPath { get; set; }
|
||||
public WindowRect Rect { get; set; }
|
||||
public bool IsVisible { get; set; }
|
||||
public bool IsMinimized { get; set; }
|
||||
public bool IsMaximized { get; set; }
|
||||
public int ZOrder { get; set; }
|
||||
public uint ProcessId { get; set; }
|
||||
public bool IsFullScreen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计算窗口是否覆盖指定区域
|
||||
/// </summary>
|
||||
public bool CoversArea(WindowRect area)
|
||||
{
|
||||
if (!IsVisible || IsMinimized) return false;
|
||||
|
||||
// 计算交集
|
||||
int left = Math.Max(Rect.Left, area.Left);
|
||||
int top = Math.Max(Rect.Top, area.Top);
|
||||
int right = Math.Min(Rect.Right, area.Right);
|
||||
int bottom = Math.Min(Rect.Bottom, area.Bottom);
|
||||
|
||||
// 如果有交集,说明窗口覆盖了该区域
|
||||
return left < right && top < bottom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算窗口覆盖指定区域的比例
|
||||
/// </summary>
|
||||
public double GetCoverageRatio(WindowRect area)
|
||||
{
|
||||
if (!IsVisible || IsMinimized) return 0.0;
|
||||
|
||||
// 计算交集
|
||||
int left = Math.Max(Rect.Left, area.Left);
|
||||
int top = Math.Max(Rect.Top, area.Top);
|
||||
int right = Math.Min(Rect.Right, area.Right);
|
||||
int bottom = Math.Min(Rect.Bottom, area.Bottom);
|
||||
|
||||
if (left >= right || top >= bottom) return 0.0;
|
||||
|
||||
// 计算交集面积
|
||||
double intersectionArea = (right - left) * (bottom - top);
|
||||
// 计算目标区域面积
|
||||
double targetArea = area.Width * area.Height;
|
||||
|
||||
if (targetArea == 0) return 0.0;
|
||||
|
||||
return intersectionArea / targetArea;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口概览模型 - 实时监控桌面所有可见窗口并计算遮挡情况
|
||||
/// </summary>
|
||||
public class WindowOverviewModel : IDisposable
|
||||
{
|
||||
#region Win32 API Declarations
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetWindowRect(IntPtr hWnd, out WindowRect lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsIconic(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsZoomed(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
|
||||
|
||||
private const uint GW_HWNDNEXT = 2;
|
||||
private const uint GW_HWNDPREV = 3;
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
#endregion
|
||||
|
||||
private readonly object _lockObject = new object();
|
||||
private List<WindowInfo> _windows = new List<WindowInfo>();
|
||||
private Timer _updateTimer;
|
||||
private bool _isDisposed = false;
|
||||
private readonly int _updateInterval = 1000; // 更新间隔(毫秒)
|
||||
|
||||
private readonly Dictionary<uint, ProcessCacheInfo> _processCache = new Dictionary<uint, ProcessCacheInfo>();
|
||||
private readonly object _processCacheLock = new object();
|
||||
private DateTime _lastProcessCacheCleanup = DateTime.Now;
|
||||
private const int PROCESS_CACHE_CLEANUP_INTERVAL_MS = 30000;
|
||||
|
||||
// 窗口缓存,用于增量更新
|
||||
private readonly Dictionary<IntPtr, WindowInfo> _windowCache = new Dictionary<IntPtr, WindowInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// 进程缓存信息
|
||||
/// </summary>
|
||||
private class ProcessCacheInfo
|
||||
{
|
||||
public string ProcessName { get; set; }
|
||||
public string ProcessPath { get; set; }
|
||||
public DateTime LastAccessTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口列表更新事件
|
||||
/// </summary>
|
||||
public event EventHandler<List<WindowInfo>> WindowsUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// 当前窗口列表(按Z顺序排序,最上层在前)
|
||||
/// </summary>
|
||||
public List<WindowInfo> Windows
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return new List<WindowInfo>(_windows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public WindowOverviewModel()
|
||||
{
|
||||
// 立即执行一次更新
|
||||
UpdateWindows();
|
||||
|
||||
// 启动定时器,定期更新窗口列表
|
||||
_updateTimer = new Timer(OnUpdateTimer, null, _updateInterval, _updateInterval);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定时器回调
|
||||
/// </summary>
|
||||
private void OnUpdateTimer(object state)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
UpdateWindows();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"窗口概览模型更新失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取进程信息
|
||||
/// </summary>
|
||||
private (string processName, string processPath) GetProcessInfo(uint processId)
|
||||
{
|
||||
lock (_processCacheLock)
|
||||
{
|
||||
// 定期清理缓存
|
||||
var now = DateTime.Now;
|
||||
if ((now - _lastProcessCacheCleanup).TotalMilliseconds > PROCESS_CACHE_CLEANUP_INTERVAL_MS)
|
||||
{
|
||||
var keysToRemove = _processCache
|
||||
.Where(kvp => (now - kvp.Value.LastAccessTime).TotalMilliseconds > PROCESS_CACHE_CLEANUP_INTERVAL_MS)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_processCache.Remove(key);
|
||||
}
|
||||
|
||||
_lastProcessCacheCleanup = now;
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
if (_processCache.TryGetValue(processId, out var cachedInfo))
|
||||
{
|
||||
cachedInfo.LastAccessTime = now;
|
||||
return (cachedInfo.ProcessName, cachedInfo.ProcessPath);
|
||||
}
|
||||
|
||||
// 缓存未命中,获取进程信息
|
||||
string processName = "Unknown";
|
||||
string processPath = "Unknown";
|
||||
|
||||
try
|
||||
{
|
||||
Process process = Process.GetProcessById((int)processId);
|
||||
processName = process.ProcessName;
|
||||
try
|
||||
{
|
||||
processPath = process.MainModule?.FileName ?? "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
processPath = "Unknown";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 进程可能已退出
|
||||
}
|
||||
|
||||
// 添加到缓存
|
||||
_processCache[processId] = new ProcessCacheInfo
|
||||
{
|
||||
ProcessName = processName,
|
||||
ProcessPath = processPath,
|
||||
LastAccessTime = now
|
||||
};
|
||||
|
||||
return (processName, processPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查窗口信息是否发生变化
|
||||
/// </summary>
|
||||
private bool HasWindowChanged(IntPtr hWnd, WindowRect rect, bool isMinimized, bool isMaximized, bool isFullScreen)
|
||||
{
|
||||
if (!_windowCache.TryGetValue(hWnd, out var cachedWindow))
|
||||
{
|
||||
return true; // 新窗口
|
||||
}
|
||||
|
||||
// 检查关键属性是否变化
|
||||
return cachedWindow.Rect.Left != rect.Left ||
|
||||
cachedWindow.Rect.Top != rect.Top ||
|
||||
cachedWindow.Rect.Right != rect.Right ||
|
||||
cachedWindow.Rect.Bottom != rect.Bottom ||
|
||||
cachedWindow.IsMinimized != isMinimized ||
|
||||
cachedWindow.IsMaximized != isMaximized ||
|
||||
cachedWindow.IsFullScreen != isFullScreen;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新窗口列表
|
||||
/// </summary>
|
||||
public void UpdateWindows()
|
||||
{
|
||||
var windows = new List<WindowInfo>();
|
||||
var zOrder = 0;
|
||||
var currentWindowHandles = new HashSet<IntPtr>();
|
||||
|
||||
EnumWindows((hWnd, lParam) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查窗口是否可见
|
||||
if (!IsWindowVisible(hWnd)) return true;
|
||||
|
||||
// 检查窗口是否最小化
|
||||
bool isMinimized = IsIconic(hWnd);
|
||||
if (isMinimized) return true;
|
||||
|
||||
// 获取窗口矩形
|
||||
if (!GetWindowRect(hWnd, out WindowRect rect)) return true;
|
||||
|
||||
// 过滤掉无效的窗口
|
||||
if (rect.Width <= 0 || rect.Height <= 0) return true;
|
||||
if (rect.Right < rect.Left || rect.Bottom < rect.Top) return true;
|
||||
|
||||
// 检查是否最大化
|
||||
bool isMaximized = IsZoomed(hWnd);
|
||||
|
||||
// 检查是否全屏(窗口大小接近屏幕大小)
|
||||
bool isFullScreen = false;
|
||||
try
|
||||
{
|
||||
var screen = System.Windows.Forms.Screen.FromHandle(hWnd);
|
||||
var screenBounds = screen.Bounds;
|
||||
// 如果窗口大小接近屏幕大小(允许10像素误差),认为是全屏
|
||||
isFullScreen = rect.Width >= screenBounds.Width - 10 &&
|
||||
rect.Height >= screenBounds.Height - 10 &&
|
||||
Math.Abs(rect.Left - screenBounds.Left) <= 10 &&
|
||||
Math.Abs(rect.Top - screenBounds.Top) <= 10;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 无法获取屏幕信息,使用默认值
|
||||
}
|
||||
|
||||
// 检查窗口是否发生变化
|
||||
bool windowChanged = HasWindowChanged(hWnd, rect, isMinimized, isMaximized, isFullScreen);
|
||||
|
||||
// 获取进程信息
|
||||
GetWindowThreadProcessId(hWnd, out uint processId);
|
||||
|
||||
// 使用缓存的进程信息
|
||||
var (processName, processPath) = GetProcessInfo(processId);
|
||||
|
||||
// 跳过当前应用程序的窗口(避免检测到自己)
|
||||
if (processName == "InkCanvasForClass" || processName == "Ink Canvas")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果窗口信息未变化且已缓存,尝试重用缓存的数据
|
||||
WindowInfo windowInfo;
|
||||
if (!windowChanged && _windowCache.TryGetValue(hWnd, out var cachedInfo))
|
||||
{
|
||||
// 重用缓存的窗口信息,只更新Z顺序和可能变化的状态
|
||||
windowInfo = new WindowInfo
|
||||
{
|
||||
Handle = hWnd,
|
||||
Title = cachedInfo.Title,
|
||||
ClassName = cachedInfo.ClassName,
|
||||
ProcessName = cachedInfo.ProcessName,
|
||||
ProcessPath = cachedInfo.ProcessPath,
|
||||
Rect = rect, // 使用最新的rect(虽然理论上应该相同)
|
||||
IsVisible = true,
|
||||
IsMinimized = false,
|
||||
IsMaximized = isMaximized,
|
||||
ZOrder = zOrder++,
|
||||
ProcessId = processId,
|
||||
IsFullScreen = isFullScreen
|
||||
};
|
||||
|
||||
// 更新缓存以保持ZOrder等属性最新
|
||||
_windowCache[hWnd] = windowInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 窗口信息变化或新窗口,需要获取完整信息
|
||||
// 获取窗口标题
|
||||
const int nChars = 256;
|
||||
StringBuilder windowTitle = new StringBuilder(nChars);
|
||||
GetWindowText(hWnd, windowTitle, nChars);
|
||||
string title = windowTitle.ToString();
|
||||
|
||||
// 获取窗口类名
|
||||
StringBuilder className = new StringBuilder(nChars);
|
||||
GetClassName(hWnd, className, nChars);
|
||||
string classNameStr = className.ToString();
|
||||
|
||||
windowInfo = new WindowInfo
|
||||
{
|
||||
Handle = hWnd,
|
||||
Title = title,
|
||||
ClassName = classNameStr,
|
||||
ProcessName = processName,
|
||||
ProcessPath = processPath,
|
||||
Rect = rect,
|
||||
IsVisible = true,
|
||||
IsMinimized = false,
|
||||
IsMaximized = isMaximized,
|
||||
ZOrder = zOrder++,
|
||||
ProcessId = processId,
|
||||
IsFullScreen = isFullScreen
|
||||
};
|
||||
|
||||
// 更新缓存
|
||||
_windowCache[hWnd] = windowInfo;
|
||||
}
|
||||
|
||||
windows.Add(windowInfo);
|
||||
currentWindowHandles.Add(hWnd);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略单个窗口的错误,继续枚举其他窗口
|
||||
}
|
||||
|
||||
return true; // 继续枚举
|
||||
}, IntPtr.Zero);
|
||||
|
||||
// 清理已关闭的窗口缓存
|
||||
var handlesToRemove = _windowCache.Keys.Where(h => !currentWindowHandles.Contains(h)).ToList();
|
||||
foreach (var handle in handlesToRemove)
|
||||
{
|
||||
_windowCache.Remove(handle);
|
||||
}
|
||||
|
||||
windows = windows.OrderByDescending(w => w.ZOrder).ToList();
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
_windows = windows;
|
||||
}
|
||||
|
||||
// 触发更新事件
|
||||
WindowsUpdated?.Invoke(this, windows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定区域是否被其他窗口覆盖
|
||||
/// </summary>
|
||||
/// <param name="area">要检查的区域</param>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表(例如当前应用程序)</param>
|
||||
/// <param name="coverageThreshold">覆盖阈值(0.0-1.0),超过此阈值认为被覆盖</param>
|
||||
/// <returns>如果被覆盖返回true</returns>
|
||||
public bool IsAreaCovered(WindowRect area, List<string> excludeProcessNames = null, double coverageThreshold = 0.5)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
|
||||
// 从最上层窗口开始检查
|
||||
foreach (var window in _windows)
|
||||
{
|
||||
// 跳过排除的进程
|
||||
if (excludeProcessNames.Contains(window.ProcessName)) continue;
|
||||
|
||||
// 计算覆盖比例
|
||||
double coverage = window.GetCoverageRatio(area);
|
||||
if (coverage >= coverageThreshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定区域是否被全屏窗口覆盖
|
||||
/// </summary>
|
||||
/// <param name="area">要检查的区域</param>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表</param>
|
||||
/// <returns>如果被全屏窗口覆盖返回true</returns>
|
||||
public bool IsAreaCoveredByFullScreenWindow(WindowRect area, List<string> excludeProcessNames = null)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
|
||||
// 查找全屏窗口
|
||||
foreach (var window in _windows)
|
||||
{
|
||||
// 跳过排除的进程
|
||||
if (excludeProcessNames.Contains(window.ProcessName)) continue;
|
||||
|
||||
// 只检查全屏窗口
|
||||
if (window.IsFullScreen && window.CoversArea(area))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取覆盖指定区域的所有窗口
|
||||
/// </summary>
|
||||
/// <param name="area">要检查的区域</param>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表</param>
|
||||
/// <param name="coverageThreshold">覆盖阈值</param>
|
||||
/// <returns>覆盖该区域的窗口列表(按Z顺序,最上层在前)</returns>
|
||||
public List<WindowInfo> GetCoveringWindows(WindowRect area, List<string> excludeProcessNames = null, double coverageThreshold = 0.1)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
var coveringWindows = new List<WindowInfo>();
|
||||
|
||||
foreach (var window in _windows)
|
||||
{
|
||||
// 跳过排除的进程
|
||||
if (excludeProcessNames.Contains(window.ProcessName)) continue;
|
||||
|
||||
// 计算覆盖比例
|
||||
double coverage = window.GetCoverageRatio(area);
|
||||
if (coverage >= coverageThreshold)
|
||||
{
|
||||
coveringWindows.Add(window);
|
||||
}
|
||||
}
|
||||
|
||||
return coveringWindows;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有全屏窗口
|
||||
/// </summary>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表</param>
|
||||
/// <returns>如果有全屏窗口返回true</returns>
|
||||
public bool HasFullScreenWindow(List<string> excludeProcessNames = null)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
|
||||
return _windows.Any(w => !excludeProcessNames.Contains(w.ProcessName) && w.IsFullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有全屏窗口
|
||||
/// </summary>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表</param>
|
||||
/// <returns>全屏窗口列表</returns>
|
||||
public List<WindowInfo> GetFullScreenWindows(List<string> excludeProcessNames = null)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
|
||||
return _windows.Where(w => !excludeProcessNames.Contains(w.ProcessName) && w.IsFullScreen).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据进程名查找窗口
|
||||
/// </summary>
|
||||
/// <param name="processName">进程名</param>
|
||||
/// <returns>匹配的窗口列表</returns>
|
||||
public List<WindowInfo> FindWindowsByProcessName(string processName)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _windows.Where(w => w.ProcessName.Equals(processName, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据窗口标题查找窗口
|
||||
/// </summary>
|
||||
/// <param name="title">窗口标题(支持部分匹配)</param>
|
||||
/// <returns>匹配的窗口列表</returns>
|
||||
public List<WindowInfo> FindWindowsByTitle(string title)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _windows.Where(w => w.Title.IndexOf(title, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
_isDisposed = true;
|
||||
_updateTimer?.Dispose();
|
||||
_updateTimer = null;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
_windows.Clear();
|
||||
}
|
||||
|
||||
lock (_processCacheLock)
|
||||
{
|
||||
_processCache.Clear();
|
||||
}
|
||||
|
||||
_windowCache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
<PropertyGroup>
|
||||
<RuntimeIdentifiers>win;win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>Ink_Canvas</RootNamespace>
|
||||
<AssemblyName>InkCanvasForClass</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
||||
@@ -148,20 +149,28 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="1.1.0" />
|
||||
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.9.27" />
|
||||
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="2.0.1" />
|
||||
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.10.2.1" />
|
||||
<PackageReference Include="iNKORE.UI.WPF" Version="1.2.8" />
|
||||
<PackageReference Include="MdXaml" Version="1.27.0" />
|
||||
<PackageReference Include="Microsoft.Office.Interop.PowerPoint" Version="15.0.4420.1018" />
|
||||
<PackageReference Include="MicrosoftOfficeCore" Version="15.0.0" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<PackageReference Include="Microsoft.International.Converters.PinYinConverter" Version="1.0.0" />
|
||||
<PackageReference Include="Sentry" Version="6.1.0" />
|
||||
</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" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="NHotkey.Wpf" Version="4.0.0" />
|
||||
<PackageReference Include="OSVersionExt" Version="4.1.0" />
|
||||
<PackageReference Include="AForge.Video" Version="2.2.5" />
|
||||
<PackageReference Include="AForge.Video.DirectShow" Version="2.2.5" />
|
||||
<PackageReference Include="AForge.Imaging" Version="2.2.5" />
|
||||
<PackageReference Include="AForge.Math" Version="2.2.5" />
|
||||
<PackageReference Include="WebDav.Client" Version="2.9.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ItemGroup Condition="'$(MSBuildRuntimeType)' == 'Full'">
|
||||
<COMReference Include="IWshRuntimeLibrary">
|
||||
<Guid>{F935DC20-1CF0-11D0-ADB9-00C04FD58A0B}</Guid>
|
||||
<VersionMajor>1</VersionMajor>
|
||||
@@ -190,6 +199,18 @@
|
||||
<EmbedInteropTypes>True</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(MSBuildRuntimeType)' != 'Full'">
|
||||
<Reference Include="Interop.IWshRuntimeLibrary">
|
||||
<HintPath>libs\Interop.IWshRuntimeLibrary.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
<EmbedInteropTypes>True</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="Interop.stdole">
|
||||
<HintPath>libs\Interop.stdole.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
<EmbedInteropTypes>True</EmbedInteropTypes>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Resources\TimerDownNotice.wav" />
|
||||
<None Include="Resources\ProgressiveAudio.wav" />
|
||||
@@ -267,6 +288,7 @@
|
||||
<Resource Include="Resources\DeveloperAvatars\STBBRD.png" />
|
||||
<Resource Include="Resources\DeveloperAvatars\WXRIW.png" />
|
||||
<Resource Include="Resources\DeveloperAvatars\PrefacedCorg.jpg" />
|
||||
<Resource Include="Resources\DeveloperAvatars\PANDA-JSR.jpg" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\down.png" />
|
||||
@@ -292,6 +314,17 @@
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_delete_24_regular.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="privacy.txt" />
|
||||
<EmbeddedResource Include="telemetry_dsn.txt" Condition="Exists('telemetry_dsn.txt') AND '$(DLASS_SENTRY_DSN)' == ''">
|
||||
<Link>telemetry_dsn.txt</Link>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(MSBuildProjectDirectory)\telemetry_dsn.generated.txt" Condition="'$(DLASS_SENTRY_DSN)' != ''">
|
||||
<Link>telemetry_dsn.txt</Link>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_cursorWITHdelete_24_regular.png" />
|
||||
</ItemGroup>
|
||||
@@ -552,6 +585,14 @@
|
||||
<ItemGroup>
|
||||
<Compile Remove="AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="Properties\Strings.en-US.resx" />
|
||||
<EmbeddedResource Remove="**\Strings.en-US.resx" />
|
||||
<None Include="Properties\Strings.en-US.resx" />
|
||||
<EmbeddedResource Include="Properties\Strings.enUS.xml">
|
||||
<LogicalName>Ink_Canvas.Properties.Strings.enUS.xml</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="MainWindow.xaml~RF6c3144.TMP" />
|
||||
<None Remove="Resources\Cursors\Cursor.cur" />
|
||||
@@ -654,4 +695,11 @@
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<Target Name="GenerateTelemetryDsn" BeforeTargets="PrepareResources" Condition="'$(DLASS_SENTRY_DSN)' != ''">
|
||||
<WriteLinesToFile File="$(MSBuildProjectDirectory)\telemetry_dsn.generated.txt" Lines="$(DLASS_SENTRY_DSN)" Overwrite="true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CleanTelemetryDsn" AfterTargets="Build;Clean" Condition="Exists('$(MSBuildProjectDirectory)\telemetry_dsn.generated.txt')">
|
||||
<Delete Files="$(MSBuildProjectDirectory)\telemetry_dsn.generated.txt" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
+2897
-1888
File diff suppressed because it is too large
Load Diff
+1807
-111
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,11 @@
|
||||
namespace Ink_Canvas
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置助手类(预留),用于未来扩展配置相关的辅助方法。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该类目前为预留类,用于未来扩展配置相关的辅助方法。
|
||||
/// </remarks>
|
||||
internal class ConfigHelper
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
using System;
|
||||
using System.Threading;
|
||||
@@ -13,9 +13,31 @@ namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 浮动栏是否折叠的标志。
|
||||
/// </summary>
|
||||
public bool isFloatingBarFolded;
|
||||
|
||||
/// <summary>
|
||||
/// 浮动栏正在改变隐藏模式的标志,用于防止重复操作。
|
||||
/// </summary>
|
||||
private bool isFloatingBarChangingHideMode;
|
||||
|
||||
/// <summary>
|
||||
/// 立即关闭白板模式,恢复到批注模式。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 检查是否正在显示或隐藏黑板,如果是则直接返回
|
||||
/// 2. 设置显示/隐藏黑板的标志为true
|
||||
/// 3. 立即隐藏所有子面板
|
||||
/// 4. 如果启用了自动切换多指手势,则关闭多指平移
|
||||
/// 5. 隐藏所有水印
|
||||
/// 6. 切换到批注模式
|
||||
/// 7. 设置退出按钮前景色为白色
|
||||
/// 8. 设置应用主题为深色
|
||||
/// 9. 200毫秒后重置显示/隐藏黑板的标志为false
|
||||
/// </remarks>
|
||||
private void CloseWhiteboardImmediately()
|
||||
{
|
||||
if (isDisplayingOrHidingBlackboard) return;
|
||||
@@ -38,11 +60,35 @@ namespace Ink_Canvas
|
||||
}).Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理折叠浮动栏的鼠标点击事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="e">鼠标按钮事件参数。</param>
|
||||
public async void FoldFloatingBar_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
await FoldFloatingBar(sender);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 折叠浮动栏,将其收纳到侧边栏。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="isAutoFoldCommand">是否为自动折叠命令。</param>
|
||||
/// <returns>表示异步操作的任务。</returns>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 检查是否应该拒绝操作(如点击了折叠图标但上次鼠标按下的对象不是折叠图标)
|
||||
/// 2. 设置折叠/展开标志
|
||||
/// 3. 检查浮动栏是否已经折叠或正在改变隐藏模式,如果是则直接返回
|
||||
/// 4. 处理墨迹重放相关的UI元素
|
||||
/// 5. 设置浮动栏状态标志,关闭白板模式(如果当前在白板模式)
|
||||
/// 6. 如果是用户手动折叠且画布上有较多墨迹,显示通知
|
||||
/// 7. 清空画布墨迹
|
||||
/// 8. 隐藏PPT导航面板和浮动栏拖动网格
|
||||
/// 9. 执行浮动栏和侧边栏的动画
|
||||
/// 10. 如果开启了彻底隐藏,则隐藏主窗口
|
||||
/// </remarks>
|
||||
public async Task FoldFloatingBar(object sender, bool isAutoFoldCommand = false)
|
||||
{
|
||||
var isShouldRejectAction = false;
|
||||
@@ -54,7 +100,10 @@ namespace Ink_Canvas
|
||||
if (sender == Fold_Icon && lastBorderMouseDownObject != Fold_Icon) isShouldRejectAction = true;
|
||||
});
|
||||
|
||||
if (isShouldRejectAction) return;
|
||||
if (isShouldRejectAction)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// FloatingBarIcons_MouseUp_New(sender);
|
||||
if (sender == null)
|
||||
@@ -63,7 +112,12 @@ namespace Ink_Canvas
|
||||
foldFloatingBarByUser = true;
|
||||
unfoldFloatingBarByUser = false;
|
||||
|
||||
if (isFloatingBarChangingHideMode) return;
|
||||
if (isFloatingBarFolded) return;
|
||||
|
||||
if (isFloatingBarChangingHideMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
@@ -103,8 +157,28 @@ namespace Ink_Canvas
|
||||
HideSubPanels("cursor");
|
||||
SidePannelMarginAnimation(-10);
|
||||
});
|
||||
|
||||
// 新增:如果开启了彻底隐藏,则隐藏主窗口
|
||||
if (Settings.Automation.ThoroughlyHideWhenFolded)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
this.Visibility = Visibility.Hidden;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理左侧展开按钮显示快捷面板的鼠标点击事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="e">鼠标按钮事件参数。</param>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 检查是否显示快捷面板
|
||||
/// 2. 如果显示快捷面板,则隐藏右侧快捷面板,显示左侧快捷面板并执行动画
|
||||
/// 3. 否则,调用展开浮动栏的方法
|
||||
/// </remarks>
|
||||
private async void LeftUnFoldButtonDisplayQuickPanel_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (Settings.Appearance.IsShowQuickPanel)
|
||||
@@ -135,6 +209,17 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理右侧展开按钮显示快捷面板的鼠标点击事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="e">鼠标按钮事件参数。</param>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 检查是否显示快捷面板
|
||||
/// 2. 如果显示快捷面板,则隐藏左侧快捷面板,显示右侧快捷面板并执行动画
|
||||
/// 3. 否则,调用展开浮动栏的方法
|
||||
/// </remarks>
|
||||
private async void RightUnFoldButtonDisplayQuickPanel_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (Settings.Appearance.IsShowQuickPanel)
|
||||
@@ -165,6 +250,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏左侧快捷面板。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 检查左侧快捷面板是否可见,如果不可见则直接返回
|
||||
/// 2. 执行左侧快捷面板的隐藏动画
|
||||
/// 3. 等待动画完成后,设置左侧快捷面板的边距并将其折叠
|
||||
/// </remarks>
|
||||
private async void HideLeftQuickPanel()
|
||||
{
|
||||
if (LeftUnFoldButtonQuickPanel.Visibility == Visibility.Visible)
|
||||
@@ -190,6 +284,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏右侧快捷面板。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 检查右侧快捷面板是否可见,如果不可见则直接返回
|
||||
/// 2. 执行右侧快捷面板的隐藏动画
|
||||
/// 3. 等待动画完成后,设置右侧快捷面板的边距并将其折叠
|
||||
/// </remarks>
|
||||
private async void HideRightQuickPanel()
|
||||
{
|
||||
if (RightUnFoldButtonQuickPanel.Visibility == Visibility.Visible)
|
||||
@@ -215,19 +318,61 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理隐藏快捷面板的鼠标点击事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="e">鼠标按钮事件参数。</param>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 隐藏左侧快捷面板
|
||||
/// 2. 隐藏右侧快捷面板
|
||||
/// </remarks>
|
||||
private void HideQuickPanel_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
HideLeftQuickPanel();
|
||||
HideRightQuickPanel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理展开浮动栏的鼠标点击事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="e">鼠标按钮事件参数。</param>
|
||||
public async void UnFoldFloatingBar_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
await UnFoldFloatingBar(sender);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 展开浮动栏,将其从侧边栏恢复到正常状态。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <returns>表示异步操作的任务。</returns>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 如果之前彻底隐藏了主窗口,先恢复显示
|
||||
/// 2. 隐藏左右侧快捷面板
|
||||
/// 3. 设置展开/折叠标志
|
||||
/// 4. 检查浮动栏是否正在改变隐藏模式,如果是则直接返回
|
||||
/// 5. 设置浮动栏状态标志,标记为未折叠
|
||||
/// 6. 根据设置决定是否自动切换至批注模式
|
||||
/// 7. 根据PPT放映模式和设置显示或隐藏翻页按钮
|
||||
/// 8. 在屏幕模式下显示浮动栏并执行动画
|
||||
/// 9. 执行侧边栏动画
|
||||
/// 10. 等待UI完全更新后,重新设置当前选中模式的按钮高亮状态
|
||||
/// </remarks>
|
||||
public async Task UnFoldFloatingBar(object sender)
|
||||
{
|
||||
// 新增:如果之前彻底隐藏了,先恢复显示
|
||||
if (this.Visibility != Visibility.Visible)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
this.Visibility = Visibility.Visible;
|
||||
});
|
||||
}
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
LeftUnFoldButtonQuickPanel.Visibility = Visibility.Collapsed;
|
||||
@@ -239,7 +384,11 @@ namespace Ink_Canvas
|
||||
unfoldFloatingBarByUser = true;
|
||||
foldFloatingBarByUser = false;
|
||||
|
||||
if (isFloatingBarChangingHideMode) return;
|
||||
if (isFloatingBarChangingHideMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
@@ -326,6 +475,21 @@ namespace Ink_Canvas
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行侧边栏边距动画,用于折叠或展开侧边栏。
|
||||
/// </summary>
|
||||
/// <param name="MarginFromEdge">侧边栏距边缘的边距值。可能的值:-50(完全折叠), -10(半展开)</param>
|
||||
/// <param name="isNoAnimation">是否禁用动画效果。</param>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 如果边距值为-10(半展开),则显示左侧边栏
|
||||
/// 2. 创建并执行左侧边栏的边距动画
|
||||
/// 3. 创建并执行右侧边栏的边距动画
|
||||
/// 4. 等待600毫秒让动画完成
|
||||
/// 5. 直接设置侧边栏的最终边距值
|
||||
/// 6. 如果边距值为-50(完全折叠),则隐藏左侧边栏
|
||||
/// 7. 重置浮动栏正在改变隐藏模式的标志为false
|
||||
/// </remarks>
|
||||
private async void SidePannelMarginAnimation(int MarginFromEdge, bool isNoAnimation = false) // Possible value: -50, -10
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using IWshRuntimeLibrary;
|
||||
using IWshRuntimeLibrary;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using Application = System.Windows.Forms.Application;
|
||||
@@ -8,6 +8,22 @@ namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建开机自启动快捷方式。
|
||||
/// </summary>
|
||||
/// <param name="exeName">可执行文件名,用于命名快捷方式。</param>
|
||||
/// <returns>创建成功返回true,失败返回false。</returns>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 创建Windows Shell对象
|
||||
/// 2. 在启动文件夹中创建快捷方式
|
||||
/// 3. 设置快捷方式的目标路径为当前可执行文件路径
|
||||
/// 4. 设置工作目录为当前目录
|
||||
/// 5. 设置窗口样式为普通窗口
|
||||
/// 6. 设置快捷方式描述
|
||||
/// 7. 保存快捷方式
|
||||
/// 8. 捕获可能的异常,确保方法不会因异常而崩溃
|
||||
/// </remarks>
|
||||
public static bool StartAutomaticallyCreate(string exeName)
|
||||
{
|
||||
try
|
||||
@@ -34,6 +50,16 @@ namespace Ink_Canvas
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除开机自启动快捷方式。
|
||||
/// </summary>
|
||||
/// <param name="exeName">可执行文件名,用于定位要删除的快捷方式。</param>
|
||||
/// <returns>删除成功返回true,失败返回false。</returns>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 在启动文件夹中删除指定名称的快捷方式
|
||||
/// 2. 捕获可能的异常,确保方法不会因异常而崩溃
|
||||
/// </remarks>
|
||||
public static bool StartAutomaticallyDel(string exeName)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Application = System.Windows.Application;
|
||||
using ui = iNKORE.UI.WPF.Modern.Controls;
|
||||
using ui = iNKORE.UI.WPF.Controls;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 浮动栏前景色,根据当前主题动态更新。
|
||||
/// </summary>
|
||||
private Color FloatBarForegroundColor;
|
||||
|
||||
/// <summary>
|
||||
/// 应用并切换到指定的主题("Light" 或 "Dark"),更新主题资源并刷新相关 UI 元素以反映主题变化。
|
||||
/// </summary>
|
||||
/// <param name="theme">主题标识,支持 "Light" 或 "Dark"(区分大小写)。</param>
|
||||
/// <param name="autoSwitchIcon">若为 true,则根据主题自动切换并保存浮动工具栏的图标设置。</param>
|
||||
private void SetTheme(string theme, bool autoSwitchIcon = false)
|
||||
{
|
||||
// 清理现有的主题资源
|
||||
@@ -35,22 +45,38 @@ namespace Ink_Canvas
|
||||
|
||||
if (theme == "Light")
|
||||
{
|
||||
// 先加载主题
|
||||
var rd1 = new ResourceDictionary
|
||||
{ Source = new Uri("Resources/Styles/Light.xaml", UriKind.Relative) };
|
||||
{
|
||||
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);
|
||||
// 异步加载图形资源,避免阻塞启动
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
var rd2 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd2);
|
||||
|
||||
var rd3 = new ResourceDictionary
|
||||
{ Source = new Uri("Resources/SeewoImageDictionary.xaml", UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd3);
|
||||
var rd3 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/SeewoImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd3);
|
||||
|
||||
var rd4 = new ResourceDictionary
|
||||
{ Source = new Uri("Resources/IconImageDictionary.xaml", UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd4);
|
||||
var rd4 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/IconImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd4);
|
||||
});
|
||||
});
|
||||
|
||||
ThemeManager.SetRequestedTheme(window, ElementTheme.Light);
|
||||
|
||||
@@ -83,21 +109,35 @@ namespace Ink_Canvas
|
||||
}
|
||||
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);
|
||||
// 异步加载图形资源,避免阻塞启动
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
var rd2 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd2);
|
||||
|
||||
var rd3 = new ResourceDictionary
|
||||
{ Source = new Uri("Resources/SeewoImageDictionary.xaml", UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd3);
|
||||
var rd3 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/SeewoImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd3);
|
||||
|
||||
var rd4 = new ResourceDictionary
|
||||
{ Source = new Uri("Resources/IconImageDictionary.xaml", UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd4);
|
||||
var rd4 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/IconImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd4);
|
||||
});
|
||||
});
|
||||
|
||||
ThemeManager.SetRequestedTheme(window, ElementTheme.Dark);
|
||||
|
||||
@@ -296,6 +336,18 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理系统主题偏好变化事件,根据当前设置更新应用主题。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="e">用户偏好变化事件参数。</param>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 根据当前主题设置(Settings.Appearance.Theme)决定使用哪种主题
|
||||
/// 2. 如果设置为0(浅色主题),则设置为Light主题
|
||||
/// 3. 如果设置为1(深色主题),则设置为Dark主题
|
||||
/// 4. 如果设置为2(跟随系统主题),则根据系统主题设置应用相应的主题
|
||||
/// </remarks>
|
||||
private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
|
||||
{
|
||||
switch (Settings.Appearance.Theme)
|
||||
@@ -313,6 +365,17 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查系统主题是否为浅色主题。
|
||||
/// </summary>
|
||||
/// <returns>系统主题为浅色返回true,深色返回false。</returns>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 从注册表中读取系统主题设置
|
||||
/// 2. 检查"SystemUsesLightTheme"键的值
|
||||
/// 3. 如果值为1,则表示系统使用浅色主题
|
||||
/// 4. 捕获可能的异常,确保方法不会因异常而崩溃
|
||||
/// </remarks>
|
||||
private bool IsSystemThemeLight()
|
||||
{
|
||||
var light = false;
|
||||
@@ -325,7 +388,7 @@ namespace Ink_Canvas
|
||||
if (themeKey != null) keyValue = (int)themeKey.GetValue("SystemUsesLightTheme");
|
||||
if (keyValue == 1) light = true;
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
return light;
|
||||
}
|
||||
@@ -536,6 +599,17 @@ namespace Ink_Canvas
|
||||
operatingGuideWindow.RefreshTheme();
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新计时器控件
|
||||
if (TimerControl != null)
|
||||
{
|
||||
TimerControl.RefreshTheme();
|
||||
}
|
||||
|
||||
if (MinimizedTimerControl != null)
|
||||
{
|
||||
MinimizedTimerControl.RefreshTheme();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -1,27 +1,62 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储每个白板页面的墨迹集合
|
||||
/// </summary>
|
||||
private StrokeCollection[] strokeCollections = new StrokeCollection[101];
|
||||
|
||||
/// <summary>
|
||||
/// 存储每个白板页面的最后操作模式是否为重做
|
||||
/// </summary>
|
||||
private bool[] whiteboadLastModeIsRedo = new bool[101];
|
||||
|
||||
/// <summary>
|
||||
/// 存储最后一次触摸按下时的墨迹集合
|
||||
/// </summary>
|
||||
private StrokeCollection lastTouchDownStrokeCollection = new StrokeCollection();
|
||||
|
||||
/// <summary>
|
||||
/// 当前白板页面索引
|
||||
/// </summary>
|
||||
private int CurrentWhiteboardIndex = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 白板页面总数
|
||||
/// </summary>
|
||||
private int WhiteboardTotalCount = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 存储每个白板页面的时间机器历史记录
|
||||
/// </summary>
|
||||
private TimeMachineHistory[][] TimeMachineHistories = new TimeMachineHistory[101][];
|
||||
|
||||
/// <summary>
|
||||
/// 存储每个白板页面的多指书写模式状态
|
||||
/// </summary>
|
||||
private bool[] savedMultiTouchModeStates = new bool[101];
|
||||
|
||||
// 保存每页白板图片信息
|
||||
/// <summary>
|
||||
/// 将当前画布上的所有未保存的图片/媒体和墨迹提交到时间机器历史并将导出结果保存为指定页的快照。
|
||||
/// </summary>
|
||||
/// <param name="isBackupMain">为 true 时将导出结果保存到主备份槽(索引 0);为 false 时保存到当前白板索引。</param>
|
||||
/// <remarks>
|
||||
/// - 会提交画布上缺失于历史记录的 Image/MediaElement(但跳过 Tag 等于 VideoPresenterLiveFrameTag 的 Image)和缺失的墨迹;
|
||||
/// - 导出后把结果存入 TimeMachineHistories 的相应索引,并保存当前多指书写模式到 savedMultiTouchModeStates;
|
||||
/// - 导出后会清除时间机器的临时墨迹历史以释放内存。
|
||||
/// - 此方法有副作用:修改 TimeMachineHistories、savedMultiTouchModeStates,并通过 timeMachine 的提交方法改变其内部历史状态。
|
||||
/// </remarks>
|
||||
private void SaveStrokes(bool isBackupMain = false)
|
||||
{
|
||||
// 确保画布上的所有UI元素都被保存到时间机器历史记录中
|
||||
@@ -48,6 +83,10 @@ namespace Ink_Canvas
|
||||
{
|
||||
if (child is Image || child is MediaElement)
|
||||
{
|
||||
if (child is Image img && img.Tag is string tag && tag == VideoPresenterLiveFrameTag)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!elementsInHistory.Contains(child))
|
||||
{
|
||||
timeMachine.CommitElementInsertHistory(child);
|
||||
@@ -116,6 +155,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除画布上的所有墨迹并执行内存清理
|
||||
/// </summary>
|
||||
/// <param name="isErasedByCode">是否由代码触发的清除操作</param>
|
||||
/// <remarks>
|
||||
/// - 根据参数设置当前提交类型
|
||||
/// - 清除画布上的所有墨迹
|
||||
/// - 恢复当前提交类型为用户输入
|
||||
/// </remarks>
|
||||
private void ClearStrokes(bool isErasedByCode)
|
||||
{
|
||||
_currentCommitType = CommitReason.ClearingCanvas;
|
||||
@@ -123,24 +171,32 @@ namespace Ink_Canvas
|
||||
|
||||
inkCanvas.Strokes.Clear();
|
||||
|
||||
// 执行内存清理
|
||||
PerformLightweightMemoryCleanup();
|
||||
|
||||
_currentCommitType = CommitReason.UserInput;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行内存清理
|
||||
/// </summary>
|
||||
private void PerformLightweightMemoryCleanup()
|
||||
private static HashSet<UIElement> CollectRemovedElementsFromHistory(TimeMachineHistory[] history)
|
||||
{
|
||||
Task.Run(() =>
|
||||
var set = new HashSet<UIElement>();
|
||||
if (history == null) return set;
|
||||
foreach (var h in history)
|
||||
{
|
||||
GC.Collect();
|
||||
});
|
||||
if (h.CommitType == TimeMachineHistoryType.ElementInsert && h.StrokeHasBeenCleared && h.InsertedElement != null)
|
||||
set.Add(h.InsertedElement);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
// 恢复每页白板图片信息
|
||||
/// <summary>
|
||||
/// 恢复指定白板页面的墨迹和元素信息
|
||||
/// </summary>
|
||||
/// <param name="isBackupMain">是否恢复主备份页面</param>
|
||||
/// <remarks>
|
||||
/// - 隐藏图片选择工具栏
|
||||
/// - 清空当前画布的墨迹和所有内容
|
||||
/// - 从时间机器历史记录中恢复页面内容
|
||||
/// - 恢复多指书写模式状态
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void RestoreStrokes(bool isBackupMain = false)
|
||||
{
|
||||
try
|
||||
@@ -175,17 +231,40 @@ namespace Ink_Canvas
|
||||
if (isBackupMain)
|
||||
{
|
||||
timeMachine.ImportTimeMachineHistory(TimeMachineHistories[0]);
|
||||
foreach (var item in TimeMachineHistories[0]) ApplyHistoryToCanvas(item);
|
||||
// 恢复多指书写模式状态
|
||||
var removed0 = CollectRemovedElementsFromHistory(TimeMachineHistories[0]);
|
||||
var elementsToProcess = new List<UIElement>();
|
||||
foreach (var item in TimeMachineHistories[0])
|
||||
{
|
||||
if (item.CommitType == TimeMachineHistoryType.ElementInsert &&
|
||||
!item.StrokeHasBeenCleared &&
|
||||
item.InsertedElement != null &&
|
||||
!removed0.Contains(item.InsertedElement))
|
||||
{
|
||||
elementsToProcess.Add(item.InsertedElement);
|
||||
}
|
||||
ApplyHistoryToCanvas(item, null, removed0);
|
||||
}
|
||||
RestoreMultiTouchModeState(0);
|
||||
ProcessElementsAfterRestore(elementsToProcess);
|
||||
}
|
||||
else
|
||||
{
|
||||
timeMachine.ImportTimeMachineHistory(TimeMachineHistories[CurrentWhiteboardIndex]);
|
||||
// 通过时间机器历史恢复所有内容(墨迹和图片)
|
||||
foreach (var item in TimeMachineHistories[CurrentWhiteboardIndex]) ApplyHistoryToCanvas(item);
|
||||
// 恢复多指书写模式状态
|
||||
var removed = CollectRemovedElementsFromHistory(TimeMachineHistories[CurrentWhiteboardIndex]);
|
||||
var elementsToProcess = new List<UIElement>();
|
||||
foreach (var item in TimeMachineHistories[CurrentWhiteboardIndex])
|
||||
{
|
||||
if (item.CommitType == TimeMachineHistoryType.ElementInsert &&
|
||||
!item.StrokeHasBeenCleared &&
|
||||
item.InsertedElement != null &&
|
||||
!removed.Contains(item.InsertedElement))
|
||||
{
|
||||
elementsToProcess.Add(item.InsertedElement);
|
||||
}
|
||||
ApplyHistoryToCanvas(item, null, removed);
|
||||
}
|
||||
RestoreMultiTouchModeState(CurrentWhiteboardIndex);
|
||||
ProcessElementsAfterRestore(elementsToProcess);
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +275,45 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在恢复页面后统一处理所有图片/媒体元素的位置和事件绑定,提升含图片页面的加载性能。
|
||||
/// 先批量添加所有元素到画布,再统一处理位置和事件,减少布局更新次数。
|
||||
/// </summary>
|
||||
private void ProcessElementsAfterRestore(List<UIElement> elements)
|
||||
{
|
||||
if (elements == null || elements.Count == 0) return;
|
||||
|
||||
// 使用低优先级异步处理,让 UI 先响应,图片位置和事件绑定稍后完成
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
|
||||
{
|
||||
foreach (var element in elements)
|
||||
{
|
||||
if (!inkCanvas.Children.Contains(element)) continue;
|
||||
|
||||
if (element is Image img)
|
||||
{
|
||||
double left = InkCanvas.GetLeft(img);
|
||||
double top = InkCanvas.GetTop(img);
|
||||
if (double.IsNaN(left) || double.IsNaN(top))
|
||||
{
|
||||
CenterAndScaleElement(img);
|
||||
}
|
||||
BindElementEvents(img);
|
||||
}
|
||||
else if (element is MediaElement media)
|
||||
{
|
||||
double left = InkCanvas.GetLeft(media);
|
||||
double top = InkCanvas.GetTop(media);
|
||||
if (double.IsNaN(left) || double.IsNaN(top))
|
||||
{
|
||||
CenterAndScaleElement(media);
|
||||
}
|
||||
BindElementEvents(media);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复多指书写模式状态
|
||||
/// </summary>
|
||||
@@ -206,9 +324,6 @@ namespace Ink_Canvas
|
||||
// 检查是否保存了多指书写模式状态
|
||||
if (savedMultiTouchModeStates[pageIndex])
|
||||
{
|
||||
// 恢复多指书写模式
|
||||
EnterMultiTouchModeIfNeeded();
|
||||
|
||||
// 更新UI状态
|
||||
if (ToggleSwitchEnableMultiTouchMode != null)
|
||||
{
|
||||
@@ -219,9 +334,6 @@ namespace Ink_Canvas
|
||||
}
|
||||
else
|
||||
{
|
||||
// 确保多指书写模式关闭
|
||||
ExitMultiTouchModeIfNeeded();
|
||||
|
||||
// 更新UI状态
|
||||
if (ToggleSwitchEnableMultiTouchMode != null)
|
||||
{
|
||||
@@ -235,6 +347,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理白板页面索引按钮点击事件,显示或隐藏侧边页面列表
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 处理左侧页面列表按钮点击:显示或隐藏左侧页面列表
|
||||
/// - 处理右侧页面列表按钮点击:显示或隐藏右侧页面列表
|
||||
/// - 显示页面列表时会刷新列表内容并滚动到当前页面
|
||||
/// </remarks>
|
||||
private async void BtnWhiteBoardPageIndex_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (sender == BtnLeftPageListWB)
|
||||
@@ -280,6 +402,12 @@ namespace Ink_Canvas
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到前一白板页并在切换过程中保存与恢复画布和相关状态(如果当前已是第一页则不执行任何操作)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该方法在切换前会取消当前选中元素(同时保留并恢复编辑模式)、调用视频呈现器的离开页前钩子、保存当前页的笔迹与元素、清空画布;切换到前一页后恢复该页内容、调用视频呈现器的页已更改钩子并刷新页面索引显示。
|
||||
/// </remarks>
|
||||
private void BtnWhiteBoardSwitchPrevious_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (CurrentWhiteboardIndex <= 1) return;
|
||||
@@ -295,22 +423,30 @@ namespace Ink_Canvas
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
|
||||
VideoPresenter_BeforePageLeave();
|
||||
SaveStrokes();
|
||||
|
||||
ClearStrokes(true);
|
||||
CurrentWhiteboardIndex--;
|
||||
|
||||
RestoreStrokes();
|
||||
VideoPresenter_OnPageChanged();
|
||||
|
||||
UpdateIndexInfoDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到白板的下一页;在到达最后一页时会新增一页。方法在切页前保存当前页面的笔迹/多媒体状态,在切页后恢复目标页面的内容并更新界面状态。
|
||||
/// </summary>
|
||||
/// <param name="sender">触发事件的源对象(通常为按钮)。</param>
|
||||
/// <param name="e">事件参数。</param>
|
||||
private void BtnWhiteBoardSwitchNext_Click(object sender, EventArgs e)
|
||||
{
|
||||
Trace.WriteLine("113223234");
|
||||
if (CurrentWhiteboardIndex < WhiteboardTotalCount &&
|
||||
Settings.Automation.IsAutoSaveStrokesAtClear &&
|
||||
inkCanvas.Strokes.Count > Settings.Automation.MinimumAutomationStrokeNumber)
|
||||
CaptureAndEnqueueScreenshotSave(isHideNotification: true);
|
||||
|
||||
if (Settings.Automation.IsAutoSaveStrokesAtClear &&
|
||||
inkCanvas.Strokes.Count > Settings.Automation.MinimumAutomationStrokeNumber) SaveScreenShot(true);
|
||||
if (CurrentWhiteboardIndex >= WhiteboardTotalCount)
|
||||
{
|
||||
// 在最后一页时,点击"新页面"按钮直接新增一页
|
||||
@@ -329,21 +465,34 @@ namespace Ink_Canvas
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
|
||||
VideoPresenter_BeforePageLeave();
|
||||
SaveStrokes();
|
||||
|
||||
ClearStrokes(true);
|
||||
CurrentWhiteboardIndex++;
|
||||
|
||||
RestoreStrokes();
|
||||
VideoPresenter_OnPageChanged();
|
||||
|
||||
UpdateIndexInfoDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在白板集合中添加一个新页面:在切换前保存并清除当前页面的笔迹与状态,插入新空白页面,恢复并刷新与页面相关的 UI 状态。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 在达到最大页面数(99)时不执行任何操作。
|
||||
/// - 在切换前若启用了自动保存且笔迹数量超过阈值,会保存当前画面截图。
|
||||
/// - 若有选中元素,会取消选中并恢复编辑模式。
|
||||
/// - 将当前页面的历史保存到时间轴并清空画布,然后在白板集合中插入一个空白页面(其历史为 null),随后恢复该页面并触发页面变更回调。
|
||||
/// - 更新页码显示并在达到上限时禁用添加按钮;若侧边页列表可见,则刷新该列表。
|
||||
/// </remarks>
|
||||
private void BtnWhiteBoardAdd_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (WhiteboardTotalCount >= 99) return;
|
||||
if (Settings.Automation.IsAutoSaveStrokesAtClear &&
|
||||
inkCanvas.Strokes.Count > Settings.Automation.MinimumAutomationStrokeNumber) SaveScreenShot(true);
|
||||
inkCanvas.Strokes.Count > Settings.Automation.MinimumAutomationStrokeNumber)
|
||||
CaptureAndEnqueueScreenshotSave(isHideNotification: true);
|
||||
|
||||
// 隐藏图片选择工具栏
|
||||
if (currentSelectedElement != null)
|
||||
@@ -356,6 +505,7 @@ namespace Ink_Canvas
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
|
||||
VideoPresenter_BeforePageLeave();
|
||||
SaveStrokes();
|
||||
ClearStrokes(true);
|
||||
|
||||
@@ -363,14 +513,20 @@ namespace Ink_Canvas
|
||||
CurrentWhiteboardIndex++;
|
||||
|
||||
if (CurrentWhiteboardIndex != WhiteboardTotalCount)
|
||||
{
|
||||
for (var i = WhiteboardTotalCount; i > CurrentWhiteboardIndex; i--)
|
||||
{
|
||||
TimeMachineHistories[i] = TimeMachineHistories[i - 1];
|
||||
savedMultiTouchModeStates[i] = savedMultiTouchModeStates[i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// 确保新页面的历史记录为空
|
||||
TimeMachineHistories[CurrentWhiteboardIndex] = null;
|
||||
|
||||
// 恢复新页面(这会清空画布,因为历史记录为null)
|
||||
RestoreStrokes();
|
||||
VideoPresenter_OnPageChanged();
|
||||
|
||||
UpdateIndexInfoDisplay();
|
||||
|
||||
@@ -382,36 +538,93 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理白板页面删除按钮点击事件,删除当前白板页面
|
||||
/// </summary>
|
||||
private void BtnWhiteBoardDelete_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 隐藏图片选择工具栏
|
||||
DeleteWhiteBoardPageByIndex(CurrentWhiteboardIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按页码删除指定白板页(用于预览列表等)。仅当总页数大于 1 时有效。
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">要删除的页码(1 到 WhiteboardTotalCount)</param>
|
||||
private void DeleteWhiteBoardPageByIndex(int pageIndex)
|
||||
{
|
||||
if (WhiteboardTotalCount <= 1 || pageIndex < 1 || pageIndex > WhiteboardTotalCount)
|
||||
return;
|
||||
|
||||
if (currentSelectedElement != null)
|
||||
{
|
||||
// 保存当前编辑模式
|
||||
var previousEditingMode = inkCanvas.EditingMode;
|
||||
UnselectElement(currentSelectedElement);
|
||||
// 恢复编辑模式
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
|
||||
ClearStrokes(true);
|
||||
if (pageIndex == CurrentWhiteboardIndex)
|
||||
{
|
||||
ClearStrokes(true);
|
||||
|
||||
if (CurrentWhiteboardIndex != WhiteboardTotalCount)
|
||||
for (var i = CurrentWhiteboardIndex; i <= WhiteboardTotalCount; i++)
|
||||
TimeMachineHistories[i] = TimeMachineHistories[i + 1];
|
||||
else
|
||||
var oldTotal = WhiteboardTotalCount;
|
||||
if (CurrentWhiteboardIndex != oldTotal)
|
||||
{
|
||||
for (var i = CurrentWhiteboardIndex; i < oldTotal; i++)
|
||||
{
|
||||
TimeMachineHistories[i] = FlattenPageHistory(TimeMachineHistories[i + 1]);
|
||||
savedMultiTouchModeStates[i] = savedMultiTouchModeStates[i + 1];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentWhiteboardIndex--;
|
||||
}
|
||||
|
||||
TimeMachineHistories[oldTotal] = null;
|
||||
WhiteboardTotalCount--;
|
||||
RestoreStrokes();
|
||||
}
|
||||
else if (pageIndex < CurrentWhiteboardIndex)
|
||||
{
|
||||
for (var i = pageIndex; i < WhiteboardTotalCount; i++)
|
||||
{
|
||||
TimeMachineHistories[i] = FlattenPageHistory(TimeMachineHistories[i + 1]);
|
||||
savedMultiTouchModeStates[i] = savedMultiTouchModeStates[i + 1];
|
||||
}
|
||||
TimeMachineHistories[WhiteboardTotalCount] = null;
|
||||
WhiteboardTotalCount--;
|
||||
CurrentWhiteboardIndex--;
|
||||
|
||||
WhiteboardTotalCount--;
|
||||
|
||||
RestoreStrokes();
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = pageIndex; i < WhiteboardTotalCount; i++)
|
||||
{
|
||||
TimeMachineHistories[i] = FlattenPageHistory(TimeMachineHistories[i + 1]);
|
||||
savedMultiTouchModeStates[i] = savedMultiTouchModeStates[i + 1];
|
||||
}
|
||||
TimeMachineHistories[WhiteboardTotalCount] = null;
|
||||
WhiteboardTotalCount--;
|
||||
}
|
||||
|
||||
UpdateIndexInfoDisplay();
|
||||
|
||||
if (WhiteboardTotalCount < 99) BtnWhiteBoardAdd.IsEnabled = true;
|
||||
if (BoardBorderLeftPageListView?.Visibility == Visibility.Visible ||
|
||||
BoardBorderRightPageListView?.Visibility == Visibility.Visible)
|
||||
RefreshBlackBoardSidePageListView();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新白板页码信息显示和按钮状态
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 更新页码显示文本
|
||||
/// - 设置下一页按钮文本(根据是否为最后一页)
|
||||
/// - 启用或禁用下一页按钮(根据是否为最后一页和最大页面数)
|
||||
/// - 设置按钮颜色和透明度
|
||||
/// - 启用或禁用上一页按钮(根据是否为第一页)
|
||||
/// - 设置删除按钮状态(根据页面总数)
|
||||
/// </remarks>
|
||||
private void UpdateIndexInfoDisplay()
|
||||
{
|
||||
TextBlockWhiteBoardIndexInfo.Text =
|
||||
@@ -424,8 +637,14 @@ namespace Ink_Canvas
|
||||
BtnLeftWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
|
||||
BtnRightWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
|
||||
|
||||
// 始终允许点击"下一页/新页面"按钮(除非已达最大页数)
|
||||
BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage;
|
||||
if (isLastPage)
|
||||
{
|
||||
BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage;
|
||||
}
|
||||
else
|
||||
{
|
||||
BtnWhiteBoardSwitchNext.IsEnabled = true;
|
||||
}
|
||||
|
||||
// 获取主题颜色资源
|
||||
var iconForegroundBrush = Application.Current.FindResource("IconForeground") as SolidColorBrush;
|
||||
|
||||
@@ -12,6 +12,19 @@ namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理背景颜色按钮点击事件,显示或隐藏背景颜色选项面板
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查应用是否已加载
|
||||
/// - 创建背景选项面板(如果不存在)
|
||||
/// - 显示或隐藏背景选项面板
|
||||
/// - 隐藏其他可能显示的面板
|
||||
/// - 处理白板/黑板模式切换
|
||||
/// - 更新背景颜色和墨迹颜色
|
||||
/// </remarks>
|
||||
private void BoardChangeBackgroundColorBtn_MouseUp(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
@@ -124,7 +137,18 @@ namespace Ink_Canvas
|
||||
CheckColorTheme(true);
|
||||
}
|
||||
|
||||
// 创建背景选项面板
|
||||
/// <summary>
|
||||
/// 创建背景颜色选项面板
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 加载自定义背景色
|
||||
/// - 创建背景选项面板UI
|
||||
/// - 添加标题栏和关闭按钮
|
||||
/// - 添加白板/黑板模式选择按钮
|
||||
/// - 添加RGB颜色选择器
|
||||
/// - 添加颜色预览和应用按钮
|
||||
/// - 将面板添加到主网格
|
||||
/// </remarks>
|
||||
private void CreateBackgroundPalette()
|
||||
{
|
||||
// 确保加载自定义背景色
|
||||
@@ -543,7 +567,13 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 更新背景按钮状态
|
||||
/// <summary>
|
||||
/// 更新背景颜色选项面板中的按钮状态
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 更新白板和黑板按钮的背景和前景色
|
||||
/// - 根据当前使用的模式设置按钮状态
|
||||
/// </remarks>
|
||||
private void UpdateBackgroundButtonsState()
|
||||
{
|
||||
if (BackgroundPalette != null && BackgroundPalette.Child is StackPanel stackPanel)
|
||||
@@ -582,10 +612,14 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 添加成员变量保存背景面板引用
|
||||
/// <summary>
|
||||
/// 背景颜色选项面板
|
||||
/// </summary>
|
||||
private Border BackgroundPalette { get; set; }
|
||||
|
||||
// 添加成员变量保存当前自定义背景色
|
||||
/// <summary>
|
||||
/// 当前自定义背景色
|
||||
/// </summary>
|
||||
private Color? CustomBackgroundColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -702,6 +736,17 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理套索工具图标点击事件,切换到选择模式
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 禁用橡皮擦模式
|
||||
/// - 禁用形状绘制模式
|
||||
/// - 设置当前工具模式为选择模式
|
||||
/// - 根据编辑模式设置光标
|
||||
/// </remarks>
|
||||
private void BoardLassoIcon_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
forceEraser = false;
|
||||
@@ -712,6 +757,22 @@ namespace Ink_Canvas
|
||||
SetCursorBasedOnEditingMode(inkCanvas);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理橡皮擦图标点击事件,切换到按笔画擦除模式
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 禁用高级橡皮擦系统
|
||||
/// - 启用橡皮擦模式
|
||||
/// - 设置橡皮擦形状为圆形
|
||||
/// - 设置当前工具模式为按笔画擦除
|
||||
/// - 禁用形状绘制模式
|
||||
/// - 重置钢笔类型和属性
|
||||
/// - 触发编辑模式变更事件
|
||||
/// - 取消单指拖动模式
|
||||
/// - 隐藏子面板
|
||||
/// </remarks>
|
||||
private void BoardEraserIconByStrokes_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
//if (BoardEraserByStrokes.Background.ToString() == "#FF679CF4") {
|
||||
@@ -740,6 +801,18 @@ namespace Ink_Canvas
|
||||
//}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理删除图标点击事件,清空画布内容
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 调用钢笔图标点击事件
|
||||
/// - 调用符号删除鼠标抬起事件
|
||||
/// - 根据设置决定是否清空图片
|
||||
/// - 如果设置为清空图片,则清空所有子元素
|
||||
/// - 否则,保存非笔画元素并在清空后恢复
|
||||
/// </remarks>
|
||||
private void BoardSymbolIconDelete_MouseUp(object sender, RoutedEventArgs e)
|
||||
{
|
||||
PenIcon_Click(null, null);
|
||||
@@ -764,6 +837,19 @@ namespace Ink_Canvas
|
||||
Debug.WriteLine($"BoardSymbolIconDelete: inkCanvas.Children.Count after restore: {inkCanvas.Children.Count}");
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 处理删除墨迹和历史记录图标点击事件,清空画布内容和时间机器历史
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 调用钢笔图标点击事件
|
||||
/// - 调用符号删除鼠标抬起事件
|
||||
/// - 根据设置决定是否清空时间机器历史
|
||||
/// - 根据设置决定是否清空图片
|
||||
/// - 如果设置为清空图片,则清空所有子元素
|
||||
/// - 否则,保存非笔画元素并在清空后恢复
|
||||
/// </remarks>
|
||||
private void BoardSymbolIconDeleteInkAndHistories_MouseUp(object sender, RoutedEventArgs e)
|
||||
{
|
||||
PenIcon_Click(null, null);
|
||||
@@ -790,12 +876,31 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理启动希沃视频展台图标点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 调用图片黑板鼠标抬起事件
|
||||
/// - 启动希沃视频展台软件
|
||||
/// </remarks>
|
||||
private void BoardLaunchEasiCamera_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
ImageBlackboard_MouseUp(null, null);
|
||||
SoftwareLauncher.LaunchEasiCamera("希沃视频展台");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理启动Desmos计算器图标点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 立即隐藏所有子面板
|
||||
/// - 调用图片黑板鼠标抬起事件
|
||||
/// - 打开Desmos计算器网页
|
||||
/// </remarks>
|
||||
private void BoardLaunchDesmos_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
HideSubPanelsImmediately();
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
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.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
@@ -19,17 +20,75 @@ namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 剪贴板更新消息常量
|
||||
/// </summary>
|
||||
private const int WM_CLIPBOARDUPDATE = 0x031D;
|
||||
|
||||
/// <summary>
|
||||
/// 添加剪贴板格式监听器
|
||||
/// </summary>
|
||||
/// <param name="hwnd">窗口句柄</param>
|
||||
/// <returns>操作是否成功</returns>
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AddClipboardFormatListener(IntPtr hwnd);
|
||||
|
||||
/// <summary>
|
||||
/// 移除剪贴板格式监听器
|
||||
/// </summary>
|
||||
/// <param name="hwnd">窗口句柄</param>
|
||||
/// <returns>操作是否成功</returns>
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool RemoveClipboardFormatListener(IntPtr hwnd);
|
||||
|
||||
/// <summary>
|
||||
/// 剪贴板监控启用状态
|
||||
/// </summary>
|
||||
private bool isClipboardMonitoringEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// 最后一次剪贴板图像
|
||||
/// </summary>
|
||||
private BitmapSource lastClipboardImage;
|
||||
|
||||
// 初始化剪贴板监控
|
||||
/// <summary>
|
||||
/// 剪贴板窗口句柄源
|
||||
/// </summary>
|
||||
private HwndSource _clipboardHwndSource;
|
||||
|
||||
/// <summary>
|
||||
/// 最后一次粘贴通知时间
|
||||
/// </summary>
|
||||
private DateTime _lastPasteNotificationTime = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// 粘贴通知防抖时间(秒)
|
||||
/// </summary>
|
||||
private const int PasteNotificationDebounceSeconds = 4;
|
||||
|
||||
/// <summary>
|
||||
/// 启用并初始化对系统剪贴板变更的监控,确保窗口消息钩子在可用时安装并订阅剪贴板更新事件。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 在首次调用时订阅内部的 ClipboardNotification.ClipboardUpdate 事件、将监控标志设为已启用,并在窗口句柄可用时安装窗口消息钩子;若句柄尚不可用则延迟到 SourceInitialized 事件完成后安装。此方法会异步调度 EnsureClipboardHookInstalled 以在加载优先级下最终确认钩子已安装。发生异常时记录错误但不会抛出。
|
||||
/// </remarks>
|
||||
private void InitializeClipboardMonitoring()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 监听剪贴板变化
|
||||
if (isClipboardMonitoringEnabled)
|
||||
return;
|
||||
|
||||
ClipboardNotification.ClipboardUpdate += OnClipboardUpdate;
|
||||
isClipboardMonitoringEnabled = true;
|
||||
|
||||
if (new WindowInteropHelper(this).Handle != IntPtr.Zero)
|
||||
OnSourceInitializedForClipboard(this, EventArgs.Empty);
|
||||
else
|
||||
SourceInitialized += OnSourceInitializedForClipboard;
|
||||
Dispatcher.BeginInvoke(new Action(EnsureClipboardHookInstalled), DispatcherPriority.Loaded);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -37,24 +96,82 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 剪贴板内容变化事件处理
|
||||
/// <summary>
|
||||
/// — 在窗口句柄可用且尚未安装钩子时,为接收剪贴板更新消息安装窗口消息钩子。
|
||||
/// </summary>
|
||||
private void EnsureClipboardHookInstalled()
|
||||
{
|
||||
if (_clipboardHwndSource != null) return;
|
||||
var handle = new WindowInteropHelper(this).Handle;
|
||||
if (handle == IntPtr.Zero) return;
|
||||
OnSourceInitializedForClipboard(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在窗口初始化后安装用于接收系统剪贴板更改消息的窗口钩子并注册剪贴板格式监听器。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 将当前窗口的 HwndSource 与 ClipboardWndProc 消息钩子关联,并调用 AddClipboardFormatListener 注册剪贴板更新通知;若无法获取窗口句柄则不执行任何操作。
|
||||
/// </remarks>
|
||||
private void OnSourceInitializedForClipboard(object sender, EventArgs e)
|
||||
{
|
||||
SourceInitialized -= OnSourceInitializedForClipboard;
|
||||
try
|
||||
{
|
||||
var handle = new WindowInteropHelper(this).Handle;
|
||||
if (handle == IntPtr.Zero) return;
|
||||
|
||||
_clipboardHwndSource = HwndSource.FromHwnd(handle);
|
||||
_clipboardHwndSource?.AddHook(ClipboardWndProc);
|
||||
|
||||
if (!AddClipboardFormatListener(handle))
|
||||
LogHelper.WriteLogToFile($"AddClipboardFormatListener 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Warning);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"安装剪贴板监听失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理窗口消息,响应剪贴板更新事件
|
||||
/// </summary>
|
||||
/// <param name="hwnd">窗口句柄</param>
|
||||
/// <param name="msg">消息类型</param>
|
||||
/// <param name="wParam">消息参数W</param>
|
||||
/// <param name="lParam">消息参数L</param>
|
||||
/// <param name="handled">消息是否已处理</param>
|
||||
/// <returns>处理结果</returns>
|
||||
/// <remarks>
|
||||
/// - 当收到剪贴板更新消息时,通知剪贴板变更
|
||||
/// - 标记消息为已处理
|
||||
/// </remarks>
|
||||
private IntPtr ClipboardWndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WM_CLIPBOARDUPDATE)
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() => ClipboardNotification.NotifyFromMessage()), DispatcherPriority.Background);
|
||||
handled = true;
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在剪贴板内容变化时检查剪贴板是否包含图像并缓存该图像。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 如果剪贴板包含图像,则读取该图像并更新字段 <c>lastClipboardImage</c>;否则不做任何操作。方法内部会捕获异常并记录日志,不会向上抛出。
|
||||
/// </remarks>
|
||||
private void OnClipboardUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Clipboard.ContainsImage())
|
||||
{
|
||||
var clipboardImage = Clipboard.GetImage();
|
||||
if (clipboardImage != null && clipboardImage != lastClipboardImage)
|
||||
{
|
||||
lastClipboardImage = clipboardImage;
|
||||
// 在白板模式下显示粘贴提示
|
||||
if (currentMode == 1) // 白板模式
|
||||
{
|
||||
ShowPasteNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Clipboard.ContainsImage())
|
||||
return;
|
||||
|
||||
var clipboardImage = Clipboard.GetImage();
|
||||
if (clipboardImage != null)
|
||||
lastClipboardImage = clipboardImage;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -62,23 +179,51 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 显示粘贴提示
|
||||
private void ShowPasteNotification()
|
||||
/// <summary>
|
||||
/// 在进入白板时检查系统剪贴板是否包含图片;如果存在图片且与上次提示间隔超过预设节流时间,则显示粘贴提示。
|
||||
/// </summary>
|
||||
public void CheckClipboardImageAndShowPasteNotificationWhenEnteringBoard()
|
||||
{
|
||||
try
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification("检测到剪贴板中有图片,右键点击白板可粘贴");
|
||||
});
|
||||
if (!Clipboard.ContainsImage())
|
||||
return;
|
||||
|
||||
bool debounceElapsed = (DateTime.Now - _lastPasteNotificationTime).TotalSeconds >= PasteNotificationDebounceSeconds;
|
||||
if (!debounceElapsed)
|
||||
return;
|
||||
|
||||
_lastPasteNotificationTime = DateTime.Now;
|
||||
ShowPasteNotification();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"显示粘贴提示失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.WriteLogToFile($"进入白板时检测剪贴板失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理右键菜单显示
|
||||
/// <summary>
|
||||
/// 在界面上显示提示,告知用户剪贴板中存在图片并可在白板上右键粘贴。
|
||||
/// </summary>
|
||||
private void ShowPasteNotification()
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ShowNotification("检测到剪贴板中有图片,右键点击白板可粘贴");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"显示粘贴提示失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}), DispatcherPriority.Normal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在指定位置显示包含“粘贴图片”项的右键菜单(仅在剪贴板包含图片时显示)。
|
||||
/// </summary>
|
||||
/// <param name="position">右键菜单应定位的画布坐标;该位置会传递给粘贴操作以确定图片粘贴位置。</param>
|
||||
private void ShowPasteContextMenu(Point position)
|
||||
{
|
||||
try
|
||||
@@ -107,7 +252,26 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 从剪贴板粘贴图片
|
||||
/// <summary>
|
||||
/// 从剪贴板粘贴图片到画布
|
||||
/// </summary>
|
||||
/// <param name="position">粘贴位置(可选)</param>
|
||||
/// <returns>异步任务</returns>
|
||||
/// <remarks>
|
||||
/// - 检查剪贴板是否包含图片
|
||||
/// - 创建Image控件并设置属性
|
||||
/// - 生成唯一名称
|
||||
/// - 初始化变换组
|
||||
/// - 设置图片属性,避免被InkCanvas选择系统处理
|
||||
/// - 添加到画布
|
||||
/// - 等待图片加载完成后进行居中处理
|
||||
/// - 如果有指定位置,调整到指定位置
|
||||
/// - 绑定事件处理器
|
||||
/// - 提交到历史记录
|
||||
/// - 插入图片后切换到选择模式并刷新浮动栏高光显示
|
||||
/// - 显示通知
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private async Task PasteImageFromClipboard(Point? position = null)
|
||||
{
|
||||
try
|
||||
@@ -223,7 +387,17 @@ namespace Ink_Canvas
|
||||
|
||||
|
||||
|
||||
// 处理白板右键事件
|
||||
/// <summary>
|
||||
/// 处理白板右键事件,显示粘贴图片菜单
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 只在白板模式下处理
|
||||
/// - 检查是否有图片在剪贴板中
|
||||
/// - 显示粘贴上下文菜单
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void InkCanvas_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -244,7 +418,17 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 处理全局粘贴快捷键
|
||||
/// <summary>
|
||||
/// 处理全局粘贴快捷键,粘贴剪贴板中的图片
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 只在白板模式下处理
|
||||
/// - 检查剪贴板是否包含图片
|
||||
/// - 从剪贴板粘贴图片
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
internal async void HandleGlobalPaste(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -263,7 +447,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 清理剪贴板监控
|
||||
/// <summary>
|
||||
/// 清理剪贴板监控资源
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 取消订阅剪贴板更新事件
|
||||
/// - 移除剪贴板格式监听器
|
||||
/// - 移除窗口消息钩子
|
||||
/// - 重置相关变量
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void CleanupClipboardMonitoring()
|
||||
{
|
||||
try
|
||||
@@ -273,6 +466,13 @@ namespace Ink_Canvas
|
||||
ClipboardNotification.ClipboardUpdate -= OnClipboardUpdate;
|
||||
isClipboardMonitoringEnabled = false;
|
||||
}
|
||||
|
||||
var handle = new WindowInteropHelper(this).Handle;
|
||||
if (handle != IntPtr.Zero)
|
||||
RemoveClipboardFormatListener(handle);
|
||||
|
||||
_clipboardHwndSource?.RemoveHook(ClipboardWndProc);
|
||||
_clipboardHwndSource = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -281,47 +481,60 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 剪贴板通知类
|
||||
/// <summary>
|
||||
/// 剪贴板通知类,用于监控剪贴板变化
|
||||
/// </summary>
|
||||
public static class ClipboardNotification
|
||||
{
|
||||
/// <summary>
|
||||
/// 剪贴板更新事件
|
||||
/// </summary>
|
||||
public static event Action ClipboardUpdate;
|
||||
|
||||
private static Timer clipboardTimer;
|
||||
/// <summary>
|
||||
/// 最后一次剪贴板文本
|
||||
/// </summary>
|
||||
private static string lastClipboardText = "";
|
||||
|
||||
/// <summary>
|
||||
/// 最后一次是否有图片
|
||||
/// </summary>
|
||||
private static bool lastHadImage;
|
||||
|
||||
static ClipboardNotification()
|
||||
{
|
||||
clipboardTimer = new Timer();
|
||||
clipboardTimer.Interval = 500; // 每500ms检查一次
|
||||
clipboardTimer.Tick += CheckClipboard;
|
||||
clipboardTimer.Start();
|
||||
}
|
||||
|
||||
private static void CheckClipboard(object sender, EventArgs e)
|
||||
/// <summary>
|
||||
/// 检查当前系统剪贴板的文本与图像状态,并在检测到相关变化或存在图像时触发 <see cref="ClipboardUpdate"/> 事件以通知订阅者。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 会比较当前剪贴板的图像存在性与文本内容与内部缓存的上一状态;若图像存在性发生变化、文本内容发生变化,或当前存在图像,则更新缓存并调用 <see cref="ClipboardUpdate"/>。方法内部捕获异常并将错误记录到日志,而不是向调用方抛出异常。
|
||||
/// </remarks>
|
||||
public static void NotifyFromMessage()
|
||||
{
|
||||
try
|
||||
{
|
||||
bool currentHasImage = Clipboard.ContainsImage();
|
||||
string currentText = Clipboard.ContainsText() ? Clipboard.GetText() : "";
|
||||
|
||||
if (currentHasImage != lastHadImage || currentText != lastClipboardText)
|
||||
if (currentHasImage != lastHadImage || currentText != lastClipboardText || currentHasImage)
|
||||
{
|
||||
lastHadImage = currentHasImage;
|
||||
lastClipboardText = currentText;
|
||||
ClipboardUpdate?.Invoke();
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 忽略剪贴板访问错误
|
||||
LogHelper.WriteLogToFile($"剪贴板 NotifyFromMessage 异常: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止剪贴板监控
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 当前实现为空方法,预留用于未来扩展
|
||||
/// </remarks>
|
||||
public static void Stop()
|
||||
{
|
||||
clipboardTimer?.Stop();
|
||||
clipboardTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,11 +14,27 @@ namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前墨水颜色
|
||||
/// </summary>
|
||||
private int inkColor = 1;
|
||||
|
||||
private void ColorSwitchCheck()
|
||||
/// <summary>
|
||||
/// 颜色切换检查,处理颜色变更和相关UI状态
|
||||
/// </summary>
|
||||
/// <param name="hidePanels">是否隐藏面板</param>
|
||||
/// <remarks>
|
||||
/// - 隐藏相关面板
|
||||
/// - 处理透明背景情况
|
||||
/// - 处理选中笔画的颜色更新
|
||||
/// - 提交笔画属性历史记录
|
||||
/// - 设置工具模式为墨水模式
|
||||
/// - 取消单指拖动模式
|
||||
/// - 检查颜色主题
|
||||
/// </remarks>
|
||||
private void ColorSwitchCheck(bool hidePanels = true)
|
||||
{
|
||||
if (penType != 1)
|
||||
if (hidePanels)
|
||||
{
|
||||
HideSubPanels("color");
|
||||
}
|
||||
@@ -76,11 +92,40 @@ namespace Ink_Canvas
|
||||
isLongPressSelected = false;
|
||||
}
|
||||
|
||||
private bool isUselightThemeColor, isDesktopUselightThemeColor;
|
||||
private int penType; // 0是签字笔,1是荧光笔
|
||||
private int lastDesktopInkColor = 1, lastBoardInkColor = 5;
|
||||
/// <summary>
|
||||
/// 是否使用亮色主题颜色
|
||||
/// </summary>
|
||||
private bool isUselightThemeColor;
|
||||
|
||||
/// <summary>
|
||||
/// 桌面模式是否使用亮色主题颜色
|
||||
/// </summary>
|
||||
private bool isDesktopUselightThemeColor;
|
||||
|
||||
/// <summary>
|
||||
/// 笔类型(0是签字笔,1是荧光笔)
|
||||
/// </summary>
|
||||
private int penType;
|
||||
|
||||
/// <summary>
|
||||
/// 桌面模式最后使用的墨水颜色
|
||||
/// </summary>
|
||||
private int lastDesktopInkColor = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 白板模式最后使用的墨水颜色
|
||||
/// </summary>
|
||||
private int lastBoardInkColor = 5;
|
||||
|
||||
/// <summary>
|
||||
/// 荧光笔颜色
|
||||
/// </summary>
|
||||
private int highlighterColor = 102;
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前模式、画笔类型与主题设置,应用并同步画布颜色、笔触颜色与界面配色指示器。
|
||||
/// </summary>
|
||||
/// <param name="changeColorTheme">为 true 时(且非桌面模式)根据白板/黑板设置刷新背景色、水印色和亮/暗主题标志;为 false 则仅同步颜色相关状态。</param>
|
||||
private void CheckColorTheme(bool changeColorTheme = false)
|
||||
{
|
||||
if (changeColorTheme)
|
||||
@@ -131,6 +176,12 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
double alpha = inkCanvas.DefaultDrawingAttributes.Color.A;
|
||||
if (penType == 0 && Settings?.Canvas != null)
|
||||
{
|
||||
double settingAlpha = Settings.Canvas.InkAlpha;
|
||||
if (settingAlpha >= 0 && settingAlpha <= 255)
|
||||
alpha = settingAlpha;
|
||||
}
|
||||
|
||||
if (penType == 0)
|
||||
{
|
||||
@@ -432,6 +483,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并更新最后使用的颜色
|
||||
/// </summary>
|
||||
/// <param name="inkColor">墨水颜色</param>
|
||||
/// <param name="isHighlighter">是否为荧光笔</param>
|
||||
/// <remarks>
|
||||
/// - 如果是荧光笔,更新荧光笔颜色
|
||||
/// - 否则,根据当前模式更新相应的最后使用颜色
|
||||
/// </remarks>
|
||||
private void CheckLastColor(int inkColor, bool isHighlighter = false)
|
||||
{
|
||||
if (isHighlighter)
|
||||
@@ -445,6 +505,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并更新笔类型UI状态
|
||||
/// </summary>
|
||||
/// <returns>异步任务</returns>
|
||||
/// <remarks>
|
||||
/// - 根据笔类型显示或隐藏相应的面板
|
||||
/// - 更新标签按钮的样式和状态
|
||||
/// - 执行面板动画
|
||||
/// - 处理签字笔和荧光笔的不同UI状态
|
||||
/// </remarks>
|
||||
private async void CheckPenTypeUIState()
|
||||
{
|
||||
if (penType == 0)
|
||||
@@ -584,6 +654,17 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到默认签字笔
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 设置笔类型为0(签字笔)
|
||||
/// - 更新笔类型UI状态
|
||||
/// - 检查颜色主题
|
||||
/// - 设置画笔属性(宽度、高度、笔尖形状、是否为荧光笔)
|
||||
/// </remarks>
|
||||
private void SwitchToDefaultPen(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
penType = 0;
|
||||
@@ -595,6 +676,18 @@ namespace Ink_Canvas
|
||||
drawingAttributes.IsHighlighter = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到荧光笔
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 设置笔类型为1(荧光笔)
|
||||
/// - 更新笔类型UI状态
|
||||
/// - 检查颜色主题
|
||||
/// - 设置画笔属性(宽度、高度、笔尖形状、是否为荧光笔)
|
||||
/// - 确保荧光笔模式切换后正确更新颜色和快捷调色板指示器
|
||||
/// </remarks>
|
||||
private void SwitchToHighlighterPen(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
penType = 1;
|
||||
@@ -606,63 +699,113 @@ namespace Ink_Canvas
|
||||
drawingAttributes.IsHighlighter = true;
|
||||
|
||||
// 确保荧光笔模式切换后正确更新颜色和快捷调色板指示器
|
||||
ColorSwitchCheck();
|
||||
ColorSwitchCheck(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理黑色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnColorBlack_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(0);
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理红色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnColorRed_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(1);
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理绿色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnColorGreen_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(2);
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理蓝色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnColorBlue_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(3);
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理黄色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnColorYellow_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(4);
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理白色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnColorWhite_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(5);
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理粉色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnColorPink_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(6);
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理橙色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnColorOrange_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(8);
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理青色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnColorTeal_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(7);
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔黑色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorBlack_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(100, true);
|
||||
@@ -671,6 +814,11 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔白色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorWhite_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(101, true);
|
||||
@@ -679,6 +827,11 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔红色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorRed_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(102, true);
|
||||
@@ -687,6 +840,11 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔黄色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorYellow_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(103, true);
|
||||
@@ -695,6 +853,11 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔绿色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorGreen_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(104, true);
|
||||
@@ -703,6 +866,11 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔锌色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorZinc_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(105, true);
|
||||
@@ -711,6 +879,11 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔蓝色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorBlue_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(106, true);
|
||||
@@ -719,6 +892,11 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔紫色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorPurple_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(107, true);
|
||||
@@ -727,6 +905,11 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔青色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorTeal_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(108, true);
|
||||
@@ -735,6 +918,11 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理荧光笔橙色按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
private void BtnHighlighterColorOrange_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
CheckLastColor(109, true);
|
||||
@@ -743,6 +931,15 @@ namespace Ink_Canvas
|
||||
ColorSwitchCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将字符串转换为颜色对象
|
||||
/// </summary>
|
||||
/// <param name="colorStr">颜色字符串(格式:#FFFFFFFF)</param>
|
||||
/// <returns>颜色对象</returns>
|
||||
/// <remarks>
|
||||
/// - 解析颜色字符串为ARGB值
|
||||
/// - 转换为Color对象返回
|
||||
/// </remarks>
|
||||
private Color StringToColor(string colorStr)
|
||||
{
|
||||
var argb = new byte[4];
|
||||
@@ -757,6 +954,15 @@ namespace Ink_Canvas
|
||||
return Color.FromArgb(argb[0], argb[1], argb[2], argb[3]); //#FFFFFFFF
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将字符转换为字节
|
||||
/// </summary>
|
||||
/// <param name="c">字符</param>
|
||||
/// <returns>字节值</returns>
|
||||
/// <remarks>
|
||||
/// - 将十六进制字符转换为对应的字节值
|
||||
/// - 支持0-9和A-F字符
|
||||
/// </remarks>
|
||||
private static byte toByte(char c)
|
||||
{
|
||||
var b = (byte)"0123456789ABCDEF".IndexOf(c);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
@@ -19,12 +19,40 @@ namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
// 当前选中的可操作元素
|
||||
/// <summary>
|
||||
/// 当前选中的可操作元素
|
||||
/// </summary>
|
||||
private FrameworkElement currentSelectedElement;
|
||||
|
||||
/// <summary>
|
||||
/// 是否正在拖动
|
||||
/// </summary>
|
||||
private bool isDragging;
|
||||
|
||||
/// <summary>
|
||||
/// 拖动起始点
|
||||
/// </summary>
|
||||
private Point dragStartPoint;
|
||||
|
||||
#region Image
|
||||
/// <summary>
|
||||
/// 处理图片插入按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 打开文件选择对话框,选择图片文件
|
||||
/// - 创建并压缩图片
|
||||
/// - 设置图片属性,避免被InkCanvas选择系统处理
|
||||
/// - 初始化InkCanvas选择设置
|
||||
/// - 添加图片到画布
|
||||
/// - 等待图片加载完成后进行后续处理
|
||||
/// - 初始化TransformGroup
|
||||
/// - 居中缩放图片
|
||||
/// - 绑定事件处理器
|
||||
/// - 提交到时间机器历史记录
|
||||
/// - 插入图片后切换到选择模式并刷新浮动栏高光显示
|
||||
/// </remarks>
|
||||
private async void BtnImageInsert_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
OpenFileDialog openFileDialog = new OpenFileDialog();
|
||||
@@ -79,7 +107,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化元素的TransformGroup
|
||||
/// <summary>
|
||||
/// 初始化元素的TransformGroup
|
||||
/// </summary>
|
||||
/// <param name="element">要初始化的元素</param>
|
||||
/// <remarks>
|
||||
/// - 创建TransformGroup
|
||||
/// - 添加ScaleTransform、TranslateTransform和RotateTransform
|
||||
/// - 设置元素的RenderTransform
|
||||
/// </remarks>
|
||||
private void InitializeElementTransform(FrameworkElement element)
|
||||
{
|
||||
var transformGroup = new TransformGroup();
|
||||
@@ -89,7 +125,17 @@ namespace Ink_Canvas
|
||||
element.RenderTransform = transformGroup;
|
||||
}
|
||||
|
||||
// 绑定元素事件处理器
|
||||
/// <summary>
|
||||
/// 绑定元素事件处理器
|
||||
/// </summary>
|
||||
/// <param name="element">要绑定事件的元素</param>
|
||||
/// <remarks>
|
||||
/// - 绑定鼠标事件(MouseLeftButtonDown、MouseLeftButtonUp、MouseMove、MouseWheel)
|
||||
/// - 启用触摸操作
|
||||
/// - 绑定触摸事件(ManipulationDelta、ManipulationCompleted)
|
||||
/// - 设置光标为手形
|
||||
/// - 禁用InkCanvas对图片的选择处理
|
||||
/// </remarks>
|
||||
private void BindElementEvents(FrameworkElement element)
|
||||
{
|
||||
// 鼠标事件
|
||||
@@ -111,11 +157,29 @@ namespace Ink_Canvas
|
||||
element.Focusable = false;
|
||||
}
|
||||
|
||||
// 鼠标左键按下事件
|
||||
/// <summary>
|
||||
/// 处理元素鼠标左键按下事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查编辑模式是否为选择模式
|
||||
/// - 取消之前选中的元素
|
||||
/// - 选中当前元素
|
||||
/// - 开始拖动
|
||||
/// - 捕获鼠标
|
||||
/// - 设置光标为全尺寸光标
|
||||
/// </remarks>
|
||||
private void Element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
if (inkCanvas.EditingMode != InkCanvasEditingMode.Select)
|
||||
{
|
||||
e.Handled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 取消之前选中的元素
|
||||
if (currentSelectedElement != null && currentSelectedElement != element)
|
||||
{
|
||||
@@ -139,7 +203,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标左键释放事件
|
||||
/// <summary>
|
||||
/// 处理元素鼠标左键释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 停止拖动
|
||||
/// - 释放鼠标捕获
|
||||
/// - 恢复光标为手形
|
||||
/// </remarks>
|
||||
private void Element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
@@ -152,7 +225,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸释放事件
|
||||
/// <summary>
|
||||
/// 处理元素触摸释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 停止拖动
|
||||
/// - 释放触摸捕获
|
||||
/// - 恢复光标为手形
|
||||
/// </remarks>
|
||||
private void Element_TouchUp(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
@@ -165,7 +247,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标移动事件
|
||||
/// <summary>
|
||||
/// 处理元素鼠标移动事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查是否正在拖动且鼠标已捕获
|
||||
/// - 获取当前鼠标位置
|
||||
/// - 应用鼠标拖动变换
|
||||
/// - 如果是图片元素,更新工具栏位置
|
||||
/// - 如果是图片元素,更新选择点位置
|
||||
/// - 更新拖动起始点
|
||||
/// </remarks>
|
||||
private void Element_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element && isDragging && element.IsMouseCaptured)
|
||||
@@ -192,7 +286,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标滚轮事件 - 缩放
|
||||
/// <summary>
|
||||
/// 处理元素鼠标滚轮事件 - 缩放
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 应用滚轮缩放变换
|
||||
/// - 如果是图片元素,更新工具栏位置
|
||||
/// - 如果是图片元素,更新选择点位置
|
||||
/// </remarks>
|
||||
private void Element_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
@@ -218,11 +321,29 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸按下事件
|
||||
/// <summary>
|
||||
/// 处理元素触摸按下事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查编辑模式是否为选择模式
|
||||
/// - 取消之前选中的元素
|
||||
/// - 选中当前元素
|
||||
/// - 开始拖动
|
||||
/// - 捕获触摸
|
||||
/// - 设置光标为全尺寸光标
|
||||
/// </remarks>
|
||||
private void Element_TouchDown(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
if (inkCanvas.EditingMode != InkCanvasEditingMode.Select)
|
||||
{
|
||||
e.Handled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 取消之前选中的元素
|
||||
if (currentSelectedElement != null && currentSelectedElement != element)
|
||||
{
|
||||
@@ -246,7 +367,18 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸操作事件
|
||||
/// <summary>
|
||||
/// 处理元素触摸操作事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查是否是双指手势
|
||||
/// - 双指手势时,让画布级别的手势处理
|
||||
/// - 单指手势时,应用触摸拖动变换
|
||||
/// - 如果是图片元素,更新工具栏位置
|
||||
/// - 如果是图片元素,更新选择点位置
|
||||
/// </remarks>
|
||||
private void Element_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
@@ -279,13 +411,30 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸操作完成事件
|
||||
/// <summary>
|
||||
/// 处理元素触摸操作完成事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 可以在这里添加操作完成后的处理逻辑
|
||||
/// </remarks>
|
||||
private void Element_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
|
||||
{
|
||||
// 可以在这里添加操作完成后的处理逻辑
|
||||
}
|
||||
|
||||
// 应用平移变换
|
||||
/// <summary>
|
||||
/// 应用平移变换到元素
|
||||
/// </summary>
|
||||
/// <param name="element">要变换的元素</param>
|
||||
/// <param name="deltaX">X轴偏移量</param>
|
||||
/// <param name="deltaY">Y轴偏移量</param>
|
||||
/// <remarks>
|
||||
/// - 获取元素的TransformGroup
|
||||
/// - 查找TranslateTransform
|
||||
/// - 应用平移变换
|
||||
/// </remarks>
|
||||
private void ApplyTranslateTransform(FrameworkElement element, double deltaX, double deltaY)
|
||||
{
|
||||
if (element.RenderTransform is TransformGroup transformGroup)
|
||||
@@ -299,7 +448,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 应用缩放变换
|
||||
/// <summary>
|
||||
/// 应用缩放变换到元素
|
||||
/// </summary>
|
||||
/// <param name="element">要变换的元素</param>
|
||||
/// <param name="scaleFactor">缩放因子</param>
|
||||
/// <param name="center">缩放中心</param>
|
||||
/// <remarks>
|
||||
/// - 获取元素的TransformGroup
|
||||
/// - 查找ScaleTransform
|
||||
/// - 设置缩放中心
|
||||
/// - 应用缩放
|
||||
/// - 限制缩放范围(0.1到5.0)
|
||||
/// </remarks>
|
||||
private void ApplyScaleTransform(FrameworkElement element, double scaleFactor, Point center)
|
||||
{
|
||||
if (element.RenderTransform is TransformGroup transformGroup)
|
||||
@@ -322,7 +483,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 应用旋转变换
|
||||
/// <summary>
|
||||
/// 应用旋转变换到元素
|
||||
/// </summary>
|
||||
/// <param name="element">要变换的元素</param>
|
||||
/// <param name="angle">旋转角度</param>
|
||||
/// <remarks>
|
||||
/// - 获取元素的TransformGroup
|
||||
/// - 查找RotateTransform
|
||||
/// - 应用旋转变换
|
||||
/// </remarks>
|
||||
private void ApplyRotateTransform(FrameworkElement element, double angle)
|
||||
{
|
||||
if (element.RenderTransform is TransformGroup transformGroup)
|
||||
@@ -335,7 +505,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 选中元素
|
||||
/// <summary>
|
||||
/// 选中元素
|
||||
/// </summary>
|
||||
/// <param name="element">要选中的元素</param>
|
||||
/// <remarks>
|
||||
/// - 设置当前选中元素
|
||||
/// - 根据元素类型显示不同的选择工具栏
|
||||
/// - 如果是图片元素,显示图片选择工具栏和缩放选择点
|
||||
/// - 如果不是图片元素,隐藏图片选择工具栏和缩放选择点
|
||||
/// - 确保选择框不显示,避免蓝色边框
|
||||
/// - 禁用InkCanvas的选择功能,去除控制点
|
||||
/// - 保持选择模式,这样用户可以直接点击墨迹来选择
|
||||
/// </remarks>
|
||||
private void SelectElement(FrameworkElement element)
|
||||
{
|
||||
currentSelectedElement = element;
|
||||
@@ -388,7 +570,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 取消选中元素
|
||||
/// <summary>
|
||||
/// 取消选中元素
|
||||
/// </summary>
|
||||
/// <param name="element">要取消选中的元素</param>
|
||||
/// <remarks>
|
||||
/// - 隐藏图片选择工具栏
|
||||
/// - 隐藏图片缩放选择点
|
||||
/// - 确保选择框隐藏
|
||||
/// - 确保InkCanvas处于选择模式,这样用户可以直接点击墨迹来选择
|
||||
/// </remarks>
|
||||
private void UnselectElement(FrameworkElement element)
|
||||
{
|
||||
// 去除选中效果
|
||||
@@ -418,7 +609,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 应用矩阵变换到元素
|
||||
/// <summary>
|
||||
/// 应用矩阵变换到元素
|
||||
/// </summary>
|
||||
/// <param name="element">要变换的元素</param>
|
||||
/// <param name="matrix">变换矩阵</param>
|
||||
/// <remarks>
|
||||
/// - 获取元素的RenderTransform,如果不存在则创建新的TransformGroup
|
||||
/// - 创建MatrixTransform
|
||||
/// - 将MatrixTransform添加到TransformGroup
|
||||
/// </remarks>
|
||||
private void ApplyElementMatrixTransform(FrameworkElement element, Matrix matrix)
|
||||
{
|
||||
if (element.RenderTransform is TransformGroup transformGroup)
|
||||
@@ -431,7 +631,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 滚轮缩放的核心机制
|
||||
/// <summary>
|
||||
/// 处理滚轮缩放的核心机制
|
||||
/// </summary>
|
||||
/// <param name="element">要缩放的元素</param>
|
||||
/// <param name="e">鼠标滚轮事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 根据滚轮方向确定缩放比例(向上1.1倍,向下0.9倍)
|
||||
/// - 计算选中元素的中心点作为缩放中心
|
||||
/// - 创建 Matrix 对象并应用 ScaleAt 变换
|
||||
/// - 对选中的图片元素应用矩阵变换
|
||||
/// - 对选中的笔画应用 Transform 方法
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void ApplyWheelScaleTransform(FrameworkElement element, MouseWheelEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -464,7 +676,20 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 矩阵变换的完整实现
|
||||
/// <summary>
|
||||
/// 矩阵变换的完整实现
|
||||
/// </summary>
|
||||
/// <param name="element">要变换的元素</param>
|
||||
/// <param name="matrix">变换矩阵</param>
|
||||
/// <param name="saveHistory">是否保存历史记录</param>
|
||||
/// <remarks>
|
||||
/// - 获取元素的 RenderTransform,如果不存在则创建新的 TransformGroup
|
||||
/// - 保存初始变换状态用于历史记录
|
||||
/// - 创建新的 TransformGroup 并添加 MatrixTransform
|
||||
/// - 将新的变换组添加到现有的变换组中
|
||||
/// - 如果启用了历史记录,提交变换历史
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void ApplyMatrixTransformToElement(FrameworkElement element, Matrix matrix, bool saveHistory = true)
|
||||
{
|
||||
try
|
||||
@@ -501,7 +726,20 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标拖动的完整实现机制
|
||||
/// <summary>
|
||||
/// 鼠标拖动的完整实现机制
|
||||
/// </summary>
|
||||
/// <param name="element">要拖动的元素</param>
|
||||
/// <param name="currentPoint">当前鼠标位置</param>
|
||||
/// <param name="startPoint">起始鼠标位置</param>
|
||||
/// <remarks>
|
||||
/// - 计算鼠标移动的位移向量
|
||||
/// - 创建 Matrix 对象并应用 Translate 变换
|
||||
/// - 对选中的图片元素应用矩阵变换
|
||||
/// - 对选中的笔画应用变换
|
||||
/// - 更新选择框的位置(如果有选择框)
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void ApplyMouseDragTransform(FrameworkElement element, Point currentPoint, Point startPoint)
|
||||
{
|
||||
try
|
||||
@@ -534,7 +772,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 更新选择框位置
|
||||
/// <summary>
|
||||
/// 更新选择框位置
|
||||
/// </summary>
|
||||
/// <param name="delta">位移向量</param>
|
||||
/// <remarks>
|
||||
/// - 更新选择框位置的逻辑
|
||||
/// - 更新 BorderStrokeSelectionControl 的位置
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void UpdateSelectionBorderPosition(Vector delta)
|
||||
{
|
||||
try
|
||||
@@ -558,7 +804,17 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 提交变换历史
|
||||
/// <summary>
|
||||
/// 提交变换历史
|
||||
/// </summary>
|
||||
/// <param name="element">变换的元素</param>
|
||||
/// <param name="initialTransform">初始变换</param>
|
||||
/// <param name="finalTransform">最终变换</param>
|
||||
/// <remarks>
|
||||
/// - 提交变换历史到时间机器的逻辑
|
||||
/// - 记录变换前后的状态
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void CommitTransformHistory(FrameworkElement element, TransformGroup initialTransform, TransformGroup finalTransform)
|
||||
{
|
||||
try
|
||||
@@ -573,7 +829,21 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸拖动的完整实现
|
||||
/// <summary>
|
||||
/// 触摸拖动的完整实现
|
||||
/// </summary>
|
||||
/// <param name="element">要操作的元素</param>
|
||||
/// <param name="e">操作事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 支持单指拖动和多指手势
|
||||
/// - 可以同时进行平移、旋转和缩放
|
||||
/// - 通过 ManipulationDelta 获取手势变化信息
|
||||
/// - 应用平移
|
||||
/// - 支持两指缩放和旋转操作
|
||||
/// - 应用变换到元素
|
||||
/// - 应用变换到选中的笔画
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void ApplyTouchManipulationTransform(FrameworkElement element, ManipulationDeltaEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -631,6 +901,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建并压缩图片
|
||||
/// </summary>
|
||||
/// <param name="filePath">图片文件路径</param>
|
||||
/// <returns>创建的Image对象</returns>
|
||||
/// <remarks>
|
||||
/// - 创建文件依赖目录
|
||||
/// - 复制文件到依赖目录
|
||||
/// - 创建BitmapImage
|
||||
/// - 如果图片尺寸大于1920x1080且设置了压缩图片,则压缩图片
|
||||
/// - 否则使用原始尺寸
|
||||
/// - 返回创建的Image对象
|
||||
/// </remarks>
|
||||
private async Task<Image> CreateAndCompressImageAsync(string filePath)
|
||||
{
|
||||
string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency");
|
||||
@@ -685,6 +968,21 @@ namespace Ink_Canvas
|
||||
#endregion
|
||||
|
||||
#region Media
|
||||
/// <summary>
|
||||
/// 处理媒体插入按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 打开文件选择对话框,选择媒体文件
|
||||
/// - 读取媒体文件字节
|
||||
/// - 创建MediaElement
|
||||
/// - 居中缩放MediaElement
|
||||
/// - 设置位置并添加到画布
|
||||
/// - 设置LoadedBehavior和UnloadedBehavior为Manual
|
||||
/// - 媒体加载完成后播放并立即暂停
|
||||
/// - 提交到时间机器历史记录
|
||||
/// </remarks>
|
||||
private async void BtnMediaInsert_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
OpenFileDialog openFileDialog = new OpenFileDialog();
|
||||
@@ -720,6 +1018,20 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建MediaElement
|
||||
/// </summary>
|
||||
/// <param name="filePath">媒体文件路径</param>
|
||||
/// <returns>创建的MediaElement对象</returns>
|
||||
/// <remarks>
|
||||
/// - 创建文件依赖目录
|
||||
/// - 创建MediaElement
|
||||
/// - 设置Source、名称、LoadedBehavior和UnloadedBehavior
|
||||
/// - 设置宽度和高度
|
||||
/// - 复制文件到依赖目录
|
||||
/// - 更新Source为新文件路径
|
||||
/// - 返回创建的MediaElement对象
|
||||
/// </remarks>
|
||||
private async Task<MediaElement> CreateMediaElementAsync(string filePath)
|
||||
{
|
||||
string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency");
|
||||
@@ -976,6 +1288,29 @@ namespace Ink_Canvas
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 居中并缩放元素
|
||||
/// </summary>
|
||||
/// <param name="element">要居中缩放的元素</param>
|
||||
/// <remarks>
|
||||
/// - 确保元素已加载且有有效尺寸
|
||||
/// - 如果元素尺寸无效,等待加载完成后再处理
|
||||
/// - 获取画布的实际尺寸
|
||||
/// - 如果画布尺寸为0,使用窗口尺寸作为备选
|
||||
/// - 如果仍然为0,使用屏幕尺寸
|
||||
/// - 计算最大允许尺寸(画布的70%)
|
||||
/// - 获取元素的当前尺寸
|
||||
/// - 计算缩放比例
|
||||
/// - 如果元素本身比最大尺寸小,不进行缩放
|
||||
/// - 计算新的尺寸
|
||||
/// - 设置元素尺寸
|
||||
/// - 计算居中位置
|
||||
/// - 确保位置不为负数
|
||||
/// - 设置位置
|
||||
/// - 保持TransformGroup,不清除RenderTransform
|
||||
/// - 只有在没有TransformGroup时才创建
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void CenterAndScaleElement(FrameworkElement element)
|
||||
{
|
||||
try
|
||||
@@ -1067,7 +1402,13 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化InkCanvas选择设置
|
||||
/// <summary>
|
||||
/// 初始化InkCanvas选择设置
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 清除当前选择,避免显示控制点
|
||||
/// - 设置编辑模式为非选择模式
|
||||
/// </remarks>
|
||||
private void InitializeInkCanvasSelectionSettings()
|
||||
{
|
||||
if (inkCanvas != null)
|
||||
@@ -1079,7 +1420,17 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图片选择工具栏位置
|
||||
/// <summary>
|
||||
/// 更新图片选择工具栏位置
|
||||
/// </summary>
|
||||
/// <param name="element">图片元素</param>
|
||||
/// <remarks>
|
||||
/// - 获取元素的实际边界(考虑变换)
|
||||
/// - 计算工具栏位置(显示在图片下方)
|
||||
/// - 确保工具栏不超出画布边界
|
||||
/// - 设置工具栏位置
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void UpdateImageSelectionToolbarPosition(FrameworkElement element)
|
||||
{
|
||||
try
|
||||
@@ -1109,7 +1460,22 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 获取元素的实际边界(考虑变换)
|
||||
/// <summary>
|
||||
/// 获取元素的实际边界(考虑变换)
|
||||
/// </summary>
|
||||
/// <param name="element">要获取边界的元素</param>
|
||||
/// <returns>元素的实际边界</returns>
|
||||
/// <remarks>
|
||||
/// - 获取元素的Left和Top位置
|
||||
/// - 如果值为NaN,设为0
|
||||
/// - 获取元素的宽度和高度
|
||||
/// - 检查是否有RenderTransform
|
||||
/// - 如果有变换,使用变换后的边界
|
||||
/// - 变换失败时回退到简单计算
|
||||
/// - 没有变换时直接使用位置和大小
|
||||
/// - 包含异常处理
|
||||
/// - 回退到基本计算
|
||||
/// </remarks>
|
||||
private Rect GetElementActualBounds(FrameworkElement element)
|
||||
{
|
||||
try
|
||||
@@ -1158,7 +1524,20 @@ namespace Ink_Canvas
|
||||
|
||||
#region Image Selection Toolbar Event Handlers
|
||||
|
||||
// 图片克隆功能
|
||||
/// <summary>
|
||||
/// 处理图片克隆功能
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查当前选中元素是否为图片
|
||||
/// - 创建克隆图片
|
||||
/// - 添加到画布
|
||||
/// - 初始化变换
|
||||
/// - 绑定事件
|
||||
/// - 记录历史
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void BorderImageClone_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -1189,7 +1568,22 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 图片克隆到新页面
|
||||
/// <summary>
|
||||
/// 处理图片克隆到新页面功能
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查当前选中元素是否为图片
|
||||
/// - 创建新页面
|
||||
/// - 创建克隆图片
|
||||
/// - 设置图片属性,避免被InkCanvas选择系统处理
|
||||
/// - 初始化变换
|
||||
/// - 绑定事件
|
||||
/// - 添加到新页面的画布
|
||||
/// - 记录历史
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void BorderImageCloneToNewBoard_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -1230,7 +1624,17 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 图片左旋转
|
||||
/// <summary>
|
||||
/// 处理图片左旋转功能
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查当前是否有选中元素
|
||||
/// - 应用旋转变换(向左旋转45度)
|
||||
/// - 如果是图片元素,更新工具栏位置
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void BorderImageRotateLeft_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -1254,7 +1658,17 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 图片右旋转
|
||||
/// <summary>
|
||||
/// 处理图片右旋转功能
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查当前是否有选中元素
|
||||
/// - 应用旋转变换(向右旋转45度)
|
||||
/// - 如果是图片元素,更新工具栏位置
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void BorderImageRotateRight_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -1278,7 +1692,18 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 图片缩放减小
|
||||
/// <summary>
|
||||
/// 处理图片缩放减小功能
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查当前是否有选中元素
|
||||
/// - 计算元素中心点
|
||||
/// - 应用缩放变换(缩小到0.9倍)
|
||||
/// - 如果是图片元素,更新工具栏位置
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void GridImageScaleDecrease_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -1303,7 +1728,18 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 图片缩放增大
|
||||
/// <summary>
|
||||
/// 处理图片缩放增大功能
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查当前是否有选中元素
|
||||
/// - 计算元素中心点
|
||||
/// - 应用缩放变换(放大到1.1倍)
|
||||
/// - 如果是图片元素,更新工具栏位置
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void GridImageScaleIncrease_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -1328,7 +1764,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 图片删除
|
||||
/// <summary>
|
||||
/// 处理图片删除功能
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查当前是否有选中元素
|
||||
/// - 保存删除前的编辑模式
|
||||
/// - 记录删除历史
|
||||
/// - 从画布中移除
|
||||
/// - 清除选中状态
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void BorderImageDelete_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
@@ -174,6 +174,9 @@ namespace Ink_Canvas
|
||||
AddedStroke = null;
|
||||
ReplacedStroke = null;
|
||||
}
|
||||
|
||||
// 橡皮擦自动切换回批注
|
||||
HandleEraserOperationEnded();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -296,6 +296,8 @@ namespace Ink_Canvas
|
||||
/// <summary>
|
||||
/// 设置拦截规则
|
||||
/// </summary>
|
||||
/// <param name="type">拦截类型</param>
|
||||
/// <param name="enabled">是否启用拦截</param>
|
||||
private void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,90 +1,117 @@
|
||||
using System.Windows;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 鼠标滚轮事件处理,用于PPT翻页
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">鼠标滚轮事件参数</param>
|
||||
private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
// 只有在PPT放映模式下才响应鼠标滚轮翻页
|
||||
if (StackPanelPPTControls.Visibility != Visibility.Visible ||
|
||||
currentMode != 0 ||
|
||||
BtnPPTSlideShowEnd.Visibility != Visibility.Visible ||
|
||||
PPTManager?.IsInSlideShow != true) return;
|
||||
|
||||
// 直接发送翻页请求到PPT放映软件,不通过软件处理
|
||||
if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return;
|
||||
if (e.Delta >= 120)
|
||||
{
|
||||
// 上一页 - 发送PageUp键到PPT放映窗口
|
||||
SendKeyToPPTSlideShow(true);
|
||||
BtnPPTSlidesUp_Click(null, null);
|
||||
}
|
||||
else if (e.Delta <= -120)
|
||||
{
|
||||
// 下一页 - 发送PageDown键到PPT放映窗口
|
||||
SendKeyToPPTSlideShow(false);
|
||||
BtnPPTSlidesDown_Click(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 键盘按键预览事件处理,用于PPT翻页
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">键盘事件参数</param>
|
||||
private void Main_Grid_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
// 只有在PPT放映模式下才响应键盘翻页快捷键
|
||||
if (StackPanelPPTControls.Visibility != Visibility.Visible ||
|
||||
currentMode != 0 ||
|
||||
BtnPPTSlideShowEnd.Visibility != Visibility.Visible ||
|
||||
PPTManager?.IsInSlideShow != true) return;
|
||||
if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return;
|
||||
|
||||
// 直接发送翻页请求到PPT放映软件,不通过软件处理
|
||||
if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N ||
|
||||
e.Key == Key.Space)
|
||||
if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N || e.Key == Key.Space)
|
||||
{
|
||||
e.Handled = true; // 阻止事件继续传播
|
||||
SendKeyToPPTSlideShow(false); // 下一页
|
||||
BtnPPTSlidesDown_Click(null, null);
|
||||
}
|
||||
else if (e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Left || e.Key == Key.P)
|
||||
if (e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Left || e.Key == Key.P)
|
||||
{
|
||||
e.Handled = true; // 阻止事件继续传播
|
||||
SendKeyToPPTSlideShow(true); // 上一页
|
||||
BtnPPTSlidesUp_Click(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// 保留PPT翻页快捷键处理
|
||||
// 以下方法保留供全局快捷键调用
|
||||
|
||||
/// <summary>
|
||||
/// 撤销操作热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
private void HotKey_Undo(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
SymbolIconUndo_MouseUp(lastBorderMouseDownObject, null);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重做操作热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
private void HotKey_Redo(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
SymbolIconRedo_MouseUp(lastBorderMouseDownObject, null);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空画布热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
private void HotKey_Clear(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
SymbolIconDelete_MouseUp(lastBorderMouseDownObject, null);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 退出PPT放映热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
internal void KeyExit(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible) BtnPPTSlideShowEnd_Click(BtnPPTSlideShowEnd, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到绘图工具热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
private void KeyChangeToDrawTool(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
PenIcon_Click(lastBorderMouseDownObject, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 退出绘图工具热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 在白板模式下,alt+q 退出白板模式
|
||||
/// 在非白板模式下,alt+q 切换到鼠标模式
|
||||
/// </remarks>
|
||||
internal void KeyChangeToQuitDrawTool(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
if (currentMode != 0)
|
||||
@@ -99,12 +126,24 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到选择工具热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
/// <remarks>仅当画布控件面板可见时生效</remarks>
|
||||
private void KeyChangeToSelect(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
if (StackPanelCanvasControls.Visibility == Visibility.Visible)
|
||||
SymbolIconSelect_MouseUp(lastBorderMouseDownObject, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到橡皮擦工具热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
/// <remarks>仅当画布控件面板可见时生效,根据当前橡皮擦状态选择相应的橡皮擦模式</remarks>
|
||||
private void KeyChangeToEraser(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
if (StackPanelCanvasControls.Visibility == Visibility.Visible)
|
||||
@@ -116,21 +155,42 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到白板模式热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
private void KeyChangeToBoard(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
ImageBlackboard_MouseUp(lastBorderMouseDownObject, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 屏幕截图热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
private void KeyCapture(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
SaveScreenShotToDesktop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制直线热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
/// <remarks>仅当画布控件面板可见时生效</remarks>
|
||||
private void KeyDrawLine(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
if (StackPanelCanvasControls.Visibility == Visibility.Visible) BtnDrawLine_Click(lastMouseDownSender, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏工具栏热键处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">执行路由事件参数</param>
|
||||
private void KeyHide(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
SymbolIconEmoji_MouseUp(null, null);
|
||||
|
||||
@@ -1,74 +1,179 @@
|
||||
namespace Ink_Canvas
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
/// <summary>
|
||||
/// 图标几何路径定义类,包含各种工具图标的XAML几何路径
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该类定义了应用程序中使用的所有工具图标,包括:
|
||||
/// - 光标图标
|
||||
/// - 画笔图标
|
||||
/// - 橡皮擦图标
|
||||
/// - 选择工具图标
|
||||
/// - 手势图标
|
||||
/// - 老版本浮动栏图标
|
||||
/// 每个图标都有实线(Lined)和虚线(Solid)两种变体
|
||||
/// </remarks>
|
||||
public static class XamlGraphicsIconGeometries
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚线光标图标
|
||||
/// </summary>
|
||||
/// <remarks>用于表示鼠标模式的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 实线光标图标
|
||||
/// </summary>
|
||||
/// <remarks>用于表示鼠标模式的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 虚线画笔图标
|
||||
/// </summary>
|
||||
/// <remarks>用于表示画笔工具的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 实线画笔图标
|
||||
/// </summary>
|
||||
/// <remarks>用于表示画笔工具的图标</remarks>
|
||||
public static string SolidPenIcon =
|
||||
"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";
|
||||
|
||||
/// <summary>
|
||||
/// 虚线橡皮擦图标(按笔画擦除模式)
|
||||
/// </summary>
|
||||
/// <remarks>用于表示橡皮擦工具的按笔画擦除模式的图标</remarks>
|
||||
public static string LinedEraserStrokeIcon =
|
||||
"F1 M24,24z M0,0z M19.0625,7.99501C19.2863,7.99501 19.4861,8.06695 19.662,8.21083 19.8538,8.35471 19.9897,8.53856 20.0696,8.76237 20.4054,9.94539 20.7011,11.2563 20.9569,12.6951 21.1967,14.0859 21.3646,15.4368 21.4605,16.7477L21.4845,17.2993C21.5005,17.5551 21.5084,17.9068 21.5084,18.3544 21.5084,19.2017 21.2926,19.8412 20.861,20.2728 20.4453,20.7044 19.7099,21.0482 18.6548,21.3039 18.3191,21.3679 17.7836,20.4327 17.0482,18.4983 16.5845,20.8643 16.1129,22.0313 15.6333,21.9994 13.9227,21.9674 12.468,21.8954 11.269,21.7835L10.8134,21.7356C9.83817,21.6077 8.86297,21.4398 7.88778,21.232 7.1524,21.0721 6.32109,20.8563 5.39386,20.5845 4.61051,20.3447 3.97904,20.089 3.49944,19.8172 3.01984,19.5454 2.70809,19.2337 2.56421,18.882 2.40434,18.4823 2.50827,18.0746 2.87596,17.659L3.09176,17.3952 3.18769,17.2273 3.23565,17.1074 3.37954,16.8197 3.47544,16.5559 3.6433,15.9324 3.73923,15.6207 3.85912,15.1411 4.17088,13.5824 4.57853,11.1844 4.98621,9.05013C5.03417,8.79435 5.16203,8.55455 5.36986,8.33073 5.59367,8.10692 5.8255,7.99501 6.0653,7.99501L19.0625,7.99501z M14.4823,2C14.7861,2 15.0418,2.08793 15.2496,2.26378 15.4575,2.43963 15.5614,2.64746 15.5614,2.88726L15.5614,4.99751 19.0625,4.99751C19.3343,4.99751 19.5661,5.10142 19.7579,5.30925 19.9498,5.50109 20.0457,5.73289 20.0457,6.00467 20.0457,6.27644 19.9498,6.51624 19.7579,6.72407 19.5661,6.91591 19.3343,7.01183 19.0625,7.01183L6.0653,7.01183C5.79352,7.01183 5.55372,6.91591 5.34589,6.72407 5.15405,6.51624 5.05814,6.27644 5.05814,6.00467 5.05814,5.73289 5.15405,5.50109 5.34589,5.30925 5.55372,5.10142 5.79352,4.99751 6.0653,4.99751L9.56638,4.99751 9.56638,2.88726C9.56638,2.66345 9.65432,2.47161 9.83017,2.31174 10.022,2.15187 10.2538,2.05595 10.5256,2.02398L10.6455,2 14.4823,2z";
|
||||
|
||||
/// <summary>
|
||||
/// 实线橡皮擦图标(按笔画擦除模式)
|
||||
/// </summary>
|
||||
/// <remarks>用于表示橡皮擦工具的按笔画擦除模式的图标</remarks>
|
||||
public static string SolidEraserStrokeIcon =
|
||||
"F1 M24,24z M0,0z M19.0625,7.99501C19.2863,7.99501 19.4861,8.06695 19.662,8.21083 19.8538,8.35471 19.9897,8.53856 20.0696,8.76237 20.4054,9.94539 20.7011,11.2563 20.9569,12.6951 21.1967,14.0859 21.3646,15.4368 21.4605,16.7477L21.4845,17.2993C21.5005,17.5551 21.5084,17.9068 21.5084,18.3544 21.5084,19.2017 21.2926,19.8412 20.861,20.2728 20.4453,20.7044 19.7099,21.0482 18.6548,21.3039 18.3191,21.3679 17.7836,20.4327 17.0482,18.4983 16.5845,20.8643 16.1129,22.0313 15.6333,21.9994 13.9227,21.9674 12.468,21.8954 11.269,21.7835L10.8134,21.7356C9.83817,21.6077 8.86297,21.4398 7.88778,21.232 7.1524,21.0721 6.32109,20.8563 5.39386,20.5845 4.61051,20.3447 3.97904,20.089 3.49944,19.8172 3.01984,19.5454 2.70809,19.2337 2.56421,18.882 2.40434,18.4823 2.50827,18.0746 2.87596,17.659L3.09176,17.3952 3.18769,17.2273 3.23565,17.1074 3.37954,16.8197 3.47544,16.5559 3.6433,15.9324 3.73923,15.6207 3.85912,15.1411 4.17088,13.5824 4.57853,11.1844 4.98621,9.05013C5.03417,8.79435 5.16203,8.55455 5.36986,8.33073 5.59367,8.10692 5.8255,7.99501 6.0653,7.99501L19.0625,7.99501z M14.4823,2C14.7861,2 15.0418,2.08793 15.2496,2.26378 15.4575,2.43963 15.5614,2.64746 15.5614,2.88726L15.5614,4.99751 19.0625,4.99751C19.3343,4.99751 19.5661,5.10142 19.7579,5.30925 19.9498,5.50109 20.0457,5.73289 20.0457,6.00467 20.0457,6.27644 19.9498,6.51624 19.7579,6.72407 19.5661,6.91591 19.3343,7.01183 19.0625,7.01183L6.0653,7.01183C5.79352,7.01183 5.55372,6.91591 5.34589,6.72407 5.15405,6.51624 5.05814,6.27644 5.05814,6.00467 5.05814,5.73289 5.15405,5.50109 5.34589,5.30925 5.55372,5.10142 5.79352,4.99751 6.0653,4.99751L9.56638,4.99751 9.56638,2.88726C9.56638,2.66345 9.65432,2.47161 9.83017,2.31174 10.022,2.15187 10.2538,2.05595 10.5256,2.02398L10.6455,2 14.4823,2z";
|
||||
|
||||
/// <summary>
|
||||
/// 虚线橡皮擦图标(圆形擦除模式)
|
||||
/// </summary>
|
||||
/// <remarks>用于表示橡皮擦工具的圆形擦除模式的图标</remarks>
|
||||
public static string LinedEraserCircleIcon =
|
||||
"F1 M24,24z M0,0z M15.0665,2.29557L21.6921,8.92118C21.8892,9.11823 21.9877,9.36453 21.9877,9.6601 21.9877,9.93924 21.8892,10.1773 21.6921,10.3744L10.3621,21.6798C10.165,21.8933 9.92694,22 9.6478,22 9.36865,22 9.12235,21.8933 8.90888,21.6798L2.30789,15.0788C2.11085,14.8654 2.01233,14.619 2.01233,14.3399 2.01233,14.0608 2.11085,13.8227 2.30789,13.6256L13.6133,2.29557C13.8103,2.09852 14.0485,2 14.3276,2 14.6232,2 14.8695,2.09852 15.0665,2.29557z M8.19458,11.5813C8.09606,11.4828 7.97292,11.4335 7.82514,11.4335 7.69377,11.4335 7.57883,11.4828 7.48031,11.5813L5.28818,13.7734C5.18965,13.8719 5.14041,13.9951 5.14041,14.1429 5.14041,14.2906 5.18965,14.4138 5.28818,14.5123L9.47539,18.6995C9.59033,18.8144 9.71347,18.8719 9.84483,18.8719 9.99261,18.8719 10.1158,18.8144 10.2143,18.6995L12.4064,16.5074C12.5049,16.4089 12.5542,16.2939 12.5542,16.1626 12.5542,16.0148 12.5049,15.8916 12.4064,15.7931L8.19458,11.5813z";
|
||||
|
||||
/// <summary>
|
||||
/// 实线橡皮擦图标(圆形擦除模式)
|
||||
/// </summary>
|
||||
/// <remarks>用于表示橡皮擦工具的圆形擦除模式的图标</remarks>
|
||||
public static string SolidEraserCircleIcon =
|
||||
"F1 M24,24z M0,0z M15.0665,2.29557L21.6921,8.92118C21.8892,9.11823 21.9877,9.36453 21.9877,9.6601 21.9877,9.93924 21.8892,10.1773 21.6921,10.3744L10.3621,21.6798C10.165,21.8933 9.92694,22 9.6478,22 9.36865,22 9.12235,21.8933 8.90888,21.6798L2.30789,15.0788C2.11085,14.8654 2.01233,14.619 2.01233,14.3399 2.01233,14.0608 2.11085,13.8227 2.30789,13.6256L13.6133,2.29557C13.8103,2.09852 14.0485,2 14.3276,2 14.6232,2 14.8695,2.09852 15.0665,2.29557z M8.19458,11.5813C8.09606,11.4828 7.97292,11.4335 7.82514,11.4335 7.69377,11.4335 7.57883,11.4828 7.48031,11.5813L5.28818,13.7734C5.18965,13.8719 5.14041,13.9951 5.14041,14.1429 5.14041,14.2906 5.18965,14.4138 5.28818,14.5123L9.47539,18.6995C9.59033,18.8144 9.71347,18.8719 9.84483,18.8719 9.99261,18.8719 10.1158,18.8144 10.2143,18.6995L12.4064,16.5074C12.5049,16.4089 12.5542,16.2939 12.5542,16.1626 12.5542,16.0148 12.5049,15.8916 12.4064,15.7931L8.19458,11.5813z";
|
||||
|
||||
/// <summary>
|
||||
/// 虚线套索选择工具图标
|
||||
/// </summary>
|
||||
/// <remarks>用于表示套索选择工具的图标</remarks>
|
||||
public static string LinedLassoSelectIcon =
|
||||
"F0 M24,24z M0,0z M21.1749,3.19033C21.2959,3.31432,21.3512,3.45083,21.3512,3.62667L21.3512,15.7344 18.9141,14.0608 18.9141,5.43716 5.30084,5.43716 5.30084,19.0505 14.7641,19.0505 15.0947,21.4876 3.49029,21.4876C3.31451,21.4876 3.178,21.4323 3.05397,21.3113 2.92046,21.1636 2.86368,21.0107 2.86368,20.8334L2.86368,3.62667C2.86368,3.44751 2.92108,3.30918 3.04695,3.18331 3.17285,3.0574 3.31118,3 3.49029,3L20.697,3C20.8743,3,21.0272,3.0568,21.1749,3.19033z M15.042,13.7475L16.02,20.0637C16.0562,20.2901,16.1015,20.6026,16.1559,21.001L16.3052,21.9247C16.3234,22.0424 16.3686,22.1239 16.4411,22.1692 16.5226,22.2144 16.6086,22.2235 16.6992,22.1963 16.7897,22.1601 16.8667,22.0877 16.9301,21.979L18.2205,19.7242 20.421,22.8755C20.4663,22.9389 20.5251,22.9796 20.5976,22.9978 20.6791,23.0068 20.7515,22.9887 20.8149,22.9434L21.7522,22.2914C21.8156,22.2461 21.8518,22.1873 21.8609,22.1148 21.879,22.0333 21.8654,21.9609 21.8201,21.8975L19.7555,18.9499 22.7981,18.9499C22.9249,18.9499 23.0154,18.9137 23.0698,18.8412 23.1332,18.7597 23.1512,18.6692 23.1241,18.5696 23.0969,18.47 23.0336,18.3839 22.934,18.3115 22.0556,17.7048 20.8693,16.8898 19.3751,15.8665 17.7542,14.7708 16.509,13.9377 15.6396,13.3672 15.531,13.2947 15.4224,13.2585 15.3137,13.2585 15.205,13.2585 15.1235,13.2992 15.0692,13.3807 15.0149,13.4622 15.0058,13.5845 15.042,13.7475z";
|
||||
|
||||
/// <summary>
|
||||
/// 实线套索选择工具图标
|
||||
/// </summary>
|
||||
/// <remarks>用于表示套索选择工具的图标</remarks>
|
||||
public static string SolidLassoSelectIcon =
|
||||
"F0 M24,24z M0,0z M21.1749,3.19033C21.2959,3.31432,21.3512,3.45083,21.3512,3.62667L21.3512,15.7344 18.9141,14.0608 18.9141,5.43716 5.30084,5.43716 5.30084,19.0505 14.7641,19.0505 15.0947,21.4876 3.49029,21.4876C3.31451,21.4876 3.178,21.4323 3.05397,21.3113 2.92046,21.1636 2.86368,21.0107 2.86368,20.8334L2.86368,3.62667C2.86368,3.44751 2.92108,3.30918 3.04695,3.18331 3.17285,3.0574 3.31118,3 3.49029,3L20.697,3C20.8743,3,21.0272,3.0568,21.1749,3.19033z M15.042,13.7475L16.02,20.0637C16.0562,20.2901,16.1015,20.6026,16.1559,21.001L16.3052,21.9247C16.3234,22.0424 16.3686,22.1239 16.4411,22.1692 16.5226,22.2144 16.6086,22.2235 16.6992,22.1963 16.7897,22.1601 16.8667,22.0877 16.9301,21.979L18.2205,19.7242 20.421,22.8755C20.4663,22.9389 20.5251,22.9796 20.5976,22.9978 20.6791,23.0068 20.7515,22.9887 20.8149,22.9434L21.7522,22.2914C21.8156,22.2461 21.8518,22.1873 21.8609,22.1148 21.879,22.0333 21.8654,21.9609 21.8201,21.8975L19.7555,18.9499 22.7981,18.9499C22.9249,18.9499 23.0154,18.9137 23.0698,18.8412 23.1332,18.7597 23.1512,18.6692 23.1241,18.5696 23.0969,18.47 23.0336,18.3839 22.934,18.3115 22.0556,17.7048 20.8693,16.8898 19.3751,15.8665 17.7542,14.7708 16.509,13.9377 15.6396,13.3672 15.531,13.2947 15.4224,13.2585 15.3137,13.2585 15.205,13.2585 15.1235,13.2992 15.0692,13.3807 15.0149,13.4622 15.0058,13.5845 15.042,13.7475z";
|
||||
|
||||
/// <summary>
|
||||
/// 禁用手势图标
|
||||
/// </summary>
|
||||
/// <remarks>用于表示手势功能已禁用的图标</remarks>
|
||||
public static string DisabledGestureIcon =
|
||||
"F0 M24,24z M0,0z M7.82154,10.0753L7.82154,3.74613C7.82154,3.06603 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.35737,17.6675 7.23419,15.7096 5.31756,12.2988 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 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 M18.2806,5.20935L19.1565,5.75549 20.259,4.01404 19.3831,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";
|
||||
|
||||
/// <summary>
|
||||
/// 启用手势图标
|
||||
/// </summary>
|
||||
/// <remarks>用于表示手势功能已启用的图标</remarks>
|
||||
public static string EnabledGestureIcon =
|
||||
"F1 M24,24z M0,0z M7.29844,9.85586L7.29844,3.52668C7.29844,2.84658 7.56636,2.1871 8.05067,1.70279 8.53498,1.21848 9.18416,0.950562 9.87456,0.950562 10.565,0.950562 11.2141,1.21848 11.6984,1.70279 12.1828,2.1871 12.4507,2.83628 12.4507,3.52668L12.4507,6.15363C12.6184,6.12002 12.7908,6.10279 12.9659,6.10279 13.6563,6.10279 14.3055,6.37071 14.7898,6.85502 14.9253,6.99055 15.0439,7.139 15.1444,7.29765 15.432,7.18971 15.7403,7.13324 16.0572,7.13324 16.7476,7.13324 17.3968,7.40116 17.8811,7.88547 18.023,8.02738 18.1464,8.18344 18.2498,8.35055 19.1625,8.04393 20.2443,8.23925 20.9416,8.93654 21.4259,9.42085 21.6938,10.0803 21.6938,10.7604L21.6938,14.2958C21.1174,13.741,20.4192,13.3118,19.6432,13.0524L19.6432,10.7604C19.6432,10.6265 19.5917,10.4925 19.4886,10.3998 19.2928,10.204 18.9528,10.204 18.757,10.3998 18.7035,10.4532 18.6603,10.5204 18.6334,10.5934L18.6334,11.255C18.6334,11.8218 18.1697,12.2855 17.6029,12.2855 17.0362,12.2855 16.5725,11.8218 16.5725,11.255L16.5725,11.0106C16.5523,10.9304 16.5415,10.8466 16.5415,10.7604 16.5415,10.6283 16.5521,10.4969 16.5725,10.3674L16.5725,9.70936C16.5725,9.5754 16.5209,9.44144 16.4179,9.3487 16.3252,9.25596 16.1912,9.19413 16.0572,9.19413 15.9233,9.19413 15.7893,9.24566 15.6966,9.3487 15.6038,9.44144 15.542,9.5754 15.542,9.70936L15.542,11.255C15.542,11.8218 15.0783,12.2855 14.5116,12.2855 13.9448,12.2855 13.4811,11.8218 13.4811,11.255L13.4811,8.67891C13.4811,8.54495 13.4296,8.41099 13.3266,8.31825 13.2338,8.22551 13.0999,8.16369 12.9659,8.16369 12.8319,8.16369 12.698,8.21521 12.6052,8.31825 12.5125,8.41099 12.4507,8.54495 12.4507,8.67891L12.4507,11.255C12.4507,11.8218 11.987,12.2855 11.4202,12.2855 10.8535,12.2855 10.3898,11.8218 10.3898,11.255L10.3898,3.52668C10.3898,3.39272 10.3383,3.25876 10.2352,3.16602 10.1425,3.07328 10.0085,3.01145 9.87456,3.01145 9.7406,3.01145 9.60664,3.06298 9.5139,3.16602 9.42116,3.25876 9.35933,3.39272 9.35933,3.52668L9.35933,12.2855C9.35933,12.6034 9.21341,12.8889 8.98523,13.0783 8.58114,13.4285 7.97109,13.4115 7.58697,13.0274L6.07221,11.5127C5.92795,11.3684 5.74247,11.286 5.54668,11.255 5.3509,11.2344 5.14481,11.2756 4.97994,11.3787 4.86659,11.4508 4.78415,11.5539 4.74293,11.6878 4.71202,11.8218 4.72232,11.9557 4.79446,12.0794 6.71109,15.4902 7.83427,17.448 8.14341,17.922L8.3495,18.2312C8.8132,18.9422 9.46238,19.5295 10.2043,19.9314 10.9565,20.3333 11.7912,20.5497 12.6362,20.5497L12.9829,20.5497C13.3696,21.3681 13.9542,22.0748 14.6748,22.608 14.6102,22.6097 14.5455,22.6106 14.4807,22.6106L12.6258,22.6106 12.6568,22.59 12.3167,22.59C11.2451,22.5384 10.1837,22.2499 9.23568,21.7347 8.18463,21.1679 7.28814,20.3436 6.62865,19.3544L6.42256,19.0452C6.08251,18.53 4.96963,16.5824 3.00148,13.0892 2.67174,12.5019 2.579,11.8218 2.75417,11.1623 2.92935,10.5131 3.34153,9.95668 3.91858,9.61663 4.48532,9.27658 5.14481,9.13232 5.79399,9.21476 6.34238,9.28439 6.86871,9.50845 7.29844,9.85586z M2.47595,6.0925L1.26003,4.43348 2.09469,3.82551C2.93965,3.20725,3.85675,2.67141,4.81507,2.24893L5.75277,1.82645 6.59774,3.71216 5.66003,4.13465C4.83567,4.50561,4.04223,4.959,3.31061,5.49484L2.47595,6.1028 2.47595,6.0925z M17.7575,4.9899L18.6334,5.53604 19.7359,3.79458 18.86,3.24845C17.5926,2.455,16.2221,1.85734,14.7795,1.46577L13.7799,1.19786 13.2441,3.18662 14.2436,3.45454C15.4802,3.78428,16.6652,4.2995,17.7575,4.9899z";
|
||||
|
||||
/// <summary>
|
||||
/// 启用手势图标徽章(带勾选标记)
|
||||
/// </summary>
|
||||
/// <remarks>用于表示手势功能已启用并带有勾选标记的图标</remarks>
|
||||
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";
|
||||
|
||||
// 老版浮动栏按钮图标
|
||||
/// <summary>
|
||||
/// 老版虚线光标图标
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示鼠标模式的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 老版实线光标图标
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示鼠标模式的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 老版虚线画笔图标
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示画笔工具的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 老版实线画笔图标
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示画笔工具的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 老版虚线橡皮擦图标(按笔画擦除模式)
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示橡皮擦工具的按笔画擦除模式的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 老版实线橡皮擦图标(按笔画擦除模式)
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示橡皮擦工具的按笔画擦除模式的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 老版虚线橡皮擦图标(圆形擦除模式)
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示橡皮擦工具的圆形擦除模式的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 老版实线橡皮擦图标(圆形擦除模式)
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示橡皮擦工具的圆形擦除模式的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 老版虚线套索选择工具图标
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示套索选择工具的图标</remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 老版实线套索选择工具图标
|
||||
/// </summary>
|
||||
/// <remarks>老版浮动栏中用于表示套索选择工具的图标</remarks>
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -29,19 +29,33 @@ namespace Ink_Canvas
|
||||
public List<Point> Path;
|
||||
public Bitmap CameraImage;
|
||||
public BitmapSource CameraBitmapSource;
|
||||
public bool AddToWhiteboard;
|
||||
|
||||
public ScreenshotResult(Rectangle area, List<Point> path = null, Bitmap cameraImage = null, BitmapSource cameraBitmapSource = null)
|
||||
public ScreenshotResult(Rectangle area, List<Point> path = null, Bitmap cameraImage = null,
|
||||
BitmapSource cameraBitmapSource = null, bool addToWhiteboard = false)
|
||||
{
|
||||
Area = area;
|
||||
Path = path;
|
||||
CameraImage = cameraImage;
|
||||
CameraBitmapSource = cameraBitmapSource;
|
||||
AddToWhiteboard = addToWhiteboard;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
// 截图并插入到画布
|
||||
/// <summary>
|
||||
/// 截图并插入到画布
|
||||
/// </summary>
|
||||
/// <returns>异步任务</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 隐藏主窗口以避免截图包含窗口本身
|
||||
/// 2. 启动区域选择截图
|
||||
/// 3. 恢复窗口显示
|
||||
/// 4. 处理截图结果并插入到画布
|
||||
/// 5. 支持摄像头截图和区域截图
|
||||
/// </remarks>
|
||||
private async Task CaptureScreenshotAndInsert()
|
||||
{
|
||||
try
|
||||
@@ -61,11 +75,17 @@ namespace Ink_Canvas
|
||||
|
||||
if (screenshotResult.HasValue)
|
||||
{
|
||||
if (screenshotResult.Value.AddToWhiteboard)
|
||||
{
|
||||
await AddScreenshotToNewWhiteboardPage(screenshotResult.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是摄像头截图
|
||||
if (screenshotResult.Value.CameraBitmapSource != null)
|
||||
{
|
||||
// 摄像头截图(使用BitmapSource)
|
||||
await InsertBitmapSourceToCanvas(screenshotResult.Value.CameraBitmapSource);
|
||||
await InsertBitmapSourceToCanvas(screenshotResult.Value.CameraBitmapSource, "摄像头截图已插入到画布", "插入摄像头截图失败");
|
||||
}
|
||||
else if (screenshotResult.Value.CameraImage != null)
|
||||
{
|
||||
@@ -118,7 +138,18 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 直接全屏截图并插入到画布
|
||||
/// <summary>
|
||||
/// 直接全屏截图并插入到画布
|
||||
/// </summary>
|
||||
/// <returns>异步任务</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 隐藏主窗口以避免截图包含窗口本身
|
||||
/// 2. 获取虚拟屏幕边界
|
||||
/// 3. 截取全屏
|
||||
/// 4. 将截图转换为WPF Image并插入到画布
|
||||
/// 5. 恢复窗口显示
|
||||
/// </remarks>
|
||||
private async Task CaptureFullScreenAndInsert()
|
||||
{
|
||||
try
|
||||
@@ -158,7 +189,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 显示截图区域选择器
|
||||
/// <summary>
|
||||
/// 显示截图区域选择器
|
||||
/// </summary>
|
||||
/// <returns>截图结果,包含区域、路径和摄像头截图信息</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 显示截图选择器窗口
|
||||
/// 2. 获取用户选择的区域或摄像头截图
|
||||
/// 3. 返回截图结果
|
||||
/// </remarks>
|
||||
private async Task<ScreenshotResult?> ShowScreenshotSelector()
|
||||
{
|
||||
ScreenshotResult? result = null;
|
||||
@@ -177,7 +217,8 @@ namespace Ink_Canvas
|
||||
Rectangle.Empty, // 摄像头截图不需要区域
|
||||
null, // 摄像头截图不需要路径
|
||||
null, // 不再使用Bitmap
|
||||
selectorWindow.CameraBitmapSource // 摄像头BitmapSource
|
||||
selectorWindow.CameraBitmapSource, // 摄像头BitmapSource
|
||||
selectorWindow.ShouldAddToWhiteboard
|
||||
);
|
||||
}
|
||||
else if (selectorWindow.CameraImage != null)
|
||||
@@ -185,14 +226,19 @@ namespace Ink_Canvas
|
||||
result = new ScreenshotResult(
|
||||
Rectangle.Empty, // 摄像头截图不需要区域
|
||||
null, // 摄像头截图不需要路径
|
||||
selectorWindow.CameraImage // 摄像头图像
|
||||
selectorWindow.CameraImage, // 摄像头图像
|
||||
null,
|
||||
selectorWindow.ShouldAddToWhiteboard
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = new ScreenshotResult(
|
||||
selectorWindow.SelectedArea.Value,
|
||||
selectorWindow.SelectedPath
|
||||
selectorWindow.SelectedPath,
|
||||
null,
|
||||
null,
|
||||
selectorWindow.ShouldAddToWhiteboard
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -206,7 +252,19 @@ namespace Ink_Canvas
|
||||
return result;
|
||||
}
|
||||
|
||||
// 截取指定屏幕区域
|
||||
/// <summary>
|
||||
/// 截取指定屏幕区域
|
||||
/// </summary>
|
||||
/// <param name="area">要截取的屏幕区域</param>
|
||||
/// <returns>截取的位图</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 确保区域在有效范围内
|
||||
/// 2. 调整区域边界,确保不超出屏幕范围
|
||||
/// 3. 创建支持透明度的位图
|
||||
/// 4. 设置高质量渲染
|
||||
/// 5. 截取屏幕区域
|
||||
/// </remarks>
|
||||
private Bitmap CaptureScreenArea(Rectangle area)
|
||||
{
|
||||
try
|
||||
@@ -246,7 +304,25 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 将截图插入到画布
|
||||
/// <summary>
|
||||
/// 将截图插入到画布
|
||||
/// </summary>
|
||||
/// <param name="bitmap">要插入的位图</param>
|
||||
/// <returns>异步任务</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 验证位图有效性
|
||||
/// 2. 将Bitmap转换为WPF BitmapSource
|
||||
/// 3. 创建WPF Image控件
|
||||
/// 4. 生成唯一名称
|
||||
/// 5. 初始化TransformGroup
|
||||
/// 6. 设置截图属性,避免被InkCanvas选择系统处理
|
||||
/// 7. 初始化InkCanvas选择设置
|
||||
/// 8. 等待图片加载完成后进行居中处理
|
||||
/// 9. 添加到画布
|
||||
/// 10. 提交历史记录
|
||||
/// 11. 插入图片后切换到选择模式并刷新浮动栏高光显示
|
||||
/// </remarks>
|
||||
private async Task InsertScreenshotToCanvas(Bitmap bitmap)
|
||||
{
|
||||
try
|
||||
@@ -325,8 +401,24 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 将BitmapSource插入到画布(用于摄像头截图)
|
||||
private async Task InsertBitmapSourceToCanvas(BitmapSource bitmapSource)
|
||||
/// <summary>
|
||||
/// 将BitmapSource插入到画布(用于摄像头截图)
|
||||
/// </summary>
|
||||
/// <param name="bitmapSource">要插入的BitmapSource</param>
|
||||
/// <returns>异步任务</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 创建WPF Image控件
|
||||
/// 2. 生成唯一名称
|
||||
/// 3. 初始化TransformGroup
|
||||
/// 4. 设置截图属性,避免被InkCanvas选择系统处理
|
||||
/// 5. 初始化InkCanvas选择设置
|
||||
/// 6. 等待图片加载完成后进行居中处理
|
||||
/// 7. 添加到画布
|
||||
/// 8. 提交历史记录
|
||||
/// 9. 插入图片后切换到选择模式并刷新浮动栏高光显示
|
||||
/// </remarks>
|
||||
private async Task InsertBitmapSourceToCanvas(BitmapSource bitmapSource, string successMessage = "截图已插入到画布", string failureMessagePrefix = "插入截图失败")
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -375,16 +467,22 @@ namespace Ink_Canvas
|
||||
UpdateCurrentToolMode("select");
|
||||
HideSubPanels("select");
|
||||
|
||||
ShowNotification("摄像头截图已插入到画布");
|
||||
ShowNotification(successMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowNotification($"插入摄像头截图失败: {ex.Message}");
|
||||
ShowNotification($"{failureMessagePrefix}: {ex.Message}");
|
||||
LogHelper.WriteLogToFile($"插入摄像头截图失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化截图的TransformGroup
|
||||
/// <summary>
|
||||
/// 初始化截图的TransformGroup
|
||||
/// </summary>
|
||||
/// <param name="image">要初始化的Image控件</param>
|
||||
/// <remarks>
|
||||
/// 该方法会为截图创建一个包含缩放、平移和旋转变换的TransformGroup。
|
||||
/// </remarks>
|
||||
private void InitializeScreenshotTransform(Image image)
|
||||
{
|
||||
var transformGroup = new TransformGroup();
|
||||
@@ -394,7 +492,17 @@ namespace Ink_Canvas
|
||||
image.RenderTransform = transformGroup;
|
||||
}
|
||||
|
||||
// 绑定截图事件处理器
|
||||
/// <summary>
|
||||
/// 绑定截图事件处理器
|
||||
/// </summary>
|
||||
/// <param name="image">要绑定事件的Image控件</param>
|
||||
/// <remarks>
|
||||
/// 该方法会为截图绑定以下事件:
|
||||
/// 1. 鼠标事件(按下、释放、移动、滚轮)
|
||||
/// 2. 触摸事件(按下、释放、操作)
|
||||
/// 3. 设置光标为手形
|
||||
/// 4. 禁用InkCanvas对截图的选择处理
|
||||
/// </remarks>
|
||||
private void BindScreenshotEvents(Image image)
|
||||
{
|
||||
// 鼠标事件
|
||||
@@ -418,7 +526,27 @@ namespace Ink_Canvas
|
||||
image.Focusable = false;
|
||||
}
|
||||
|
||||
// 专门为截图优化的居中缩放方法
|
||||
/// <summary>
|
||||
/// 专门为截图优化的居中缩放方法
|
||||
/// </summary>
|
||||
/// <param name="image">要居中缩放的Image控件</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 确保图片已加载
|
||||
/// 2. 获取画布的实际尺寸
|
||||
/// 3. 如果画布尺寸为0,使用窗口尺寸作为备选
|
||||
/// 4. 如果仍然为0,使用屏幕尺寸
|
||||
/// 5. 计算最大允许尺寸(画布的80%)
|
||||
/// 6. 获取图片的原始尺寸
|
||||
/// 7. 计算缩放比例
|
||||
/// 8. 如果图片本身比最大尺寸小,不进行缩放
|
||||
/// 9. 计算新的尺寸
|
||||
/// 10. 设置图片尺寸
|
||||
/// 11. 计算居中位置
|
||||
/// 12. 确保位置不为负数
|
||||
/// 13. 设置位置
|
||||
/// 14. 保持滚轮缩放和拖动功能
|
||||
/// </remarks>
|
||||
private void CenterAndScaleScreenshot(Image image)
|
||||
{
|
||||
try
|
||||
@@ -502,7 +630,27 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 应用形状遮罩到截图
|
||||
/// <summary>
|
||||
/// 应用形状遮罩到截图
|
||||
/// </summary>
|
||||
/// <param name="bitmap">要应用遮罩的位图</param>
|
||||
/// <param name="path">遮罩路径</param>
|
||||
/// <param name="area">截图区域</param>
|
||||
/// <returns>应用遮罩后的位图</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 验证路径参数
|
||||
/// 2. 获取DPI缩放比例
|
||||
/// 3. 创建结果位图,确保支持透明度
|
||||
/// 4. 首先将整个位图设置为透明
|
||||
/// 5. 创建路径
|
||||
/// 6. 转换WPF坐标到GDI+坐标,考虑DPI缩放和屏幕偏移
|
||||
/// 7. 添加路径
|
||||
/// 8. 验证路径是否有效
|
||||
/// 9. 设置裁剪区域为路径内部
|
||||
/// 10. 在裁剪区域内绘制原始图像
|
||||
/// 11. 重置裁剪区域,确保后续操作不受影响
|
||||
/// </remarks>
|
||||
private Bitmap ApplyShapeMask(Bitmap bitmap, List<Point> path, Rectangle area)
|
||||
{
|
||||
try
|
||||
@@ -588,7 +736,21 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 将System.Drawing.Bitmap转换为WPF BitmapSource
|
||||
/// <summary>
|
||||
/// 将System.Drawing.Bitmap转换为WPF BitmapSource
|
||||
/// </summary>
|
||||
/// <param name="bitmap">要转换的位图</param>
|
||||
/// <returns>转换后的BitmapSource</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 验证位图有效性
|
||||
/// 2. 验证位图尺寸
|
||||
/// 3. 使用更安全的方法转换位图
|
||||
/// 4. 根据像素格式选择合适的WPF像素格式
|
||||
/// 5. 创建BitmapSource
|
||||
/// 6. 冻结BitmapSource以提高性能
|
||||
/// 7. 如果转换失败,尝试使用备用方法
|
||||
/// </remarks>
|
||||
private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap)
|
||||
{
|
||||
try
|
||||
@@ -673,7 +835,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 备用的位图转换方法(使用内存流)
|
||||
/// <summary>
|
||||
/// 备用的位图转换方法(使用内存流)
|
||||
/// </summary>
|
||||
/// <param name="bitmap">要转换的位图</param>
|
||||
/// <returns>转换后的BitmapSource</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 验证位图有效性
|
||||
/// 2. 创建一个新的位图,确保格式正确
|
||||
/// 3. 在内存流中保存为PNG格式
|
||||
/// 4. 创建BitmapImage并加载内存流中的数据
|
||||
/// 5. 冻结BitmapImage以提高性能
|
||||
/// </remarks>
|
||||
private BitmapSource ConvertBitmapToBitmapSourceFallback(Bitmap bitmap)
|
||||
{
|
||||
try
|
||||
@@ -682,11 +856,12 @@ namespace Ink_Canvas
|
||||
if (bitmap == null || bitmap.Width <= 0 || bitmap.Height <= 0)
|
||||
return null;
|
||||
|
||||
// 创建一个新的位图,确保格式正确
|
||||
using (var convertedBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb))
|
||||
// 创建一个新的位图,确保保留Alpha通道
|
||||
using (var convertedBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb))
|
||||
{
|
||||
using (var graphics = Graphics.FromImage(convertedBitmap))
|
||||
{
|
||||
graphics.CompositingMode = CompositingMode.SourceCopy;
|
||||
graphics.DrawImage(bitmap, 0, 0);
|
||||
}
|
||||
|
||||
@@ -713,7 +888,21 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 最简单的位图转换方法
|
||||
/// <summary>
|
||||
/// 最简单的位图转换方法
|
||||
/// </summary>
|
||||
/// <param name="bitmap">要转换的位图</param>
|
||||
/// <returns>转换后的BitmapSource</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 验证位图有效性
|
||||
/// 2. 使用最基础的方法:直接保存为PNG然后加载
|
||||
/// 3. 创建临时文件
|
||||
/// 4. 将位图保存为PNG格式到临时文件
|
||||
/// 5. 创建BitmapImage并加载临时文件
|
||||
/// 6. 冻结BitmapImage以提高性能
|
||||
/// 7. 清理临时文件
|
||||
/// </remarks>
|
||||
private BitmapSource ConvertBitmapToBitmapSourceSimple(Bitmap bitmap)
|
||||
{
|
||||
try
|
||||
@@ -760,7 +949,14 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 获取DPI缩放比例
|
||||
/// <summary>
|
||||
/// 获取DPI缩放比例
|
||||
/// </summary>
|
||||
/// <returns>DPI缩放比例</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会从当前窗口的PresentationSource获取DPI缩放比例。
|
||||
/// 如果无法获取,则返回默认值1.0。
|
||||
/// </remarks>
|
||||
private double GetDpiScale()
|
||||
{
|
||||
var source = PresentationSource.FromVisual(this);
|
||||
@@ -771,4 +967,4 @@ namespace Ink_Canvas
|
||||
return 1.0; // 默认DPI
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -11,16 +11,35 @@ namespace Ink_Canvas
|
||||
private int lastNotificationShowTime;
|
||||
private int notificationShowTime = 2500;
|
||||
|
||||
/// <summary>
|
||||
/// 静态方法,用于在主窗口中显示通知
|
||||
/// </summary>
|
||||
/// <param name="notice">要显示的通知文本</param>
|
||||
/// <param name="isShowImmediately">指示是否应立即显示通知</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 获取应用程序中的主窗口实例
|
||||
/// 2. 调用主窗口的ShowNotification方法显示通知
|
||||
/// </remarks>
|
||||
public static void ShowNewMessage(string notice, bool isShowImmediately = true)
|
||||
{
|
||||
(Application.Current?.Windows.Cast<Window>().FirstOrDefault(window => window is MainWindow) as MainWindow)
|
||||
?.ShowNotification(notice, isShowImmediately);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在窗口中显示带从底部滑入并淡入的通知文本,并在配置的时长后自动隐藏(若未被新通知覆盖)。
|
||||
/// </summary>
|
||||
/// <param name="notice">要显示的通知文本。</param>
|
||||
/// <param name="isShowImmediately">指示是否应立即显示通知;当前实现默认立即显示。</param>
|
||||
public void ShowNotification(string notice, bool isShowImmediately = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (TextBlockNotice == null || GridNotifications == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
lastNotificationShowTime = Environment.TickCount;
|
||||
|
||||
TextBlockNotice.Text = notice;
|
||||
@@ -36,7 +55,10 @@ namespace Ink_Canvas
|
||||
});
|
||||
}).Start();
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ShowNotification 异常: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1077
-461
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -19,8 +20,11 @@ namespace Ink_Canvas
|
||||
ObservableCollection<PageListViewItem> blackBoardSidePageListViewObservableCollection = new ObservableCollection<PageListViewItem>();
|
||||
|
||||
/// <summary>
|
||||
/// <para>刷新白板的缩略图页面列表。</para>
|
||||
/// 刷新白板的缩略图页面列表,更新左右侧缩略页列表,使其与当前白板页及历史快照一致,并将左右列表的选中项同步到当前白板页。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 为每页生成或更新对应的 PageListViewItem(通过应用时间线历史并裁剪到画布边界),用当前画布的笔迹替换当前页的条目,并将两个侧边 ListView 的 SelectedIndex 设置为当前白板索引 - 1。
|
||||
/// </remarks>
|
||||
private void RefreshBlackBoardSidePageListView()
|
||||
{
|
||||
if (blackBoardSidePageListViewObservableCollection.Count == WhiteboardTotalCount)
|
||||
@@ -66,6 +70,79 @@ namespace Ink_Canvas
|
||||
BlackBoardRightSidePageListView.SelectedIndex = CurrentWhiteboardIndex - 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据传入相对于 <paramref name="scrollViewer"/> 的点,查找并选中列表中对应的缩略图项;在需要时切换当前白板页并更新画布状态与左右侧缩略图选择状态。
|
||||
/// </summary>
|
||||
/// <param name="listView">承载页面缩略图的 ListView。</param>
|
||||
/// <param name="scrollViewer">包含该 ListView 的 ScrollViewer,用于将触点坐标从滚动视图坐标系转换到 ListView。</param>
|
||||
/// <param name="pointInScrollViewer">相对于 <paramref name="scrollViewer"/> 的触点坐标(用于命中测试)。</param>
|
||||
/// <remarks>
|
||||
/// - 如果命中到 ListViewItem,会隐藏左右侧页面边框、在必要时保存/清空/恢复画笔笔迹并更新 CurrentWhiteboardIndex 与显示信息;还会将左右两侧 ListView 的 SelectedIndex 同步为命中项索引。
|
||||
/// - 在查找命中或切换过程中发生的异常将被捕获并忽略,不会向上抛出。
|
||||
/// </remarks>
|
||||
private void TrySwitchWhiteboardPageByTouchPoint(ListView listView, ScrollViewer scrollViewer, Point pointInScrollViewer)
|
||||
{
|
||||
if (listView == null || scrollViewer == null) return;
|
||||
try
|
||||
{
|
||||
var transform = scrollViewer.TransformToVisual(listView);
|
||||
if (transform == null) return;
|
||||
var pointInListView = transform.Transform(pointInScrollViewer);
|
||||
var hit = VisualTreeHelper.HitTest(listView, pointInListView);
|
||||
if (hit?.VisualHit == null) return;
|
||||
var container = FindAncestorOfType<ListViewItem>(hit.VisualHit);
|
||||
if (container == null) return;
|
||||
int index = listView.ItemContainerGenerator.IndexFromContainer(container);
|
||||
if (index < 0 || index >= blackBoardSidePageListViewObservableCollection.Count) return;
|
||||
var item = blackBoardSidePageListViewObservableCollection[index];
|
||||
if (item == null) return;
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardBorderLeftPageListView);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardBorderRightPageListView);
|
||||
if (index + 1 != CurrentWhiteboardIndex)
|
||||
{
|
||||
if (currentSelectedElement != null)
|
||||
{
|
||||
var previousEditingMode = inkCanvas.EditingMode;
|
||||
UnselectElement(currentSelectedElement);
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
SaveStrokes();
|
||||
ClearStrokes(true);
|
||||
CurrentWhiteboardIndex = index + 1;
|
||||
RestoreStrokes();
|
||||
UpdateIndexInfoDisplay();
|
||||
}
|
||||
BlackBoardLeftSidePageListView.SelectedIndex = index;
|
||||
BlackBoardRightSidePageListView.SelectedIndex = index;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略命中测试或切换过程中的异常
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在视觉树中自下而上查找并返回第一个匹配指定类型的祖先元素。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要查找的祖先类型,必须继承自 <see cref="DependencyObject"/>。</typeparam>
|
||||
/// <param name="current">起始节点;从此节点开始向上遍历视觉树。</param>
|
||||
/// <returns>找到的第一个类型为 <typeparamref name="T"/> 的祖先元素,未找到时返回 <c>null</c>。</returns>
|
||||
private static T FindAncestorOfType<T>(DependencyObject current) where T : DependencyObject
|
||||
{
|
||||
while (current != null)
|
||||
{
|
||||
if (current is T found) return found;
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将指定元素在给定 ScrollViewer 中滚动,使该元素与可视区域的顶部对齐。
|
||||
/// </summary>
|
||||
/// <param name="element">要对齐到顶部的元素。</param>
|
||||
/// <param name="scrollViewer">包含该元素的目标 ScrollViewer。</param>
|
||||
public static void ScrollViewToVerticalTop(FrameworkElement element, ScrollViewer scrollViewer)
|
||||
{
|
||||
if (element == null || scrollViewer == null)
|
||||
@@ -86,6 +163,24 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 左侧页面列表视图的鼠标释放事件处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 隐藏左右侧页面边框
|
||||
/// 2. 获取选中的项目和索引
|
||||
/// 3. 只有当选择的页面与当前页面不同时才进行切换
|
||||
/// 4. 如果有选中的元素,先取消选择
|
||||
/// 5. 保存当前页面的笔画
|
||||
/// 6. 清空画布
|
||||
/// 7. 更新当前白板索引
|
||||
/// 8. 恢复新页面的笔画
|
||||
/// 9. 更新索引信息显示
|
||||
/// 10. 更新选择索引
|
||||
/// </remarks>
|
||||
private void BlackBoardLeftSidePageListView_OnMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardBorderLeftPageListView);
|
||||
@@ -119,6 +214,24 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 右侧页面列表视图的鼠标释放事件处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 隐藏左右侧页面边框
|
||||
/// 2. 获取选中的项目和索引
|
||||
/// 3. 只有当选择的页面与当前页面不同时才进行切换
|
||||
/// 4. 如果有选中的元素,先取消选择
|
||||
/// 5. 保存当前页面的笔画
|
||||
/// 6. 清空画布
|
||||
/// 7. 更新当前白板索引
|
||||
/// 8. 恢复新页面的笔画
|
||||
/// 9. 更新索引信息显示
|
||||
/// 10. 更新选择索引
|
||||
/// </remarks>
|
||||
private void BlackBoardRightSidePageListView_OnMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardBorderLeftPageListView);
|
||||
@@ -152,5 +265,14 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预览列表中某页的“删除”按钮点击:删除该页,并阻止事件继续冒泡(避免触发选中/切页)。
|
||||
/// </summary>
|
||||
private void WhiteBoardPageListItem_DeleteClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
if (sender is FrameworkElement fe && fe.DataContext is PageListViewItem item)
|
||||
DeleteWhiteBoardPageByIndex(item.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Forms;
|
||||
@@ -14,6 +16,8 @@ using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Color = System.Drawing.Color;
|
||||
using File = System.IO.File;
|
||||
using Image = System.Windows.Controls.Image;
|
||||
@@ -34,6 +38,18 @@ namespace Ink_Canvas
|
||||
}
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 保存墨迹的鼠标释放事件处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 检查是否是当前按下的对象,且墨迹画布是否可见
|
||||
/// 2. 隐藏工具面板
|
||||
/// 3. 隐藏通知面板
|
||||
/// 4. 调用SaveInkCanvasStrokes方法保存墨迹
|
||||
/// </remarks>
|
||||
private void SymbolIconSaveStrokes_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender || inkCanvas.Visibility != Visibility.Visible) return;
|
||||
@@ -46,6 +62,23 @@ namespace Ink_Canvas
|
||||
SaveInkCanvasStrokes(true, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存墨迹画布的墨迹
|
||||
/// </summary>
|
||||
/// <param name="newNotice">是否显示新的通知</param>
|
||||
/// <param name="saveByUser">是否是用户手动保存</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 根据保存类型和模式确定保存路径
|
||||
/// 2. 创建保存目录
|
||||
/// 3. 根据当前模式生成保存文件名
|
||||
/// 4. 根据设置选择保存模式:
|
||||
/// - 全页面保存模式:保存为图像或压缩包
|
||||
/// - XML保存模式:保存为XML文件或压缩包
|
||||
/// - 常规保存模式:保存为二进制格式或XML格式
|
||||
/// 5. 异步上传保存的文件到Dlass
|
||||
/// 6. 保存元素信息
|
||||
/// </remarks>
|
||||
private void SaveInkCanvasStrokes(bool newNotice = true, bool saveByUser = false)
|
||||
{
|
||||
try
|
||||
@@ -62,7 +95,125 @@ namespace Ink_Canvas
|
||||
//savePathWithName = savePath + @"\" + DateTime.Now.ToString("u").Replace(':', '-') + ".icstk";
|
||||
savePathWithName = savePath + @"\" + DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss-fff") + ".icstk";
|
||||
|
||||
if (Settings.Automation.IsSaveFullPageStrokes)
|
||||
if (Settings.Automation.IsSaveStrokesAsXML)
|
||||
{
|
||||
// XML保存模式 - 检查是否存在多页面墨迹
|
||||
bool hasMultiplePages = false;
|
||||
List<StrokeCollection> allPageStrokes = new List<StrokeCollection>();
|
||||
|
||||
// 检查PPT放映模式下的多页面墨迹
|
||||
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible && _pptManager?.IsConnected == true)
|
||||
{
|
||||
hasMultiplePages = true;
|
||||
var totalSlides = _pptManager.SlidesCount;
|
||||
var currentSlide = _pptManager.GetCurrentSlideNumber();
|
||||
|
||||
for (int i = 1; i <= totalSlides; i++)
|
||||
{
|
||||
var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i);
|
||||
if (slideStrokes != null && slideStrokes.Count > 0)
|
||||
{
|
||||
allPageStrokes.Add(slideStrokes);
|
||||
}
|
||||
else if (i == currentSlide && inkCanvas.Strokes.Count > 0)
|
||||
{
|
||||
allPageStrokes.Add(inkCanvas.Strokes.Clone());
|
||||
}
|
||||
else
|
||||
{
|
||||
allPageStrokes.Add(new StrokeCollection());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查白板模式下的多页面墨迹
|
||||
else if (currentMode != 0 && WhiteboardTotalCount > 1)
|
||||
{
|
||||
hasMultiplePages = true;
|
||||
for (int i = 1; i <= WhiteboardTotalCount; i++)
|
||||
{
|
||||
if (TimeMachineHistories[i] != null)
|
||||
{
|
||||
var strokes = ApplyHistoriesToNewStrokeCollection(TimeMachineHistories[i]);
|
||||
allPageStrokes.Add(strokes);
|
||||
}
|
||||
else
|
||||
{
|
||||
allPageStrokes.Add(new StrokeCollection());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMultiplePages && allPageStrokes.Count > 0)
|
||||
{
|
||||
// 检查是否是PPT模式
|
||||
bool isPPTMode = BtnPPTSlideShowEnd.Visibility == Visibility.Visible && _pptManager?.IsConnected == true;
|
||||
|
||||
if (isPPTMode)
|
||||
{
|
||||
// PPT模式:保存为多个XML文件
|
||||
string basePath = Path.GetDirectoryName(savePathWithName);
|
||||
string baseFileName = Path.GetFileNameWithoutExtension(savePathWithName);
|
||||
|
||||
int savedCount = 0;
|
||||
for (int i = 0; i < allPageStrokes.Count; i++)
|
||||
{
|
||||
var strokes = allPageStrokes[i];
|
||||
if (strokes.Count > 0)
|
||||
{
|
||||
string pageFileName = Path.Combine(basePath, $"{baseFileName}_Page-{i + 1}.xml");
|
||||
SaveStrokesAsXML(strokes, pageFileName, false);
|
||||
savedCount++;
|
||||
|
||||
// 异步上传每个XML文件
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Helpers.UploadHelper.UploadFileAsync(pageFileName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (newNotice)
|
||||
{
|
||||
Task.Delay(100).ContinueWith(t =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification($"多页面XML墨迹成功保存为 {savedCount} 个XML文件");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非PPT模式:保存为XML压缩包
|
||||
string zipFileName = Path.ChangeExtension(savePathWithName, "zip");
|
||||
SaveMultiPageStrokesAsXMLZip(allPageStrokes, zipFileName, newNotice);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 单页面XML保存
|
||||
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
|
||||
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
|
||||
if (newNotice)
|
||||
{
|
||||
Task.Delay(100).ContinueWith(t =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Settings.Automation.IsSaveFullPageStrokes)
|
||||
{
|
||||
// 全页面保存模式 - 检查是否存在多页面墨迹
|
||||
bool hasMultiplePages = false;
|
||||
@@ -78,7 +229,7 @@ namespace Ink_Canvas
|
||||
|
||||
for (int i = 1; i <= totalSlides; i++)
|
||||
{
|
||||
var slideStrokes = _multiPPTInkManager?.LoadSlideStrokes(i);
|
||||
var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i);
|
||||
if (slideStrokes != null && slideStrokes.Count > 0)
|
||||
{
|
||||
allPageStrokes.Add(slideStrokes);
|
||||
@@ -128,30 +279,165 @@ namespace Ink_Canvas
|
||||
}
|
||||
else
|
||||
{
|
||||
// 常规保存模式 - 仅保存墨迹对象
|
||||
var fs = new FileStream(savePathWithName, FileMode.Create);
|
||||
inkCanvas.Strokes.Save(fs);
|
||||
fs.Close();
|
||||
// 保存元素信息
|
||||
var elementInfos = new List<CanvasElementInfo>();
|
||||
foreach (var child in inkCanvas.Children)
|
||||
// 常规保存模式 - 检查是否存在多页面墨迹
|
||||
bool hasMultiplePages = false;
|
||||
List<StrokeCollection> allPageStrokes = new List<StrokeCollection>();
|
||||
|
||||
// 检查PPT放映模式下的多页面墨迹
|
||||
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible && _pptManager?.IsConnected == true)
|
||||
{
|
||||
if (child is Image img && img.Source is BitmapImage bmp)
|
||||
hasMultiplePages = true;
|
||||
var totalSlides = _pptManager.SlidesCount;
|
||||
var currentSlide = _pptManager.GetCurrentSlideNumber();
|
||||
|
||||
for (int i = 1; i <= totalSlides; i++)
|
||||
{
|
||||
elementInfos.Add(new CanvasElementInfo
|
||||
var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i);
|
||||
if (slideStrokes != null && slideStrokes.Count > 0)
|
||||
{
|
||||
Type = "Image",
|
||||
SourcePath = bmp.UriSource?.LocalPath ?? "",
|
||||
Left = InkCanvas.GetLeft(img),
|
||||
Top = InkCanvas.GetTop(img),
|
||||
Width = img.Width,
|
||||
Height = img.Height,
|
||||
Stretch = img.Stretch.ToString()
|
||||
allPageStrokes.Add(slideStrokes);
|
||||
}
|
||||
else if (i == currentSlide && inkCanvas.Strokes.Count > 0)
|
||||
{
|
||||
allPageStrokes.Add(inkCanvas.Strokes.Clone());
|
||||
}
|
||||
else
|
||||
{
|
||||
allPageStrokes.Add(new StrokeCollection());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查白板模式下的多页面墨迹
|
||||
else if (currentMode != 0 && WhiteboardTotalCount > 1)
|
||||
{
|
||||
hasMultiplePages = true;
|
||||
for (int i = 1; i <= WhiteboardTotalCount; i++)
|
||||
{
|
||||
if (TimeMachineHistories[i] != null)
|
||||
{
|
||||
var strokes = ApplyHistoriesToNewStrokeCollection(TimeMachineHistories[i]);
|
||||
allPageStrokes.Add(strokes);
|
||||
}
|
||||
else
|
||||
{
|
||||
allPageStrokes.Add(new StrokeCollection());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMultiplePages && allPageStrokes.Count > 0)
|
||||
{
|
||||
// 多页面保存为多个icstk文件
|
||||
string basePath = Path.GetDirectoryName(savePathWithName);
|
||||
string baseFileName = Path.GetFileNameWithoutExtension(savePathWithName);
|
||||
|
||||
for (int i = 0; i < allPageStrokes.Count; i++)
|
||||
{
|
||||
var strokes = allPageStrokes[i];
|
||||
if (strokes.Count > 0)
|
||||
{
|
||||
string pageFileName = Path.Combine(basePath, $"{baseFileName}_Page-{i + 1}.icstk");
|
||||
using (var fs = new FileStream(pageFileName, FileMode.Create))
|
||||
{
|
||||
strokes.Save(fs);
|
||||
}
|
||||
|
||||
// 异步上传每个icstk文件
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Helpers.UploadHelper.UploadFileAsync(pageFileName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (newNotice)
|
||||
{
|
||||
Task.Delay(100).ContinueWith(t =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification($"多页面墨迹成功保存为 {allPageStrokes.Count} 个icstk文件");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
File.WriteAllText(Path.ChangeExtension(savePathWithName, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Formatting.Indented));
|
||||
if (newNotice) ShowNotification("墨迹成功保存至 " + savePathWithName);
|
||||
else
|
||||
{
|
||||
// 单页面保存
|
||||
if (Settings.Automation.IsSaveStrokesAsXML)
|
||||
{
|
||||
// 保存为XML格式
|
||||
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
|
||||
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
|
||||
if (newNotice)
|
||||
{
|
||||
Task.Delay(100).ContinueWith(t =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 保存为二进制格式
|
||||
var fs = new FileStream(savePathWithName, FileMode.Create);
|
||||
inkCanvas.Strokes.Save(fs);
|
||||
fs.Close();
|
||||
if (newNotice)
|
||||
{
|
||||
Task.Delay(100).ContinueWith(t =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification("墨迹成功保存至 " + savePathWithName);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 异步上传文件
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string uploadPath = Settings.Automation.IsSaveStrokesAsXML ? Path.ChangeExtension(savePathWithName, ".xml") : savePathWithName;
|
||||
await Helpers.UploadHelper.UploadFileAsync(uploadPath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
// 保存元素信息
|
||||
var elementInfos = new List<CanvasElementInfo>();
|
||||
foreach (var child in inkCanvas.Children)
|
||||
{
|
||||
if (child is Image img && img.Source is BitmapImage bmp)
|
||||
{
|
||||
elementInfos.Add(new CanvasElementInfo
|
||||
{
|
||||
Type = "Image",
|
||||
SourcePath = bmp.UriSource?.LocalPath ?? "",
|
||||
Left = InkCanvas.GetLeft(img),
|
||||
Top = InkCanvas.GetTop(img),
|
||||
Width = img.Width,
|
||||
Height = img.Height,
|
||||
Stretch = img.Stretch.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
string elementsPath = Settings.Automation.IsSaveStrokesAsXML ? Path.ChangeExtension(savePathWithName, ".elements.json") : Path.ChangeExtension(savePathWithName, ".elements.json");
|
||||
File.WriteAllText(elementsPath, JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -161,6 +447,201 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将StrokeCollection保存为XML格式
|
||||
/// </summary>
|
||||
private void SaveStrokesAsXML(StrokeCollection strokes, string xmlPath, bool triggerUpload = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用XDocument创建XML文档
|
||||
XDocument doc = new XDocument(
|
||||
new XDeclaration("1.0", "utf-8", "yes"),
|
||||
new XElement("InkCanvasStrokes",
|
||||
new XAttribute("Version", "1.0"),
|
||||
new XAttribute("StrokeCount", strokes.Count),
|
||||
new XAttribute("SaveTime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")),
|
||||
from stroke in strokes
|
||||
select new XElement("Stroke",
|
||||
new XAttribute("DrawingAttributes", SerializeDrawingAttributes(stroke.DrawingAttributes)),
|
||||
new XElement("StylusPoints",
|
||||
from point in stroke.StylusPoints
|
||||
select new XElement("StylusPoint",
|
||||
new XAttribute("X", point.X),
|
||||
new XAttribute("Y", point.Y),
|
||||
new XAttribute("PressureFactor", point.PressureFactor)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// 保存XML文件
|
||||
using (var writer = new XmlTextWriter(xmlPath, Encoding.UTF8))
|
||||
{
|
||||
writer.Formatting = System.Xml.Formatting.Indented;
|
||||
doc.Save(writer);
|
||||
}
|
||||
|
||||
// 同时保存元素信息
|
||||
var elementInfos = new List<CanvasElementInfo>();
|
||||
foreach (var child in inkCanvas.Children)
|
||||
{
|
||||
if (child is Image img && img.Source is BitmapImage bmp)
|
||||
{
|
||||
elementInfos.Add(new CanvasElementInfo
|
||||
{
|
||||
Type = "Image",
|
||||
SourcePath = bmp.UriSource?.LocalPath ?? "",
|
||||
Left = InkCanvas.GetLeft(img),
|
||||
Top = InkCanvas.GetTop(img),
|
||||
Width = img.Width,
|
||||
Height = img.Height,
|
||||
Stretch = img.Stretch.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
File.WriteAllText(Path.ChangeExtension(xmlPath, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented));
|
||||
|
||||
// 异步上传到Dlass
|
||||
if (triggerUpload)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Helpers.UploadHelper.UploadFileAsync(xmlPath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存XML格式墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序列化DrawingAttributes为字符串
|
||||
/// </summary>
|
||||
private string SerializeDrawingAttributes(DrawingAttributes da)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"Color={da.Color};");
|
||||
sb.Append($"Width={da.Width};");
|
||||
sb.Append($"Height={da.Height};");
|
||||
sb.Append($"FitToCurve={da.FitToCurve};");
|
||||
sb.Append($"IsHighlighter={da.IsHighlighter};");
|
||||
sb.Append($"IgnorePressure={da.IgnorePressure};");
|
||||
sb.Append($"StylusTip={da.StylusTip};");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将多页面墨迹保存为XML格式压缩包
|
||||
/// </summary>
|
||||
private void SaveMultiPageStrokesAsXMLZip(List<StrokeCollection> allPageStrokes, string zipFileName, bool newNotice)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建临时目录来存放文件
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), $"InkCanvas_MultiPage_XML_{DateTime.Now:yyyyMMdd_HHmmss}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
// 保存所有页面的XML文件到临时目录
|
||||
for (int i = 0; i < allPageStrokes.Count; i++)
|
||||
{
|
||||
var strokes = allPageStrokes[i];
|
||||
if (strokes.Count > 0)
|
||||
{
|
||||
// 保存XML文件(临时文件,不触发上传)
|
||||
string xmlFileName = Path.Combine(tempDir, $"page_{i + 1:D4}.xml");
|
||||
SaveStrokesAsXML(strokes, xmlFileName, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存元数据信息
|
||||
string metadataFile = Path.Combine(tempDir, "metadata.txt");
|
||||
using (var writer = new StreamWriter(metadataFile, false, Encoding.UTF8))
|
||||
{
|
||||
writer.WriteLine($"保存时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
writer.WriteLine($"总页数: {allPageStrokes.Count}");
|
||||
writer.WriteLine($"模式: {(currentMode == 0 ? "PPT放映" : "白板")}");
|
||||
writer.WriteLine($"格式: XML");
|
||||
if (currentMode != 0)
|
||||
{
|
||||
writer.WriteLine($"当前页面: {CurrentWhiteboardIndex}");
|
||||
writer.WriteLine($"总页面数: {WhiteboardTotalCount}");
|
||||
}
|
||||
else if (pptApplication != null)
|
||||
{
|
||||
writer.WriteLine($"PPT名称: {pptApplication.SlideShowWindows[1].Presentation.Name}");
|
||||
writer.WriteLine($"PPT总页数: {pptApplication.SlideShowWindows[1].Presentation.Slides.Count}");
|
||||
writer.WriteLine($"PPT文件路径: {pptApplication.SlideShowWindows[1].Presentation.FullName}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < allPageStrokes.Count; i++)
|
||||
{
|
||||
writer.WriteLine($"页面 {i + 1}: {allPageStrokes[i].Count} 条墨迹");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建ZIP文件
|
||||
if (File.Exists(zipFileName))
|
||||
File.Delete(zipFileName);
|
||||
|
||||
ZipFile.CreateFromDirectory(tempDir, zipFileName);
|
||||
|
||||
// 异步上传ZIP文件到Dlass
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Helpers.UploadHelper.UploadFileAsync(zipFileName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
if (newNotice)
|
||||
{
|
||||
Task.Delay(100).ContinueWith(t =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification($"多页面XML墨迹成功保存至压缩包 {zipFileName}");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 清理临时目录
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理临时目录失败: {ex}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存多页面XML墨迹压缩包失败: {ex}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将多页面墨迹保存为压缩包
|
||||
/// </summary>
|
||||
@@ -228,7 +709,28 @@ namespace Ink_Canvas
|
||||
// 使用System.IO.Compression.FileSystem来创建ZIP
|
||||
ZipFile.CreateFromDirectory(tempDir, zipFileName);
|
||||
|
||||
if (newNotice) ShowNotification($"多页面墨迹成功保存至压缩包 {zipFileName}");
|
||||
// 异步上传ZIP文件到Dlass
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Helpers.UploadHelper.UploadFileAsync(zipFileName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
if (newNotice)
|
||||
{
|
||||
Task.Delay(100).ContinueWith(t =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification($"多页面墨迹成功保存至压缩包 {zipFileName}");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -310,11 +812,31 @@ namespace Ink_Canvas
|
||||
var fs = new FileStream(savePathWithName, FileMode.Create);
|
||||
inkCanvas.Strokes.Save(fs);
|
||||
fs.Close();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Helpers.UploadHelper.UploadFileAsync(imagePathWithName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
if (newNotice) ShowNotification("墨迹成功全页面保存至 " + Path.ChangeExtension(savePathWithName, "png"));
|
||||
if (newNotice)
|
||||
{
|
||||
Task.Delay(100).ContinueWith(t =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification("墨迹成功全页面保存至 " + Path.ChangeExtension(savePathWithName, "png"));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -349,6 +871,22 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开墨迹文件的鼠标释放事件处理
|
||||
/// </summary>
|
||||
/// <param name="sender">发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 检查是否是当前按下的对象
|
||||
/// 2. 隐藏工具面板
|
||||
/// 3. 打开文件选择对话框
|
||||
/// 4. 根据文件扩展名选择不同的打开方式:
|
||||
/// - .zip:处理ICC压缩包
|
||||
/// - .xml:处理XML格式墨迹文件
|
||||
/// - 其他:处理单个墨迹文件(二进制格式)
|
||||
/// 5. 如果墨迹画布不可见,切换到鼠标模式
|
||||
/// </remarks>
|
||||
private void SymbolIconOpenStrokes_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
@@ -358,7 +896,7 @@ namespace Ink_Canvas
|
||||
var openFileDialog = new OpenFileDialog();
|
||||
openFileDialog.InitialDirectory = Settings.Automation.AutoSavedStrokesLocation;
|
||||
openFileDialog.Title = "打开墨迹文件";
|
||||
openFileDialog.Filter = "Ink Canvas Strokes File (*.icstk)|*.icstk|ICC压缩包 (*.zip)|*.zip";
|
||||
openFileDialog.Filter = "Ink Canvas Strokes File (*.icstk)|*.icstk|XML墨迹文件 (*.xml)|*.xml|ICC压缩包 (*.zip)|*.zip|所有支持的文件 (*.icstk;*.xml;*.zip)|*.icstk;*.xml;*.zip";
|
||||
if (openFileDialog.ShowDialog() != true) return;
|
||||
LogHelper.WriteLogToFile($"Strokes Insert: Name: {openFileDialog.FileName}",
|
||||
LogHelper.LogType.Event);
|
||||
@@ -369,12 +907,17 @@ namespace Ink_Canvas
|
||||
|
||||
if (fileExtension == ".zip")
|
||||
{
|
||||
// 处理ICC压缩包
|
||||
// 处理ICC压缩包(可能包含XML格式)
|
||||
OpenICCZipFile(openFileDialog.FileName);
|
||||
}
|
||||
else if (fileExtension == ".xml")
|
||||
{
|
||||
// 处理XML格式墨迹文件
|
||||
OpenXMLStrokeFile(openFileDialog.FileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 处理单个墨迹文件
|
||||
// 处理单个墨迹文件(二进制格式)
|
||||
OpenSingleStrokeFile(openFileDialog.FileName);
|
||||
}
|
||||
|
||||
@@ -401,7 +944,7 @@ namespace Ink_Canvas
|
||||
try
|
||||
{
|
||||
// 解压ZIP文件
|
||||
ZipFile.ExtractToDirectory(zipFilePath, tempDir);
|
||||
SafeZipExtractor.ExtractZipSafely(zipFilePath, tempDir, overwrite: true);
|
||||
|
||||
// 读取元数据文件
|
||||
string metadataFile = Path.Combine(tempDir, "metadata.txt");
|
||||
@@ -513,8 +1056,8 @@ namespace Ink_Canvas
|
||||
if (!string.IsNullOrEmpty(savedPptPath) && !string.IsNullOrEmpty(currentPptPath))
|
||||
{
|
||||
// 使用文件路径哈希值进行比较,避免路径格式差异
|
||||
string savedHash = GetFileHash(savedPptPath);
|
||||
string currentHash = GetFileHash(currentPptPath);
|
||||
string savedHash = HashHelper.GetFileHash(savedPptPath);
|
||||
string currentHash = HashHelper.GetFileHash(currentPptPath);
|
||||
|
||||
if (savedHash != currentHash)
|
||||
{
|
||||
@@ -528,23 +1071,41 @@ namespace Ink_Canvas
|
||||
timeMachine.ClearStrokeHistory();
|
||||
|
||||
// 重置PPT墨迹存储
|
||||
_multiPPTInkManager?.ClearAllStrokes();
|
||||
_singlePPTInkManager?.ClearAllStrokes();
|
||||
|
||||
// 读取所有页面的墨迹文件
|
||||
var files = Directory.GetFiles(tempDir, "page_*.icstk");
|
||||
foreach (var file in files)
|
||||
// 读取所有页面的墨迹文件(支持.icstk和.xml格式)
|
||||
var icstkFiles = Directory.GetFiles(tempDir, "page_*.icstk");
|
||||
var xmlFiles = Directory.GetFiles(tempDir, "page_*.xml");
|
||||
var allFiles = new List<string>();
|
||||
allFiles.AddRange(icstkFiles);
|
||||
allFiles.AddRange(xmlFiles);
|
||||
|
||||
foreach (var file in allFiles)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(file);
|
||||
if (fileName.StartsWith("page_") && int.TryParse(fileName.Substring(5), out int pageNumber))
|
||||
{
|
||||
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read))
|
||||
StrokeCollection strokes = null;
|
||||
string extension = Path.GetExtension(file).ToLower();
|
||||
|
||||
if (extension == ".xml")
|
||||
{
|
||||
var strokes = new StrokeCollection(fs);
|
||||
if (strokes.Count > 0)
|
||||
// 从XML文件加载
|
||||
strokes = LoadStrokesFromXML(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 从二进制文件加载
|
||||
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
_multiPPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes);
|
||||
strokes = new StrokeCollection(fs);
|
||||
}
|
||||
}
|
||||
|
||||
if (strokes != null && strokes.Count > 0)
|
||||
{
|
||||
_singlePPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,14 +1113,14 @@ namespace Ink_Canvas
|
||||
if (_pptManager?.IsInSlideShow == true)
|
||||
{
|
||||
int currentSlide = _pptManager.GetCurrentSlideNumber();
|
||||
var currentStrokes = _multiPPTInkManager?.LoadSlideStrokes(currentSlide);
|
||||
var currentStrokes = _singlePPTInkManager?.LoadSlideStrokes(currentSlide);
|
||||
if (currentStrokes != null && currentStrokes.Count > 0)
|
||||
{
|
||||
inkCanvas.Strokes.Add(currentStrokes);
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"成功恢复PPT墨迹,共{files.Length}页");
|
||||
LogHelper.WriteLogToFile($"成功恢复PPT墨迹,共{allFiles.Count}页");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -640,9 +1201,185 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开XML格式的墨迹文件
|
||||
/// </summary>
|
||||
public void OpenXMLStrokeFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
XDocument doc = XDocument.Load(filePath);
|
||||
var root = doc.Root;
|
||||
if (root == null || root.Name != "InkCanvasStrokes")
|
||||
{
|
||||
throw new Exception("无效的XML墨迹文件格式");
|
||||
}
|
||||
|
||||
var strokes = new StrokeCollection();
|
||||
foreach (var strokeElement in root.Elements("Stroke"))
|
||||
{
|
||||
var drawingAttributesStr = strokeElement.Attribute("DrawingAttributes")?.Value ?? "";
|
||||
var da = ParseDrawingAttributes(drawingAttributesStr);
|
||||
|
||||
var stylusPoints = new StylusPointCollection();
|
||||
var stylusPointsElement = strokeElement.Element("StylusPoints");
|
||||
if (stylusPointsElement != null)
|
||||
{
|
||||
foreach (var pointElement in stylusPointsElement.Elements("StylusPoint"))
|
||||
{
|
||||
double x = double.Parse(pointElement.Attribute("X")?.Value ?? "0");
|
||||
double y = double.Parse(pointElement.Attribute("Y")?.Value ?? "0");
|
||||
float pressure = float.Parse(pointElement.Attribute("PressureFactor")?.Value ?? "0.5");
|
||||
stylusPoints.Add(new StylusPoint(x, y, pressure));
|
||||
}
|
||||
}
|
||||
|
||||
if (stylusPoints.Count > 0)
|
||||
{
|
||||
var stroke = new Stroke(stylusPoints) { DrawingAttributes = da };
|
||||
strokes.Add(stroke);
|
||||
}
|
||||
}
|
||||
|
||||
ClearStrokes(true);
|
||||
timeMachine.ClearStrokeHistory();
|
||||
inkCanvas.Strokes.Add(strokes);
|
||||
LogHelper.NewLog($"XML Strokes Insert: Strokes Count: {inkCanvas.Strokes.Count}");
|
||||
|
||||
// 恢复元素信息
|
||||
var elementsFile = Path.ChangeExtension(filePath, ".elements.json");
|
||||
if (File.Exists(elementsFile))
|
||||
{
|
||||
var elementInfos = JsonConvert.DeserializeObject<List<CanvasElementInfo>>(File.ReadAllText(elementsFile));
|
||||
foreach (var info in elementInfos)
|
||||
{
|
||||
if (info.Type == "Image" && File.Exists(info.SourcePath))
|
||||
{
|
||||
var img = new Image
|
||||
{
|
||||
Source = new BitmapImage(new Uri(info.SourcePath)),
|
||||
Width = info.Width,
|
||||
Height = info.Height,
|
||||
Stretch = Enum.TryParse<Stretch>(info.Stretch, out var stretch) ? stretch : Stretch.Fill
|
||||
};
|
||||
InkCanvas.SetLeft(img, info.Left);
|
||||
InkCanvas.SetTop(img, info.Top);
|
||||
inkCanvas.Children.Add(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"打开XML墨迹文件失败: {ex}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从XML文件加载StrokeCollection(辅助方法,用于ZIP文件恢复)
|
||||
/// </summary>
|
||||
private StrokeCollection LoadStrokesFromXML(string xmlPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
XDocument doc = XDocument.Load(xmlPath);
|
||||
var root = doc.Root;
|
||||
if (root == null || root.Name != "InkCanvasStrokes")
|
||||
{
|
||||
return new StrokeCollection();
|
||||
}
|
||||
|
||||
var strokes = new StrokeCollection();
|
||||
foreach (var strokeElement in root.Elements("Stroke"))
|
||||
{
|
||||
var drawingAttributesStr = strokeElement.Attribute("DrawingAttributes")?.Value ?? "";
|
||||
var da = ParseDrawingAttributes(drawingAttributesStr);
|
||||
|
||||
var stylusPoints = new StylusPointCollection();
|
||||
var stylusPointsElement = strokeElement.Element("StylusPoints");
|
||||
if (stylusPointsElement != null)
|
||||
{
|
||||
foreach (var pointElement in stylusPointsElement.Elements("StylusPoint"))
|
||||
{
|
||||
double x = double.Parse(pointElement.Attribute("X")?.Value ?? "0");
|
||||
double y = double.Parse(pointElement.Attribute("Y")?.Value ?? "0");
|
||||
float pressure = float.Parse(pointElement.Attribute("PressureFactor")?.Value ?? "0.5");
|
||||
stylusPoints.Add(new StylusPoint(x, y, pressure));
|
||||
}
|
||||
}
|
||||
|
||||
if (stylusPoints.Count > 0)
|
||||
{
|
||||
var stroke = new Stroke(stylusPoints) { DrawingAttributes = da };
|
||||
strokes.Add(stroke);
|
||||
}
|
||||
}
|
||||
|
||||
return strokes;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从XML加载墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
return new StrokeCollection();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从字符串解析DrawingAttributes
|
||||
/// </summary>
|
||||
private DrawingAttributes ParseDrawingAttributes(string attributesStr)
|
||||
{
|
||||
var da = new DrawingAttributes();
|
||||
var parts = attributesStr.Split(';');
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var kv = part.Split('=');
|
||||
if (kv.Length == 2)
|
||||
{
|
||||
var key = kv[0].Trim();
|
||||
var value = kv[1].Trim();
|
||||
switch (key)
|
||||
{
|
||||
case "Color":
|
||||
da.Color = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(value);
|
||||
break;
|
||||
case "Width":
|
||||
da.Width = double.Parse(value);
|
||||
break;
|
||||
case "Height":
|
||||
da.Height = double.Parse(value);
|
||||
break;
|
||||
case "FitToCurve":
|
||||
da.FitToCurve = bool.Parse(value);
|
||||
break;
|
||||
case "IsHighlighter":
|
||||
da.IsHighlighter = bool.Parse(value);
|
||||
break;
|
||||
case "IgnorePressure":
|
||||
da.IgnorePressure = bool.Parse(value);
|
||||
break;
|
||||
case "StylusTip":
|
||||
da.StylusTip = Enum.TryParse<StylusTip>(value, out var tip) ? tip : StylusTip.Ellipse;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return da;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开单个墨迹文件
|
||||
/// </summary>
|
||||
/// <param name="filePath">墨迹文件的路径</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 打开墨迹文件并加载墨迹
|
||||
/// 2. 检查文件是否包含墨迹
|
||||
/// 3. 如果包含墨迹,清空当前墨迹并添加新墨迹
|
||||
/// 4. 恢复元素信息
|
||||
/// 5. 如果文件流中没有墨迹,尝试从内存流中加载
|
||||
/// </remarks>
|
||||
public void OpenSingleStrokeFile(string filePath)
|
||||
{
|
||||
var fileStreamHasNoStroke = false;
|
||||
@@ -695,3 +1432,4 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,129 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 在切页/加页场景下使用:先捕获当前画面到内存并克隆墨迹,然后立即返回;截图与墨迹保存在后台异步执行,不阻塞切页。
|
||||
/// 调用方应在调用本方法后立即执行 SaveStrokes、ClearStrokes、切页、RestoreStrokes 等逻辑。
|
||||
/// </summary>
|
||||
/// <param name="isHideNotification">是否隐藏保存成功通知</param>
|
||||
/// <param name="fileName">截图文件名(可选)</param>
|
||||
private void CaptureAndEnqueueScreenshotSave(bool isHideNotification, string fileName = null)
|
||||
{
|
||||
var savePath = Settings.Automation.IsSaveScreenshotsInDateFolders
|
||||
? GetDateFolderPath(fileName)
|
||||
: GetDefaultFolderPath();
|
||||
|
||||
System.Drawing.Bitmap bitmap = null;
|
||||
StrokeCollection strokesToSave = null;
|
||||
int pageIndexForStrokes = 0;
|
||||
string strokeSavePath = null;
|
||||
|
||||
try
|
||||
{
|
||||
bitmap = CaptureScreenshotToBitmap();
|
||||
if (bitmap == null) return;
|
||||
|
||||
if (Settings.Automation.IsAutoSaveStrokesAtScreenshot && inkCanvas.Strokes.Count > 0)
|
||||
{
|
||||
strokesToSave = inkCanvas.Strokes.Clone();
|
||||
pageIndexForStrokes = CurrentWhiteboardIndex;
|
||||
var basePath = Settings.Automation.AutoSavedStrokesLocation
|
||||
+ @"\Auto Saved - BlackBoard Strokes";
|
||||
if (!Directory.Exists(basePath)) Directory.CreateDirectory(basePath);
|
||||
strokeSavePath = Path.Combine(basePath,
|
||||
$"{DateTime.Now:yyyy-MM-dd HH-mm-ss-fff} Page-{pageIndexForStrokes} StrokesCount-{strokesToSave.Count}.icstk");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"CaptureAndEnqueueScreenshotSave 捕获失败: {ex}", LogHelper.LogType.Error);
|
||||
bitmap?.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
var bitmapToSave = bitmap;
|
||||
var path = savePath;
|
||||
var hideNotification = isHideNotification;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (bitmapToSave != null)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!Directory.Exists(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
bitmapToSave.Save(path, ImageFormat.Png);
|
||||
bitmapToSave.Dispose();
|
||||
}
|
||||
|
||||
if (strokesToSave != null && !string.IsNullOrEmpty(strokeSavePath))
|
||||
{
|
||||
using (var fs = new FileStream(strokeSavePath, FileMode.Create))
|
||||
{
|
||||
strokesToSave.Save(fs);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hideNotification && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
Dispatcher.Invoke(() => ShowNotification($"截图成功保存至 {path}"));
|
||||
}
|
||||
|
||||
// 使用上传帮助类上传到所有启用的服务
|
||||
await Helpers.UploadHelper.UploadFileAsync(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"后台保存截图/墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
bitmapToSave?.Dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前屏幕内容捕获为位图(仅内存,不写文件)。调用方或后台任务负责 Dispose。
|
||||
/// </summary>
|
||||
private System.Drawing.Bitmap CaptureScreenshotToBitmap()
|
||||
{
|
||||
var rc = SystemInformation.VirtualScreen;
|
||||
var bitmap = new System.Drawing.Bitmap(rc.Width, rc.Height, PixelFormat.Format32bppArgb);
|
||||
using (var memoryGraphics = Graphics.FromImage(bitmap))
|
||||
{
|
||||
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);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存截图
|
||||
/// </summary>
|
||||
/// <param name="isHideNotification">是否隐藏通知</param>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 根据设置确定保存路径
|
||||
/// 2. 调用CaptureAndSaveScreenshot方法捕获并保存截图
|
||||
/// 3. 如果设置了自动保存墨迹,调用SaveInkCanvasStrokes方法保存墨迹
|
||||
/// </remarks>
|
||||
private void SaveScreenShot(bool isHideNotification, string fileName = null)
|
||||
{
|
||||
var savePath = Settings.Automation.IsSaveScreenshotsInDateFolders
|
||||
@@ -22,6 +136,15 @@ namespace Ink_Canvas
|
||||
SaveInkCanvasStrokes(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存截图到桌面
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 生成桌面路径和文件名
|
||||
/// 2. 调用CaptureAndSaveScreenshot方法捕获并保存截图到桌面
|
||||
/// 3. 如果设置了自动保存墨迹,调用SaveInkCanvasStrokes方法保存墨迹
|
||||
/// </remarks>
|
||||
internal void SaveScreenShotToDesktop()
|
||||
{
|
||||
var desktopPath = Path.Combine(
|
||||
@@ -34,7 +157,177 @@ namespace Ink_Canvas
|
||||
SaveInkCanvasStrokes(false);
|
||||
}
|
||||
|
||||
// 提取公共的截图和保存逻辑
|
||||
internal async Task SaveAreaScreenShotToDesktop()
|
||||
{
|
||||
var originalVisibility = Visibility;
|
||||
try
|
||||
{
|
||||
Visibility = Visibility.Hidden;
|
||||
await Task.Delay(200);
|
||||
|
||||
var screenshotResult = await ShowScreenshotSelector();
|
||||
|
||||
if (!screenshotResult.HasValue)
|
||||
{
|
||||
ShowNotification("截图已取消");
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenshotResult.Value.AddToWhiteboard)
|
||||
{
|
||||
await AddScreenshotToNewWhiteboardPage(screenshotResult.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenshotResult.Value.Area.Width <= 0 || screenshotResult.Value.Area.Height <= 0)
|
||||
{
|
||||
ShowNotification("未选择有效截图区域");
|
||||
return;
|
||||
}
|
||||
|
||||
var desktopPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
|
||||
$"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.png");
|
||||
|
||||
using (var originalBitmap = CaptureScreenArea(screenshotResult.Value.Area))
|
||||
{
|
||||
if (originalBitmap == null)
|
||||
{
|
||||
ShowNotification("截图失败");
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(desktopPath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
finalBitmap.Save(desktopPath, ImageFormat.Png);
|
||||
ShowNotification($"截图成功保存至 {desktopPath}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
|
||||
{
|
||||
finalBitmap.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Settings.Automation.IsAutoSaveStrokesAtScreenshot)
|
||||
SaveInkCanvasStrokes(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowNotification($"截图失败: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Visibility = originalVisibility;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddScreenshotToNewWhiteboardPage(ScreenshotResult screenshotResult)
|
||||
{
|
||||
// 先在当前场景准备截图数据,再进白板,避免误截到白板页面
|
||||
BitmapSource bitmapSourceForClipboard = null;
|
||||
|
||||
// 摄像头截图(BitmapSource)
|
||||
if (screenshotResult.CameraBitmapSource != null)
|
||||
{
|
||||
bitmapSourceForClipboard = screenshotResult.CameraBitmapSource;
|
||||
}
|
||||
// 摄像头截图(Bitmap)
|
||||
else if (screenshotResult.CameraImage != null)
|
||||
{
|
||||
bitmapSourceForClipboard = ConvertBitmapToBitmapSource(screenshotResult.CameraImage);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (screenshotResult.Area.Width <= 0 || screenshotResult.Area.Height <= 0)
|
||||
{
|
||||
ShowNotification("未选择有效截图区域");
|
||||
return;
|
||||
}
|
||||
|
||||
using (var originalBitmap = CaptureScreenArea(screenshotResult.Area))
|
||||
{
|
||||
if (originalBitmap == null)
|
||||
{
|
||||
ShowNotification("截图失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap finalBitmap = originalBitmap;
|
||||
bool needDisposeFinalBitmap = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (screenshotResult.Path != null && screenshotResult.Path.Count > 0)
|
||||
{
|
||||
finalBitmap = ApplyShapeMask(originalBitmap, screenshotResult.Path, screenshotResult.Area);
|
||||
needDisposeFinalBitmap = true;
|
||||
}
|
||||
|
||||
bitmapSourceForClipboard = ConvertBitmapToBitmapSource(finalBitmap);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
|
||||
{
|
||||
finalBitmap.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmapSourceForClipboard == null)
|
||||
{
|
||||
ShowNotification("截图转换失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 图像已拷贝到内存后再进入白板
|
||||
bitmapSourceForClipboard.Freeze();
|
||||
|
||||
if (currentMode != 1)
|
||||
{
|
||||
SwitchToBoardMode();
|
||||
await Task.Delay(150);
|
||||
}
|
||||
|
||||
BtnWhiteBoardAdd_Click(null, EventArgs.Empty);
|
||||
|
||||
await InsertBitmapSourceToCanvas(bitmapSourceForClipboard);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提取公共的截图和保存逻辑
|
||||
/// </summary>
|
||||
/// <param name="savePath">保存路径</param>
|
||||
/// <param name="isHideNotification">是否隐藏通知</param>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 获取虚拟屏幕边界
|
||||
/// 2. 创建位图并设置高质量渲染
|
||||
/// 3. 从屏幕复制内容到位图
|
||||
/// 4. 确保保存目录存在
|
||||
/// 5. 保存为PNG格式
|
||||
/// 6. 如果不隐藏通知,显示保存成功通知
|
||||
/// 7. 异步上传截图到Dlass
|
||||
/// </remarks>
|
||||
private void CaptureAndSaveScreenshot(string savePath, bool isHideNotification)
|
||||
{
|
||||
var rc = SystemInformation.VirtualScreen;
|
||||
@@ -63,11 +356,38 @@ namespace Ink_Canvas
|
||||
|
||||
if (!isHideNotification)
|
||||
{
|
||||
ShowNotification($"截图成功保存至 {savePath}");
|
||||
Task.Delay(100).ContinueWith(t =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowNotification($"截图成功保存至 {savePath}");
|
||||
});
|
||||
});
|
||||
}
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用上传帮助类上传到所有启用的服务
|
||||
await Helpers.UploadHelper.UploadFileAsync(savePath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取日期文件夹路径
|
||||
/// <summary>
|
||||
/// 获取日期文件夹路径
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <returns>日期文件夹路径</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 如果文件名为空,使用当前时间作为文件名
|
||||
/// 2. 获取基础路径和日期文件夹名
|
||||
/// 3. 组合路径并返回
|
||||
/// </remarks>
|
||||
private string GetDateFolderPath(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
@@ -85,7 +405,17 @@ namespace Ink_Canvas
|
||||
$"{fileName}.png");
|
||||
}
|
||||
|
||||
// 获取默认文件夹路径
|
||||
/// <summary>
|
||||
/// 获取默认文件夹路径
|
||||
/// </summary>
|
||||
/// <returns>默认文件夹路径</returns>
|
||||
/// <remarks>
|
||||
/// 该方法会:
|
||||
/// 1. 获取基础路径
|
||||
/// 2. 组合截图文件夹路径
|
||||
/// 3. 确保截图文件夹存在
|
||||
/// 4. 生成文件名并组合完整路径返回
|
||||
/// </remarks>
|
||||
private string GetDefaultFolderPath()
|
||||
{
|
||||
var basePath = Settings.Automation.AutoSavedStrokesLocation;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using iNKORE.UI.WPF.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -17,8 +17,20 @@ namespace Ink_Canvas
|
||||
{
|
||||
#region Floating Control
|
||||
|
||||
/// <summary>
|
||||
/// 存储最后一次鼠标按下的边界对象
|
||||
/// </summary>
|
||||
private object lastBorderMouseDownObject;
|
||||
|
||||
/// <summary>
|
||||
/// 处理边界鼠标按下事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 如果发送者是 RandomDrawPanel 或 SingleDrawPanel,且它们被隐藏,则不处理事件
|
||||
/// 否则存储当前鼠标按下的对象
|
||||
/// </remarks>
|
||||
private void Border_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 如果发送者是 RandomDrawPanel 或 SingleDrawPanel,且它们被隐藏,则不处理事件
|
||||
@@ -34,7 +46,15 @@ namespace Ink_Canvas
|
||||
lastBorderMouseDownObject = sender;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 处理墨迹选择克隆鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 执行墨迹克隆操作并记录日志
|
||||
/// </remarks>
|
||||
private void BorderStrokeSelectionClone_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
@@ -55,6 +75,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理墨迹选择克隆到新画板鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 克隆选中的墨迹到新画板并清除当前选择
|
||||
/// </remarks>
|
||||
private void BorderStrokeSelectionCloneToNewBoard_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
@@ -64,24 +93,60 @@ namespace Ink_Canvas
|
||||
CloneStrokesToNewBoard(strokes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理墨迹选择删除鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 调用 SymbolIconDelete_MouseUp 方法执行删除操作
|
||||
/// </remarks>
|
||||
private void BorderStrokeSelectionDelete_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
SymbolIconDelete_MouseUp(sender, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理笔宽减小鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 调用 ChangeStrokeThickness 方法减小笔宽
|
||||
/// </remarks>
|
||||
private void GridPenWidthDecrease_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
ChangeStrokeThickness(0.8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理笔宽增大鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 调用 ChangeStrokeThickness 方法增大笔宽
|
||||
/// </remarks>
|
||||
private void GridPenWidthIncrease_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
ChangeStrokeThickness(1.25);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更改选中墨迹的粗细
|
||||
/// </summary>
|
||||
/// <param name="multipler">缩放倍数</param>
|
||||
/// <remarks>
|
||||
/// 对选中的每个墨迹应用缩放倍数
|
||||
/// 确保新的粗细在允许的范围内
|
||||
/// 如果有 DrawingAttributesHistory,则提交历史记录
|
||||
/// </remarks>
|
||||
private void ChangeStrokeThickness(double multipler)
|
||||
{
|
||||
foreach (var stroke in inkCanvas.GetSelectedStrokes())
|
||||
@@ -107,6 +172,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理笔宽恢复默认鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 将选中墨迹的粗细恢复为默认值
|
||||
/// </remarks>
|
||||
private void GridPenWidthRestore_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
@@ -118,6 +192,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理水平翻转鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 对选中的墨迹应用水平翻转变换
|
||||
/// 如果有 DrawingAttributesHistory,则提交历史记录
|
||||
/// </remarks>
|
||||
private void ImageFlipHorizontal_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
@@ -155,6 +239,16 @@ namespace Ink_Canvas
|
||||
//updateBorderStrokeSelectionControlLocation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理垂直翻转鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 对选中的墨迹应用垂直翻转变换
|
||||
/// 如果有 DrawingAttributesHistory,则提交历史记录
|
||||
/// </remarks>
|
||||
private void ImageFlipVertical_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
@@ -186,6 +280,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
// ... existing code ...
|
||||
/// <summary>
|
||||
/// 处理顺时针旋转45度鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 对选中的墨迹应用45度旋转变换
|
||||
/// 如果有 DrawingAttributesHistory,则提交历史记录
|
||||
/// </remarks>
|
||||
private void ImageRotate45_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
@@ -216,6 +320,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理顺时针旋转90度鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 只有当鼠标按下和释放的是同一个对象时才处理
|
||||
/// 对选中的墨迹应用90度旋转变换
|
||||
/// 如果有 DrawingAttributesHistory,则提交历史记录
|
||||
/// </remarks>
|
||||
private void ImageRotate90_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
@@ -254,17 +368,51 @@ namespace Ink_Canvas
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 墨迹选择覆盖层鼠标按下状态
|
||||
/// </summary>
|
||||
private bool isGridInkCanvasSelectionCoverMouseDown;
|
||||
/// <summary>
|
||||
/// 墨迹拖动状态
|
||||
/// </summary>
|
||||
private bool isStrokeDragging = false;
|
||||
/// <summary>
|
||||
/// 墨迹拖动起始点
|
||||
/// </summary>
|
||||
private Point strokeDragStartPoint;
|
||||
/// <summary>
|
||||
/// 墨迹选择克隆集合
|
||||
/// </summary>
|
||||
private StrokeCollection StrokesSelectionClone = new StrokeCollection();
|
||||
|
||||
// 选择框和选择点相关变量
|
||||
/// <summary>
|
||||
/// 调整大小状态
|
||||
/// </summary>
|
||||
private bool isResizing = false;
|
||||
/// <summary>
|
||||
/// 当前调整把手
|
||||
/// </summary>
|
||||
private string currentResizeHandle = "";
|
||||
/// <summary>
|
||||
/// 调整起始点
|
||||
/// </summary>
|
||||
private Point resizeStartPoint;
|
||||
/// <summary>
|
||||
/// 原始选择边界
|
||||
/// </summary>
|
||||
private Rect originalSelectionBounds;
|
||||
|
||||
/// <summary>
|
||||
/// 处理墨迹选择覆盖层鼠标按下事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 如果有选中的墨迹,检查点击位置是否在选择框边界内
|
||||
/// 如果在边界内,开始拖动墨迹
|
||||
/// 如果在边界外,取消选择
|
||||
/// </remarks>
|
||||
private void GridInkCanvasSelectionCover_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
isGridInkCanvasSelectionCoverMouseDown = true;
|
||||
@@ -297,6 +445,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理墨迹选择覆盖层鼠标移动事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标事件参数</param>
|
||||
/// <remarks>
|
||||
/// 如果正在拖动墨迹,执行拖动操作
|
||||
/// 如果鼠标在选中区域移动,更新墨迹选中栏位置
|
||||
/// </remarks>
|
||||
private void GridInkCanvasSelectionCover_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!isGridInkCanvasSelectionCoverMouseDown) return;
|
||||
@@ -331,6 +488,15 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理墨迹选择覆盖层鼠标释放事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标按钮事件参数</param>
|
||||
/// <remarks>
|
||||
/// 结束墨迹拖动
|
||||
/// 只有在没有选中墨迹时才隐藏选中栏
|
||||
/// </remarks>
|
||||
private void GridInkCanvasSelectionCover_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isGridInkCanvasSelectionCoverMouseDown) return;
|
||||
@@ -352,9 +518,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理选择按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 如果当前是选择模式,检查是否全选
|
||||
/// 如果全选,则切换到墨迹模式再切换回选择模式
|
||||
/// 如果不是全选,则选择所有有效墨迹
|
||||
/// 如果当前不是选择模式,则切换到选择模式
|
||||
/// </remarks>
|
||||
private void BtnSelect_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExitMultiTouchModeIfNeeded();
|
||||
forceEraser = true;
|
||||
drawingShapeMode = 0;
|
||||
inkCanvas.IsManipulationEnabled = false;
|
||||
@@ -382,10 +558,30 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 墨迹选择控件宽度
|
||||
/// </summary>
|
||||
private double BorderStrokeSelectionControlWidth = 490.0;
|
||||
/// <summary>
|
||||
/// 墨迹选择控件高度
|
||||
/// </summary>
|
||||
private double BorderStrokeSelectionControlHeight = 80.0;
|
||||
/// <summary>
|
||||
/// 程序更改墨迹选择状态
|
||||
/// </summary>
|
||||
private bool isProgramChangeStrokeSelection;
|
||||
|
||||
/// <summary>
|
||||
/// 处理墨迹画布选择更改事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// 优先检查墨迹选择状态
|
||||
/// 如果有墨迹被选中,显示墨迹选择栏和选择框
|
||||
/// 如果有图片元素被选中,不显示选择框
|
||||
/// 如果没有选中任何内容,隐藏选择框
|
||||
/// </remarks>
|
||||
private void inkCanvas_SelectionChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (isProgramChangeStrokeSelection) return;
|
||||
@@ -439,6 +635,14 @@ namespace Ink_Canvas
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 更新墨迹选中栏位置
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 计算墨迹选中栏的位置,确保在墨迹下方显示
|
||||
/// 如果选中栏会超出屏幕底部,则显示在墨迹上方
|
||||
/// 如果上方也没有空间,则显示在顶部
|
||||
/// </remarks>
|
||||
private void updateBorderStrokeSelectionControlLocation()
|
||||
{
|
||||
var borderLeft = (inkCanvas.GetSelectionBounds().Left + inkCanvas.GetSelectionBounds().Right -
|
||||
@@ -462,11 +666,28 @@ namespace Ink_Canvas
|
||||
BorderStrokeSelectionControl.Margin = new Thickness(borderLeft, borderTop, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理墨迹选择覆盖层操作开始事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">操作开始事件参数</param>
|
||||
/// <remarks>
|
||||
/// 设置操作模式为所有模式
|
||||
/// </remarks>
|
||||
private void GridInkCanvasSelectionCover_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
|
||||
{
|
||||
e.Mode = ManipulationModes.All;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理墨迹选择覆盖层操作完成事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">操作完成事件参数</param>
|
||||
/// <remarks>
|
||||
/// 如果有 StrokeManipulationHistory,则提交历史记录
|
||||
/// 如果有 DrawingAttributesHistory,则提交历史记录
|
||||
/// </remarks>
|
||||
private void GridInkCanvasSelectionCover_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
|
||||
{
|
||||
if (StrokeManipulationHistory?.Count > 0)
|
||||
@@ -491,12 +712,29 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理选择覆盖层的操作增量事件,将来自触摸或操作的平移、缩放和旋转应用到当前选中的墨迹上并更新选择控件位置。
|
||||
/// </summary>
|
||||
/// <param name="sender">触发事件的元素(通常为选择覆盖层)。</param>
|
||||
/// <param name="e">包含平移、缩放和旋转增量的 ManipulationDeltaEventArgs。</param>
|
||||
/// <remarks>
|
||||
/// - 当只有单指触摸且已有选中墨迹时,不在此处处理拖动(由 TouchMove 处理)。
|
||||
/// - 三指及以上触摸时禁用缩放。
|
||||
/// - 若 StrokesSelectionClone 非空,则对其内的墨迹应用变换;否则在允许两指旋转时也会应用旋转变换。
|
||||
/// - 处理完成后会刷新并更新边框/选择控件的位置。
|
||||
/// </remarks>
|
||||
private void GridInkCanvasSelectionCover_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (dec.Count >= 1)
|
||||
{
|
||||
// 单指时,让TouchMove处理拖动
|
||||
if (dec.Count == 1 && inkCanvas.GetSelectedStrokes().Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool disableScale = dec.Count >= 3;
|
||||
var md = e.DeltaManipulation;
|
||||
var trans = md.Translation; // 获得位移矢量
|
||||
@@ -538,89 +776,12 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理选区覆盖层的触摸按下事件:记录触摸设备 ID,并在第一个触点时保存选择中心与用于拖拽的初始触点位置。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源(触摸事件的发送者)。</param>
|
||||
/// <param name="e">触摸事件参数,包含触点位置与设备 ID。</param>
|
||||
private void GridInkCanvasSelectionCover_TouchDown(object sender, TouchEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
private void GridInkCanvasSelectionCover_TouchUp(object sender, TouchEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
private void GridInkCanvasSelectionCover_TouchMove(object sender, TouchEventArgs e)
|
||||
{
|
||||
// 处理触摸移动事件 - 用于拖动选中的墨迹
|
||||
if (inkCanvas.GetSelectedStrokes().Count > 0 && dec.Count == 1)
|
||||
{
|
||||
var currentTouchPoint = e.GetTouchPoint(inkCanvas).Position;
|
||||
|
||||
// 检查是否有有效的起始触摸点
|
||||
if (lastTouchPointOnGridInkCanvasCover != new Point(0, 0))
|
||||
{
|
||||
var delta = currentTouchPoint - lastTouchPointOnGridInkCanvasCover;
|
||||
|
||||
// 只有当移动距离足够大时才进行拖动(避免微小移动造成的抖动)
|
||||
if (Math.Abs(delta.X) > 1 || Math.Abs(delta.Y) > 1)
|
||||
{
|
||||
// 创建变换矩阵
|
||||
var matrix = new Matrix();
|
||||
matrix.Translate(delta.X, delta.Y);
|
||||
|
||||
// 对选中的墨迹应用变换
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
|
||||
// 更新选中栏位置
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
|
||||
// 更新最后触摸点
|
||||
lastTouchPointOnGridInkCanvasCover = currentTouchPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GridInkCanvasSelectionCover_PreviewTouchMove(object sender, TouchEventArgs e)
|
||||
{
|
||||
// 预览触摸移动事件 - 用于更精确的触摸处理
|
||||
if (inkCanvas.GetSelectedStrokes().Count > 0 && dec.Count == 1)
|
||||
{
|
||||
var currentTouchPoint = e.GetTouchPoint(inkCanvas).Position;
|
||||
|
||||
// 检查是否有有效的起始触摸点
|
||||
if (lastTouchPointOnGridInkCanvasCover != new Point(0, 0))
|
||||
{
|
||||
var delta = currentTouchPoint - lastTouchPointOnGridInkCanvasCover;
|
||||
|
||||
// 只有当移动距离足够大时才进行拖动(避免微小移动造成的抖动)
|
||||
if (Math.Abs(delta.X) > 1 || Math.Abs(delta.Y) > 1)
|
||||
{
|
||||
// 创建变换矩阵
|
||||
var matrix = new Matrix();
|
||||
matrix.Translate(delta.X, delta.Y);
|
||||
|
||||
// 对选中的墨迹应用变换
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
|
||||
// 更新选中栏位置
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
|
||||
// 更新最后触摸点
|
||||
lastTouchPointOnGridInkCanvasCover = currentTouchPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Point lastTouchPointOnGridInkCanvasCover = new Point(0, 0);
|
||||
|
||||
private void GridInkCanvasSelectionCover_PreviewTouchDown(object sender, TouchEventArgs e)
|
||||
{
|
||||
dec.Add(e.TouchDevice.Id);
|
||||
//设备1个的时候,记录中心点
|
||||
@@ -628,70 +789,43 @@ namespace Ink_Canvas
|
||||
{
|
||||
var touchPoint = e.GetTouchPoint(null);
|
||||
centerPoint = touchPoint.Position;
|
||||
lastTouchPointOnGridInkCanvasCover = e.GetTouchPoint(inkCanvas).Position;
|
||||
lastTouchPointOnGridInkCanvasCover = touchPoint.Position;
|
||||
|
||||
// 检查是否有选中的墨迹
|
||||
if (inkCanvas.GetSelectedStrokes().Count > 0)
|
||||
{
|
||||
// 获取触摸点位置
|
||||
var touchPosition = e.GetTouchPoint(inkCanvas).Position;
|
||||
var selectionBounds = inkCanvas.GetSelectionBounds();
|
||||
|
||||
// 检查触摸位置是否在选择框边界内
|
||||
if (touchPosition.X >= selectionBounds.Left &&
|
||||
touchPosition.X <= selectionBounds.Right &&
|
||||
touchPosition.Y >= selectionBounds.Top &&
|
||||
touchPosition.Y <= selectionBounds.Bottom)
|
||||
{
|
||||
// 只有在选择框边界内才允许拖动
|
||||
// 触摸拖动状态已通过TouchMove事件处理
|
||||
}
|
||||
else
|
||||
{
|
||||
// 触摸在选择框外,取消选择
|
||||
inkCanvas.Select(new StrokeCollection());
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SetCurrentToolMode(InkCanvasEditingMode.Select);
|
||||
inkCanvas.Select(new StrokeCollection());
|
||||
var touchPointInCanvas = e.GetTouchPoint(inkCanvas);
|
||||
lastDragPointInCanvas = touchPointInCanvas.Position;
|
||||
}
|
||||
}
|
||||
|
||||
private void GridInkCanvasSelectionCover_PreviewTouchUp(object sender, TouchEventArgs e)
|
||||
/// <summary>
|
||||
/// 处理选区覆盖层的触摸结束事件:更新触摸跟踪状态并根据触摸位置和当前选区状态显示或隐藏选区覆盖层与克隆集合。
|
||||
/// </summary>
|
||||
/// <param name="sender">触发事件的对象(通常为选区覆盖层)。</param>
|
||||
/// <param name="e">触摸事件参数,包含触点信息。</param>
|
||||
private void GridInkCanvasSelectionCover_TouchUp(object sender, TouchEventArgs e)
|
||||
{
|
||||
dec.Remove(e.TouchDevice.Id);
|
||||
if (dec.Count >= 1) return;
|
||||
|
||||
// 重置触摸状态
|
||||
lastTouchPointOnGridInkCanvasCover = new Point(0, 0);
|
||||
isProgramChangeStrokeSelection = false;
|
||||
|
||||
// 检查是否有点击(没有移动)
|
||||
var currentTouchPoint = e.GetTouchPoint(null).Position;
|
||||
if (Math.Abs(currentTouchPoint.X - centerPoint.X) < 5 && Math.Abs(currentTouchPoint.Y - centerPoint.Y) < 5)
|
||||
{
|
||||
// 点击在选择框内,保持选择状态
|
||||
if (inkCanvas.GetSelectedStrokes().Count > 0)
|
||||
{
|
||||
var selectionBounds = inkCanvas.GetSelectionBounds();
|
||||
if (currentTouchPoint.X >= selectionBounds.Left &&
|
||||
currentTouchPoint.X <= selectionBounds.Right &&
|
||||
currentTouchPoint.Y >= selectionBounds.Top &&
|
||||
currentTouchPoint.Y <= selectionBounds.Bottom)
|
||||
{
|
||||
// 点击在选择框内,保持选择
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Visible;
|
||||
StrokesSelectionClone = new StrokeCollection();
|
||||
return;
|
||||
}
|
||||
}
|
||||
lastDragPointInCanvas = new Point(0, 0);
|
||||
|
||||
// 点击在选择框外,取消选择
|
||||
var touchUpPoint = e.GetTouchPoint(null).Position;
|
||||
if (lastTouchPointOnGridInkCanvasCover == touchUpPoint)
|
||||
{
|
||||
var touchPointInCanvas = e.GetTouchPoint(inkCanvas).Position;
|
||||
var selectionBounds = inkCanvas.GetSelectionBounds();
|
||||
|
||||
if (!(touchPointInCanvas.X < selectionBounds.Left) &&
|
||||
!(touchPointInCanvas.Y < selectionBounds.Top) &&
|
||||
!(touchPointInCanvas.X > selectionBounds.Right) &&
|
||||
!(touchPointInCanvas.Y > selectionBounds.Bottom))
|
||||
{
|
||||
return;
|
||||
}
|
||||
isProgramChangeStrokeSelection = true;
|
||||
inkCanvas.Select(new StrokeCollection());
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
isProgramChangeStrokeSelection = false;
|
||||
StrokesSelectionClone = new StrokeCollection();
|
||||
}
|
||||
else if (inkCanvas.GetSelectedStrokes().Count == 0)
|
||||
@@ -704,12 +838,59 @@ namespace Ink_Canvas
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Visible;
|
||||
StrokesSelectionClone = new StrokeCollection();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理触摸移动事件,按单指拖动并平移当前选中的墨迹,同时更新选择控件的位置。
|
||||
/// </summary>
|
||||
/// <remarks>仅在存在选中墨迹且当前仅有一个触摸点时生效;若起始触摸点未记录(lastDragPointInCanvas 为 (0,0))则不移动。只有当触摸位移在任一方向超过 1 像素时,才对每条选中墨迹应用平移变换,并更新选择控件位置与最后触摸点。</remarks>
|
||||
private void GridInkCanvasSelectionCover_TouchMove(object sender, TouchEventArgs e)
|
||||
{
|
||||
// 处理触摸移动事件 - 用于拖动选中的墨迹
|
||||
if (inkCanvas.GetSelectedStrokes().Count > 0 && dec.Count == 1)
|
||||
{
|
||||
var currentTouchPoint = e.GetTouchPoint(inkCanvas).Position;
|
||||
|
||||
// 检查是否有有效的起始触摸点
|
||||
if (lastDragPointInCanvas != new Point(0, 0))
|
||||
{
|
||||
var delta = currentTouchPoint - lastDragPointInCanvas;
|
||||
|
||||
// 只有当移动距离足够大时才进行拖动(避免微小移动造成的抖动)
|
||||
if (Math.Abs(delta.X) > 1 || Math.Abs(delta.Y) > 1)
|
||||
{
|
||||
// 创建变换矩阵
|
||||
var matrix = new Matrix();
|
||||
matrix.Translate(delta.X, delta.Y);
|
||||
|
||||
// 对选中的墨迹应用变换
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
|
||||
// 更新选中栏位置
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
|
||||
// 更新最后触摸点
|
||||
lastDragPointInCanvas = currentTouchPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Point lastTouchPointOnGridInkCanvasCover = new Point(0, 0);
|
||||
private Point lastDragPointInCanvas = new Point(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 切换到选择(套索)工具模式并同步光标显示。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 同时取消强制橡皮擦和点式橡皮擦状态,并将绘制形状模式重置为默认(0)。
|
||||
/// </remarks>
|
||||
private void LassoSelect_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExitMultiTouchModeIfNeeded();
|
||||
forceEraser = false;
|
||||
forcePointEraser = false;
|
||||
drawingShapeMode = 0;
|
||||
@@ -718,9 +899,18 @@ namespace Ink_Canvas
|
||||
SetCursorBasedOnEditingMode(inkCanvas);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理套索选择按钮点击事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 设置工具模式为选择模式
|
||||
/// 启用墨迹画布的操作支持
|
||||
/// 设置光标为选择模式光标
|
||||
/// </remarks>
|
||||
private void BtnLassoSelect_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExitMultiTouchModeIfNeeded();
|
||||
forceEraser = false;
|
||||
forcePointEraser = false;
|
||||
drawingShapeMode = 0;
|
||||
@@ -732,6 +922,17 @@ namespace Ink_Canvas
|
||||
|
||||
#region UIElement Selection and Resize
|
||||
|
||||
/// <summary>
|
||||
/// 获取UI元素的边界
|
||||
/// </summary>
|
||||
/// <param name="element">UI元素</param>
|
||||
/// <returns>UI元素的边界矩形</returns>
|
||||
/// <remarks>
|
||||
/// 如果元素是FrameworkElement,获取其位置和大小
|
||||
/// 如果元素有RenderTransform,尝试使用变换后的边界
|
||||
/// 如果变换失败,回退到简单计算
|
||||
/// 如果元素不是FrameworkElement,返回空矩形
|
||||
/// </remarks>
|
||||
private Rect GetUIElementBounds(UIElement element)
|
||||
{
|
||||
if (element is FrameworkElement fe)
|
||||
@@ -774,6 +975,12 @@ namespace Ink_Canvas
|
||||
|
||||
#region Selection Display and Resize Handles
|
||||
|
||||
/// <summary>
|
||||
/// 在画布上显示当前选中墨迹的可视选择框与调整控件(如有选中墨迹则显示,否则隐藏)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 如果没有选中任何墨迹,隐藏选择显示;否则计算选区边界,将选择框在所有方向各扩展 8 像素,设置选择框的位置与尺寸,并更新调整句柄的位置后显示句柄画布。
|
||||
/// </remarks>
|
||||
private void UpdateSelectionDisplay()
|
||||
{
|
||||
if (inkCanvas.GetSelectedStrokes().Count == 0)
|
||||
@@ -784,38 +991,59 @@ namespace Ink_Canvas
|
||||
|
||||
var selectionBounds = inkCanvas.GetSelectionBounds();
|
||||
|
||||
// 更新选择框
|
||||
// 向外扩展8像素
|
||||
double expandOffset = 8;
|
||||
|
||||
// 更新选择框,向外扩展8像素
|
||||
SelectionRectangle.Visibility = Visibility.Visible;
|
||||
SelectionRectangle.Margin = new Thickness(selectionBounds.Left, selectionBounds.Top, 0, 0);
|
||||
SelectionRectangle.Width = selectionBounds.Width;
|
||||
SelectionRectangle.Height = selectionBounds.Height;
|
||||
SelectionRectangle.Margin = new Thickness(selectionBounds.Left - expandOffset, selectionBounds.Top - expandOffset, 0, 0);
|
||||
SelectionRectangle.Width = selectionBounds.Width + expandOffset * 2;
|
||||
SelectionRectangle.Height = selectionBounds.Height + expandOffset * 2;
|
||||
|
||||
// 更新选择点位置
|
||||
UpdateSelectionHandles(selectionBounds);
|
||||
SelectionHandlesCanvas.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏选择显示
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 隐藏选择矩形和选择把手画布
|
||||
/// </remarks>
|
||||
private void HideSelectionDisplay()
|
||||
{
|
||||
SelectionRectangle.Visibility = Visibility.Collapsed;
|
||||
SelectionHandlesCanvas.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据给定的选择边界定位并设置八个缩放/移动把手的位置,四个把手在各边外扩展 8 像素,四个角把手完全位于外部。
|
||||
/// </summary>
|
||||
/// <param name="bounds">当前选区在画布坐标系中的边界矩形(未包含用于显示的 8 像素外扩展)。</param>
|
||||
private void UpdateSelectionHandles(Rect bounds)
|
||||
{
|
||||
// 四个角选择点
|
||||
TopLeftHandle.Margin = new Thickness(bounds.Left - 4, bounds.Top - 4, 0, 0);
|
||||
TopRightHandle.Margin = new Thickness(bounds.Right - 4, bounds.Top - 4, 0, 0);
|
||||
BottomLeftHandle.Margin = new Thickness(bounds.Left - 4, bounds.Bottom - 4, 0, 0);
|
||||
BottomRightHandle.Margin = new Thickness(bounds.Right - 4, bounds.Bottom - 4, 0, 0);
|
||||
// 四个边选择点,向外扩展8像素
|
||||
TopHandle.Margin = new Thickness(bounds.Left + bounds.Width / 2 - 4, bounds.Top - 12, 0, 0);
|
||||
BottomHandle.Margin = new Thickness(bounds.Left + bounds.Width / 2 - 4, bounds.Bottom + 4, 0, 0);
|
||||
LeftHandle.Margin = new Thickness(bounds.Left - 12, bounds.Top + bounds.Height / 2 - 4, 0, 0);
|
||||
RightHandle.Margin = new Thickness(bounds.Right + 4, bounds.Top + bounds.Height / 2 - 4, 0, 0);
|
||||
|
||||
// 四个边选择点
|
||||
TopHandle.Margin = new Thickness(bounds.Left + bounds.Width / 2 - 4, bounds.Top - 4, 0, 0);
|
||||
BottomHandle.Margin = new Thickness(bounds.Left + bounds.Width / 2 - 4, bounds.Bottom - 4, 0, 0);
|
||||
LeftHandle.Margin = new Thickness(bounds.Left - 4, bounds.Top + bounds.Height / 2 - 4, 0, 0);
|
||||
RightHandle.Margin = new Thickness(bounds.Right - 4, bounds.Top + bounds.Height / 2 - 4, 0, 0);
|
||||
// 四个角选择点,完全位于选择框外部
|
||||
TopLeftHandle.Margin = new Thickness(bounds.Left - 12, bounds.Top - 12, 0, 0);
|
||||
TopRightHandle.Margin = new Thickness(bounds.Right + 4, bounds.Top - 12, 0, 0);
|
||||
BottomLeftHandle.Margin = new Thickness(bounds.Left - 12, bounds.Bottom + 4, 0, 0);
|
||||
BottomRightHandle.Margin = new Thickness(bounds.Right + 4, bounds.Bottom + 4, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在用户按下选择框的缩放把手时开始缩放操作。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 记录缩放开始位置和当前选区的初始边界,设置当前活动把手并捕获鼠标以便后续移动/释放事件处理。
|
||||
/// </remarks>
|
||||
/// <param name="sender">触发事件的缩放把手,预期为一个 Rectangle。</param>
|
||||
/// <param name="e">包含鼠标按下事件的位置信息和处理标志的 <see cref="MouseButtonEventArgs"/> 实例。</param>
|
||||
private void SelectionHandle_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Rectangle handle)
|
||||
@@ -829,6 +1057,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理选择把手鼠标移动事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">鼠标事件参数</param>
|
||||
/// <remarks>
|
||||
/// 如果正在调整大小,计算新的边界
|
||||
/// 应用新的边界到选中的墨迹
|
||||
/// 更新选择框显示
|
||||
/// </remarks>
|
||||
private void SelectionHandle_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!isResizing || !(sender is Rectangle handle)) return;
|
||||
@@ -845,6 +1083,11 @@ namespace Ink_Canvas
|
||||
UpdateSelectionDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在选择框的大小调整句柄上释放鼠标时结束调整操作、释放句柄的鼠标捕获并将事件标记为已处理。
|
||||
/// </summary>
|
||||
/// <param name="sender">触发事件的对象,应为表示调整句柄的 <see cref="System.Windows.Shapes.Rectangle"/>。</param>
|
||||
/// <param name="e">鼠标事件参数;在处理后该事件会被标记为已处理。</param>
|
||||
private void SelectionHandle_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Rectangle handle)
|
||||
@@ -856,6 +1099,71 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在触摸按下选择调整句柄时开始调整操作并记录初始状态(活动句柄、起始点和当前选区边界)。
|
||||
/// </summary>
|
||||
/// <param name="sender">触发事件的调整句柄(应为 Rectangle)。</param>
|
||||
/// <param name="e">触摸事件数据;方法会标记事件为已处理并使用其触点相对于 inkCanvas 的位置。</param>
|
||||
private void SelectionHandle_TouchDown(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (sender is Rectangle handle)
|
||||
{
|
||||
isResizing = true;
|
||||
currentResizeHandle = handle.Name;
|
||||
var touchPoint = e.GetTouchPoint(inkCanvas);
|
||||
resizeStartPoint = touchPoint.Position;
|
||||
originalSelectionBounds = inkCanvas.GetSelectionBounds();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在触摸移动时根据拖动更新选区的边界并将其应用到当前所选的墨迹。
|
||||
/// </summary>
|
||||
/// <param name="sender">触发事件的调整句柄(应为一个 Rectangle)。</param>
|
||||
/// <param name="e">包含触摸位置和状态的事件参数。</param>
|
||||
/// <remarks>计算新的选择边界、将其应用到所选墨迹并刷新选择显示;将事件标记为已处理。</remarks>
|
||||
private void SelectionHandle_TouchMove(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (!isResizing || !(sender is Rectangle handle)) return;
|
||||
|
||||
var touchPoint = e.GetTouchPoint(inkCanvas);
|
||||
var currentPoint = touchPoint.Position;
|
||||
var delta = new Point(currentPoint.X - resizeStartPoint.X, currentPoint.Y - resizeStartPoint.Y);
|
||||
|
||||
var newBounds = CalculateNewBounds(originalSelectionBounds, delta, currentResizeHandle);
|
||||
|
||||
// 应用新的边界到选中的墨迹
|
||||
ApplyBoundsToStrokes(newBounds);
|
||||
|
||||
// 更新选择框显示
|
||||
UpdateSelectionDisplay();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结束触摸对选择框调整大小的交互,重置调整状态并标记事件已处理。
|
||||
/// </summary>
|
||||
/// <param name="sender">触发事件的调整句柄(Rectangle)。</param>
|
||||
/// <param name="e">触摸事件数据;方法会将其标记为已处理。</param>
|
||||
private void SelectionHandle_TouchUp(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (sender is Rectangle handle)
|
||||
{
|
||||
isResizing = false;
|
||||
currentResizeHandle = "";
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据指定的拖动增量和所操作的调整柄,计算并返回调整后的边界矩形。
|
||||
/// </summary>
|
||||
/// <param name="originalBounds">调整前的原始边界矩形。</param>
|
||||
/// <param name="delta">从初始位置到当前拖动位置的偏移量,用于更新对应边或角的位置和尺寸。</param>
|
||||
/// <param name="handleName">被拖动的调整柄的名称,支持的值:`TopLeftHandle`、`TopRightHandle`、`BottomLeftHandle`、`BottomRightHandle`、`TopHandle`、`BottomHandle`、`LeftHandle`、`RightHandle`。</param>
|
||||
/// <returns>应用偏移并强制最小宽高限制(10×10)后的新的边界矩形。</returns>
|
||||
private Rect CalculateNewBounds(Rect originalBounds, Point delta, string handleName)
|
||||
{
|
||||
var newBounds = originalBounds;
|
||||
@@ -912,6 +1220,15 @@ namespace Ink_Canvas
|
||||
return newBounds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用新的边界到选中的墨迹
|
||||
/// </summary>
|
||||
/// <param name="newBounds">新的边界矩形</param>
|
||||
/// <remarks>
|
||||
/// 计算缩放比例和平移量
|
||||
/// 创建变换矩阵
|
||||
/// 应用变换到选中的墨迹
|
||||
/// </remarks>
|
||||
private void ApplyBoundsToStrokes(Rect newBounds)
|
||||
{
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
@@ -942,4 +1259,3 @@ namespace Ink_Canvas
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,10 @@
|
||||
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;
|
||||
@@ -19,7 +21,21 @@ namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private void LoadSettings(bool isStartup = false)
|
||||
/// <summary>
|
||||
/// 从配置文件加载用户设置并将其应用到主窗口和相关控件的状态(包括启动、外观、画布、手势、PPT、自动化等各项配置)。
|
||||
/// </summary>
|
||||
/// <param name="isStartup">指示当前为应用启动阶段;为 true 时按启动流程应用启动相关设置(例如触发启动专用动作和启动时的行为)。</param>
|
||||
/// <summary>
|
||||
/// 从当前配置文件重新加载设置并应用到界面(热重载),不触发启动逻辑与自动更新检查。
|
||||
/// 用于配置文件切换后立即生效。
|
||||
/// </summary>
|
||||
public void ReloadSettingsFromFile()
|
||||
{
|
||||
LoadSettings(false, skipAutoUpdateCheck: true);
|
||||
}
|
||||
|
||||
/// <param name="skipAutoUpdateCheck">指示是否跳过自动更新检查;为 true 时不会在加载设置后执行自动更新检测。</param>
|
||||
private void LoadSettings(bool isStartup = false, bool skipAutoUpdateCheck = false)
|
||||
{
|
||||
AppVersionTextBlock.Text = Assembly.GetExecutingAssembly().GetName().Version.ToString();
|
||||
try
|
||||
@@ -31,6 +47,11 @@ namespace Ink_Canvas
|
||||
string text = File.ReadAllText(App.RootPath + settingsFileName);
|
||||
Settings = JsonConvert.DeserializeObject<Settings>(text);
|
||||
|
||||
if (Settings != null)
|
||||
{
|
||||
CleanupObsoleteSettings(text);
|
||||
}
|
||||
|
||||
// 验证设置是否成功加载
|
||||
if (Settings == null)
|
||||
{
|
||||
@@ -42,6 +63,8 @@ namespace Ink_Canvas
|
||||
Settings = JsonConvert.DeserializeObject<Settings>(text);
|
||||
if (Settings != null)
|
||||
{
|
||||
// 清理过期配置项
|
||||
CleanupObsoleteSettings(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +90,8 @@ namespace Ink_Canvas
|
||||
Settings = JsonConvert.DeserializeObject<Settings>(text);
|
||||
if (Settings != null)
|
||||
{
|
||||
// 清理过期配置项
|
||||
CleanupObsoleteSettings(text);
|
||||
}
|
||||
}
|
||||
catch (Exception restoreEx)
|
||||
@@ -95,6 +120,8 @@ namespace Ink_Canvas
|
||||
Settings = JsonConvert.DeserializeObject<Settings>(text);
|
||||
if (Settings != null)
|
||||
{
|
||||
// 清理过期配置项
|
||||
CleanupObsoleteSettings(text);
|
||||
}
|
||||
}
|
||||
catch (Exception restoreEx)
|
||||
@@ -123,12 +150,72 @@ namespace Ink_Canvas
|
||||
LogHelper.WriteLogToFile(ex.ToString(), LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Settings?.Appearance != null)
|
||||
{
|
||||
var preferredLanguage = Settings.Appearance.Language ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(preferredLanguage))
|
||||
{
|
||||
LocalizationHelper.TrySetCulture(preferredLanguage);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从配置应用界面语言失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProcessProtectionManager.ApplyFromSettings();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// Startup
|
||||
if (isStartup)
|
||||
{
|
||||
CursorIcon_Click(null, null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Settings?.Startup != null)
|
||||
{
|
||||
if (ComboBoxTelemetryUploadLevel != null)
|
||||
{
|
||||
int idx = 0;
|
||||
switch (Settings.Startup.TelemetryUploadLevel)
|
||||
{
|
||||
case TelemetryUploadLevel.None:
|
||||
idx = 0;
|
||||
break;
|
||||
case TelemetryUploadLevel.Basic:
|
||||
idx = 1;
|
||||
break;
|
||||
case TelemetryUploadLevel.Extended:
|
||||
idx = 2;
|
||||
break;
|
||||
default:
|
||||
idx = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
ComboBoxTelemetryUploadLevel.SelectedIndex = idx;
|
||||
}
|
||||
|
||||
if (CheckBoxTelemetryPrivacyAccepted != null)
|
||||
{
|
||||
CheckBoxTelemetryPrivacyAccepted.IsChecked = Settings.Startup.HasAcceptedTelemetryPrivacy;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(Environment.GetFolderPath(Environment.SpecialFolder.Startup) +
|
||||
@@ -180,7 +267,7 @@ namespace Ink_Canvas
|
||||
ToggleSwitchIsAutoUpdate.IsOn = Settings.Startup.IsAutoUpdate;
|
||||
|
||||
// 只有在启用了自动更新功能时才检查更新
|
||||
if (Settings.Startup.IsAutoUpdate)
|
||||
if (Settings.Startup.IsAutoUpdate && !skipAutoUpdateCheck)
|
||||
{
|
||||
if (isStartup)
|
||||
{
|
||||
@@ -195,7 +282,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;
|
||||
@@ -332,6 +419,20 @@ namespace Ink_Canvas
|
||||
|
||||
ComboBoxChickenSoupSource.SelectedIndex = Settings.Appearance.ChickenSoupSource;
|
||||
|
||||
// 初始化自定义按钮的可见性(仅在选择API时显示)
|
||||
if (BtnHitokotoCustomize != null)
|
||||
{
|
||||
BtnHitokotoCustomize.Visibility = Settings.Appearance.ChickenSoupSource == 3
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// 初始化HitokotoCategories,如果为空则默认全选
|
||||
if (Settings.Appearance.HitokotoCategories == null || Settings.Appearance.HitokotoCategories.Count == 0)
|
||||
{
|
||||
Settings.Appearance.HitokotoCategories = new List<string> { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" };
|
||||
}
|
||||
|
||||
ToggleSwitchEnableQuickPanel.IsOn = Settings.Appearance.IsShowQuickPanel;
|
||||
|
||||
ToggleSwitchEnableSplashScreen.IsOn = Settings.Appearance.EnableSplashScreen;
|
||||
@@ -456,6 +557,11 @@ namespace Ink_Canvas
|
||||
|
||||
ToggleSwitchShowCanvasAtNewSlideShow.IsOn = Settings.PowerPointSettings.IsShowCanvasAtNewSlideShow;
|
||||
|
||||
if (ToggleSwitchUseRotPptLink != null)
|
||||
{
|
||||
ToggleSwitchUseRotPptLink.IsOn = Settings.PowerPointSettings.UseRotPptLink;
|
||||
}
|
||||
|
||||
ToggleSwitchEnableTwoFingerGestureInPresentationMode.IsOn =
|
||||
Settings.PowerPointSettings.IsEnableTwoFingerGestureInPresentationMode;
|
||||
|
||||
@@ -467,6 +573,25 @@ namespace Ink_Canvas
|
||||
|
||||
ToggleSwitchNotifyPreviousPage.IsOn = Settings.PowerPointSettings.IsNotifyPreviousPage;
|
||||
|
||||
// PPT时间显示胶囊设置
|
||||
if (ToggleSwitchEnablePPTTimeCapsule != null)
|
||||
{
|
||||
ToggleSwitchEnablePPTTimeCapsule.IsOn = Settings.PowerPointSettings.EnablePPTTimeCapsule;
|
||||
}
|
||||
if (ComboBoxPPTTimeCapsulePosition != null)
|
||||
{
|
||||
int position = Settings.PowerPointSettings.PPTTimeCapsulePosition;
|
||||
if (position < 0 || position > 2)
|
||||
{
|
||||
position = 1; // 默认右上角
|
||||
}
|
||||
ComboBoxPPTTimeCapsulePosition.SelectedIndex = position;
|
||||
}
|
||||
if (ToggleSwitchShowPPTSidebarByDefault != null)
|
||||
{
|
||||
ToggleSwitchShowPPTSidebarByDefault.IsOn = Settings.PowerPointSettings.ShowPPTSidebarByDefault;
|
||||
}
|
||||
|
||||
// -- new --
|
||||
ToggleSwitchShowPPTButton.IsOn = Settings.PowerPointSettings.ShowPPTButton;
|
||||
|
||||
@@ -540,6 +665,38 @@ namespace Ink_Canvas
|
||||
|
||||
PPTButtonRBPositionValueSlider.Value = Settings.PowerPointSettings.PPTRBButtonPosition;
|
||||
|
||||
// 初始化PPT翻页按钮透明度滑块值,根据半透明选项设置默认值
|
||||
// 重用之前定义的sopsc和bopsc变量
|
||||
bool isSideHalfOpacity = sopsc.Length >= 2 && sopsc[1] == '2';
|
||||
// 如果透明度为0或未设置,根据半透明选项设置默认值
|
||||
if (Settings.PowerPointSettings.PPTLSButtonOpacity == 0.0 ||
|
||||
(Settings.PowerPointSettings.PPTLSButtonOpacity == 1.0 && isSideHalfOpacity))
|
||||
{
|
||||
Settings.PowerPointSettings.PPTLSButtonOpacity = isSideHalfOpacity ? 0.5 : 1.0;
|
||||
}
|
||||
if (Settings.PowerPointSettings.PPTRSButtonOpacity == 0.0 ||
|
||||
(Settings.PowerPointSettings.PPTRSButtonOpacity == 1.0 && isSideHalfOpacity))
|
||||
{
|
||||
Settings.PowerPointSettings.PPTRSButtonOpacity = isSideHalfOpacity ? 0.5 : 1.0;
|
||||
}
|
||||
PPTLSButtonOpacityValueSlider.Value = Settings.PowerPointSettings.PPTLSButtonOpacity;
|
||||
PPTRSButtonOpacityValueSlider.Value = Settings.PowerPointSettings.PPTRSButtonOpacity;
|
||||
|
||||
bool isBottomHalfOpacity = bopsc.Length >= 2 && bopsc[1] == '2';
|
||||
// 如果透明度为0或未设置,根据半透明选项设置默认值
|
||||
if (Settings.PowerPointSettings.PPTLBButtonOpacity == 0.0 ||
|
||||
(Settings.PowerPointSettings.PPTLBButtonOpacity == 1.0 && isBottomHalfOpacity))
|
||||
{
|
||||
Settings.PowerPointSettings.PPTLBButtonOpacity = isBottomHalfOpacity ? 0.5 : 1.0;
|
||||
}
|
||||
if (Settings.PowerPointSettings.PPTRBButtonOpacity == 0.0 ||
|
||||
(Settings.PowerPointSettings.PPTRBButtonOpacity == 1.0 && isBottomHalfOpacity))
|
||||
{
|
||||
Settings.PowerPointSettings.PPTRBButtonOpacity = isBottomHalfOpacity ? 0.5 : 1.0;
|
||||
}
|
||||
PPTLBButtonOpacityValueSlider.Value = Settings.PowerPointSettings.PPTLBButtonOpacity;
|
||||
PPTRBButtonOpacityValueSlider.Value = Settings.PowerPointSettings.PPTRBButtonOpacity;
|
||||
|
||||
UpdatePPTBtnSlidersStatus();
|
||||
|
||||
UpdatePPTBtnPreview();
|
||||
@@ -552,6 +709,8 @@ namespace Ink_Canvas
|
||||
|
||||
ToggleSwitchSupportWPS.IsOn = Settings.PowerPointSettings.IsSupportWPS;
|
||||
|
||||
ToggleSwitchSkipAnimationsWhenGoNext.IsOn = Settings.PowerPointSettings.SkipAnimationsWhenGoNext;
|
||||
|
||||
ToggleSwitchPowerPointEnhancement.IsOn = Settings.PowerPointSettings.EnablePowerPointEnhancement;
|
||||
|
||||
ToggleSwitchAutoSaveScreenShotInPowerPoint.IsOn =
|
||||
@@ -592,7 +751,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
|
||||
{
|
||||
@@ -619,6 +778,15 @@ namespace Ink_Canvas
|
||||
InkWidthSlider.Value = Settings.Canvas.InkWidth * 2;
|
||||
HighlighterWidthSlider.Value = Settings.Canvas.HighlighterWidth;
|
||||
|
||||
int alpha = (int)Settings.Canvas.InkAlpha;
|
||||
if (alpha < 0) alpha = 0; if (alpha > 255) alpha = 255;
|
||||
var inkColor = drawingAttributes.Color;
|
||||
drawingAttributes.Color = Color.FromArgb((byte)alpha, inkColor.R, inkColor.G, inkColor.B);
|
||||
inkCanvas.DefaultDrawingAttributes.Color = drawingAttributes.Color;
|
||||
if (InkAlphaSlider != null) InkAlphaSlider.Value = alpha;
|
||||
if (BoardInkAlphaSlider != null) BoardInkAlphaSlider.Value = alpha;
|
||||
|
||||
|
||||
ComboBoxHyperbolaAsymptoteOption.SelectedIndex = (int)Settings.Canvas.HyperbolaAsymptoteOption;
|
||||
|
||||
if (Settings.Canvas.UsingWhiteboard)
|
||||
@@ -795,7 +963,9 @@ 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;
|
||||
ToggleSwitchIsEnableUriScheme.IsOn = Settings.Advanced.IsEnableUriScheme;
|
||||
ToggleSwitchIsQuadIR.IsOn = Settings.Advanced.IsQuadIR;
|
||||
ToggleSwitchEraserBindTouchMultiplier.IsOn = Settings.Advanced.EraserBindTouchMultiplier;
|
||||
ToggleSwitchIsEnableFullScreenHelper.IsOn = Settings.Advanced.IsEnableFullScreenHelper;
|
||||
@@ -886,20 +1056,20 @@ namespace Ink_Canvas
|
||||
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;
|
||||
|
||||
// 渐进提醒设置
|
||||
@@ -928,16 +1098,16 @@ namespace Ink_Canvas
|
||||
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;
|
||||
@@ -1053,6 +1223,8 @@ namespace Ink_Canvas
|
||||
|
||||
ToggleSwitchSaveFullPageStrokes.IsOn = Settings.Automation.IsSaveFullPageStrokes;
|
||||
|
||||
ToggleSwitchSaveStrokesAsXML.IsOn = Settings.Automation.IsSaveStrokesAsXML;
|
||||
|
||||
// 加载定时保存墨迹设置
|
||||
ToggleSwitchEnableAutoSaveStrokes.IsOn = Settings.Automation.IsEnableAutoSaveStrokes;
|
||||
// 初始化保存间隔下拉框
|
||||
@@ -1100,6 +1272,105 @@ namespace Ink_Canvas
|
||||
|
||||
// 加载墨迹渐隐设置
|
||||
LoadInkFadeSettings();
|
||||
|
||||
// 加载画笔自动恢复设置
|
||||
LoadBrushAutoRestoreSettings();
|
||||
|
||||
// 刷新配置文件列表
|
||||
try { RefreshConfigProfileList(); } catch (Exception ex) { LogHelper.WriteLogToFile($"刷新配置文件列表失败: {ex.Message}", LogHelper.LogType.Warning); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将画笔自动恢复相关的设置应用到界面控件并在启用时初始化自动恢复定时器。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 会将 Settings.Canvas 中的 BrushAutoRestore 配置同步到对应的切换开关、时间文本框、颜色下拉框、宽度和透明度滑块;当颜色缺失时会使用默认值 `#FFFF0000`,当宽度无效时使用默认值 `5`。若功能被启用,会初始化并启动定时器以执行自动恢复任务。方法执行过程中会记录加载结果或错误信息到日志。
|
||||
/// </remarks>
|
||||
private void LoadBrushAutoRestoreSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 同步设置面板中的开关状态
|
||||
if (ToggleSwitchBrushAutoRestore != null)
|
||||
{
|
||||
ToggleSwitchBrushAutoRestore.IsOn = Settings.Canvas.EnableBrushAutoRestore;
|
||||
}
|
||||
|
||||
// 同步时间点输入框
|
||||
if (BrushAutoRestoreTimesTextBox != null)
|
||||
{
|
||||
BrushAutoRestoreTimesTextBox.Text = Settings.Canvas.BrushAutoRestoreTimes ?? string.Empty;
|
||||
}
|
||||
|
||||
// 同步颜色下拉框
|
||||
if (ComboBoxBrushAutoRestoreColor != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Settings.Canvas.BrushAutoRestoreColor))
|
||||
{
|
||||
Settings.Canvas.BrushAutoRestoreColor = "#FFFF0000";
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
foreach (ComboBoxItem item in ComboBoxBrushAutoRestoreColor.Items)
|
||||
{
|
||||
if (item.Tag != null && item.Tag.ToString() == Settings.Canvas.BrushAutoRestoreColor)
|
||||
{
|
||||
ComboBoxBrushAutoRestoreColor.SelectionChanged -= ComboBoxBrushAutoRestoreColor_SelectionChanged;
|
||||
try
|
||||
{
|
||||
ComboBoxBrushAutoRestoreColor.SelectedItem = item;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ComboBoxBrushAutoRestoreColor.SelectionChanged += ComboBoxBrushAutoRestoreColor_SelectionChanged;
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found && ComboBoxBrushAutoRestoreColor.Items.Count > 0)
|
||||
{
|
||||
ComboBoxBrushAutoRestoreColor.SelectionChanged -= ComboBoxBrushAutoRestoreColor_SelectionChanged;
|
||||
try
|
||||
{
|
||||
ComboBoxBrushAutoRestoreColor.SelectedIndex = 0;
|
||||
Settings.Canvas.BrushAutoRestoreColor = "#FFFF0000";
|
||||
}
|
||||
finally
|
||||
{
|
||||
ComboBoxBrushAutoRestoreColor.SelectionChanged += ComboBoxBrushAutoRestoreColor_SelectionChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 同步粗细滑块
|
||||
if (BrushAutoRestoreWidthSlider != null)
|
||||
{
|
||||
BrushAutoRestoreWidthSlider.Value = Settings.Canvas.BrushAutoRestoreWidth > 0
|
||||
? Settings.Canvas.BrushAutoRestoreWidth
|
||||
: 5;
|
||||
}
|
||||
|
||||
// 同步透明度滑块
|
||||
if (BrushAutoRestoreAlphaSlider != null)
|
||||
{
|
||||
BrushAutoRestoreAlphaSlider.Value = Settings.Canvas.BrushAutoRestoreAlpha;
|
||||
}
|
||||
|
||||
// 如果功能已启用,初始化并启动定时器
|
||||
if (Settings.Canvas.EnableBrushAutoRestore)
|
||||
{
|
||||
InitBrushAutoRestoreTimer();
|
||||
ScheduleBrushAutoRestore();
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("画笔自动恢复设置已加载", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载画笔自动恢复设置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1142,6 +1413,15 @@ namespace Ink_Canvas
|
||||
_inkFadeManager.UpdateFadeTime(Settings.Canvas.InkFadeTime);
|
||||
}
|
||||
|
||||
// 同步在笔工具菜单中隐藏墨迹渐隐控制开关的设置
|
||||
if (ToggleSwitchHideInkFadeControlInPenMenu != null)
|
||||
{
|
||||
ToggleSwitchHideInkFadeControlInPenMenu.IsOn = Settings.Canvas.HideInkFadeControlInPenMenu;
|
||||
}
|
||||
|
||||
// 根据设置更新墨迹渐隐控制开关的可见性
|
||||
UpdateInkFadeControlVisibility();
|
||||
|
||||
LogHelper.WriteLogToFile("墨迹渐隐设置已加载", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1149,5 +1429,127 @@ namespace Ink_Canvas
|
||||
LogHelper.WriteLogToFile($"加载墨迹渐隐设置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理配置文件中的过期设置
|
||||
/// </summary>
|
||||
/// <param name="userConfigJson">用户配置的JSON字符串</param>
|
||||
/// <remarks>
|
||||
/// 清理过期设置时:
|
||||
/// 1. 创建默认配置对象
|
||||
/// 2. 将默认配置和用户配置都序列化为JObject
|
||||
/// 3. 递归比较并删除用户配置中多余的键
|
||||
/// 4. 如果有清理操作,重新反序列化并保存
|
||||
/// 5. 记录清理结果到日志
|
||||
/// </remarks>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归删除用户配置中多余的属性
|
||||
/// </summary>
|
||||
/// <param name="userObj">用户配置的JObject</param>
|
||||
/// <param name="defaultObj">默认配置的JObject</param>
|
||||
/// <param name="hasChanges">是否有变更的引用标志</param>
|
||||
/// <remarks>
|
||||
/// 递归删除多余属性时:
|
||||
/// 1. 检查用户配置和默认配置是否为空
|
||||
/// 2. 获取需要删除的键列表
|
||||
/// 3. 遍历用户配置的所有属性
|
||||
/// 4. 如果默认配置中不存在该属性,标记为删除
|
||||
/// 5. 如果两个属性都是对象类型,递归比较
|
||||
/// 6. 处理数组中的对象(如自定义图标列表等)
|
||||
/// 7. 删除标记的键
|
||||
/// 8. 设置变更标志
|
||||
/// </remarks>
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -12,29 +12,70 @@ namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 提交原因枚举,用于标识不同类型的操作
|
||||
/// </summary>
|
||||
private enum CommitReason
|
||||
{
|
||||
/// <summary>用户输入操作</summary>
|
||||
UserInput,
|
||||
/// <summary>代码输入操作</summary>
|
||||
CodeInput,
|
||||
/// <summary>形状绘制操作</summary>
|
||||
ShapeDrawing,
|
||||
/// <summary>形状识别操作</summary>
|
||||
ShapeRecognition,
|
||||
/// <summary>清除画布操作</summary>
|
||||
ClearingCanvas,
|
||||
/// <summary>笔画操作操作</summary>
|
||||
Manipulation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前提交类型
|
||||
/// </summary>
|
||||
private CommitReason _currentCommitType = CommitReason.UserInput;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为点橡皮擦模式
|
||||
/// </summary>
|
||||
private bool IsEraseByPoint => inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint;
|
||||
|
||||
/// <summary>
|
||||
/// 替换的笔画集合
|
||||
/// </summary>
|
||||
private StrokeCollection ReplacedStroke;
|
||||
|
||||
/// <summary>
|
||||
/// 添加的笔画集合
|
||||
/// </summary>
|
||||
private StrokeCollection AddedStroke;
|
||||
|
||||
/// <summary>
|
||||
/// 长方体笔画集合
|
||||
/// </summary>
|
||||
private StrokeCollection CuboidStrokeCollection;
|
||||
|
||||
/// <summary>
|
||||
/// 笔画操作历史记录
|
||||
/// </summary>
|
||||
private Dictionary<Stroke, Tuple<StylusPointCollection, StylusPointCollection>> StrokeManipulationHistory;
|
||||
|
||||
/// <summary>
|
||||
/// 笔画初始状态历史记录
|
||||
/// </summary>
|
||||
private Dictionary<Stroke, StylusPointCollection> StrokeInitialHistory =
|
||||
new Dictionary<Stroke, StylusPointCollection>();
|
||||
|
||||
/// <summary>
|
||||
/// 绘制属性历史记录
|
||||
/// </summary>
|
||||
private Dictionary<Stroke, Tuple<DrawingAttributes, DrawingAttributes>> DrawingAttributesHistory =
|
||||
new Dictionary<Stroke, Tuple<DrawingAttributes, DrawingAttributes>>();
|
||||
|
||||
/// <summary>
|
||||
/// 绘制属性历史记录标志
|
||||
/// </summary>
|
||||
private Dictionary<Guid, List<Stroke>> DrawingAttributesHistoryFlag = new Dictionary<Guid, List<Stroke>> {
|
||||
{ DrawingAttributeIds.Color, new List<Stroke>() },
|
||||
{ DrawingAttributeIds.DrawingFlags, new List<Stroke>() },
|
||||
@@ -45,9 +86,27 @@ namespace Ink_Canvas
|
||||
{ DrawingAttributeIds.StylusWidth, new List<Stroke>() }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 时间机器实例,用于撤销/重做操作
|
||||
/// </summary>
|
||||
private TimeMachine timeMachine = new TimeMachine();
|
||||
|
||||
private void ApplyHistoryToCanvas(TimeMachineHistory item, InkCanvas applyCanvas = null)
|
||||
/// <summary>
|
||||
/// 将历史记录应用到画布
|
||||
/// </summary>
|
||||
/// <param name="item">时间机器历史记录项</param>
|
||||
/// <param name="applyCanvas">要应用的画布,默认为null(使用主画布)</param>
|
||||
/// <remarks>
|
||||
/// 根据历史记录类型执行不同的操作:
|
||||
/// 1. UserInput: 处理用户输入的笔画
|
||||
/// 2. ShapeRecognition: 处理形状识别的笔画
|
||||
/// 3. Manipulation: 处理笔画操作
|
||||
/// 4. DrawingAttributes: 处理绘制属性变化
|
||||
/// 5. Clear: 处理清除画布操作
|
||||
/// 6. ElementInsert: 处理元素插入操作
|
||||
/// </remarks>
|
||||
/// <param name="elementsRemovedInThisPage"></param>
|
||||
private void ApplyHistoryToCanvas(TimeMachineHistory item, InkCanvas applyCanvas = null, HashSet<UIElement> elementsRemovedInThisPage = null)
|
||||
{
|
||||
_currentCommitType = CommitReason.CodeInput;
|
||||
var canvas = inkCanvas;
|
||||
@@ -169,54 +228,42 @@ namespace Ink_Canvas
|
||||
}
|
||||
else if (item.CommitType == TimeMachineHistoryType.ElementInsert)
|
||||
{
|
||||
// 使用传入的canvas参数,而不是总是使用inkCanvas
|
||||
var targetCanvas = canvas ?? inkCanvas;
|
||||
|
||||
if (item.StrokeHasBeenCleared)
|
||||
{
|
||||
// Undo: 移除元素
|
||||
if (elementsRemovedInThisPage != null)
|
||||
return;
|
||||
if (item.InsertedElement != null && targetCanvas.Children.Contains(item.InsertedElement))
|
||||
targetCanvas.Children.Remove(item.InsertedElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Redo: 添加元素
|
||||
if (elementsRemovedInThisPage != null && item.InsertedElement != null && elementsRemovedInThisPage.Contains(item.InsertedElement))
|
||||
return;
|
||||
if (item.InsertedElement != null && !targetCanvas.Children.Contains(item.InsertedElement))
|
||||
{
|
||||
targetCanvas.Children.Add(item.InsertedElement);
|
||||
|
||||
// 重新绑定事件处理器(仅对主画布)
|
||||
if (targetCanvas == inkCanvas)
|
||||
if (targetCanvas != inkCanvas)
|
||||
{
|
||||
if (item.InsertedElement is Image img)
|
||||
{
|
||||
// 检查图片是否有位置信息,如果没有则应用居中
|
||||
double left = InkCanvas.GetLeft(img);
|
||||
double top = InkCanvas.GetTop(img);
|
||||
|
||||
if (double.IsNaN(left) || double.IsNaN(top))
|
||||
{
|
||||
// 图片没有位置信息,应用居中
|
||||
CenterAndScaleElement(img);
|
||||
}
|
||||
|
||||
// 重新绑定事件处理器
|
||||
BindElementEvents(img);
|
||||
}
|
||||
else if (item.InsertedElement is MediaElement media)
|
||||
{
|
||||
// 检查媒体元素是否有位置信息,如果没有则应用居中
|
||||
double left = InkCanvas.GetLeft(media);
|
||||
double top = InkCanvas.GetTop(media);
|
||||
|
||||
if (double.IsNaN(left) || double.IsNaN(top))
|
||||
{
|
||||
// 媒体元素没有位置信息,应用居中
|
||||
CenterAndScaleElement(media);
|
||||
}
|
||||
|
||||
// 重新绑定事件处理器
|
||||
BindElementEvents(media);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +273,15 @@ namespace Ink_Canvas
|
||||
_currentCommitType = CommitReason.UserInput;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将历史记录应用到新的笔画集合
|
||||
/// </summary>
|
||||
/// <param name="items">时间机器历史记录数组</param>
|
||||
/// <returns>返回应用历史记录后的笔画集合</returns>
|
||||
/// <remarks>
|
||||
/// 创建一个临时画布,应用历史记录,然后返回画布中的笔画集合
|
||||
/// 只处理笔画历史,不处理图片元素历史
|
||||
/// </remarks>
|
||||
private StrokeCollection ApplyHistoriesToNewStrokeCollection(TimeMachineHistory[] items)
|
||||
{
|
||||
InkCanvas fakeInkCanv = new InkCanvas
|
||||
@@ -251,7 +307,52 @@ namespace Ink_Canvas
|
||||
return fakeInkCanv.Strokes;
|
||||
}
|
||||
|
||||
// 新增:获取页面的所有图片元素
|
||||
/// <summary>
|
||||
/// 将一页的完整历史扁平化为“仅最终状态”:在临时画布上重放该页历史,再导出为最少条目的新历史(一笔画集合 + 若干元素插入)。
|
||||
/// 用于删除页面前移后,避免移入槽位保留冗长历史导致翻到该页码时卡顿。
|
||||
/// </summary>
|
||||
/// <param name="history">该页的 TimeMachineHistory 数组,可为 null 或空</param>
|
||||
/// <returns>扁平化后的新历史数组;若输入为 null 或空则返回 null</returns>
|
||||
private TimeMachineHistory[] FlattenPageHistory(TimeMachineHistory[] history)
|
||||
{
|
||||
if (history == null || history.Length == 0) return null;
|
||||
|
||||
var removed = CollectRemovedElementsFromHistory(history);
|
||||
var fakeInkCanv = new InkCanvas
|
||||
{
|
||||
Width = inkCanvas.ActualWidth > 0 ? inkCanvas.ActualWidth : 1920,
|
||||
Height = inkCanvas.ActualHeight > 0 ? inkCanvas.ActualHeight : 1080,
|
||||
EditingMode = InkCanvasEditingMode.None,
|
||||
};
|
||||
|
||||
foreach (var item in history)
|
||||
ApplyHistoryToCanvas(item, fakeInkCanv, removed);
|
||||
|
||||
var list = new List<TimeMachineHistory>();
|
||||
if (fakeInkCanv.Strokes.Count > 0)
|
||||
list.Add(new TimeMachineHistory(fakeInkCanv.Strokes.Clone(), TimeMachineHistoryType.UserInput, false));
|
||||
var childrenSnapshot = new List<UIElement>();
|
||||
foreach (UIElement c in fakeInkCanv.Children)
|
||||
childrenSnapshot.Add(c);
|
||||
foreach (UIElement child in childrenSnapshot)
|
||||
{
|
||||
if (child is Image || child is MediaElement)
|
||||
{
|
||||
list.Add(new TimeMachineHistory(child, TimeMachineHistoryType.ElementInsert));
|
||||
fakeInkCanv.Children.Remove(child);
|
||||
}
|
||||
}
|
||||
return list.Count == 0 ? null : list.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取页面的所有图片元素
|
||||
/// </summary>
|
||||
/// <param name="items">时间机器历史记录数组</param>
|
||||
/// <returns>返回页面的图片元素列表</returns>
|
||||
/// <remarks>
|
||||
/// 遍历历史记录,收集所有插入的图片元素
|
||||
/// </remarks>
|
||||
private List<UIElement> GetPageImageElements(TimeMachineHistory[] items)
|
||||
{
|
||||
var imageElements = new List<UIElement>();
|
||||
@@ -272,6 +373,13 @@ namespace Ink_Canvas
|
||||
return imageElements;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理撤销状态变化事件
|
||||
/// </summary>
|
||||
/// <param name="status">撤销状态</param>
|
||||
/// <remarks>
|
||||
/// 根据撤销状态更新撤销按钮的可见性和启用状态
|
||||
/// </remarks>
|
||||
private void TimeMachine_OnUndoStateChanged(bool status)
|
||||
{
|
||||
var result = status ? Visibility.Visible : Visibility.Collapsed;
|
||||
@@ -279,6 +387,13 @@ namespace Ink_Canvas
|
||||
BtnUndo.IsEnabled = status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理重做状态变化事件
|
||||
/// </summary>
|
||||
/// <param name="status">重做状态</param>
|
||||
/// <remarks>
|
||||
/// 根据重做状态更新重做按钮的可见性和启用状态
|
||||
/// </remarks>
|
||||
private void TimeMachine_OnRedoStateChanged(bool status)
|
||||
{
|
||||
var result = status ? Visibility.Visible : Visibility.Collapsed;
|
||||
@@ -286,6 +401,18 @@ namespace Ink_Canvas
|
||||
BtnRedo.IsEnabled = status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理笔画集合变化事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">笔画集合变化事件参数</param>
|
||||
/// <remarks>
|
||||
/// 当笔画集合发生变化时:
|
||||
/// 1. 书写时自动隐藏二级菜单
|
||||
/// 2. 处理移除的笔画:移除事件处理器,从历史记录中移除
|
||||
/// 3. 处理添加的笔画:添加事件处理器,记录初始状态
|
||||
/// 4. 根据不同的提交类型处理历史记录
|
||||
/// </remarks>
|
||||
private void StrokesOnStrokesChanged(object sender, StrokeCollectionChangedEventArgs e)
|
||||
{
|
||||
if (!isHidingSubPanelsWhenInking)
|
||||
@@ -347,6 +474,14 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理笔画绘制属性变化事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">属性数据变化事件参数</param>
|
||||
/// <remarks>
|
||||
/// 当笔画的绘制属性发生变化时,记录变化历史
|
||||
/// </remarks>
|
||||
private void Stroke_DrawingAttributesChanged(object sender, PropertyDataChangedEventArgs e)
|
||||
{
|
||||
var key = sender as Stroke;
|
||||
@@ -399,11 +534,31 @@ namespace Ink_Canvas
|
||||
new Tuple<DrawingAttributes, DrawingAttributes>(previousValue, currentValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理笔画触笔点替换事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">触笔点替换事件参数</param>
|
||||
/// <remarks>
|
||||
/// 当笔画的触笔点被替换时,更新初始状态历史
|
||||
/// </remarks>
|
||||
private void Stroke_StylusPointsReplaced(object sender, StylusPointsReplacedEventArgs e)
|
||||
{
|
||||
StrokeInitialHistory[sender as Stroke] = e.NewStylusPoints.Clone();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理笔画触笔点变化事件
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// 当笔画的触笔点发生变化时:
|
||||
/// 1. 获取选中的笔画数量
|
||||
/// 2. 初始化笔画操作历史记录
|
||||
/// 3. 记录笔画的初始状态和当前状态
|
||||
/// 4. 当所有选中的笔画都已处理时,提交操作历史
|
||||
/// </remarks>
|
||||
private void Stroke_StylusPointsChanged(object sender, EventArgs e)
|
||||
{
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using iNKORE.UI.WPF.Controls;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
@@ -16,15 +16,26 @@ namespace Ink_Canvas
|
||||
public partial class App : Application
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 系统托盘菜单打开时的事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理系统托盘菜单打开时的逻辑,包括以下步骤:
|
||||
/// 1. 获取系统托盘菜单及其相关菜单项和图标
|
||||
/// 2. 获取主窗口实例
|
||||
/// 3. 如果主窗口已加载:
|
||||
/// - 在无焦点模式下,暂时取消主窗口置顶,让系统菜单能够正常显示
|
||||
/// - 根据浮动栏是否处于收纳模式,更新菜单项图标和文本
|
||||
/// - 根据浮动栏状态和主窗口是否隐藏,更新重置浮动栏位置菜单项的启用状态
|
||||
/// </remarks>
|
||||
private void SysTrayMenu_Opened(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var s = (ContextMenu)sender;
|
||||
var FoldFloatingBarTrayIconMenuItemIconEyeOff =
|
||||
(Image)((Grid)((MenuItem)s.Items[s.Items.Count - 5]).Icon).Children[0];
|
||||
var FoldFloatingBarTrayIconMenuItemIconEyeOn =
|
||||
(Image)((Grid)((MenuItem)s.Items[s.Items.Count - 5]).Icon).Children[1];
|
||||
var FoldFloatingBarTrayIconMenuItemHeaderText =
|
||||
(TextBlock)((SimpleStackPanel)((MenuItem)s.Items[s.Items.Count - 5]).Header).Children[0];
|
||||
var FoldFloatingBarTrayIconMenuItemIconEyeOff = (Image)((Grid)((MenuItem)s.Items[s.Items.Count - 5]).Icon).Children[0];
|
||||
var FoldFloatingBarTrayIconMenuItemIconEyeOn = (Image)((Grid)((MenuItem)s.Items[s.Items.Count - 5]).Icon).Children[1];
|
||||
var FoldFloatingBarTrayIconMenuItemHeaderText = (TextBlock)((SimpleStackPanel)((MenuItem)s.Items[s.Items.Count - 5]).Header).Children[0];
|
||||
var ResetFloatingBarPositionTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 4];
|
||||
var HideICCMainWindowTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 9];
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
@@ -63,6 +74,16 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 系统托盘菜单关闭时的事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理系统托盘菜单关闭时的逻辑,包括以下步骤:
|
||||
/// 1. 获取主窗口实例
|
||||
/// 2. 如果主窗口已加载,且在无焦点模式下启用了始终置顶,则恢复主窗口的置顶状态
|
||||
/// </remarks>
|
||||
private void SysTrayMenu_Closed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
@@ -76,17 +97,42 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭应用程序托盘菜单项点击事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理关闭应用程序托盘菜单项的点击事件,包括以下步骤:
|
||||
/// 1. 获取主窗口实例
|
||||
/// 2. 如果主窗口已加载:
|
||||
/// - 设置IsAppExitByUser为true,表示用户主动退出
|
||||
/// - 关闭应用程序
|
||||
/// </remarks>
|
||||
private void CloseAppTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
if (mainWin.IsLoaded)
|
||||
{
|
||||
IsAppExitByUser = true;
|
||||
Current.Shutdown();
|
||||
// mainWin.BtnExit_Click(null,null);
|
||||
mainWin.BtnExit_Click(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重启应用程序托盘菜单项点击事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理重启应用程序托盘菜单项的点击事件,包括以下步骤:
|
||||
/// 1. 获取主窗口实例
|
||||
/// 2. 如果主窗口已加载:
|
||||
/// - 设置IsAppExitByUser为true,表示用户主动退出
|
||||
/// - 尝试启动应用程序的新实例,带延迟参数
|
||||
/// - 捕获并记录启动新实例时可能出现的异常
|
||||
/// - 关闭当前应用程序实例
|
||||
/// </remarks>
|
||||
private void RestartAppTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
@@ -115,6 +161,18 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制全屏化托盘菜单项点击事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理强制全屏化托盘菜单项的点击事件,包括以下步骤:
|
||||
/// 1. 获取主窗口实例
|
||||
/// 2. 如果主窗口已加载:
|
||||
/// - 调用MoveWindow方法将主窗口移动到屏幕左上角并设置为全屏大小
|
||||
/// - 显示强制全屏化的消息,包含屏幕分辨率和缩放比例信息
|
||||
/// </remarks>
|
||||
private void ForceFullScreenTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
@@ -126,6 +184,18 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换浮动栏收纳模式托盘菜单项点击事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理切换浮动栏收纳模式托盘菜单项的点击事件,包括以下步骤:
|
||||
/// 1. 获取主窗口实例
|
||||
/// 2. 如果主窗口已加载:
|
||||
/// - 如果浮动栏当前处于收纳模式,则调用UnFoldFloatingBar_MouseUp方法退出收纳模式
|
||||
/// - 否则,调用FoldFloatingBar_MouseUp方法进入收纳模式
|
||||
/// </remarks>
|
||||
private void FoldFloatingBarTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
@@ -134,6 +204,20 @@ namespace Ink_Canvas
|
||||
else mainWin.FoldFloatingBar_MouseUp(new object(), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置浮动栏位置托盘菜单项点击事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理重置浮动栏位置托盘菜单项的点击事件,包括以下步骤:
|
||||
/// 1. 获取主窗口实例
|
||||
/// 2. 如果主窗口已加载:
|
||||
/// - 检查是否处于PPT演示模式
|
||||
/// - 如果浮动栏当前未处于收纳模式:
|
||||
/// - 如果不处于PPT演示模式,调用PureViewboxFloatingBarMarginAnimationInDesktopMode方法重置浮动栏位置
|
||||
/// - 否则,调用PureViewboxFloatingBarMarginAnimationInPPTMode方法重置浮动栏位置
|
||||
/// </remarks>
|
||||
private void ResetFloatingBarPositionTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
@@ -152,6 +236,23 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏主窗口托盘菜单项选中事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理隐藏主窗口托盘菜单项的选中事件,包括以下步骤:
|
||||
/// 1. 获取菜单项和主窗口实例
|
||||
/// 2. 如果主窗口已加载:
|
||||
/// - 隐藏主窗口
|
||||
/// - 获取系统托盘菜单
|
||||
/// - 禁用并设置半透明效果给以下菜单项:
|
||||
/// - 重置浮动栏位置
|
||||
/// - 切换浮动栏收纳模式
|
||||
/// - 强制全屏化
|
||||
/// 3. 否则,取消菜单项的选中状态
|
||||
/// </remarks>
|
||||
private void HideICCMainWindowTrayIconMenuItem_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mi = (MenuItem)sender;
|
||||
@@ -177,6 +278,23 @@ namespace Ink_Canvas
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示主窗口托盘菜单项取消选中事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理显示主窗口托盘菜单项的取消选中事件,包括以下步骤:
|
||||
/// 1. 获取菜单项和主窗口实例
|
||||
/// 2. 如果主窗口已加载:
|
||||
/// - 显示主窗口
|
||||
/// - 获取系统托盘菜单
|
||||
/// - 启用并设置正常透明度给以下菜单项:
|
||||
/// - 重置浮动栏位置
|
||||
/// - 切换浮动栏收纳模式
|
||||
/// - 强制全屏化
|
||||
/// 3. 否则,取消菜单项的选中状态
|
||||
/// </remarks>
|
||||
private void HideICCMainWindowTrayIconMenuItem_UnChecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mi = (MenuItem)sender;
|
||||
@@ -201,6 +319,24 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用/启用所有快捷键托盘菜单项点击事件处理方法
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">路由事件参数</param>
|
||||
/// <remarks>
|
||||
/// 处理禁用/启用所有快捷键托盘菜单项的点击事件,包括以下步骤:
|
||||
/// 1. 获取主窗口实例
|
||||
/// 2. 如果主窗口已加载,尝试:
|
||||
/// - 通过反射获取全局快捷键管理器
|
||||
/// - 如果获取成功:
|
||||
/// - 禁用快捷键注册
|
||||
/// - 更新菜单项文本和状态:
|
||||
/// - 如果当前文本是"禁用所有快捷键",则更改为"启用所有快捷键"并记录日志
|
||||
/// - 否则,更改为"禁用所有快捷键",重新启用快捷键注册并记录日志
|
||||
/// - 如果获取失败,记录错误日志
|
||||
/// 3. 捕获并记录可能出现的异常
|
||||
/// </remarks>
|
||||
private void DisableAllHotkeysMenuItem_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
@@ -209,8 +345,7 @@ namespace Ink_Canvas
|
||||
try
|
||||
{
|
||||
// 获取全局快捷键管理器
|
||||
var hotkeyManagerField = typeof(MainWindow).GetField("_globalHotkeyManager",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var hotkeyManagerField = typeof(MainWindow).GetField("_globalHotkeyManager", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var hotkeyManager = hotkeyManagerField?.GetValue(mainWin) as GlobalHotkeyManager;
|
||||
|
||||
if (hotkeyManager != null)
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 icc: URL 协议命令
|
||||
/// 支持:收纳/展开/切换、彻底隐藏、点名/计时器/白板、工具状态切换与查询、配置方案列表与切换。
|
||||
/// 配置方案:icc://config-profile/list 输出列表到 %TEMP%\InkCanvasConfigProfileList.json;
|
||||
/// icc://config-profile/switch?name=方案名 切换方案,结果写入 %TEMP%\InkCanvasConfigProfileSwitchResult.txt。
|
||||
/// </summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
public void HandleUriCommand(string uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(uri)) return;
|
||||
|
||||
if (!Settings.Advanced.IsEnableUriScheme)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"URI 协议已禁用,忽略请求: {uri}", LogHelper.LogType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"正在处理 URI 命令: {uri}", LogHelper.LogType.Event);
|
||||
|
||||
string command = ParseUriCommand(uri);
|
||||
if (string.IsNullOrEmpty(command)) return;
|
||||
|
||||
string path = command;
|
||||
string pathLower = path.ToLowerInvariant();
|
||||
|
||||
switch (pathLower)
|
||||
{
|
||||
case "fold":
|
||||
if (!isFloatingBarFolded)
|
||||
{
|
||||
FoldFloatingBar_MouseUp(new object(), null);
|
||||
ShowNotification("已进入收纳模式");
|
||||
}
|
||||
return;
|
||||
case "unfold":
|
||||
case "show":
|
||||
if (isFloatingBarFolded)
|
||||
{
|
||||
UnFoldFloatingBar_MouseUp(new object(), null);
|
||||
ShowNotification("已退出收纳模式");
|
||||
}
|
||||
return;
|
||||
case "toggle":
|
||||
if (isFloatingBarFolded)
|
||||
{
|
||||
UnFoldFloatingBar_MouseUp(new object(), null);
|
||||
ShowNotification("已退出收纳模式");
|
||||
}
|
||||
else
|
||||
{
|
||||
FoldFloatingBar_MouseUp(new object(), null);
|
||||
ShowNotification("已进入收纳模式");
|
||||
}
|
||||
return;
|
||||
case "thoroughhideon":
|
||||
Settings.Automation.ThoroughlyHideWhenFolded = true;
|
||||
SaveSettingsToFile();
|
||||
ShowNotification("已开启:收起时彻底隐藏");
|
||||
if (isFloatingBarFolded)
|
||||
this.Visibility = Visibility.Hidden;
|
||||
return;
|
||||
case "thoroughhideoff":
|
||||
Settings.Automation.ThoroughlyHideWhenFolded = false;
|
||||
SaveSettingsToFile();
|
||||
ShowNotification("已关闭:收起时彻底隐藏");
|
||||
this.Visibility = Visibility.Visible;
|
||||
return;
|
||||
case "thoroughhidetoggle":
|
||||
Settings.Automation.ThoroughlyHideWhenFolded = !Settings.Automation.ThoroughlyHideWhenFolded;
|
||||
SaveSettingsToFile();
|
||||
ShowNotification(Settings.Automation.ThoroughlyHideWhenFolded ? "已开启:收起时彻底隐藏" : "已关闭:收起时彻底隐藏");
|
||||
if (isFloatingBarFolded)
|
||||
this.Visibility = Settings.Automation.ThoroughlyHideWhenFolded ? Visibility.Hidden : Visibility.Visible;
|
||||
return;
|
||||
case "randone":
|
||||
SymbolIconRandOne_MouseUp(null, null);
|
||||
return;
|
||||
case "rand":
|
||||
SymbolIconRand_MouseUp(null, null);
|
||||
return;
|
||||
case "timer":
|
||||
ImageCountdownTimer_MouseUp(null, null);
|
||||
return;
|
||||
case "whiteboard":
|
||||
case "board":
|
||||
ImageBlackboard_MouseUp(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathLower == "tool/state")
|
||||
{
|
||||
string state = GetCurrentSelectedMode() ?? "cursor";
|
||||
string stateFile = Path.Combine(Path.GetTempPath(), "InkCanvasToolState.txt");
|
||||
File.WriteAllText(stateFile, state, System.Text.Encoding.UTF8);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathLower.StartsWith("tool/"))
|
||||
{
|
||||
string tool = pathLower.Length > 5 ? pathLower.Substring(5).TrimEnd('/') : "";
|
||||
switch (tool)
|
||||
{
|
||||
case "pen":
|
||||
case "color":
|
||||
PenIcon_Click(null, null);
|
||||
break;
|
||||
case "cursor":
|
||||
CursorIcon_Click(null, null);
|
||||
break;
|
||||
case "eraser":
|
||||
PenIcon_Click(null, null);
|
||||
EraserIcon_Click(null, null);
|
||||
break;
|
||||
case "eraserbystrokes":
|
||||
case "eraserstroke":
|
||||
PenIcon_Click(null, null);
|
||||
EraserIconByStrokes_Click(EraserByStrokes_Icon, null);
|
||||
break;
|
||||
default:
|
||||
LogHelper.WriteLogToFile($"未知的 URI 工具: {tool}", LogHelper.LogType.Warning);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathLower == "config-profile/list")
|
||||
{
|
||||
WriteConfigProfileListToTemp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathLower.StartsWith("config-profile/switch"))
|
||||
{
|
||||
string profileName = GetUriQueryValue(uri, "name");
|
||||
HandleUriConfigProfileSwitch(profileName);
|
||||
return;
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"未知的 URI 命令: {command}", LogHelper.LogType.Warning);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理 URI 命令时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ParseUriCommand(string uri)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri) || !uri.Trim().StartsWith("icc:", StringComparison.OrdinalIgnoreCase))
|
||||
return "";
|
||||
|
||||
if (Uri.TryCreate(uri, UriKind.Absolute, out Uri uriObj))
|
||||
{
|
||||
string host = (uriObj.Host ?? "").Trim().ToLowerInvariant();
|
||||
string path = (uriObj.AbsolutePath ?? "").Trim('/').ToLowerInvariant();
|
||||
if (!string.IsNullOrEmpty(host))
|
||||
return string.IsNullOrEmpty(path) ? host : host + "/" + path;
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
return path;
|
||||
}
|
||||
|
||||
string raw = uri.Trim().Substring(4).TrimStart('/').ToLowerInvariant();
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static string GetUriQueryValue(string uri, string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uri) || string.IsNullOrEmpty(key)) return "";
|
||||
try
|
||||
{
|
||||
if (!Uri.TryCreate(uri, UriKind.Absolute, out Uri u) || string.IsNullOrEmpty(u.Query))
|
||||
return "";
|
||||
string q = u.Query.TrimStart('?');
|
||||
foreach (var pair in q.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var kv = pair.Split(new[] { '=' }, 2, StringSplitOptions.None);
|
||||
if (kv.Length == 2 && kv[0].Trim().Equals(key, StringComparison.OrdinalIgnoreCase))
|
||||
return Uri.UnescapeDataString(kv[1].Trim());
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { LogHelper.WriteLogToFile($"解析 URI 参数失败: {ex.Message}", LogHelper.LogType.Warning); }
|
||||
return "";
|
||||
}
|
||||
|
||||
private const string ConfigProfileListTempFile = "InkCanvasConfigProfileList.json";
|
||||
private const string ConfigProfileSwitchResultTempFile = "InkCanvasConfigProfileSwitchResult.txt";
|
||||
|
||||
private void WriteConfigProfileListToTemp()
|
||||
{
|
||||
try
|
||||
{
|
||||
var names = ConfigProfileManager.ListProfileNames();
|
||||
var current = _lastAppliedProfileName ?? "";
|
||||
var payload = new { list = names, current = current };
|
||||
string path = Path.Combine(Path.GetTempPath(), ConfigProfileListTempFile);
|
||||
File.WriteAllText(path, JsonConvert.SerializeObject(payload, Formatting.Indented), System.Text.Encoding.UTF8);
|
||||
LogHelper.WriteLogToFile($"URI 已输出配置方案列表到: {path}", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"URI 输出配置方案列表失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleUriConfigProfileSwitch(string profileName)
|
||||
{
|
||||
string resultPath = Path.Combine(Path.GetTempPath(), ConfigProfileSwitchResultTempFile);
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(profileName))
|
||||
{
|
||||
File.WriteAllText(resultPath, "error: 缺少参数 name", System.Text.Encoding.UTF8);
|
||||
LogHelper.WriteLogToFile("URI 切换配置方案: 未指定方案名", LogHelper.LogType.Warning);
|
||||
return;
|
||||
}
|
||||
if (!ConfigProfileManager.ApplyProfile(profileName.Trim()))
|
||||
{
|
||||
File.WriteAllText(resultPath, "error: 方案不存在或应用失败", System.Text.Encoding.UTF8);
|
||||
ShowNotification($"切换失败:方案「{profileName}」不存在");
|
||||
return;
|
||||
}
|
||||
_lastAppliedProfileName = profileName.Trim();
|
||||
ReloadSettingsFromFile();
|
||||
RefreshConfigProfileList();
|
||||
File.WriteAllText(resultPath, "ok", System.Text.Encoding.UTF8);
|
||||
ShowNotification($"已通过 URI 切换至方案「{profileName}」");
|
||||
LogHelper.WriteLogToFile($"URI 已切换配置方案: {profileName}", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { File.WriteAllText(resultPath, "error: " + ex.Message, System.Text.Encoding.UTF8); } catch { }
|
||||
LogHelper.WriteLogToFile($"URI 切换配置方案失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
using Ink_Canvas.Properties;
|
||||
using System;
|
||||
using System.Windows.Markup;
|
||||
|
||||
namespace Ink_Canvas.MarkupExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// XAML 中用键名取本地化字符串,无需在 Strings.Designer.cs 中为每个键添加属性。
|
||||
/// 用法:xmlns:i18n="clr-namespace:Ink_Canvas.MarkupExtensions" 然后 Text="{i18n:I18n Key=Settings_Title}"
|
||||
/// </summary>
|
||||
public class I18nExtension : MarkupExtension
|
||||
{
|
||||
public string Key { get; set; }
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return string.IsNullOrEmpty(Key) ? string.Empty : (Strings.GetString(Key) ?? $"#{Key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Ink_Canvas.Models
|
||||
{
|
||||
public class CapturedImage
|
||||
{
|
||||
public BitmapImage Image { get; }
|
||||
public BitmapImage Thumbnail { get; }
|
||||
public StrokeCollection Strokes { get; }
|
||||
public string Timestamp { get; }
|
||||
public string FilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的位图创建一个 CapturedImage 实例,并为其生成缩略图、空白笔划集合和时间戳。
|
||||
/// </summary>
|
||||
/// <param name="image">用于初始化的位图;不能为空。传入的图像将在内部确保为冻结状态以便安全跨线程使用。</param>
|
||||
/// <exception cref="System.ArgumentNullException">当 <paramref name="image"/> 为 null 时抛出。</exception>
|
||||
public CapturedImage(BitmapImage image)
|
||||
{
|
||||
if (image == null)
|
||||
throw new ArgumentNullException(nameof(image), "图像不能为空");
|
||||
|
||||
// 确保 Image 被冻结,避免跨线程访问风险
|
||||
Image = EnsureFrozen(image);
|
||||
Thumbnail = CreateThumbnail(Image);
|
||||
Strokes = new StrokeCollection();
|
||||
Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
FilePath = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 CapturedImage 实例:将指定图像冻结用于线程安全、生成缩略图并初始化空的笔迹集合,同时设置文件路径和时间戳(尝试从文件名提取时间戳,失败则使用当前时间)。
|
||||
/// </summary>
|
||||
/// <param name="image">源图像,不能为空。</param>
|
||||
/// <param name="filePath">关联文件的路径,可能为 null。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="image"/> 为 null 时抛出。</exception>
|
||||
public CapturedImage(BitmapImage image, string filePath)
|
||||
{
|
||||
if (image == null)
|
||||
throw new ArgumentNullException(nameof(image), "图像不能为空");
|
||||
|
||||
// 确保 Image 被冻结,避免跨线程访问风险
|
||||
Image = EnsureFrozen(image);
|
||||
Thumbnail = CreateThumbnail(Image);
|
||||
Strokes = new StrokeCollection();
|
||||
FilePath = filePath;
|
||||
Timestamp = TryExtractTimestampFromFilePath(filePath) ?? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从给定文件路径的文件名中解析并返回规范化的时间戳。
|
||||
/// </summary>
|
||||
/// <param name="filePath">要从其文件名中解析时间戳的文件路径;可以为 null 或空字符串。</param>
|
||||
/// <returns>解析得到的时间戳,格式为 "yyyy-MM-dd HH:mm:ss.fff";无法解析时返回 null。</returns>
|
||||
private static string TryExtractTimestampFromFilePath(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath)) return null;
|
||||
var name = System.IO.Path.GetFileNameWithoutExtension(filePath);
|
||||
if (DateTime.TryParseExact(
|
||||
name,
|
||||
"yyyy-MM-dd HH-mm-ss-fff",
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None,
|
||||
out var dt))
|
||||
{
|
||||
return dt.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
}
|
||||
if (name.Length >= 23)
|
||||
{
|
||||
var tail = name.Substring(name.Length - 23);
|
||||
if (DateTime.TryParseExact(
|
||||
tail,
|
||||
"yyyy-MM-dd HH-mm-ss-fff",
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.None,
|
||||
out var dt2))
|
||||
{
|
||||
return dt2.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保并返回一个已冻结的 BitmapImage 副本,以便在跨线程场景中安全使用。
|
||||
/// </summary>
|
||||
/// <param name="image">要确保为冻结状态的源 BitmapImage。</param>
|
||||
/// <returns>与输入图像内容一致且已调用 Freeze 的 BitmapImage 实例。</returns>
|
||||
/// <exception cref="ArgumentNullException">在 <paramref name="image"/> 为 null 时抛出。</exception>
|
||||
private static BitmapImage EnsureFrozen(BitmapImage image)
|
||||
{
|
||||
if (image == null)
|
||||
throw new ArgumentNullException(nameof(image));
|
||||
|
||||
if (image.IsFrozen)
|
||||
return image;
|
||||
|
||||
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(image));
|
||||
|
||||
var stream = new System.IO.MemoryStream();
|
||||
encoder.Save(stream);
|
||||
stream.Position = 0;
|
||||
|
||||
var frozenCopy = new BitmapImage();
|
||||
frozenCopy.BeginInit();
|
||||
frozenCopy.CacheOption = BitmapCacheOption.OnLoad;
|
||||
frozenCopy.StreamSource = stream;
|
||||
frozenCopy.EndInit();
|
||||
frozenCopy.Freeze();
|
||||
|
||||
return frozenCopy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成并返回一个在 290×180 约束内按比例缩放并已冻结的缩略图。
|
||||
/// </summary>
|
||||
/// <param name="original">用于生成缩略图的源 <see cref="BitmapImage"/>;不得为 <c>null</c>,且其像素宽度和高度必须大于 0。</param>
|
||||
/// <returns>已冻结的 <see cref="BitmapImage"/> 缩略图,尺寸不超过 290×180 且保持原图纵横比。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="original"/> 为 <c>null</c> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="original"/> 的像素宽度或高度小于等于 0 时抛出。</exception>
|
||||
/// <exception cref="InvalidOperationException">当无法计算出有效的缩放比例(例如结果为 NaN、Infinity 或非正数)时抛出。</exception>
|
||||
private static BitmapImage CreateThumbnail(BitmapImage original)
|
||||
{
|
||||
if (original == null)
|
||||
throw new ArgumentNullException(nameof(original));
|
||||
|
||||
if (original.PixelWidth <= 0 || original.PixelHeight <= 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"图像尺寸无效:宽度={original.PixelWidth}, 高度={original.PixelHeight}。图像必须具有有效的像素尺寸。",
|
||||
nameof(original));
|
||||
}
|
||||
|
||||
double targetWidth = 290.0;
|
||||
double targetHeight = 180.0;
|
||||
double scale = Math.Min(targetWidth / original.PixelWidth, targetHeight / original.PixelHeight);
|
||||
|
||||
if (double.IsInfinity(scale) || double.IsNaN(scale) || scale <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"无法计算有效的缩放比例:scale={scale}, 图像尺寸={original.PixelWidth}x{original.PixelHeight}");
|
||||
}
|
||||
|
||||
var thumbnail = new TransformedBitmap(original, new System.Windows.Media.ScaleTransform(scale, scale));
|
||||
|
||||
var bmp = new JpegBitmapEncoder { QualityLevel = 85 };
|
||||
bmp.Frames.Add(BitmapFrame.Create(thumbnail));
|
||||
|
||||
using (var stream = new System.IO.MemoryStream())
|
||||
{
|
||||
bmp.Save(stream);
|
||||
stream.Seek(0, System.IO.SeekOrigin.Begin);
|
||||
|
||||
var result = new BitmapImage();
|
||||
result.BeginInit();
|
||||
result.CacheOption = BitmapCacheOption.OnLoad;
|
||||
result.StreamSource = stream;
|
||||
result.EndInit();
|
||||
result.Freeze();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ using System.Windows;
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("CJK_mkp")]
|
||||
[assembly: AssemblyProduct("InkCanvasForClass")]
|
||||
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025")]
|
||||
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025-2026")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
@@ -19,14 +19,8 @@ using System.Windows;
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
//In order to begin building localizable applications, set
|
||||
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
|
||||
//inside a <PropertyGroup>. For example, if you are using US english
|
||||
//in your source files, set the <UICulture> to en-US. Then uncomment
|
||||
//the NeutralResourceLanguage attribute below. Update the "en-US" in
|
||||
//the line below to match the UICulture setting in the project file.
|
||||
|
||||
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
|
||||
// i18n: 默认/回退语言为简体中文,与 Strings.resx 默认文案一致。
|
||||
[assembly: System.Resources.NeutralResourcesLanguage("zh-CN", System.Resources.UltimateResourceFallbackLocation.MainAssembly)]
|
||||
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
@@ -49,5 +43,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.17.0")]
|
||||
[assembly: AssemblyFileVersion("1.7.17.0")]
|
||||
[assembly: AssemblyVersion("1.7.18.8")]
|
||||
[assembly: AssemblyFileVersion("1.7.18.8")]
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// i18n 字符串资源 - 由 Strings.resx 生成,请勿直接编辑。
|
||||
// 修改文案请编辑 Strings.resx 及对应语言的 Strings.{Culture}.resx。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ink_Canvas.Properties
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地化字符串资源,用于 i18n。当前 UI 语言由 Thread.CurrentThread.CurrentUICulture 决定。
|
||||
/// </summary>
|
||||
[CompilerGenerated]
|
||||
public static class Strings
|
||||
{
|
||||
private const string EmbeddedEnUsResxName = "Ink_Canvas.Properties.Strings.enUS.xml";
|
||||
private static readonly object EnUsLock = new object();
|
||||
private static Dictionary<string, string> _embeddedEnUs;
|
||||
private static ResourceManager _resourceMan;
|
||||
private static CultureInfo _resourceCulture;
|
||||
|
||||
/// <summary>
|
||||
/// 用于此类资源查找的 ResourceManager。
|
||||
/// </summary>
|
||||
public static ResourceManager ResourceManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ReferenceEquals(_resourceMan, null))
|
||||
{
|
||||
var temp = new ResourceManager("Ink_Canvas.Properties.Strings", typeof(Strings).Assembly);
|
||||
_resourceMan = temp;
|
||||
}
|
||||
return _resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写当前线程的 CurrentUICulture,以在此强类型资源类中进行资源查找。
|
||||
/// 若不设置,则使用 Thread.CurrentThread.CurrentUICulture。
|
||||
/// </summary>
|
||||
public static CultureInfo Culture
|
||||
{
|
||||
get => _resourceCulture;
|
||||
set => _resourceCulture = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据键获取本地化字符串;若未设置 Culture 则使用当前线程 CurrentUICulture。
|
||||
/// </summary>
|
||||
public static string GetString(string key)
|
||||
{
|
||||
var culture = _resourceCulture ?? CultureInfo.CurrentUICulture;
|
||||
if (IsEnglishCulture(culture))
|
||||
{
|
||||
var enUsMap = GetEmbeddedEnUsMap();
|
||||
if (enUsMap.TryGetValue(key, out var enValue))
|
||||
{
|
||||
return enValue;
|
||||
}
|
||||
}
|
||||
return ResourceManager.GetString(key, culture);
|
||||
}
|
||||
|
||||
private static bool IsEnglishCulture(CultureInfo culture)
|
||||
{
|
||||
if (culture == null) return false;
|
||||
if (culture.Name.StartsWith("en", System.StringComparison.OrdinalIgnoreCase)) return true;
|
||||
return culture.TwoLetterISOLanguageName.Equals("en", System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> GetEmbeddedEnUsMap()
|
||||
{
|
||||
if (_embeddedEnUs != null) return _embeddedEnUs;
|
||||
lock (EnUsLock)
|
||||
{
|
||||
if (_embeddedEnUs != null) return _embeddedEnUs;
|
||||
var map = new Dictionary<string, string>(System.StringComparer.Ordinal);
|
||||
var asm = typeof(Strings).Assembly;
|
||||
using (var stream = asm.GetManifestResourceStream(EmbeddedEnUsResxName))
|
||||
{
|
||||
if (stream != null)
|
||||
{
|
||||
using (var reader = new ResXResourceReader(stream))
|
||||
{
|
||||
foreach (DictionaryEntry entry in reader)
|
||||
{
|
||||
if (entry.Key is string k && entry.Value is string v)
|
||||
{
|
||||
map[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_embeddedEnUs = map;
|
||||
return _embeddedEnUs;
|
||||
}
|
||||
}
|
||||
|
||||
public static string Nav_Plugins => GetString(nameof(Nav_Plugins)) ?? "插件";
|
||||
public static string Nav_Startup => GetString(nameof(Nav_Startup)) ?? "启动设置";
|
||||
public static string Nav_Canvas => GetString(nameof(Nav_Canvas)) ?? "画布设置";
|
||||
public static string Nav_CrashAction => GetString(nameof(Nav_CrashAction)) ?? "崩溃处理";
|
||||
public static string Nav_Gesture => GetString(nameof(Nav_Gesture)) ?? "手势";
|
||||
public static string Nav_InkRecognition => GetString(nameof(Nav_InkRecognition)) ?? "墨迹识别";
|
||||
public static string Nav_PPT => GetString(nameof(Nav_PPT)) ?? "PPT";
|
||||
public static string Nav_Advanced => GetString(nameof(Nav_Advanced)) ?? "高级";
|
||||
public static string Nav_Automation => GetString(nameof(Nav_Automation)) ?? "自动化";
|
||||
public static string Nav_RandomWindow => GetString(nameof(Nav_RandomWindow)) ?? "随机选人";
|
||||
public static string Nav_Theme => GetString(nameof(Nav_Theme)) ?? "主题";
|
||||
public static string Nav_Shortcuts => GetString(nameof(Nav_Shortcuts)) ?? "快捷键";
|
||||
public static string Nav_About => GetString(nameof(Nav_About)) ?? "关于";
|
||||
public static string App_Title => GetString(nameof(App_Title)) ?? "InkCanvasforClass";
|
||||
public static string Booth_Resolution_Tooltip => GetString(nameof(Booth_Resolution_Tooltip)) ?? "展台/截图分辨率";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,757 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Nav_Plugins" xml:space="preserve">
|
||||
<value>Plugins</value>
|
||||
</data>
|
||||
<data name="Nav_Startup" xml:space="preserve">
|
||||
<value>Startup</value>
|
||||
</data>
|
||||
<data name="Nav_Canvas" xml:space="preserve">
|
||||
<value>Canvas</value>
|
||||
</data>
|
||||
<data name="Nav_CrashAction" xml:space="preserve">
|
||||
<value>Crash Action</value>
|
||||
</data>
|
||||
<data name="Nav_Gesture" xml:space="preserve">
|
||||
<value>Gesture</value>
|
||||
</data>
|
||||
<data name="Nav_InkRecognition" xml:space="preserve">
|
||||
<value>Ink Recognition</value>
|
||||
</data>
|
||||
<data name="Nav_PPT" xml:space="preserve">
|
||||
<value>PPT</value>
|
||||
</data>
|
||||
<data name="Nav_Advanced" xml:space="preserve">
|
||||
<value>Advanced</value>
|
||||
</data>
|
||||
<data name="Nav_Automation" xml:space="preserve">
|
||||
<value>Automation</value>
|
||||
</data>
|
||||
<data name="Nav_RandomWindow" xml:space="preserve">
|
||||
<value>Random Picker</value>
|
||||
</data>
|
||||
<data name="Nav_Theme" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="Nav_Shortcuts" xml:space="preserve">
|
||||
<value>Shortcuts</value>
|
||||
</data>
|
||||
<data name="Nav_About" xml:space="preserve">
|
||||
<value>About</value>
|
||||
</data>
|
||||
<data name="App_Title" xml:space="preserve">
|
||||
<value>InkCanvasforClass</value>
|
||||
</data>
|
||||
<data name="Booth_Resolution_Tooltip" xml:space="preserve">
|
||||
<value>Booth / Screenshot resolution</value>
|
||||
</data>
|
||||
<data name="Nav_Gesture_Settings" xml:space="preserve"><value>Gesture</value></data>
|
||||
<data name="Nav_Theme_Settings" xml:space="preserve"><value>Appearance</value></data>
|
||||
<data name="Nav_PPT_Settings" xml:space="preserve"><value>PPT</value></data>
|
||||
<data name="Nav_Advanced_Settings" xml:space="preserve"><value>Advanced</value></data>
|
||||
<data name="Nav_Automation_Settings" xml:space="preserve"><value>Automation</value></data>
|
||||
<data name="Nav_RandomWindow_Settings" xml:space="preserve"><value>Random Picker</value></data>
|
||||
<data name="Nav_Shortcuts_Settings" xml:space="preserve"><value>Shortcuts</value></data>
|
||||
<data name="CollapseNavSidebar" xml:space="preserve"><value>Collapse sidebar</value></data>
|
||||
<data name="ShowNavSidebar" xml:space="preserve"><value>Show sidebar</value></data>
|
||||
<data name="Tooltip_IccProtocol" xml:space="preserve"><value>Control via icc:// protocol</value></data>
|
||||
<data name="Settings_Title" xml:space="preserve"><value>Settings</value></data>
|
||||
<data name="Settings_AutoSaveHint" xml:space="preserve"><value>Changes are saved automatically; some require restart.</value></data>
|
||||
<data name="Btn_Restart" xml:space="preserve"><value>Restart</value></data>
|
||||
<data name="Btn_Reset" xml:space="preserve"><value>Reset</value></data>
|
||||
<data name="Btn_Exit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="Settings_Mode" xml:space="preserve"><value>Mode</value></data>
|
||||
<data name="Settings_ModeDesc" xml:space="preserve"><value>Choose run mode. In PPT-only mode the app is hidden until slide show. (Experimental)</value></data>
|
||||
<data name="Mode_Normal" xml:space="preserve"><value>Normal</value></data>
|
||||
<data name="Mode_PPTOnly" xml:space="preserve"><value>PPT only</value></data>
|
||||
<data name="Settings_NewWindow" xml:space="preserve"><value>New settings window</value></data>
|
||||
<data name="Settings_NewWindowDesc" xml:space="preserve"><value>Open a new settings window. (In development)</value></data>
|
||||
<data name="Btn_OpenNewSettings" xml:space="preserve"><value>Open new settings</value></data>
|
||||
<data name="Settings_Plugins" xml:space="preserve"><value>Plugins</value></data>
|
||||
<data name="Settings_PluginsDesc" xml:space="preserve"><value>Extend Ink Canvas with plugins. Enable, disable, or load custom plugins.</value></data>
|
||||
<data name="Btn_OpenPluginManager" xml:space="preserve"><value>Open plugin manager</value></data>
|
||||
<data name="Startup_Start" xml:space="preserve"><value>Startup</value></data>
|
||||
<data name="Startup_NoFocusMode" xml:space="preserve"><value>No-focus mode</value></data>
|
||||
<data name="Startup_NoBorderMode" xml:space="preserve"><value>Borderless</value></data>
|
||||
<data name="Startup_TopMost" xml:space="preserve"><value>Topmost</value></data>
|
||||
<data name="Startup_UIATopMost" xml:space="preserve"><value>UIA topmost</value></data>
|
||||
<data name="Startup_UIATopMostHint" xml:space="preserve"><value># UIA topmost requires admin to take effect.</value></data>
|
||||
<data name="Header_AutoUpdate" xml:space="preserve"><value>Auto-update</value></data>
|
||||
<data name="Header_SilentUpdate" xml:space="preserve"><value>Silent update</value></data>
|
||||
<data name="SilentUpdate_Hint" xml:space="preserve"><value># Silent update installs when app is idle.</value></data>
|
||||
<data name="Update_Channel" xml:space="preserve"><value>Update channel</value></data>
|
||||
<data name="Channel_Release" xml:space="preserve"><value>Stable (Release)</value></data>
|
||||
<data name="Channel_Preview" xml:space="preserve"><value>Preview</value></data>
|
||||
<data name="Channel_Beta" xml:space="preserve"><value>Beta</value></data>
|
||||
<data name="Channel_Hint" xml:space="preserve"><value># Stable for reliability; Preview for new features.</value></data>
|
||||
<data name="Btn_ManualUpdate" xml:space="preserve"><value>Check for updates</value></data>
|
||||
<data name="ManualUpdate_Hint" xml:space="preserve"><value># Check and download now.</value></data>
|
||||
<data name="Btn_VersionFix" xml:space="preserve"><value>Version fix</value></data>
|
||||
<data name="VersionFix_Hint" xml:space="preserve"><value># Download and install latest for current channel.</value></data>
|
||||
<data name="Btn_Rollback" xml:space="preserve"><value>Rollback</value></data>
|
||||
<data name="Rollback_Hint" xml:space="preserve"><value># Open rollback page.</value></data>
|
||||
<data name="SilentUpdate_AfterDownloadHint" xml:space="preserve"><value># When silent update is off, you will be prompted after download.</value></data>
|
||||
<data name="SilentUpdate_TimeRange" xml:space="preserve"><value>Silent update time range</value></data>
|
||||
<data name="Time_Start" xml:space="preserve"><value>Start time</value></data>
|
||||
<data name="Time_End" xml:space="preserve"><value>End time</value></data>
|
||||
<data name="TimeRange_Hint" xml:space="preserve"><value># If end < start…</value></data>
|
||||
<data name="Startup_RunAtLogin" xml:space="preserve"><value>Run at login</value></data>
|
||||
<data name="Startup_MinimizeToSidebar" xml:space="preserve"><value>Minimize to sidebar at startup</value></data>
|
||||
<data name="Canvas_AndInk" xml:space="preserve"><value>Canvas & ink</value></data>
|
||||
<data name="Canvas_ShowPenCursor" xml:space="preserve"><value>Show pen cursor</value></data>
|
||||
<data name="Canvas_PressureTouch" xml:space="preserve"><value>Pressure-sensitive touch</value></data>
|
||||
<data name="Canvas_PressureTouchHint" xml:space="preserve"><value># Touch devices will support pressure.</value></data>
|
||||
<data name="Canvas_IgnorePressure" xml:space="preserve"><value>Ignore pressure</value></data>
|
||||
<data name="Canvas_IgnorePressureHint" xml:space="preserve"><value># Ignore all device pressure.</value></data>
|
||||
<data name="Canvas_EraserSize" xml:space="preserve"><value>Eraser size</value></data>
|
||||
<data name="Size_VerySmall" xml:space="preserve"><value>Very small</value></data>
|
||||
<data name="Size_Small" xml:space="preserve"><value>Small</value></data>
|
||||
<data name="Size_Medium" xml:space="preserve"><value>Medium</value></data>
|
||||
<data name="Size_Large" xml:space="preserve"><value>Large</value></data>
|
||||
<data name="Size_VeryLarge" xml:space="preserve"><value>Very large</value></data>
|
||||
<data name="EraserSize_SwitchHint" xml:space="preserve"><value># Takes effect on next area eraser use.</value></data>
|
||||
<data name="Canvas_HideInkOnExit" xml:space="preserve"><value>Hide ink when leaving canvas</value></data>
|
||||
<data name="Canvas_HideInkOnExitHint" xml:space="preserve"><value># When enabled…</value></data>
|
||||
<data name="Canvas_ClearInkHistory" xml:space="preserve"><value>Clear ink history when clearing</value></data>
|
||||
<data name="Canvas_ClearImageOnClear" xml:space="preserve"><value>Clear images with canvas</value></data>
|
||||
<data name="Canvas_CompressImage" xml:space="preserve"><value>Compress images >1920×1080</value></data>
|
||||
<data name="Canvas_KeepAsymptote" xml:space="preserve"><value>Keep hyperbola asymptotes</value></data>
|
||||
<data name="Yes" xml:space="preserve"><value>Yes</value></data>
|
||||
<data name="No" xml:space="preserve"><value>No</value></data>
|
||||
<data name="AskEachTime" xml:space="preserve"><value>Ask each time</value></data>
|
||||
<data name="Canvas_AsymptoteHint" xml:space="preserve"><value># Disabling may cause undo bugs.</value></data>
|
||||
<data name="Canvas_ShowCircleCenter" xml:space="preserve"><value>Show circle center</value></data>
|
||||
<data name="Canvas_WPFBezier" xml:space="preserve"><value>WPF default Bezier smoothing</value></data>
|
||||
<data name="Canvas_AdvancedSmoothing" xml:space="preserve"><value>Advanced curve smoothing (recommended)</value></data>
|
||||
<data name="Canvas_InkFade" xml:space="preserve"><value>Ink fade</value></data>
|
||||
<data name="Canvas_InkFadeHint" xml:space="preserve"><value># Ink will not be drawn on canvas when enabled.</value></data>
|
||||
<data name="Canvas_InkFadeTime" xml:space="preserve"><value>Ink fade time</value></data>
|
||||
<data name="Canvas_HideFadeInPenMenu" xml:space="preserve"><value>Hide fade in pen menu</value></data>
|
||||
<data name="Canvas_HideFadeInPenMenuHint" xml:space="preserve"><value># Fade control will be hidden in pen context menu.</value></data>
|
||||
<data name="Color" xml:space="preserve"><value>Color</value></data>
|
||||
<data name="Color_Default" xml:space="preserve"><value>Default</value></data>
|
||||
<data name="Color_Black" xml:space="preserve"><value>Black</value></data>
|
||||
<data name="Color_White" xml:space="preserve"><value>White</value></data>
|
||||
<data name="Color_Red" xml:space="preserve"><value>Red</value></data>
|
||||
<data name="Color_Yellow" xml:space="preserve"><value>Yellow</value></data>
|
||||
<data name="Color_Blue" xml:space="preserve"><value>Blue</value></data>
|
||||
<data name="Color_Green" xml:space="preserve"><value>Green</value></data>
|
||||
<data name="Color_Orange" xml:space="preserve"><value>Orange</value></data>
|
||||
<data name="Color_Purple" xml:space="preserve"><value>Purple</value></data>
|
||||
<data name="Msg_UpdateReady" xml:space="preserve"><value>Update downloaded. It will install when you close the app.</value></data>
|
||||
<data name="Msg_UpdateReadyTitle" xml:space="preserve"><value>Update ready</value></data>
|
||||
<data name="Msg_UpdateDownloadFailed" xml:space="preserve"><value>Update download failed. Please check your network and try again.</value></data>
|
||||
<data name="Msg_DownloadFailedTitle" xml:space="preserve"><value>Download failed</value></data>
|
||||
<data name="Msg_SkipVersion" xml:space="preserve"><value>Version {0} skipped; you will not be prompted until a newer version is released.</value></data>
|
||||
<data name="Msg_SkipVersionTitle" xml:space="preserve"><value>Version skipped</value></data>
|
||||
<data name="Msg_UnexpectedError" xml:space="preserve"><value>An unexpected error occurred. Save your ink and restart the app.</value></data>
|
||||
<data name="Msg_RestartLimitTitle" xml:space="preserve"><value>Too many restarts</value></data>
|
||||
<data name="Msg_RestartLimit" xml:space="preserve"><value>App has restarted 5 times. Auto-restart stopped. Contact the developer or check the system.</value></data>
|
||||
<data name="Splash_Starting" xml:space="preserve"><value>Starting Ink Canvas...</value></data>
|
||||
<data name="Crash_Title" xml:space="preserve"><value>Crash action</value></data>
|
||||
<data name="Crash_Desc" xml:space="preserve"><value>Choose what to do when an unhandled exception occurs:</value></data>
|
||||
<data name="Crash_SilentRestart" xml:space="preserve"><value>Silent restart</value></data>
|
||||
<data name="Crash_NoAction" xml:space="preserve"><value>No action</value></data>
|
||||
<data name="Crash_Hint" xml:space="preserve"><value># Silent restart: automatically restart without prompt. No action: only log, do not restart.</value></data>
|
||||
<data name="Gesture_Title" xml:space="preserve"><value>Gestures</value></data>
|
||||
<data name="Gesture_AutoToggleTwoFinger" xml:space="preserve"><value>Auto-toggle two-finger move in/out of whiteboard</value></data>
|
||||
<data name="Gesture_AutoToggleHint" xml:space="preserve"><value># When enabled: leaving canvas disables two-finger move; entering whiteboard enables it.</value></data>
|
||||
<data name="Gesture_AllowRotateScale" xml:space="preserve"><value>Allow rotate & scale selected ink</value></data>
|
||||
<data name="Gesture_AllowRotateScaleHint" xml:space="preserve"><value># Allows scaling selected ink with two or more fingers (independent of rotate setting).</value></data>
|
||||
<data name="Gesture_EnablePalmEraser" xml:space="preserve"><value>Enable palm eraser</value></data>
|
||||
<data name="Gesture_PalmSensitivity" xml:space="preserve"><value>Palm eraser sensitivity</value></data>
|
||||
<data name="Gesture_PalmSensitivityLow" xml:space="preserve"><value>Low sensitivity</value></data>
|
||||
<data name="Gesture_PalmSensitivityMedium" xml:space="preserve"><value>Medium sensitivity</value></data>
|
||||
<data name="Gesture_PalmSensitivityHigh" xml:space="preserve"><value>High sensitivity</value></data>
|
||||
<data name="Gesture_PalmHint" xml:space="preserve"><value># Low: larger area/more touches required (less false positive); High: easier to trigger but may mis-detect fingers.</value></data>
|
||||
<data name="InkRecog_Title" xml:space="preserve"><value>Ink correction</value></data>
|
||||
<data name="InkRecog_EnableInkRecognition" xml:space="preserve"><value>Enable ink recognition</value></data>
|
||||
<data name="InkRecog_BlockRectFakePressure" xml:space="preserve"><value>Block fake pressure on corrected rectangles</value></data>
|
||||
<data name="InkRecog_BlockTriFakePressure" xml:space="preserve"><value>Block fake pressure on corrected triangles</value></data>
|
||||
<data name="InkRecog_FixTriangle" xml:space="preserve"><value>Correct freehand triangles</value></data>
|
||||
<data name="InkRecog_FixRectangle" xml:space="preserve"><value>Correct freehand rectangles</value></data>
|
||||
<data name="InkRecog_FixEllipse" xml:space="preserve"><value>Correct circles and ellipses</value></data>
|
||||
<data name="InkRecog_AutoStraightLine" xml:space="preserve"><value>Auto-straighten lines</value></data>
|
||||
<data name="InkRecog_LengthThreshold" xml:space="preserve"><value>Length threshold</value></data>
|
||||
<data name="InkRecog_Sensitivity" xml:space="preserve"><value>Sensitivity</value></data>
|
||||
<data name="InkRecog_HighPrecisionStraighten" xml:space="preserve"><value>High-precision straightening</value></data>
|
||||
<data name="InkRecog_HighPrecisionHint" xml:space="preserve"><value># When enabled, lines longer than the threshold will be straightened. Sensitivity 0.05–2.0: smaller = stricter; larger = easier to treat as straight. High-precision samples every 10px for better judgement.</value></data>
|
||||
<data name="InkRecog_LineEndpointSnapping" xml:space="preserve"><value>Line endpoint snapping</value></data>
|
||||
<data name="InkRecog_SnappingDistance" xml:space="preserve"><value>Snapping distance</value></data>
|
||||
<data name="Theme_GroupTitle" xml:space="preserve"><value>Personalization</value></data>
|
||||
<data name="Theme_Label" xml:space="preserve"><value>Theme</value></data>
|
||||
<data name="Theme_Light" xml:space="preserve"><value>Light theme</value></data>
|
||||
<data name="Theme_Dark" xml:space="preserve"><value>Dark theme</value></data>
|
||||
<data name="Theme_System" xml:space="preserve"><value>Follow system</value></data>
|
||||
<data name="Theme_EnableSplash" xml:space="preserve"><value>Enable startup animation</value></data>
|
||||
<data name="Theme_SplashStyle" xml:space="preserve"><value>Startup animation style</value></data>
|
||||
<data name="Theme_Splash_Random" xml:space="preserve"><value>Random</value></data>
|
||||
<data name="Theme_Splash_Seasonal" xml:space="preserve"><value>Follow seasons</value></data>
|
||||
<data name="Theme_Splash_Spring" xml:space="preserve"><value>Spring</value></data>
|
||||
<data name="Theme_Splash_Summer" xml:space="preserve"><value>Summer</value></data>
|
||||
<data name="Theme_Splash_Autumn" xml:space="preserve"><value>Autumn</value></data>
|
||||
<data name="Theme_Splash_Winter" xml:space="preserve"><value>Winter</value></data>
|
||||
<data name="Theme_Splash_Horse" xml:space="preserve"><value>Year-of-Horse special</value></data>
|
||||
<data name="Theme_FloatingBarIcon" xml:space="preserve"><value>Floating toolbar icon</value></data>
|
||||
<data name="Theme_FloatingIcon_IccDefault" xml:space="preserve"><value>“ICC-CE” default</value></data>
|
||||
<data name="Theme_FloatingIcon_IccNoShadow" xml:space="preserve"><value>“ICC-CE” no shadow</value></data>
|
||||
<data name="Theme_FloatingIcon_IccDark" xml:space="preserve"><value>“ICC-CE” dark</value></data>
|
||||
<data name="Theme_FloatingIcon_IccDarkBreath" xml:space="preserve"><value>“ICC-CE” dark breathing</value></data>
|
||||
<data name="Theme_FloatingIcon_IccWhiteTransparent" xml:space="preserve"><value>“ICC-CE” white transparent</value></data>
|
||||
<data name="Theme_FloatingIcon_IccBlackTransparent" xml:space="preserve"><value>“ICC-CE” black transparent</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkCrossEye" xml:space="preserve"><value>Coolapk cross-eye emoji</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkAbused" xml:space="preserve"><value>Coolapk abused emoji</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkSmile" xml:space="preserve"><value>Coolapk grin emoji</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkUnderwear" xml:space="preserve"><value>Coolapk underwear emoji</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkGreenHatDoge" xml:space="preserve"><value>Coolapk green-hat Doge</value></data>
|
||||
<data name="Theme_FloatingIcon_TiebaEmoji" xml:space="preserve"><value>Tieba emoji</value></data>
|
||||
<data name="Theme_CustomFloatingIconLabel" xml:space="preserve"><value>Custom floating icon</value></data>
|
||||
<data name="Theme_Upload" xml:space="preserve"><value>Upload</value></data>
|
||||
<data name="Theme_Manage" xml:space="preserve"><value>Manage</value></data>
|
||||
<data name="Theme_FloatingBarScale" xml:space="preserve"><value>Floating toolbar scale</value></data>
|
||||
<data name="Theme_FloatingBarOpacity" xml:space="preserve"><value>Floating toolbar opacity</value></data>
|
||||
<data name="Theme_FloatingBarOpacityInPPT" xml:space="preserve"><value>Floating bar opacity in PPT</value></data>
|
||||
<data name="Theme_FloatingBarOpacityInPPTHint" xml:space="preserve"><value># Takes effect after re-entering slide show</value></data>
|
||||
<data name="Theme_ShowNibButton" xml:space="preserve"><value>Show nib-mode button in palette</value></data>
|
||||
<data name="Theme_BlackboardScale80" xml:space="preserve"><value>Whiteboard UI 80% scale</value></data>
|
||||
<data name="Theme_ShowTimeInWhiteboard" xml:space="preserve"><value>Show time and date in whiteboard</value></data>
|
||||
<data name="Theme_ShowQuoteInWhiteboard" xml:space="preserve"><value>Show quotes in whiteboard</value></data>
|
||||
<data name="Theme_QuoteSource" xml:space="preserve"><value>Where is the quote from?</value></data>
|
||||
<data name="Theme_QuoteSource_OsuQuotes" xml:space="preserve"><value>osu! player quotes</value></data>
|
||||
<data name="Theme_QuoteSource_Mottos" xml:space="preserve"><value>Inspirational mottos</value></data>
|
||||
<data name="Theme_QuoteSource_GaokaoBless" xml:space="preserve"><value>Gaokao blessings</value></data>
|
||||
<data name="Theme_QuoteSource_Hitokoto" xml:space="preserve"><value>Hitokoto API</value></data>
|
||||
<data name="Theme_Customize" xml:space="preserve"><value>Custom</value></data>
|
||||
<data name="Theme_EnableQuickPanel" xml:space="preserve"><value>Enable quick panel in docked mode</value></data>
|
||||
<data name="Theme_UnfoldButtonIcon" xml:space="preserve"><value>Un-dock button icon</value></data>
|
||||
<data name="Theme_UnfoldIcon_Arrow" xml:space="preserve"><value>Arrow</value></data>
|
||||
<data name="Theme_UnfoldIcon_Pen" xml:space="preserve"><value>Pen</value></data>
|
||||
<data name="Theme_FloatingBarButtonsTitle" xml:space="preserve"><value>Floating bar buttons</value></data>
|
||||
<data name="Theme_UseLegacyFloatingBarUI" xml:space="preserve"><value>Use legacy floating bar UI</value></data>
|
||||
<data name="Theme_ShowShapeButton" xml:space="preserve"><value>Show shape button</value></data>
|
||||
<data name="Theme_ShowUndoButton" xml:space="preserve"><value>Show undo button</value></data>
|
||||
<data name="Theme_ShowRedoButton" xml:space="preserve"><value>Show redo button</value></data>
|
||||
<data name="Theme_ShowClearButton" xml:space="preserve"><value>Show clear button</value></data>
|
||||
<data name="Theme_ShowWhiteboardButton" xml:space="preserve"><value>Show whiteboard button</value></data>
|
||||
<data name="Theme_ShowHideButton" xml:space="preserve"><value>Show hide button</value></data>
|
||||
<data name="Theme_ShowLassoButton" xml:space="preserve"><value>Show lasso select button</value></data>
|
||||
<data name="Theme_ShowClearAndMouseButton" xml:space="preserve"><value>Show clear+mouse button</value></data>
|
||||
<data name="Theme_ShowQuickPalette" xml:space="preserve"><value>Show quick palette</value></data>
|
||||
<data name="Theme_QuickPaletteMode" xml:space="preserve"><value>Quick palette display mode</value></data>
|
||||
<data name="Theme_QuickPalette_SingleRow" xml:space="preserve"><value>Single row (6 colors)</value></data>
|
||||
<data name="Theme_QuickPalette_DoubleRow" xml:space="preserve"><value>Double row (8 colors)</value></data>
|
||||
<data name="Theme_EraserButtonDisplay" xml:space="preserve"><value>Eraser button display</value></data>
|
||||
<data name="Theme_EraserDisplay_Both" xml:space="preserve"><value>Show both</value></data>
|
||||
<data name="Theme_EraserDisplay_AreaOnly" xml:space="preserve"><value>Area eraser only</value></data>
|
||||
<data name="Theme_EraserDisplay_LineOnly" xml:space="preserve"><value>Line eraser only</value></data>
|
||||
<data name="Theme_EraserDisplay_None" xml:space="preserve"><value>Hide all</value></data>
|
||||
<data name="Tray_GroupTitle" xml:space="preserve"><value>Taskbar tray icon</value></data>
|
||||
<data name="Tray_EnableTrayIcon" xml:space="preserve"><value>Enable tray icon</value></data>
|
||||
<data name="PPT_GroupTitle" xml:space="preserve"><value>PPT integration</value></data>
|
||||
<data name="PPT_GroupHint" xml:space="preserve"><value>These settings apply during slide show and override others.</value></data>
|
||||
<data name="PPT_SupportPowerPoint" xml:space="preserve"><value>Microsoft PowerPoint support</value></data>
|
||||
<data name="PPT_Enhancement" xml:space="preserve"><value>PowerPoint enhancement</value></data>
|
||||
<data name="PPT_SkipAnimations" xml:space="preserve"><value>Steal focus to skip animations (PPT)</value></data>
|
||||
<data name="PPT_UseRot" xml:space="preserve"><value>Use ROT integration</value></data>
|
||||
<data name="PPT_SupportWPS" xml:space="preserve"><value>WPS support</value></data>
|
||||
<data name="PPT_KillWppProcess" xml:space="preserve"><value>Kill WPP process (avoid leftovers)</value></data>
|
||||
<data name="PPT_KillWppHint" xml:space="preserve"><value># When disabled, leftover WPP processes may cause slow close or cannot exit completely.</value></data>
|
||||
<data name="PPT_WpsHint1" xml:space="preserve"><value># If you only use PowerPoint, do not enable WPS integration. If you use WPS, it is recommended not to use PowerPoint together.</value></data>
|
||||
<data name="PPT_WpsLagWarning" xml:space="preserve"><value>Enabling WPS support may cause lag when closing WPS!</value></data>
|
||||
<data name="PPT_WpsSupportHint" xml:space="preserve"><value># WPS is supported, but MS Office and WPS cannot be supported at the same time. To enable WPS support, make sure “WPS Office compatibility with third-party systems and software” is enabled in the WPS config tool, otherwise WPS cannot be detected.</value></data>
|
||||
<data name="Canvas_HideStrokeWhenSelecting" xml:space="preserve"><value>Hide ink when exiting board mode</value></data>
|
||||
<data name="Canvas_HideStrokeWhenSelectingHint" xml:space="preserve"><value># When this option is on, ink will not be shown in PPT mode if not in annotation mode.</value></data>
|
||||
<data name="Canvas_ClearInkAlsoClearHistory" xml:space="preserve"><value>Clear ink history when clearing ink</value></data>
|
||||
<data name="Canvas_ClearCanvasAlsoClearImages" xml:space="preserve"><value>Clear images when clearing canvas</value></data>
|
||||
<data name="Canvas_CompressPicturesUploaded" xml:space="preserve"><value>Auto-compress images when inserting (larger than 1920x1080)</value></data>
|
||||
<data name="PPT_FlipButtonsTitle" xml:space="preserve"><value>PPT page-turn buttons</value></data>
|
||||
<data name="PPT_ShowFlipButtons" xml:space="preserve"><value>Show page-turn buttons in PPT mode</value></data>
|
||||
<data name="PPT_Position_LeftBottom" xml:space="preserve"><value>Bottom left</value></data>
|
||||
<data name="PPT_Position_RightBottom" xml:space="preserve"><value>Bottom right</value></data>
|
||||
<data name="PPT_Position_Left" xml:space="preserve"><value>Left</value></data>
|
||||
<data name="PPT_Position_Right" xml:space="preserve"><value>Right</value></data>
|
||||
<data name="PPT_LeftOffset" xml:space="preserve"><value>Left offset</value></data>
|
||||
<data name="PPT_LeftOpacity" xml:space="preserve"><value>Left opacity</value></data>
|
||||
<data name="PPT_RightOffset" xml:space="preserve"><value>Right offset</value></data>
|
||||
<data name="PPT_RightOpacity" xml:space="preserve"><value>Right opacity</value></data>
|
||||
<data name="PPT_OffsetHint" xml:space="preserve"><value># Increase for up, decrease for down; 0 = no offset, centered.</value></data>
|
||||
<data name="PPT_LeftBottomOffset" xml:space="preserve"><value>Bottom left offset</value></data>
|
||||
<data name="PPT_LeftBottomOpacity" xml:space="preserve"><value>Bottom left opacity</value></data>
|
||||
<data name="PPT_RightBottomOffset" xml:space="preserve"><value>Bottom right offset</value></data>
|
||||
<data name="PPT_RightBottomOpacity" xml:space="preserve"><value>Bottom right opacity</value></data>
|
||||
<data name="PPT_OffsetHintHorizontal" xml:space="preserve"><value># Increase for right, decrease for left; 0 = no offset, centered.</value></data>
|
||||
<data name="PPT_SideGroupTitle" xml:space="preserve"><value>Sides</value></data>
|
||||
<data name="PPT_ShowPageNumber" xml:space="preserve"><value>Show page number</value></data>
|
||||
<data name="PPT_HalfOpacity" xml:space="preserve"><value>Half opacity</value></data>
|
||||
<data name="PPT_BlackBackground" xml:space="preserve"><value>Black background</value></data>
|
||||
<data name="PPT_BottomGroupTitle" xml:space="preserve"><value>Bottom left & right</value></data>
|
||||
<data name="PPT_PageButtonClickable" xml:space="preserve"><value>PPT page button clickable</value></data>
|
||||
<data name="PPT_PageButtonClickableHint" xml:space="preserve"><value># When enabled, clicking the page button opens PowerPoint grid thumbnails. Not supported in WPS.</value></data>
|
||||
<data name="PPT_LongPressPageTurn" xml:space="preserve"><value>PPT long-press to turn page</value></data>
|
||||
<data name="PPT_LongPressPageTurnHint" xml:space="preserve"><value># When enabled, long-press on PPT page button to turn pages continuously.</value></data>
|
||||
<data name="Startup_UIAccessTopMostHint" xml:space="preserve"><value># With UIA topmost on, app needs admin to stay on top. To turn off, fully quit then start again; restart will not disable it.</value></data>
|
||||
<data name="Header_SilentUpdate" xml:space="preserve"><value>Silent update</value></data>
|
||||
<data name="Startup_SilentUpdateHint" xml:space="preserve"><value># Silent update installs when the app is idle; no manual action needed.</value></data>
|
||||
<data name="Startup_UpdateChannel" xml:space="preserve"><value>Update channel</value></data>
|
||||
<data name="Update_Release" xml:space="preserve"><value>Stable (Release)</value></data>
|
||||
<data name="Update_Preview" xml:space="preserve"><value>Preview</value></data>
|
||||
<data name="Update_Beta" xml:space="preserve"><value>Beta</value></data>
|
||||
<data name="Startup_UpdateChannelHint" xml:space="preserve"><value># Stable: reliable updates. Preview: new features with better stability than Beta. Beta: earliest new features.</value></data>
|
||||
<data name="Btn_ManualUpdate" xml:space="preserve"><value>Check for updates</value></data>
|
||||
<data name="Startup_ManualUpdateHint" xml:space="preserve"><value># Check and download the latest version now.</value></data>
|
||||
<data name="Btn_FixVersion" xml:space="preserve"><value>Repair installation</value></data>
|
||||
<data name="Startup_FixVersionHint" xml:space="preserve"><value># Repair downloads the latest build for the selected channel and reinstalls; use to fix broken installs.</value></data>
|
||||
<data name="Btn_HistoryRollback" xml:space="preserve"><value>Rollback to previous version</value></data>
|
||||
<data name="Startup_HistoryRollbackHint" xml:space="preserve"><value># Opens a page to manually roll back to an earlier version.</value></data>
|
||||
<data name="Startup_SilentUpdateFullHint" xml:space="preserve"><value># When silent update is off, you will be prompted after download. When on, every 10 minutes the app checks: 1) within silent-update time window 2) not in writing mode 3) not in canvas. If all pass, it will close and update.</value></data>
|
||||
<data name="Startup_SilentUpdateTimePeriod" xml:space="preserve"><value>Silent update time window</value></data>
|
||||
<data name="Startup_StartTime" xml:space="preserve"><value>Start time</value></data>
|
||||
<data name="Startup_EndTime" xml:space="preserve"><value>End time</value></data>
|
||||
<data name="Startup_TimePeriodHint" xml:space="preserve"><value># If end < start, end is next day. If start = end, window is 24h.</value></data>
|
||||
<data name="Startup_RunAtStartup" xml:space="preserve"><value>Run at startup</value></data>
|
||||
<data name="Startup_FoldAtStartup" xml:space="preserve"><value>Dock to sidebar after startup</value></data>
|
||||
<data name="Canvas_GroupTitle" xml:space="preserve"><value>Canvas and ink</value></data>
|
||||
<data name="Canvas_ShowCursor" xml:space="preserve"><value>Show pen cursor</value></data>
|
||||
<data name="Canvas_EnablePressureTouch" xml:space="preserve"><value>Enable pressure-sensitive touch</value></data>
|
||||
<data name="Canvas_EnablePressureTouchHint" xml:space="preserve"><value># When on, touch screens that support pressure will show pressure; for devices not recognized by the system.</value></data>
|
||||
<data name="Canvas_DisablePressure" xml:space="preserve"><value>Ignore pressure</value></data>
|
||||
<data name="Canvas_DisablePressureHint" xml:space="preserve"><value># When on, all strokes use uniform thickness; mutually exclusive with pressure-sensitive touch.</value></data>
|
||||
<data name="Canvas_EraserSize" xml:space="preserve"><value>Eraser size</value></data>
|
||||
<data name="Canvas_EraserSize_VerySmall" xml:space="preserve"><value>Very small</value></data>
|
||||
<data name="Canvas_EraserSize_Small" xml:space="preserve"><value>Small</value></data>
|
||||
<data name="Canvas_EraserSize_Medium" xml:space="preserve"><value>Medium</value></data>
|
||||
<data name="Canvas_EraserSize_Large" xml:space="preserve"><value>Large</value></data>
|
||||
<data name="Canvas_EraserSize_VeryLarge" xml:space="preserve"><value>Very large</value></data>
|
||||
<data name="Canvas_EraserSizeHint" xml:space="preserve"><value># Change takes effect next time you use area eraser.</value></data>
|
||||
<data name="Canvas_KeepHyperbolaAsymptote" xml:space="preserve"><value>Keep hyperbola asymptotes</value></data>
|
||||
<data name="Canvas_Yes" xml:space="preserve"><value>Yes</value></data>
|
||||
<data name="Canvas_No" xml:space="preserve"><value>No</value></data>
|
||||
<data name="Canvas_AskEachTime" xml:space="preserve"><value>Ask each time</value></data>
|
||||
<data name="Canvas_HyperbolaAsymptoteHint" xml:space="preserve"><value># If not kept, undo-related bugs may occur.</value></data>
|
||||
<data name="Canvas_ShowCircleCenter" xml:space="preserve"><value>Show circle center when drawing</value></data>
|
||||
<data name="Canvas_WPFBezierSmoothing" xml:space="preserve"><value>Use WPF default Bezier smoothing</value></data>
|
||||
<data name="Canvas_AdvancedBezierSmoothing" xml:space="preserve"><value>Use advanced curve smoothing (recommended)</value></data>
|
||||
<data name="Canvas_EnableInkFade" xml:space="preserve"><value>Enable ink fade</value></data>
|
||||
<data name="Canvas_EnableInkFadeHint" xml:space="preserve"><value># When on, ink is not committed to canvas; it fades after the set time.</value></data>
|
||||
<data name="Canvas_InkFadeTime" xml:space="preserve"><value>Ink fade time</value></data>
|
||||
<data name="Canvas_HideInkFadeInPenMenu" xml:space="preserve"><value>Hide ink fade control in pen menu</value></data>
|
||||
<data name="Canvas_HideInkFadeInPenMenuHint" xml:space="preserve"><value># When on, the pen context menu will not show the ink fade control.</value></data>
|
||||
<data name="Canvas_BrushAutoRestore" xml:space="preserve"><value>Enable brush auto-restore</value></data>
|
||||
<data name="Canvas_BrushAutoRestoreHint" xml:space="preserve"><value># When on, temporary brush changes will restore at the configured time(s) to the color/opacity/width set here.</value></data>
|
||||
<data name="Canvas_AutoRestoreTimePoints" xml:space="preserve"><value>Auto-restore time points (HH:mm, multiple with ;)</value></data>
|
||||
<data name="Canvas_RestoreColor" xml:space="preserve"><value>Restore color</value></data>
|
||||
<data name="Canvas_Color_Default" xml:space="preserve"><value>Default</value></data>
|
||||
<data name="Canvas_Color_Black" xml:space="preserve"><value>Black</value></data>
|
||||
<data name="Canvas_Color_White" xml:space="preserve"><value>White</value></data>
|
||||
<data name="Canvas_Color_Red" xml:space="preserve"><value>Red</value></data>
|
||||
<data name="Canvas_Color_Yellow" xml:space="preserve"><value>Yellow</value></data>
|
||||
<data name="Canvas_Color_Blue" xml:space="preserve"><value>Blue</value></data>
|
||||
<data name="Canvas_Color_Green" xml:space="preserve"><value>Green</value></data>
|
||||
<data name="Canvas_Color_Orange" xml:space="preserve"><value>Orange</value></data>
|
||||
<data name="Canvas_Color_Purple" xml:space="preserve"><value>Purple</value></data>
|
||||
<data name="Canvas_RestoreWidth" xml:space="preserve"><value>Restore stroke width</value></data>
|
||||
<data name="Canvas_RestoreOpacity" xml:space="preserve"><value>Restore opacity</value></data>
|
||||
<data name="Canvas_SwitchBackAfterEraser" xml:space="preserve"><value>Switch back to annotation after eraser</value></data>
|
||||
<data name="Canvas_SwitchBackAfterEraserHint" xml:space="preserve"><value># When on, after erasing, staying idle for a while will switch back to annotation mode.</value></data>
|
||||
<data name="Canvas_SwitchBackDelay" xml:space="preserve"><value>Auto switch delay</value></data>
|
||||
<data name="Canvas_SwitchBackDelayHint" xml:space="preserve"><value># If you erase again within the delay, the timer resets.</value></data>
|
||||
<data name="InkRecog_LineEndpointSnappingHint" xml:space="preserve"><value># When on, line endpoints near other endpoints will snap and connect.</value></data>
|
||||
<data name="PPT_EnterAnnotationOnShow" xml:space="preserve"><value>Enter annotation mode when starting PPT slide show</value></data>
|
||||
<data name="PPT_ConflictWithAutoFold" xml:space="preserve"><value>Conflicts with "Auto fold when playing PPT" in Automation!</value></data>
|
||||
<data name="PPT_TwoFingerGesture" xml:space="preserve"><value>Allow two-finger gestures in slide show</value></data>
|
||||
<data name="PPT_FingerGestureSlide" xml:space="preserve"><value>Allow finger gesture to turn slides</value></data>
|
||||
<data name="PPT_FingerGestureSlideHint" xml:space="preserve"><value># When canvas is on, finger swipe (not pen) can turn slides in show mode when canvas has no ink.</value></data>
|
||||
<data name="PPT_ShowGestureButtonInShow" xml:space="preserve"><value>Show gesture buttons in PPT slide show</value></data>
|
||||
<data name="PPT_ShowGestureButtonInShowHint" xml:space="preserve"><value># When on, gesture buttons are shown in PPT slide show.</value></data>
|
||||
<data name="PPT_TimeCapsule" xml:space="preserve"><value>PPT time capsule</value></data>
|
||||
<data name="PPT_TimeCapsuleHint" xml:space="preserve"><value># When on, show time capsule in PPT show; can replace minimized timer window.</value></data>
|
||||
<data name="PPT_TimeCapsulePosition" xml:space="preserve"><value>Time capsule position:</value></data>
|
||||
<data name="PPT_TimeCapsulePos_TL" xml:space="preserve"><value>Top left</value></data>
|
||||
<data name="PPT_TimeCapsulePos_TR" xml:space="preserve"><value>Top right</value></data>
|
||||
<data name="PPT_TimeCapsulePos_Center" xml:space="preserve"><value>Top center</value></data>
|
||||
<data name="PPT_ShowQuickPanelInShow" xml:space="preserve"><value>Show quick panel in PPT slide show</value></data>
|
||||
<data name="PPT_ShowQuickPanelInShowHint" xml:space="preserve"><value># When off, quick panel is hidden in PPT slide show.</value></data>
|
||||
<data name="PPT_AutoScreenshot" xml:space="preserve"><value>Auto screenshot on slide change</value></data>
|
||||
<data name="PPT_AutoScreenshotHint" xml:space="preserve"><value># When on, auto-screenshot when turning page with ink on slide.</value></data>
|
||||
<data name="PPT_AutoSaveStrokes" xml:space="preserve"><value>Auto-save slide ink</value></data>
|
||||
<data name="PPT_AutoSaveStrokesHint" xml:space="preserve"><value># When on, ink is saved when ending slide show and loaded next time (same file and page).</value></data>
|
||||
<data name="PPT_RememberLastPage" xml:space="preserve"><value>Remember and prompt last slide position</value></data>
|
||||
<data name="PPT_RememberLastPageHint" xml:space="preserve"><value># When on, last page is recorded; choose Yes to jump to it.</value></data>
|
||||
<data name="PPT_GoToFirstPageOnReenter" xml:space="preserve"><value>Go to first slide when entering show</value></data>
|
||||
<data name="PPT_NotifyHiddenPage" xml:space="preserve"><value>Warn about hidden slides</value></data>
|
||||
<data name="PPT_NotifyAutoPlay" xml:space="preserve"><value>Warn if auto-play is enabled</value></data>
|
||||
<data name="Advanced_Title" xml:space="preserve"><value>Advanced</value></data>
|
||||
<data name="Advanced_TouchMultiplierHint" xml:space="preserve"><value>Adjust when finger-touch shows circle eraser or palm eraser is much larger than palm</value></data>
|
||||
<data name="Advanced_SpecialScreenMode" xml:space="preserve"><value>Special screen mode</value></data>
|
||||
<data name="Advanced_TouchMultiplier" xml:space="preserve"><value>Touch multiplier</value></data>
|
||||
<data name="Advanced_TouchMultiplierCalibrateHint" xml:space="preserve"><value>Tap with pen in the area below to estimate touch size multiplier</value></data>
|
||||
<data name="Advanced_TouchMultiplierValueHint" xml:space="preserve"><value># Value is for reference only</value></data>
|
||||
<data name="Advanced_EraserBindTouchMultiplier" xml:space="preserve"><value>Bind eraser to touch size multiplier</value></data>
|
||||
<data name="Advanced_EraserBindTouchHint" xml:space="preserve"><value># BoundsWidth is used as contact area threshold</value></data>
|
||||
<data name="Advanced_QuadIRMode" xml:space="preserve"><value>Quad IR mode</value></data>
|
||||
<data name="Advanced_Logging" xml:space="preserve"><value>Enable logging</value></data>
|
||||
<data name="Advanced_LogByDate" xml:space="preserve"><value>Save logs by date</value></data>
|
||||
<data name="Advanced_LogRotateHint" xml:space="preserve"><value># Log files over 512 KB are auto-deleted. With date save, logs go to Logs folder; folder is cleared when over 5 MB.</value></data>
|
||||
<data name="Advanced_ConfirmExit" xml:space="preserve"><value>Confirm exit with dialog</value></data>
|
||||
<data name="Advanced_FullScreenHelper" xml:space="preserve"><value>Enable FullScreenHelper</value></data>
|
||||
<data name="Advanced_Experimental" xml:space="preserve"><value>Experimental</value></data>
|
||||
<data name="Advanced_FullScreenHelperHint" xml:space="preserve"><value># Thanks to lindexi for FullScreenHelper; reduces taskbar pop-up and supports multi-monitor fullscreen. Disable if you see odd issues; restart ICC to apply.</value></data>
|
||||
<data name="Advanced_AvoidFullScreenHelper" xml:space="preserve"><value>Enable AvoidFullScreenHelper</value></data>
|
||||
<data name="Advanced_AvoidFullScreenHelperHint" xml:space="preserve"><value># Avoid canvas fullscreen; may fix taskbar not on top and Win11 taskbar unclickable. Can cause floating bar offset with AppBar on left/top. Restart ICC to apply.</value></data>
|
||||
<data name="Advanced_EdgeGestureUtil" xml:space="preserve"><value>Enable EdgeGestureUtil</value></data>
|
||||
<data name="Tools_MoreFeaturesTitle" xml:space="preserve"><value>More features</value></data>
|
||||
<data name="Tools_Timer" xml:space="preserve"><value>Timer</value></data>
|
||||
<data name="Tools_RandomDraw" xml:space="preserve"><value>Random draw</value></data>
|
||||
<data name="Tools_SingleDraw" xml:space="preserve"><value>Single draw</value></data>
|
||||
<data name="Tools_Save" xml:space="preserve"><value>Save</value></data>
|
||||
<data name="Tools_Open" xml:space="preserve"><value>Open...</value></data>
|
||||
<data name="Tools_Replay" xml:space="preserve"><value>Replay</value></data>
|
||||
<data name="Tools_Screenshot" xml:space="preserve"><value>Screenshot</value></data>
|
||||
<data name="Tools_Manual" xml:space="preserve"><value>Manual</value></data>
|
||||
<data name="Tools_Settings" xml:space="preserve"><value>Settings</value></data>
|
||||
<data name="QuickPanel_SingleDraw" xml:space="preserve"><value>Single draw</value></data>
|
||||
<data name="QuickPanel_RandomDraw" xml:space="preserve"><value>Random draw</value></data>
|
||||
<data name="QuickPanel_Timer" xml:space="preserve"><value>Timer</value></data>
|
||||
<data name="QuickPanel_Whiteboard" xml:space="preserve"><value>Whiteboard</value></data>
|
||||
<data name="QuickPanel_ExitShow" xml:space="preserve"><value>Exit slide show</value></data>
|
||||
<data name="QuickPanel_Show" xml:space="preserve"><value>Show</value></data>
|
||||
<data name="QuickPanel_Exit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="Backup_Title" xml:space="preserve"><value>Settings backup & restore</value></data>
|
||||
<data name="Backup_Desc" xml:space="preserve"><value># You can manually back up current settings or restore previous backups; backups are also created automatically before updates.</value></data>
|
||||
<data name="Backup_AutoBeforeUpdate" xml:space="preserve"><value>Backup before update</value></data>
|
||||
<data name="Backup_AutoPeriodic" xml:space="preserve"><value>Periodic auto-backup</value></data>
|
||||
<data name="Backup_Interval" xml:space="preserve"><value>Backup interval</value></data>
|
||||
<data name="Backup_Interval_1Day" xml:space="preserve"><value>1 day</value></data>
|
||||
<data name="Backup_Interval_3Days" xml:space="preserve"><value>3 days</value></data>
|
||||
<data name="Backup_Interval_7Days" xml:space="preserve"><value>7 days</value></data>
|
||||
<data name="Backup_Interval_14Days" xml:space="preserve"><value>14 days</value></data>
|
||||
<data name="Backup_Interval_30Days" xml:space="preserve"><value>30 days</value></data>
|
||||
<data name="Backup_Interval_DefaultHint" xml:space="preserve"><value>(default: 7 days)</value></data>
|
||||
<data name="Backup_Manual" xml:space="preserve"><value>Backup now</value></data>
|
||||
<data name="Backup_Restore" xml:space="preserve"><value>Restore backup</value></data>
|
||||
<data name="ConfigProfiles_Title" xml:space="preserve"><value>Config profiles & hot reload</value></data>
|
||||
<data name="ConfigProfiles_Desc" xml:space="preserve"><value># Selecting a profile switches and hot-reloads it; \"Save as\" saves current settings as a new profile.</value></data>
|
||||
<data name="ConfigProfiles_Label" xml:space="preserve"><value>Profile:</value></data>
|
||||
<data name="ConfigProfiles_Delete" xml:space="preserve"><value>Delete profile</value></data>
|
||||
<data name="ConfigProfiles_SaveAs" xml:space="preserve"><value>Save as profile</value></data>
|
||||
<data name="Automation_Title" xml:space="preserve"><value>Automation</value></data>
|
||||
<data name="Automation_AutoFoldTitle" xml:space="preserve"><value>Auto fold</value></data>
|
||||
<data name="AutoFold_App_SeewoBoard5" xml:space="preserve"><value>Seewo Whiteboard 5</value></data>
|
||||
<data name="AutoFold_App_SeewoCamera" xml:space="preserve"><value>Seewo Visual Presenter</value></data>
|
||||
<data name="AutoFold_App_SeewoBoard3" xml:space="preserve"><value>Seewo Whiteboard 3</value></data>
|
||||
<data name="AutoFold_App_SeewoLightBoard" xml:space="preserve"><value>Seewo Lite Whiteboard</value></data>
|
||||
<data name="AutoFold_App_SeewoLightBoard5C" xml:space="preserve"><value>Seewo Lite Whiteboard 5C</value></data>
|
||||
<data name="AutoFold_App_SeewoPinco" xml:space="preserve"><value>Seewo Pinco</value></data>
|
||||
<data name="AutoFold_App_HiteBoard" xml:space="preserve"><value>HiteBoard</value></data>
|
||||
<data name="AutoFold_App_HiteCamera" xml:space="preserve"><value>Hite visual presenter</value></data>
|
||||
<data name="AutoFold_App_HiteLightBoard" xml:space="preserve"><value>Hite Lite Whiteboard</value></data>
|
||||
<data name="AutoFold_App_WenXiangBoard" xml:space="preserve"><value>WenXiang Whiteboard</value></data>
|
||||
<data name="AutoFold_App_MSWhiteboard" xml:space="preserve"><value>Microsoft Whiteboard</value></data>
|
||||
<data name="AutoFold_App_AdmoxBoard" xml:space="preserve"><value>Admox Whiteboard</value></data>
|
||||
<data name="AutoFold_App_AdmoxBooth" xml:space="preserve"><value>Admox visual presenter</value></data>
|
||||
<data name="AutoFold_App_YiYunBoard" xml:space="preserve"><value>YiYun Whiteboard</value></data>
|
||||
<data name="AutoFold_App_YiYunBooth" xml:space="preserve"><value>YiYun visual presenter</value></data>
|
||||
<data name="AutoFold_App_MaxHubBoard" xml:space="preserve"><value>MaxHub Whiteboard</value></data>
|
||||
<data name="AutoFold_IgnoreEasiNoteDesktopAnno" xml:space="preserve"><value>Ignore EN5 desktop annotation window when auto folding</value></data>
|
||||
<data name="AutoFold_OldZyBoard" xml:space="preserve"><value>Auto fold when entering old ZhongYuan whiteboard</value></data>
|
||||
<data name="Automation_AutoFoldInPPT" xml:space="preserve"><value>Auto fold while playing PPT</value></data>
|
||||
<data name="Automation_KeepFoldAfterExit" xml:space="preserve"><value>Keep folded after app exit</value></data>
|
||||
<data name="Automation_KeepFoldAfterExitHint" xml:space="preserve"><value># When on, apps that trigger auto fold will stay folded even after they exit.</value></data>
|
||||
<data name="AutoKill_Title" xml:space="preserve"><value>Auto kill</value></data>
|
||||
<data name="AutoKill_PptTools" xml:space="preserve"><value>Auto kill Seewo PPT tools</value></data>
|
||||
<data name="AutoKill_PptToolsHint" xml:space="preserve"><value># Killing PPT tools disables Seewo classroom helper. Delete Office.dll in its install folder to stop the PPT toolbar without auto kill.</value></data>
|
||||
<data name="AutoKill_EasiNote5" xml:space="preserve"><value>Auto kill Seewo Whiteboard 5</value></data>
|
||||
<data name="AutoKill_HiteAnnotation" xml:space="preserve"><value>Auto kill Hite screen writing</value></data>
|
||||
<data name="AutoKill_HiteAfterKillEnterAnnotation" xml:space="preserve"><value>Enter annotation after killing Hite screen writing</value></data>
|
||||
<data name="AutoKill_YouJiao" xml:space="preserve"><value>Auto kill YouJiao teacher</value></data>
|
||||
<data name="AutoKill_SeewoDesktop2Anno" xml:space="preserve"><value>Auto kill Seewo Desktop 2.0 annotation</value></data>
|
||||
<data name="AutoKill_SeewoDesktop2AnnoHint" xml:space="preserve"><value># Seewo Desktop 2.0 annotation is 64-bit so ICC (32-bit) cannot inspect it deeply; only process name DesktopAnnotation is matched. If you have another app with the same name, keep this off.</value></data>
|
||||
<data name="AutoKill_SameAppTitle" xml:space="preserve"><value>Kill similar apps</value></data>
|
||||
<data name="AutoKill_InkCanvasIC" xml:space="preserve"><value>Auto kill Ink Canvas and IC+</value></data>
|
||||
<data name="AutoKill_ICA" xml:space="preserve"><value>Auto kill ICA (both new & old)</value></data>
|
||||
<data name="AutoKill_Inkeys" xml:space="preserve"><value>Auto kill Inkeys (new only)</value></data>
|
||||
<data name="FileAssoc_Title" xml:space="preserve"><value>File association</value></data>
|
||||
<data name="FileAssoc_Desc" xml:space="preserve"><value>Manage .icstk file association so double-click opens in Ink Canvas.</value></data>
|
||||
<data name="FileAssoc_Unregister" xml:space="preserve"><value>Remove association</value></data>
|
||||
<data name="FileAssoc_Check" xml:space="preserve"><value>Check status</value></data>
|
||||
<data name="FileAssoc_Register" xml:space="preserve"><value>Register association</value></data>
|
||||
<data name="FloatingInterceptor_Title" xml:space="preserve"><value>Floating window interceptor</value></data>
|
||||
<data name="FloatingInterceptor_Desc" xml:space="preserve"><value>Detect and block floating windows from similar software</value></data>
|
||||
<data name="FloatingInterceptor_Enable" xml:space="preserve"><value>Enable floating window interceptor</value></data>
|
||||
<data name="FloatingInterceptor_StatusNotRunning" xml:space="preserve"><value>Interceptor not running</value></data>
|
||||
<data name="Storage_AutoScreenshotOnClear" xml:space="preserve"><value>Auto screenshot on clear</value></data>
|
||||
<data name="Storage_ScreenshotsByDateFolder" xml:space="preserve"><value>Save screenshots in date folders</value></data>
|
||||
<data name="Storage_AutoSaveInkOnScreenshot" xml:space="preserve"><value>Auto-save ink when screenshotting</value></data>
|
||||
<data name="Storage_AutoSaveInk" xml:space="preserve"><value>Auto-save ink periodically</value></data>
|
||||
<data name="Storage_AutoSaveInterval" xml:space="preserve"><value>Save interval</value></data>
|
||||
<data name="Storage_AutoSaveInterval_1Min" xml:space="preserve"><value>1 minute</value></data>
|
||||
<data name="Storage_AutoSaveInterval_3Min" xml:space="preserve"><value>3 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_5Min" xml:space="preserve"><value>5 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_10Min" xml:space="preserve"><value>10 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_15Min" xml:space="preserve"><value>15 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_30Min" xml:space="preserve"><value>30 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_60Min" xml:space="preserve"><value>60 minutes</value></data>
|
||||
<data name="Storage_AutoSaveHint" xml:space="preserve"><value># When on, strokes are auto-saved at the set interval, only when canvas is visible and has ink.</value></data>
|
||||
<data name="Storage_SaveFullPageStrokes" xml:space="preserve"><value>Save full-page strokes</value></data>
|
||||
<data name="Storage_SaveFullPageHint" xml:space="preserve"><value># When on, auto/manual saves store all pages in fullscreen; multiple pages are packed in one archive (whiteboard strokes open only in whiteboard mode; PPT strokes only in slide show mode).</value></data>
|
||||
<data name="Storage_SaveAsXml" xml:space="preserve"><value>Save as XML format</value></data>
|
||||
<data name="Storage_SaveAsXmlHint" xml:space="preserve"><value># When on, strokes are saved as XML (ISF) for easier inspection and editing.</value></data>
|
||||
<data name="Storage_AutoScreenshotMinInk" xml:space="preserve"><value>Minimum ink for auto screenshot</value></data>
|
||||
<data name="Storage_PathTitle" xml:space="preserve"><value>Stroke and screenshot save path</value></data>
|
||||
<data name="Storage_PathBrowse" xml:space="preserve"><value>Browse</value></data>
|
||||
<data name="Storage_PathSetToD" xml:space="preserve"><value>Set save path to D:\Ink Canvas</value></data>
|
||||
<data name="Storage_PathSetToDocuments" xml:space="preserve"><value>Set save path to Documents</value></data>
|
||||
<data name="Storage_PathPermissionHint" xml:space="preserve"><value># Please ensure the save folder is writable.</value></data>
|
||||
<data name="Storage_AutoDeleteTitle" xml:space="preserve"><value>Auto delete old strokes and screenshots</value></data>
|
||||
<data name="Storage_AutoDeleteHint" xml:space="preserve"><value># When on, all .icstk and .png files in the auto-save folder may be deleted!</value></data>
|
||||
<data name="Storage_RetentionTitle" xml:space="preserve"><value>Retention duration</value></data>
|
||||
<data name="Storage_RetentionUnitDays" xml:space="preserve"><value>days</value></data>
|
||||
<data name="CloudStorage_Manage" xml:space="preserve"><value>Cloud storage management</value></data>
|
||||
<data name="FoldMode_Title" xml:space="preserve"><value>Fold mode</value></data>
|
||||
<data name="FoldMode_ExitToAnnotation" xml:space="preserve"><value>Switch to annotation when exiting fold mode</value></data>
|
||||
<data name="FoldMode_ExitToAnnotationHint" xml:space="preserve"><value># When on, exiting fold mode switches back to annotation for convenience.</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterPPT" xml:space="preserve"><value>Auto fold floating bar after PPT show</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterPPTHint" xml:space="preserve"><value># When on, floating bar is auto-folded after exiting PPT slide show.</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterWhiteboard" xml:space="preserve"><value>Auto fold when exiting whiteboard</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterWhiteboardHint" xml:space="preserve"><value># When on, exiting whiteboard folds back to sidebar.</value></data>
|
||||
<data name="Random_Title" xml:space="preserve"><value>Random roll call</value></data>
|
||||
<data name="Random_ShowEditNamesButton" xml:space="preserve"><value>Show button to edit name list</value></data>
|
||||
<data name="Random_BackgroundSettingsTitle" xml:space="preserve"><value>Roll-call window background (legacy UI only)</value></data>
|
||||
<data name="Random_BackgroundSelectLabel" xml:space="preserve"><value>Background:</value></data>
|
||||
<data name="Random_Background_Default" xml:space="preserve"><value>Default background</value></data>
|
||||
<data name="Random_CustomBackgroundLabel" xml:space="preserve"><value>Custom background:</value></data>
|
||||
<data name="Random_CustomBackground_Upload" xml:space="preserve"><value>Upload</value></data>
|
||||
<data name="Random_CustomBackground_Manage" xml:space="preserve"><value>Manage</value></data>
|
||||
<data name="Random_EnableButtons" xml:space="preserve"><value>Enable random & single-draw buttons</value></data>
|
||||
<data name="Random_EnableQuickButton" xml:space="preserve"><value>Enable quick-draw floating button</value></data>
|
||||
<data name="Random_UseExternal" xml:space="preserve"><value>Use external roll-call app</value></data>
|
||||
<data name="Random_ExternalTypeLabel" xml:space="preserve"><value>Roll-call type</value></data>
|
||||
<data name="Random_ExternalType_ClassIsland" xml:space="preserve"><value>ClassIsland</value></data>
|
||||
<data name="Random_ExternalType_SecRandom" xml:space="preserve"><value>SecRandom</value></data>
|
||||
<data name="Random_ExternalType_NamePicker" xml:space="preserve"><value>NamePicker</value></data>
|
||||
<data name="Random_OnceCloseDelay" xml:space="preserve"><value>Single-draw window close delay</value></data>
|
||||
<data name="Random_OnceMaxStudents" xml:space="preserve"><value>Max students per single draw</value></data>
|
||||
<data name="Random_NewUI_Title" xml:space="preserve"><value>New roll-call UI</value></data>
|
||||
<data name="Random_NewUI_Enable" xml:space="preserve"><value>Enable new roll-call UI</value></data>
|
||||
<data name="Random_ML_AvoidRepeat" xml:space="preserve"><value>Use machine learning to avoid repeats</value></data>
|
||||
<data name="Random_ML_HistoryCount" xml:space="preserve"><value>History count for avoidance</value></data>
|
||||
<data name="Random_ML_Weight" xml:space="preserve"><value>Avoidance weight</value></data>
|
||||
<data name="Random_ML_Hint" xml:space="preserve"><value># ML analyzes recent roll-call history to avoid repeating the same students.</value></data>
|
||||
<data name="Timer_Title" xml:space="preserve"><value>Timer settings</value></data>
|
||||
<data name="Timer_UseLegacyButtons" xml:space="preserve"><value>Use legacy timer button UI</value></data>
|
||||
<data name="Timer_NewUI" xml:space="preserve"><value>New timer UI</value></data>
|
||||
<data name="Timer_EnableCountUp" xml:space="preserve"><value>Enable count-up after timeout</value></data>
|
||||
<data name="Timer_OvertimeHighlight" xml:space="preserve"><value>Highlight numbers when overtime</value></data>
|
||||
<data name="Timer_Volume" xml:space="preserve"><value>Timer alert volume</value></data>
|
||||
<data name="Timer_CustomSoundLabel" xml:space="preserve"><value>Custom alert sound:</value></data>
|
||||
<data name="Timer_SelectFile" xml:space="preserve"><value>Select file</value></data>
|
||||
<data name="Timer_Reset" xml:space="preserve"><value>Reset</value></data>
|
||||
<data name="Timer_Progressive" xml:space="preserve"><value>Progressive reminder</value></data>
|
||||
<data name="Timer_ProgressiveVolume" xml:space="preserve"><value>Progressive reminder volume</value></data>
|
||||
<data name="Timer_ProgressiveCustomLabel" xml:space="preserve"><value>Custom progressive reminder audio:</value></data>
|
||||
<data name="Timer_ProgressiveSelectFile" xml:space="preserve"><value>Select file</value></data>
|
||||
<data name="Timer_ProgressiveReset" xml:space="preserve"><value>Reset</value></data>
|
||||
<data name="About_Title" xml:space="preserve"><value>About</value></data>
|
||||
<data name="About_DeviceInfo" xml:space="preserve"><value>Device information</value></data>
|
||||
<data name="About_DeviceIdLabel" xml:space="preserve"><value>Device ID:</value></data>
|
||||
<data name="About_UsageFrequencyLabel" xml:space="preserve"><value>Usage frequency:</value></data>
|
||||
<data name="About_UpdatePriorityLabel" xml:space="preserve"><value>Update priority:</value></data>
|
||||
<data name="About_LaunchCountLabel" xml:space="preserve"><value>Launch count:</value></data>
|
||||
<data name="About_TotalUsageLabel" xml:space="preserve"><value>Total usage time:</value></data>
|
||||
<data name="About_DeviceInfo_Loading" xml:space="preserve"><value>Loading...</value></data>
|
||||
<data name="About_RefreshDeviceInfo" xml:space="preserve"><value>Refresh device info</value></data>
|
||||
<data name="About_PrivacyCheckboxPrefix" xml:space="preserve"><value>I have read and agree to the </value></data>
|
||||
<data name="About_PrivacyCheckboxSuffix" xml:space="preserve"><value> privacy statement</value></data>
|
||||
<data name="About_TelemetryLabel" xml:space="preserve"><value>Anonymous usage data upload:</value></data>
|
||||
<data name="About_Telemetry_Off" xml:space="preserve"><value>Off (no upload)</value></data>
|
||||
<data name="About_Telemetry_Basic" xml:space="preserve"><value>Upload basic data</value></data>
|
||||
<data name="About_Telemetry_Optional" xml:space="preserve"><value>Upload basic + optional data</value></data>
|
||||
<data name="About_LicenseHint" xml:space="preserve"><value># Before using or distributing this software, you must be aware of the related open-source licenses. This software is based on https://github.com/WXRIW/Ink-Canvas.</value></data>
|
||||
<data name="About_LicenseTitle" xml:space="preserve"><value>This software, ICA and Ink Canvas are all open sourced under a license</value></data>
|
||||
<data name="About_LicenseBody" xml:space="preserve"><value>The strong copyleft license requires that complete source code and modifications of the licensed work (including large works using it) be provided under the same license. Copyright and license notices must be retained. Contributors explicitly grant patent rights.</value></data>
|
||||
<data name="About_DevelopersLabel" xml:space="preserve"><value>Developers:</value></data>
|
||||
<data name="About_Dev_ICCCE" xml:space="preserve"><value>Developer of ICC CE</value></data>
|
||||
<data name="About_Dev_ICC" xml:space="preserve"><value>Developer of ICC</value></data>
|
||||
<data name="About_Dev_ICA" xml:space="preserve"><value>Developer of ICA</value></data>
|
||||
<data name="About_Dev_InkCanvas" xml:space="preserve"><value>Developer of Ink Canvas</value></data>
|
||||
<data name="About_Source_ICC" xml:space="preserve"><value>ICC repository:</value></data>
|
||||
<data name="About_Source_ICA" xml:space="preserve"><value>ICA repository:</value></data>
|
||||
<data name="About_Source_InkCanvas" xml:space="preserve"><value>Ink Canvas repository:</value></data>
|
||||
<data name="About_ThanksContributors" xml:space="preserve"><value>Thanks to the following contributors:</value></data>
|
||||
<data name="About_Copyright" xml:space="preserve"><value>© 2025-2026 CJK_mkp. All rights reserved.</value></data>
|
||||
<data name="About_OpenSourceSlogan" xml:space="preserve"><value>We love open-source forever!</value></data>
|
||||
<data name="About_VersionLabel" xml:space="preserve"><value>Version:</value></data>
|
||||
<data name="Common_Close" xml:space="preserve"><value>Close</value></data>
|
||||
<data name="Common_On" xml:space="preserve"><value>On</value></data>
|
||||
<data name="Common_Off" xml:space="preserve"><value>Off</value></data>
|
||||
<data name="Advanced_UriSchemeName" xml:space="preserve"><value>External URI scheme (icc://)</value></data>
|
||||
<data name="Advanced_NibModeBoundsWidthHeader" xml:space="preserve"><value>Nib mode BoundsWidth</value></data>
|
||||
<data name="Advanced_FingerModeBoundsWidthHeader" xml:space="preserve"><value>Finger mode BoundsWidth</value></data>
|
||||
<data name="Advanced_EdgeGestureUtilHint_Part1" xml:space="preserve"><value># EdgeGestureUtil is newly introduced in ICC to temporarily block edge gestures when using touch (e.g., on Windows 10: swipe from the left edge to Task View, from the right edge to Action Center; on Windows 11: swipe up from the bottom to open Start). It works by using</value></data>
|
||||
<data name="Advanced_EdgeGestureUtilHint_Part2" xml:space="preserve"><value>(When the app window is active and in full-screen mode (or an owned window is active), prevents edge gesture behavior.) If anything is abnormal, turn this option off; it should take effect immediately. (Not available on Windows 7/8.)</value></data>
|
||||
<data name="Advanced_ForceFullScreen" xml:space="preserve"><value>Enable ForceFullScreen</value></data>
|
||||
<data name="Advanced_ForceFullScreenHint" xml:space="preserve"><value># When a window size change is detected, automatically uses Win32 API to set this window size to the primary monitor size (in device pixels). Turn it off if you don't need it; takes effect immediately.</value></data>
|
||||
<data name="Advanced_DPIChangeDetection" xml:space="preserve"><value>Enable DPIChangeDetection</value></data>
|
||||
<data name="Advanced_DPIChangeDetectionHint" xml:space="preserve"><value># When a system DPI change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Increasing DPI may trigger this; decreasing DPI won't auto-move—adjust manually.)</value></data>
|
||||
<data name="Advanced_ResolutionChangeDetection" xml:space="preserve"><value>Enable ResolutionChangeDetection</value></data>
|
||||
<data name="Advanced_ResolutionChangeDetectionHint" xml:space="preserve"><value># When a screen resolution change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Reducing resolution may trigger this; if it's still on-screen it won't auto-adjust—adjust manually.)</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoBoard3" xml:space="preserve"><value>Seewo Whiteboard 3</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoBoard5" xml:space="preserve"><value>Seewo Whiteboard 5</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoBoard5C" xml:space="preserve"><value>Seewo Whiteboard 5C</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoPinco" xml:space="preserve"><value>Seewo Pinco</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoPincoDrawing" xml:space="preserve"><value>Seewo Pinco pen</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoPPTTools" xml:space="preserve"><value>Seewo PPT Tools</value></data>
|
||||
<data name="FloatingInterceptor_App_AiClass" xml:space="preserve"><value>AiClass</value></data>
|
||||
<data name="FloatingInterceptor_App_HiteAnnotation" xml:space="preserve"><value>Hite screen writing</value></data>
|
||||
<data name="FloatingInterceptor_App_ChangYanClass" xml:space="preserve"><value>Changyan smart classroom</value></data>
|
||||
<data name="FloatingInterceptor_App_ChangYanPPT" xml:space="preserve"><value>Changyan PPT</value></data>
|
||||
<data name="FloatingInterceptor_App_IntelligentClass" xml:space="preserve"><value>Tianyu Education Cloud</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoDesktopAnnotation" xml:space="preserve"><value>Seewo desktop pen</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoDesktopSideBar" xml:space="preserve"><value>Seewo desktop sidebar</value></data>
|
||||
<data name="Board_MultiTouchWriting" xml:space="preserve"><value>Multi-touch writing</value></data>
|
||||
<data name="Board_TwoFingerMove" xml:space="preserve"><value>Two-finger move</value></data>
|
||||
<data name="Board_TwoFingerZoom" xml:space="preserve"><value>Two-finger zoom</value></data>
|
||||
<data name="Board_TwoFingerRotate" xml:space="preserve"><value>Two-finger rotate</value></data>
|
||||
<data name="Board_Background" xml:space="preserve"><value>Background</value></data>
|
||||
<data name="Board_Select" xml:space="preserve"><value>Select</value></data>
|
||||
<data name="Board_Pen" xml:space="preserve"><value>Pen</value></data>
|
||||
<data name="Board_Highlighter" xml:space="preserve"><value>Highlighter</value></data>
|
||||
<data name="Board_Eraser" xml:space="preserve"><value>Eraser</value></data>
|
||||
<data name="Board_EraserOptions" xml:space="preserve"><value>Eraser options</value></data>
|
||||
<data name="Board_Size" xml:space="preserve"><value>Size</value></data>
|
||||
<data name="Board_EraserShape" xml:space="preserve"><value>Eraser shape</value></data>
|
||||
<data name="Board_EraserShape_Circle" xml:space="preserve"><value>Circle</value></data>
|
||||
<data name="Board_EraserShape_Blackboard" xml:space="preserve"><value>Blackboard</value></data>
|
||||
<data name="Board_ClearInk" xml:space="preserve"><value>Clear ink</value></data>
|
||||
<data name="Board_ClearInkAndHistory" xml:space="preserve"><value>Clear ink & history</value></data>
|
||||
<data name="Board_StrokeEraser" xml:space="preserve"><value>Stroke eraser</value></data>
|
||||
<data name="Board_Shape" xml:space="preserve"><value>Shapes</value></data>
|
||||
<data name="Board_ShapeHintLongPress" xml:space="preserve"><value>(Long-press in first row to keep selected)</value></data>
|
||||
<data name="Board_AutoHide" xml:space="preserve"><value>Auto-hide</value></data>
|
||||
<data name="Board_InsertImage" xml:space="preserve"><value>Insert image</value></data>
|
||||
<data name="Board_SelectImage" xml:space="preserve"><value>Select image</value></data>
|
||||
<data name="Board_Screenshot" xml:space="preserve"><value>Screenshot</value></data>
|
||||
<data name="Board_Undo" xml:space="preserve"><value>Undo</value></data>
|
||||
<data name="Board_Redo" xml:space="preserve"><value>Redo</value></data>
|
||||
<data name="Board_Tools" xml:space="preserve"><value>Tools</value></data>
|
||||
<data name="Board_Exit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="Board_NewPage" xml:space="preserve"><value>New page</value></data>
|
||||
<data name="Board_PreviousPage" xml:space="preserve"><value>Previous</value></data>
|
||||
<data name="Board_NextPage" xml:space="preserve"><value>Next</value></data>
|
||||
<data name="Board_Page" xml:space="preserve"><value>Page</value></data>
|
||||
<data name="Board_DeleteThisPage" xml:space="preserve"><value>Delete this page</value></data>
|
||||
<data name="Notification_TestText" xml:space="preserve"><value>Test text</value></data>
|
||||
<data name="OldUI_Exit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="OldUI_Thickness" xml:space="preserve"><value>Thickness</value></data>
|
||||
<data name="OldUI_Dark" xml:space="preserve"><value>Dark</value></data>
|
||||
<data name="OldUI_Background" xml:space="preserve"><value>Background</value></data>
|
||||
<data name="OldUI_HideCanvas" xml:space="preserve"><value>Hide
canvas</value></data>
|
||||
<data name="OldUI_Check" xml:space="preserve"><value>Check</value></data>
|
||||
<data name="OldUI_SlideshowFromStart" xml:space="preserve"><value>From start
slideshow</value></data>
|
||||
<data name="OldUI_SlideshowEnd" xml:space="preserve"><value>End
slideshow</value></data>
|
||||
<data name="OldUI_SingleFingerDrag" xml:space="preserve"><value>One finger
drag</value></data>
|
||||
<data name="OldUI_Restore" xml:space="preserve"><value>Restore</value></data>
|
||||
<data name="OldUI_ClearAndHide" xml:space="preserve"><value>Clear
&
Hide</value></data>
|
||||
<data name="FloatingBar_Mouse" xml:space="preserve"><value>Mouse</value></data>
|
||||
<data name="FloatingBar_Annotate" xml:space="preserve"><value>Annotate</value></data>
|
||||
<data name="FloatingBar_Clear" xml:space="preserve"><value>Clear</value></data>
|
||||
<data name="Booth_Title" xml:space="preserve"><value>Visual presenter</value></data>
|
||||
<data name="Booth_CapturedPhotos" xml:space="preserve"><value>Captured photos</value></data>
|
||||
<data name="Booth_CameraDevices" xml:space="preserve"><value>Camera devices</value></data>
|
||||
<data name="Booth_Present" xml:space="preserve"><value>Present</value></data>
|
||||
<data name="Booth_Correction" xml:space="preserve"><value>Correct</value></data>
|
||||
<data name="Booth_Capture" xml:space="preserve"><value>Capture</value></data>
|
||||
<data name="Booth_Rotate" xml:space="preserve"><value>Rotate</value></data>
|
||||
<data name="Theme_LanguageLabel" xml:space="preserve"><value>UI language</value></data>
|
||||
<data name="Theme_Language_System" xml:space="preserve"><value>Follow system</value></data>
|
||||
<data name="Theme_Language_ChineseSimplified" xml:space="preserve"><value>Chinese (Simplified)</value></data>
|
||||
<data name="Theme_Language_English" xml:space="preserve"><value>English</value></data>
|
||||
<data name="Theme_Language_RestartHint" xml:space="preserve"><value>You need to restart the app for language changes to fully take effect.</value></data>
|
||||
<data name="FloatingBar_AreaEraser" xml:space="preserve"><value>Area eraser</value></data>
|
||||
<data name="FloatingBar_StrokeEraser" xml:space="preserve"><value>Stroke eraser</value></data>
|
||||
<data name="FloatingBar_LassoSelect" xml:space="preserve"><value>Lasso</value></data>
|
||||
<data name="FloatingBar_Geometry" xml:space="preserve"><value>Geometry</value></data>
|
||||
<data name="FloatingBar_ClearAndMouse" xml:space="preserve"><value>Clear & cursor</value></data>
|
||||
<data name="FloatingBar_Whiteboard" xml:space="preserve"><value>Board</value></data>
|
||||
<data name="FloatingBar_Hide" xml:space="preserve"><value>Hide</value></data>
|
||||
<data name="Geometry_Title" xml:space="preserve"><value>Geometry drawing</value></data>
|
||||
<data name="Geometry_DrawLine" xml:space="preserve"><value>Line</value></data>
|
||||
<data name="Geometry_DrawDashedLine" xml:space="preserve"><value>Dashed line</value></data>
|
||||
<data name="Geometry_DrawDottedLine" xml:space="preserve"><value>Dotted line</value></data>
|
||||
<data name="Geometry_DrawArrow" xml:space="preserve"><value>Arrow</value></data>
|
||||
<data name="Geometry_DrawParallelLines" xml:space="preserve"><value>4 parallel lines</value></data>
|
||||
<data name="Geometry_DrawCenteredSquare" xml:space="preserve"><value>Centered square</value></data>
|
||||
<data name="Geometry_DrawCenteredCircle" xml:space="preserve"><value>Centered circle</value></data>
|
||||
<data name="Geometry_DrawCenteredDashedCircle" xml:space="preserve"><value>Centered dashed circle</value></data>
|
||||
<data name="Geometry_DrawCenteredEllipse" xml:space="preserve"><value>Centered ellipse</value></data>
|
||||
<data name="Geometry_DrawCuboid" xml:space="preserve"><value>Cuboid</value></data>
|
||||
<data name="Geometry_DrawSquare" xml:space="preserve"><value>Square</value></data>
|
||||
<data name="Geometry_DrawCylinder" xml:space="preserve"><value>Cylinder</value></data>
|
||||
<data name="Geometry_DrawCone" xml:space="preserve"><value>Cone</value></data>
|
||||
<data name="FloatingBar_GestureButton" xml:space="preserve"><value>Gesture</value></data>
|
||||
<data name="FloatingBar_GesturePanelTitle" xml:space="preserve"><value>Gesture options</value></data>
|
||||
<data name="FloatingBar_Gesture_MultiTouchWriting" xml:space="preserve"><value>Multi-touch writing</value></data>
|
||||
<data name="FloatingBar_Gesture_TwoFingerMove" xml:space="preserve"><value>Two-finger move</value></data>
|
||||
<data name="FloatingBar_Gesture_TwoFingerZoom" xml:space="preserve"><value>Two-finger zoom</value></data>
|
||||
<data name="FloatingBar_Gesture_TwoFingerRotate" xml:space="preserve"><value>Two-finger rotate</value></data>
|
||||
<data name="Board_Gesture" xml:space="preserve"><value>Gesture</value></data>
|
||||
<data name="Board_GestureOptions" xml:space="preserve"><value>Gesture options</value></data>
|
||||
</root>
|
||||
@@ -0,0 +1,757 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Nav_Plugins" xml:space="preserve">
|
||||
<value>Plugins</value>
|
||||
</data>
|
||||
<data name="Nav_Startup" xml:space="preserve">
|
||||
<value>Startup</value>
|
||||
</data>
|
||||
<data name="Nav_Canvas" xml:space="preserve">
|
||||
<value>Canvas</value>
|
||||
</data>
|
||||
<data name="Nav_CrashAction" xml:space="preserve">
|
||||
<value>Crash Action</value>
|
||||
</data>
|
||||
<data name="Nav_Gesture" xml:space="preserve">
|
||||
<value>Gesture</value>
|
||||
</data>
|
||||
<data name="Nav_InkRecognition" xml:space="preserve">
|
||||
<value>Ink Recognition</value>
|
||||
</data>
|
||||
<data name="Nav_PPT" xml:space="preserve">
|
||||
<value>PPT</value>
|
||||
</data>
|
||||
<data name="Nav_Advanced" xml:space="preserve">
|
||||
<value>Advanced</value>
|
||||
</data>
|
||||
<data name="Nav_Automation" xml:space="preserve">
|
||||
<value>Automation</value>
|
||||
</data>
|
||||
<data name="Nav_RandomWindow" xml:space="preserve">
|
||||
<value>Random Picker</value>
|
||||
</data>
|
||||
<data name="Nav_Theme" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="Nav_Shortcuts" xml:space="preserve">
|
||||
<value>Shortcuts</value>
|
||||
</data>
|
||||
<data name="Nav_About" xml:space="preserve">
|
||||
<value>About</value>
|
||||
</data>
|
||||
<data name="App_Title" xml:space="preserve">
|
||||
<value>InkCanvasforClass</value>
|
||||
</data>
|
||||
<data name="Booth_Resolution_Tooltip" xml:space="preserve">
|
||||
<value>Booth / Screenshot resolution</value>
|
||||
</data>
|
||||
<data name="Nav_Gesture_Settings" xml:space="preserve"><value>Gesture</value></data>
|
||||
<data name="Nav_Theme_Settings" xml:space="preserve"><value>Appearance</value></data>
|
||||
<data name="Nav_PPT_Settings" xml:space="preserve"><value>PPT</value></data>
|
||||
<data name="Nav_Advanced_Settings" xml:space="preserve"><value>Advanced</value></data>
|
||||
<data name="Nav_Automation_Settings" xml:space="preserve"><value>Automation</value></data>
|
||||
<data name="Nav_RandomWindow_Settings" xml:space="preserve"><value>Random Picker</value></data>
|
||||
<data name="Nav_Shortcuts_Settings" xml:space="preserve"><value>Shortcuts</value></data>
|
||||
<data name="CollapseNavSidebar" xml:space="preserve"><value>Collapse sidebar</value></data>
|
||||
<data name="ShowNavSidebar" xml:space="preserve"><value>Show sidebar</value></data>
|
||||
<data name="Tooltip_IccProtocol" xml:space="preserve"><value>Control via icc:// protocol</value></data>
|
||||
<data name="Settings_Title" xml:space="preserve"><value>Settings</value></data>
|
||||
<data name="Settings_AutoSaveHint" xml:space="preserve"><value>Changes are saved automatically; some require restart.</value></data>
|
||||
<data name="Btn_Restart" xml:space="preserve"><value>Restart</value></data>
|
||||
<data name="Btn_Reset" xml:space="preserve"><value>Reset</value></data>
|
||||
<data name="Btn_Exit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="Settings_Mode" xml:space="preserve"><value>Mode</value></data>
|
||||
<data name="Settings_ModeDesc" xml:space="preserve"><value>Choose run mode. In PPT-only mode the app is hidden until slide show. (Experimental)</value></data>
|
||||
<data name="Mode_Normal" xml:space="preserve"><value>Normal</value></data>
|
||||
<data name="Mode_PPTOnly" xml:space="preserve"><value>PPT only</value></data>
|
||||
<data name="Settings_NewWindow" xml:space="preserve"><value>New settings window</value></data>
|
||||
<data name="Settings_NewWindowDesc" xml:space="preserve"><value>Open a new settings window. (In development)</value></data>
|
||||
<data name="Btn_OpenNewSettings" xml:space="preserve"><value>Open new settings</value></data>
|
||||
<data name="Settings_Plugins" xml:space="preserve"><value>Plugins</value></data>
|
||||
<data name="Settings_PluginsDesc" xml:space="preserve"><value>Extend Ink Canvas with plugins. Enable, disable, or load custom plugins.</value></data>
|
||||
<data name="Btn_OpenPluginManager" xml:space="preserve"><value>Open plugin manager</value></data>
|
||||
<data name="Startup_Start" xml:space="preserve"><value>Startup</value></data>
|
||||
<data name="Startup_NoFocusMode" xml:space="preserve"><value>No-focus mode</value></data>
|
||||
<data name="Startup_NoBorderMode" xml:space="preserve"><value>Borderless</value></data>
|
||||
<data name="Startup_TopMost" xml:space="preserve"><value>Topmost</value></data>
|
||||
<data name="Startup_UIATopMost" xml:space="preserve"><value>UIA topmost</value></data>
|
||||
<data name="Startup_UIATopMostHint" xml:space="preserve"><value># UIA topmost requires admin to take effect.</value></data>
|
||||
<data name="Header_AutoUpdate" xml:space="preserve"><value>Auto-update</value></data>
|
||||
<data name="Header_SilentUpdate" xml:space="preserve"><value>Silent update</value></data>
|
||||
<data name="SilentUpdate_Hint" xml:space="preserve"><value># Silent update installs when app is idle.</value></data>
|
||||
<data name="Update_Channel" xml:space="preserve"><value>Update channel</value></data>
|
||||
<data name="Channel_Release" xml:space="preserve"><value>Stable (Release)</value></data>
|
||||
<data name="Channel_Preview" xml:space="preserve"><value>Preview</value></data>
|
||||
<data name="Channel_Beta" xml:space="preserve"><value>Beta</value></data>
|
||||
<data name="Channel_Hint" xml:space="preserve"><value># Stable for reliability; Preview for new features.</value></data>
|
||||
<data name="Btn_ManualUpdate" xml:space="preserve"><value>Check for updates</value></data>
|
||||
<data name="ManualUpdate_Hint" xml:space="preserve"><value># Check and download now.</value></data>
|
||||
<data name="Btn_VersionFix" xml:space="preserve"><value>Version fix</value></data>
|
||||
<data name="VersionFix_Hint" xml:space="preserve"><value># Download and install latest for current channel.</value></data>
|
||||
<data name="Btn_Rollback" xml:space="preserve"><value>Rollback</value></data>
|
||||
<data name="Rollback_Hint" xml:space="preserve"><value># Open rollback page.</value></data>
|
||||
<data name="SilentUpdate_AfterDownloadHint" xml:space="preserve"><value># When silent update is off, you will be prompted after download.</value></data>
|
||||
<data name="SilentUpdate_TimeRange" xml:space="preserve"><value>Silent update time range</value></data>
|
||||
<data name="Time_Start" xml:space="preserve"><value>Start time</value></data>
|
||||
<data name="Time_End" xml:space="preserve"><value>End time</value></data>
|
||||
<data name="TimeRange_Hint" xml:space="preserve"><value># If end < start…</value></data>
|
||||
<data name="Startup_RunAtLogin" xml:space="preserve"><value>Run at login</value></data>
|
||||
<data name="Startup_MinimizeToSidebar" xml:space="preserve"><value>Minimize to sidebar at startup</value></data>
|
||||
<data name="Canvas_AndInk" xml:space="preserve"><value>Canvas & ink</value></data>
|
||||
<data name="Canvas_ShowPenCursor" xml:space="preserve"><value>Show pen cursor</value></data>
|
||||
<data name="Canvas_PressureTouch" xml:space="preserve"><value>Pressure-sensitive touch</value></data>
|
||||
<data name="Canvas_PressureTouchHint" xml:space="preserve"><value># Touch devices will support pressure.</value></data>
|
||||
<data name="Canvas_IgnorePressure" xml:space="preserve"><value>Ignore pressure</value></data>
|
||||
<data name="Canvas_IgnorePressureHint" xml:space="preserve"><value># Ignore all device pressure.</value></data>
|
||||
<data name="Canvas_EraserSize" xml:space="preserve"><value>Eraser size</value></data>
|
||||
<data name="Size_VerySmall" xml:space="preserve"><value>Very small</value></data>
|
||||
<data name="Size_Small" xml:space="preserve"><value>Small</value></data>
|
||||
<data name="Size_Medium" xml:space="preserve"><value>Medium</value></data>
|
||||
<data name="Size_Large" xml:space="preserve"><value>Large</value></data>
|
||||
<data name="Size_VeryLarge" xml:space="preserve"><value>Very large</value></data>
|
||||
<data name="EraserSize_SwitchHint" xml:space="preserve"><value># Takes effect on next area eraser use.</value></data>
|
||||
<data name="Canvas_HideInkOnExit" xml:space="preserve"><value>Hide ink when leaving canvas</value></data>
|
||||
<data name="Canvas_HideInkOnExitHint" xml:space="preserve"><value># When enabled…</value></data>
|
||||
<data name="Canvas_ClearInkHistory" xml:space="preserve"><value>Clear ink history when clearing</value></data>
|
||||
<data name="Canvas_ClearImageOnClear" xml:space="preserve"><value>Clear images with canvas</value></data>
|
||||
<data name="Canvas_CompressImage" xml:space="preserve"><value>Compress images >1920×1080</value></data>
|
||||
<data name="Canvas_KeepAsymptote" xml:space="preserve"><value>Keep hyperbola asymptotes</value></data>
|
||||
<data name="Yes" xml:space="preserve"><value>Yes</value></data>
|
||||
<data name="No" xml:space="preserve"><value>No</value></data>
|
||||
<data name="AskEachTime" xml:space="preserve"><value>Ask each time</value></data>
|
||||
<data name="Canvas_AsymptoteHint" xml:space="preserve"><value># Disabling may cause undo bugs.</value></data>
|
||||
<data name="Canvas_ShowCircleCenter" xml:space="preserve"><value>Show circle center</value></data>
|
||||
<data name="Canvas_WPFBezier" xml:space="preserve"><value>WPF default Bezier smoothing</value></data>
|
||||
<data name="Canvas_AdvancedSmoothing" xml:space="preserve"><value>Advanced curve smoothing (recommended)</value></data>
|
||||
<data name="Canvas_InkFade" xml:space="preserve"><value>Ink fade</value></data>
|
||||
<data name="Canvas_InkFadeHint" xml:space="preserve"><value># Ink will not be drawn on canvas when enabled.</value></data>
|
||||
<data name="Canvas_InkFadeTime" xml:space="preserve"><value>Ink fade time</value></data>
|
||||
<data name="Canvas_HideFadeInPenMenu" xml:space="preserve"><value>Hide fade in pen menu</value></data>
|
||||
<data name="Canvas_HideFadeInPenMenuHint" xml:space="preserve"><value># Fade control will be hidden in pen context menu.</value></data>
|
||||
<data name="Color" xml:space="preserve"><value>Color</value></data>
|
||||
<data name="Color_Default" xml:space="preserve"><value>Default</value></data>
|
||||
<data name="Color_Black" xml:space="preserve"><value>Black</value></data>
|
||||
<data name="Color_White" xml:space="preserve"><value>White</value></data>
|
||||
<data name="Color_Red" xml:space="preserve"><value>Red</value></data>
|
||||
<data name="Color_Yellow" xml:space="preserve"><value>Yellow</value></data>
|
||||
<data name="Color_Blue" xml:space="preserve"><value>Blue</value></data>
|
||||
<data name="Color_Green" xml:space="preserve"><value>Green</value></data>
|
||||
<data name="Color_Orange" xml:space="preserve"><value>Orange</value></data>
|
||||
<data name="Color_Purple" xml:space="preserve"><value>Purple</value></data>
|
||||
<data name="Msg_UpdateReady" xml:space="preserve"><value>Update downloaded. It will install when you close the app.</value></data>
|
||||
<data name="Msg_UpdateReadyTitle" xml:space="preserve"><value>Update ready</value></data>
|
||||
<data name="Msg_UpdateDownloadFailed" xml:space="preserve"><value>Update download failed. Please check your network and try again.</value></data>
|
||||
<data name="Msg_DownloadFailedTitle" xml:space="preserve"><value>Download failed</value></data>
|
||||
<data name="Msg_SkipVersion" xml:space="preserve"><value>Version {0} skipped; you will not be prompted until a newer version is released.</value></data>
|
||||
<data name="Msg_SkipVersionTitle" xml:space="preserve"><value>Version skipped</value></data>
|
||||
<data name="Msg_UnexpectedError" xml:space="preserve"><value>An unexpected error occurred. Save your ink and restart the app.</value></data>
|
||||
<data name="Msg_RestartLimitTitle" xml:space="preserve"><value>Too many restarts</value></data>
|
||||
<data name="Msg_RestartLimit" xml:space="preserve"><value>App has restarted 5 times. Auto-restart stopped. Contact the developer or check the system.</value></data>
|
||||
<data name="Splash_Starting" xml:space="preserve"><value>Starting Ink Canvas...</value></data>
|
||||
<data name="Crash_Title" xml:space="preserve"><value>Crash action</value></data>
|
||||
<data name="Crash_Desc" xml:space="preserve"><value>Choose what to do when an unhandled exception occurs:</value></data>
|
||||
<data name="Crash_SilentRestart" xml:space="preserve"><value>Silent restart</value></data>
|
||||
<data name="Crash_NoAction" xml:space="preserve"><value>No action</value></data>
|
||||
<data name="Crash_Hint" xml:space="preserve"><value># Silent restart: automatically restart without prompt. No action: only log, do not restart.</value></data>
|
||||
<data name="Gesture_Title" xml:space="preserve"><value>Gestures</value></data>
|
||||
<data name="Gesture_AutoToggleTwoFinger" xml:space="preserve"><value>Auto-toggle two-finger move in/out of whiteboard</value></data>
|
||||
<data name="Gesture_AutoToggleHint" xml:space="preserve"><value># When enabled: leaving canvas disables two-finger move; entering whiteboard enables it.</value></data>
|
||||
<data name="Gesture_AllowRotateScale" xml:space="preserve"><value>Allow rotate & scale selected ink</value></data>
|
||||
<data name="Gesture_AllowRotateScaleHint" xml:space="preserve"><value># Allows scaling selected ink with two or more fingers (independent of rotate setting).</value></data>
|
||||
<data name="Gesture_EnablePalmEraser" xml:space="preserve"><value>Enable palm eraser</value></data>
|
||||
<data name="Gesture_PalmSensitivity" xml:space="preserve"><value>Palm eraser sensitivity</value></data>
|
||||
<data name="Gesture_PalmSensitivityLow" xml:space="preserve"><value>Low sensitivity</value></data>
|
||||
<data name="Gesture_PalmSensitivityMedium" xml:space="preserve"><value>Medium sensitivity</value></data>
|
||||
<data name="Gesture_PalmSensitivityHigh" xml:space="preserve"><value>High sensitivity</value></data>
|
||||
<data name="Gesture_PalmHint" xml:space="preserve"><value># Low: larger area/more touches required (less false positive); High: easier to trigger but may mis-detect fingers.</value></data>
|
||||
<data name="InkRecog_Title" xml:space="preserve"><value>Ink correction</value></data>
|
||||
<data name="InkRecog_EnableInkRecognition" xml:space="preserve"><value>Enable ink recognition</value></data>
|
||||
<data name="InkRecog_BlockRectFakePressure" xml:space="preserve"><value>Block fake pressure on corrected rectangles</value></data>
|
||||
<data name="InkRecog_BlockTriFakePressure" xml:space="preserve"><value>Block fake pressure on corrected triangles</value></data>
|
||||
<data name="InkRecog_FixTriangle" xml:space="preserve"><value>Correct freehand triangles</value></data>
|
||||
<data name="InkRecog_FixRectangle" xml:space="preserve"><value>Correct freehand rectangles</value></data>
|
||||
<data name="InkRecog_FixEllipse" xml:space="preserve"><value>Correct circles and ellipses</value></data>
|
||||
<data name="InkRecog_AutoStraightLine" xml:space="preserve"><value>Auto-straighten lines</value></data>
|
||||
<data name="InkRecog_LengthThreshold" xml:space="preserve"><value>Length threshold</value></data>
|
||||
<data name="InkRecog_Sensitivity" xml:space="preserve"><value>Sensitivity</value></data>
|
||||
<data name="InkRecog_HighPrecisionStraighten" xml:space="preserve"><value>High-precision straightening</value></data>
|
||||
<data name="InkRecog_HighPrecisionHint" xml:space="preserve"><value># When enabled, lines longer than the threshold will be straightened. Sensitivity 0.05–2.0: smaller = stricter; larger = easier to treat as straight. High-precision samples every 10px for better judgement.</value></data>
|
||||
<data name="InkRecog_LineEndpointSnapping" xml:space="preserve"><value>Line endpoint snapping</value></data>
|
||||
<data name="InkRecog_SnappingDistance" xml:space="preserve"><value>Snapping distance</value></data>
|
||||
<data name="Theme_GroupTitle" xml:space="preserve"><value>Personalization</value></data>
|
||||
<data name="Theme_Label" xml:space="preserve"><value>Theme</value></data>
|
||||
<data name="Theme_Light" xml:space="preserve"><value>Light theme</value></data>
|
||||
<data name="Theme_Dark" xml:space="preserve"><value>Dark theme</value></data>
|
||||
<data name="Theme_System" xml:space="preserve"><value>Follow system</value></data>
|
||||
<data name="Theme_EnableSplash" xml:space="preserve"><value>Enable startup animation</value></data>
|
||||
<data name="Theme_SplashStyle" xml:space="preserve"><value>Startup animation style</value></data>
|
||||
<data name="Theme_Splash_Random" xml:space="preserve"><value>Random</value></data>
|
||||
<data name="Theme_Splash_Seasonal" xml:space="preserve"><value>Follow seasons</value></data>
|
||||
<data name="Theme_Splash_Spring" xml:space="preserve"><value>Spring</value></data>
|
||||
<data name="Theme_Splash_Summer" xml:space="preserve"><value>Summer</value></data>
|
||||
<data name="Theme_Splash_Autumn" xml:space="preserve"><value>Autumn</value></data>
|
||||
<data name="Theme_Splash_Winter" xml:space="preserve"><value>Winter</value></data>
|
||||
<data name="Theme_Splash_Horse" xml:space="preserve"><value>Year-of-Horse special</value></data>
|
||||
<data name="Theme_FloatingBarIcon" xml:space="preserve"><value>Floating toolbar icon</value></data>
|
||||
<data name="Theme_FloatingIcon_IccDefault" xml:space="preserve"><value>“ICC-CE” default</value></data>
|
||||
<data name="Theme_FloatingIcon_IccNoShadow" xml:space="preserve"><value>“ICC-CE” no shadow</value></data>
|
||||
<data name="Theme_FloatingIcon_IccDark" xml:space="preserve"><value>“ICC-CE” dark</value></data>
|
||||
<data name="Theme_FloatingIcon_IccDarkBreath" xml:space="preserve"><value>“ICC-CE” dark breathing</value></data>
|
||||
<data name="Theme_FloatingIcon_IccWhiteTransparent" xml:space="preserve"><value>“ICC-CE” white transparent</value></data>
|
||||
<data name="Theme_FloatingIcon_IccBlackTransparent" xml:space="preserve"><value>“ICC-CE” black transparent</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkCrossEye" xml:space="preserve"><value>Coolapk cross-eye emoji</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkAbused" xml:space="preserve"><value>Coolapk abused emoji</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkSmile" xml:space="preserve"><value>Coolapk grin emoji</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkUnderwear" xml:space="preserve"><value>Coolapk underwear emoji</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkGreenHatDoge" xml:space="preserve"><value>Coolapk green-hat Doge</value></data>
|
||||
<data name="Theme_FloatingIcon_TiebaEmoji" xml:space="preserve"><value>Tieba emoji</value></data>
|
||||
<data name="Theme_CustomFloatingIconLabel" xml:space="preserve"><value>Custom floating icon</value></data>
|
||||
<data name="Theme_Upload" xml:space="preserve"><value>Upload</value></data>
|
||||
<data name="Theme_Manage" xml:space="preserve"><value>Manage</value></data>
|
||||
<data name="Theme_FloatingBarScale" xml:space="preserve"><value>Floating toolbar scale</value></data>
|
||||
<data name="Theme_FloatingBarOpacity" xml:space="preserve"><value>Floating toolbar opacity</value></data>
|
||||
<data name="Theme_FloatingBarOpacityInPPT" xml:space="preserve"><value>Floating bar opacity in PPT</value></data>
|
||||
<data name="Theme_FloatingBarOpacityInPPTHint" xml:space="preserve"><value># Takes effect after re-entering slide show</value></data>
|
||||
<data name="Theme_ShowNibButton" xml:space="preserve"><value>Show nib-mode button in palette</value></data>
|
||||
<data name="Theme_BlackboardScale80" xml:space="preserve"><value>Whiteboard UI 80% scale</value></data>
|
||||
<data name="Theme_ShowTimeInWhiteboard" xml:space="preserve"><value>Show time and date in whiteboard</value></data>
|
||||
<data name="Theme_ShowQuoteInWhiteboard" xml:space="preserve"><value>Show quotes in whiteboard</value></data>
|
||||
<data name="Theme_QuoteSource" xml:space="preserve"><value>Where is the quote from?</value></data>
|
||||
<data name="Theme_QuoteSource_OsuQuotes" xml:space="preserve"><value>osu! player quotes</value></data>
|
||||
<data name="Theme_QuoteSource_Mottos" xml:space="preserve"><value>Inspirational mottos</value></data>
|
||||
<data name="Theme_QuoteSource_GaokaoBless" xml:space="preserve"><value>Gaokao blessings</value></data>
|
||||
<data name="Theme_QuoteSource_Hitokoto" xml:space="preserve"><value>Hitokoto API</value></data>
|
||||
<data name="Theme_Customize" xml:space="preserve"><value>Custom</value></data>
|
||||
<data name="Theme_EnableQuickPanel" xml:space="preserve"><value>Enable quick panel in docked mode</value></data>
|
||||
<data name="Theme_UnfoldButtonIcon" xml:space="preserve"><value>Un-dock button icon</value></data>
|
||||
<data name="Theme_UnfoldIcon_Arrow" xml:space="preserve"><value>Arrow</value></data>
|
||||
<data name="Theme_UnfoldIcon_Pen" xml:space="preserve"><value>Pen</value></data>
|
||||
<data name="Theme_FloatingBarButtonsTitle" xml:space="preserve"><value>Floating bar buttons</value></data>
|
||||
<data name="Theme_UseLegacyFloatingBarUI" xml:space="preserve"><value>Use legacy floating bar UI</value></data>
|
||||
<data name="Theme_ShowShapeButton" xml:space="preserve"><value>Show shape button</value></data>
|
||||
<data name="Theme_ShowUndoButton" xml:space="preserve"><value>Show undo button</value></data>
|
||||
<data name="Theme_ShowRedoButton" xml:space="preserve"><value>Show redo button</value></data>
|
||||
<data name="Theme_ShowClearButton" xml:space="preserve"><value>Show clear button</value></data>
|
||||
<data name="Theme_ShowWhiteboardButton" xml:space="preserve"><value>Show whiteboard button</value></data>
|
||||
<data name="Theme_ShowHideButton" xml:space="preserve"><value>Show hide button</value></data>
|
||||
<data name="Theme_ShowLassoButton" xml:space="preserve"><value>Show lasso select button</value></data>
|
||||
<data name="Theme_ShowClearAndMouseButton" xml:space="preserve"><value>Show clear+mouse button</value></data>
|
||||
<data name="Theme_ShowQuickPalette" xml:space="preserve"><value>Show quick palette</value></data>
|
||||
<data name="Theme_QuickPaletteMode" xml:space="preserve"><value>Quick palette display mode</value></data>
|
||||
<data name="Theme_QuickPalette_SingleRow" xml:space="preserve"><value>Single row (6 colors)</value></data>
|
||||
<data name="Theme_QuickPalette_DoubleRow" xml:space="preserve"><value>Double row (8 colors)</value></data>
|
||||
<data name="Theme_EraserButtonDisplay" xml:space="preserve"><value>Eraser button display</value></data>
|
||||
<data name="Theme_EraserDisplay_Both" xml:space="preserve"><value>Show both</value></data>
|
||||
<data name="Theme_EraserDisplay_AreaOnly" xml:space="preserve"><value>Area eraser only</value></data>
|
||||
<data name="Theme_EraserDisplay_LineOnly" xml:space="preserve"><value>Line eraser only</value></data>
|
||||
<data name="Theme_EraserDisplay_None" xml:space="preserve"><value>Hide all</value></data>
|
||||
<data name="Tray_GroupTitle" xml:space="preserve"><value>Taskbar tray icon</value></data>
|
||||
<data name="Tray_EnableTrayIcon" xml:space="preserve"><value>Enable tray icon</value></data>
|
||||
<data name="PPT_GroupTitle" xml:space="preserve"><value>PPT integration</value></data>
|
||||
<data name="PPT_GroupHint" xml:space="preserve"><value>These settings apply during slide show and override others.</value></data>
|
||||
<data name="PPT_SupportPowerPoint" xml:space="preserve"><value>Microsoft PowerPoint support</value></data>
|
||||
<data name="PPT_Enhancement" xml:space="preserve"><value>PowerPoint enhancement</value></data>
|
||||
<data name="PPT_SkipAnimations" xml:space="preserve"><value>Steal focus to skip animations (PPT)</value></data>
|
||||
<data name="PPT_UseRot" xml:space="preserve"><value>Use ROT integration</value></data>
|
||||
<data name="PPT_SupportWPS" xml:space="preserve"><value>WPS support</value></data>
|
||||
<data name="PPT_KillWppProcess" xml:space="preserve"><value>Kill WPP process (avoid leftovers)</value></data>
|
||||
<data name="PPT_KillWppHint" xml:space="preserve"><value># When disabled, leftover WPP processes may cause slow close or cannot exit completely.</value></data>
|
||||
<data name="PPT_WpsHint1" xml:space="preserve"><value># If you only use PowerPoint, do not enable WPS integration. If you use WPS, it is recommended not to use PowerPoint together.</value></data>
|
||||
<data name="PPT_WpsLagWarning" xml:space="preserve"><value>Enabling WPS support may cause lag when closing WPS!</value></data>
|
||||
<data name="PPT_WpsSupportHint" xml:space="preserve"><value># WPS is supported, but MS Office and WPS cannot be supported at the same time. To enable WPS support, make sure “WPS Office compatibility with third-party systems and software” is enabled in the WPS config tool, otherwise WPS cannot be detected.</value></data>
|
||||
<data name="Canvas_HideStrokeWhenSelecting" xml:space="preserve"><value>Hide ink when exiting board mode</value></data>
|
||||
<data name="Canvas_HideStrokeWhenSelectingHint" xml:space="preserve"><value># When this option is on, ink will not be shown in PPT mode if not in annotation mode.</value></data>
|
||||
<data name="Canvas_ClearInkAlsoClearHistory" xml:space="preserve"><value>Clear ink history when clearing ink</value></data>
|
||||
<data name="Canvas_ClearCanvasAlsoClearImages" xml:space="preserve"><value>Clear images when clearing canvas</value></data>
|
||||
<data name="Canvas_CompressPicturesUploaded" xml:space="preserve"><value>Auto-compress images when inserting (larger than 1920x1080)</value></data>
|
||||
<data name="PPT_FlipButtonsTitle" xml:space="preserve"><value>PPT page-turn buttons</value></data>
|
||||
<data name="PPT_ShowFlipButtons" xml:space="preserve"><value>Show page-turn buttons in PPT mode</value></data>
|
||||
<data name="PPT_Position_LeftBottom" xml:space="preserve"><value>Bottom left</value></data>
|
||||
<data name="PPT_Position_RightBottom" xml:space="preserve"><value>Bottom right</value></data>
|
||||
<data name="PPT_Position_Left" xml:space="preserve"><value>Left</value></data>
|
||||
<data name="PPT_Position_Right" xml:space="preserve"><value>Right</value></data>
|
||||
<data name="PPT_LeftOffset" xml:space="preserve"><value>Left offset</value></data>
|
||||
<data name="PPT_LeftOpacity" xml:space="preserve"><value>Left opacity</value></data>
|
||||
<data name="PPT_RightOffset" xml:space="preserve"><value>Right offset</value></data>
|
||||
<data name="PPT_RightOpacity" xml:space="preserve"><value>Right opacity</value></data>
|
||||
<data name="PPT_OffsetHint" xml:space="preserve"><value># Increase for up, decrease for down; 0 = no offset, centered.</value></data>
|
||||
<data name="PPT_LeftBottomOffset" xml:space="preserve"><value>Bottom left offset</value></data>
|
||||
<data name="PPT_LeftBottomOpacity" xml:space="preserve"><value>Bottom left opacity</value></data>
|
||||
<data name="PPT_RightBottomOffset" xml:space="preserve"><value>Bottom right offset</value></data>
|
||||
<data name="PPT_RightBottomOpacity" xml:space="preserve"><value>Bottom right opacity</value></data>
|
||||
<data name="PPT_OffsetHintHorizontal" xml:space="preserve"><value># Increase for right, decrease for left; 0 = no offset, centered.</value></data>
|
||||
<data name="PPT_SideGroupTitle" xml:space="preserve"><value>Sides</value></data>
|
||||
<data name="PPT_ShowPageNumber" xml:space="preserve"><value>Show page number</value></data>
|
||||
<data name="PPT_HalfOpacity" xml:space="preserve"><value>Half opacity</value></data>
|
||||
<data name="PPT_BlackBackground" xml:space="preserve"><value>Black background</value></data>
|
||||
<data name="PPT_BottomGroupTitle" xml:space="preserve"><value>Bottom left & right</value></data>
|
||||
<data name="PPT_PageButtonClickable" xml:space="preserve"><value>PPT page button clickable</value></data>
|
||||
<data name="PPT_PageButtonClickableHint" xml:space="preserve"><value># When enabled, clicking the page button opens PowerPoint grid thumbnails. Not supported in WPS.</value></data>
|
||||
<data name="PPT_LongPressPageTurn" xml:space="preserve"><value>PPT long-press to turn page</value></data>
|
||||
<data name="PPT_LongPressPageTurnHint" xml:space="preserve"><value># When enabled, long-press on PPT page button to turn pages continuously.</value></data>
|
||||
<data name="Startup_UIAccessTopMostHint" xml:space="preserve"><value># With UIA topmost on, app needs admin to stay on top. To turn off, fully quit then start again; restart will not disable it.</value></data>
|
||||
<data name="Header_SilentUpdate" xml:space="preserve"><value>Silent update</value></data>
|
||||
<data name="Startup_SilentUpdateHint" xml:space="preserve"><value># Silent update installs when the app is idle; no manual action needed.</value></data>
|
||||
<data name="Startup_UpdateChannel" xml:space="preserve"><value>Update channel</value></data>
|
||||
<data name="Update_Release" xml:space="preserve"><value>Stable (Release)</value></data>
|
||||
<data name="Update_Preview" xml:space="preserve"><value>Preview</value></data>
|
||||
<data name="Update_Beta" xml:space="preserve"><value>Beta</value></data>
|
||||
<data name="Startup_UpdateChannelHint" xml:space="preserve"><value># Stable: reliable updates. Preview: new features with better stability than Beta. Beta: earliest new features.</value></data>
|
||||
<data name="Btn_ManualUpdate" xml:space="preserve"><value>Check for updates</value></data>
|
||||
<data name="Startup_ManualUpdateHint" xml:space="preserve"><value># Check and download the latest version now.</value></data>
|
||||
<data name="Btn_FixVersion" xml:space="preserve"><value>Repair installation</value></data>
|
||||
<data name="Startup_FixVersionHint" xml:space="preserve"><value># Repair downloads the latest build for the selected channel and reinstalls; use to fix broken installs.</value></data>
|
||||
<data name="Btn_HistoryRollback" xml:space="preserve"><value>Rollback to previous version</value></data>
|
||||
<data name="Startup_HistoryRollbackHint" xml:space="preserve"><value># Opens a page to manually roll back to an earlier version.</value></data>
|
||||
<data name="Startup_SilentUpdateFullHint" xml:space="preserve"><value># When silent update is off, you will be prompted after download. When on, every 10 minutes the app checks: 1) within silent-update time window 2) not in writing mode 3) not in canvas. If all pass, it will close and update.</value></data>
|
||||
<data name="Startup_SilentUpdateTimePeriod" xml:space="preserve"><value>Silent update time window</value></data>
|
||||
<data name="Startup_StartTime" xml:space="preserve"><value>Start time</value></data>
|
||||
<data name="Startup_EndTime" xml:space="preserve"><value>End time</value></data>
|
||||
<data name="Startup_TimePeriodHint" xml:space="preserve"><value># If end < start, end is next day. If start = end, window is 24h.</value></data>
|
||||
<data name="Startup_RunAtStartup" xml:space="preserve"><value>Run at startup</value></data>
|
||||
<data name="Startup_FoldAtStartup" xml:space="preserve"><value>Dock to sidebar after startup</value></data>
|
||||
<data name="Canvas_GroupTitle" xml:space="preserve"><value>Canvas and ink</value></data>
|
||||
<data name="Canvas_ShowCursor" xml:space="preserve"><value>Show pen cursor</value></data>
|
||||
<data name="Canvas_EnablePressureTouch" xml:space="preserve"><value>Enable pressure-sensitive touch</value></data>
|
||||
<data name="Canvas_EnablePressureTouchHint" xml:space="preserve"><value># When on, touch screens that support pressure will show pressure; for devices not recognized by the system.</value></data>
|
||||
<data name="Canvas_DisablePressure" xml:space="preserve"><value>Ignore pressure</value></data>
|
||||
<data name="Canvas_DisablePressureHint" xml:space="preserve"><value># When on, all strokes use uniform thickness; mutually exclusive with pressure-sensitive touch.</value></data>
|
||||
<data name="Canvas_EraserSize" xml:space="preserve"><value>Eraser size</value></data>
|
||||
<data name="Canvas_EraserSize_VerySmall" xml:space="preserve"><value>Very small</value></data>
|
||||
<data name="Canvas_EraserSize_Small" xml:space="preserve"><value>Small</value></data>
|
||||
<data name="Canvas_EraserSize_Medium" xml:space="preserve"><value>Medium</value></data>
|
||||
<data name="Canvas_EraserSize_Large" xml:space="preserve"><value>Large</value></data>
|
||||
<data name="Canvas_EraserSize_VeryLarge" xml:space="preserve"><value>Very large</value></data>
|
||||
<data name="Canvas_EraserSizeHint" xml:space="preserve"><value># Change takes effect next time you use area eraser.</value></data>
|
||||
<data name="Canvas_KeepHyperbolaAsymptote" xml:space="preserve"><value>Keep hyperbola asymptotes</value></data>
|
||||
<data name="Canvas_Yes" xml:space="preserve"><value>Yes</value></data>
|
||||
<data name="Canvas_No" xml:space="preserve"><value>No</value></data>
|
||||
<data name="Canvas_AskEachTime" xml:space="preserve"><value>Ask each time</value></data>
|
||||
<data name="Canvas_HyperbolaAsymptoteHint" xml:space="preserve"><value># If not kept, undo-related bugs may occur.</value></data>
|
||||
<data name="Canvas_ShowCircleCenter" xml:space="preserve"><value>Show circle center when drawing</value></data>
|
||||
<data name="Canvas_WPFBezierSmoothing" xml:space="preserve"><value>Use WPF default Bezier smoothing</value></data>
|
||||
<data name="Canvas_AdvancedBezierSmoothing" xml:space="preserve"><value>Use advanced curve smoothing (recommended)</value></data>
|
||||
<data name="Canvas_EnableInkFade" xml:space="preserve"><value>Enable ink fade</value></data>
|
||||
<data name="Canvas_EnableInkFadeHint" xml:space="preserve"><value># When on, ink is not committed to canvas; it fades after the set time.</value></data>
|
||||
<data name="Canvas_InkFadeTime" xml:space="preserve"><value>Ink fade time</value></data>
|
||||
<data name="Canvas_HideInkFadeInPenMenu" xml:space="preserve"><value>Hide ink fade control in pen menu</value></data>
|
||||
<data name="Canvas_HideInkFadeInPenMenuHint" xml:space="preserve"><value># When on, the pen context menu will not show the ink fade control.</value></data>
|
||||
<data name="Canvas_BrushAutoRestore" xml:space="preserve"><value>Enable brush auto-restore</value></data>
|
||||
<data name="Canvas_BrushAutoRestoreHint" xml:space="preserve"><value># When on, temporary brush changes will restore at the configured time(s) to the color/opacity/width set here.</value></data>
|
||||
<data name="Canvas_AutoRestoreTimePoints" xml:space="preserve"><value>Auto-restore time points (HH:mm, multiple with ;)</value></data>
|
||||
<data name="Canvas_RestoreColor" xml:space="preserve"><value>Restore color</value></data>
|
||||
<data name="Canvas_Color_Default" xml:space="preserve"><value>Default</value></data>
|
||||
<data name="Canvas_Color_Black" xml:space="preserve"><value>Black</value></data>
|
||||
<data name="Canvas_Color_White" xml:space="preserve"><value>White</value></data>
|
||||
<data name="Canvas_Color_Red" xml:space="preserve"><value>Red</value></data>
|
||||
<data name="Canvas_Color_Yellow" xml:space="preserve"><value>Yellow</value></data>
|
||||
<data name="Canvas_Color_Blue" xml:space="preserve"><value>Blue</value></data>
|
||||
<data name="Canvas_Color_Green" xml:space="preserve"><value>Green</value></data>
|
||||
<data name="Canvas_Color_Orange" xml:space="preserve"><value>Orange</value></data>
|
||||
<data name="Canvas_Color_Purple" xml:space="preserve"><value>Purple</value></data>
|
||||
<data name="Canvas_RestoreWidth" xml:space="preserve"><value>Restore stroke width</value></data>
|
||||
<data name="Canvas_RestoreOpacity" xml:space="preserve"><value>Restore opacity</value></data>
|
||||
<data name="Canvas_SwitchBackAfterEraser" xml:space="preserve"><value>Switch back to annotation after eraser</value></data>
|
||||
<data name="Canvas_SwitchBackAfterEraserHint" xml:space="preserve"><value># When on, after erasing, staying idle for a while will switch back to annotation mode.</value></data>
|
||||
<data name="Canvas_SwitchBackDelay" xml:space="preserve"><value>Auto switch delay</value></data>
|
||||
<data name="Canvas_SwitchBackDelayHint" xml:space="preserve"><value># If you erase again within the delay, the timer resets.</value></data>
|
||||
<data name="InkRecog_LineEndpointSnappingHint" xml:space="preserve"><value># When on, line endpoints near other endpoints will snap and connect.</value></data>
|
||||
<data name="PPT_EnterAnnotationOnShow" xml:space="preserve"><value>Enter annotation mode when starting PPT slide show</value></data>
|
||||
<data name="PPT_ConflictWithAutoFold" xml:space="preserve"><value>Conflicts with "Auto fold when playing PPT" in Automation!</value></data>
|
||||
<data name="PPT_TwoFingerGesture" xml:space="preserve"><value>Allow two-finger gestures in slide show</value></data>
|
||||
<data name="PPT_FingerGestureSlide" xml:space="preserve"><value>Allow finger gesture to turn slides</value></data>
|
||||
<data name="PPT_FingerGestureSlideHint" xml:space="preserve"><value># When canvas is on, finger swipe (not pen) can turn slides in show mode when canvas has no ink.</value></data>
|
||||
<data name="PPT_ShowGestureButtonInShow" xml:space="preserve"><value>Show gesture buttons in PPT slide show</value></data>
|
||||
<data name="PPT_ShowGestureButtonInShowHint" xml:space="preserve"><value># When on, gesture buttons are shown in PPT slide show.</value></data>
|
||||
<data name="PPT_TimeCapsule" xml:space="preserve"><value>PPT time capsule</value></data>
|
||||
<data name="PPT_TimeCapsuleHint" xml:space="preserve"><value># When on, show time capsule in PPT show; can replace minimized timer window.</value></data>
|
||||
<data name="PPT_TimeCapsulePosition" xml:space="preserve"><value>Time capsule position:</value></data>
|
||||
<data name="PPT_TimeCapsulePos_TL" xml:space="preserve"><value>Top left</value></data>
|
||||
<data name="PPT_TimeCapsulePos_TR" xml:space="preserve"><value>Top right</value></data>
|
||||
<data name="PPT_TimeCapsulePos_Center" xml:space="preserve"><value>Top center</value></data>
|
||||
<data name="PPT_ShowQuickPanelInShow" xml:space="preserve"><value>Show quick panel in PPT slide show</value></data>
|
||||
<data name="PPT_ShowQuickPanelInShowHint" xml:space="preserve"><value># When off, quick panel is hidden in PPT slide show.</value></data>
|
||||
<data name="PPT_AutoScreenshot" xml:space="preserve"><value>Auto screenshot on slide change</value></data>
|
||||
<data name="PPT_AutoScreenshotHint" xml:space="preserve"><value># When on, auto-screenshot when turning page with ink on slide.</value></data>
|
||||
<data name="PPT_AutoSaveStrokes" xml:space="preserve"><value>Auto-save slide ink</value></data>
|
||||
<data name="PPT_AutoSaveStrokesHint" xml:space="preserve"><value># When on, ink is saved when ending slide show and loaded next time (same file and page).</value></data>
|
||||
<data name="PPT_RememberLastPage" xml:space="preserve"><value>Remember and prompt last slide position</value></data>
|
||||
<data name="PPT_RememberLastPageHint" xml:space="preserve"><value># When on, last page is recorded; choose Yes to jump to it.</value></data>
|
||||
<data name="PPT_GoToFirstPageOnReenter" xml:space="preserve"><value>Go to first slide when entering show</value></data>
|
||||
<data name="PPT_NotifyHiddenPage" xml:space="preserve"><value>Warn about hidden slides</value></data>
|
||||
<data name="PPT_NotifyAutoPlay" xml:space="preserve"><value>Warn if auto-play is enabled</value></data>
|
||||
<data name="Advanced_Title" xml:space="preserve"><value>Advanced</value></data>
|
||||
<data name="Advanced_TouchMultiplierHint" xml:space="preserve"><value>Adjust when finger-touch shows circle eraser or palm eraser is much larger than palm</value></data>
|
||||
<data name="Advanced_SpecialScreenMode" xml:space="preserve"><value>Special screen mode</value></data>
|
||||
<data name="Advanced_TouchMultiplier" xml:space="preserve"><value>Touch multiplier</value></data>
|
||||
<data name="Advanced_TouchMultiplierCalibrateHint" xml:space="preserve"><value>Tap with pen in the area below to estimate touch size multiplier</value></data>
|
||||
<data name="Advanced_TouchMultiplierValueHint" xml:space="preserve"><value># Value is for reference only</value></data>
|
||||
<data name="Advanced_EraserBindTouchMultiplier" xml:space="preserve"><value>Bind eraser to touch size multiplier</value></data>
|
||||
<data name="Advanced_EraserBindTouchHint" xml:space="preserve"><value># BoundsWidth is used as contact area threshold</value></data>
|
||||
<data name="Advanced_QuadIRMode" xml:space="preserve"><value>Quad IR mode</value></data>
|
||||
<data name="Advanced_Logging" xml:space="preserve"><value>Enable logging</value></data>
|
||||
<data name="Advanced_LogByDate" xml:space="preserve"><value>Save logs by date</value></data>
|
||||
<data name="Advanced_LogRotateHint" xml:space="preserve"><value># Log files over 512 KB are auto-deleted. With date save, logs go to Logs folder; folder is cleared when over 5 MB.</value></data>
|
||||
<data name="Advanced_ConfirmExit" xml:space="preserve"><value>Confirm exit with dialog</value></data>
|
||||
<data name="Advanced_FullScreenHelper" xml:space="preserve"><value>Enable FullScreenHelper</value></data>
|
||||
<data name="Advanced_Experimental" xml:space="preserve"><value>Experimental</value></data>
|
||||
<data name="Advanced_FullScreenHelperHint" xml:space="preserve"><value># Thanks to lindexi for FullScreenHelper; reduces taskbar pop-up and supports multi-monitor fullscreen. Disable if you see odd issues; restart ICC to apply.</value></data>
|
||||
<data name="Advanced_AvoidFullScreenHelper" xml:space="preserve"><value>Enable AvoidFullScreenHelper</value></data>
|
||||
<data name="Advanced_AvoidFullScreenHelperHint" xml:space="preserve"><value># Avoid canvas fullscreen; may fix taskbar not on top and Win11 taskbar unclickable. Can cause floating bar offset with AppBar on left/top. Restart ICC to apply.</value></data>
|
||||
<data name="Advanced_EdgeGestureUtil" xml:space="preserve"><value>Enable EdgeGestureUtil</value></data>
|
||||
<data name="Tools_MoreFeaturesTitle" xml:space="preserve"><value>More features</value></data>
|
||||
<data name="Tools_Timer" xml:space="preserve"><value>Timer</value></data>
|
||||
<data name="Tools_RandomDraw" xml:space="preserve"><value>Random draw</value></data>
|
||||
<data name="Tools_SingleDraw" xml:space="preserve"><value>Single draw</value></data>
|
||||
<data name="Tools_Save" xml:space="preserve"><value>Save</value></data>
|
||||
<data name="Tools_Open" xml:space="preserve"><value>Open...</value></data>
|
||||
<data name="Tools_Replay" xml:space="preserve"><value>Replay</value></data>
|
||||
<data name="Tools_Screenshot" xml:space="preserve"><value>Screenshot</value></data>
|
||||
<data name="Tools_Manual" xml:space="preserve"><value>Manual</value></data>
|
||||
<data name="Tools_Settings" xml:space="preserve"><value>Settings</value></data>
|
||||
<data name="QuickPanel_SingleDraw" xml:space="preserve"><value>Single draw</value></data>
|
||||
<data name="QuickPanel_RandomDraw" xml:space="preserve"><value>Random draw</value></data>
|
||||
<data name="QuickPanel_Timer" xml:space="preserve"><value>Timer</value></data>
|
||||
<data name="QuickPanel_Whiteboard" xml:space="preserve"><value>Whiteboard</value></data>
|
||||
<data name="QuickPanel_ExitShow" xml:space="preserve"><value>Exit slide show</value></data>
|
||||
<data name="QuickPanel_Show" xml:space="preserve"><value>Show</value></data>
|
||||
<data name="QuickPanel_Exit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="Backup_Title" xml:space="preserve"><value>Settings backup & restore</value></data>
|
||||
<data name="Backup_Desc" xml:space="preserve"><value># You can manually back up current settings or restore previous backups; backups are also created automatically before updates.</value></data>
|
||||
<data name="Backup_AutoBeforeUpdate" xml:space="preserve"><value>Backup before update</value></data>
|
||||
<data name="Backup_AutoPeriodic" xml:space="preserve"><value>Periodic auto-backup</value></data>
|
||||
<data name="Backup_Interval" xml:space="preserve"><value>Backup interval</value></data>
|
||||
<data name="Backup_Interval_1Day" xml:space="preserve"><value>1 day</value></data>
|
||||
<data name="Backup_Interval_3Days" xml:space="preserve"><value>3 days</value></data>
|
||||
<data name="Backup_Interval_7Days" xml:space="preserve"><value>7 days</value></data>
|
||||
<data name="Backup_Interval_14Days" xml:space="preserve"><value>14 days</value></data>
|
||||
<data name="Backup_Interval_30Days" xml:space="preserve"><value>30 days</value></data>
|
||||
<data name="Backup_Interval_DefaultHint" xml:space="preserve"><value>(default: 7 days)</value></data>
|
||||
<data name="Backup_Manual" xml:space="preserve"><value>Backup now</value></data>
|
||||
<data name="Backup_Restore" xml:space="preserve"><value>Restore backup</value></data>
|
||||
<data name="ConfigProfiles_Title" xml:space="preserve"><value>Config profiles & hot reload</value></data>
|
||||
<data name="ConfigProfiles_Desc" xml:space="preserve"><value># Selecting a profile switches and hot-reloads it; \"Save as\" saves current settings as a new profile.</value></data>
|
||||
<data name="ConfigProfiles_Label" xml:space="preserve"><value>Profile:</value></data>
|
||||
<data name="ConfigProfiles_Delete" xml:space="preserve"><value>Delete profile</value></data>
|
||||
<data name="ConfigProfiles_SaveAs" xml:space="preserve"><value>Save as profile</value></data>
|
||||
<data name="Automation_Title" xml:space="preserve"><value>Automation</value></data>
|
||||
<data name="Automation_AutoFoldTitle" xml:space="preserve"><value>Auto fold</value></data>
|
||||
<data name="AutoFold_App_SeewoBoard5" xml:space="preserve"><value>Seewo Whiteboard 5</value></data>
|
||||
<data name="AutoFold_App_SeewoCamera" xml:space="preserve"><value>Seewo Visual Presenter</value></data>
|
||||
<data name="AutoFold_App_SeewoBoard3" xml:space="preserve"><value>Seewo Whiteboard 3</value></data>
|
||||
<data name="AutoFold_App_SeewoLightBoard" xml:space="preserve"><value>Seewo Lite Whiteboard</value></data>
|
||||
<data name="AutoFold_App_SeewoLightBoard5C" xml:space="preserve"><value>Seewo Lite Whiteboard 5C</value></data>
|
||||
<data name="AutoFold_App_SeewoPinco" xml:space="preserve"><value>Seewo Pinco</value></data>
|
||||
<data name="AutoFold_App_HiteBoard" xml:space="preserve"><value>HiteBoard</value></data>
|
||||
<data name="AutoFold_App_HiteCamera" xml:space="preserve"><value>Hite visual presenter</value></data>
|
||||
<data name="AutoFold_App_HiteLightBoard" xml:space="preserve"><value>Hite Lite Whiteboard</value></data>
|
||||
<data name="AutoFold_App_WenXiangBoard" xml:space="preserve"><value>WenXiang Whiteboard</value></data>
|
||||
<data name="AutoFold_App_MSWhiteboard" xml:space="preserve"><value>Microsoft Whiteboard</value></data>
|
||||
<data name="AutoFold_App_AdmoxBoard" xml:space="preserve"><value>Admox Whiteboard</value></data>
|
||||
<data name="AutoFold_App_AdmoxBooth" xml:space="preserve"><value>Admox visual presenter</value></data>
|
||||
<data name="AutoFold_App_YiYunBoard" xml:space="preserve"><value>YiYun Whiteboard</value></data>
|
||||
<data name="AutoFold_App_YiYunBooth" xml:space="preserve"><value>YiYun visual presenter</value></data>
|
||||
<data name="AutoFold_App_MaxHubBoard" xml:space="preserve"><value>MaxHub Whiteboard</value></data>
|
||||
<data name="AutoFold_IgnoreEasiNoteDesktopAnno" xml:space="preserve"><value>Ignore EN5 desktop annotation window when auto folding</value></data>
|
||||
<data name="AutoFold_OldZyBoard" xml:space="preserve"><value>Auto fold when entering old ZhongYuan whiteboard</value></data>
|
||||
<data name="Automation_AutoFoldInPPT" xml:space="preserve"><value>Auto fold while playing PPT</value></data>
|
||||
<data name="Automation_KeepFoldAfterExit" xml:space="preserve"><value>Keep folded after app exit</value></data>
|
||||
<data name="Automation_KeepFoldAfterExitHint" xml:space="preserve"><value># When on, apps that trigger auto fold will stay folded even after they exit.</value></data>
|
||||
<data name="AutoKill_Title" xml:space="preserve"><value>Auto kill</value></data>
|
||||
<data name="AutoKill_PptTools" xml:space="preserve"><value>Auto kill Seewo PPT tools</value></data>
|
||||
<data name="AutoKill_PptToolsHint" xml:space="preserve"><value># Killing PPT tools disables Seewo classroom helper. Delete Office.dll in its install folder to stop the PPT toolbar without auto kill.</value></data>
|
||||
<data name="AutoKill_EasiNote5" xml:space="preserve"><value>Auto kill Seewo Whiteboard 5</value></data>
|
||||
<data name="AutoKill_HiteAnnotation" xml:space="preserve"><value>Auto kill Hite screen writing</value></data>
|
||||
<data name="AutoKill_HiteAfterKillEnterAnnotation" xml:space="preserve"><value>Enter annotation after killing Hite screen writing</value></data>
|
||||
<data name="AutoKill_YouJiao" xml:space="preserve"><value>Auto kill YouJiao teacher</value></data>
|
||||
<data name="AutoKill_SeewoDesktop2Anno" xml:space="preserve"><value>Auto kill Seewo Desktop 2.0 annotation</value></data>
|
||||
<data name="AutoKill_SeewoDesktop2AnnoHint" xml:space="preserve"><value># Seewo Desktop 2.0 annotation is 64-bit so ICC (32-bit) cannot inspect it deeply; only process name DesktopAnnotation is matched. If you have another app with the same name, keep this off.</value></data>
|
||||
<data name="AutoKill_SameAppTitle" xml:space="preserve"><value>Kill similar apps</value></data>
|
||||
<data name="AutoKill_InkCanvasIC" xml:space="preserve"><value>Auto kill Ink Canvas and IC+</value></data>
|
||||
<data name="AutoKill_ICA" xml:space="preserve"><value>Auto kill ICA (both new & old)</value></data>
|
||||
<data name="AutoKill_Inkeys" xml:space="preserve"><value>Auto kill Inkeys (new only)</value></data>
|
||||
<data name="FileAssoc_Title" xml:space="preserve"><value>File association</value></data>
|
||||
<data name="FileAssoc_Desc" xml:space="preserve"><value>Manage .icstk file association so double-click opens in Ink Canvas.</value></data>
|
||||
<data name="FileAssoc_Unregister" xml:space="preserve"><value>Remove association</value></data>
|
||||
<data name="FileAssoc_Check" xml:space="preserve"><value>Check status</value></data>
|
||||
<data name="FileAssoc_Register" xml:space="preserve"><value>Register association</value></data>
|
||||
<data name="FloatingInterceptor_Title" xml:space="preserve"><value>Floating window interceptor</value></data>
|
||||
<data name="FloatingInterceptor_Desc" xml:space="preserve"><value>Detect and block floating windows from similar software</value></data>
|
||||
<data name="FloatingInterceptor_Enable" xml:space="preserve"><value>Enable floating window interceptor</value></data>
|
||||
<data name="FloatingInterceptor_StatusNotRunning" xml:space="preserve"><value>Interceptor not running</value></data>
|
||||
<data name="Storage_AutoScreenshotOnClear" xml:space="preserve"><value>Auto screenshot on clear</value></data>
|
||||
<data name="Storage_ScreenshotsByDateFolder" xml:space="preserve"><value>Save screenshots in date folders</value></data>
|
||||
<data name="Storage_AutoSaveInkOnScreenshot" xml:space="preserve"><value>Auto-save ink when screenshotting</value></data>
|
||||
<data name="Storage_AutoSaveInk" xml:space="preserve"><value>Auto-save ink periodically</value></data>
|
||||
<data name="Storage_AutoSaveInterval" xml:space="preserve"><value>Save interval</value></data>
|
||||
<data name="Storage_AutoSaveInterval_1Min" xml:space="preserve"><value>1 minute</value></data>
|
||||
<data name="Storage_AutoSaveInterval_3Min" xml:space="preserve"><value>3 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_5Min" xml:space="preserve"><value>5 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_10Min" xml:space="preserve"><value>10 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_15Min" xml:space="preserve"><value>15 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_30Min" xml:space="preserve"><value>30 minutes</value></data>
|
||||
<data name="Storage_AutoSaveInterval_60Min" xml:space="preserve"><value>60 minutes</value></data>
|
||||
<data name="Storage_AutoSaveHint" xml:space="preserve"><value># When on, strokes are auto-saved at the set interval, only when canvas is visible and has ink.</value></data>
|
||||
<data name="Storage_SaveFullPageStrokes" xml:space="preserve"><value>Save full-page strokes</value></data>
|
||||
<data name="Storage_SaveFullPageHint" xml:space="preserve"><value># When on, auto/manual saves store all pages in fullscreen; multiple pages are packed in one archive (whiteboard strokes open only in whiteboard mode; PPT strokes only in slide show mode).</value></data>
|
||||
<data name="Storage_SaveAsXml" xml:space="preserve"><value>Save as XML format</value></data>
|
||||
<data name="Storage_SaveAsXmlHint" xml:space="preserve"><value># When on, strokes are saved as XML (ISF) for easier inspection and editing.</value></data>
|
||||
<data name="Storage_AutoScreenshotMinInk" xml:space="preserve"><value>Minimum ink for auto screenshot</value></data>
|
||||
<data name="Storage_PathTitle" xml:space="preserve"><value>Stroke and screenshot save path</value></data>
|
||||
<data name="Storage_PathBrowse" xml:space="preserve"><value>Browse</value></data>
|
||||
<data name="Storage_PathSetToD" xml:space="preserve"><value>Set save path to D:\Ink Canvas</value></data>
|
||||
<data name="Storage_PathSetToDocuments" xml:space="preserve"><value>Set save path to Documents</value></data>
|
||||
<data name="Storage_PathPermissionHint" xml:space="preserve"><value># Please ensure the save folder is writable.</value></data>
|
||||
<data name="Storage_AutoDeleteTitle" xml:space="preserve"><value>Auto delete old strokes and screenshots</value></data>
|
||||
<data name="Storage_AutoDeleteHint" xml:space="preserve"><value># When on, all .icstk and .png files in the auto-save folder may be deleted!</value></data>
|
||||
<data name="Storage_RetentionTitle" xml:space="preserve"><value>Retention duration</value></data>
|
||||
<data name="Storage_RetentionUnitDays" xml:space="preserve"><value>days</value></data>
|
||||
<data name="CloudStorage_Manage" xml:space="preserve"><value>Cloud storage management</value></data>
|
||||
<data name="FoldMode_Title" xml:space="preserve"><value>Fold mode</value></data>
|
||||
<data name="FoldMode_ExitToAnnotation" xml:space="preserve"><value>Switch to annotation when exiting fold mode</value></data>
|
||||
<data name="FoldMode_ExitToAnnotationHint" xml:space="preserve"><value># When on, exiting fold mode switches back to annotation for convenience.</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterPPT" xml:space="preserve"><value>Auto fold floating bar after PPT show</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterPPTHint" xml:space="preserve"><value># When on, floating bar is auto-folded after exiting PPT slide show.</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterWhiteboard" xml:space="preserve"><value>Auto fold when exiting whiteboard</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterWhiteboardHint" xml:space="preserve"><value># When on, exiting whiteboard folds back to sidebar.</value></data>
|
||||
<data name="Random_Title" xml:space="preserve"><value>Random roll call</value></data>
|
||||
<data name="Random_ShowEditNamesButton" xml:space="preserve"><value>Show button to edit name list</value></data>
|
||||
<data name="Random_BackgroundSettingsTitle" xml:space="preserve"><value>Roll-call window background (legacy UI only)</value></data>
|
||||
<data name="Random_BackgroundSelectLabel" xml:space="preserve"><value>Background:</value></data>
|
||||
<data name="Random_Background_Default" xml:space="preserve"><value>Default background</value></data>
|
||||
<data name="Random_CustomBackgroundLabel" xml:space="preserve"><value>Custom background:</value></data>
|
||||
<data name="Random_CustomBackground_Upload" xml:space="preserve"><value>Upload</value></data>
|
||||
<data name="Random_CustomBackground_Manage" xml:space="preserve"><value>Manage</value></data>
|
||||
<data name="Random_EnableButtons" xml:space="preserve"><value>Enable random & single-draw buttons</value></data>
|
||||
<data name="Random_EnableQuickButton" xml:space="preserve"><value>Enable quick-draw floating button</value></data>
|
||||
<data name="Random_UseExternal" xml:space="preserve"><value>Use external roll-call app</value></data>
|
||||
<data name="Random_ExternalTypeLabel" xml:space="preserve"><value>Roll-call type</value></data>
|
||||
<data name="Random_ExternalType_ClassIsland" xml:space="preserve"><value>ClassIsland</value></data>
|
||||
<data name="Random_ExternalType_SecRandom" xml:space="preserve"><value>SecRandom</value></data>
|
||||
<data name="Random_ExternalType_NamePicker" xml:space="preserve"><value>NamePicker</value></data>
|
||||
<data name="Random_OnceCloseDelay" xml:space="preserve"><value>Single-draw window close delay</value></data>
|
||||
<data name="Random_OnceMaxStudents" xml:space="preserve"><value>Max students per single draw</value></data>
|
||||
<data name="Random_NewUI_Title" xml:space="preserve"><value>New roll-call UI</value></data>
|
||||
<data name="Random_NewUI_Enable" xml:space="preserve"><value>Enable new roll-call UI</value></data>
|
||||
<data name="Random_ML_AvoidRepeat" xml:space="preserve"><value>Use machine learning to avoid repeats</value></data>
|
||||
<data name="Random_ML_HistoryCount" xml:space="preserve"><value>History count for avoidance</value></data>
|
||||
<data name="Random_ML_Weight" xml:space="preserve"><value>Avoidance weight</value></data>
|
||||
<data name="Random_ML_Hint" xml:space="preserve"><value># ML analyzes recent roll-call history to avoid repeating the same students.</value></data>
|
||||
<data name="Timer_Title" xml:space="preserve"><value>Timer settings</value></data>
|
||||
<data name="Timer_UseLegacyButtons" xml:space="preserve"><value>Use legacy timer button UI</value></data>
|
||||
<data name="Timer_NewUI" xml:space="preserve"><value>New timer UI</value></data>
|
||||
<data name="Timer_EnableCountUp" xml:space="preserve"><value>Enable count-up after timeout</value></data>
|
||||
<data name="Timer_OvertimeHighlight" xml:space="preserve"><value>Highlight numbers when overtime</value></data>
|
||||
<data name="Timer_Volume" xml:space="preserve"><value>Timer alert volume</value></data>
|
||||
<data name="Timer_CustomSoundLabel" xml:space="preserve"><value>Custom alert sound:</value></data>
|
||||
<data name="Timer_SelectFile" xml:space="preserve"><value>Select file</value></data>
|
||||
<data name="Timer_Reset" xml:space="preserve"><value>Reset</value></data>
|
||||
<data name="Timer_Progressive" xml:space="preserve"><value>Progressive reminder</value></data>
|
||||
<data name="Timer_ProgressiveVolume" xml:space="preserve"><value>Progressive reminder volume</value></data>
|
||||
<data name="Timer_ProgressiveCustomLabel" xml:space="preserve"><value>Custom progressive reminder audio:</value></data>
|
||||
<data name="Timer_ProgressiveSelectFile" xml:space="preserve"><value>Select file</value></data>
|
||||
<data name="Timer_ProgressiveReset" xml:space="preserve"><value>Reset</value></data>
|
||||
<data name="About_Title" xml:space="preserve"><value>About</value></data>
|
||||
<data name="About_DeviceInfo" xml:space="preserve"><value>Device information</value></data>
|
||||
<data name="About_DeviceIdLabel" xml:space="preserve"><value>Device ID:</value></data>
|
||||
<data name="About_UsageFrequencyLabel" xml:space="preserve"><value>Usage frequency:</value></data>
|
||||
<data name="About_UpdatePriorityLabel" xml:space="preserve"><value>Update priority:</value></data>
|
||||
<data name="About_LaunchCountLabel" xml:space="preserve"><value>Launch count:</value></data>
|
||||
<data name="About_TotalUsageLabel" xml:space="preserve"><value>Total usage time:</value></data>
|
||||
<data name="About_DeviceInfo_Loading" xml:space="preserve"><value>Loading...</value></data>
|
||||
<data name="About_RefreshDeviceInfo" xml:space="preserve"><value>Refresh device info</value></data>
|
||||
<data name="About_PrivacyCheckboxPrefix" xml:space="preserve"><value>I have read and agree to the </value></data>
|
||||
<data name="About_PrivacyCheckboxSuffix" xml:space="preserve"><value> privacy statement</value></data>
|
||||
<data name="About_TelemetryLabel" xml:space="preserve"><value>Anonymous usage data upload:</value></data>
|
||||
<data name="About_Telemetry_Off" xml:space="preserve"><value>Off (no upload)</value></data>
|
||||
<data name="About_Telemetry_Basic" xml:space="preserve"><value>Upload basic data</value></data>
|
||||
<data name="About_Telemetry_Optional" xml:space="preserve"><value>Upload basic + optional data</value></data>
|
||||
<data name="About_LicenseHint" xml:space="preserve"><value># Before using or distributing this software, you must be aware of the related open-source licenses. This software is based on https://github.com/WXRIW/Ink-Canvas.</value></data>
|
||||
<data name="About_LicenseTitle" xml:space="preserve"><value>This software, ICA and Ink Canvas are all open sourced under a license</value></data>
|
||||
<data name="About_LicenseBody" xml:space="preserve"><value>The strong copyleft license requires that complete source code and modifications of the licensed work (including large works using it) be provided under the same license. Copyright and license notices must be retained. Contributors explicitly grant patent rights.</value></data>
|
||||
<data name="About_DevelopersLabel" xml:space="preserve"><value>Developers:</value></data>
|
||||
<data name="About_Dev_ICCCE" xml:space="preserve"><value>Developer of ICC CE</value></data>
|
||||
<data name="About_Dev_ICC" xml:space="preserve"><value>Developer of ICC</value></data>
|
||||
<data name="About_Dev_ICA" xml:space="preserve"><value>Developer of ICA</value></data>
|
||||
<data name="About_Dev_InkCanvas" xml:space="preserve"><value>Developer of Ink Canvas</value></data>
|
||||
<data name="About_Source_ICC" xml:space="preserve"><value>ICC repository:</value></data>
|
||||
<data name="About_Source_ICA" xml:space="preserve"><value>ICA repository:</value></data>
|
||||
<data name="About_Source_InkCanvas" xml:space="preserve"><value>Ink Canvas repository:</value></data>
|
||||
<data name="About_ThanksContributors" xml:space="preserve"><value>Thanks to the following contributors:</value></data>
|
||||
<data name="About_Copyright" xml:space="preserve"><value>© 2025-2026 CJK_mkp. All rights reserved.</value></data>
|
||||
<data name="About_OpenSourceSlogan" xml:space="preserve"><value>We love open-source forever!</value></data>
|
||||
<data name="About_VersionLabel" xml:space="preserve"><value>Version:</value></data>
|
||||
<data name="Common_Close" xml:space="preserve"><value>Close</value></data>
|
||||
<data name="Common_On" xml:space="preserve"><value>On</value></data>
|
||||
<data name="Common_Off" xml:space="preserve"><value>Off</value></data>
|
||||
<data name="Advanced_UriSchemeName" xml:space="preserve"><value>External URI scheme (icc://)</value></data>
|
||||
<data name="Advanced_NibModeBoundsWidthHeader" xml:space="preserve"><value>Nib mode BoundsWidth</value></data>
|
||||
<data name="Advanced_FingerModeBoundsWidthHeader" xml:space="preserve"><value>Finger mode BoundsWidth</value></data>
|
||||
<data name="Advanced_EdgeGestureUtilHint_Part1" xml:space="preserve"><value># EdgeGestureUtil is newly introduced in ICC to temporarily block edge gestures when using touch (e.g., on Windows 10: swipe from the left edge to Task View, from the right edge to Action Center; on Windows 11: swipe up from the bottom to open Start). It works by using</value></data>
|
||||
<data name="Advanced_EdgeGestureUtilHint_Part2" xml:space="preserve"><value>(When the app window is active and in full-screen mode (or an owned window is active), prevents edge gesture behavior.) If anything is abnormal, turn this option off; it should take effect immediately. (Not available on Windows 7/8.)</value></data>
|
||||
<data name="Advanced_ForceFullScreen" xml:space="preserve"><value>Enable ForceFullScreen</value></data>
|
||||
<data name="Advanced_ForceFullScreenHint" xml:space="preserve"><value># When a window size change is detected, automatically uses Win32 API to set this window size to the primary monitor size (in device pixels). Turn it off if you don't need it; takes effect immediately.</value></data>
|
||||
<data name="Advanced_DPIChangeDetection" xml:space="preserve"><value>Enable DPIChangeDetection</value></data>
|
||||
<data name="Advanced_DPIChangeDetectionHint" xml:space="preserve"><value># When a system DPI change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Increasing DPI may trigger this; decreasing DPI won't auto-move—adjust manually.)</value></data>
|
||||
<data name="Advanced_ResolutionChangeDetection" xml:space="preserve"><value>Enable ResolutionChangeDetection</value></data>
|
||||
<data name="Advanced_ResolutionChangeDetectionHint" xml:space="preserve"><value># When a screen resolution change is detected, it tries to keep FloatingBar visible. If it goes off-screen, it will attempt to move it into the visible area. (Reducing resolution may trigger this; if it's still on-screen it won't auto-adjust—adjust manually.)</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoBoard3" xml:space="preserve"><value>Seewo Whiteboard 3</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoBoard5" xml:space="preserve"><value>Seewo Whiteboard 5</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoBoard5C" xml:space="preserve"><value>Seewo Whiteboard 5C</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoPinco" xml:space="preserve"><value>Seewo Pinco</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoPincoDrawing" xml:space="preserve"><value>Seewo Pinco pen</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoPPTTools" xml:space="preserve"><value>Seewo PPT Tools</value></data>
|
||||
<data name="FloatingInterceptor_App_AiClass" xml:space="preserve"><value>AiClass</value></data>
|
||||
<data name="FloatingInterceptor_App_HiteAnnotation" xml:space="preserve"><value>Hite screen writing</value></data>
|
||||
<data name="FloatingInterceptor_App_ChangYanClass" xml:space="preserve"><value>Changyan smart classroom</value></data>
|
||||
<data name="FloatingInterceptor_App_ChangYanPPT" xml:space="preserve"><value>Changyan PPT</value></data>
|
||||
<data name="FloatingInterceptor_App_IntelligentClass" xml:space="preserve"><value>Tianyu Education Cloud</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoDesktopAnnotation" xml:space="preserve"><value>Seewo desktop pen</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoDesktopSideBar" xml:space="preserve"><value>Seewo desktop sidebar</value></data>
|
||||
<data name="Board_MultiTouchWriting" xml:space="preserve"><value>Multi-touch writing</value></data>
|
||||
<data name="Board_TwoFingerMove" xml:space="preserve"><value>Two-finger move</value></data>
|
||||
<data name="Board_TwoFingerZoom" xml:space="preserve"><value>Two-finger zoom</value></data>
|
||||
<data name="Board_TwoFingerRotate" xml:space="preserve"><value>Two-finger rotate</value></data>
|
||||
<data name="Board_Background" xml:space="preserve"><value>Background</value></data>
|
||||
<data name="Board_Select" xml:space="preserve"><value>Select</value></data>
|
||||
<data name="Board_Pen" xml:space="preserve"><value>Pen</value></data>
|
||||
<data name="Board_Highlighter" xml:space="preserve"><value>Highlighter</value></data>
|
||||
<data name="Board_Eraser" xml:space="preserve"><value>Eraser</value></data>
|
||||
<data name="Board_EraserOptions" xml:space="preserve"><value>Eraser options</value></data>
|
||||
<data name="Board_Size" xml:space="preserve"><value>Size</value></data>
|
||||
<data name="Board_EraserShape" xml:space="preserve"><value>Eraser shape</value></data>
|
||||
<data name="Board_EraserShape_Circle" xml:space="preserve"><value>Circle</value></data>
|
||||
<data name="Board_EraserShape_Blackboard" xml:space="preserve"><value>Blackboard</value></data>
|
||||
<data name="Board_ClearInk" xml:space="preserve"><value>Clear ink</value></data>
|
||||
<data name="Board_ClearInkAndHistory" xml:space="preserve"><value>Clear ink & history</value></data>
|
||||
<data name="Board_StrokeEraser" xml:space="preserve"><value>Stroke eraser</value></data>
|
||||
<data name="Board_Shape" xml:space="preserve"><value>Shapes</value></data>
|
||||
<data name="Board_ShapeHintLongPress" xml:space="preserve"><value>(Long-press in first row to keep selected)</value></data>
|
||||
<data name="Board_AutoHide" xml:space="preserve"><value>Auto-hide</value></data>
|
||||
<data name="Board_InsertImage" xml:space="preserve"><value>Insert image</value></data>
|
||||
<data name="Board_SelectImage" xml:space="preserve"><value>Select image</value></data>
|
||||
<data name="Board_Screenshot" xml:space="preserve"><value>Screenshot</value></data>
|
||||
<data name="Board_Undo" xml:space="preserve"><value>Undo</value></data>
|
||||
<data name="Board_Redo" xml:space="preserve"><value>Redo</value></data>
|
||||
<data name="Board_Tools" xml:space="preserve"><value>Tools</value></data>
|
||||
<data name="Board_Exit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="Board_NewPage" xml:space="preserve"><value>New page</value></data>
|
||||
<data name="Board_PreviousPage" xml:space="preserve"><value>Previous</value></data>
|
||||
<data name="Board_NextPage" xml:space="preserve"><value>Next</value></data>
|
||||
<data name="Board_Page" xml:space="preserve"><value>Page</value></data>
|
||||
<data name="Board_DeleteThisPage" xml:space="preserve"><value>Delete this page</value></data>
|
||||
<data name="Notification_TestText" xml:space="preserve"><value>Test text</value></data>
|
||||
<data name="OldUI_Exit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="OldUI_Thickness" xml:space="preserve"><value>Thickness</value></data>
|
||||
<data name="OldUI_Dark" xml:space="preserve"><value>Dark</value></data>
|
||||
<data name="OldUI_Background" xml:space="preserve"><value>Background</value></data>
|
||||
<data name="OldUI_HideCanvas" xml:space="preserve"><value>Hide
canvas</value></data>
|
||||
<data name="OldUI_Check" xml:space="preserve"><value>Check</value></data>
|
||||
<data name="OldUI_SlideshowFromStart" xml:space="preserve"><value>From start
slideshow</value></data>
|
||||
<data name="OldUI_SlideshowEnd" xml:space="preserve"><value>End
slideshow</value></data>
|
||||
<data name="OldUI_SingleFingerDrag" xml:space="preserve"><value>One finger
drag</value></data>
|
||||
<data name="OldUI_Restore" xml:space="preserve"><value>Restore</value></data>
|
||||
<data name="OldUI_ClearAndHide" xml:space="preserve"><value>Clear
&
Hide</value></data>
|
||||
<data name="FloatingBar_Mouse" xml:space="preserve"><value>Mouse</value></data>
|
||||
<data name="FloatingBar_Annotate" xml:space="preserve"><value>Annotate</value></data>
|
||||
<data name="FloatingBar_Clear" xml:space="preserve"><value>Clear</value></data>
|
||||
<data name="Booth_Title" xml:space="preserve"><value>Visual presenter</value></data>
|
||||
<data name="Booth_CapturedPhotos" xml:space="preserve"><value>Captured photos</value></data>
|
||||
<data name="Booth_CameraDevices" xml:space="preserve"><value>Camera devices</value></data>
|
||||
<data name="Booth_Present" xml:space="preserve"><value>Present</value></data>
|
||||
<data name="Booth_Correction" xml:space="preserve"><value>Correct</value></data>
|
||||
<data name="Booth_Capture" xml:space="preserve"><value>Capture</value></data>
|
||||
<data name="Booth_Rotate" xml:space="preserve"><value>Rotate</value></data>
|
||||
<data name="Theme_LanguageLabel" xml:space="preserve"><value>UI language</value></data>
|
||||
<data name="Theme_Language_System" xml:space="preserve"><value>Follow system</value></data>
|
||||
<data name="Theme_Language_ChineseSimplified" xml:space="preserve"><value>Chinese (Simplified)</value></data>
|
||||
<data name="Theme_Language_English" xml:space="preserve"><value>English</value></data>
|
||||
<data name="Theme_Language_RestartHint" xml:space="preserve"><value>You need to restart the app for language changes to fully take effect.</value></data>
|
||||
<data name="FloatingBar_AreaEraser" xml:space="preserve"><value>Area eraser</value></data>
|
||||
<data name="FloatingBar_StrokeEraser" xml:space="preserve"><value>Stroke eraser</value></data>
|
||||
<data name="FloatingBar_LassoSelect" xml:space="preserve"><value>Lasso</value></data>
|
||||
<data name="FloatingBar_Geometry" xml:space="preserve"><value>Geometry</value></data>
|
||||
<data name="FloatingBar_ClearAndMouse" xml:space="preserve"><value>Clear & cursor</value></data>
|
||||
<data name="FloatingBar_Whiteboard" xml:space="preserve"><value>Board</value></data>
|
||||
<data name="FloatingBar_Hide" xml:space="preserve"><value>Hide</value></data>
|
||||
<data name="Geometry_Title" xml:space="preserve"><value>Geometry drawing</value></data>
|
||||
<data name="Geometry_DrawLine" xml:space="preserve"><value>Line</value></data>
|
||||
<data name="Geometry_DrawDashedLine" xml:space="preserve"><value>Dashed line</value></data>
|
||||
<data name="Geometry_DrawDottedLine" xml:space="preserve"><value>Dotted line</value></data>
|
||||
<data name="Geometry_DrawArrow" xml:space="preserve"><value>Arrow</value></data>
|
||||
<data name="Geometry_DrawParallelLines" xml:space="preserve"><value>4 parallel lines</value></data>
|
||||
<data name="Geometry_DrawCenteredSquare" xml:space="preserve"><value>Centered square</value></data>
|
||||
<data name="Geometry_DrawCenteredCircle" xml:space="preserve"><value>Centered circle</value></data>
|
||||
<data name="Geometry_DrawCenteredDashedCircle" xml:space="preserve"><value>Centered dashed circle</value></data>
|
||||
<data name="Geometry_DrawCenteredEllipse" xml:space="preserve"><value>Centered ellipse</value></data>
|
||||
<data name="Geometry_DrawCuboid" xml:space="preserve"><value>Cuboid</value></data>
|
||||
<data name="Geometry_DrawSquare" xml:space="preserve"><value>Square</value></data>
|
||||
<data name="Geometry_DrawCylinder" xml:space="preserve"><value>Cylinder</value></data>
|
||||
<data name="Geometry_DrawCone" xml:space="preserve"><value>Cone</value></data>
|
||||
<data name="FloatingBar_GestureButton" xml:space="preserve"><value>Gesture</value></data>
|
||||
<data name="FloatingBar_GesturePanelTitle" xml:space="preserve"><value>Gesture options</value></data>
|
||||
<data name="FloatingBar_Gesture_MultiTouchWriting" xml:space="preserve"><value>Multi-touch writing</value></data>
|
||||
<data name="FloatingBar_Gesture_TwoFingerMove" xml:space="preserve"><value>Two-finger move</value></data>
|
||||
<data name="FloatingBar_Gesture_TwoFingerZoom" xml:space="preserve"><value>Two-finger zoom</value></data>
|
||||
<data name="FloatingBar_Gesture_TwoFingerRotate" xml:space="preserve"><value>Two-finger rotate</value></data>
|
||||
<data name="Board_Gesture" xml:space="preserve"><value>Gesture</value></data>
|
||||
<data name="Board_GestureOptions" xml:space="preserve"><value>Gesture options</value></data>
|
||||
</root>
|
||||
@@ -0,0 +1,772 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Nav_Plugins" xml:space="preserve">
|
||||
<value>插件</value>
|
||||
<comment>设置侧栏 - 插件</comment>
|
||||
</data>
|
||||
<data name="Nav_Startup" xml:space="preserve">
|
||||
<value>启动设置</value>
|
||||
<comment>设置侧栏 - 启动设置</comment>
|
||||
</data>
|
||||
<data name="Nav_Canvas" xml:space="preserve">
|
||||
<value>画布设置</value>
|
||||
<comment>设置侧栏 - 画布设置</comment>
|
||||
</data>
|
||||
<data name="Nav_CrashAction" xml:space="preserve">
|
||||
<value>崩溃处理</value>
|
||||
<comment>设置侧栏 - 崩溃处理</comment>
|
||||
</data>
|
||||
<data name="Nav_Gesture" xml:space="preserve">
|
||||
<value>手势</value>
|
||||
<comment>设置侧栏 - 手势</comment>
|
||||
</data>
|
||||
<data name="Nav_InkRecognition" xml:space="preserve">
|
||||
<value>墨迹识别</value>
|
||||
<comment>设置侧栏 - 墨迹识别</comment>
|
||||
</data>
|
||||
<data name="Nav_PPT" xml:space="preserve">
|
||||
<value>PPT</value>
|
||||
<comment>设置侧栏 - PPT</comment>
|
||||
</data>
|
||||
<data name="Nav_Advanced" xml:space="preserve">
|
||||
<value>高级</value>
|
||||
<comment>设置侧栏 - 高级</comment>
|
||||
</data>
|
||||
<data name="Nav_Automation" xml:space="preserve">
|
||||
<value>自动化</value>
|
||||
<comment>设置侧栏 - 自动化</comment>
|
||||
</data>
|
||||
<data name="Nav_RandomWindow" xml:space="preserve">
|
||||
<value>随机选人</value>
|
||||
<comment>设置侧栏 - 随机选人</comment>
|
||||
</data>
|
||||
<data name="Nav_Theme" xml:space="preserve">
|
||||
<value>主题</value>
|
||||
<comment>设置侧栏 - 主题</comment>
|
||||
</data>
|
||||
<data name="Nav_Shortcuts" xml:space="preserve">
|
||||
<value>快捷键</value>
|
||||
<comment>设置侧栏 - 快捷键</comment>
|
||||
</data>
|
||||
<data name="Nav_About" xml:space="preserve">
|
||||
<value>关于</value>
|
||||
<comment>设置侧栏 - 关于</comment>
|
||||
</data>
|
||||
<data name="App_Title" xml:space="preserve">
|
||||
<value>InkCanvasforClass</value>
|
||||
<comment>主窗口标题</comment>
|
||||
</data>
|
||||
<data name="Booth_Resolution_Tooltip" xml:space="preserve">
|
||||
<value>展台/截图分辨率</value>
|
||||
<comment>展台分辨率选项卡提示</comment>
|
||||
</data>
|
||||
<data name="Nav_Gesture_Settings" xml:space="preserve"><value>手势设置</value></data>
|
||||
<data name="Nav_Theme_Settings" xml:space="preserve"><value>个性化设置</value></data>
|
||||
<data name="Nav_PPT_Settings" xml:space="preserve"><value>PPT设置</value></data>
|
||||
<data name="Nav_Advanced_Settings" xml:space="preserve"><value>高级设置</value></data>
|
||||
<data name="Nav_Automation_Settings" xml:space="preserve"><value>自动化设置</value></data>
|
||||
<data name="Nav_RandomWindow_Settings" xml:space="preserve"><value>随机窗口设置</value></data>
|
||||
<data name="Nav_Shortcuts_Settings" xml:space="preserve"><value>快捷键设置</value></data>
|
||||
<data name="CollapseNavSidebar" xml:space="preserve"><value>折叠侧边栏</value></data>
|
||||
<data name="ShowNavSidebar" xml:space="preserve"><value>显示侧边栏</value></data>
|
||||
<data name="Tooltip_IccProtocol" xml:space="preserve"><value>通过 icc:// 协议从外部控制软件</value></data>
|
||||
<data name="Settings_Title" xml:space="preserve"><value>设置</value></data>
|
||||
<data name="Settings_AutoSaveHint" xml:space="preserve"><value>设置更改将自动保存,部分设置需要重启软件后生效</value></data>
|
||||
<data name="Btn_Restart" xml:space="preserve"><value>重启</value></data>
|
||||
<data name="Btn_Reset" xml:space="preserve"><value>重置</value></data>
|
||||
<data name="Btn_Exit" xml:space="preserve"><value>退出</value></data>
|
||||
<data name="Settings_Mode" xml:space="preserve"><value>模式设置</value></data>
|
||||
<data name="Settings_ModeDesc" xml:space="preserve"><value>选择软件运行模式。仅PPT模式下,软件将完全隐藏,仅在PPT放映时出现。(实验性功能,可能不稳定。)</value></data>
|
||||
<data name="Mode_Normal" xml:space="preserve"><value>正常模式</value></data>
|
||||
<data name="Mode_PPTOnly" xml:space="preserve"><value>仅PPT模式</value></data>
|
||||
<data name="Settings_NewWindow" xml:space="preserve"><value>新设置窗口</value></data>
|
||||
<data name="Settings_NewWindowDesc" xml:space="preserve"><value>打开新的设置窗口,提供更丰富的设置选项和更好的用户体验。(开发中)</value></data>
|
||||
<data name="Btn_OpenNewSettings" xml:space="preserve"><value>打开新设置窗口</value></data>
|
||||
<data name="Settings_Plugins" xml:space="preserve"><value>插件管理</value></data>
|
||||
<data name="Settings_PluginsDesc" xml:space="preserve"><value>通过插件扩展InkCanvas的功能。您可以启用或禁用插件,或加载自定义插件。</value></data>
|
||||
<data name="Btn_OpenPluginManager" xml:space="preserve"><value>打开插件管理器</value></data>
|
||||
<data name="Startup_Start" xml:space="preserve"><value>启动</value></data>
|
||||
<data name="Startup_NoFocusMode" xml:space="preserve"><value>窗口无焦点模式</value></data>
|
||||
<data name="Startup_NoBorderMode" xml:space="preserve"><value>窗口无边框模式</value></data>
|
||||
<data name="Startup_TopMost" xml:space="preserve"><value>窗口置顶</value></data>
|
||||
<data name="Startup_UIATopMost" xml:space="preserve"><value>UIA置顶</value></data>
|
||||
<data name="Startup_UIATopMostHint" xml:space="preserve"><value># 开启UIA置顶后,软件需要管理员启动才能置顶…</value></data>
|
||||
<data name="Header_AutoUpdate" xml:space="preserve"><value>自动检查更新</value></data>
|
||||
<data name="Header_SilentUpdate" xml:space="preserve"><value>静默更新</value></data>
|
||||
<data name="SilentUpdate_Hint" xml:space="preserve"><value># 静默更新将在软件不使用时自动安装,无需手动操作</value></data>
|
||||
<data name="Update_Channel" xml:space="preserve"><value>更新通道</value></data>
|
||||
<data name="Channel_Release" xml:space="preserve"><value>稳定版 (Release)</value></data>
|
||||
<data name="Channel_Preview" xml:space="preserve"><value>预览版 (Preview)</value></data>
|
||||
<data name="Channel_Beta" xml:space="preserve"><value>测试版 (Beta)</value></data>
|
||||
<data name="Channel_Hint" xml:space="preserve"><value># 稳定版提供可靠更新,预览版提供新功能体验…</value></data>
|
||||
<data name="Btn_ManualUpdate" xml:space="preserve"><value>手动更新</value></data>
|
||||
<data name="ManualUpdate_Hint" xml:space="preserve"><value># 点击后立即检查并下载最新版本</value></data>
|
||||
<data name="Btn_VersionFix" xml:space="preserve"><value>版本修复</value></data>
|
||||
<data name="VersionFix_Hint" xml:space="preserve"><value># 版本修复会根据当前选择的通道下载最新版本并执行安装…</value></data>
|
||||
<data name="Btn_Rollback" xml:space="preserve"><value>历史版本回滚</value></data>
|
||||
<data name="Rollback_Hint" xml:space="preserve"><value># 历史版本回滚,点击后会弹出相应页面…</value></data>
|
||||
<data name="SilentUpdate_AfterDownloadHint" xml:space="preserve"><value># 关闭静默更新后,已完成安装包的下载后将会弹窗询问…</value></data>
|
||||
<data name="SilentUpdate_TimeRange" xml:space="preserve"><value>静默更新时间段</value></data>
|
||||
<data name="Time_Start" xml:space="preserve"><value>起始时间</value></data>
|
||||
<data name="Time_End" xml:space="preserve"><value>终止时间</value></data>
|
||||
<data name="TimeRange_Hint" xml:space="preserve"><value># 若终止时间小于起始时间…</value></data>
|
||||
<data name="Startup_RunAtLogin" xml:space="preserve"><value>开机时运行</value></data>
|
||||
<data name="Startup_MinimizeToSidebar" xml:space="preserve"><value>开机运行后收纳到侧边栏</value></data>
|
||||
<data name="Canvas_AndInk" xml:space="preserve"><value>画板和墨迹</value></data>
|
||||
<data name="Canvas_ShowPenCursor" xml:space="preserve"><value>显示画笔光标</value></data>
|
||||
<data name="Canvas_PressureTouch" xml:space="preserve"><value>启用压感触屏模式</value></data>
|
||||
<data name="Canvas_PressureTouchHint" xml:space="preserve"><value># 开启后,触屏设备也将支持压感效果…</value></data>
|
||||
<data name="Canvas_IgnorePressure" xml:space="preserve"><value>屏蔽压感</value></data>
|
||||
<data name="Canvas_IgnorePressureHint" xml:space="preserve"><value># 开启后,将忽略所有设备的压感信息…</value></data>
|
||||
<data name="Canvas_EraserSize" xml:space="preserve"><value>橡皮大小</value></data>
|
||||
<data name="Size_VerySmall" xml:space="preserve"><value>很小</value></data>
|
||||
<data name="Size_Small" xml:space="preserve"><value>较小</value></data>
|
||||
<data name="Size_Medium" xml:space="preserve"><value>中等</value></data>
|
||||
<data name="Size_Large" xml:space="preserve"><value>较大</value></data>
|
||||
<data name="Size_VeryLarge" xml:space="preserve"><value>很大</value></data>
|
||||
<data name="EraserSize_SwitchHint" xml:space="preserve"><value># 非实时切换,下一次使用面积擦时生效。</value></data>
|
||||
<data name="Canvas_HideInkOnExit" xml:space="preserve"><value>退出画板模式后隐藏墨迹</value></data>
|
||||
<data name="Canvas_HideInkOnExitHint" xml:space="preserve"><value># 开启 退出画板模式后隐藏墨迹 选项后…</value></data>
|
||||
<data name="Canvas_ClearInkHistory" xml:space="preserve"><value>清空墨迹时删除墨迹历史记录</value></data>
|
||||
<data name="Canvas_ClearImageOnClear" xml:space="preserve"><value>清空画布时同时清空图片</value></data>
|
||||
<data name="Canvas_CompressImage" xml:space="preserve"><value>插入图片时自动压缩(大于1920x1080)</value></data>
|
||||
<data name="Canvas_KeepAsymptote" xml:space="preserve"><value>保留双曲线渐近线</value></data>
|
||||
<data name="Yes" xml:space="preserve"><value>是</value></data>
|
||||
<data name="No" xml:space="preserve"><value>否</value></data>
|
||||
<data name="AskEachTime" xml:space="preserve"><value>每次询问</value></data>
|
||||
<data name="Canvas_AsymptoteHint" xml:space="preserve"><value># 请注意,若不保留双曲线渐近线可能会有遇到撤回相关的 BUG…</value></data>
|
||||
<data name="Canvas_ShowCircleCenter" xml:space="preserve"><value>绘制圆时显示圆心位置</value></data>
|
||||
<data name="Canvas_WPFBezier" xml:space="preserve"><value>使用WPF默认贝塞尔曲线平滑</value></data>
|
||||
<data name="Canvas_AdvancedSmoothing" xml:space="preserve"><value>使用高级曲线平滑(推荐)</value></data>
|
||||
<data name="Canvas_InkFade" xml:space="preserve"><value>启用墨迹渐隐功能</value></data>
|
||||
<data name="Canvas_InkFadeHint" xml:space="preserve"><value># 开启后墨迹不会绘制到画布上…</value></data>
|
||||
<data name="Canvas_InkFadeTime" xml:space="preserve"><value>墨迹渐隐时间</value></data>
|
||||
<data name="Canvas_HideFadeInPenMenu" xml:space="preserve"><value>在笔工具菜单中隐藏墨迹渐隐控制</value></data>
|
||||
<data name="Canvas_HideFadeInPenMenuHint" xml:space="preserve"><value># 开启后,主工具栏上点击笔工具后弹出的上下文菜单中将不显示…</value></data>
|
||||
<data name="Color" xml:space="preserve"><value>颜色</value></data>
|
||||
<data name="Color_Default" xml:space="preserve"><value>默认</value></data>
|
||||
<data name="Color_Black" xml:space="preserve"><value>黑色</value></data>
|
||||
<data name="Color_White" xml:space="preserve"><value>白色</value></data>
|
||||
<data name="Color_Red" xml:space="preserve"><value>红色</value></data>
|
||||
<data name="Color_Yellow" xml:space="preserve"><value>黄色</value></data>
|
||||
<data name="Color_Blue" xml:space="preserve"><value>蓝色</value></data>
|
||||
<data name="Color_Green" xml:space="preserve"><value>绿色</value></data>
|
||||
<data name="Color_Orange" xml:space="preserve"><value>橙色</value></data>
|
||||
<data name="Color_Purple" xml:space="preserve"><value>紫色</value></data>
|
||||
<data name="Msg_UpdateReady" xml:space="preserve"><value>更新已下载完成,将在软件关闭时自动安装。</value></data>
|
||||
<data name="Msg_UpdateReadyTitle" xml:space="preserve"><value>更新已准备就绪</value></data>
|
||||
<data name="Msg_UpdateDownloadFailed" xml:space="preserve"><value>更新下载失败,请检查网络连接后重试。</value></data>
|
||||
<data name="Msg_DownloadFailedTitle" xml:space="preserve"><value>下载失败</value></data>
|
||||
<data name="Msg_SkipVersion" xml:space="preserve"><value>已设置跳过版本 {0},在下次发布新版本之前不会再提示更新。</value></data>
|
||||
<data name="Msg_SkipVersionTitle" xml:space="preserve"><value>已跳过此版本</value></data>
|
||||
<data name="Msg_UnexpectedError" xml:space="preserve"><value>抱歉,出现未预期的异常,可能导致 InkCanvasForClass 运行不稳定。\n建议保存墨迹后重启应用。</value></data>
|
||||
<data name="Msg_RestartLimitTitle" xml:space="preserve"><value>重启次数过多</value></data>
|
||||
<data name="Msg_RestartLimit" xml:space="preserve"><value>检测到程序已连续重启5次,已停止自动重启。请联系开发者或检查系统环境。</value></data>
|
||||
<data name="Splash_Starting" xml:space="preserve"><value>正在启动 Ink Canvas...</value></data>
|
||||
<data name="Crash_Title" xml:space="preserve"><value>崩溃后操作</value></data>
|
||||
<data name="Crash_Desc" xml:space="preserve"><value>请选择软件发生未处理异常时的自动操作:</value></data>
|
||||
<data name="Crash_SilentRestart" xml:space="preserve"><value>静默重启软件</value></data>
|
||||
<data name="Crash_NoAction" xml:space="preserve"><value>无操作</value></data>
|
||||
<data name="Crash_Hint" xml:space="preserve"><value># 静默重启:崩溃后自动重启软件,无提示。无操作:崩溃后仅记录日志,不自动重启。</value></data>
|
||||
<data name="Gesture_Title" xml:space="preserve"><value>手势</value></data>
|
||||
<data name="Gesture_AutoToggleTwoFinger" xml:space="preserve"><value>进退白板模式自动开关双指移动功能</value></data>
|
||||
<data name="Gesture_AutoToggleHint" xml:space="preserve"><value># 开启后退出画板模式时自动关闭双指移动手势,进入白板模式时自动开启双指移动手势</value></data>
|
||||
<data name="Gesture_AllowRotateScale" xml:space="preserve"><value>允许双指旋转与缩放选中的墨迹</value></data>
|
||||
<data name="Gesture_AllowRotateScaleHint" xml:space="preserve"><value># 允许选中墨迹后对墨迹进行双指或多指缩放操作(此设置不受“允许双指旋转”设置的影响)</value></data>
|
||||
<data name="Gesture_EnablePalmEraser" xml:space="preserve"><value>启用手掌擦</value></data>
|
||||
<data name="Gesture_PalmSensitivity" xml:space="preserve"><value>手掌擦敏感度</value></data>
|
||||
<data name="Gesture_PalmSensitivityLow" xml:space="preserve"><value>低敏感度</value></data>
|
||||
<data name="Gesture_PalmSensitivityMedium" xml:space="preserve"><value>中敏感度</value></data>
|
||||
<data name="Gesture_PalmSensitivityHigh" xml:space="preserve"><value>高敏感度</value></data>
|
||||
<data name="Gesture_PalmHint" xml:space="preserve"><value># 低敏感度:需要更大的触摸面积和更多触摸点,减少误判;高敏感度:更容易触发手掌擦,但可能误判手指。</value></data>
|
||||
<data name="InkRecog_Title" xml:space="preserve"><value>墨迹纠正</value></data>
|
||||
<data name="InkRecog_EnableInkRecognition" xml:space="preserve"><value>启用墨迹识别</value></data>
|
||||
<data name="InkRecog_BlockRectFakePressure" xml:space="preserve"><value>阻止矫正后的矩形带有模拟压感值</value></data>
|
||||
<data name="InkRecog_BlockTriFakePressure" xml:space="preserve"><value>阻止矫正后的三角形带有模拟压感值</value></data>
|
||||
<data name="InkRecog_FixTriangle" xml:space="preserve"><value>矫正手绘三角形</value></data>
|
||||
<data name="InkRecog_FixRectangle" xml:space="preserve"><value>矫正手绘矩形</value></data>
|
||||
<data name="InkRecog_FixEllipse" xml:space="preserve"><value>矫正手绘圆形与椭圆</value></data>
|
||||
<data name="InkRecog_AutoStraightLine" xml:space="preserve"><value>直线自动拉直</value></data>
|
||||
<data name="InkRecog_LengthThreshold" xml:space="preserve"><value>长度阈值</value></data>
|
||||
<data name="InkRecog_Sensitivity" xml:space="preserve"><value>灵敏度</value></data>
|
||||
<data name="InkRecog_HighPrecisionStraighten" xml:space="preserve"><value>高精度直线拉直</value></data>
|
||||
<data name="InkRecog_HighPrecisionHint" xml:space="preserve"><value># 开启后,当绘制的直线超过设定长度阈值时,将自动调整为完美直线。灵敏度范围0.05-2.0,越小要求越严格,越弯曲的线条越不容易被拉直;值越大越容易识别为直线。高精度模式下,每隔10像素取一个计数点,获取更准确的平均值用于判断。</value></data>
|
||||
<data name="InkRecog_LineEndpointSnapping" xml:space="preserve"><value>直线端点吸附</value></data>
|
||||
<data name="InkRecog_SnappingDistance" xml:space="preserve"><value>吸附距离</value></data>
|
||||
<data name="Theme_GroupTitle" xml:space="preserve"><value>个性化</value></data>
|
||||
<data name="Theme_Label" xml:space="preserve"><value>主题</value></data>
|
||||
<data name="Theme_Light" xml:space="preserve"><value>浅色主题</value></data>
|
||||
<data name="Theme_Dark" xml:space="preserve"><value>深色主题</value></data>
|
||||
<data name="Theme_System" xml:space="preserve"><value>跟随系统</value></data>
|
||||
<data name="Theme_EnableSplash" xml:space="preserve"><value>启用启动动画</value></data>
|
||||
<data name="Theme_SplashStyle" xml:space="preserve"><value>启动动画样式</value></data>
|
||||
<data name="Theme_Splash_Random" xml:space="preserve"><value>随机</value></data>
|
||||
<data name="Theme_Splash_Seasonal" xml:space="preserve"><value>跟随四季</value></data>
|
||||
<data name="Theme_Splash_Spring" xml:space="preserve"><value>春季</value></data>
|
||||
<data name="Theme_Splash_Summer" xml:space="preserve"><value>夏季</value></data>
|
||||
<data name="Theme_Splash_Autumn" xml:space="preserve"><value>秋季</value></data>
|
||||
<data name="Theme_Splash_Winter" xml:space="preserve"><value>冬季</value></data>
|
||||
<data name="Theme_Splash_Horse" xml:space="preserve"><value>马年限定</value></data>
|
||||
<data name="Theme_FloatingBarIcon" xml:space="preserve"><value>浮动工具栏图标</value></data>
|
||||
<data name="Theme_FloatingIcon_IccDefault" xml:space="preserve"><value>“ICC-CE”默认</value></data>
|
||||
<data name="Theme_FloatingIcon_IccNoShadow" xml:space="preserve"><value>“ICC-CE”无阴影</value></data>
|
||||
<data name="Theme_FloatingIcon_IccDark" xml:space="preserve"><value>“ICC-CE”深色</value></data>
|
||||
<data name="Theme_FloatingIcon_IccDarkBreath" xml:space="preserve"><value>“ICC-CE”深色呼吸版</value></data>
|
||||
<data name="Theme_FloatingIcon_IccWhiteTransparent" xml:space="preserve"><value>“ICC-CE”白色透明版</value></data>
|
||||
<data name="Theme_FloatingIcon_IccBlackTransparent" xml:space="preserve"><value>“ICC-CE”黑色透明版</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkCrossEye" xml:space="preserve"><value>酷安斗鸡眼滑稽</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkAbused" xml:space="preserve"><value>酷安受虐滑稽</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkSmile" xml:space="preserve"><value>酷安呲牙笑</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkUnderwear" xml:space="preserve"><value>酷安头戴内裤滑稽</value></data>
|
||||
<data name="Theme_FloatingIcon_CoolapkGreenHatDoge" xml:space="preserve"><value>酷安绿帽Doge</value></data>
|
||||
<data name="Theme_FloatingIcon_TiebaEmoji" xml:space="preserve"><value>贴吧滑稽</value></data>
|
||||
<data name="Theme_CustomFloatingIconLabel" xml:space="preserve"><value>自定义浮动栏图标</value></data>
|
||||
<data name="Theme_Upload" xml:space="preserve"><value>上传</value></data>
|
||||
<data name="Theme_Manage" xml:space="preserve"><value>管理</value></data>
|
||||
<data name="Theme_FloatingBarScale" xml:space="preserve"><value>浮动工具栏缩放</value></data>
|
||||
<data name="Theme_FloatingBarOpacity" xml:space="preserve"><value>浮动工具栏透明度</value></data>
|
||||
<data name="Theme_FloatingBarOpacityInPPT" xml:space="preserve"><value>浮栏在PPT下透明度</value></data>
|
||||
<data name="Theme_FloatingBarOpacityInPPTHint" xml:space="preserve"><value># 重新进入PPT放映后生效</value></data>
|
||||
<data name="Theme_ShowNibButton" xml:space="preserve"><value>在调色盘窗口中显示 笔尖模式 按钮</value></data>
|
||||
<data name="Theme_BlackboardScale80" xml:space="preserve"><value>白板 UI 80% 缩放</value></data>
|
||||
<data name="Theme_ShowTimeInWhiteboard" xml:space="preserve"><value>在白板中显示当前时间和日期</value></data>
|
||||
<data name="Theme_ShowQuoteInWhiteboard" xml:space="preserve"><value>在白板中显示信仰の源1(好喝的/毒的鸡汤)</value></data>
|
||||
<data name="Theme_QuoteSource" xml:space="preserve"><value>信仰の源出自Where?</value></data>
|
||||
<data name="Theme_QuoteSource_OsuQuotes" xml:space="preserve"><value>osu!玩家语录</value></data>
|
||||
<data name="Theme_QuoteSource_Mottos" xml:space="preserve"><value>励志立志的名言警句</value></data>
|
||||
<data name="Theme_QuoteSource_GaokaoBless" xml:space="preserve"><value>高考祝福语</value></data>
|
||||
<data name="Theme_QuoteSource_Hitokoto" xml:space="preserve"><value>一言(Hitokoto API)</value></data>
|
||||
<data name="Theme_Customize" xml:space="preserve"><value>自定义</value></data>
|
||||
<data name="Theme_EnableQuickPanel" xml:space="preserve"><value>在收纳模式下启用快速面板</value></data>
|
||||
<data name="Theme_UnfoldButtonIcon" xml:space="preserve"><value>取消收纳按钮图标</value></data>
|
||||
<data name="Theme_UnfoldIcon_Arrow" xml:space="preserve"><value>箭头</value></data>
|
||||
<data name="Theme_UnfoldIcon_Pen" xml:space="preserve"><value>笔</value></data>
|
||||
<data name="Theme_FloatingBarButtonsTitle" xml:space="preserve"><value>浮动栏按钮显示</value></data>
|
||||
<data name="Theme_UseLegacyFloatingBarUI" xml:space="preserve"><value>使用老版浮动栏按钮UI</value></data>
|
||||
<data name="Theme_ShowShapeButton" xml:space="preserve"><value>显示形状按钮</value></data>
|
||||
<data name="Theme_ShowUndoButton" xml:space="preserve"><value>显示撤销按钮</value></data>
|
||||
<data name="Theme_ShowRedoButton" xml:space="preserve"><value>显示重做按钮</value></data>
|
||||
<data name="Theme_ShowClearButton" xml:space="preserve"><value>显示清空按钮</value></data>
|
||||
<data name="Theme_ShowWhiteboardButton" xml:space="preserve"><value>显示白板按钮</value></data>
|
||||
<data name="Theme_ShowHideButton" xml:space="preserve"><value>显示隐藏按钮</value></data>
|
||||
<data name="Theme_ShowLassoButton" xml:space="preserve"><value>显示套索选择按钮</value></data>
|
||||
<data name="Theme_ShowClearAndMouseButton" xml:space="preserve"><value>显示清并鼠按钮</value></data>
|
||||
<data name="Theme_ShowQuickPalette" xml:space="preserve"><value>显示快捷调色盘</value></data>
|
||||
<data name="Theme_QuickPaletteMode" xml:space="preserve"><value>快捷调色盘显示模式</value></data>
|
||||
<data name="Theme_QuickPalette_SingleRow" xml:space="preserve"><value>单行显示(6色)</value></data>
|
||||
<data name="Theme_QuickPalette_DoubleRow" xml:space="preserve"><value>双行显示(8色)</value></data>
|
||||
<data name="Theme_EraserButtonDisplay" xml:space="preserve"><value>橡皮按钮显示</value></data>
|
||||
<data name="Theme_EraserDisplay_Both" xml:space="preserve"><value>两个都显示</value></data>
|
||||
<data name="Theme_EraserDisplay_AreaOnly" xml:space="preserve"><value>仅显示面积擦</value></data>
|
||||
<data name="Theme_EraserDisplay_LineOnly" xml:space="preserve"><value>仅显示线擦</value></data>
|
||||
<data name="Theme_EraserDisplay_None" xml:space="preserve"><value>都不显示</value></data>
|
||||
<data name="Tray_GroupTitle" xml:space="preserve"><value>任务栏托盘图标</value></data>
|
||||
<data name="Tray_EnableTrayIcon" xml:space="preserve"><value>启用托盘图标</value></data>
|
||||
<data name="PPT_GroupTitle" xml:space="preserve"><value>PPT联动</value></data>
|
||||
<data name="PPT_GroupHint" xml:space="preserve"><value>此部分的设置项将会在幻灯片播放时使用,优先级高于其他设置项</value></data>
|
||||
<data name="PPT_SupportPowerPoint" xml:space="preserve"><value>Microsoft PowerPoint 支持</value></data>
|
||||
<data name="PPT_Enhancement" xml:space="preserve"><value>PowerPoint 联动增强</value></data>
|
||||
<data name="PPT_SkipAnimations" xml:space="preserve"><value>抢占焦点放映(用于跳过PPT动画)</value></data>
|
||||
<data name="PPT_UseRot" xml:space="preserve"><value>使用 ROT 联动</value></data>
|
||||
<data name="PPT_SupportWPS" xml:space="preserve"><value>WPS 支持</value></data>
|
||||
<data name="PPT_KillWppProcess" xml:space="preserve"><value>WPP进程查杀(防止WPP残留进程)</value></data>
|
||||
<data name="PPT_KillWppHint" xml:space="preserve"><value># 关闭后将不会自动查杀WPP残留进程,可能导致WPP关闭卡顿或无法彻底退出。</value></data>
|
||||
<data name="PPT_WpsHint1" xml:space="preserve"><value># 如果您只使用PowerPoint请不要打开WPS联动开关,如果使用WPS建议不要使用PowerPoint!</value></data>
|
||||
<data name="PPT_WpsLagWarning" xml:space="preserve"><value>开启WPS支持后会导致WPS关闭时卡顿!</value></data>
|
||||
<data name="PPT_WpsSupportHint" xml:space="preserve"><value># 可支持 WPS,但目前无法同时支持 MSOffice 和 WPS。若要启用WPS支持,请确保 WPS 是否在 “配置工具” 中开启了 “WPS Office 兼容第三方系统和软件” 选项,否则将无法识别到WPS!</value></data>
|
||||
<data name="Canvas_HideStrokeWhenSelecting" xml:space="preserve"><value>退出画板模式后隐藏墨迹</value></data>
|
||||
<data name="Canvas_HideStrokeWhenSelectingHint" xml:space="preserve"><value># 开启 退出画板模式后隐藏墨迹 选项后,进入 PPT 模式时未处于批注模式时不会显示墨迹。</value></data>
|
||||
<data name="Canvas_ClearInkAlsoClearHistory" xml:space="preserve"><value>清空墨迹时删除墨迹历史记录</value></data>
|
||||
<data name="Canvas_ClearCanvasAlsoClearImages" xml:space="preserve"><value>清空画布时同时清空图片</value></data>
|
||||
<data name="Canvas_CompressPicturesUploaded" xml:space="preserve"><value>插入图片时自动压缩(大于1920x1080)</value></data>
|
||||
<data name="PPT_FlipButtonsTitle" xml:space="preserve"><value>PPT翻页按钮</value></data>
|
||||
<data name="PPT_ShowFlipButtons" xml:space="preserve"><value>在 PPT 模式下显示翻页按钮</value></data>
|
||||
<data name="PPT_Position_LeftBottom" xml:space="preserve"><value>左下</value></data>
|
||||
<data name="PPT_Position_RightBottom" xml:space="preserve"><value>右下</value></data>
|
||||
<data name="PPT_Position_Left" xml:space="preserve"><value>左侧</value></data>
|
||||
<data name="PPT_Position_Right" xml:space="preserve"><value>右侧</value></data>
|
||||
<data name="PPT_LeftOffset" xml:space="preserve"><value>左侧偏移</value></data>
|
||||
<data name="PPT_LeftOpacity" xml:space="preserve"><value>左侧透明度</value></data>
|
||||
<data name="PPT_RightOffset" xml:space="preserve"><value>右侧偏移</value></data>
|
||||
<data name="PPT_RightOpacity" xml:space="preserve"><value>右侧透明度</value></data>
|
||||
<data name="PPT_OffsetHint" xml:space="preserve"><value># 调大往上偏移,调小往下偏移,修改为0为不偏移,居中放置</value></data>
|
||||
<data name="PPT_LeftBottomOffset" xml:space="preserve"><value>左下偏移</value></data>
|
||||
<data name="PPT_LeftBottomOpacity" xml:space="preserve"><value>左下透明度</value></data>
|
||||
<data name="PPT_RightBottomOffset" xml:space="preserve"><value>右下偏移</value></data>
|
||||
<data name="PPT_RightBottomOpacity" xml:space="preserve"><value>右下透明度</value></data>
|
||||
<data name="PPT_OffsetHintHorizontal" xml:space="preserve"><value># 调大往右偏移,调小往左偏移,修改为0为不偏移,居中放置</value></data>
|
||||
<data name="PPT_SideGroupTitle" xml:space="preserve"><value>两侧</value></data>
|
||||
<data name="PPT_ShowPageNumber" xml:space="preserve"><value>显示页码</value></data>
|
||||
<data name="PPT_HalfOpacity" xml:space="preserve"><value>半透明</value></data>
|
||||
<data name="PPT_BlackBackground" xml:space="preserve"><value>黑色背景</value></data>
|
||||
<data name="PPT_BottomGroupTitle" xml:space="preserve"><value>左下右下</value></data>
|
||||
<data name="PPT_PageButtonClickable" xml:space="preserve"><value>PPT 页码按钮可点击</value></data>
|
||||
<data name="PPT_PageButtonClickableHint" xml:space="preserve"><value># 开启该选项后,点击页码按钮可以唤起PowerPoint自带的网格缩略图视图。WPS不支持该功能,开启也没用。</value></data>
|
||||
<data name="PPT_LongPressPageTurn" xml:space="preserve"><value>PPT 翻页按钮长按翻页</value></data>
|
||||
<data name="PPT_LongPressPageTurnHint" xml:space="preserve"><value># 开启该选项后,长按PPT翻页按钮可以连续翻页,提高翻页效率。</value></data>
|
||||
<data name="Startup_UIAccessTopMostHint" xml:space="preserve"><value># 开启UIA置顶后,软件需要管理员启动才能置顶,关闭此功能需要完全关闭软件后再手动启动,无法使用重启来关闭此功能</value></data>
|
||||
<data name="Header_SilentUpdate" xml:space="preserve"><value>静默更新</value></data>
|
||||
<data name="Startup_SilentUpdateHint" xml:space="preserve"><value># 静默更新将在软件不使用时自动安装,无需手动操作</value></data>
|
||||
<data name="Startup_UpdateChannel" xml:space="preserve"><value>更新通道</value></data>
|
||||
<data name="Update_Release" xml:space="preserve"><value>稳定版 (Release)</value></data>
|
||||
<data name="Update_Preview" xml:space="preserve"><value>预览版 (Preview)</value></data>
|
||||
<data name="Update_Beta" xml:space="preserve"><value>测试版 (Beta)</value></data>
|
||||
<data name="Startup_UpdateChannelHint" xml:space="preserve"><value># 稳定版提供可靠更新,预览版提供新功能体验同时拥有相较Beta版更强的稳定性,测试版提供新功能抢先体验</value></data>
|
||||
<data name="Btn_ManualUpdate" xml:space="preserve"><value>手动更新</value></data>
|
||||
<data name="Startup_ManualUpdateHint" xml:space="preserve"><value># 点击后立即检查并下载最新版本</value></data>
|
||||
<data name="Btn_FixVersion" xml:space="preserve"><value>版本修复</value></data>
|
||||
<data name="Startup_FixVersionHint" xml:space="preserve"><value># 版本修复会根据当前选择的通道下载最新版本并执行安装,可用于修复损坏的安装</value></data>
|
||||
<data name="Btn_HistoryRollback" xml:space="preserve"><value>历史版本回滚</value></data>
|
||||
<data name="Startup_HistoryRollbackHint" xml:space="preserve"><value># 历史版本回滚,点击后会弹出相应页面供用户手动回滚到之前的版本</value></data>
|
||||
<data name="Startup_SilentUpdateFullHint" xml:space="preserve"><value># 关闭静默更新后,已完成安装包的下载后将会弹窗询问是否进行更新,开启静默更新后将会在安装包下载完成后每隔十分钟进行如下检测:①处于静默更新时间段内 ②未处于书写模式 ③未处于画板内。若以上检测通过即会关闭软件进行自动更新。</value></data>
|
||||
<data name="Startup_SilentUpdateTimePeriod" xml:space="preserve"><value>静默更新时间段</value></data>
|
||||
<data name="Startup_StartTime" xml:space="preserve"><value>起始时间</value></data>
|
||||
<data name="Startup_EndTime" xml:space="preserve"><value>终止时间</value></data>
|
||||
<data name="Startup_TimePeriodHint" xml:space="preserve"><value># 若终止时间小于起始时间,即将终止时间视为第二天的时间。# 若起始时间与终止时间相同,即视为全天候时间。</value></data>
|
||||
<data name="Startup_RunAtStartup" xml:space="preserve"><value>开机时运行</value></data>
|
||||
<data name="Startup_FoldAtStartup" xml:space="preserve"><value>开机运行后收纳到侧边栏</value></data>
|
||||
<data name="Canvas_GroupTitle" xml:space="preserve"><value>画板和墨迹</value></data>
|
||||
<data name="Canvas_ShowCursor" xml:space="preserve"><value>显示画笔光标</value></data>
|
||||
<data name="Canvas_EnablePressureTouch" xml:space="preserve"><value>启用压感触屏模式</value></data>
|
||||
<data name="Canvas_EnablePressureTouchHint" xml:space="preserve"><value># 开启后,触屏设备也将支持压感效果,适用于部分支持压感但无法被系统识别的触屏设备。</value></data>
|
||||
<data name="Canvas_DisablePressure" xml:space="preserve"><value>屏蔽压感</value></data>
|
||||
<data name="Canvas_DisablePressureHint" xml:space="preserve"><value># 开启后,将忽略所有设备的压感信息,使所有笔画具有统一的粗细。与压感触屏模式互斥。</value></data>
|
||||
<data name="Canvas_EraserSize" xml:space="preserve"><value>橡皮大小</value></data>
|
||||
<data name="Canvas_EraserSize_VerySmall" xml:space="preserve"><value>很小</value></data>
|
||||
<data name="Canvas_EraserSize_Small" xml:space="preserve"><value>较小</value></data>
|
||||
<data name="Canvas_EraserSize_Medium" xml:space="preserve"><value>中等</value></data>
|
||||
<data name="Canvas_EraserSize_Large" xml:space="preserve"><value>较大</value></data>
|
||||
<data name="Canvas_EraserSize_VeryLarge" xml:space="preserve"><value>很大</value></data>
|
||||
<data name="Canvas_EraserSizeHint" xml:space="preserve"><value># 非实时切换,下一次使用面积擦时生效。</value></data>
|
||||
<data name="Canvas_KeepHyperbolaAsymptote" xml:space="preserve"><value>保留双曲线渐近线</value></data>
|
||||
<data name="Canvas_Yes" xml:space="preserve"><value>是</value></data>
|
||||
<data name="Canvas_No" xml:space="preserve"><value>否</value></data>
|
||||
<data name="Canvas_AskEachTime" xml:space="preserve"><value>每次询问</value></data>
|
||||
<data name="Canvas_HyperbolaAsymptoteHint" xml:space="preserve"><value># 请注意,若不保留双曲线渐近线可能会有遇到撤回相关的 BUG 影响用。</value></data>
|
||||
<data name="Canvas_ShowCircleCenter" xml:space="preserve"><value>绘制圆时显示圆心位置</value></data>
|
||||
<data name="Canvas_WPFBezierSmoothing" xml:space="preserve"><value>使用WPF默认贝塞尔曲线平滑</value></data>
|
||||
<data name="Canvas_AdvancedBezierSmoothing" xml:space="preserve"><value>使用高级曲线平滑(推荐)</value></data>
|
||||
<data name="Canvas_EnableInkFade" xml:space="preserve"><value>启用墨迹渐隐功能</value></data>
|
||||
<data name="Canvas_EnableInkFadeHint" xml:space="preserve"><value># 开启后墨迹不会绘制到画布上,而是保持湿墨迹状态,根据设置的渐隐时间自动消失</value></data>
|
||||
<data name="Canvas_InkFadeTime" xml:space="preserve"><value>墨迹渐隐时间</value></data>
|
||||
<data name="Canvas_HideInkFadeInPenMenu" xml:space="preserve"><value>在笔工具菜单中隐藏墨迹渐隐控制</value></data>
|
||||
<data name="Canvas_HideInkFadeInPenMenuHint" xml:space="preserve"><value># 开启后,主工具栏上点击笔工具后弹出的上下文菜单中将不显示墨迹渐隐控制开关</value></data>
|
||||
<data name="Canvas_BrushAutoRestore" xml:space="preserve"><value>启用画笔自动恢复</value></data>
|
||||
<data name="Canvas_BrushAutoRestoreHint" xml:space="preserve"><value># 启用后,临时修改画笔设置后将在指定时间点自动恢复到你在此处配置的颜色 / 透明度 / 粗细</value></data>
|
||||
<data name="Canvas_AutoRestoreTimePoints" xml:space="preserve"><value>自动恢复时间点 (HH:mm,可多个,用 ; 分隔)</value></data>
|
||||
<data name="Canvas_RestoreColor" xml:space="preserve"><value>恢复目标颜色</value></data>
|
||||
<data name="Canvas_Color_Default" xml:space="preserve"><value>默认</value></data>
|
||||
<data name="Canvas_Color_Black" xml:space="preserve"><value>黑色</value></data>
|
||||
<data name="Canvas_Color_White" xml:space="preserve"><value>白色</value></data>
|
||||
<data name="Canvas_Color_Red" xml:space="preserve"><value>红色</value></data>
|
||||
<data name="Canvas_Color_Yellow" xml:space="preserve"><value>黄色</value></data>
|
||||
<data name="Canvas_Color_Blue" xml:space="preserve"><value>蓝色</value></data>
|
||||
<data name="Canvas_Color_Green" xml:space="preserve"><value>绿色</value></data>
|
||||
<data name="Canvas_Color_Orange" xml:space="preserve"><value>橙色</value></data>
|
||||
<data name="Canvas_Color_Purple" xml:space="preserve"><value>紫色</value></data>
|
||||
<data name="Canvas_RestoreWidth" xml:space="preserve"><value>恢复笔粗细</value></data>
|
||||
<data name="Canvas_RestoreOpacity" xml:space="preserve"><value>恢复透明度</value></data>
|
||||
<data name="Canvas_SwitchBackAfterEraser" xml:space="preserve"><value>使用橡皮擦后自动切换回批注模式</value></data>
|
||||
<data name="Canvas_SwitchBackAfterEraserHint" xml:space="preserve"><value># 开启后,使用橡皮擦进行擦除操作后静置一段时间将自动切换回批注模式</value></data>
|
||||
<data name="Canvas_SwitchBackDelay" xml:space="preserve"><value>自动切换延迟时间</value></data>
|
||||
<data name="Canvas_SwitchBackDelayHint" xml:space="preserve"><value># 若在计时时间内再次进行擦除操作,计时器将重新开始计时</value></data>
|
||||
<data name="InkRecog_LineEndpointSnappingHint" xml:space="preserve"><value># 开启后,当绘制的直线端点靠近其他直线端点时,将自动吸附连接。</value></data>
|
||||
<data name="PPT_EnterAnnotationOnShow" xml:space="preserve"><value>进入 PPT 放映时自动进入批注模式</value></data>
|
||||
<data name="PPT_ConflictWithAutoFold" xml:space="preserve"><value>该项与"自动化"中的"播放PPT时自动收纳"选项冲突!</value></data>
|
||||
<data name="PPT_TwoFingerGesture" xml:space="preserve"><value>允许幻灯片模式下的双指手势</value></data>
|
||||
<data name="PPT_FingerGestureSlide" xml:space="preserve"><value>允许使用手指手势进行幻灯片翻页</value></data>
|
||||
<data name="PPT_FingerGestureSlideHint" xml:space="preserve"><value># 允许开启画板时使用手指手势进行幻灯片翻页(启用后,在幻灯片放映模式下,当画板无墨迹时,使用手指(笔尖或手掌无法识别)左右滑动即可控制幻灯片翻页。)</value></data>
|
||||
<data name="PPT_ShowGestureButtonInShow" xml:space="preserve"><value>PPT 放映模式显示手势按钮</value></data>
|
||||
<data name="PPT_ShowGestureButtonInShowHint" xml:space="preserve"><value># 开启后在 PPT 放映模式下也显示手势按钮</value></data>
|
||||
<data name="PPT_TimeCapsule" xml:space="preserve"><value>PPT时间显示胶囊</value></data>
|
||||
<data name="PPT_TimeCapsuleHint" xml:space="preserve"><value># 开启后在 PPT 放映模式下显示时间胶囊,可替代最小化计时器窗口</value></data>
|
||||
<data name="PPT_TimeCapsulePosition" xml:space="preserve"><value>时间胶囊位置:</value></data>
|
||||
<data name="PPT_TimeCapsulePos_TL" xml:space="preserve"><value>左上角</value></data>
|
||||
<data name="PPT_TimeCapsulePos_TR" xml:space="preserve"><value>右上角</value></data>
|
||||
<data name="PPT_TimeCapsulePos_Center" xml:space="preserve"><value>顶部居中</value></data>
|
||||
<data name="PPT_ShowQuickPanelInShow" xml:space="preserve"><value>PPT 放映时显示快速面板</value></data>
|
||||
<data name="PPT_ShowQuickPanelInShowHint" xml:space="preserve"><value># 关闭后在 PPT 放映时不显示快速面板</value></data>
|
||||
<data name="PPT_AutoScreenshot" xml:space="preserve"><value>自动幻灯片截屏</value></data>
|
||||
<data name="PPT_AutoScreenshotHint" xml:space="preserve"><value># 开启 自动幻灯片截屏 后将会在幻灯片有墨迹时翻页自动截屏</value></data>
|
||||
<data name="PPT_AutoSaveStrokes" xml:space="preserve"><value>自动保存幻灯片墨迹</value></data>
|
||||
<data name="PPT_AutoSaveStrokesHint" xml:space="preserve"><value># 开启 自动保存幻灯片墨迹 后将在结束幻灯片放映时自动将保存已有墨迹,并在下次打开时自动加载(文件名和幻灯片页数都要相同)</value></data>
|
||||
<data name="PPT_RememberLastPage" xml:space="preserve"><value>记忆并提示上次播放位置</value></data>
|
||||
<data name="PPT_RememberLastPageHint" xml:space="preserve"><value>#开启后会记录上次播放的页数,点击"是"后会自动跳转</value></data>
|
||||
<data name="PPT_GoToFirstPageOnReenter" xml:space="preserve"><value>进入放映时回到首页</value></data>
|
||||
<data name="PPT_NotifyHiddenPage" xml:space="preserve"><value>提示隐藏幻灯片</value></data>
|
||||
<data name="PPT_NotifyAutoPlay" xml:space="preserve"><value>提示是否已启用自动播放</value></data>
|
||||
<data name="Advanced_Title" xml:space="preserve"><value>高级设置</value></data>
|
||||
<data name="Advanced_TouchMultiplierHint" xml:space="preserve"><value>可在手指触摸画板时显示圆形橡皮或手掌触摸画板时显示的橡皮比
手掌大很多时调整</value></data>
|
||||
<data name="Advanced_SpecialScreenMode" xml:space="preserve"><value>特殊屏幕模式</value></data>
|
||||
<data name="Advanced_TouchMultiplier" xml:space="preserve"><value>触摸倍数</value></data>
|
||||
<data name="Advanced_TouchMultiplierCalibrateHint" xml:space="preserve"><value>在下方区域内用笔尖点击以估计触摸大小倍数</value></data>
|
||||
<data name="Advanced_TouchMultiplierValueHint" xml:space="preserve"><value># 数值仅供参考</value></data>
|
||||
<data name="Advanced_EraserBindTouchMultiplier" xml:space="preserve"><value>橡皮擦绑定触摸大小倍数</value></data>
|
||||
<data name="Advanced_EraserBindTouchHint" xml:space="preserve"><value># BoundsWidth 参数作为接触面积区分界限</value></data>
|
||||
<data name="Advanced_QuadIRMode" xml:space="preserve"><value>四边红外模式</value></data>
|
||||
<data name="Advanced_Logging" xml:space="preserve"><value>记录日志</value></data>
|
||||
<data name="Advanced_LogByDate" xml:space="preserve"><value>日志以日期保存</value></data>
|
||||
<data name="Advanced_LogRotateHint" xml:space="preserve"><value># 日志文件超过 512 KB 时会自动删除。开启日期保存后,日志将保存在Logs文件夹中,当文件夹大小超过5MB时自动清空。</value></data>
|
||||
<data name="Advanced_ConfirmExit" xml:space="preserve"><value>关闭软件时二次弹窗确认</value></data>
|
||||
<data name="Advanced_FullScreenHelper" xml:space="preserve"><value>启用FullScreenHelper</value></data>
|
||||
<data name="Advanced_Experimental" xml:space="preserve"><value>实验性选项</value></data>
|
||||
<data name="Advanced_FullScreenHelperHint" xml:space="preserve"><value># 感谢lindexi大佬提供的FullScreenHelper,可以减少任务栏弹出的问题,且支持多显示器自动全屏(虽然对icc来说没什么用就是了),如果遇到一些玄学问题,可以关闭该功能,重启icc后生效。</value></data>
|
||||
<data name="Advanced_AvoidFullScreenHelper" xml:space="preserve"><value>启用AvoidFullScreenHelper</value></data>
|
||||
<data name="Advanced_AvoidFullScreenHelperHint" xml:space="preserve"><value># 避免画布全屏,应该可解决任务栏非置顶和Win11任务栏无法点击的问题,会导致左侧或顶部有AppBar(Dock栏软件)时导致浮动工具栏偏移,重启icc后生效。</value></data>
|
||||
<data name="Advanced_EdgeGestureUtil" xml:space="preserve"><value>启用EdgeGestureUtil</value></data>
|
||||
<data name="Tools_MoreFeaturesTitle" xml:space="preserve"><value>更多功能</value></data>
|
||||
<data name="Tools_Timer" xml:space="preserve"><value>计时器</value></data>
|
||||
<data name="Tools_RandomDraw" xml:space="preserve"><value>随机抽</value></data>
|
||||
<data name="Tools_SingleDraw" xml:space="preserve"><value>单次抽选</value></data>
|
||||
<data name="Tools_Save" xml:space="preserve"><value>保存</value></data>
|
||||
<data name="Tools_Open" xml:space="preserve"><value>打开...</value></data>
|
||||
<data name="Tools_Replay" xml:space="preserve"><value>重播</value></data>
|
||||
<data name="Tools_Screenshot" xml:space="preserve"><value>截屏</value></data>
|
||||
<data name="Tools_Manual" xml:space="preserve"><value>说明书</value></data>
|
||||
<data name="Tools_Settings" xml:space="preserve"><value>设置</value></data>
|
||||
<data name="QuickPanel_SingleDraw" xml:space="preserve"><value>单次抽</value></data>
|
||||
<data name="QuickPanel_RandomDraw" xml:space="preserve"><value>随机抽</value></data>
|
||||
<data name="QuickPanel_Timer" xml:space="preserve"><value>计时器</value></data>
|
||||
<data name="QuickPanel_Whiteboard" xml:space="preserve"><value>白板</value></data>
|
||||
<data name="QuickPanel_ExitShow" xml:space="preserve"><value>退出放映</value></data>
|
||||
<data name="QuickPanel_Show" xml:space="preserve"><value>显示</value></data>
|
||||
<data name="QuickPanel_Exit" xml:space="preserve"><value>退出</value></data>
|
||||
<data name="Backup_Title" xml:space="preserve"><value>设置备份与还原</value></data>
|
||||
<data name="Backup_Desc" xml:space="preserve"><value># 可手动备份当前设置或还原之前的备份,自动更新前也会自动备份</value></data>
|
||||
<data name="Backup_AutoBeforeUpdate" xml:space="preserve"><value>自动更新前备份</value></data>
|
||||
<data name="Backup_AutoPeriodic" xml:space="preserve"><value>定期自动备份</value></data>
|
||||
<data name="Backup_Interval" xml:space="preserve"><value>备份间隔</value></data>
|
||||
<data name="Backup_Interval_1Day" xml:space="preserve"><value>1天</value></data>
|
||||
<data name="Backup_Interval_3Days" xml:space="preserve"><value>3天</value></data>
|
||||
<data name="Backup_Interval_7Days" xml:space="preserve"><value>7天</value></data>
|
||||
<data name="Backup_Interval_14Days" xml:space="preserve"><value>14天</value></data>
|
||||
<data name="Backup_Interval_30Days" xml:space="preserve"><value>30天</value></data>
|
||||
<data name="Backup_Interval_DefaultHint" xml:space="preserve"><value>(默认7天)</value></data>
|
||||
<data name="Backup_Manual" xml:space="preserve"><value>手动备份</value></data>
|
||||
<data name="Backup_Restore" xml:space="preserve"><value>还原备份</value></data>
|
||||
<data name="ConfigProfiles_Title" xml:space="preserve"><value>配置文件切换与热重载</value></data>
|
||||
<data name="ConfigProfiles_Desc" xml:space="preserve"><value># 选择配置文件即切换并热重载;另存为可将当前配置保存为新配置文件</value></data>
|
||||
<data name="ConfigProfiles_Label" xml:space="preserve"><value>配置文件:</value></data>
|
||||
<data name="ConfigProfiles_Delete" xml:space="preserve"><value>删除配置文件</value></data>
|
||||
<data name="ConfigProfiles_SaveAs" xml:space="preserve"><value>另存为配置文件</value></data>
|
||||
<data name="Automation_Title" xml:space="preserve"><value>自动化</value></data>
|
||||
<data name="Automation_AutoFoldTitle" xml:space="preserve"><value>自动收纳</value></data>
|
||||
<data name="AutoFold_App_SeewoBoard5" xml:space="preserve"><value>希沃白板5</value></data>
|
||||
<data name="AutoFold_App_SeewoCamera" xml:space="preserve"><value>希沃展台</value></data>
|
||||
<data name="AutoFold_App_SeewoBoard3" xml:space="preserve"><value>希沃白板3</value></data>
|
||||
<data name="AutoFold_App_SeewoLightBoard" xml:space="preserve"><value>希沃轻白板</value></data>
|
||||
<data name="AutoFold_App_SeewoLightBoard5C" xml:space="preserve"><value>希沃轻白板5C</value></data>
|
||||
<data name="AutoFold_App_SeewoPinco" xml:space="preserve"><value>希沃品课</value></data>
|
||||
<data name="AutoFold_App_HiteBoard" xml:space="preserve"><value>鸿合白板</value></data>
|
||||
<data name="AutoFold_App_HiteCamera" xml:space="preserve"><value>鸿合展台</value></data>
|
||||
<data name="AutoFold_App_HiteLightBoard" xml:space="preserve"><value>鸿合轻量白板</value></data>
|
||||
<data name="AutoFold_App_WenXiangBoard" xml:space="preserve"><value>文香白板</value></data>
|
||||
<data name="AutoFold_App_MSWhiteboard" xml:space="preserve"><value>微软白板</value></data>
|
||||
<data name="AutoFold_App_AdmoxBoard" xml:space="preserve"><value>安道白板</value></data>
|
||||
<data name="AutoFold_App_AdmoxBooth" xml:space="preserve"><value>安道展台</value></data>
|
||||
<data name="AutoFold_App_YiYunBoard" xml:space="preserve"><value>艺云白板</value></data>
|
||||
<data name="AutoFold_App_YiYunBooth" xml:space="preserve"><value>艺云展台</value></data>
|
||||
<data name="AutoFold_App_MaxHubBoard" xml:space="preserve"><value>MaxHub白板</value></data>
|
||||
<data name="AutoFold_IgnoreEasiNoteDesktopAnno" xml:space="preserve"><value>自动收纳忽略桌面EN5批注窗口</value></data>
|
||||
<data name="AutoFold_OldZyBoard" xml:space="preserve"><value>进入“中原旧白板”时自动收纳</value></data>
|
||||
<data name="Automation_AutoFoldInPPT" xml:space="preserve"><value>播放PPT时自动收纳</value></data>
|
||||
<data name="Automation_KeepFoldAfterExit" xml:space="preserve"><value>软件退出后保持收纳模式</value></data>
|
||||
<data name="Automation_KeepFoldAfterExitHint" xml:space="preserve"><value># 开启后,执行自动收纳的软件在软件退出后不退出收纳模式,保持收纳状态</value></data>
|
||||
<data name="AutoKill_Title" xml:space="preserve"><value>自动查杀</value></data>
|
||||
<data name="AutoKill_PptTools" xml:space="preserve"><value>自动查杀希沃“PPT 小工具”</value></data>
|
||||
<data name="AutoKill_PptToolsHint" xml:space="preserve"><value># 请注意,查杀 PPT 小工具会导致希沃课堂授课助手无法使用,直接进入希沃课堂授课助手安装目录删除 Office.dll 文件即可进入 PPT 放映时不会启动希沃的 PPT 工具栏。</value></data>
|
||||
<data name="AutoKill_EasiNote5" xml:space="preserve"><value>自动查杀 希沃白板5</value></data>
|
||||
<data name="AutoKill_HiteAnnotation" xml:space="preserve"><value>自动查杀 鸿合屏幕书写</value></data>
|
||||
<data name="AutoKill_HiteAfterKillEnterAnnotation" xml:space="preserve"><value>鸿合屏幕书写查杀后自动进入批注</value></data>
|
||||
<data name="AutoKill_YouJiao" xml:space="preserve"><value>自动查杀 优教授课端</value></data>
|
||||
<data name="AutoKill_SeewoDesktop2Anno" xml:space="preserve"><value>自动查杀 希沃桌面2.0 桌面批注</value></data>
|
||||
<data name="AutoKill_SeewoDesktop2AnnoHint" xml:space="preserve"><value># 由于希沃桌面2.0提供的桌面批注是64位应用程序,icc是32位程序无法访问,所以目前暂不做精准匹配,只匹配进程名称DesktopAnnotation,后面会考虑封装一套基于P/Invoke和WMI的综合进程识别方案。如果遇到同进程名的软件直接不开启该选项就行了,见谅!</value></data>
|
||||
<data name="AutoKill_SameAppTitle" xml:space="preserve"><value>同类软件查杀</value></data>
|
||||
<data name="AutoKill_InkCanvasIC" xml:space="preserve"><value>自动查杀 Ink Canvas 和 IC+</value></data>
|
||||
<data name="AutoKill_ICA" xml:space="preserve"><value>自动查杀ICA(新版旧版通杀)</value></data>
|
||||
<data name="AutoKill_Inkeys" xml:space="preserve"><value>自动查杀 智绘教Inkeys(仅限新版)</value></data>
|
||||
<data name="FileAssoc_Title" xml:space="preserve"><value>文件关联管理</value></data>
|
||||
<data name="FileAssoc_Desc" xml:space="preserve"><value>管理.icstk文件的关联设置,双击.icstk文件可直接在Ink Canvas中打开</value></data>
|
||||
<data name="FileAssoc_Unregister" xml:space="preserve"><value>取消文件关联</value></data>
|
||||
<data name="FileAssoc_Check" xml:space="preserve"><value>检查关联状态</value></data>
|
||||
<data name="FileAssoc_Register" xml:space="preserve"><value>重新注册关联</value></data>
|
||||
<data name="FloatingInterceptor_Title" xml:space="preserve"><value>悬浮窗拦截</value></data>
|
||||
<data name="FloatingInterceptor_Desc" xml:space="preserve"><value>自动检测并拦截同类软件的悬浮窗</value></data>
|
||||
<data name="FloatingInterceptor_Enable" xml:space="preserve"><value>启用悬浮窗拦截</value></data>
|
||||
<data name="FloatingInterceptor_StatusNotRunning" xml:space="preserve"><value>拦截器未启动</value></data>
|
||||
<data name="Storage_AutoScreenshotOnClear" xml:space="preserve"><value>清屏时自动截图</value></data>
|
||||
<data name="Storage_ScreenshotsByDateFolder" xml:space="preserve"><value>截图分日期文件夹保存</value></data>
|
||||
<data name="Storage_AutoSaveInkOnScreenshot" xml:space="preserve"><value>截图时自动保存墨迹</value></data>
|
||||
<data name="Storage_AutoSaveInk" xml:space="preserve"><value>定时自动保存墨迹</value></data>
|
||||
<data name="Storage_AutoSaveInterval" xml:space="preserve"><value>保存间隔</value></data>
|
||||
<data name="Storage_AutoSaveInterval_1Min" xml:space="preserve"><value>1分钟</value></data>
|
||||
<data name="Storage_AutoSaveInterval_3Min" xml:space="preserve"><value>3分钟</value></data>
|
||||
<data name="Storage_AutoSaveInterval_5Min" xml:space="preserve"><value>5分钟</value></data>
|
||||
<data name="Storage_AutoSaveInterval_10Min" xml:space="preserve"><value>10分钟</value></data>
|
||||
<data name="Storage_AutoSaveInterval_15Min" xml:space="preserve"><value>15分钟</value></data>
|
||||
<data name="Storage_AutoSaveInterval_30Min" xml:space="preserve"><value>30分钟</value></data>
|
||||
<data name="Storage_AutoSaveInterval_60Min" xml:space="preserve"><value>60分钟</value></data>
|
||||
<data name="Storage_AutoSaveHint" xml:space="preserve"><value># 开启后将在设定时间间隔自动保存墨迹,仅在画布可见且有墨迹时才会保存</value></data>
|
||||
<data name="Storage_SaveFullPageStrokes" xml:space="preserve"><value>墨迹全页面保存</value></data>
|
||||
<data name="Storage_SaveFullPageHint" xml:space="preserve"><value># 开启后自动保存和手动保存墨迹时将以全屏模式保存。如果存在多个画布和墨迹,将把所有页面的墨迹按照每页为单位保存进一个压缩包中(注意,白板的墨迹只能在白板模式下打开,PPT的墨迹只能在PPT放映模式下打开)</value></data>
|
||||
<data name="Storage_SaveAsXml" xml:space="preserve"><value>保存为XML格式</value></data>
|
||||
<data name="Storage_SaveAsXmlHint" xml:space="preserve"><value># 开启后保存墨迹时将使用XML格式(ISF格式),便于查看和编辑墨迹数据</value></data>
|
||||
<data name="Storage_AutoScreenshotMinInk" xml:space="preserve"><value>自动截图最小墨迹量</value></data>
|
||||
<data name="Storage_PathTitle" xml:space="preserve"><value>墨迹与截图的保存路径</value></data>
|
||||
<data name="Storage_PathBrowse" xml:space="preserve"><value>浏览</value></data>
|
||||
<data name="Storage_PathSetToD" xml:space="preserve"><value>设置保存到 D:\Ink Canvas</value></data>
|
||||
<data name="Storage_PathSetToDocuments" xml:space="preserve"><value>设置保存到 文档</value></data>
|
||||
<data name="Storage_PathPermissionHint" xml:space="preserve"><value># 请注意检查保存文件夹是否有写入权限</value></data>
|
||||
<data name="Storage_AutoDeleteTitle" xml:space="preserve"><value>定期自动删除超过保存时间的墨迹、截图文件</value></data>
|
||||
<data name="Storage_AutoDeleteHint" xml:space="preserve"><value># 请注意如果开启自动删除功能,将会删除自动保存目录下所有后缀名为 .icstk 和 .png 的文件!</value></data>
|
||||
<data name="Storage_RetentionTitle" xml:space="preserve"><value>保存时长</value></data>
|
||||
<data name="Storage_RetentionUnitDays" xml:space="preserve"><value>天</value></data>
|
||||
<data name="CloudStorage_Manage" xml:space="preserve"><value>云存储管理</value></data>
|
||||
<data name="FoldMode_Title" xml:space="preserve"><value>收纳模式</value></data>
|
||||
<data name="FoldMode_ExitToAnnotation" xml:space="preserve"><value>退出收纳模式时自动切换至批注模式</value></data>
|
||||
<data name="FoldMode_ExitToAnnotationHint" xml:space="preserve"><value># 开启后,退出收纳模式时将自动切换至批注模式,便于快速批注</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterPPT" xml:space="preserve"><value>退出PPT放映后自动收纳浮动栏</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterPPTHint" xml:space="preserve"><value># 开启后,退出PPT放映后会自动收纳浮动栏</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterWhiteboard" xml:space="preserve"><value>退出白板时自动收纳</value></data>
|
||||
<data name="FoldMode_AutoFoldAfterWhiteboardHint" xml:space="preserve"><value># 开启后,退出白板模式时会自动收纳到侧边栏</value></data>
|
||||
<data name="Random_Title" xml:space="preserve"><value>随机点名</value></data>
|
||||
<data name="Random_ShowEditNamesButton" xml:space="preserve"><value>显示修改随机点名名单的按钮</value></data>
|
||||
<data name="Random_BackgroundSettingsTitle" xml:space="preserve"><value>点名窗口背景设置(仅老版点名UI有效)</value></data>
|
||||
<data name="Random_BackgroundSelectLabel" xml:space="preserve"><value>背景选择:</value></data>
|
||||
<data name="Random_Background_Default" xml:space="preserve"><value>默认背景</value></data>
|
||||
<data name="Random_CustomBackgroundLabel" xml:space="preserve"><value>自定义背景:</value></data>
|
||||
<data name="Random_CustomBackground_Upload" xml:space="preserve"><value>上传</value></data>
|
||||
<data name="Random_CustomBackground_Manage" xml:space="preserve"><value>管理</value></data>
|
||||
<data name="Random_EnableButtons" xml:space="preserve"><value>启用随机抽和单次抽按钮</value></data>
|
||||
<data name="Random_EnableQuickButton" xml:space="preserve"><value>启用快抽悬浮按钮</value></data>
|
||||
<data name="Random_UseExternal" xml:space="preserve"><value>直接调用外部点名</value></data>
|
||||
<data name="Random_ExternalTypeLabel" xml:space="preserve"><value>点名类型</value></data>
|
||||
<data name="Random_ExternalType_ClassIsland" xml:space="preserve"><value>ClassIsland点名</value></data>
|
||||
<data name="Random_ExternalType_SecRandom" xml:space="preserve"><value>SecRandom点名</value></data>
|
||||
<data name="Random_ExternalType_NamePicker" xml:space="preserve"><value>NamePicker点名</value></data>
|
||||
<data name="Random_OnceCloseDelay" xml:space="preserve"><value>单次抽人窗口关闭延迟</value></data>
|
||||
<data name="Random_OnceMaxStudents" xml:space="preserve"><value>单次随机点名人数上限</value></data>
|
||||
<data name="Random_NewUI_Title" xml:space="preserve"><value>新点名UI设置</value></data>
|
||||
<data name="Random_NewUI_Enable" xml:space="preserve"><value>启用新点名UI</value></data>
|
||||
<data name="Random_ML_AvoidRepeat" xml:space="preserve"><value>启用机器学习避免重复</value></data>
|
||||
<data name="Random_ML_HistoryCount" xml:space="preserve"><value>避免重复历史记录数量</value></data>
|
||||
<data name="Random_ML_Weight" xml:space="preserve"><value>避免重复权重</value></data>
|
||||
<data name="Random_ML_Hint" xml:space="preserve"><value># 机器学习算法会分析最近的点名历史,智能避免重复选择相同人员</value></data>
|
||||
<data name="Timer_Title" xml:space="preserve"><value>计时器设置</value></data>
|
||||
<data name="Timer_UseLegacyButtons" xml:space="preserve"><value>使用老版计时器按钮UI</value></data>
|
||||
<data name="Timer_NewUI" xml:space="preserve"><value>新计时器UI</value></data>
|
||||
<data name="Timer_EnableCountUp" xml:space="preserve"><value>启用正计时</value></data>
|
||||
<data name="Timer_OvertimeHighlight" xml:space="preserve"><value>超时醒目数字</value></data>
|
||||
<data name="Timer_Volume" xml:space="preserve"><value>计时器提醒音量</value></data>
|
||||
<data name="Timer_CustomSoundLabel" xml:space="preserve"><value>自定义提醒铃声:</value></data>
|
||||
<data name="Timer_SelectFile" xml:space="preserve"><value>选择文件</value></data>
|
||||
<data name="Timer_Reset" xml:space="preserve"><value>重置</value></data>
|
||||
<data name="Timer_Progressive" xml:space="preserve"><value>渐进提醒</value></data>
|
||||
<data name="Timer_ProgressiveVolume" xml:space="preserve"><value>渐进提醒音量</value></data>
|
||||
<data name="Timer_ProgressiveCustomLabel" xml:space="preserve"><value>自定义渐进提醒音频:</value></data>
|
||||
<data name="Timer_ProgressiveSelectFile" xml:space="preserve"><value>选择文件</value></data>
|
||||
<data name="Timer_ProgressiveReset" xml:space="preserve"><value>重置</value></data>
|
||||
<data name="About_Title" xml:space="preserve"><value>关于</value></data>
|
||||
<data name="About_DeviceInfo" xml:space="preserve"><value>设备信息</value></data>
|
||||
<data name="About_DeviceIdLabel" xml:space="preserve"><value>设备ID:</value></data>
|
||||
<data name="About_UsageFrequencyLabel" xml:space="preserve"><value>使用频率:</value></data>
|
||||
<data name="About_UpdatePriorityLabel" xml:space="preserve"><value>更新优先级:</value></data>
|
||||
<data name="About_LaunchCountLabel" xml:space="preserve"><value>启动次数:</value></data>
|
||||
<data name="About_TotalUsageLabel" xml:space="preserve"><value>总使用时长:</value></data>
|
||||
<data name="About_DeviceInfo_Loading" xml:space="preserve"><value>正在获取...</value></data>
|
||||
<data name="About_RefreshDeviceInfo" xml:space="preserve"><value>刷新设备信息</value></data>
|
||||
<data name="About_PrivacyCheckboxPrefix" xml:space="preserve"><value>我已阅读并同意 </value></data>
|
||||
<data name="About_PrivacyCheckboxSuffix" xml:space="preserve"><value> 中的隐私说明</value></data>
|
||||
<data name="About_TelemetryLabel" xml:space="preserve"><value>匿名使用数据上传:</value></data>
|
||||
<data name="About_Telemetry_Off" xml:space="preserve"><value>关闭(不上传)</value></data>
|
||||
<data name="About_Telemetry_Basic" xml:space="preserve"><value>上传基础数据</value></data>
|
||||
<data name="About_Telemetry_Optional" xml:space="preserve"><value>上传基础 + 可选数据</value></data>
|
||||
<data name="About_LicenseHint" xml:space="preserve"><value># 使用和分发本软件前,请您应当且务必知晓相关开源协议,且您应当知晓本软件基于 https://github.com/WXRIW/Ink-Canvas 修改而成。</value></data>
|
||||
<data name="About_LicenseTitle" xml:space="preserve"><value>本软件和ICA,Ink Canvas均基于许可证开源</value></data>
|
||||
<data name="About_LicenseBody" xml:space="preserve"><value>本强许可协议的许可条件是,在相同许可协议下,提供许可作品的完整源代码和修改,包括使用许可作品的大型作品。版权和许可声明必须保留。贡献者明确授予专利权。</value></data>
|
||||
<data name="About_DevelopersLabel" xml:space="preserve"><value>开发者:</value></data>
|
||||
<data name="About_Dev_ICCCE" xml:space="preserve"><value>ICC CE 的开发者</value></data>
|
||||
<data name="About_Dev_ICC" xml:space="preserve"><value>ICC 的开发者</value></data>
|
||||
<data name="About_Dev_ICA" xml:space="preserve"><value>ICA 的开发者</value></data>
|
||||
<data name="About_Dev_InkCanvas" xml:space="preserve"><value>Ink Canvas 的开发者</value></data>
|
||||
<data name="About_Source_ICC" xml:space="preserve"><value>ICC 仓库源:</value></data>
|
||||
<data name="About_Source_ICA" xml:space="preserve"><value>ICA 仓库源:</value></data>
|
||||
<data name="About_Source_InkCanvas" xml:space="preserve"><value>Ink Canvas 仓库源:</value></data>
|
||||
<data name="About_ThanksContributors" xml:space="preserve"><value>感谢下列贡献者:</value></data>
|
||||
<data name="About_Copyright" xml:space="preserve"><value>© 2025-2026 CJK_mkp 版权所有</value></data>
|
||||
<data name="About_OpenSourceSlogan" xml:space="preserve"><value>我们永远热爱开源!</value></data>
|
||||
<data name="About_VersionLabel" xml:space="preserve"><value>版本:</value></data>
|
||||
<data name="Common_Close" xml:space="preserve"><value>关闭</value></data>
|
||||
<data name="Common_On" xml:space="preserve"><value>开</value></data>
|
||||
<data name="Common_Off" xml:space="preserve"><value>关</value></data>
|
||||
<data name="Advanced_UriSchemeName" xml:space="preserve"><value>外部协议调用 (icc://)</value></data>
|
||||
<data name="Advanced_NibModeBoundsWidthHeader" xml:space="preserve"><value>笔尖模式 BoundsWidth</value></data>
|
||||
<data name="Advanced_FingerModeBoundsWidthHeader" xml:space="preserve"><value>手指模式 BoundsWidth</value></data>
|
||||
<data name="Advanced_EdgeGestureUtilHint_Part1" xml:space="preserve"><value># EdgeGestureUtil是icc最新引入的可以暂时阻止在使用触摸时触发边缘手势(如Windows10环境下,屏幕左边缘滑动进入任务视图,右边缘滑动弹出通知中心;Windows11环境下,底部向上滑动打开开始菜单),其原理是使用了</value></data>
|
||||
<data name="Advanced_EdgeGestureUtilHint_Part2" xml:space="preserve"><value>(当应用程序窗口处于活动状态且处于全屏模式 (或拥有的窗口) 处于活动状态时,防止边缘手势行为。)来实现的。如果有异常,请关闭该选项,该选项应该能够实时生效。(Win7和Win8用户该选项无法使用)</value></data>
|
||||
<data name="Advanced_ForceFullScreen" xml:space="preserve"><value>启用ForceFullScreen</value></data>
|
||||
<data name="Advanced_ForceFullScreenHint" xml:space="preserve"><value># 当检测到窗口大小变化时,自动使用Win32API将本窗口的大小设置为主显示器大小(设备像素大小),不需要可以关闭,实时生效。</value></data>
|
||||
<data name="Advanced_DPIChangeDetection" xml:space="preserve"><value>启用DPIChangeDetection</value></data>
|
||||
<data name="Advanced_DPIChangeDetectionHint" xml:space="preserve"><value># 当检测到系统DPI变化时,会尝试检测FloatingBar是否在屏幕内显示,如果不在屏幕内显示将会尝试移动到屏幕内可见区域(DPI调大会触发,如果DPI调小是不会触发工具栏位置移动的,请您手动调整)。</value></data>
|
||||
<data name="Advanced_ResolutionChangeDetection" xml:space="preserve"><value>启用ResolutionChangeDetection</value></data>
|
||||
<data name="Advanced_ResolutionChangeDetectionHint" xml:space="preserve"><value># 当检测到系统分辨率变化时,会尝试检测FloatingBar是否在屏幕内显示,如果不在屏幕内显示将会尝试移动到屏幕内可见区域(分辨率调小可能会触发,如果在屏幕内不会自动调整位置,请手动挡)。</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoBoard3" xml:space="preserve"><value>希沃白板3</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoBoard5" xml:space="preserve"><value>希沃白板5</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoBoard5C" xml:space="preserve"><value>希沃白板5C</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoPinco" xml:space="preserve"><value>希沃品课</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoPincoDrawing" xml:space="preserve"><value>希沃品课画笔</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoPPTTools" xml:space="preserve"><value>希沃PPT小工具</value></data>
|
||||
<data name="FloatingInterceptor_App_AiClass" xml:space="preserve"><value>AiClass</value></data>
|
||||
<data name="FloatingInterceptor_App_HiteAnnotation" xml:space="preserve"><value>鸿合屏幕书写</value></data>
|
||||
<data name="FloatingInterceptor_App_ChangYanClass" xml:space="preserve"><value>畅言智慧课堂</value></data>
|
||||
<data name="FloatingInterceptor_App_ChangYanPPT" xml:space="preserve"><value>畅言PPT</value></data>
|
||||
<data name="FloatingInterceptor_App_IntelligentClass" xml:space="preserve"><value>天喻教育云</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoDesktopAnnotation" xml:space="preserve"><value>希沃桌面画笔</value></data>
|
||||
<data name="FloatingInterceptor_App_SeewoDesktopSideBar" xml:space="preserve"><value>希沃桌面侧栏</value></data>
|
||||
<data name="Board_MultiTouchWriting" xml:space="preserve"><value>多指书写</value></data>
|
||||
<data name="Board_TwoFingerMove" xml:space="preserve"><value>双指移动</value></data>
|
||||
<data name="Board_TwoFingerZoom" xml:space="preserve"><value>双指缩放</value></data>
|
||||
<data name="Board_TwoFingerRotate" xml:space="preserve"><value>双指旋转</value></data>
|
||||
<data name="Board_Background" xml:space="preserve"><value>背景</value></data>
|
||||
<data name="Board_Select" xml:space="preserve"><value>选择</value></data>
|
||||
<data name="Board_Pen" xml:space="preserve"><value>笔</value></data>
|
||||
<data name="Board_Highlighter" xml:space="preserve"><value>荧光笔</value></data>
|
||||
<data name="Board_Eraser" xml:space="preserve"><value>橡皮</value></data>
|
||||
<data name="Board_EraserOptions" xml:space="preserve"><value>橡皮选项</value></data>
|
||||
<data name="Board_Size" xml:space="preserve"><value>大小</value></data>
|
||||
<data name="Board_EraserShape" xml:space="preserve"><value>橡皮形状</value></data>
|
||||
<data name="Board_EraserShape_Circle" xml:space="preserve"><value>圆形擦</value></data>
|
||||
<data name="Board_EraserShape_Blackboard" xml:space="preserve"><value>黑板擦</value></data>
|
||||
<data name="Board_ClearInk" xml:space="preserve"><value>清空墨迹</value></data>
|
||||
<data name="Board_ClearInkAndHistory" xml:space="preserve"><value>清空墨迹和历史</value></data>
|
||||
<data name="Board_StrokeEraser" xml:space="preserve"><value>墨迹擦</value></data>
|
||||
<data name="Board_Shape" xml:space="preserve"><value>图形</value></data>
|
||||
<data name="Board_ShapeHintLongPress" xml:space="preserve"><value>(第一行支持长按保持选中)</value></data>
|
||||
<data name="Board_AutoHide" xml:space="preserve"><value>自动隐藏</value></data>
|
||||
<data name="Board_InsertImage" xml:space="preserve"><value>插入图片</value></data>
|
||||
<data name="Board_SelectImage" xml:space="preserve"><value>选择图片</value></data>
|
||||
<data name="Board_Screenshot" xml:space="preserve"><value>截图</value></data>
|
||||
<data name="Board_Undo" xml:space="preserve"><value>撤销</value></data>
|
||||
<data name="Board_Redo" xml:space="preserve"><value>重做</value></data>
|
||||
<data name="Board_Tools" xml:space="preserve"><value>工具</value></data>
|
||||
<data name="Board_Exit" xml:space="preserve"><value>退出</value></data>
|
||||
<data name="Board_NewPage" xml:space="preserve"><value>新页面</value></data>
|
||||
<data name="Board_PreviousPage" xml:space="preserve"><value>上一页</value></data>
|
||||
<data name="Board_NextPage" xml:space="preserve"><value>下一页</value></data>
|
||||
<data name="Board_Page" xml:space="preserve"><value>页面</value></data>
|
||||
<data name="Board_DeleteThisPage" xml:space="preserve"><value>删除此页</value></data>
|
||||
<data name="Notification_TestText" xml:space="preserve"><value>测试文本</value></data>
|
||||
<data name="OldUI_Exit" xml:space="preserve"><value>退出</value></data>
|
||||
<data name="OldUI_Thickness" xml:space="preserve"><value>粗细</value></data>
|
||||
<data name="OldUI_Dark" xml:space="preserve"><value>深色</value></data>
|
||||
<data name="OldUI_Background" xml:space="preserve"><value>背景</value></data>
|
||||
<data name="OldUI_HideCanvas" xml:space="preserve"><value>隐藏
画板</value></data>
|
||||
<data name="OldUI_Check" xml:space="preserve"><value>检查</value></data>
|
||||
<data name="OldUI_SlideshowFromStart" xml:space="preserve"><value>从头
放映</value></data>
|
||||
<data name="OldUI_SlideshowEnd" xml:space="preserve"><value>结束
放映</value></data>
|
||||
<data name="OldUI_SingleFingerDrag" xml:space="preserve"><value>单指
拖动</value></data>
|
||||
<data name="OldUI_Restore" xml:space="preserve"><value>恢复</value></data>
|
||||
<data name="OldUI_ClearAndHide" xml:space="preserve"><value>清屏
&
隐藏</value></data>
|
||||
<data name="FloatingBar_Mouse" xml:space="preserve"><value>鼠标</value></data>
|
||||
<data name="FloatingBar_Annotate" xml:space="preserve"><value>批注</value></data>
|
||||
<data name="FloatingBar_Clear" xml:space="preserve"><value>清空</value></data>
|
||||
<data name="Booth_Title" xml:space="preserve"><value>视频展台</value></data>
|
||||
<data name="Booth_CapturedPhotos" xml:space="preserve"><value>拍摄照片</value></data>
|
||||
<data name="Booth_CameraDevices" xml:space="preserve"><value>摄像头设备</value></data>
|
||||
<data name="Booth_Present" xml:space="preserve"><value>上屏</value></data>
|
||||
<data name="Booth_Correction" xml:space="preserve"><value>矫正</value></data>
|
||||
<data name="Booth_Capture" xml:space="preserve"><value>拍照</value></data>
|
||||
<data name="Booth_Rotate" xml:space="preserve"><value>旋转</value></data>
|
||||
<data name="Theme_LanguageLabel" xml:space="preserve"><value>界面语言</value></data>
|
||||
<data name="Theme_Language_System" xml:space="preserve"><value>跟随系统</value></data>
|
||||
<data name="Theme_Language_ChineseSimplified" xml:space="preserve"><value>简体中文</value></data>
|
||||
<data name="Theme_Language_English" xml:space="preserve"><value>English</value></data>
|
||||
<data name="Theme_Language_RestartHint" xml:space="preserve"><value>更改界面语言后需要重启应用程序才能完全生效。</value></data>
|
||||
<data name="FloatingBar_AreaEraser" xml:space="preserve"><value>面积擦</value></data>
|
||||
<data name="FloatingBar_StrokeEraser" xml:space="preserve"><value>线擦</value></data>
|
||||
<data name="FloatingBar_LassoSelect" xml:space="preserve"><value>套索选</value></data>
|
||||
<data name="FloatingBar_Geometry" xml:space="preserve"><value>几何</value></data>
|
||||
<data name="FloatingBar_ClearAndMouse" xml:space="preserve"><value>清并鼠</value></data>
|
||||
<data name="FloatingBar_Whiteboard" xml:space="preserve"><value>白板</value></data>
|
||||
<data name="FloatingBar_Hide" xml:space="preserve"><value>隐藏</value></data>
|
||||
<data name="Geometry_Title" xml:space="preserve"><value>几何绘图</value></data>
|
||||
<data name="Geometry_DrawLine" xml:space="preserve"><value>画直线</value></data>
|
||||
<data name="Geometry_DrawDashedLine" xml:space="preserve"><value>画虚线</value></data>
|
||||
<data name="Geometry_DrawDottedLine" xml:space="preserve"><value>画点线</value></data>
|
||||
<data name="Geometry_DrawArrow" xml:space="preserve"><value>画箭头</value></data>
|
||||
<data name="Geometry_DrawParallelLines" xml:space="preserve"><value>4平行线</value></data>
|
||||
<data name="Geometry_DrawCenteredSquare" xml:space="preserve"><value>中心正方</value></data>
|
||||
<data name="Geometry_DrawCenteredCircle" xml:space="preserve"><value>中心圆</value></data>
|
||||
<data name="Geometry_DrawCenteredDashedCircle" xml:space="preserve"><value>中心虚圆</value></data>
|
||||
<data name="Geometry_DrawCenteredEllipse" xml:space="preserve"><value>中心椭圆</value></data>
|
||||
<data name="Geometry_DrawCuboid" xml:space="preserve"><value>长方体</value></data>
|
||||
<data name="Geometry_DrawSquare" xml:space="preserve"><value>正方形</value></data>
|
||||
<data name="Geometry_DrawCylinder" xml:space="preserve"><value>画圆柱</value></data>
|
||||
<data name="Geometry_DrawCone" xml:space="preserve"><value>画圆锥</value></data>
|
||||
<data name="FloatingBar_GestureButton" xml:space="preserve"><value>手势</value></data>
|
||||
<data name="FloatingBar_GesturePanelTitle" xml:space="preserve"><value>手势选项</value></data>
|
||||
<data name="FloatingBar_Gesture_MultiTouchWriting" xml:space="preserve"><value>多指书写</value></data>
|
||||
<data name="FloatingBar_Gesture_TwoFingerMove" xml:space="preserve"><value>双指移动</value></data>
|
||||
<data name="FloatingBar_Gesture_TwoFingerZoom" xml:space="preserve"><value>双指缩放</value></data>
|
||||
<data name="FloatingBar_Gesture_TwoFingerRotate" xml:space="preserve"><value>双指旋转</value></data>
|
||||
<data name="Board_Gesture" xml:space="preserve"><value>手势</value></data>
|
||||
<data name="Board_GestureOptions" xml:space="preserve"><value>手势选项</value></data>
|
||||
</root>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user