Compare commits
1599 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8383002b5c | |||
| 8c37b91b5b | |||
| 5a387eef96 | |||
| c27759189d | |||
| a33a399989 | |||
| 6c7c76958f | |||
| 6980abe331 | |||
| 5fc92cdd10 | |||
| 4bcc39a7ae | |||
| a044a8bc21 | |||
| f15a293a69 | |||
| 448a695503 | |||
| 3915893ee6 | |||
| 914e6eea2e | |||
| 3434ab6227 | |||
| 90ba3f7fa6 | |||
| f05bcf6cc8 | |||
| 21d5ee25ea | |||
| 13c73fbfe4 | |||
| 97dbedfed5 | |||
| e5ef1c6472 | |||
| 17945a9298 | |||
| 17c0ecc0f5 | |||
| 8c2fc15d81 | |||
| ac399773d0 | |||
| 199674feca | |||
| cb6af3e21f | |||
| efe0bb6ae2 | |||
| 4a1403eed3 | |||
| 77a7628453 | |||
| 21186d1edb | |||
| 1cb77b4f48 | |||
| a6f92a883c | |||
| 2c0b09a9ad | |||
| 003fffca2d | |||
| 28b719e9db | |||
| 4d8fbef455 | |||
| e55f8e8ea1 | |||
| 8ad987a64b | |||
| b0aff1e378 | |||
| 6239ac0b88 | |||
| 875fa9c6f0 | |||
| a7b020b0ff | |||
| b614517728 | |||
| 329e9bd933 | |||
| 73f27a9423 | |||
| ffee3aa564 | |||
| 9268ebb580 | |||
| 40fc4e89e0 | |||
| 6fd4a4f036 | |||
| 483991b85c | |||
| a0d24ea6cc | |||
| 9613285513 | |||
| 649a939a57 | |||
| f20e360c0b | |||
| c1e599971e | |||
| 21c93d38c7 | |||
| 4902559cfa | |||
| 49b22dc184 | |||
| f1f75ff015 | |||
| 6e68fa9cfc | |||
| c68596b91e | |||
| 7d52573595 | |||
| 6ac34ba8aa | |||
| 9136f1dbe3 | |||
| 98cfdb53c3 | |||
| e17387c99e | |||
| d30ed9e726 | |||
| 988af60a30 | |||
| 8b53456b5d | |||
| d23193527e | |||
| b58376847e | |||
| d0551db70a | |||
| e416151a56 | |||
| b02b496b88 | |||
| e31dbbcedc | |||
| 09ad1bac86 | |||
| a2ac761f70 | |||
| 9e5ad7e2b4 | |||
| 4f015fb155 | |||
| b4526473fa | |||
| 3ab9fcc857 | |||
| 7ef231a4c7 | |||
| da3a1f7dea | |||
| 81001542b8 | |||
| aacf5b696c | |||
| 062c0feb65 | |||
| 92f97eab78 | |||
| 31120eab09 | |||
| 9b7d718f78 | |||
| 9132c9eb14 | |||
| adebf8ec38 | |||
| 854e14f9b0 | |||
| d7faac257c | |||
| 1b3c5294d9 | |||
| f7802ba4d0 | |||
| 8e84dce73f | |||
| 620da0e645 | |||
| c4657ebb86 | |||
| 24cbaf69ea | |||
| f3ef2f7aec | |||
| 77dff81217 | |||
| 004364c3a9 | |||
| 877d702978 | |||
| a8bc9f442b | |||
| f09f05ab3c | |||
| ef5377f85c | |||
| 5dbdd7bf12 | |||
| d73e3eb0ea | |||
| b7d03d694c | |||
| b7d4983af5 | |||
| 919cf8a736 | |||
| 8a7d2ddf30 | |||
| b1640f44c2 | |||
| 6e61538dec | |||
| f789a919e1 | |||
| b9651240df | |||
| 5266368f79 | |||
| 42854ff924 | |||
| 48b0e09278 | |||
| f05062f902 | |||
| 532aa03c56 | |||
| b891cb6fe3 | |||
| 0683779e09 | |||
| b949b1651a | |||
| 259ce3dd11 | |||
| cab703b98e | |||
| a57e0496ee | |||
| df66a49ba5 | |||
| 245a29f797 | |||
| 4e8f574fdb | |||
| a5d6135f0c | |||
| 12c7fb1713 | |||
| 4dd56a4e5d | |||
| abe5992d21 | |||
| 8d74b6ee30 | |||
| 8b5797ac66 | |||
| 231b850f74 | |||
| 7003bb8426 | |||
| c4b5be783e | |||
| c38829f7d8 | |||
| fdf3633d8c | |||
| 3916476af7 | |||
| edc94547ab | |||
| 8a8cf9a679 | |||
| 21f3b99838 | |||
| 3bfa0b1d85 | |||
| de55fd43a0 | |||
| 6e68e53b1e | |||
| 4b8d89854e | |||
| 8298f7d5bb | |||
| 816153a1db | |||
| 1e8ddf4754 | |||
| d73e87b980 | |||
| b64b0a3618 | |||
| d52ae90b56 | |||
| 0045f97569 | |||
| 41be1e901d | |||
| 0a14d96e10 | |||
| 57c3fe358b | |||
| c255db6ff3 | |||
| f6484f759e | |||
| b16636d06b | |||
| b50049c822 | |||
| 220a316d70 | |||
| dbac80509a | |||
| bb82eb4891 | |||
| c1d584e3e7 | |||
| f776dac04d | |||
| b56f99fff9 | |||
| 4151dfaf1f | |||
| 836e0b0fde | |||
| 6e7a0e36f4 | |||
| a7fe7937a5 | |||
| dba59b92ac | |||
| 0d6ffa6de6 | |||
| 9edf5ee36c | |||
| 585b712c4c | |||
| d6f20a365e | |||
| f7befe387c | |||
| b1a34b7a64 | |||
| d556cf7741 | |||
| 76a046cca6 | |||
| d6385aea1c | |||
| 31bf2b5af1 | |||
| de613c8a4b | |||
| 84b626d344 | |||
| 2ca7d2a337 | |||
| ea03e8e7c7 | |||
| 778894482f | |||
| 1035f7ef92 | |||
| 6e4f8bc982 | |||
| 36e47cec43 | |||
| 8b2bc352a6 | |||
| 256b3c0887 | |||
| 77cf91e802 | |||
| ea55eb1738 | |||
| 302ef307fe | |||
| 3f3a10de7d | |||
| c73123cd23 | |||
| 73935e0f22 | |||
| 0dfe835eef | |||
| 1ec07421b5 | |||
| 441e600b5d | |||
| bd4b4bd233 | |||
| f0343c98b5 | |||
| 36ff945384 | |||
| 057fb35d00 | |||
| bcbece5bd6 | |||
| 1b01e4a5c5 | |||
| 5aad206e0d | |||
| 74344c4782 | |||
| f280358f56 | |||
| 243201502b | |||
| 8c090218d1 | |||
| 6ca1c598d4 | |||
| 5eff424b2d | |||
| 0c078ef863 | |||
| fea6576dfb | |||
| 998d829783 | |||
| 226a6942dc | |||
| 52a3d184c7 | |||
| 44cdab063c | |||
| 24bc2cf138 | |||
| e6b707ea51 | |||
| 6183011952 | |||
| d80a59556e | |||
| ebbe018bae | |||
| bad05f77b5 | |||
| ea375a9ce6 | |||
| d3382d7856 | |||
| acf0c17d7a | |||
| 83568c8b33 | |||
| cc80529498 | |||
| 002970921d | |||
| ea74592e89 | |||
| fa38d3d664 | |||
| cffedb8cb7 | |||
| c77beb662e | |||
| 77dd83c2d6 | |||
| ceb99cea2b | |||
| 04a8224484 | |||
| 2f54e9d7b3 | |||
| 1165e5bbf2 | |||
| 24c6ca60a3 | |||
| 8f3b65c6d4 | |||
| 4835e3db50 | |||
| 7a30c93ff5 | |||
| 1fca17d557 | |||
| 67a70fd6e4 | |||
| 2e8660de26 | |||
| 069a478559 | |||
| c70d8e1c4e | |||
| 379d514bb5 | |||
| 25dc6a00d3 | |||
| eb2f65e6e5 | |||
| 16c86cd02d | |||
| f5a657d5c3 | |||
| dfc23b4428 | |||
| b62055e705 | |||
| 3190211f9a | |||
| e138f600a1 | |||
| 2173b82fb4 | |||
| ceeb9bffba | |||
| ba70d67c89 | |||
| 74341cc162 | |||
| fc4a3a1194 | |||
| c33ac03255 | |||
| 66afe271c5 | |||
| 9279783fc3 | |||
| 3cf1ea438b | |||
| 277e46030d | |||
| af19ffb736 | |||
| 1f00962c47 | |||
| 742a62b6ff | |||
| 140e92eeda | |||
| 34c2dab82a | |||
| fa2366c373 | |||
| e2d898df14 | |||
| cacc67b11d | |||
| aef860a8cc | |||
| c890f95092 | |||
| 7c9a5f8265 | |||
| 7eafcb02cb | |||
| d4517d3c53 | |||
| 62a9a097aa | |||
| e7fa1caf6c | |||
| e347443a0a | |||
| b77e540863 | |||
| df4168e8fa | |||
| 576f86ce48 | |||
| c3335c0c24 | |||
| 26f79da2f9 | |||
| 11cb1815ad | |||
| a6fdb07e8b | |||
| ffdb3cd431 | |||
| 5f9b01ef04 | |||
| ca53624149 | |||
| 6484450ad3 | |||
| 7c97b683ea | |||
| 378b3e73f2 | |||
| a2f21357b6 | |||
| 79e03dc0d5 | |||
| e0f35450e1 | |||
| cc9f58fb6a | |||
| e8be85141b | |||
| 1bf57ea2f5 | |||
| 18b737b22b | |||
| 4ec3332808 | |||
| b2290ecf65 | |||
| 55c336daab | |||
| f2ed3f619c | |||
| 114f400bd9 | |||
| 506d1118b2 | |||
| 20441543f0 | |||
| 2dbc47ac3c | |||
| bbe99649cd | |||
| 04806d2004 | |||
| 759a635026 | |||
| e68bd9286f | |||
| a6b31e82c0 | |||
| bc3e37e541 | |||
| 1124bb6bfa | |||
| b19c19d73c | |||
| feb3fad4da | |||
| 3db745d684 | |||
| b4089ae62e | |||
| 97bdf78b08 | |||
| dc9fb26260 | |||
| 97b0972fdf | |||
| ea23145349 | |||
| f7013196f7 | |||
| 9d0baa0799 | |||
| 91c2fa4eee | |||
| 837311b2b7 | |||
| bc53eab669 | |||
| 55eb811193 | |||
| f7aa107a62 | |||
| 56a65af9a7 | |||
| de3f5d16a2 | |||
| d325a58f17 | |||
| fd137ae787 | |||
| bb1b893961 | |||
| 01c247ac29 | |||
| 1fa75640ee | |||
| 6e0191af6b | |||
| 9ac4070e4e | |||
| 2590ea5bdb | |||
| 290e031f77 | |||
| 9bc9af5eec | |||
| 8faffe9d4e | |||
| 95dfad64ce | |||
| c9dd98fa8d | |||
| 3ae99c82cc | |||
| 016abafee4 | |||
| dca606cf5f | |||
| e7c77b173d | |||
| 68b588ee7b | |||
| b4c753dfb4 | |||
| 037bcf6007 | |||
| b2e0e7b9e2 | |||
| 03c1399768 | |||
| 9b3a59f089 | |||
| e8f824a046 | |||
| 4772b9cce7 | |||
| 74b64bd21f | |||
| 3934270ed2 | |||
| 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 | |||
| 0c3938b652 | |||
| 16f80adb0d | |||
| 637b6bb4f9 | |||
| 12e91927a5 | |||
| 8f6f22ba7f | |||
| b520d6a334 | |||
| b7bff30445 | |||
| 0d790bbd80 | |||
| 8394e99127 | |||
| 16ae32bfd7 | |||
| cc6423e384 | |||
| 06cc587599 | |||
| 9d36088f1d | |||
| f9f73b015c | |||
| 77ffd696bb | |||
| bdb8bed053 | |||
| 4b17c8e96e | |||
| 8109711f4e | |||
| 327eba3fa7 | |||
| f3dccb2e99 | |||
| 7112d58e7c | |||
| 6e0aad853c | |||
| c64b1d0846 | |||
| 6eba16ce99 | |||
| 2f6f719843 | |||
| f34bac49e4 | |||
| 39cdc6231f | |||
| 745e24da70 | |||
| a4f4f4fb15 | |||
| 3833c229c6 | |||
| 3dc3e9b5a8 | |||
| 12eeb79e9f | |||
| e08c84f70d | |||
| 10f55d5b65 | |||
| 761992d089 | |||
| 991a823700 | |||
| 61dbcf762c | |||
| 60b0149a9c | |||
| 501c034cfa | |||
| 11593db23c | |||
| a9ae2a004f | |||
| 7b2a31781c | |||
| ab73eb9632 | |||
| 8ade170b4e | |||
| 91c3d1161d | |||
| bb63805e87 | |||
| 8caf04990c | |||
| f28dd0a965 | |||
| b5d83268e5 | |||
| 19adc42122 | |||
| 8afee913be | |||
| 82d101365f | |||
| 73e679f268 | |||
| 372a8a1de1 | |||
| 171cc34c91 | |||
| fbd217d674 | |||
| f24960ab26 | |||
| d1a871c8f6 | |||
| 086b97906b | |||
| db76a11347 | |||
| 729b10cce8 | |||
| 4cbd25ccb3 | |||
| 0ef7b738a9 | |||
| 33b5563601 | |||
| f6f799f534 | |||
| 8c3f1360e1 | |||
| fafbabd603 | |||
| 254e38895c | |||
| aa0bb22cdd | |||
| 5bfd0c7b2f | |||
| c3885c170c | |||
| 88a7cce269 | |||
| a1fccc2905 | |||
| a889041896 | |||
| 8d778aba2c | |||
| ba5db63e0b | |||
| 0259d83429 | |||
| b6999e57ae | |||
| 04184cf731 | |||
| 04f98eb9e7 | |||
| 869dd045af | |||
| 339ebb862e | |||
| 7b8598bf9f | |||
| ab1c460225 | |||
| 298164d6ed | |||
| 703a8a4e0d | |||
| deaf5fcbf6 | |||
| 18b46689f1 | |||
| edbe3f8311 | |||
| 94c4c3e2d4 | |||
| 077f72737d | |||
| ae089a9390 | |||
| 0f087c2aa6 | |||
| 19d2ffb48e | |||
| a9bb6f73de | |||
| ca124732fa | |||
| 11da14aeab | |||
| 7d5f037c85 | |||
| f62b415227 | |||
| f2b8d4014e | |||
| 3ef047fb41 | |||
| aa3be4ab0d | |||
| 7500049ea2 | |||
| 44600df75c | |||
| dec2a15773 | |||
| 4da78a04cb | |||
| 98ec204bab | |||
| cd9499b064 | |||
| f5e824be86 | |||
| c76254f4f9 | |||
| 77ac6f88ca | |||
| 7e10911991 | |||
| 161b67b09d | |||
| 9614536a29 | |||
| 8ba7aab468 | |||
| 4ea9f79de1 | |||
| b7f7025d97 | |||
| 20d5dd2668 | |||
| adc2d02fbb | |||
| ef2dbdc93b | |||
| 9721ec1f0b | |||
| 4d069d87d7 | |||
| 0308f9ce65 | |||
| 6bcd3cb217 | |||
| 7f83c490db | |||
| bbf9c895b8 | |||
| ffa2063c52 | |||
| d464b1f78e | |||
| 92cb071408 | |||
| 9141b60d03 | |||
| df0a196931 | |||
| a8cb1dd495 | |||
| adc4966d49 | |||
| 1b2ea8c522 | |||
| 7c8bdb489b | |||
| 16458fbb42 | |||
| c72839cdcb | |||
| a31ad5803c | |||
| cf800cbd36 | |||
| 3c06ef0b1a | |||
| 2c3b921f09 | |||
| 402ecc66ae | |||
| e25f56a9b5 | |||
| b28fa887a2 | |||
| f83a02e619 | |||
| eaad089d68 | |||
| a2fda16df9 | |||
| e9e8ff57ae | |||
| 1fd95a2f2e | |||
| 228584ee48 | |||
| c651df0f6e | |||
| 5cced9baf2 | |||
| 5dd26e554b | |||
| 696fd3e8cd | |||
| 1d19b705d3 | |||
| d54074cb57 | |||
| cc054aeb75 | |||
| c32eaed534 | |||
| fa23f73ec4 | |||
| fbf6a13f92 | |||
| 3eba662772 | |||
| e007ee271f | |||
| 59a8d65a89 | |||
| 793519ae1b | |||
| 5c44062aa2 | |||
| fe03a2c2c0 | |||
| d1d74a3770 | |||
| 16108bf779 | |||
| 5bebf077e4 | |||
| efcc01ad6b | |||
| e8a4b45446 | |||
| 6cba297d77 | |||
| b149dc3cb9 | |||
| 1ee4f934b9 | |||
| f7b4adb85d | |||
| da3c41567d | |||
| 5f73795220 | |||
| 36bf1122c6 | |||
| 402f8bb9f9 | |||
| eef2a915fa | |||
| 26ee4d172f | |||
| 08eb450446 | |||
| ecae7d818c | |||
| 47ffccff68 | |||
| 0ba5286c94 | |||
| db8e1e3589 | |||
| d0764a6a77 | |||
| 7b6c347d6b | |||
| d58fec180c | |||
| ac4e4877a1 | |||
| ddea61245d | |||
| 98915bcff2 | |||
| 8e0f0450df | |||
| 1d9a669829 | |||
| 97bcf168b7 | |||
| e81198165b | |||
| b03b8da586 | |||
| d497e07187 | |||
| f284a99194 | |||
| 7d6dd6f805 | |||
| d70e28d198 | |||
| 4c6f138e5c | |||
| 5974841a7b | |||
| 5df54439ba | |||
| 8f627c6b7f | |||
| 3f28bc5c6c | |||
| 903fa699ca | |||
| d9e8f64699 | |||
| 8ad6ca8d41 | |||
| 4017efc65e | |||
| 6ed15d52de | |||
| 5246a2f79b | |||
| b670932dd7 | |||
| ddf30bd96b | |||
| 7e92428485 | |||
| 4c16050c85 | |||
| 2ed035525a | |||
| 1b89b0d7b6 | |||
| 0364821f0b | |||
| fa9d6f37ae | |||
| a4777904b9 | |||
| 0b3c2f95c5 | |||
| 8f54955432 | |||
| 034db2fc27 | |||
| 59141b0241 | |||
| 2e43b96a2c | |||
| edff96299c | |||
| 8d9587b790 | |||
| ad0cf2a849 | |||
| f928946c61 | |||
| 6cc9d0bee2 | |||
| 287e6bb91f | |||
| 3509036d85 | |||
| 752901dbb9 | |||
| e3add94546 | |||
| 8d31c74b39 | |||
| e6f608d206 | |||
| d80b9e29a7 | |||
| 092674465d | |||
| b2e3a5bb18 | |||
| 147a2f957e | |||
| ce56f2919c | |||
| 18059102a3 | |||
| 4542fcccbb | |||
| d12009d30c | |||
| d85e0fbd13 | |||
| f7a5f9ae6b | |||
| 3d4c3d0acc | |||
| c106c41048 | |||
| cea777e8b2 | |||
| 6f069b73da | |||
| c80af8c984 | |||
| d51cbd0682 | |||
| 7e94c945ff | |||
| bd9095b4c2 | |||
| c135bf8fb8 | |||
| f6aebb15b4 | |||
| 2b4f88becd | |||
| 7fbd3639b6 | |||
| 9e52c2c31d | |||
| 46a1e5ff14 | |||
| fa7c1fd646 | |||
| c347809eea | |||
| dd09a42f02 | |||
| db8ffd05ea | |||
| 045c29ca20 | |||
| 4ea6d19602 | |||
| df00447c41 | |||
| db5b1caea7 | |||
| 8432954b8d | |||
| 4a000e23e9 | |||
| b42db51346 | |||
| 1ef968ea93 | |||
| 5b50537333 | |||
| 8c624b48fb | |||
| ad329fc2c8 | |||
| f045b8f659 | |||
| 00a9da45bb | |||
| b7ecd12f8c | |||
| a8a3164ee7 | |||
| b1aec69e98 | |||
| d713a8c5a4 | |||
| 3895faf941 | |||
| bf336fdb10 | |||
| c96c26288c | |||
| 83558c089d | |||
| 26acb72e17 | |||
| 4724a432ab | |||
| 5b9c1627c0 | |||
| d06c2585cf | |||
| 3e96e51efd | |||
| 9afa7191c4 | |||
| dfddec5586 | |||
| 93022424b3 | |||
| aed77a187f | |||
| 583f47c98b | |||
| ff528b3f9d | |||
| 6d98d96ccb | |||
| 6f8e08c21a | |||
| 3a7417f493 | |||
| 66164a0c33 | |||
| 0d7a478e16 | |||
| 9caef310df | |||
| 688f742c16 | |||
| 38dd083cdc | |||
| 851bd1c3c6 | |||
| c91b8a1a7a | |||
| 69c45764b2 | |||
| 07a62d2f78 | |||
| 46b064b0a8 | |||
| 1d3b96bb65 | |||
| 916425b7f5 | |||
| 1c570ca242 | |||
| ae41108971 | |||
| 395e7609a4 | |||
| 185c9dc284 | |||
| a9b0ac0595 | |||
| d9e3524211 | |||
| f40ef15a24 | |||
| 4d7544eefb | |||
| 92bb458345 | |||
| 92e695ef7c | |||
| 45ff3fbb9b | |||
| ba252e92d3 | |||
| 4778f459e3 | |||
| c997854ae0 | |||
| 65b5322ad5 | |||
| dd43bf0476 | |||
| 15c808eebd | |||
| 28748a99ca | |||
| fb37d3b9e6 | |||
| e1f10e054c | |||
| c670357c01 | |||
| 92dce9b36e | |||
| aa2b62e8da | |||
| a34a65f354 | |||
| eea5f8496c | |||
| 091a256bcc | |||
| 14c9ce3ce1 | |||
| fd1e5e13fe | |||
| 19685b8f95 | |||
| a9da8dc10c | |||
| fcfba7a978 | |||
| 2c8c4351dd | |||
| 79264b187d | |||
| 52eba9117b | |||
| 37a69032f6 | |||
| dd16b4f5a1 | |||
| e9c20255a6 | |||
| 630b4edf91 | |||
| 04ef638e17 | |||
| 35bad240c2 | |||
| 0cb4749cf3 | |||
| 7c8281eb00 | |||
| 1957288219 | |||
| c4d2b15c48 | |||
| beebfb0dae | |||
| d67172b795 | |||
| 2b012bc042 | |||
| 84da68f950 | |||
| ad97fd13bf | |||
| ebf6d0d5f7 | |||
| 0f3b4b4384 | |||
| 226cf46d6b | |||
| 5689e33541 | |||
| 15e59e7117 | |||
| 6b538f6662 | |||
| ad6808b696 | |||
| 8347b18efc | |||
| f92b132f0a | |||
| 79057d6757 | |||
| a9b5ee8f62 | |||
| 65da2c449e | |||
| 0ef8c12650 | |||
| 1dfa48aecb | |||
| b5d1713b43 | |||
| 102421997d | |||
| 43370bb8d1 | |||
| 310f50fcde | |||
| 9b5d157333 | |||
| 53fbb7a0bd | |||
| fe92117c4e | |||
| 4a0d13457a | |||
| 1ad6405003 | |||
| 75e7e36011 | |||
| 933c695b8c | |||
| b34f7142f6 | |||
| 7392fa8165 | |||
| 349d417869 | |||
| b8e94cac9c | |||
| cd90490b8d | |||
| a9cc94ccb6 | |||
| 17f137af09 | |||
| ea7d0bbf71 | |||
| 176f1cf405 | |||
| 2ee93bbcc1 | |||
| ba0629000e | |||
| 313049f873 | |||
| d1a1b17d29 | |||
| eb91d2ce0a | |||
| 71c77da898 | |||
| 1b96c70386 | |||
| 703cecbd4f | |||
| e20bf41cc7 | |||
| 0f7b9524f9 | |||
| 53a9e901ec | |||
| 9a79fc71ed | |||
| e7f7a8038f | |||
| 5361f8ae6f | |||
| ec3bcddc9d | |||
| 326a9f1d75 | |||
| 8c07e9b8a3 | |||
| 406d01febf | |||
| 98663422b1 | |||
| 879dfcea28 | |||
| 40edb4c8de | |||
| 07b6d142ad | |||
| 5716b3e3cf | |||
| 1b242a07e1 | |||
| 1516a73228 | |||
| eef00eb28f | |||
| a5ac282ab6 | |||
| dbb88d4999 | |||
| 39addad3a1 | |||
| 87fd9a4470 | |||
| 7a9156449f | |||
| 6e2938e9c1 | |||
| 39e1498b26 | |||
| cc2dc9c1a7 | |||
| fa87198131 | |||
| 9b70b952f6 | |||
| b29fca1cdb | |||
| 501af800ce | |||
| 979be117c6 | |||
| 13594371bb | |||
| 9628d1b27f | |||
| fd0bd0b343 | |||
| 9ea58bfdad | |||
| c54d140107 | |||
| 98d4a4213c | |||
| a7d1de5ee3 | |||
| fab9e4b265 | |||
| d5fa46033e | |||
| 32c179bbf9 | |||
| 99f9886876 | |||
| c1fcaff28a | |||
| 2da5e8d71b | |||
| e80e64a287 | |||
| 0344e51ef7 | |||
| 9fd31b0584 | |||
| c6a48f79da | |||
| 110d050cc6 | |||
| 18188ef235 | |||
| 3e1c397132 | |||
| cda1f0b77d | |||
| afd49f154c | |||
| 505c620103 | |||
| 5d9e340d6d | |||
| f705c4575c | |||
| 1508bd806f | |||
| 1c542e0615 | |||
| ed5bcbde18 | |||
| 084cbcd362 | |||
| ad8369cfe9 | |||
| 7d36b3993e | |||
| ead0854c8e | |||
| 60b5655574 | |||
| 9037630990 | |||
| ba23fc9506 | |||
| 938ca7c0ea | |||
| a034f7a9a0 | |||
| d4c4fcfd74 | |||
| 5b457f2a8a | |||
| adc22441fc | |||
| 1ba59136c6 | |||
| 77702b2ac9 | |||
| 0d6c814908 | |||
| 42ce58db60 | |||
| 1ef56033fb | |||
| 8c1d3c0248 | |||
| a725a12d25 | |||
| f67db9beed | |||
| f22fd7b5d1 | |||
| 060664260b | |||
| 9da5ec7413 | |||
| 076240f7cd | |||
| d97a4ad243 | |||
| 56209d8491 | |||
| 61fe7197f5 | |||
| 1881bfd69e | |||
| 4287b7def5 | |||
| 27d683a7cc | |||
| 38902c8f88 | |||
| d8e8142ff4 | |||
| fdaf9cb3ef | |||
| b2fe091c88 | |||
| 8548244cef | |||
| 91a5881600 | |||
| 93fd043b14 | |||
| 47948ef530 | |||
| 61fa618673 | |||
| 545aecc6ae | |||
| ee41f53286 | |||
| cc8036f736 | |||
| 67a982e6e4 | |||
| b16000f8e5 | |||
| a142ce0542 | |||
| c87b8b65fc | |||
| 7332df1d56 | |||
| de90c17ab1 | |||
| 3d54edf25b | |||
| 111819dbf3 | |||
| 3665753ba2 | |||
| 75e38aa8f3 | |||
| 29dc6938c3 | |||
| 304933b02b | |||
| 49f268bb62 | |||
| d0793c546d | |||
| 375aec6f6c | |||
| 12ec527dcd | |||
| 52d95173d7 | |||
| 67e3d51106 | |||
| 9081aea926 | |||
| 02c6caf465 | |||
| 1a9ddf1969 | |||
| 41bcae9d05 | |||
| 28109a0c97 | |||
| d8825f0a73 | |||
| 82c045a243 | |||
| 4d11f69282 | |||
| 664e4ea048 | |||
| 6d21c5e24e | |||
| ba21b6884d | |||
| 60065aab3e | |||
| 4d978936ac | |||
| 4ea585633b | |||
| dee0bd7f4d | |||
| edb206ffa5 | |||
| 21932056f3 | |||
| 0c3809b789 | |||
| d5ae596ad6 | |||
| 42db7e60f2 | |||
| 85827820a4 | |||
| fad2571edb | |||
| c53b6de68c | |||
| c86eb12168 | |||
| fb31f8df11 | |||
| 147b4fc5ed | |||
| cdcb2d2af0 | |||
| ff086e497c | |||
| a2b711da05 | |||
| 40ecfbefa3 | |||
| 2097a1250b | |||
| 22025f9b00 | |||
| 80c1badba5 | |||
| be308ba280 | |||
| fa7f3d44e4 | |||
| 9bb00489fe | |||
| 33948c604c | |||
| b8fe5bbd66 | |||
| 3fcc01c253 | |||
| 658d48c17b | |||
| 8d76c014c8 | |||
| 26cd125534 | |||
| 1bc23af61a | |||
| a2aa6b48a8 | |||
| 5784c974f7 | |||
| 80503dc42e | |||
| d76195f7ae | |||
| 2fe482b802 | |||
| 16283f4643 | |||
| cd7a801400 | |||
| f6d8558d07 | |||
| de20b506f0 | |||
| ea1d52292e | |||
| 463e506ca3 | |||
| 3bae64a2c7 | |||
| 5d13c8b543 | |||
| c6c0789794 | |||
| d7df39290f | |||
| b64cefad46 | |||
| 4690ab3c30 | |||
| b61c7490b3 | |||
| 9e511d29a6 | |||
| a9cdc36967 | |||
| 96c51cd7ff | |||
| 636769c0ef | |||
| 7beb2a3cc5 | |||
| 52d318054c | |||
| 5ee2ad6f3d | |||
| 097d414d0d | |||
| e0ce119374 | |||
| da9a5242cb | |||
| 0211b38be9 | |||
| 512e9b21cd | |||
| 9181ad55ae | |||
| a1cb9e1b28 | |||
| ea09a3e798 | |||
| 21fa146f6f | |||
| 1a3130041f | |||
| 7095a5890c | |||
| 4a86d1aa05 | |||
| 19fe7223fb | |||
| 8867fde3f2 | |||
| 588d1822b1 | |||
| d2b3b38d9e | |||
| 206ae3d9ed | |||
| a67b7a2fd0 | |||
| f5a08b225c | |||
| c807f97c29 | |||
| 8382413137 | |||
| c6acafd3a6 | |||
| 9d43056361 | |||
| 52f8e24954 | |||
| dac05fec84 | |||
| cb4ed77572 | |||
| 3427cbdc2e | |||
| ec0fb0c3cf | |||
| 357983179c | |||
| 78b66c141e | |||
| 3ac88bb400 | |||
| 27493b857c | |||
| e5a976abcf | |||
| 9e89bf5303 | |||
| 54eb330711 | |||
| e9014c6f5d | |||
| 620bcf6fab | |||
| c03bbd9e13 | |||
| b428b4ec5b | |||
| 876cc116ea | |||
| 0ba00c08ef | |||
| d41ecd9d55 | |||
| 35d351ce6b | |||
| 5d3af58361 | |||
| 8554b92f42 | |||
| 76363b4263 | |||
| b4250b9161 | |||
| 441f40886f | |||
| e8e8ad5d63 | |||
| 454078ec82 | |||
| 63f29c2686 | |||
| 280445f613 | |||
| d7f6433b53 | |||
| 22da4cc408 | |||
| 4e185ef584 | |||
| fd9b4b4ba6 | |||
| 2f79bbcb0f | |||
| 2c70d243df | |||
| 7710b77255 | |||
| d1eed23399 | |||
| ff9ce4df44 | |||
| b7c52842f2 | |||
| 365459f649 | |||
| e6354f724f | |||
| 14eedca939 | |||
| 83529cfe09 | |||
| bd4e1c1810 | |||
| 5665fcc823 | |||
| 710a9014dd | |||
| 108c6b2b17 | |||
| 70735943c3 | |||
| d01a24f879 | |||
| ec2d5043ff | |||
| 15082c2c52 | |||
| 44278d68b4 | |||
| 40eeb9db66 | |||
| a5eb1dfca7 | |||
| 8719677f11 | |||
| 8f01b6c5fe | |||
| 9e63a3f49b | |||
| 84edb7bbe6 | |||
| c86ce00a17 | |||
| aba6c18a25 | |||
| 62c81e6d44 | |||
| 68ea323855 | |||
| f94d81ad20 | |||
| 063f8b1751 | |||
| 35cfd26ece | |||
| f331cb1b4d | |||
| 502e205071 | |||
| 975b563b8d | |||
| cc88630859 | |||
| cce0b930ec | |||
| 723c825df2 | |||
| 5b4c966354 | |||
| 69023f98d9 | |||
| dce5722e96 |
+78
-13
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/all-contributors.json",
|
||||
"projectName": "community",
|
||||
"projectOwner": "InkCanvasForClass",
|
||||
"files": [
|
||||
@@ -6,7 +7,7 @@
|
||||
],
|
||||
"commitType": "docs",
|
||||
"commitConvention": "angular",
|
||||
"contributorsPerLine": 7,
|
||||
"contributorsPerLine": 5,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "CJKmkp",
|
||||
@@ -16,16 +17,8 @@
|
||||
"contributions": [
|
||||
"maintenance",
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Hydro11451",
|
||||
"name": "Hydrogen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/214308559?v=4",
|
||||
"profile": "http://hydro11451.qzz.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
"code",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -45,7 +38,10 @@
|
||||
"contributions": [
|
||||
"blog",
|
||||
"doc",
|
||||
"design"
|
||||
"design",
|
||||
"test",
|
||||
"tutorial",
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -85,9 +81,78 @@
|
||||
"name": "PrefacedCorg",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/129855423?v=4",
|
||||
"profile": "https://github.com/PrefacedCorg",
|
||||
"contributions": [
|
||||
"code",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Jursin",
|
||||
"name": "Jursin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/127487914?v=4",
|
||||
"profile": "http://blog.jursin.top",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Tayasui-rainnya",
|
||||
"name": "tayasui rainnya!",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/156585442?v=4",
|
||||
"profile": "https://github.com/Tayasui-rainnya",
|
||||
"contributions": [
|
||||
"design",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "doudou0720",
|
||||
"name": "doudou0720",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/98651603?v=4",
|
||||
"profile": "https://github.com/doudou0720",
|
||||
"contributions": [
|
||||
"code",
|
||||
"blog",
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Hao3288",
|
||||
"name": "NoobHao",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/119276078?v=4",
|
||||
"profile": "https://github.com/Hao3288",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"repoType": "github"
|
||||
}
|
||||
|
||||
@@ -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,24 +36,31 @@ 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.
|
||||
3.
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 期望结果 | Expected Behavior
|
||||
description: 你期望的正确行为或结果 | What did you expect to happen?
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
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
|
||||
+9
-2
@@ -1,6 +1,6 @@
|
||||
name: 功能请求 | Feature Request
|
||||
description: 提出你对本项目的功能建议 | Suggest an idea for this project
|
||||
labels: [enhancement]
|
||||
type: Feature
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -20,7 +20,7 @@ body:
|
||||
label: 需求动机 | Motivation
|
||||
description: 为什么需要这个功能?| Why do you need this feature?
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: textarea
|
||||
id: design
|
||||
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,106 @@
|
||||
name: .NET Build
|
||||
name: .NET Build & Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main,beta ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [ net6 ]
|
||||
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
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture: [AnyCPU, x86]
|
||||
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@v3
|
||||
|
||||
- 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="${{ matrix.architecture }}" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
|
||||
- name: Build the Solution
|
||||
run: |
|
||||
msbuild -t:restore /p:GitFlow="Github Action"
|
||||
msbuild /p:platform="Any CPU" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj"
|
||||
- name: Check if exe file is generated
|
||||
id: check-exe
|
||||
run: |
|
||||
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
|
||||
|
||||
if (Test-Path $exePath) {
|
||||
echo "build_success=true" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "build_success=false" >> $env:GITHUB_OUTPUT
|
||||
|
||||
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
- name: Upload to artifact
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: InkCanvasForClass
|
||||
path: "Ink Canvas/bin/Any CPU/Release/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.${{ matrix.architecture }}
|
||||
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
|
||||
|
||||
- name: Create Summary
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo "# Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then
|
||||
echo "## ✅ Build Successful" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Download Artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip)\)" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "## ❌ Build Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
name: PR Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
branches: [ main, net6 ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.head_ref || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-package:
|
||||
name: Build & Package
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture: [AnyCPU, x86]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5
|
||||
|
||||
- name: Restore Package
|
||||
run: dotnet restore "Ink Canvas.sln" --locked-mode
|
||||
|
||||
- name: Build the Solution
|
||||
run: msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
|
||||
- name: Check if exe file is generated
|
||||
id: check-exe
|
||||
run: |
|
||||
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
|
||||
|
||||
if (Test-Path $exePath) {
|
||||
echo "build_success=true" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "build_success=false" >> $env:GITHUB_OUTPUT
|
||||
|
||||
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
- name: Create Package (if build succeeded)
|
||||
id: create-archive
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
env:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||
run: |
|
||||
$shortSha = $env:GITHUB_SHA.Substring(0, 7)
|
||||
$version = "debug-$shortSha-$env:GITHUB_RUN_NUMBER"
|
||||
echo "archive_name=$version" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Artifact (if build succeeded)
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: InkCanvasForClass.CE.debug.${{ matrix.architecture }}
|
||||
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
|
||||
|
||||
|
||||
- name: Create Summary
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo "# Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then
|
||||
echo "## ✅ Build Successful" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Download Artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip)\)" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "## ❌ Build Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -0,0 +1,754 @@
|
||||
name: Pre-release and Changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_type:
|
||||
description: 'Version bump type'
|
||||
required: true
|
||||
default: 'patch'
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- build
|
||||
prerelease:
|
||||
description: 'Create as pre-release'
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
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:
|
||||
prepare:
|
||||
runs-on: windows-latest
|
||||
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@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"
|
||||
}
|
||||
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
|
||||
|
||||
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('.')
|
||||
|
||||
# 确保版本号格式正确(至少4部分)
|
||||
if ($versionParts.Length -ge 4) {
|
||||
$major = [int]$versionParts[0]
|
||||
$minor = [int]$versionParts[1]
|
||||
$patch = [int]$versionParts[2]
|
||||
$build = [int]$versionParts[3]
|
||||
} else {
|
||||
# 如果版本号格式不正确,补充为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"
|
||||
}
|
||||
|
||||
- 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
|
||||
}
|
||||
|
||||
# ========== 使用 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
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture: [AnyCPU, x86]
|
||||
outputs:
|
||||
archive_name: ${{ steps.create_archive.outputs.archive_name }}
|
||||
zip_size: ${{ steps.calculate_size.outputs.zip_size }}
|
||||
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@v3
|
||||
|
||||
- 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="${{ matrix.architecture }}" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
|
||||
- name: Check if exe file is generated
|
||||
id: check-exe
|
||||
run: |
|
||||
$exePath = "Ink Canvas\bin\Release\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
|
||||
|
||||
if (Test-Path $exePath) {
|
||||
echo "build_success=true" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "build_success=false" >> $env:GITHUB_OUTPUT
|
||||
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 }}"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$archiveName = "InkCanvasForClass.CE.$version$suffix.zip"
|
||||
|
||||
# 创建发布目录
|
||||
New-Item -ItemType Directory -Path "release" -Force
|
||||
|
||||
# 复制发布文件(使用架构特定的路径)
|
||||
Copy-Item "Ink Canvas\bin\Release\$architecture\net6.0-windows10.0.19041.0\*" "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 }}"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 更新 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 }}"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$setupFile = "InkCanvasForClass CE Setup.exe"
|
||||
$newSetupName = "InkCanvasForClass.CE.$version$suffix.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 }}"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$archiveName = "InkCanvasForClass.CE.$version$suffix.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 }}"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$installerName = "InkCanvasForClass.CE.$version$suffix.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'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$zipFile = "InkCanvasForClass.CE.$version$suffix.zip"
|
||||
$setupFile = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
|
||||
|
||||
echo "zip_file=$zipFile" >> $env:GITHUB_OUTPUT
|
||||
echo "setup_file=$setupFile" >> $env:GITHUB_OUTPUT
|
||||
id: get_file_names
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}-${{ matrix.architecture }}
|
||||
path: |
|
||||
${{ steps.get_file_names.outputs.zip_file }}
|
||||
${{ steps.get_file_names.outputs.setup_file }}
|
||||
|
||||
- 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 "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
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 "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
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:
|
||||
pattern: build-files-${{ needs.prepare.outputs.version }}-*
|
||||
merge-multiple: false
|
||||
|
||||
- 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.3.0
|
||||
with:
|
||||
inputs: |
|
||||
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.zip
|
||||
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.exe
|
||||
build-files-${{ needs.prepare.outputs.version }}-x86/*.zip
|
||||
build-files-${{ needs.prepare.outputs.version }}-x86/*.exe
|
||||
release-signing-artifacts: true
|
||||
upload-signing-artifacts: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Signed Artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: signed-files-${{ needs.prepare.outputs.version }}
|
||||
path: |
|
||||
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.sigstore.json
|
||||
build-files-${{ needs.prepare.outputs.version }}-x86/*.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:
|
||||
pattern: build-files-${{ needs.prepare.outputs.version }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- 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'
|
||||
|
||||
# AnyCPU (x64) 架构文件
|
||||
if [ -f "InkCanvasForClass.CE.$version-x64.zip" ]; then
|
||||
zipSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip | '"$zipSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe" ]; then
|
||||
installerSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe | '"$installerSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
if [ -f "InkCanvasForClass.CE.$version-x64.zip.sigstore.json" ]; then
|
||||
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json" ]; then
|
||||
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
# x86 架构文件
|
||||
if [ -f "InkCanvasForClass.CE.$version.zip" ]; then
|
||||
zipSize=$(wc -c < "InkCanvasForClass.CE.$version.zip")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip | '"$zipSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
if [ -f "InkCanvasForClass.CE.$version.Setup.exe" ]; then
|
||||
installerSize=$(wc -c < "InkCanvasForClass.CE.$version.Setup.exe")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe | '"$installerSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
if [ -f "InkCanvasForClass.CE.$version.zip.sigstore.json" ]; then
|
||||
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version.zip.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
if [ -f "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json" ]; then
|
||||
sigstoreSize=$(wc -c < "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 }}-x64.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip.sigstore.json
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe.sigstore.json
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json
|
||||
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() && github.event.inputs.draft != 'true'
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: build-files-${{ needs.prepare.outputs.version }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- 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 }}"
|
||||
X64_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION-x64.zip"
|
||||
X86_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION.zip"
|
||||
|
||||
if [ "$IS_PRERELEASE" == "true" ]; then
|
||||
mkdir -p Beta
|
||||
if [ -f "$X64_ZIP_FILE" ]; then
|
||||
cp "$X64_ZIP_FILE" Beta/
|
||||
git add Beta/InkCanvasForClass.CE.$VERSION-x64.zip
|
||||
fi
|
||||
if [ -f "$X86_ZIP_FILE" ]; then
|
||||
cp "$X86_ZIP_FILE" Beta/
|
||||
git add Beta/InkCanvasForClass.CE.$VERSION.zip
|
||||
fi
|
||||
git commit -m "Add $VERSION PreRelease"
|
||||
else
|
||||
mkdir -p Release Beta
|
||||
if [ -f "$X64_ZIP_FILE" ]; then
|
||||
cp "$X64_ZIP_FILE" Release/
|
||||
cp "$X64_ZIP_FILE" Beta/
|
||||
git add Release/InkCanvasForClass.CE.$VERSION-x64.zip Beta/InkCanvasForClass.CE.$VERSION-x64.zip
|
||||
fi
|
||||
if [ -f "$X86_ZIP_FILE" ]; then
|
||||
cp "$X86_ZIP_FILE" Release/
|
||||
cp "$X86_ZIP_FILE" Beta/
|
||||
git add Release/InkCanvasForClass.CE.$VERSION.zip Beta/InkCanvasForClass.CE.$VERSION.zip
|
||||
fi
|
||||
git commit -m "Add $VERSION Release"
|
||||
fi
|
||||
git push origin main
|
||||
|
||||
- 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 }}-x64.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
fail_on_unmatched_files: false
|
||||
repository: "InkCanvasForClass/community-beta"
|
||||
token: ${{ steps.octo-sts-beta.outputs.token }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }}
|
||||
|
||||
- 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="net6" \
|
||||
${SHA:+-f sha="$SHA"}
|
||||
+432
-3
@@ -1,4 +1,433 @@
|
||||
obj/
|
||||
bin/
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.env
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
|
||||
[Dd]ebug/x64/
|
||||
[Dd]ebugPublic/x64/
|
||||
[Rr]elease/x64/
|
||||
[Rr]eleases/x64/
|
||||
bin/x64/
|
||||
obj/x64/
|
||||
|
||||
[Dd]ebug/x86/
|
||||
[Dd]ebugPublic/x86/
|
||||
[Rr]elease/x86/
|
||||
[Rr]eleases/x86/
|
||||
bin/x86/
|
||||
obj/x86/
|
||||
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
[Aa][Rr][Mm]64[Ee][Cc]/
|
||||
bld/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Build results on 'Bin' directories
|
||||
**/[Bb]in/*
|
||||
# Uncomment if you have tasks that rely on *.refresh files to move binaries
|
||||
# (https://github.com/github/gitignore/pull/3736)
|
||||
#!**/[Bb]in/*.refresh
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
/Ink Canvas/obj
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
*.trx
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Approval Tests result files
|
||||
*.received.*
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.idb
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
**/.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
**/.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
**/.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
#tools/**
|
||||
#!tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
MSBuild_Logs/
|
||||
|
||||
# AWS SAM Build and Temporary Artifacts folder
|
||||
.aws-sam
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
**/.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
**/.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
**/.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Telemetry DSN configuration file (contains sensitive information)
|
||||
telemetry_dsn.txt
|
||||
**/telemetry_dsn.txt
|
||||
.trae/skills/migrate-toggle-switch/SKILL.md
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.7.7.0
|
||||
1.7.18.0
|
||||
|
||||
+57
-13
@@ -1,10 +1,14 @@
|
||||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.33530.505
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.4.11626.88 stable
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InkCanvasForClass", "Ink Canvas\InkCanvasForClass.csproj", "{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.PluginSdk", "InkCanvas.PluginSdk\InkCanvas.PluginSdk.csproj", "{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.Controls", "InkCanvas.Controls\InkCanvas.Controls.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -23,22 +27,62 @@ Global
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x64.Build.0 = Debug|x64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.Build.0 = Debug|x86
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM.Build.0 = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.ActiveCfg = Release|x64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.Build.0 = Release|x64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.ActiveCfg = Release|x86
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.Build.0 = Release|x86
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.Build.0 = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM64.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/PencilsConfiguration/ActualSeverity/@EntryValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\MSBuild.exe</s:String>
|
||||
<s:Int64 x:Key="/Default/Environment/Hierarchy/Build/BuildTool/MsbuildVersion/@EntryValue">1114112</s:Int64></wpf:ResourceDictionary>
|
||||
+64
-33
@@ -1,21 +1,23 @@
|
||||
<Application x:Class="Ink_Canvas.App"
|
||||
<Application x:Class="Ink_Canvas.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Ink_Canvas"
|
||||
xmlns:tb="http://www.hardcodet.net/taskbar"
|
||||
xmlns:props="clr-namespace:Ink_Canvas.Properties"
|
||||
xmlns:tb="clr-namespace:H.NotifyIcon;assembly=H.NotifyIcon.Wpf"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
StartupUri="MainWindow.xaml">
|
||||
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
|
||||
>
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style TargetType="ui:ScrollViewerEx">
|
||||
<EventSetter Event="PreviewMouseWheel" Handler="ScrollViewer_PreviewMouseWheel"/>
|
||||
</Style>
|
||||
<ContextMenu Opened="SysTrayMenu_Opened" x:Shared="false" x:Key="SysTrayMenu" Padding="6" ui:ThemeManager.RequestedTheme="Light">
|
||||
<ContextMenu Opened="SysTrayMenu_Opened" Closed="SysTrayMenu_Closed" x:Shared="false" x:Key="SysTrayMenu" Padding="6" ui:ThemeManager.RequestedTheme="Light">
|
||||
<MenuItem IsCheckable="True" IsChecked="False" Checked="HideICCMainWindowTrayIconMenuItem_Checked" Unchecked="HideICCMainWindowTrayIconMenuItem_UnChecked" Name="HideICCMainWindowTrayIconMenuItem">
|
||||
<MenuItem.Header>
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<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">
|
||||
@@ -31,12 +33,42 @@
|
||||
</Image>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator Margin="0,3" />
|
||||
<MenuItem>
|
||||
<MenuItem Name="TempShowMainWindowTrayIconMenuItem" Click="TempShowMainWindowTrayIconMenuItem_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="{x:Static props:Strings.Tray_TempShowMainWindow}" />
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="28" Height="28" Margin="-2">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
|
||||
<GeometryDrawing Brush="#27272a" Geometry="F0 M24,24z M0,0z M5,6C4.73478,6 4.48043,6.10536 4.29289,6.29289 4.10536,6.48043 4,6.73478 4,7L4,17C4,17.2652 4.10536,17.5196 4.29289,17.7071 4.48043,17.8946 4.73478,18 5,18L19,18C19.2652,18 19.5196,17.8946 19.7071,17.7071 19.8946,17.5196 20,17.2652 20,17L20,7C20,6.73478 19.8946,6.48043 19.7071,6.29289 19.5196,6.10536 19.2652,6 19,6L5,6z M2.87868,4.87868C3.44129,4.31607,4.20435,4,5,4L19,4C19.7957,4 20.5587,4.31607 21.1213,4.87868 21.6839,5.44129 22,6.20435 22,7L22,17C22,17.7957 21.6839,18.5587 21.1213,19.1213 20.5587,19.6839 19.7957,20 19,20L5,20C4.20435,20 3.44129,19.6839 2.87868,19.1213 2.31607,18.5587 2,17.7956 2,17L2,7C2,6.20435,2.31607,5.44129,2.87868,4.87868z M5,8C5,7.44772,5.44772,7,6,7L6.01,7C6.56228,7 7.01,7.44772 7.01,8 7.01,8.55228 6.56228,9 6.01,9L6,9C5.44772,9,5,8.55228,5,8z M9,7C8.44772,7 8,7.44772 8,8 8,8.55228 8.44772,9 9,9L9.01,9C9.56228,9 10.01,8.55228 10.01,8 10.01,7.44772 9.56228,7 9.01,7L9,7z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Name="OpenSettingsTrayIconMenuItem" Click="OpenSettingsTrayIconMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="{x:Static props:Strings.Tray_OpenSettings}" />
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="28" Height="28" Margin="-2" Source="/Resources/Icons-Fluent/ic_fluent_settings_24_regular.png" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator Margin="0,3" />
|
||||
<MenuItem Name="DisableAllHotkeysMenuItem" Click="DisableAllHotkeysMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<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 +86,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 +96,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 +119,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 +129,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 +164,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 +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="T" />
|
||||
</Border>
|
||||
</ui:SimpleStackPanel>
|
||||
</ui:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="28" Height="28" Margin="-2">
|
||||
@@ -163,9 +195,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 +205,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 +227,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 +237,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">
|
||||
@@ -231,13 +263,12 @@
|
||||
ToolTipText="InkCanvasForClass"
|
||||
ContextMenu="{StaticResource SysTrayMenu}"
|
||||
IconSource="/Resources/icc.ico"/>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemeResources RequestedTheme="Light"/>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemeResources/>
|
||||
<ui:XamlControlsResources />
|
||||
<ResourceDictionary Source="Resources/SeewoImageDictionary.xaml"/>
|
||||
<ResourceDictionary Source="Resources/DrawShapeImageDictionary.xaml"/>
|
||||
<ResourceDictionary Source="Resources/IconImageDictionary.xaml"/>
|
||||
<ResourceDictionary Source="Resources/Styles/Light.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
+1152
-721
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
|
||||
[assembly: System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
@@ -10,7 +11,7 @@ using System.Windows;
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("CJK_mkp")]
|
||||
[assembly: AssemblyProduct("InkCanvasForClass")]
|
||||
[assembly: AssemblyCopyright("Copyright © HARKOTEK Studio 2024")]
|
||||
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025-2026")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
@@ -19,14 +20,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 +44,5 @@ using System.Windows;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.7.7.5")]
|
||||
[assembly: AssemblyFileVersion("1.7.7.5")]
|
||||
[assembly: AssemblyVersion("1.7.18.10")]
|
||||
[assembly: AssemblyFileVersion("1.7.18.10")]
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Controls
|
||||
{
|
||||
[TemplatePart(Name = PartCloseImage, Type = typeof(UIElement))]
|
||||
[TemplatePart(Name = PartAnimationRoot, Type = typeof(UIElement))]
|
||||
public class BoardMenuFrame : ContentControl
|
||||
{
|
||||
private const string PartCloseImage = "PART_CloseImage";
|
||||
private const string PartAnimationRoot = "PART_AnimationRoot";
|
||||
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register(nameof(Title), typeof(object), typeof(BoardMenuFrame), new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty TitleFontSizeProperty =
|
||||
DependencyProperty.Register(nameof(TitleFontSize), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(11d));
|
||||
|
||||
public static readonly DependencyProperty HeaderHeightProperty =
|
||||
DependencyProperty.Register(nameof(HeaderHeight), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(48d));
|
||||
|
||||
public static readonly DependencyProperty PanelCornerRadiusProperty =
|
||||
DependencyProperty.Register(nameof(PanelCornerRadius), typeof(CornerRadius), typeof(BoardMenuFrame), new PropertyMetadata(new CornerRadius(5)));
|
||||
|
||||
public static readonly DependencyProperty HeaderCornerRadiusProperty =
|
||||
DependencyProperty.Register(nameof(HeaderCornerRadius), typeof(CornerRadius), typeof(BoardMenuFrame), new PropertyMetadata(new CornerRadius(6, 6, 0, 0)));
|
||||
|
||||
public static readonly DependencyProperty PanelBackgroundProperty =
|
||||
DependencyProperty.Register(nameof(PanelBackground), typeof(Brush), typeof(BoardMenuFrame), new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty HeaderBackgroundProperty =
|
||||
DependencyProperty.Register(nameof(HeaderBackground), typeof(Brush), typeof(BoardMenuFrame),
|
||||
new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2563eb"))));
|
||||
|
||||
public static readonly DependencyProperty HeaderBorderBrushProperty =
|
||||
DependencyProperty.Register(nameof(HeaderBorderBrush), typeof(Brush), typeof(BoardMenuFrame),
|
||||
new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1e3a8a"))));
|
||||
|
||||
public static readonly DependencyProperty IsOpenProperty =
|
||||
DependencyProperty.Register(nameof(IsOpen), typeof(bool), typeof(BoardMenuFrame), new PropertyMetadata(false));
|
||||
|
||||
public static readonly DependencyProperty PlacementTargetProperty =
|
||||
DependencyProperty.Register(nameof(PlacementTarget), typeof(UIElement), typeof(BoardMenuFrame), new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty PlacementProperty =
|
||||
DependencyProperty.Register(nameof(Placement), typeof(PlacementMode), typeof(BoardMenuFrame), new PropertyMetadata(PlacementMode.Custom));
|
||||
|
||||
public static readonly DependencyProperty CustomPopupPlacementCallbackProperty =
|
||||
DependencyProperty.Register(nameof(CustomPopupPlacementCallback), typeof(CustomPopupPlacementCallback), typeof(BoardMenuFrame),
|
||||
new PropertyMetadata((CustomPopupPlacementCallback)PlaceCenteredAbove));
|
||||
|
||||
private static CustomPopupPlacement[] PlaceCenteredAbove(Size popupSize, Size targetSize, Point offset)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new CustomPopupPlacement(
|
||||
new Point((targetSize.Width - popupSize.Width) / 2 + offset.X,
|
||||
-popupSize.Height + offset.Y),
|
||||
PopupPrimaryAxis.Horizontal),
|
||||
new CustomPopupPlacement(
|
||||
new Point((targetSize.Width - popupSize.Width) / 2 + offset.X,
|
||||
targetSize.Height - offset.Y),
|
||||
PopupPrimaryAxis.Horizontal)
|
||||
};
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PopupHorizontalOffsetProperty =
|
||||
DependencyProperty.Register(nameof(PopupHorizontalOffset), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(0d));
|
||||
|
||||
public static readonly DependencyProperty PopupVerticalOffsetProperty =
|
||||
DependencyProperty.Register(nameof(PopupVerticalOffset), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(-4d));
|
||||
|
||||
public object Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
|
||||
public double TitleFontSize { get => (double)GetValue(TitleFontSizeProperty); set => SetValue(TitleFontSizeProperty, value); }
|
||||
public double HeaderHeight { get => (double)GetValue(HeaderHeightProperty); set => SetValue(HeaderHeightProperty, value); }
|
||||
public CornerRadius PanelCornerRadius { get => (CornerRadius)GetValue(PanelCornerRadiusProperty); set => SetValue(PanelCornerRadiusProperty, value); }
|
||||
public CornerRadius HeaderCornerRadius { get => (CornerRadius)GetValue(HeaderCornerRadiusProperty); set => SetValue(HeaderCornerRadiusProperty, value); }
|
||||
public Brush PanelBackground { get => (Brush)GetValue(PanelBackgroundProperty); set => SetValue(PanelBackgroundProperty, value); }
|
||||
public Brush HeaderBackground { get => (Brush)GetValue(HeaderBackgroundProperty); set => SetValue(HeaderBackgroundProperty, value); }
|
||||
public Brush HeaderBorderBrush { get => (Brush)GetValue(HeaderBorderBrushProperty); set => SetValue(HeaderBorderBrushProperty, value); }
|
||||
public bool IsOpen { get => (bool)GetValue(IsOpenProperty); set => SetValue(IsOpenProperty, value); }
|
||||
public UIElement PlacementTarget { get => (UIElement)GetValue(PlacementTargetProperty); set => SetValue(PlacementTargetProperty, value); }
|
||||
public PlacementMode Placement { get => (PlacementMode)GetValue(PlacementProperty); set => SetValue(PlacementProperty, value); }
|
||||
public CustomPopupPlacementCallback CustomPopupPlacementCallback
|
||||
{
|
||||
get => (CustomPopupPlacementCallback)GetValue(CustomPopupPlacementCallbackProperty);
|
||||
set => SetValue(CustomPopupPlacementCallbackProperty, value);
|
||||
}
|
||||
public double PopupHorizontalOffset { get => (double)GetValue(PopupHorizontalOffsetProperty); set => SetValue(PopupHorizontalOffsetProperty, value); }
|
||||
public double PopupVerticalOffset { get => (double)GetValue(PopupVerticalOffsetProperty); set => SetValue(PopupVerticalOffsetProperty, value); }
|
||||
|
||||
public event MouseButtonEventHandler CloseMouseDown;
|
||||
public event MouseButtonEventHandler CloseMouseUp;
|
||||
|
||||
public UIElement AnimationTarget { get; private set; }
|
||||
|
||||
private UIElement _closeImage;
|
||||
|
||||
static BoardMenuFrame()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(BoardMenuFrame), new FrameworkPropertyMetadata(typeof(BoardMenuFrame)));
|
||||
VisibilityProperty.OverrideMetadata(typeof(BoardMenuFrame),
|
||||
new FrameworkPropertyMetadata(Visibility.Collapsed, OnVisibilityChanged));
|
||||
}
|
||||
|
||||
private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((BoardMenuFrame)d).IsOpen = (Visibility)e.NewValue == Visibility.Visible;
|
||||
}
|
||||
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
if (_closeImage != null)
|
||||
{
|
||||
_closeImage.MouseDown -= CloseImage_MouseDown;
|
||||
_closeImage.MouseUp -= CloseImage_MouseUp;
|
||||
}
|
||||
_closeImage = GetTemplateChild(PartCloseImage) as UIElement;
|
||||
if (_closeImage != null)
|
||||
{
|
||||
_closeImage.MouseDown += CloseImage_MouseDown;
|
||||
_closeImage.MouseUp += CloseImage_MouseUp;
|
||||
}
|
||||
AnimationTarget = GetTemplateChild(PartAnimationRoot) as UIElement;
|
||||
}
|
||||
|
||||
private void CloseImage_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
CloseMouseDown?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void CloseImage_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
CloseMouseUp?.Invoke(sender, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Ink_Canvas.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// 画布上的多页 PDF:仅显示当前页;翻页与页码由主窗口 PDF 侧栏控制(无 XAML 文件)。
|
||||
/// </summary>
|
||||
public class PdfEmbeddedView : UserControl
|
||||
{
|
||||
private readonly Image _pageImage;
|
||||
|
||||
private string _pdfPath;
|
||||
private uint _pageCount;
|
||||
private uint _currentIndex;
|
||||
private bool _compressLargePictures;
|
||||
private bool _isPagingBusy;
|
||||
private bool _layoutSizeCommitted;
|
||||
|
||||
/// <summary>页码或可翻页状态变化(用于更新侧栏)。</summary>
|
||||
public event EventHandler PageNavigationStateChanged;
|
||||
|
||||
public PdfEmbeddedView()
|
||||
{
|
||||
MinWidth = 80;
|
||||
MinHeight = 60;
|
||||
|
||||
var grid = new Grid { ClipToBounds = true };
|
||||
_pageImage = new Image
|
||||
{
|
||||
Stretch = Stretch.Uniform,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
grid.Children.Add(_pageImage);
|
||||
Content = grid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化并显示指定页;由 MainWindow 在 UI 线程创建后调用。
|
||||
/// </summary>
|
||||
/// <param name="initialPageIndex">从 0 开始的页码,超出范围时夹紧到合法区间。</param>
|
||||
public async Task InitializeAsync(string pdfFilePath, uint pageCount, bool compressLargePictures, uint initialPageIndex = 0)
|
||||
{
|
||||
_pdfPath = pdfFilePath ?? throw new ArgumentNullException(nameof(pdfFilePath));
|
||||
_pageCount = pageCount;
|
||||
_compressLargePictures = compressLargePictures;
|
||||
if (_pageCount == 0)
|
||||
_currentIndex = 0;
|
||||
else
|
||||
_currentIndex = initialPageIndex >= _pageCount ? _pageCount - 1 : initialPageIndex;
|
||||
|
||||
await ShowPageAsync(_currentIndex);
|
||||
}
|
||||
|
||||
public string PdfPath => _pdfPath;
|
||||
|
||||
public uint PageCount => _pageCount;
|
||||
|
||||
public uint CurrentPageIndex => _currentIndex;
|
||||
|
||||
public string PageLabelText => _pageCount == 0 ? "" : $"{_currentIndex + 1} / {_pageCount}";
|
||||
|
||||
public bool CanGoPrevious => !_isPagingBusy && _pageCount > 1 && _currentIndex > 0;
|
||||
|
||||
public bool CanGoNext => !_isPagingBusy && _pageCount > 1 && _currentIndex < _pageCount - 1;
|
||||
|
||||
public async Task GoToPreviousPageAsync()
|
||||
{
|
||||
await GoRelativeAsync(-1);
|
||||
}
|
||||
|
||||
public async Task GoToNextPageAsync()
|
||||
{
|
||||
await GoRelativeAsync(1);
|
||||
}
|
||||
|
||||
private void NotifyPageNavigationStateChanged()
|
||||
{
|
||||
PageNavigationStateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private async Task GoRelativeAsync(int delta)
|
||||
{
|
||||
if (_isPagingBusy || _pageCount <= 1)
|
||||
return;
|
||||
int next = (int)_currentIndex + delta;
|
||||
if (next < 0 || next >= _pageCount)
|
||||
return;
|
||||
_currentIndex = (uint)next;
|
||||
await ShowPageAsync(_currentIndex);
|
||||
}
|
||||
|
||||
private async Task ShowPageAsync(uint pageIndex)
|
||||
{
|
||||
_isPagingBusy = true;
|
||||
NotifyPageNavigationStateChanged();
|
||||
try
|
||||
{
|
||||
BitmapSource raw = await PdfWinRtHelper.RenderPageToBitmapSourceAsync(_pdfPath, pageIndex);
|
||||
if (raw == null)
|
||||
return;
|
||||
|
||||
BitmapSource display = ApplyCompressionIfNeeded(raw);
|
||||
_pageImage.Source = display;
|
||||
if (!_layoutSizeCommitted)
|
||||
{
|
||||
bool callerSized = !double.IsNaN(Width) && Width > 0 && !double.IsNaN(Height) && Height > 0;
|
||||
if (!callerSized)
|
||||
{
|
||||
Width = display.PixelWidth;
|
||||
Height = display.PixelHeight;
|
||||
}
|
||||
|
||||
_layoutSizeCommitted = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isPagingBusy = false;
|
||||
NotifyPageNavigationStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapSource ApplyCompressionIfNeeded(BitmapSource rendered)
|
||||
{
|
||||
int width = rendered.PixelWidth;
|
||||
int height = rendered.PixelHeight;
|
||||
if (_compressLargePictures && (width > 1920 || height > 1080))
|
||||
{
|
||||
double scaleX = 1920.0 / width;
|
||||
double scaleY = 1080.0 / height;
|
||||
double scale = Math.Min(scaleX, scaleY);
|
||||
return new TransformedBitmap(rendered, new ScaleTransform(scale, scale));
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<UserControl x:Class="Ink_Canvas.Controls.PptNavBar"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
|
||||
xmlns:local="clr-namespace:Ink_Canvas.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="50" d:DesignWidth="200">
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="PptNavBarItemForeground" Color="#71717a"/>
|
||||
<DataTemplate x:Key="PptPreviewItemTemplate">
|
||||
<Grid Margin="2,4">
|
||||
<Border CornerRadius="8" Background="Transparent" Padding="6">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PptNavBarItemForeground}"
|
||||
Text="{Binding SlideNumber}"/>
|
||||
<Image Grid.Column="1" Source="{Binding Thumbnail}" Stretch="Uniform"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<Style x:Key="PptPreviewItemContainerStyle" TargetType="ListBoxItem">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd" CornerRadius="8" Background="Transparent"
|
||||
BorderBrush="Transparent" BorderThickness="2">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#1affffff"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#66CCFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border x:Name="RootBorder" BorderThickness="1" BorderBrush="#a1a1aa"
|
||||
Background="#f4f4f5" Opacity="1" CornerRadius="6">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="#000000" BlurRadius="14" ShadowDepth="2" Opacity="0.16"/>
|
||||
</Border.Effect>
|
||||
<DockPanel x:Name="LayoutRoot" LastChildFill="True">
|
||||
<ListBox x:Name="PreviewList"
|
||||
Visibility="Collapsed"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Padding="6,8,6,4"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ItemTemplate="{StaticResource PptPreviewItemTemplate}"
|
||||
ItemContainerStyle="{StaticResource PptPreviewItemContainerStyle}"
|
||||
MouseUp="PreviewList_MouseUp"/>
|
||||
<ikw:SimpleStackPanel x:Name="ButtonRow" Orientation="Horizontal"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Border x:Name="PreviousButtonBorder" Width="50" Height="50"
|
||||
MouseDown="PreviousButton_MouseDown"
|
||||
MouseUp="PreviousButton_MouseUp"
|
||||
MouseLeave="PreviousButton_MouseLeave"
|
||||
CornerRadius="5 5 0 0" Background="Transparent">
|
||||
<Grid>
|
||||
<Border x:Name="PreviousButtonFeedbackBorder" Margin="3" CornerRadius="5"
|
||||
Background="#18181b" Opacity="0"/>
|
||||
<Image Height="28" Width="28">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
|
||||
<GeometryDrawing x:Name="PreviousButtonGeometry" Brush="#27272a"/>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border x:Name="PageButtonBorder" Visibility="Visible" TextBlock.Foreground="#171717"
|
||||
MouseDown="PageButton_MouseDown"
|
||||
MouseUp="PageButton_MouseUp"
|
||||
MouseLeave="PageButton_MouseLeave"
|
||||
MinWidth="50" MinHeight="50" Background="Transparent">
|
||||
<Grid>
|
||||
<Border x:Name="PageButtonFeedbackBorder" Margin="0,3" CornerRadius="5"
|
||||
Background="#18181b" Opacity="0"/>
|
||||
<ikw:SimpleStackPanel VerticalAlignment="Center" Orientation="Vertical"
|
||||
Spacing="0.5" Margin="12,0">
|
||||
<TextBlock x:Name="PageNowText" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontSize="17" FontWeight="Bold" Text="?"/>
|
||||
<TextBlock x:Name="PageTotalText" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontSize="10" Text="/ ?"/>
|
||||
</ikw:SimpleStackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border x:Name="NextButtonBorder" Width="50" Height="50"
|
||||
MouseDown="NextButton_MouseDown"
|
||||
MouseUp="NextButton_MouseUp"
|
||||
MouseLeave="NextButton_MouseLeave"
|
||||
CornerRadius="0 0 5 5" Background="Transparent">
|
||||
<Grid>
|
||||
<Border x:Name="NextButtonFeedbackBorder" Margin="3" CornerRadius="5"
|
||||
Background="#18181b" Opacity="0"/>
|
||||
<Image VerticalAlignment="Center" HorizontalAlignment="Center" Height="28" Width="28">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
|
||||
<GeometryDrawing x:Name="NextButtonGeometry" Brush="#27272a"/>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ikw:SimpleStackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,393 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// PPT 翻页 + 增强预览一体化控件。
|
||||
/// 通过 <see cref="Direction"/> 切换底部条 (LB/RB) 与侧边条 (LS/RS) 布局,
|
||||
/// 预览列表内嵌于同一个 Border,展开时占据按钮组之外的剩余空间。
|
||||
/// </summary>
|
||||
public partial class PptNavBar : UserControl
|
||||
{
|
||||
public sealed class PreviewItem
|
||||
{
|
||||
public int SlideNumber { get; set; }
|
||||
public BitmapImage Thumbnail { get; set; }
|
||||
}
|
||||
|
||||
public enum NavDirection
|
||||
{
|
||||
LeftBottom,
|
||||
RightBottom,
|
||||
LeftSide,
|
||||
RightSide
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register(
|
||||
nameof(Direction), typeof(NavDirection), typeof(PptNavBar),
|
||||
new PropertyMetadata(NavDirection.LeftBottom, OnDirectionChanged));
|
||||
|
||||
public static readonly DependencyProperty CurrentSlideProperty = DependencyProperty.Register(
|
||||
nameof(CurrentSlide), typeof(int), typeof(PptNavBar),
|
||||
new PropertyMetadata(0, OnPageChanged));
|
||||
|
||||
public static readonly DependencyProperty TotalSlidesProperty = DependencyProperty.Register(
|
||||
nameof(TotalSlides), typeof(int), typeof(PptNavBar),
|
||||
new PropertyMetadata(0, OnPageChanged));
|
||||
|
||||
public static readonly DependencyProperty PreviewItemsProperty = DependencyProperty.Register(
|
||||
nameof(PreviewItems), typeof(IList<PreviewItem>), typeof(PptNavBar),
|
||||
new PropertyMetadata(null, OnPreviewItemsChanged));
|
||||
|
||||
public static readonly DependencyProperty IsPreviewExpandedProperty = DependencyProperty.Register(
|
||||
nameof(IsPreviewExpanded), typeof(bool), typeof(PptNavBar),
|
||||
new PropertyMetadata(false, OnIsPreviewExpandedChanged));
|
||||
|
||||
public NavDirection Direction
|
||||
{
|
||||
get => (NavDirection)GetValue(DirectionProperty);
|
||||
set => SetValue(DirectionProperty, value);
|
||||
}
|
||||
|
||||
public int CurrentSlide
|
||||
{
|
||||
get => (int)GetValue(CurrentSlideProperty);
|
||||
set => SetValue(CurrentSlideProperty, value);
|
||||
}
|
||||
|
||||
public int TotalSlides
|
||||
{
|
||||
get => (int)GetValue(TotalSlidesProperty);
|
||||
set => SetValue(TotalSlidesProperty, value);
|
||||
}
|
||||
|
||||
public IList<PreviewItem> PreviewItems
|
||||
{
|
||||
get => (IList<PreviewItem>)GetValue(PreviewItemsProperty);
|
||||
set => SetValue(PreviewItemsProperty, value);
|
||||
}
|
||||
|
||||
public bool IsPreviewExpanded
|
||||
{
|
||||
get => (bool)GetValue(IsPreviewExpandedProperty);
|
||||
set => SetValue(IsPreviewExpandedProperty, value);
|
||||
}
|
||||
|
||||
public event EventHandler PreviousClick;
|
||||
public event EventHandler NextClick;
|
||||
public event EventHandler PageClick;
|
||||
public event EventHandler<int> SlideSelected;
|
||||
public event EventHandler PreviousPressedDown;
|
||||
public event EventHandler NextPressedDown;
|
||||
public event EventHandler PressEnded;
|
||||
public event EventHandler<bool> PreviewExpandedChanged;
|
||||
|
||||
// 静态几何(左下/右下:水平箭头;左侧/右侧:垂直箭头)
|
||||
private static readonly Geometry HArrowLeft = Geometry.Parse("F0 M24,24z M0,0z M3.3994,12.9642C2.86687,12.4317,2.86687,11.5683,3.3994,11.0358L9.94485,4.49031C10.4774,3.95777 11.3408,3.95777 11.8733,4.49031 12.4059,5.02284 12.4059,5.88625 11.8733,6.41878L7.65575,10.6364 19.6364,10.6364C20.3895,10.6364 21,11.2469 21,12 21,12.7531 20.3895,13.3636 19.6364,13.3636L7.65575,13.3636 11.8733,17.5812C12.4059,18.1137 12.4059,18.9772 11.8733,19.5097 11.3408,20.0422 10.4774,20.0422 9.94485,19.5097L3.3994,12.9642z");
|
||||
private static readonly Geometry HArrowRight = Geometry.Parse("F0 M24,24z M0,0z M20.6006,12.9642C21.1331,12.4317,21.1331,11.5683,20.6006,11.0358L14.0551,4.49031C13.5226,3.95777 12.6592,3.95777 12.1267,4.49031 11.5941,5.02284 11.5941,5.88625 12.1267,6.41878L16.3443,10.6364 4.36364,10.6364C3.61052,10.6364 3,11.2469 3,12 3,12.7531 3.61052,13.3636 4.36364,13.3636L16.3443,13.3636 12.1267,17.5812C11.5941,18.1137 11.5941,18.9772 12.1267,19.5097 12.6592,20.0422 13.5226,20.0422 14.0551,19.5097L20.6006,12.9642z");
|
||||
private static readonly Geometry VArrowUp = Geometry.Parse("F0 M24,24z M0,0z M11.0357,3.3994C11.5682,2.86687,12.4316,2.86687,12.9641,3.3994L19.5096,9.94485C20.0421,10.4774 20.0421,11.3408 19.5096,11.8733 18.9771,12.4059 18.1137,12.4059 17.5811,11.8733L13.3635,7.65575 13.3635,19.6364C13.3635,20.3895 12.753,21 11.9999,21 11.2468,21 10.6363,20.3895 10.6363,19.6364L10.6363,7.65575 6.41869,11.8733C5.88616,12.4059 5.02275,12.4059 4.49022,11.8733 3.95769,11.3408 3.95769,10.4774 4.49022,9.94485L11.0357,3.3994z");
|
||||
private static readonly Geometry VArrowDown = Geometry.Parse("F0 M24,24z M0,0z M11.0357,20.6006C11.5682,21.1331,12.4316,21.1331,12.9641,20.6006L19.5096,14.0551C20.0421,13.5226 20.0421,12.6592 19.5096,12.1267 18.9771,11.5941 18.1137,11.5941 17.5811,12.1267L13.3635,16.3443 13.3635,4.36364C13.3635,3.61052 12.753,3 11.9999,3 11.2468,3 10.6363,3.61052 10.6363,4.36364L10.6363,16.3443 6.41869,12.1267C5.88616,11.5941 5.02275,11.5941 4.49022,12.1267 3.95769,12.6592 3.95769,13.5226 4.49022,14.0551L11.0357,20.6006z");
|
||||
|
||||
public PptNavBar()
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplyDirection(Direction);
|
||||
}
|
||||
|
||||
private static void OnDirectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is PptNavBar bar) bar.ApplyDirection((NavDirection)e.NewValue);
|
||||
}
|
||||
|
||||
private static void OnPageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is PptNavBar bar) bar.RefreshPageText();
|
||||
}
|
||||
|
||||
private static void OnPreviewItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is PptNavBar bar)
|
||||
{
|
||||
bar.PreviewList.ItemsSource = e.NewValue as IList<PreviewItem>;
|
||||
bar.SyncPreviewSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnIsPreviewExpandedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is PptNavBar bar)
|
||||
{
|
||||
bool expanded = (bool)e.NewValue;
|
||||
bar.PreviewList.Visibility = expanded ? Visibility.Visible : Visibility.Collapsed;
|
||||
bar.ApplyLayout();
|
||||
if (expanded)
|
||||
{
|
||||
bar.SyncPreviewSelection();
|
||||
bar.HookOutsideClick();
|
||||
}
|
||||
else
|
||||
{
|
||||
bar.UnhookOutsideClick();
|
||||
}
|
||||
bar.PreviewExpandedChanged?.Invoke(bar, expanded);
|
||||
}
|
||||
}
|
||||
|
||||
private Window _hookedWindow;
|
||||
private void HookOutsideClick()
|
||||
{
|
||||
if (_hookedWindow != null) return;
|
||||
_hookedWindow = Window.GetWindow(this);
|
||||
if (_hookedWindow != null)
|
||||
{
|
||||
_hookedWindow.PreviewMouseDown += OnWindowPreviewMouseDown;
|
||||
}
|
||||
}
|
||||
private void UnhookOutsideClick()
|
||||
{
|
||||
if (_hookedWindow != null)
|
||||
{
|
||||
_hookedWindow.PreviewMouseDown -= OnWindowPreviewMouseDown;
|
||||
_hookedWindow = null;
|
||||
}
|
||||
}
|
||||
private void OnWindowPreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is DependencyObject d && !IsDescendantOf(d, this))
|
||||
{
|
||||
IsPreviewExpanded = false;
|
||||
}
|
||||
}
|
||||
private static bool IsDescendantOf(DependencyObject child, DependencyObject ancestor)
|
||||
{
|
||||
while (child != null)
|
||||
{
|
||||
if (ReferenceEquals(child, ancestor)) return true;
|
||||
child = System.Windows.Media.VisualTreeHelper.GetParent(child)
|
||||
?? System.Windows.LogicalTreeHelper.GetParent(child);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ApplyDirection(NavDirection dir) => ApplyLayout();
|
||||
|
||||
private void ApplyLayout()
|
||||
{
|
||||
var dir = Direction;
|
||||
bool expanded = IsPreviewExpanded;
|
||||
|
||||
// 重置可能在不同状态下被设置的属性
|
||||
ButtonRow.ClearValue(WidthProperty);
|
||||
ButtonRow.ClearValue(HeightProperty);
|
||||
ButtonRow.ClearValue(HorizontalAlignmentProperty);
|
||||
PreviewList.ClearValue(WidthProperty);
|
||||
PreviewList.ClearValue(HeightProperty);
|
||||
PreviewList.ClearValue(MaxHeightProperty);
|
||||
PreviewList.ClearValue(MaxWidthProperty);
|
||||
PreviewList.ClearValue(HorizontalAlignmentProperty);
|
||||
ClearValue(HeightProperty);
|
||||
ClearValue(MaxHeightProperty);
|
||||
|
||||
double availableHeight = ComputeAvailableHeight();
|
||||
|
||||
switch (dir)
|
||||
{
|
||||
case NavDirection.LeftBottom:
|
||||
case NavDirection.RightBottom:
|
||||
DockPanel.SetDock(PreviewList, Dock.Top);
|
||||
DockPanel.SetDock(ButtonRow, Dock.Bottom);
|
||||
ButtonRow.Orientation = Orientation.Horizontal;
|
||||
ButtonRow.Height = 50;
|
||||
if (expanded)
|
||||
{
|
||||
// 预览面板拉宽到 280,贴向同侧角落
|
||||
PreviewList.Width = 280;
|
||||
PreviewList.MaxHeight = Math.Max(200, availableHeight - 50);
|
||||
PreviewList.HorizontalAlignment = dir == NavDirection.LeftBottom
|
||||
? HorizontalAlignment.Left
|
||||
: HorizontalAlignment.Right;
|
||||
// 按钮组宽度限制为原始内容宽度,并贴向同侧,保持按钮位置不变
|
||||
ButtonRow.HorizontalAlignment = dir == NavDirection.LeftBottom
|
||||
? HorizontalAlignment.Left
|
||||
: HorizontalAlignment.Right;
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviewList.SetBinding(WidthProperty, new System.Windows.Data.Binding(nameof(ButtonRow.ActualWidth)) { Source = ButtonRow });
|
||||
PreviewList.MaxHeight = 380;
|
||||
}
|
||||
PreviousButtonGeometry.Geometry = HArrowLeft;
|
||||
NextButtonGeometry.Geometry = HArrowRight;
|
||||
break;
|
||||
|
||||
case NavDirection.LeftSide:
|
||||
DockPanel.SetDock(PreviewList, Dock.Right);
|
||||
DockPanel.SetDock(ButtonRow, Dock.Left);
|
||||
ButtonRow.Orientation = Orientation.Vertical;
|
||||
ButtonRow.Width = 50;
|
||||
PreviewList.Width = 240;
|
||||
PreviewList.MaxHeight = 480;
|
||||
PreviousButtonGeometry.Geometry = VArrowUp;
|
||||
NextButtonGeometry.Geometry = VArrowDown;
|
||||
break;
|
||||
case NavDirection.RightSide:
|
||||
DockPanel.SetDock(PreviewList, Dock.Left);
|
||||
DockPanel.SetDock(ButtonRow, Dock.Right);
|
||||
ButtonRow.Orientation = Orientation.Vertical;
|
||||
ButtonRow.Width = 50;
|
||||
PreviewList.Width = 240;
|
||||
PreviewList.MaxHeight = 480;
|
||||
PreviousButtonGeometry.Geometry = VArrowUp;
|
||||
NextButtonGeometry.Geometry = VArrowDown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private double ComputeAvailableHeight()
|
||||
{
|
||||
var window = Window.GetWindow(this);
|
||||
double h = window != null ? window.ActualHeight : SystemParameters.PrimaryScreenHeight;
|
||||
return Math.Max(240, h - 12);
|
||||
}
|
||||
|
||||
private void RefreshPageText()
|
||||
{
|
||||
if (CurrentSlide > 0 && TotalSlides > 0)
|
||||
{
|
||||
PageNowText.Text = CurrentSlide.ToString();
|
||||
PageTotalText.Text = $"/ {TotalSlides}";
|
||||
}
|
||||
else
|
||||
{
|
||||
PageNowText.Text = "?";
|
||||
PageTotalText.Text = "/ ?";
|
||||
}
|
||||
SyncPreviewSelection();
|
||||
}
|
||||
|
||||
private void SyncPreviewSelection()
|
||||
{
|
||||
if (PreviewItems == null || CurrentSlide <= 0) return;
|
||||
foreach (var item in PreviewItems)
|
||||
{
|
||||
if (item.SlideNumber == CurrentSlide)
|
||||
{
|
||||
PreviewList.SelectedItem = item;
|
||||
PreviewList.ScrollIntoView(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetFeedback(Border feedback, double opacity) => feedback.Opacity = opacity;
|
||||
|
||||
private object _lastDown;
|
||||
|
||||
private void PreviousButton_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_lastDown = sender;
|
||||
SetFeedback(PreviousButtonFeedbackBorder, 0.15);
|
||||
PreviousPressedDown?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void PreviousButton_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
SetFeedback(PreviousButtonFeedbackBorder, 0);
|
||||
PressEnded?.Invoke(this, EventArgs.Empty);
|
||||
if (_lastDown != sender) return;
|
||||
_lastDown = null;
|
||||
PreviousClick?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void PreviousButton_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
SetFeedback(PreviousButtonFeedbackBorder, 0);
|
||||
_lastDown = null;
|
||||
PressEnded?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void NextButton_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_lastDown = sender;
|
||||
SetFeedback(NextButtonFeedbackBorder, 0.15);
|
||||
NextPressedDown?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void NextButton_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
SetFeedback(NextButtonFeedbackBorder, 0);
|
||||
PressEnded?.Invoke(this, EventArgs.Empty);
|
||||
if (_lastDown != sender) return;
|
||||
_lastDown = null;
|
||||
NextClick?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void NextButton_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
SetFeedback(NextButtonFeedbackBorder, 0);
|
||||
_lastDown = null;
|
||||
PressEnded?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void PageButton_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_lastDown = sender;
|
||||
SetFeedback(PageButtonFeedbackBorder, 0.15);
|
||||
}
|
||||
|
||||
private void PageButton_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
SetFeedback(PageButtonFeedbackBorder, 0);
|
||||
if (_lastDown != sender) return;
|
||||
_lastDown = null;
|
||||
PageClick?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void PageButton_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
SetFeedback(PageButtonFeedbackBorder, 0);
|
||||
_lastDown = null;
|
||||
}
|
||||
|
||||
private void PreviewList_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (PreviewList.SelectedItem is PreviewItem item)
|
||||
{
|
||||
SlideSelected?.Invoke(this, item.SlideNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyTheme(bool isDark)
|
||||
{
|
||||
var fgBrush = isDark ? Brushes.White : new SolidColorBrush(Color.FromRgb(39, 39, 42));
|
||||
var feedbackBrush = isDark ? Brushes.White : new SolidColorBrush(Color.FromRgb(24, 24, 27));
|
||||
var bgBrush = isDark
|
||||
? new SolidColorBrush(Color.FromRgb(39, 39, 42))
|
||||
: new SolidColorBrush(Color.FromRgb(244, 244, 245));
|
||||
var borderBrush = isDark
|
||||
? new SolidColorBrush(Color.FromRgb(82, 82, 91))
|
||||
: new SolidColorBrush(Color.FromRgb(161, 161, 170));
|
||||
|
||||
PreviousButtonGeometry.Brush = fgBrush;
|
||||
NextButtonGeometry.Brush = fgBrush;
|
||||
PreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
NextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
PageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
PageNowText.Foreground = fgBrush;
|
||||
PageTotalText.Foreground = fgBrush;
|
||||
RootBorder.Background = bgBrush;
|
||||
RootBorder.BorderBrush = borderBrush;
|
||||
Resources["PptNavBarItemForeground"] = fgBrush;
|
||||
}
|
||||
|
||||
public void SetPageButtonVisibility(Visibility v) => PageButtonBorder.Visibility = v;
|
||||
public void SetBarOpacity(double opacity) => RootBorder.Opacity = opacity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// 工具栏按钮插件与宿主之间的桥梁。Phase 1 粗粒度暴露 MainWindow,后续收窄。
|
||||
/// </summary>
|
||||
public interface IToolbarHost
|
||||
{
|
||||
MainWindow Window { get; }
|
||||
|
||||
/// <summary>按 id 登记按钮的 view 实例(供 MainWindow 字段回填和互相查找)。</summary>
|
||||
void RegisterView(string id, FrameworkElement view);
|
||||
|
||||
/// <summary>按 id 获取之前注册的 view。不存在返回 null。</summary>
|
||||
FrameworkElement FindView(string id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// 一个工具栏按钮(或任意浮动栏/白板栏条目)的插件化契约。
|
||||
/// 实现类必须有无参构造函数,启动时会被 ToolbarRegistry 反射实例化。
|
||||
/// </summary>
|
||||
public interface IToolbarItem
|
||||
{
|
||||
/// <summary>稳定、唯一的 id,用于持久化用户配置。不要随便改。</summary>
|
||||
string Id { get; }
|
||||
|
||||
ToolbarSlot DefaultSlot { get; }
|
||||
|
||||
/// <summary>同一 slot 内的默认顺序,小的在前。</summary>
|
||||
int DefaultOrder { get; }
|
||||
|
||||
bool DefaultVisible { get; }
|
||||
|
||||
ToolbarInsertPosition DefaultPosition { get; }
|
||||
|
||||
/// <summary>仅当 Position 为 BeforeAnchor/AfterAnchor 时有意义,对应 XAML 里 x:Name。</summary>
|
||||
string DefaultAnchorName { get; }
|
||||
|
||||
/// <summary>构造 UI 元素并接线所有行为。</summary>
|
||||
FrameworkElement BuildView(IToolbarHost host);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
/// <summary>
|
||||
/// 清空按钮。位置:夹在颜色面板与 StackPanelCanvasControls 之间,
|
||||
/// 所以用 BeforeAnchor 锚到 StackPanelCanvasControls。
|
||||
/// </summary>
|
||||
internal sealed class ClearToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.clear";
|
||||
public override string LocalizationKey => "FloatingBar_Clear";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
|
||||
public override int DefaultOrder => 0;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.BeforeAnchor;
|
||||
public override string DefaultAnchorName => "StackPanelCanvasControls";
|
||||
|
||||
protected override string IconBrushResourceKey => "RedBrush";
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconDelete_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachSymbolIconDelete(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class CursorToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.cursor";
|
||||
public override string LocalizationKey => "FloatingBar_Mouse";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
|
||||
public override int DefaultOrder => 100;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.CursorIcon_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachCursorIconView(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class CursorWithDelToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.cursorWithDel";
|
||||
public override string LocalizationKey => "FloatingBar_ClearAndMouse";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 320;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.CursorWithDelIcon_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachCursorWithDelBtn(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class EraserByStrokesToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.eraserByStrokes";
|
||||
public override string LocalizationKey => "FloatingBar_StrokeEraser";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 110;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.EraserIconByStrokes_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachEraserByStrokesIcon(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class EraserToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.eraser";
|
||||
public override string LocalizationKey => "FloatingBar_AreaEraser";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 100;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.EraserIcon_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachEraserIcon(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class FoldToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.fold";
|
||||
public override string LocalizationKey => "FloatingBar_Hide";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
|
||||
public override int DefaultOrder => 120;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
|
||||
public override string DefaultAnchorName => "FloatingBarEndSeparator";
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.FoldFloatingBar_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachFoldIcon(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class PenToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.pen";
|
||||
public override string LocalizationKey => "FloatingBar_Annotate";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
|
||||
public override int DefaultOrder => 110;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.PenIcon_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachPenIconView(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class RedoToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.redo";
|
||||
public override string LocalizationKey => "Board_Redo";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 310;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconRedo_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
{
|
||||
host.Window.AttachSymbolIconRedo(view);
|
||||
view.SetBinding(System.Windows.UIElement.IsEnabledProperty,
|
||||
new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnRedo" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class SelectToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.select";
|
||||
public override string LocalizationKey => "FloatingBar_LassoSelect";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 120;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconSelect_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachSymbolIconSelect(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class ShapeDrawToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.shapeDraw";
|
||||
public override string LocalizationKey => "FloatingBar_Geometry";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 130;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.ImageDrawShape_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachShapeDrawBtn(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Ink_Canvas.Properties;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用 ToolbarImageButton 工具栏条目基类——大幅减少每个按钮的样板代码。
|
||||
/// 派生类通常只需给 Id / 本地化键 / Slot / Order / 点击处理 / Attach 回填。
|
||||
/// </summary>
|
||||
internal abstract class ToolbarImageButtonItemBase : IToolbarItem
|
||||
{
|
||||
public abstract string Id { get; }
|
||||
public abstract string LocalizationKey { get; }
|
||||
public abstract ToolbarSlot DefaultSlot { get; }
|
||||
public abstract int DefaultOrder { get; }
|
||||
public virtual bool DefaultVisible => true;
|
||||
public virtual ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Prepend;
|
||||
public virtual string DefaultAnchorName => null;
|
||||
|
||||
/// <summary>DynamicResource 名称,用于 IconBrush。默认为 null(使用控件自带前景色)。</summary>
|
||||
protected virtual string IconBrushResourceKey => null;
|
||||
|
||||
protected abstract void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e);
|
||||
|
||||
/// <summary>构建后调用,用于回填 MainWindow 的原命名属性(partial 扩展里的 Attach*)。可选。</summary>
|
||||
protected virtual void AfterBuild(IToolbarHost host, ToolbarImageButton view) { }
|
||||
|
||||
public FrameworkElement BuildView(IToolbarHost host)
|
||||
{
|
||||
var btn = new ToolbarImageButton
|
||||
{
|
||||
Label = Strings.GetString(LocalizationKey) ?? LocalizationKey
|
||||
};
|
||||
if (!string.IsNullOrEmpty(IconBrushResourceKey))
|
||||
{
|
||||
if (btn.TryFindResource(IconBrushResourceKey) is Brush brush) btn.IconBrush = brush;
|
||||
else btn.SetResourceReference(ToolbarImageButton.IconBrushProperty, IconBrushResourceKey);
|
||||
}
|
||||
btn.ButtonMouseUp += (s, e) => OnClick(host, s, e);
|
||||
AfterBuild(host, btn);
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class ToolsToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.tools";
|
||||
public override string LocalizationKey => "Board_Tools";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
|
||||
public override int DefaultOrder => 110;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
|
||||
public override string DefaultAnchorName => "FloatingBarEndSeparator";
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconTools_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachToolsBtn(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class UndoToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.undo";
|
||||
public override string LocalizationKey => "Board_Undo";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 300;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconUndo_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
{
|
||||
host.Window.AttachSymbolIconUndo(view);
|
||||
view.SetBinding(System.Windows.UIElement.IsEnabledProperty,
|
||||
new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnUndo" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class WhiteboardToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.whiteboard";
|
||||
public override string LocalizationKey => "FloatingBar_Whiteboard";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
|
||||
public override int DefaultOrder => 100;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
|
||||
public override string DefaultAnchorName => "FloatingBarEndSeparator";
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.ImageBlackboard_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachWhiteboardBtn(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// MainWindow 版的 IToolbarHost 实现。Phase 1 直接把 MainWindow 引用暴露给插件,
|
||||
/// 插件可通过 host.Window 访问私有/内部成员(partial class 扩展或 internal 字段)。
|
||||
/// 后续阶段逐步把具体行为抽成 Host 上的方法/事件,收窄这个接口。
|
||||
/// </summary>
|
||||
public sealed class ToolbarHost : IToolbarHost
|
||||
{
|
||||
private readonly Dictionary<string, FrameworkElement> _views = new Dictionary<string, FrameworkElement>();
|
||||
|
||||
public ToolbarHost(MainWindow window)
|
||||
{
|
||||
Window = window;
|
||||
}
|
||||
|
||||
public MainWindow Window { get; }
|
||||
|
||||
public void RegisterView(string id, FrameworkElement view)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id) || view == null) return;
|
||||
_views[id] = view;
|
||||
}
|
||||
|
||||
public FrameworkElement FindView(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return null;
|
||||
return _views.TryGetValue(id, out var v) ? v : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
public enum ToolbarInsertPosition
|
||||
{
|
||||
/// <summary>从容器头部依次插入;Order 小的在前。</summary>
|
||||
Prepend,
|
||||
/// <summary>追加到容器末尾。</summary>
|
||||
Append,
|
||||
/// <summary>插入到由 AnchorName 指定的已有元素之前。</summary>
|
||||
BeforeAnchor,
|
||||
/// <summary>插入到由 AnchorName 指定的已有元素之后(同一锚点多项按 Order 依次排列)。</summary>
|
||||
AfterAnchor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个工具栏按钮的用户配置(可见性、顺序、所属 slot、插入位置)。
|
||||
/// 由 Settings.Toolbar 持久化。
|
||||
/// </summary>
|
||||
public class ToolbarItemConfig
|
||||
{
|
||||
[JsonProperty("visible")]
|
||||
public bool Visible { get; set; } = true;
|
||||
|
||||
[JsonProperty("order")]
|
||||
public int Order { get; set; }
|
||||
|
||||
[JsonProperty("slot")]
|
||||
public ToolbarSlot Slot { get; set; } = ToolbarSlot.FloatingBarMain;
|
||||
|
||||
[JsonProperty("position")]
|
||||
public ToolbarInsertPosition Position { get; set; } = ToolbarInsertPosition.Prepend;
|
||||
|
||||
[JsonProperty("anchorName")]
|
||||
public string AnchorName { get; set; }
|
||||
}
|
||||
|
||||
public class ToolbarLayoutSettings
|
||||
{
|
||||
[JsonProperty("items")]
|
||||
public Dictionary<string, ToolbarItemConfig> Items { get; set; } = new Dictionary<string, ToolbarItemConfig>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Ink_Canvas.Helpers;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描当前程序集里的 IToolbarItem 实现,按用户配置(Settings.Toolbar)排序/过滤后注入到目标容器。
|
||||
/// </summary>
|
||||
public static class ToolbarRegistry
|
||||
{
|
||||
private static List<IToolbarItem> _items;
|
||||
|
||||
public static IReadOnlyList<IToolbarItem> Discover()
|
||||
{
|
||||
if (_items != null) return _items;
|
||||
|
||||
var itemType = typeof(IToolbarItem);
|
||||
_items = Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(t => !t.IsAbstract && !t.IsInterface && itemType.IsAssignableFrom(t))
|
||||
.Select(t =>
|
||||
{
|
||||
try { return (IToolbarItem)Activator.CreateInstance(t); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ToolbarRegistry: 实例化 {t.FullName} 失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(i => i != null)
|
||||
.ToList();
|
||||
return _items;
|
||||
}
|
||||
|
||||
/// <summary>按 slot 分配工具栏条目到对应容器。调用者负责清空目标容器里要被接管的旧内容。</summary>
|
||||
public static void Populate(IToolbarHost host, IDictionary<ToolbarSlot, Panel> slots, ToolbarLayoutSettings layout)
|
||||
{
|
||||
if (host == null || slots == null) return;
|
||||
layout = layout ?? new ToolbarLayoutSettings();
|
||||
|
||||
var grouped = new Dictionary<ToolbarSlot, List<(IToolbarItem item, ToolbarItemConfig cfg)>>();
|
||||
foreach (var item in Discover())
|
||||
{
|
||||
if (!layout.Items.TryGetValue(item.Id, out var cfg))
|
||||
{
|
||||
cfg = new ToolbarItemConfig
|
||||
{
|
||||
Visible = item.DefaultVisible,
|
||||
Order = item.DefaultOrder,
|
||||
Slot = item.DefaultSlot,
|
||||
Position = item.DefaultPosition,
|
||||
AnchorName = item.DefaultAnchorName
|
||||
};
|
||||
}
|
||||
if (!cfg.Visible) continue;
|
||||
if (!grouped.TryGetValue(cfg.Slot, out var list))
|
||||
{
|
||||
list = new List<(IToolbarItem, ToolbarItemConfig)>();
|
||||
grouped[cfg.Slot] = list;
|
||||
}
|
||||
list.Add((item, cfg));
|
||||
}
|
||||
|
||||
foreach (var kv in grouped)
|
||||
{
|
||||
if (!slots.TryGetValue(kv.Key, out var container) || container == null) continue;
|
||||
InjectIntoContainer(host, container, kv.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InjectIntoContainer(IToolbarHost host, Panel container,
|
||||
List<(IToolbarItem item, ToolbarItemConfig cfg)> entries)
|
||||
{
|
||||
// 按 Position 分桶,每桶内按 Order 升序。
|
||||
var prepend = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Prepend).OrderBy(e => e.cfg.Order).ToList();
|
||||
var append = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Append).OrderBy(e => e.cfg.Order).ToList();
|
||||
var before = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.BeforeAnchor).ToList();
|
||||
var after = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.AfterAnchor).ToList();
|
||||
|
||||
var prependIndex = 0;
|
||||
foreach (var entry in prepend)
|
||||
{
|
||||
var view = BuildAndRegister(host, entry.item);
|
||||
if (view == null) continue;
|
||||
container.Children.Insert(prependIndex++, view);
|
||||
}
|
||||
|
||||
foreach (var entry in append)
|
||||
{
|
||||
var view = BuildAndRegister(host, entry.item);
|
||||
if (view == null) continue;
|
||||
container.Children.Add(view);
|
||||
}
|
||||
|
||||
foreach (var group in before.GroupBy(e => e.cfg.AnchorName))
|
||||
{
|
||||
var anchor = FindNamedChild(container, group.Key);
|
||||
if (anchor == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (BeforeAnchor)", LogHelper.LogType.Warning);
|
||||
continue;
|
||||
}
|
||||
var idx = container.Children.IndexOf(anchor);
|
||||
foreach (var entry in group.OrderBy(e => e.cfg.Order))
|
||||
{
|
||||
var view = BuildAndRegister(host, entry.item);
|
||||
if (view == null) continue;
|
||||
container.Children.Insert(idx++, view);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var group in after.GroupBy(e => e.cfg.AnchorName))
|
||||
{
|
||||
var anchor = FindNamedChild(container, group.Key);
|
||||
if (anchor == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (AfterAnchor)", LogHelper.LogType.Warning);
|
||||
continue;
|
||||
}
|
||||
var idx = container.Children.IndexOf(anchor) + 1;
|
||||
foreach (var entry in group.OrderBy(e => e.cfg.Order))
|
||||
{
|
||||
var view = BuildAndRegister(host, entry.item);
|
||||
if (view == null) continue;
|
||||
container.Children.Insert(idx++, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static UIElement FindNamedChild(Panel container, string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return null;
|
||||
foreach (UIElement child in container.Children)
|
||||
{
|
||||
if (child is FrameworkElement fe && fe.Name == name) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static FrameworkElement BuildAndRegister(IToolbarHost host, IToolbarItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
var view = item.BuildView(host);
|
||||
if (view == null) return null;
|
||||
host.RegisterView(item.Id, view);
|
||||
return view;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ToolbarRegistry: 构建 {item.Id} 失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
public enum ToolbarSlot
|
||||
{
|
||||
FloatingBarMain,
|
||||
FloatingBarCanvasControls,
|
||||
FloatingBarEnd,
|
||||
BlackboardLeft,
|
||||
BlackboardRight
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX86Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinArm64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCompression" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCleanup" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableEventSubscription" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UnmanagedWinX86Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UnmanagedWinX64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UnmanagedWinArm64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
@@ -11,7 +12,7 @@ using System.Windows.Threading;
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步硬件加速的墨迹平滑处理器
|
||||
/// 改进的异步硬件加速墨迹平滑处理器,使用优化的三次贝塞尔曲线拟合
|
||||
/// </summary>
|
||||
public class AsyncAdvancedBezierSmoothing
|
||||
{
|
||||
@@ -26,11 +27,13 @@ namespace Ink_Canvas.Helpers
|
||||
_processingTasks = new ConcurrentDictionary<Stroke, CancellationTokenSource>();
|
||||
}
|
||||
|
||||
public double SmoothingStrength { get; set; } = 0.3; // 大幅降低强度
|
||||
public double ResampleInterval { get; set; } = 3.0; // 大幅增加间隔减少点数
|
||||
public int InterpolationSteps { get; set; } = 8; // 从4增加到8,提高插值步数
|
||||
public double SmoothingStrength { get; set; } = 0.4; // 适中的平滑强度
|
||||
public double ResampleInterval { get; set; } = 2.5; // 适中的重采样间隔
|
||||
public int InterpolationSteps { get; set; } = 12; // 增加插值步数提高精度
|
||||
public bool UseHardwareAcceleration { get; set; } = true;
|
||||
public int MaxConcurrentTasks { get; set; } = Environment.ProcessorCount;
|
||||
public bool UseAdaptiveInterpolation { get; set; } = true; // 自适应插值
|
||||
public double CurveTension { get; set; } = 0.3; // 曲线张力参数
|
||||
|
||||
/// <summary>
|
||||
/// 异步平滑笔画
|
||||
@@ -89,15 +92,26 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 简化处理:只进行轻度平滑,避免点数爆炸
|
||||
var smoothedPoints = ApplyLightSmoothing(originalPoints);
|
||||
// 使用改进的贝塞尔曲线拟合
|
||||
var smoothedPoints = ApplyImprovedBezierSmoothing(originalPoints);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 原始点数={originalPoints.Length}, 平滑后点数={smoothedPoints.Length}");
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 确保点数不会过多
|
||||
if (smoothedPoints.Length > originalPoints.Length * 2)
|
||||
// 放宽点数限制
|
||||
if (smoothedPoints.Length > originalPoints.Length * 3.0)
|
||||
{
|
||||
// 如果点数增加太多,回退到原始笔画
|
||||
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 点数过多,进行重采样");
|
||||
// 如果点数增加太多,进行重采样
|
||||
smoothedPoints = ResampleEquidistantOptimized(smoothedPoints, ResampleInterval);
|
||||
}
|
||||
|
||||
// 进一步放宽最终检查
|
||||
if (smoothedPoints.Length > originalPoints.Length * 2.5)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 重采样后点数仍然过多,返回原始笔画");
|
||||
// 如果仍然太多点,使用原始笔画
|
||||
return stroke;
|
||||
}
|
||||
|
||||
@@ -107,38 +121,404 @@ namespace Ink_Canvas.Helpers
|
||||
DrawingAttributes = stroke.DrawingAttributes.Clone()
|
||||
};
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 成功创建平滑笔画");
|
||||
return smoothedStroke;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轻度平滑处理,避免点数爆炸
|
||||
/// 改进的贝塞尔曲线平滑处理
|
||||
/// </summary>
|
||||
private StylusPoint[] ApplyLightSmoothing(StylusPoint[] points)
|
||||
private StylusPoint[] ApplyImprovedBezierSmoothing(StylusPoint[] points)
|
||||
{
|
||||
if (points.Length < 3) return points;
|
||||
if (points.Length < 6) return points; // 5次贝塞尔需要6个点
|
||||
|
||||
var result = new List<StylusPoint>();
|
||||
result.Add(points[0]); // 保持第一个点
|
||||
|
||||
// 简单的3点平均平滑
|
||||
for (int i = 1; i < points.Length - 1; i++)
|
||||
// 添加第一个点
|
||||
result.Add(points[0]);
|
||||
|
||||
// 使用5次贝塞尔曲线,每次移动1个点确保连续性
|
||||
for (int i = 0; i < points.Length - 5; i++)
|
||||
{
|
||||
var prev = points[i - 1];
|
||||
var curr = points[i];
|
||||
var next = points[i + 1];
|
||||
var p0 = points[i];
|
||||
var p1 = points[i + 1];
|
||||
var p2 = points[i + 2];
|
||||
var p3 = points[i + 3];
|
||||
var p4 = points[i + 4];
|
||||
var p5 = points[i + 5];
|
||||
|
||||
// 3点平均
|
||||
double x = (prev.X + curr.X + next.X) / 3.0;
|
||||
double y = (prev.Y + curr.Y + next.Y) / 3.0;
|
||||
float pressure = (prev.PressureFactor + curr.PressureFactor + next.PressureFactor) / 3.0f;
|
||||
// 计算5次贝塞尔的控制点
|
||||
var controlPoints = CalculateQuinticControlPoints(p0, p1, p2, p3, p4, p5);
|
||||
|
||||
result.Add(new StylusPoint(x, y, Math.Max(pressure, 0.1f)));
|
||||
// 生成插值点
|
||||
if (i == 0)
|
||||
{
|
||||
// 第一个窗口:生成更多插值点
|
||||
for (int j = 1; j <= 4; j++)
|
||||
{
|
||||
double t = (double)j / 5;
|
||||
var bezierPoint = CalculateQuinticBezierPoint(p0, controlPoints, p5, t);
|
||||
result.Add(bezierPoint);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 后续窗口:只生成最后一个插值点,避免重复
|
||||
double t = 4.0 / 5.0; // 只取最后一个插值点
|
||||
var bezierPoint = CalculateQuinticBezierPoint(p0, controlPoints, p5, t);
|
||||
result.Add(bezierPoint);
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(points[points.Length - 1]); // 保持最后一个点
|
||||
// 添加最后一个点
|
||||
result.Add(points[points.Length - 1]);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"ApplyImprovedBezierSmoothing: 原始点数={points.Length}, 生成点数={result.Count}");
|
||||
|
||||
// 使用更宽松的去重
|
||||
return RemoveDuplicatePointsLoose(result.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 5次贝塞尔曲线控制点计算
|
||||
/// </summary>
|
||||
private (Point cp1, Point cp2, Point cp3, Point cp4) CalculateQuinticControlPoints(
|
||||
StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3, StylusPoint p4, StylusPoint p5)
|
||||
{
|
||||
// 计算控制点距离(基于相邻点距离)
|
||||
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
|
||||
double dist2 = Math.Sqrt((p2.X - p1.X) * (p2.X - p1.X) + (p2.Y - p1.Y) * (p2.Y - p1.Y));
|
||||
double dist3 = Math.Sqrt((p4.X - p3.X) * (p4.X - p3.X) + (p4.Y - p3.Y) * (p4.Y - p3.Y));
|
||||
double dist4 = Math.Sqrt((p5.X - p4.X) * (p5.X - p4.X) + (p5.Y - p4.Y) * (p5.Y - p4.Y));
|
||||
|
||||
// 使用更小的控制点距离,产生超平滑的曲线
|
||||
double controlDist1 = dist1 * 0.15;
|
||||
double controlDist2 = dist2 * 0.15;
|
||||
double controlDist3 = dist3 * 0.15;
|
||||
double controlDist4 = dist4 * 0.15;
|
||||
|
||||
// 计算控制点方向 - 使用更平滑的方向计算
|
||||
double dir1X = p2.X - p0.X;
|
||||
double dir1Y = p2.Y - p0.Y;
|
||||
double dir2X = p3.X - p1.X;
|
||||
double dir2Y = p3.Y - p1.Y;
|
||||
double dir3X = p4.X - p2.X;
|
||||
double dir3Y = p4.Y - p2.Y;
|
||||
double dir4X = p5.X - p3.X;
|
||||
double dir4Y = p5.Y - p3.Y;
|
||||
|
||||
// 归一化方向
|
||||
NormalizeVector(ref dir1X, ref dir1Y);
|
||||
NormalizeVector(ref dir2X, ref dir2Y);
|
||||
NormalizeVector(ref dir3X, ref dir3Y);
|
||||
NormalizeVector(ref dir4X, ref dir4Y);
|
||||
|
||||
// 计算控制点
|
||||
var cp1 = new Point(p1.X + dir1X * controlDist1, p1.Y + dir1Y * controlDist1);
|
||||
var cp2 = new Point(p2.X + dir2X * controlDist2, p2.Y + dir2Y * controlDist2);
|
||||
var cp3 = new Point(p3.X - dir3X * controlDist3, p3.Y - dir3Y * controlDist3);
|
||||
var cp4 = new Point(p4.X - dir4X * controlDist4, p4.Y - dir4Y * controlDist4);
|
||||
|
||||
return (cp1, cp2, cp3, cp4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 归一化向量
|
||||
/// </summary>
|
||||
private void NormalizeVector(ref double x, ref double y)
|
||||
{
|
||||
double length = Math.Sqrt(x * x + y * y);
|
||||
if (length > 0)
|
||||
{
|
||||
x /= length;
|
||||
y /= length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 5次贝塞尔曲线点计算
|
||||
/// </summary>
|
||||
private StylusPoint CalculateQuinticBezierPoint(StylusPoint p0, (Point cp1, Point cp2, Point cp3, Point cp4) controlPoints, StylusPoint p5, double t)
|
||||
{
|
||||
double oneMinusT = 1 - t;
|
||||
double oneMinusT2 = oneMinusT * oneMinusT;
|
||||
double oneMinusT3 = oneMinusT2 * oneMinusT;
|
||||
double oneMinusT4 = oneMinusT3 * oneMinusT;
|
||||
double oneMinusT5 = oneMinusT4 * oneMinusT;
|
||||
|
||||
double t2 = t * t;
|
||||
double t3 = t2 * t;
|
||||
double t4 = t3 * t;
|
||||
double t5 = t4 * t;
|
||||
|
||||
// 5次贝塞尔曲线公式
|
||||
double x = oneMinusT5 * p0.X +
|
||||
5 * oneMinusT4 * t * controlPoints.cp1.X +
|
||||
10 * oneMinusT3 * t2 * controlPoints.cp2.X +
|
||||
10 * oneMinusT2 * t3 * controlPoints.cp3.X +
|
||||
5 * oneMinusT * t4 * controlPoints.cp4.X +
|
||||
t5 * p5.X;
|
||||
|
||||
double y = oneMinusT5 * p0.Y +
|
||||
5 * oneMinusT4 * t * controlPoints.cp1.Y +
|
||||
10 * oneMinusT3 * t2 * controlPoints.cp2.Y +
|
||||
10 * oneMinusT2 * t3 * controlPoints.cp3.Y +
|
||||
5 * oneMinusT * t4 * controlPoints.cp4.Y +
|
||||
t5 * p5.Y;
|
||||
|
||||
// 压力插值 - 使用线性插值
|
||||
float pressure = (float)((1 - t) * p0.PressureFactor + t * p5.PressureFactor);
|
||||
|
||||
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 简化的控制点计算
|
||||
/// </summary>
|
||||
private (Point cp1, Point cp2) CalculateSimpleControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
|
||||
{
|
||||
// 计算控制点距离(基于线段长度)
|
||||
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
|
||||
double dist2 = Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
|
||||
|
||||
// 使用更小的控制点距离,产生更平滑的曲线
|
||||
double controlDist1 = dist1 * 0.2; // 进一步减少控制点影响
|
||||
double controlDist2 = dist2 * 0.2;
|
||||
|
||||
// 计算控制点方向 - 使用更平滑的方向计算
|
||||
double dir1X = p2.X - p0.X; // 使用更远的点计算方向
|
||||
double dir1Y = p2.Y - p0.Y;
|
||||
double dir2X = p3.X - p1.X;
|
||||
double dir2Y = p3.Y - p1.Y;
|
||||
|
||||
// 归一化方向
|
||||
double len1 = Math.Sqrt(dir1X * dir1X + dir1Y * dir1Y);
|
||||
double len2 = Math.Sqrt(dir2X * dir2X + dir2Y * dir2Y);
|
||||
|
||||
if (len1 > 0)
|
||||
{
|
||||
dir1X /= len1;
|
||||
dir1Y /= len1;
|
||||
}
|
||||
|
||||
if (len2 > 0)
|
||||
{
|
||||
dir2X /= len2;
|
||||
dir2Y /= len2;
|
||||
}
|
||||
|
||||
// 计算控制点
|
||||
var cp1 = new Point(
|
||||
p1.X + dir1X * controlDist1,
|
||||
p1.Y + dir1Y * controlDist1
|
||||
);
|
||||
|
||||
var cp2 = new Point(
|
||||
p2.X - dir2X * controlDist2,
|
||||
p2.Y - dir2Y * controlDist2
|
||||
);
|
||||
|
||||
return (cp1, cp2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 宽松的去重算法
|
||||
/// </summary>
|
||||
private StylusPoint[] RemoveDuplicatePointsLoose(StylusPoint[] points)
|
||||
{
|
||||
if (points.Length < 2) return points;
|
||||
|
||||
var result = new List<StylusPoint>();
|
||||
result.Add(points[0]);
|
||||
|
||||
double minDistance = 0.1; // 非常小的距离阈值,几乎不去重
|
||||
|
||||
for (int i = 1; i < points.Length; i++)
|
||||
{
|
||||
var lastPoint = result[result.Count - 1];
|
||||
var currentPoint = points[i];
|
||||
|
||||
// 计算距离
|
||||
double distance = Math.Sqrt(
|
||||
(currentPoint.X - lastPoint.X) * (currentPoint.X - lastPoint.X) +
|
||||
(currentPoint.Y - lastPoint.Y) * (currentPoint.Y - lastPoint.Y));
|
||||
|
||||
// 如果距离足够大,添加这个点
|
||||
if (distance >= minDistance)
|
||||
{
|
||||
result.Add(currentPoint);
|
||||
}
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"RemoveDuplicatePointsLoose: 输入点数={points.Length}, 输出点数={result.Count}");
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算贝塞尔曲线上的点
|
||||
/// </summary>
|
||||
private StylusPoint CalculateBezierPoint(StylusPoint p0, Point cp1, Point cp2, StylusPoint p3, double t)
|
||||
{
|
||||
double x = Math.Pow(1 - t, 3) * p0.X +
|
||||
3 * Math.Pow(1 - t, 2) * t * cp1.X +
|
||||
3 * (1 - t) * Math.Pow(t, 2) * cp2.X +
|
||||
Math.Pow(t, 3) * p3.X;
|
||||
|
||||
double y = Math.Pow(1 - t, 3) * p0.Y +
|
||||
3 * Math.Pow(1 - t, 2) * t * cp1.Y +
|
||||
3 * (1 - t) * Math.Pow(t, 2) * cp2.Y +
|
||||
Math.Pow(t, 3) * p3.Y;
|
||||
|
||||
// 压力插值
|
||||
float pressure = (float)(Math.Pow(1 - t, 3) * p0.PressureFactor +
|
||||
3 * Math.Pow(1 - t, 2) * t * p0.PressureFactor +
|
||||
3 * (1 - t) * Math.Pow(t, 2) * p3.PressureFactor +
|
||||
Math.Pow(t, 3) * p3.PressureFactor);
|
||||
|
||||
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算改进的控制点
|
||||
/// </summary>
|
||||
private (Point cp1, Point cp2) CalculateImprovedControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
|
||||
{
|
||||
// 计算切线方向
|
||||
var tangent1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
|
||||
var tangent2 = new Vector(p3.X - p2.X, p3.Y - p2.Y);
|
||||
|
||||
// 归一化切线
|
||||
if (tangent1.Length > 0) tangent1.Normalize();
|
||||
if (tangent2.Length > 0) tangent2.Normalize();
|
||||
|
||||
// 计算控制点距离(基于点间距离)
|
||||
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
|
||||
double dist2 = Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
|
||||
|
||||
double controlDist1 = dist1 * CurveTension;
|
||||
double controlDist2 = dist2 * CurveTension;
|
||||
|
||||
// 计算控制点
|
||||
var cp1 = new Point(
|
||||
p1.X + tangent1.X * controlDist1,
|
||||
p1.Y + tangent1.Y * controlDist1
|
||||
);
|
||||
|
||||
var cp2 = new Point(
|
||||
p2.X - tangent2.X * controlDist2,
|
||||
p2.Y - tangent2.Y * controlDist2
|
||||
);
|
||||
|
||||
return (cp1, cp2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自适应插值步数计算
|
||||
/// </summary>
|
||||
private int CalculateAdaptiveSteps(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
|
||||
{
|
||||
// 基于曲线长度和复杂度计算步数
|
||||
double totalLength = 0;
|
||||
totalLength += Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
|
||||
totalLength += Math.Sqrt((p2.X - p1.X) * (p2.X - p1.X) + (p2.Y - p1.Y) * (p2.Y - p1.Y));
|
||||
totalLength += Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
|
||||
|
||||
// 计算曲率(简化版本)
|
||||
double curvature = CalculateCurvature(p0, p1, p2, p3);
|
||||
|
||||
// 基于长度和曲率计算步数
|
||||
int baseSteps = Math.Max(8, Math.Min(20, (int)(totalLength / 10)));
|
||||
int curvatureSteps = (int)(curvature * 10);
|
||||
|
||||
return Math.Max(InterpolationSteps, Math.Min(24, baseSteps + curvatureSteps));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算曲率(简化版本)
|
||||
/// </summary>
|
||||
private double CalculateCurvature(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
|
||||
{
|
||||
// 计算三个向量的角度变化
|
||||
var v1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
|
||||
var v2 = new Vector(p2.X - p1.X, p2.Y - p1.Y);
|
||||
var v3 = new Vector(p3.X - p2.X, p3.Y - p2.Y);
|
||||
|
||||
if (v1.Length == 0 || v2.Length == 0 || v3.Length == 0) return 0;
|
||||
|
||||
v1.Normalize();
|
||||
v2.Normalize();
|
||||
v3.Normalize();
|
||||
|
||||
// 计算角度变化
|
||||
double angle1 = Math.Acos(Math.Max(-1, Math.Min(1, Vector.Multiply(v1, v2))));
|
||||
double angle2 = Math.Acos(Math.Max(-1, Math.Min(1, Vector.Multiply(v2, v3))));
|
||||
|
||||
return (angle1 + angle2) / Math.PI; // 归一化到0-1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 去除重复和过近的点
|
||||
/// </summary>
|
||||
private StylusPoint[] RemoveDuplicatePoints(StylusPoint[] points)
|
||||
{
|
||||
if (points.Length < 2) return points;
|
||||
|
||||
var result = new List<StylusPoint>();
|
||||
result.Add(points[0]);
|
||||
|
||||
double minDistance = 0.3; // 进一步减少最小距离阈值,保留更多平滑点
|
||||
|
||||
for (int i = 1; i < points.Length; i++)
|
||||
{
|
||||
var lastPoint = result[result.Count - 1];
|
||||
var currentPoint = points[i];
|
||||
|
||||
// 计算距离
|
||||
double distance = Math.Sqrt(
|
||||
(currentPoint.X - lastPoint.X) * (currentPoint.X - lastPoint.X) +
|
||||
(currentPoint.Y - lastPoint.Y) * (currentPoint.Y - lastPoint.Y));
|
||||
|
||||
// 如果距离足够大,添加这个点
|
||||
if (distance >= minDistance)
|
||||
{
|
||||
result.Add(currentPoint);
|
||||
}
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"RemoveDuplicatePoints: 输入点数={points.Length}, 输出点数={result.Count}");
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用控制点的三次贝塞尔曲线计算
|
||||
/// </summary>
|
||||
private StylusPoint CubicBezierWithControlPoints((Point cp1, Point cp2) controlPoints, double t, StylusPoint p0, StylusPoint p3)
|
||||
{
|
||||
var p1 = controlPoints.cp1;
|
||||
var p2 = controlPoints.cp2;
|
||||
|
||||
double u = 1 - t;
|
||||
double tt = t * t;
|
||||
double uu = u * u;
|
||||
double uuu = uu * u;
|
||||
double ttt = tt * t;
|
||||
|
||||
// 预计算系数
|
||||
double c0 = uuu;
|
||||
double c1 = 3 * uu * t;
|
||||
double c2 = 3 * u * tt;
|
||||
double c3 = ttt;
|
||||
|
||||
double x = c0 * p0.X + c1 * p1.X + c2 * p2.X + c3 * p3.X;
|
||||
double y = c0 * p0.Y + c1 * p1.Y + c2 * p2.Y + c3 * p3.Y;
|
||||
|
||||
// 插值压力值
|
||||
float pressure = (float)(p0.PressureFactor * u + p3.PressureFactor * t);
|
||||
pressure = Math.Max(pressure, 0.1f);
|
||||
|
||||
return new StylusPoint(x, y, pressure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 硬件加速的向量化指数平滑
|
||||
/// </summary>
|
||||
@@ -352,23 +732,31 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
public class AdvancedBezierSmoothing
|
||||
{
|
||||
public double SmoothingStrength { get; set; } = 0.3;
|
||||
public double ResampleInterval { get; set; } = 3.0;
|
||||
public int InterpolationSteps { get; set; } = 8;
|
||||
public double SmoothingStrength { get; set; } = 0.6;
|
||||
public double ResampleInterval { get; set; } = 2.0;
|
||||
public int InterpolationSteps { get; set; } = 12;
|
||||
|
||||
public Stroke SmoothStroke(Stroke stroke)
|
||||
{
|
||||
if (stroke == null || stroke.StylusPoints.Count < 3)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 笔画点数不足,跳过平滑 (点数: {stroke?.StylusPoints.Count ?? 0})");
|
||||
return stroke;
|
||||
}
|
||||
|
||||
var originalPoints = stroke.StylusPoints.ToList();
|
||||
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 开始平滑处理,原始点数: {stroke.StylusPoints.Count}");
|
||||
|
||||
// 简化处理:只进行轻度平滑
|
||||
var smoothedPoints = ApplyLightExponentialSmoothing(originalPoints, 0.2); // 很轻的平滑
|
||||
var originalPoints = stroke.StylusPoints.ToArray();
|
||||
|
||||
// 使用真正的贝塞尔曲线平滑
|
||||
var smoothedPoints = ApplyCubicBezierSmoothing(originalPoints);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 平滑完成,平滑后点数: {smoothedPoints.Length}");
|
||||
|
||||
// 检查点数是否合理
|
||||
if (smoothedPoints.Count > originalPoints.Count * 1.5)
|
||||
if (smoothedPoints.Length > originalPoints.Length * 10.0)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 点数增加过多,返回原始笔画 (原始:{originalPoints.Length}, 平滑后:{smoothedPoints.Length})");
|
||||
return stroke; // 如果点数增加太多,返回原始笔画
|
||||
}
|
||||
|
||||
@@ -376,9 +764,140 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
DrawingAttributes = stroke.DrawingAttributes.Clone()
|
||||
};
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 创建平滑笔画成功");
|
||||
return smoothedStroke;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 三次贝塞尔曲线平滑
|
||||
/// </summary>
|
||||
private StylusPoint[] ApplyCubicBezierSmoothing(StylusPoint[] points)
|
||||
{
|
||||
if (points.Length < 4) return points;
|
||||
|
||||
var result = new List<StylusPoint>();
|
||||
result.Add(points[0]);
|
||||
|
||||
// 使用更保守的窗口大小和插值
|
||||
int windowSize = Math.Min(4, points.Length);
|
||||
int stepSize = Math.Max(1, points.Length / 10); // 根据点数动态调整步长
|
||||
|
||||
for (int i = 0; i <= points.Length - windowSize; i += stepSize)
|
||||
{
|
||||
if (i + windowSize - 1 >= points.Length) break;
|
||||
|
||||
var p0 = points[i];
|
||||
var p1 = points[Math.Min(i + 1, points.Length - 1)];
|
||||
var p2 = points[Math.Min(i + 2, points.Length - 1)];
|
||||
var p3 = points[Math.Min(i + windowSize - 1, points.Length - 1)];
|
||||
|
||||
// 计算控制点
|
||||
var controlPoints = CalculateControlPoints(p0, p1, p2, p3);
|
||||
|
||||
// 只生成2-3个插值点
|
||||
int steps = 2;
|
||||
|
||||
// 生成贝塞尔曲线点
|
||||
for (int j = 1; j <= steps; j++)
|
||||
{
|
||||
double t = (double)j / steps;
|
||||
var bezierPoint = CalculateBezierPoint(p0, controlPoints.cp1, controlPoints.cp2, p3, t);
|
||||
result.Add(bezierPoint);
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(points[points.Length - 1]);
|
||||
|
||||
// 去重处理
|
||||
return RemoveDuplicatePoints(result.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 去除重复点
|
||||
/// </summary>
|
||||
private StylusPoint[] RemoveDuplicatePoints(StylusPoint[] points)
|
||||
{
|
||||
if (points.Length <= 1) return points;
|
||||
|
||||
var result = new List<StylusPoint> { points[0] };
|
||||
double minDistance = 1.0; // 最小距离阈值
|
||||
|
||||
for (int i = 1; i < points.Length; i++)
|
||||
{
|
||||
var lastPoint = result[result.Count - 1];
|
||||
var currentPoint = points[i];
|
||||
|
||||
double distance = Math.Sqrt(Math.Pow(currentPoint.X - lastPoint.X, 2) +
|
||||
Math.Pow(currentPoint.Y - lastPoint.Y, 2));
|
||||
|
||||
if (distance > minDistance)
|
||||
{
|
||||
result.Add(currentPoint);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算控制点
|
||||
/// </summary>
|
||||
private (Point cp1, Point cp2) CalculateControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
|
||||
{
|
||||
// 计算切线方向
|
||||
var tangent1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
|
||||
var tangent2 = new Vector(p3.X - p2.X, p3.Y - p2.Y);
|
||||
|
||||
// 归一化切线
|
||||
if (tangent1.Length > 0) tangent1.Normalize();
|
||||
if (tangent2.Length > 0) tangent2.Normalize();
|
||||
|
||||
// 计算控制点距离
|
||||
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
|
||||
double dist2 = Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
|
||||
|
||||
double controlDist1 = dist1 * SmoothingStrength;
|
||||
double controlDist2 = dist2 * SmoothingStrength;
|
||||
|
||||
// 计算控制点
|
||||
var cp1 = new Point(
|
||||
p1.X + tangent1.X * controlDist1,
|
||||
p1.Y + tangent1.Y * controlDist1
|
||||
);
|
||||
|
||||
var cp2 = new Point(
|
||||
p2.X - tangent2.X * controlDist2,
|
||||
p2.Y - tangent2.Y * controlDist2
|
||||
);
|
||||
|
||||
return (cp1, cp2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算贝塞尔曲线上的点
|
||||
/// </summary>
|
||||
private StylusPoint CalculateBezierPoint(StylusPoint p0, Point cp1, Point cp2, StylusPoint p3, double t)
|
||||
{
|
||||
double x = Math.Pow(1 - t, 3) * p0.X +
|
||||
3 * Math.Pow(1 - t, 2) * t * cp1.X +
|
||||
3 * (1 - t) * Math.Pow(t, 2) * cp2.X +
|
||||
Math.Pow(t, 3) * p3.X;
|
||||
|
||||
double y = Math.Pow(1 - t, 3) * p0.Y +
|
||||
3 * Math.Pow(1 - t, 2) * t * cp1.Y +
|
||||
3 * (1 - t) * Math.Pow(t, 2) * cp2.Y +
|
||||
Math.Pow(t, 3) * p3.Y;
|
||||
|
||||
// 压力插值
|
||||
float pressure = (float)(Math.Pow(1 - t, 3) * p0.PressureFactor +
|
||||
3 * Math.Pow(1 - t, 2) * t * p0.PressureFactor +
|
||||
3 * (1 - t) * Math.Pow(t, 2) * p3.PressureFactor +
|
||||
Math.Pow(t, 3) * p3.PressureFactor);
|
||||
|
||||
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轻度指数平滑
|
||||
/// </summary>
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using Ink_Canvas.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
internal class AnimationsHelper
|
||||
{
|
||||
private static UIElement ResolveAnimationTarget(UIElement element)
|
||||
{
|
||||
if (element is BoardMenuFrame frame)
|
||||
{
|
||||
frame.ApplyTemplate();
|
||||
return frame.AnimationTarget ?? element;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
public static void ShowWithFadeIn(UIElement element, double duration = 0.15)
|
||||
{
|
||||
if (element.Visibility == Visibility.Visible) return;
|
||||
@@ -36,14 +47,17 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
if (element.Visibility == Visibility.Visible) return;
|
||||
|
||||
if (element == null)
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
|
||||
if (element.Visibility == Visibility.Visible) return;
|
||||
|
||||
element.Visibility = Visibility.Visible;
|
||||
|
||||
var target = ResolveAnimationTarget(element);
|
||||
|
||||
var sb = new Storyboard();
|
||||
|
||||
// 渐变动画
|
||||
var fadeInAnimation = new DoubleAnimation
|
||||
{
|
||||
From = 0.5,
|
||||
@@ -54,10 +68,9 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(UIElement.OpacityProperty));
|
||||
|
||||
// 滑动动画
|
||||
var slideAnimation = new DoubleAnimation
|
||||
{
|
||||
From = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
|
||||
From = 10,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromSeconds(duration)
|
||||
};
|
||||
@@ -68,12 +81,11 @@ namespace Ink_Canvas.Helpers
|
||||
sb.Children.Add(fadeInAnimation);
|
||||
sb.Children.Add(slideAnimation);
|
||||
|
||||
element.Visibility = Visibility.Visible;
|
||||
element.RenderTransform = new TranslateTransform();
|
||||
target.RenderTransform = new TranslateTransform();
|
||||
|
||||
sb.Begin((FrameworkElement)element);
|
||||
sb.Begin((FrameworkElement)target);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
public static void ShowWithSlideFromLeftAndFade(UIElement element, double duration = 0.25)
|
||||
@@ -113,7 +125,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 +168,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,21 +212,22 @@ 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)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (element.Visibility == Visibility.Collapsed) return;
|
||||
|
||||
if (element == null)
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
|
||||
if (element.Visibility == Visibility.Collapsed) return;
|
||||
|
||||
var target = ResolveAnimationTarget(element);
|
||||
|
||||
var sb = new Storyboard();
|
||||
|
||||
// 渐变动画
|
||||
var fadeOutAnimation = new DoubleAnimation
|
||||
{
|
||||
From = 1,
|
||||
@@ -224,11 +237,10 @@ namespace Ink_Canvas.Helpers
|
||||
fadeOutAnimation.EasingFunction = new CubicEase();
|
||||
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(UIElement.OpacityProperty));
|
||||
|
||||
// 滑动动画
|
||||
var slideAnimation = new DoubleAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
|
||||
To = 10,
|
||||
Duration = TimeSpan.FromSeconds(duration)
|
||||
};
|
||||
slideAnimation.EasingFunction = new CubicEase();
|
||||
@@ -243,10 +255,10 @@ namespace Ink_Canvas.Helpers
|
||||
element.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
|
||||
element.RenderTransform = new TranslateTransform();
|
||||
sb.Begin((FrameworkElement)element);
|
||||
target.RenderTransform = new TranslateTransform();
|
||||
sb.Begin((FrameworkElement)target);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
public static void HideWithFadeOut(UIElement element, double duration = 0.15)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using Ink_Canvas.Windows.SettingsViews.Helpers;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Principal;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public static class AppRestartHelper
|
||||
{
|
||||
public static bool IsRunningAsAdmin()
|
||||
{
|
||||
try
|
||||
{
|
||||
var identity = WindowsIdentity.GetCurrent();
|
||||
var principal = new WindowsPrincipal(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void RestartApp(bool asAdmin)
|
||||
{
|
||||
try
|
||||
{
|
||||
App.IsAppExitByUser = true;
|
||||
|
||||
(Application.Current as App)?.ReleaseMutexForRestart();
|
||||
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
|
||||
if (asAdmin)
|
||||
{
|
||||
var psi = new ProcessStartInfo(exePath) { UseShellExecute = true, Verb = "runas" };
|
||||
Process.Start(psi);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 当前已是管理员时,直接通过用户令牌降权启动,避免经由 explorer 中转的延迟
|
||||
if (IsRunningAsAdmin() && UIAccessHelper.RestartAsNormalUser())
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start("explorer.exe", "\"" + exePath + "\"");
|
||||
}
|
||||
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"重启应用时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void RestartWithCurrentPrivileges()
|
||||
{
|
||||
RestartApp(IsRunningAsAdmin());
|
||||
}
|
||||
|
||||
public static void RestartAsAdmin()
|
||||
{
|
||||
RestartApp(true);
|
||||
}
|
||||
|
||||
public static void RestartAsNormal()
|
||||
{
|
||||
RestartApp(false);
|
||||
}
|
||||
|
||||
public static void SwitchToUIATopMostAndRestart()
|
||||
{
|
||||
try
|
||||
{
|
||||
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = true;
|
||||
|
||||
if (!SettingsManager.Settings.Advanced.IsAlwaysOnTop)
|
||||
{
|
||||
SettingsManager.Settings.Advanced.IsAlwaysOnTop = true;
|
||||
}
|
||||
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
|
||||
App.IsUIAccessTopMostEnabled = true;
|
||||
RestartApp(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"切换到UIA置顶模式时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void SwitchToNormalTopMostAndRestart()
|
||||
{
|
||||
try
|
||||
{
|
||||
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = false;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
|
||||
App.IsUIAccessTopMostEnabled = false;
|
||||
RestartApp(IsRunningAsAdmin());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"切换到普通置顶模式时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动备份管理器
|
||||
/// 负责管理配置文件的自动备份功能
|
||||
/// </summary>
|
||||
public static class AutoBackupManager
|
||||
{
|
||||
private static readonly string BackupDir = Path.Combine(App.RootPath, "Backups");
|
||||
private static readonly string SettingsFile = Path.Combine(App.RootPath, "Configs", "Settings.json");
|
||||
private static readonly string BackupPrefix = "Settings_AutoBackup_";
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否需要执行自动备份
|
||||
/// </summary>
|
||||
/// <param name="settings">设置对象</param>
|
||||
/// <returns>如果需要备份返回<see langword="true"/>,否则返回<see langword="false"/></returns>
|
||||
public static bool ShouldPerformAutoBackup(Settings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果自动备份功能未启用,不执行备份
|
||||
if (!settings.Advanced.IsAutoBackupEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果从未备份过,需要创建首次备份
|
||||
if (settings.Advanced.LastAutoBackupTime == DateTime.MinValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否已超过备份间隔
|
||||
var daysSinceLastBackup = (DateTime.Now - settings.Advanced.LastAutoBackupTime).TotalDays;
|
||||
return daysSinceLastBackup >= settings.Advanced.AutoBackupIntervalDays;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"检查自动备份条件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行自动备份
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 为主配置文件创建一次自动备份并在成功后更新并保存设置中的最后备份时间。
|
||||
/// </remarks>
|
||||
/// <param name="settings">应用的设置对象;在成功备份后会更新 settings.Advanced.LastAutoBackupTime 并调用保存操作。</param>
|
||||
/// <returns><see langword="true"/> 表示备份成功,<see langword="false"/> 表示备份失败或被跳过。</returns>
|
||||
public static bool PerformAutoBackup(Settings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保备份目录存在
|
||||
if (!Directory.Exists(BackupDir))
|
||||
{
|
||||
ProcessProtectionManager.WithWriteAccess(BackupDir, () => Directory.CreateDirectory(BackupDir));
|
||||
}
|
||||
|
||||
// 检查主配置文件是否存在
|
||||
if (!File.Exists(SettingsFile))
|
||||
{
|
||||
LogHelper.WriteLogToFile("主配置文件不存在,跳过自动备份", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建备份文件名(使用当前日期时间)
|
||||
string backupFileName = $"{BackupPrefix}{DateTime.Now:yyyyMMdd_HHmmss}.json";
|
||||
string backupPath = Path.Combine(BackupDir, backupFileName);
|
||||
|
||||
// 复制主配置文件到备份位置
|
||||
ProcessProtectionManager.WithWriteAccess(backupPath, () => File.Copy(SettingsFile, backupPath, true));
|
||||
|
||||
// 更新最后备份时间
|
||||
settings.Advanced.LastAutoBackupTime = DateTime.Now;
|
||||
MainWindow.SaveSettingsToFile();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"执行自动备份时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从备份恢复配置文件
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 从最新可用的自动备份恢复主设置文件(Settings.json)。如果当前设置文件存在,会先将其复制到备份目录并加上时间戳作为“损坏”的备份副本,然后用最新备份覆盖原文件。
|
||||
/// </remarks>
|
||||
/// <returns><see langword="true"/> 如果恢复成功,<see langword="false"/> 否则。</returns>
|
||||
public static bool TryRestoreFromBackup()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保备份目录存在
|
||||
if (!Directory.Exists(BackupDir))
|
||||
{
|
||||
LogHelper.WriteLogToFile("备份目录不存在,无法从备份恢复", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查找最新的备份文件
|
||||
var backupFiles = Directory.GetFiles(BackupDir, $"{BackupPrefix}*.json")
|
||||
.OrderByDescending(f => File.GetCreationTime(f))
|
||||
.ToArray();
|
||||
|
||||
if (backupFiles.Length == 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile("没有找到可用的备份文件", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试使用最新的备份文件
|
||||
string latestBackup = backupFiles[0];
|
||||
|
||||
// 验证备份文件是否有效
|
||||
try
|
||||
{
|
||||
string backupJson = File.ReadAllText(latestBackup);
|
||||
var testSettings = JsonConvert.DeserializeObject<Settings>(backupJson);
|
||||
if (testSettings == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("备份文件内容无效,无法恢复", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"备份文件验证失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 备份当前损坏的配置文件(如果存在)
|
||||
if (File.Exists(SettingsFile))
|
||||
{
|
||||
string corruptedBackup = Path.Combine(BackupDir, $"Settings_Corrupted_{DateTime.Now:yyyyMMdd_HHmmss}.json");
|
||||
ProcessProtectionManager.WithWriteAccess(corruptedBackup, () => File.Copy(SettingsFile, corruptedBackup, true));
|
||||
}
|
||||
|
||||
// 从备份恢复配置文件
|
||||
ProcessProtectionManager.WithWriteAccess(SettingsFile, () => File.Copy(latestBackup, SettingsFile, true));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从备份恢复配置文件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理过期的备份文件
|
||||
/// 保留最近30天的备份文件
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 删除备份目录中按“备份前缀”匹配且创建时间早于 30 天的自动备份文件(即自动备份文件的命名前缀),不会删除诸如 Settings_Corrupted_*.json 之类的其他备份或错误状态文件。
|
||||
/// 如果备份目录不存在则不执行任何操作;删除操作在受写入保护的上下文中执行,任何错误会被记录但不会抛出异常。
|
||||
/// </remarks>
|
||||
public static void CleanupOldBackups()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(BackupDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cutoffDate = DateTime.Now.AddDays(-30);
|
||||
var backupFiles = Directory.GetFiles(BackupDir, $"{BackupPrefix}*.json");
|
||||
|
||||
int deletedCount = 0;
|
||||
foreach (var file in backupFiles)
|
||||
{
|
||||
if (File.GetCreationTime(file) < cutoffDate)
|
||||
{
|
||||
ProcessProtectionManager.WithWriteAccess(file, () => File.Delete(file));
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0)
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理过期备份文件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化自动备份功能
|
||||
/// 在应用程序启动时调用
|
||||
/// </summary>
|
||||
/// <param name="settings">设置对象</param>
|
||||
public static void Initialize(Settings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查是否需要执行自动备份
|
||||
if (ShouldPerformAutoBackup(settings))
|
||||
{
|
||||
PerformAutoBackup(settings);
|
||||
}
|
||||
|
||||
// 清理过期备份
|
||||
CleanupOldBackups();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化自动备份功能时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1088
-173
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
using AForge.Video;
|
||||
using AForge.Video.DirectShow;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public class CameraService : IDisposable
|
||||
{
|
||||
private VideoCaptureDevice _videoSource;
|
||||
private bool _isCapturing;
|
||||
private Bitmap _currentFrame;
|
||||
private readonly object _frameLock = new object();
|
||||
private Dispatcher _dispatcher;
|
||||
|
||||
// 新增属性
|
||||
private int _rotationAngle = 0; // 0=0度,1=90度,2=180度,3=270度
|
||||
private int _resolutionWidth = 640;
|
||||
private int _resolutionHeight = 480;
|
||||
|
||||
public event EventHandler<Bitmap> FrameReceived;
|
||||
public event EventHandler<string> ErrorOccurred;
|
||||
|
||||
public bool IsCapturing => _isCapturing;
|
||||
public List<FilterInfo> AvailableCameras { get; private set; }
|
||||
public FilterInfo CurrentCamera { get; private set; }
|
||||
|
||||
// 新增属性
|
||||
public int RotationAngle
|
||||
{
|
||||
get => _rotationAngle;
|
||||
set => _rotationAngle = Math.Max(0, Math.Min(3, value));
|
||||
}
|
||||
|
||||
public int ResolutionWidth
|
||||
{
|
||||
get => _resolutionWidth;
|
||||
set => _resolutionWidth = Math.Max(320, Math.Min(3840, value));
|
||||
}
|
||||
|
||||
public int ResolutionHeight
|
||||
{
|
||||
get => _resolutionHeight;
|
||||
set => _resolutionHeight = Math.Max(240, Math.Min(2160, value));
|
||||
}
|
||||
|
||||
public CameraService()
|
||||
{
|
||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||
AvailableCameras = new List<FilterInfo>();
|
||||
RefreshCameraList();
|
||||
}
|
||||
|
||||
public CameraService(int rotationAngle, int resolutionWidth, int resolutionHeight)
|
||||
{
|
||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||
AvailableCameras = new List<FilterInfo>();
|
||||
_rotationAngle = rotationAngle;
|
||||
_resolutionWidth = resolutionWidth;
|
||||
_resolutionHeight = resolutionHeight;
|
||||
RefreshCameraList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新可用摄像头列表
|
||||
/// </summary>
|
||||
public void RefreshCameraList()
|
||||
{
|
||||
try
|
||||
{
|
||||
AvailableCameras.Clear();
|
||||
var videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
|
||||
|
||||
foreach (FilterInfo device in videoDevices)
|
||||
{
|
||||
AvailableCameras.Add(device);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"刷新摄像头列表失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
ErrorOccurred?.Invoke(this, $"刷新摄像头列表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始摄像头预览
|
||||
/// </summary>
|
||||
/// <param name="cameraIndex">摄像头索引</param>
|
||||
public bool StartPreview(int cameraIndex = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (AvailableCameras.Count == 0)
|
||||
{
|
||||
RefreshCameraList();
|
||||
if (AvailableCameras.Count == 0)
|
||||
{
|
||||
ErrorOccurred?.Invoke(this, "未找到可用的摄像头设备");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
|
||||
{
|
||||
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 停止当前预览
|
||||
StopPreview();
|
||||
|
||||
CurrentCamera = AvailableCameras[cameraIndex];
|
||||
_videoSource = new VideoCaptureDevice(CurrentCamera.MonikerString);
|
||||
|
||||
// 设置视频源事件处理
|
||||
_videoSource.NewFrame += VideoSource_NewFrame;
|
||||
|
||||
// 启动视频源
|
||||
_videoSource.Start();
|
||||
|
||||
_isCapturing = true;
|
||||
LogHelper.WriteLogToFile($"开始摄像头预览: {CurrentCamera.Name}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
ErrorOccurred?.Invoke(this, $"启动摄像头预览失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止摄像头预览
|
||||
/// </summary>
|
||||
public void StopPreview()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_videoSource != null && _videoSource.IsRunning)
|
||||
{
|
||||
_videoSource.SignalToStop();
|
||||
_videoSource.WaitForStop();
|
||||
_videoSource.NewFrame -= VideoSource_NewFrame;
|
||||
_videoSource = null;
|
||||
}
|
||||
|
||||
_isCapturing = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"停止摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定摄像头
|
||||
/// </summary>
|
||||
/// <param name="cameraIndex">摄像头索引</param>
|
||||
public bool SwitchCamera(int cameraIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
|
||||
{
|
||||
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
|
||||
return false;
|
||||
}
|
||||
|
||||
return StartPreview(cameraIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"切换摄像头失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
ErrorOccurred?.Invoke(this, $"切换摄像头失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前帧的BitmapSource(WPF格式),直接返回可用的WPF位图
|
||||
/// </summary>
|
||||
public BitmapSource GetCurrentFrameAsBitmapSource()
|
||||
{
|
||||
lock (_frameLock)
|
||||
{
|
||||
if (_currentFrame == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// 验证位图有效性
|
||||
if (_currentFrame.Width <= 0 || _currentFrame.Height <= 0)
|
||||
return null;
|
||||
|
||||
// 使用更安全的方法转换位图
|
||||
var bitmapData = _currentFrame.LockBits(
|
||||
new Rectangle(0, 0, _currentFrame.Width, _currentFrame.Height),
|
||||
ImageLockMode.ReadOnly,
|
||||
_currentFrame.PixelFormat);
|
||||
|
||||
try
|
||||
{
|
||||
// 根据像素格式选择合适的WPF像素格式
|
||||
System.Windows.Media.PixelFormat wpfPixelFormat;
|
||||
switch (_currentFrame.PixelFormat)
|
||||
{
|
||||
case PixelFormat.Format24bppRgb:
|
||||
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
|
||||
break;
|
||||
case PixelFormat.Format32bppArgb:
|
||||
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgra32;
|
||||
break;
|
||||
case PixelFormat.Format32bppRgb:
|
||||
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr32;
|
||||
break;
|
||||
default:
|
||||
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
|
||||
break;
|
||||
}
|
||||
|
||||
var bitmapSource = BitmapSource.Create(
|
||||
bitmapData.Width,
|
||||
bitmapData.Height,
|
||||
_currentFrame.HorizontalResolution,
|
||||
_currentFrame.VerticalResolution,
|
||||
wpfPixelFormat,
|
||||
null,
|
||||
bitmapData.Scan0,
|
||||
bitmapData.Stride * bitmapData.Height,
|
||||
bitmapData.Stride);
|
||||
|
||||
bitmapSource.Freeze();
|
||||
return bitmapSource;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentFrame.UnlockBits(bitmapData);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"转换帧为BitmapSource失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 视频源新帧事件处理
|
||||
/// </summary>
|
||||
private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_frameLock)
|
||||
{
|
||||
// 释放之前的帧
|
||||
_currentFrame?.Dispose();
|
||||
|
||||
// 创建新的位图,避免Clone的问题
|
||||
var sourceFrame = eventArgs.Frame;
|
||||
|
||||
if (sourceFrame != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var width = sourceFrame.Width;
|
||||
var height = sourceFrame.Height;
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
// 应用旋转
|
||||
Bitmap rotatedFrame = ApplyRotation(sourceFrame);
|
||||
|
||||
int targetWidth = _resolutionWidth;
|
||||
int targetHeight = _resolutionHeight;
|
||||
|
||||
if (_rotationAngle == 1 || _rotationAngle == 3)
|
||||
{
|
||||
targetWidth = _resolutionHeight;
|
||||
targetHeight = _resolutionWidth;
|
||||
}
|
||||
|
||||
_currentFrame = ResizeImageWithAspectRatio(rotatedFrame, targetWidth, targetHeight);
|
||||
|
||||
rotatedFrame?.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentFrame = null;
|
||||
}
|
||||
}
|
||||
catch (Exception frameEx)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理源帧失败: {frameEx.Message}", LogHelper.LogType.Error);
|
||||
_currentFrame = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 在UI线程中触发事件
|
||||
_dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
FrameReceived?.Invoke(this, _currentFrame);
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理新帧失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
ErrorOccurred?.Invoke(this, $"处理新帧失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取摄像头名称列表
|
||||
/// </summary>
|
||||
public List<string> GetCameraNames()
|
||||
{
|
||||
return AvailableCameras.Select(camera => camera.Name).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有可用摄像头
|
||||
/// </summary>
|
||||
public bool HasAvailableCameras()
|
||||
{
|
||||
if (AvailableCameras.Count == 0)
|
||||
{
|
||||
RefreshCameraList();
|
||||
}
|
||||
return AvailableCameras.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用旋转到图像
|
||||
/// </summary>
|
||||
private Bitmap ApplyRotation(Bitmap source)
|
||||
{
|
||||
if (_rotationAngle == 0)
|
||||
return new Bitmap(source);
|
||||
|
||||
var rotationType = RotateFlipType.RotateNoneFlipNone;
|
||||
switch (_rotationAngle)
|
||||
{
|
||||
case 1: rotationType = RotateFlipType.Rotate90FlipNone; break;
|
||||
case 2: rotationType = RotateFlipType.Rotate180FlipNone; break;
|
||||
case 3: rotationType = RotateFlipType.Rotate270FlipNone; break;
|
||||
}
|
||||
|
||||
var rotated = new Bitmap(source);
|
||||
rotated.RotateFlip(rotationType);
|
||||
return rotated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调整图像大小
|
||||
/// </summary>
|
||||
private Bitmap ResizeImageWithAspectRatio(Bitmap source, int targetWidth, int targetHeight)
|
||||
{
|
||||
if (source.Width == targetWidth && source.Height == targetHeight)
|
||||
return new Bitmap(source);
|
||||
|
||||
double scaleX = (double)targetWidth / source.Width;
|
||||
double scaleY = (double)targetHeight / source.Height;
|
||||
double scale = Math.Min(scaleX, scaleY);
|
||||
|
||||
// 计算实际尺寸
|
||||
int actualWidth = (int)(source.Width * scale);
|
||||
int actualHeight = (int)(source.Height * scale);
|
||||
|
||||
var resized = new Bitmap(actualWidth, actualHeight, PixelFormat.Format24bppRgb);
|
||||
using (var graphics = Graphics.FromImage(resized))
|
||||
{
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
|
||||
graphics.DrawImage(source, 0, 0, actualWidth, actualHeight);
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调整图像大小
|
||||
/// </summary>
|
||||
private Bitmap ResizeImage(Bitmap source, int width, int height)
|
||||
{
|
||||
if (source.Width == width && source.Height == height)
|
||||
return new Bitmap(source);
|
||||
|
||||
var resized = new Bitmap(width, height, PixelFormat.Format24bppRgb);
|
||||
using (var graphics = Graphics.FromImage(resized))
|
||||
{
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
|
||||
graphics.DrawImage(source, 0, 0, width, height);
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopPreview();
|
||||
|
||||
lock (_frameLock)
|
||||
{
|
||||
_currentFrame?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Converter
|
||||
{
|
||||
@@ -112,4 +113,67 @@ namespace Ink_Canvas.Converter
|
||||
}
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public class InverseBooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if ((bool)value)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if ((bool)value)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public class StringToGeometryConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (value is string geometryString && !string.IsNullOrEmpty(geometryString))
|
||||
{
|
||||
return Geometry.Parse(geometryString);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public static class DebugConsoleManager
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool FreeConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetConsoleWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool SetConsoleTitle(string lpConsoleTitle);
|
||||
|
||||
private const int SW_HIDE = 0;
|
||||
private const int SW_SHOW = 5;
|
||||
private const uint SC_CLOSE = 0xF060;
|
||||
private const uint MF_BYCOMMAND = 0x00000000;
|
||||
|
||||
private static bool _allocated;
|
||||
|
||||
public static bool IsVisible { get; private set; }
|
||||
|
||||
public static void Show()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_allocated)
|
||||
{
|
||||
if (GetConsoleWindow() == IntPtr.Zero)
|
||||
{
|
||||
if (!AllocConsole()) return;
|
||||
}
|
||||
_allocated = true;
|
||||
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
SetConsoleTitle("InkCanvasForClass - Debug Console");
|
||||
|
||||
// 移除关闭菜单,避免用户点 X 时直接结束进程
|
||||
var hWnd = GetConsoleWindow();
|
||||
if (hWnd != IntPtr.Zero)
|
||||
{
|
||||
var hMenu = GetSystemMenu(hWnd, false);
|
||||
if (hMenu != IntPtr.Zero) DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var hWnd = GetConsoleWindow();
|
||||
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_SHOW);
|
||||
}
|
||||
IsVisible = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Show failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Hide()
|
||||
{
|
||||
try
|
||||
{
|
||||
var hWnd = GetConsoleWindow();
|
||||
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_HIDE);
|
||||
IsVisible = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Hide failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteLine(string line)
|
||||
{
|
||||
if (!IsVisible) return;
|
||||
try { Console.WriteLine(line); }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+669
-1749
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public static class ExternalCallerLauncher
|
||||
{
|
||||
private static readonly string[] ClassIslandProtocols =
|
||||
{
|
||||
"classisland://plugins/IslandCaller/Simple/1",
|
||||
"classisland://plugins/IslandCaller/Simple",
|
||||
"classisland://plugins/IslandCaller/Run"
|
||||
};
|
||||
|
||||
public static string[] GetProtocolsByType(int externalCallerType)
|
||||
{
|
||||
switch (externalCallerType)
|
||||
{
|
||||
case 0:
|
||||
return ClassIslandProtocols;
|
||||
case 1:
|
||||
return new[]
|
||||
{
|
||||
"secrandom://roll_call/quick_draw",
|
||||
"secrandom://direct_extraction"
|
||||
};
|
||||
case 2:
|
||||
return new[] { "namepicker://" };
|
||||
default:
|
||||
return ClassIslandProtocols;
|
||||
}
|
||||
}
|
||||
|
||||
public static string[] GetProtocolsByName(string externalCallerName)
|
||||
{
|
||||
switch (externalCallerName)
|
||||
{
|
||||
case "ClassIsland":
|
||||
return ClassIslandProtocols;
|
||||
case "SecRandom":
|
||||
return new[]
|
||||
{
|
||||
"secrandom://roll_call/quick_draw",
|
||||
"secrandom://direct_extraction"
|
||||
};
|
||||
case "NamePicker":
|
||||
return new[] { "namepicker://" };
|
||||
default:
|
||||
return ClassIslandProtocols;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryLaunch(IEnumerable<string> protocols, out Exception lastException)
|
||||
{
|
||||
lastException = null;
|
||||
if (protocols == null) return false;
|
||||
|
||||
foreach (var protocol in protocols)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(protocol)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = protocol,
|
||||
UseShellExecute = true
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件关联管理器,用于注册和处理.icstk文件的关联
|
||||
/// </summary>
|
||||
public static class FileAssociationManager
|
||||
{
|
||||
private const string FileExtension = ".icstk";
|
||||
private const string FileTypeName = "InkCanvasStrokesFile";
|
||||
private const string AppName = "Ink Canvas";
|
||||
private const string AppDescription = "Ink Canvas Strokes File";
|
||||
|
||||
// IPC相关常量
|
||||
private const string IpcMutexName = "InkCanvasFileAssociationIpc";
|
||||
private const string IpcEventName = "InkCanvasFileAssociationEvent";
|
||||
private const string IpcFilePrefix = "InkCanvasFileAssociation_";
|
||||
private const string IpcBoardModePrefix = "InkCanvasBoardMode_";
|
||||
private const string IpcShowModePrefix = "InkCanvasShowMode_";
|
||||
private const string IpcUriCommandPrefix = "InkCanvasUriCommand_";
|
||||
private const int IpcTimeout = 5000; // 5秒超时
|
||||
|
||||
/// <summary>
|
||||
/// 注册.icstk文件关联
|
||||
/// </summary>
|
||||
public static bool RegisterFileAssociation()
|
||||
{
|
||||
try
|
||||
{
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
|
||||
// 注册文件类型
|
||||
using (RegistryKey fileTypeKey = Registry.ClassesRoot.CreateSubKey(FileTypeName))
|
||||
{
|
||||
fileTypeKey.SetValue("", AppDescription);
|
||||
fileTypeKey.SetValue("FriendlyTypeName", AppDescription);
|
||||
|
||||
// 设置默认图标
|
||||
using (RegistryKey defaultIconKey = fileTypeKey.CreateSubKey("DefaultIcon"))
|
||||
{
|
||||
defaultIconKey.SetValue("", $"\"{exePath}\",0");
|
||||
}
|
||||
|
||||
// 设置打开命令
|
||||
using (RegistryKey shellKey = fileTypeKey.CreateSubKey("shell"))
|
||||
using (RegistryKey openKey = shellKey.CreateSubKey("open"))
|
||||
using (RegistryKey commandKey = openKey.CreateSubKey("command"))
|
||||
{
|
||||
commandKey.SetValue("", $"\"{exePath}\" \"%1\"");
|
||||
}
|
||||
}
|
||||
|
||||
// 注册文件扩展名
|
||||
using (RegistryKey extensionKey = Registry.ClassesRoot.CreateSubKey(FileExtension))
|
||||
{
|
||||
extensionKey.SetValue("", FileTypeName);
|
||||
}
|
||||
|
||||
// 刷新系统文件关联缓存
|
||||
RefreshSystemFileAssociations();
|
||||
|
||||
LogHelper.WriteLogToFile($"成功注册{FileExtension}文件关联", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (SecurityException ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册文件关联时权限不足: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册文件关联时访问被拒绝: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注销.icstk文件关联
|
||||
/// </summary>
|
||||
public static bool UnregisterFileAssociation()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 删除文件扩展名关联
|
||||
Registry.ClassesRoot.DeleteSubKeyTree(FileExtension, false);
|
||||
|
||||
// 删除文件类型定义
|
||||
Registry.ClassesRoot.DeleteSubKeyTree(FileTypeName, false);
|
||||
|
||||
// 刷新系统文件关联缓存
|
||||
RefreshSystemFileAssociations();
|
||||
|
||||
LogHelper.WriteLogToFile($"成功注销{FileExtension}文件关联", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注销文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查文件关联是否已注册
|
||||
/// </summary>
|
||||
public static bool IsFileAssociationRegistered()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (RegistryKey extensionKey = Registry.ClassesRoot.OpenSubKey(FileExtension))
|
||||
{
|
||||
if (extensionKey == null) return false;
|
||||
|
||||
string fileType = extensionKey.GetValue("") as string;
|
||||
if (string.IsNullOrEmpty(fileType)) return false;
|
||||
|
||||
using (RegistryKey fileTypeKey = Registry.ClassesRoot.OpenSubKey(fileType))
|
||||
{
|
||||
if (fileTypeKey == null) return false;
|
||||
|
||||
using (RegistryKey shellKey = fileTypeKey.OpenSubKey("shell\\open\\command"))
|
||||
{
|
||||
if (shellKey == null) return false;
|
||||
|
||||
string command = shellKey.GetValue("") as string;
|
||||
if (string.IsNullOrEmpty(command)) return false;
|
||||
|
||||
// 检查命令是否指向当前应用程序
|
||||
string currentExePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
return command.Contains(currentExePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"检查文件关联状态时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示文件关联状态
|
||||
/// </summary>
|
||||
public static void ShowFileAssociationStatus()
|
||||
{
|
||||
bool isRegistered = IsFileAssociationRegistered();
|
||||
LogHelper.WriteLogToFile($"{FileExtension}文件关联状态: {(isRegistered ? "已注册" : "未注册")}", LogHelper.LogType.Event);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新系统文件关联缓存
|
||||
/// </summary>
|
||||
private static void RefreshSystemFileAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 通知系统文件关联已更改
|
||||
SHChangeNotify(0x08000000, 0, IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"刷新文件关联缓存时出错: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理命令行参数中的文件路径
|
||||
/// </summary>
|
||||
/// <param name="args">命令行参数</param>
|
||||
/// <returns>找到的.icstk文件路径,如果没有找到则返回null</returns>
|
||||
public static string GetIcstkFileFromArgs(string[] args)
|
||||
{
|
||||
if (args == null || args.Length == 0) return null;
|
||||
|
||||
foreach (string arg in args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(arg)) continue;
|
||||
|
||||
// 检查是否为.icstk文件
|
||||
if (Path.GetExtension(arg).Equals(FileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 检查文件是否存在
|
||||
if (File.Exists(arg))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从命令行参数中找到.icstk文件: {arg}", LogHelper.LogType.Event);
|
||||
return arg;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"命令行参数中的.icstk文件不存在: {arg}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过IPC将文件路径发送给已运行的实例
|
||||
/// </summary>
|
||||
/// <param name="filePath">要打开的文件路径</param>
|
||||
/// <returns>是否成功发送</returns>
|
||||
public static bool TrySendFileToExistingInstance(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"尝试通过IPC发送文件路径给已运行实例: {filePath}", LogHelper.LogType.Event);
|
||||
|
||||
// 创建IPC文件
|
||||
string tempDir = Path.GetTempPath();
|
||||
string ipcFileName = IpcFilePrefix + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
|
||||
|
||||
// 写入文件路径到IPC文件
|
||||
File.WriteAllText(ipcFilePath, filePath, Encoding.UTF8);
|
||||
|
||||
// 创建事件通知已运行实例
|
||||
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
|
||||
{
|
||||
ipcEvent.Set();
|
||||
}
|
||||
|
||||
// 等待一段时间让已运行实例处理文件
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// 清理IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFilePath))
|
||||
{
|
||||
File.Delete(ipcFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("IPC文件路径发送完成", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"通过IPC发送文件路径失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过IPC将白板模式命令发送给已运行的实例
|
||||
/// </summary>
|
||||
/// <returns>是否成功发送</returns>
|
||||
public static bool TrySendBoardModeCommandToExistingInstance()
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("尝试通过IPC发送白板模式命令给已运行实例", LogHelper.LogType.Event);
|
||||
|
||||
// 创建IPC文件
|
||||
string tempDir = Path.GetTempPath();
|
||||
string ipcFileName = IpcBoardModePrefix + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
|
||||
|
||||
// 写入白板模式命令到IPC文件
|
||||
File.WriteAllText(ipcFilePath, "BOARD_MODE", Encoding.UTF8);
|
||||
|
||||
// 创建事件通知已运行实例
|
||||
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
|
||||
{
|
||||
ipcEvent.Set();
|
||||
}
|
||||
|
||||
// 等待一段时间让已运行实例处理命令
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// 清理IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFilePath))
|
||||
{
|
||||
File.Delete(ipcFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("IPC白板模式命令发送完成", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"通过IPC发送白板模式命令失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过IPC将展开浮动栏命令发送给已运行的实例
|
||||
/// </summary>
|
||||
/// <returns>是否成功发送</returns>
|
||||
public static bool TrySendShowModeCommandToExistingInstance()
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("尝试通过IPC发送展开浮动栏命令给已运行实例", LogHelper.LogType.Event);
|
||||
|
||||
// 创建IPC文件
|
||||
string tempDir = Path.GetTempPath();
|
||||
string ipcFileName = IpcShowModePrefix + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
|
||||
|
||||
// 写入展开浮动栏命令到IPC文件
|
||||
File.WriteAllText(ipcFilePath, "SHOW_MODE", Encoding.UTF8);
|
||||
|
||||
// 创建事件通知已运行实例
|
||||
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
|
||||
{
|
||||
ipcEvent.Set();
|
||||
}
|
||||
|
||||
// 等待一段时间让已运行实例处理命令
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// 清理IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFilePath))
|
||||
{
|
||||
File.Delete(ipcFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("IPC展开浮动栏命令发送完成", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"通过IPC发送展开浮动栏命令失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过IPC将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>
|
||||
public static void StartIpcListener()
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread ipcThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("启动IPC监听器", LogHelper.LogType.Event);
|
||||
|
||||
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// 等待IPC事件
|
||||
if (ipcEvent.WaitOne(IpcTimeout))
|
||||
{
|
||||
// 处理IPC文件
|
||||
ProcessIpcFiles();
|
||||
|
||||
// 重置事件
|
||||
ipcEvent.Reset();
|
||||
}
|
||||
|
||||
// 检查应用是否还在运行
|
||||
if (Application.Current == null || Application.Current.Dispatcher == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC监听器出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcThread.IsBackground = true;
|
||||
ipcThread.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动IPC监听器失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理IPC文件
|
||||
/// </summary>
|
||||
private static void ProcessIpcFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
string tempDir = Path.GetTempPath();
|
||||
|
||||
// 处理文件路径IPC文件
|
||||
string[] ipcFiles = Directory.GetFiles(tempDir, IpcFilePrefix + "*.tmp");
|
||||
foreach (string ipcFile in ipcFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取文件路径
|
||||
string filePath = File.ReadAllText(ipcFile, Encoding.UTF8);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC接收到文件路径: {filePath}", LogHelper.LogType.Event);
|
||||
|
||||
// 在UI线程中处理文件打开
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取主窗口并打开文件
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.OpenSingleStrokeFile(filePath);
|
||||
mainWindow.ShowNotification($"已加载墨迹文件: {Path.GetFileName(filePath)}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC处理文件打开失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 删除IPC文件
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
|
||||
// 尝试删除损坏的IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFile))
|
||||
{
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
|
||||
// 处理白板模式命令IPC文件
|
||||
string[] boardModeFiles = Directory.GetFiles(tempDir, IpcBoardModePrefix + "*.tmp");
|
||||
foreach (string ipcFile in boardModeFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取命令内容
|
||||
string command = File.ReadAllText(ipcFile, Encoding.UTF8);
|
||||
|
||||
if (command == "BOARD_MODE")
|
||||
{
|
||||
LogHelper.WriteLogToFile("IPC接收到白板模式命令", LogHelper.LogType.Event);
|
||||
|
||||
// 在UI线程中处理白板模式切换
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取主窗口并切换到白板模式
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.SwitchToBoardMode();
|
||||
mainWindow.ShowNotification("已切换到白板模式");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC处理白板模式切换失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 删除IPC文件
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理白板模式IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
|
||||
// 尝试删除损坏的IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFile))
|
||||
{
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
|
||||
// 处理展开浮动栏命令IPC文件
|
||||
string[] showModeFiles = Directory.GetFiles(tempDir, IpcShowModePrefix + "*.tmp");
|
||||
foreach (string ipcFile in showModeFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取命令内容
|
||||
string command = File.ReadAllText(ipcFile, Encoding.UTF8);
|
||||
|
||||
if (command == "SHOW_MODE")
|
||||
{
|
||||
LogHelper.WriteLogToFile("IPC接收到展开浮动栏命令", LogHelper.LogType.Event);
|
||||
|
||||
// 在UI线程中处理展开浮动栏
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取主窗口并展开浮动栏
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
// 如果当前处于收纳模式,则展开浮动栏
|
||||
if (mainWindow.isFloatingBarFolded)
|
||||
{
|
||||
await mainWindow.UnFoldFloatingBar(new object());
|
||||
}
|
||||
mainWindow.ShowNotification("已退出收纳模式并恢复浮动栏");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC处理展开浮动栏失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 删除IPC文件
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理展开浮动栏IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
|
||||
// 尝试删除损坏的IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFile))
|
||||
{
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch (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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理IPC文件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("shell32.dll")]
|
||||
private static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,398 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
/// <summary>
|
||||
/// 悬浮窗拦截管理器
|
||||
/// </summary>
|
||||
public class FloatingWindowInterceptorManager : IDisposable
|
||||
{
|
||||
#region 私有字段
|
||||
|
||||
private FloatingWindowInterceptor _interceptor;
|
||||
private bool _isInitialized;
|
||||
private bool _disposed;
|
||||
private FloatingWindowInterceptorSettings _settings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件
|
||||
|
||||
public event EventHandler<FloatingWindowInterceptor.WindowInterceptedEventArgs> WindowIntercepted;
|
||||
public event EventHandler<FloatingWindowInterceptor.WindowRestoredEventArgs> WindowRestored;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公共属性
|
||||
|
||||
public bool IsEnabled => _interceptor != null && _settings != null && _settings.IsEnabled;
|
||||
public bool IsRunning => _interceptor != null && _interceptor.IsRunning;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公共方法
|
||||
|
||||
/// <summary>
|
||||
/// 初始化拦截器
|
||||
/// </summary>
|
||||
public void Initialize(FloatingWindowInterceptorSettings settings)
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
try
|
||||
{
|
||||
_settings = settings ?? new FloatingWindowInterceptorSettings();
|
||||
_interceptor = new FloatingWindowInterceptor();
|
||||
|
||||
// 订阅事件
|
||||
_interceptor.WindowIntercepted += OnWindowIntercepted;
|
||||
_interceptor.WindowRestored += OnWindowRestored;
|
||||
|
||||
// 应用配置
|
||||
ApplySettings();
|
||||
|
||||
_isInitialized = true;
|
||||
|
||||
// 如果设置了自动启动,则启动拦截器
|
||||
if (_settings.AutoStart && _settings.IsEnabled)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动拦截器
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
if (!_isInitialized || _settings == null) return;
|
||||
|
||||
if (_interceptor == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_interceptor.Start(_settings.ScanIntervalMs);
|
||||
LogHelper.WriteLogToFile("悬浮窗拦截器已启动", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止拦截器
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (_interceptor == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_interceptor.Stop();
|
||||
LogHelper.WriteLogToFile("悬浮窗拦截器已停止", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"停止悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置拦截规则
|
||||
/// </summary>
|
||||
public void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
|
||||
{
|
||||
if (_interceptor == null || _settings == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_interceptor.SetInterceptRule(type, enabled);
|
||||
|
||||
// 更新设置
|
||||
var ruleName = type.ToString();
|
||||
if (_settings.InterceptRules.ContainsKey(ruleName))
|
||||
{
|
||||
_settings.InterceptRules[ruleName] = enabled;
|
||||
}
|
||||
|
||||
// 获取规则信息以处理父子关系
|
||||
var rule = _interceptor.GetInterceptRule(type);
|
||||
if (rule != null)
|
||||
{
|
||||
// 如果是父规则,更新所有子规则的设置
|
||||
if (rule.ChildTypes.Count > 0)
|
||||
{
|
||||
foreach (var childType in rule.ChildTypes)
|
||||
{
|
||||
var childRuleName = childType.ToString();
|
||||
if (_settings.InterceptRules.ContainsKey(childRuleName))
|
||||
{
|
||||
_settings.InterceptRules[childRuleName] = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果是子规则,更新父规则的设置
|
||||
else if (rule.ParentType.HasValue)
|
||||
{
|
||||
var parentRule = _interceptor.GetInterceptRule(rule.ParentType.Value);
|
||||
if (parentRule != null)
|
||||
{
|
||||
var parentRuleName = rule.ParentType.Value.ToString();
|
||||
if (_settings.InterceptRules.ContainsKey(parentRuleName))
|
||||
{
|
||||
_settings.InterceptRules[parentRuleName] = parentRule.IsEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"设置拦截规则失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取拦截规则
|
||||
/// </summary>
|
||||
public FloatingWindowInterceptor.InterceptRule GetInterceptRule(FloatingWindowInterceptor.InterceptType type)
|
||||
{
|
||||
return _interceptor?.GetInterceptRule(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有拦截规则
|
||||
/// </summary>
|
||||
public Dictionary<FloatingWindowInterceptor.InterceptType, FloatingWindowInterceptor.InterceptRule> GetAllRules()
|
||||
{
|
||||
return _interceptor?.GetAllRules() ?? new Dictionary<FloatingWindowInterceptor.InterceptType, FloatingWindowInterceptor.InterceptRule>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动扫描一次
|
||||
/// </summary>
|
||||
public void ScanOnce()
|
||||
{
|
||||
if (_interceptor == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_interceptor.ScanOnce();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"手动扫描失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 恢复所有被拦截的窗口
|
||||
/// </summary>
|
||||
public void RestoreAllWindows()
|
||||
{
|
||||
if (_interceptor == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_interceptor.RestoreAllWindows();
|
||||
LogHelper.WriteLogToFile("已恢复所有被拦截的窗口", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"恢复窗口失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用设置
|
||||
/// </summary>
|
||||
public void ApplySettings()
|
||||
{
|
||||
if (_interceptor == null || _settings == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 应用拦截规则设置
|
||||
foreach (var kvp in _settings.InterceptRules)
|
||||
{
|
||||
if (Enum.TryParse<FloatingWindowInterceptor.InterceptType>(kvp.Key, out var type))
|
||||
{
|
||||
_interceptor.SetInterceptRule(type, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果启用了拦截器,则启动
|
||||
if (_settings.IsEnabled && !IsRunning)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
// 如果禁用了拦截器,则停止
|
||||
else if (!_settings.IsEnabled && IsRunning)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用设置失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新扫描间隔
|
||||
/// </summary>
|
||||
public void UpdateScanInterval(int intervalMs)
|
||||
{
|
||||
if (_interceptor == null || _settings == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_settings.ScanIntervalMs = intervalMs;
|
||||
|
||||
// 如果正在运行,重启以应用新间隔
|
||||
if (IsRunning)
|
||||
{
|
||||
Stop();
|
||||
Start();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新扫描间隔失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取拦截统计信息
|
||||
/// </summary>
|
||||
public InterceptStatistics GetStatistics()
|
||||
{
|
||||
if (_interceptor == null || _settings == null) return new InterceptStatistics();
|
||||
|
||||
try
|
||||
{
|
||||
var rules = GetAllRules();
|
||||
var enabledRules = rules.Count(r => r.Value.IsEnabled);
|
||||
var totalRules = rules.Count;
|
||||
|
||||
return new InterceptStatistics
|
||||
{
|
||||
TotalRules = totalRules,
|
||||
EnabledRules = enabledRules,
|
||||
IsRunning = IsRunning,
|
||||
ScanIntervalMs = _settings.ScanIntervalMs
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取统计信息失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return new InterceptStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有方法
|
||||
|
||||
private void OnWindowIntercepted(object sender, FloatingWindowInterceptor.WindowInterceptedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 记录日志
|
||||
LogHelper.WriteLogToFile($"拦截窗口: {e.WindowTitle} ({e.InterceptType})", LogHelper.LogType.Event);
|
||||
|
||||
// 显示通知(如果启用)
|
||||
if (_settings != null && _settings.ShowNotifications)
|
||||
{
|
||||
ShowNotification($"已拦截悬浮窗: {e.Rule.Description}");
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
WindowIntercepted?.Invoke(this, e);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理窗口拦截事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowRestored(object sender, FloatingWindowInterceptor.WindowRestoredEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 记录日志
|
||||
LogHelper.WriteLogToFile($"恢复窗口: {e.InterceptType}", LogHelper.LogType.Event);
|
||||
|
||||
// 触发事件
|
||||
WindowRestored?.Invoke(this, e);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理窗口恢复事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotification(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 这里可以集成系统通知或自定义通知
|
||||
// 暂时使用调试输出
|
||||
System.Diagnostics.Debug.WriteLine($"通知: {message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"显示通知失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助类
|
||||
|
||||
public class InterceptStatistics
|
||||
{
|
||||
public int TotalRules { get; set; }
|
||||
public int EnabledRules { get; set; }
|
||||
public bool IsRunning { get; set; }
|
||||
public int ScanIntervalMs { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
Stop();
|
||||
_interceptor?.Dispose();
|
||||
_interceptor = null;
|
||||
_isInitialized = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"释放悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -36,6 +36,29 @@ 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 IntPtr GetForegroundWindowHandle()
|
||||
{
|
||||
return GetForegroundWindow();
|
||||
}
|
||||
|
||||
public static string WindowTitle()
|
||||
{
|
||||
IntPtr foregroundWindowHandle = GetForegroundWindow();
|
||||
@@ -106,10 +129,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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
@@ -189,9 +188,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
/// <summary>
|
||||
/// 确保窗口全屏的Hook
|
||||
/// 使用HandleProcessCorruptedStateExceptions,防止访问内存过程中因为一些致命异常导致程序崩溃
|
||||
/// </summary>
|
||||
[HandleProcessCorruptedStateExceptions]
|
||||
private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
//处理WM_WINDOWPOSCHANGING消息
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
private readonly RenderTargetBitmap _renderTarget;
|
||||
private readonly DrawingVisual _drawingVisual;
|
||||
private readonly DrawingContext _drawingContext;
|
||||
private bool _isInitialized;
|
||||
|
||||
public HardwareAcceleratedInkProcessor(int width = 1920, int height = 1080)
|
||||
@@ -187,71 +186,14 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放GPU资源
|
||||
/// 释放GPU相关资源标记
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_drawingContext?.Close();
|
||||
_renderTarget?.Clear();
|
||||
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 质量配置枚举
|
||||
/// </summary>
|
||||
public enum InkSmoothingQuality
|
||||
{
|
||||
HighPerformance = 0, // 高性能低质量
|
||||
Balanced = 1, // 平衡
|
||||
HighQuality = 2 // 高质量低性能
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 墨迹平滑配置
|
||||
/// </summary>
|
||||
public class InkSmoothingConfig
|
||||
{
|
||||
public InkSmoothingQuality Quality { get; set; } = InkSmoothingQuality.HighQuality;
|
||||
public bool UseHardwareAcceleration { get; set; } = true;
|
||||
public bool UseAsyncProcessing { get; set; } = true;
|
||||
public int MaxConcurrentTasks { get; set; } = Environment.ProcessorCount;
|
||||
public double SmoothingStrength { get; set; } = 0.8; // 高质量模式的平滑强度
|
||||
public double ResampleInterval { get; set; } = 0.8; // 高质量模式的重采样间隔
|
||||
public int InterpolationSteps { get; set; } = 64; // 高质量模式的插值步数
|
||||
|
||||
public static InkSmoothingConfig FromSettings()
|
||||
{
|
||||
return new InkSmoothingConfig
|
||||
{
|
||||
Quality = (InkSmoothingQuality)MainWindow.Settings.Canvas.InkSmoothingQuality,
|
||||
UseHardwareAcceleration = MainWindow.Settings.Canvas.UseHardwareAcceleration,
|
||||
UseAsyncProcessing = MainWindow.Settings.Canvas.UseAsyncInkSmoothing,
|
||||
MaxConcurrentTasks = MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks > 0 ?
|
||||
MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks : Environment.ProcessorCount
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyQualitySettings()
|
||||
{
|
||||
switch (Quality)
|
||||
{
|
||||
case InkSmoothingQuality.HighPerformance:
|
||||
SmoothingStrength = 0.4;
|
||||
ResampleInterval = 2.0;
|
||||
InterpolationSteps = 16;
|
||||
break;
|
||||
case InkSmoothingQuality.Balanced:
|
||||
SmoothingStrength = 0.6;
|
||||
ResampleInterval = 1.2;
|
||||
InterpolationSteps = 32;
|
||||
break;
|
||||
case InkSmoothingQuality.HighQuality:
|
||||
SmoothingStrength = 0.8;
|
||||
ResampleInterval = 0.8;
|
||||
InterpolationSteps = 64;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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,325 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 改进的三次贝塞尔曲线平滑算法
|
||||
/// </summary>
|
||||
public class ImprovedBezierSmoothing
|
||||
{
|
||||
private readonly InkSmoothingConfig _config;
|
||||
|
||||
public ImprovedBezierSmoothing(InkSmoothingConfig config = null)
|
||||
{
|
||||
_config = config ?? new InkSmoothingConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用改进的贝塞尔曲线算法平滑笔画
|
||||
/// </summary>
|
||||
public Stroke SmoothStroke(Stroke originalStroke)
|
||||
{
|
||||
if (originalStroke == null || originalStroke.StylusPoints.Count < 3)
|
||||
return originalStroke;
|
||||
|
||||
var originalPoints = originalStroke.StylusPoints.ToArray();
|
||||
|
||||
// 预处理:去除噪声点
|
||||
var cleanedPoints = RemoveNoisePoints(originalPoints);
|
||||
|
||||
// 使用改进的贝塞尔曲线拟合
|
||||
var smoothedPoints = ApplyCubicBezierSmoothing(cleanedPoints);
|
||||
|
||||
// 后处理:重采样和优化
|
||||
var finalPoints = PostProcessPoints(smoothedPoints);
|
||||
|
||||
return new Stroke(new StylusPointCollection(finalPoints))
|
||||
{
|
||||
DrawingAttributes = originalStroke.DrawingAttributes.Clone()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 去除噪声点
|
||||
/// </summary>
|
||||
private StylusPoint[] RemoveNoisePoints(StylusPoint[] points)
|
||||
{
|
||||
if (points.Length < 3) return points;
|
||||
|
||||
var result = new List<StylusPoint> { points[0] };
|
||||
double minDistance = _config.ResampleInterval * 0.5;
|
||||
|
||||
for (int i = 1; i < points.Length - 1; i++)
|
||||
{
|
||||
var prev = result[result.Count - 1];
|
||||
var curr = points[i];
|
||||
var next = points[i + 1];
|
||||
|
||||
// 计算到前一个点的距离
|
||||
double distToPrev = Math.Sqrt((curr.X - prev.X) * (curr.X - prev.X) +
|
||||
(curr.Y - prev.Y) * (curr.Y - prev.Y));
|
||||
|
||||
// 如果距离太近,跳过这个点
|
||||
if (distToPrev < minDistance)
|
||||
continue;
|
||||
|
||||
// 检查是否为异常点(与前后点形成锐角)
|
||||
if (IsOutlierPoint(prev, curr, next))
|
||||
continue;
|
||||
|
||||
result.Add(curr);
|
||||
}
|
||||
|
||||
result.Add(points[points.Length - 1]);
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否为异常点
|
||||
/// </summary>
|
||||
private bool IsOutlierPoint(StylusPoint prev, StylusPoint curr, StylusPoint next)
|
||||
{
|
||||
var v1 = new Vector(curr.X - prev.X, curr.Y - prev.Y);
|
||||
var v2 = new Vector(next.X - curr.X, next.Y - curr.Y);
|
||||
|
||||
if (v1.Length == 0 || v2.Length == 0) return false;
|
||||
|
||||
v1.Normalize();
|
||||
v2.Normalize();
|
||||
|
||||
double dotProduct = Vector.Multiply(v1, v2);
|
||||
double angle = Math.Acos(Math.Max(-1, Math.Min(1, dotProduct)));
|
||||
|
||||
// 如果角度小于30度,认为是异常点
|
||||
return angle < Math.PI / 6;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用三次贝塞尔曲线平滑
|
||||
/// </summary>
|
||||
private StylusPoint[] ApplyCubicBezierSmoothing(StylusPoint[] points)
|
||||
{
|
||||
if (points.Length < 4) return points;
|
||||
|
||||
var result = new List<StylusPoint>();
|
||||
result.Add(points[0]);
|
||||
|
||||
// 使用滑动窗口进行贝塞尔曲线拟合
|
||||
for (int i = 0; i <= points.Length - 4; i++)
|
||||
{
|
||||
var p0 = points[i];
|
||||
var p1 = points[i + 1];
|
||||
var p2 = points[i + 2];
|
||||
var p3 = points[i + 3];
|
||||
|
||||
// 计算控制点
|
||||
var controlPoints = CalculateOptimalControlPoints(p0, p1, p2, p3);
|
||||
|
||||
// 计算插值步数
|
||||
int steps = CalculateInterpolationSteps(p0, p1, p2, p3);
|
||||
|
||||
// 生成贝塞尔曲线点
|
||||
for (int j = 1; j <= steps; j++)
|
||||
{
|
||||
double t = (double)j / steps;
|
||||
var bezierPoint = CalculateBezierPoint(p0, controlPoints.cp1, controlPoints.cp2, p3, t);
|
||||
result.Add(bezierPoint);
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(points[points.Length - 1]);
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算最优控制点
|
||||
/// </summary>
|
||||
private (Point cp1, Point cp2) CalculateOptimalControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
|
||||
{
|
||||
// 计算切线方向
|
||||
var tangent1 = CalculateTangent(p0, p1, p2);
|
||||
var tangent2 = CalculateTangent(p1, p2, p3);
|
||||
|
||||
// 计算控制点距离
|
||||
double dist1 = CalculateDistance(p0, p1);
|
||||
double dist2 = CalculateDistance(p2, p3);
|
||||
|
||||
double controlDist1 = dist1 * _config.CurveTension;
|
||||
double controlDist2 = dist2 * _config.CurveTension;
|
||||
|
||||
// 计算控制点
|
||||
var cp1 = new Point(
|
||||
p1.X + tangent1.X * controlDist1,
|
||||
p1.Y + tangent1.Y * controlDist1
|
||||
);
|
||||
|
||||
var cp2 = new Point(
|
||||
p2.X - tangent2.X * controlDist2,
|
||||
p2.Y - tangent2.Y * controlDist2
|
||||
);
|
||||
|
||||
return (cp1, cp2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算切线方向
|
||||
/// </summary>
|
||||
private Vector CalculateTangent(StylusPoint p0, StylusPoint p1, StylusPoint p2)
|
||||
{
|
||||
var v1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
|
||||
var v2 = new Vector(p2.X - p1.X, p2.Y - p1.Y);
|
||||
|
||||
// 如果向量长度为零,返回零向量
|
||||
if (v1.Length == 0 || v2.Length == 0)
|
||||
return new Vector(0, 0);
|
||||
|
||||
v1.Normalize();
|
||||
v2.Normalize();
|
||||
|
||||
// 返回平均方向
|
||||
var tangent = (v1 + v2) / 2;
|
||||
if (tangent.Length > 0)
|
||||
tangent.Normalize();
|
||||
|
||||
return tangent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算两点间距离
|
||||
/// </summary>
|
||||
private double CalculateDistance(StylusPoint p1, StylusPoint p2)
|
||||
{
|
||||
double dx = p2.X - p1.X;
|
||||
double dy = p2.Y - p1.Y;
|
||||
return Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算插值步数
|
||||
/// </summary>
|
||||
private int CalculateInterpolationSteps(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
|
||||
{
|
||||
if (!_config.UseAdaptiveInterpolation)
|
||||
return _config.InterpolationSteps;
|
||||
|
||||
// 计算曲线长度
|
||||
double totalLength = CalculateDistance(p0, p1) + CalculateDistance(p1, p2) + CalculateDistance(p2, p3);
|
||||
|
||||
// 计算曲率
|
||||
double curvature = CalculateCurvature(p0, p1, p2, p3);
|
||||
|
||||
// 基于长度和曲率计算步数
|
||||
int baseSteps = Math.Max(8, Math.Min(20, (int)(totalLength / 10)));
|
||||
int curvatureSteps = (int)(curvature * 15);
|
||||
|
||||
return Math.Max(_config.InterpolationSteps, Math.Min(30, baseSteps + curvatureSteps));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算曲率
|
||||
/// </summary>
|
||||
private double CalculateCurvature(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
|
||||
{
|
||||
var v1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
|
||||
var v2 = new Vector(p2.X - p1.X, p2.Y - p1.Y);
|
||||
var v3 = new Vector(p3.X - p2.X, p3.Y - p2.Y);
|
||||
|
||||
if (v1.Length == 0 || v2.Length == 0 || v3.Length == 0) return 0;
|
||||
|
||||
v1.Normalize();
|
||||
v2.Normalize();
|
||||
v3.Normalize();
|
||||
|
||||
// 计算角度变化
|
||||
double angle1 = Math.Acos(Math.Max(-1, Math.Min(1, Vector.Multiply(v1, v2))));
|
||||
double angle2 = Math.Acos(Math.Max(-1, Math.Min(1, Vector.Multiply(v2, v3))));
|
||||
|
||||
return (angle1 + angle2) / Math.PI; // 归一化到0-1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算贝塞尔曲线上的点
|
||||
/// </summary>
|
||||
private StylusPoint CalculateBezierPoint(StylusPoint p0, Point cp1, Point cp2, StylusPoint p3, double t)
|
||||
{
|
||||
double u = 1 - t;
|
||||
double tt = t * t;
|
||||
double uu = u * u;
|
||||
double uuu = uu * u;
|
||||
double ttt = tt * t;
|
||||
|
||||
// 预计算系数
|
||||
double c0 = uuu;
|
||||
double c1 = 3 * uu * t;
|
||||
double c2 = 3 * u * tt;
|
||||
double c3 = ttt;
|
||||
|
||||
double x = c0 * p0.X + c1 * cp1.X + c2 * cp2.X + c3 * p3.X;
|
||||
double y = c0 * p0.Y + c1 * cp1.Y + c2 * cp2.Y + c3 * p3.Y;
|
||||
|
||||
// 插值压力值
|
||||
float pressure = (float)(p0.PressureFactor * u + p3.PressureFactor * t);
|
||||
pressure = Math.Max(pressure, 0.1f);
|
||||
|
||||
return new StylusPoint(x, y, pressure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后处理点集
|
||||
/// </summary>
|
||||
private StylusPoint[] PostProcessPoints(StylusPoint[] points)
|
||||
{
|
||||
if (points.Length == 0) return points;
|
||||
|
||||
// 如果点数过多,进行重采样
|
||||
if (points.Length > _config.MaxPointsPerStroke)
|
||||
{
|
||||
return ResamplePoints(points, _config.ResampleInterval);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重采样点集
|
||||
/// </summary>
|
||||
private StylusPoint[] ResamplePoints(StylusPoint[] points, double interval)
|
||||
{
|
||||
var result = new List<StylusPoint> { points[0] };
|
||||
double accumulated = 0;
|
||||
|
||||
for (int i = 1; i < points.Length; i++)
|
||||
{
|
||||
var prev = result[result.Count - 1];
|
||||
var curr = points[i];
|
||||
double dx = curr.X - prev.X;
|
||||
double dy = curr.Y - prev.Y;
|
||||
double dist = Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist + accumulated >= interval)
|
||||
{
|
||||
double t = (interval - accumulated) / dist;
|
||||
double x = prev.X + t * dx;
|
||||
double y = prev.Y + t * dy;
|
||||
float pressure = (float)(prev.PressureFactor * (1 - t) + curr.PressureFactor * t);
|
||||
pressure = Math.Max(pressure, 0.1f);
|
||||
|
||||
result.Add(new StylusPoint(x, y, pressure));
|
||||
accumulated = 0;
|
||||
i--; // 重新处理当前点
|
||||
}
|
||||
else
|
||||
{
|
||||
accumulated += dist;
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,895 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 墨迹渐隐管理器 - 管理墨迹的渐隐动画和状态
|
||||
/// </summary>
|
||||
public class InkFadeManager
|
||||
{
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// 是否启用墨迹渐隐功能
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 墨迹渐隐时间(毫秒)
|
||||
/// </summary>
|
||||
public int FadeTime { get; set; } = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// 渐隐动画持续时间(毫秒)
|
||||
/// </summary>
|
||||
public int AnimationDuration { get; set; } = 1000;
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
private readonly MainWindow _mainWindow;
|
||||
private readonly Dispatcher _dispatcher;
|
||||
private readonly Dictionary<Stroke, DispatcherTimer> _fadeTimers;
|
||||
private readonly Dictionary<Stroke, UIElement> _strokeVisuals;
|
||||
private readonly Dictionary<Stroke, Point> _strokeStartPoints;
|
||||
private readonly Dictionary<Stroke, Point> _strokeEndPoints;
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public InkFadeManager(MainWindow mainWindow)
|
||||
{
|
||||
_mainWindow = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow));
|
||||
_dispatcher = _mainWindow.Dispatcher;
|
||||
_fadeTimers = new Dictionary<Stroke, DispatcherTimer>();
|
||||
_strokeVisuals = new Dictionary<Stroke, UIElement>();
|
||||
_strokeStartPoints = new Dictionary<Stroke, Point>();
|
||||
_strokeEndPoints = new Dictionary<Stroke, Point>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// 添加需要渐隐的墨迹
|
||||
/// </summary>
|
||||
/// <param name="stroke">墨迹对象</param>
|
||||
/// <param name="startPoint">落笔点</param>
|
||||
/// <param name="endPoint">抬笔点</param>
|
||||
public void AddFadingStroke(Stroke stroke, Point startPoint, Point endPoint)
|
||||
{
|
||||
if (!IsEnabled || stroke == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 确保主窗口的InkCanvas保持Ink编辑模式,防止墨迹渐隐时切换到鼠标模式
|
||||
if (_mainWindow.inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
|
||||
{
|
||||
_mainWindow.inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
|
||||
}
|
||||
|
||||
// 记录墨迹的起点和终点
|
||||
_strokeStartPoints[stroke] = startPoint;
|
||||
_strokeEndPoints[stroke] = endPoint;
|
||||
|
||||
// 创建墨迹的视觉元素(湿墨迹状态)
|
||||
var strokeVisual = CreateStrokeVisual(stroke);
|
||||
if (strokeVisual == null) return;
|
||||
|
||||
_strokeVisuals[stroke] = strokeVisual;
|
||||
|
||||
// 创建定时器,在指定时间后开始渐隐动画
|
||||
var timer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(FadeTime)
|
||||
};
|
||||
|
||||
timer.Tick += (sender, e) =>
|
||||
{
|
||||
StartFadeAnimation(stroke);
|
||||
timer.Stop();
|
||||
_fadeTimers.Remove(stroke);
|
||||
};
|
||||
|
||||
_fadeTimers[stroke] = timer;
|
||||
timer.Start();
|
||||
|
||||
// 将视觉元素添加到画布上
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_mainWindow.inkCanvas != null)
|
||||
{
|
||||
// 将墨迹添加到 inkCanvas 的父容器中,而不是 inkCanvas.Children
|
||||
// 这样可以避免坐标系统问题
|
||||
var parent = _mainWindow.inkCanvas.Parent as Panel;
|
||||
if (parent != null)
|
||||
{
|
||||
parent.Children.Add(strokeVisual);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果无法获取父容器,则添加到 inkCanvas.Children
|
||||
_mainWindow.inkCanvas.Children.Add(strokeVisual);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"添加墨迹视觉元素到画布失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"添加渐隐墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除墨迹
|
||||
/// </summary>
|
||||
/// <param name="stroke">要移除的墨迹</param>
|
||||
public void RemoveStroke(Stroke stroke)
|
||||
{
|
||||
if (stroke == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (_fadeTimers.TryGetValue(stroke, out var timer))
|
||||
{
|
||||
timer.Stop();
|
||||
_fadeTimers.Remove(stroke);
|
||||
}
|
||||
|
||||
if (_strokeVisuals.TryGetValue(stroke, out var visual))
|
||||
{
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从父容器中移除墨迹
|
||||
var parent = _mainWindow.inkCanvas?.Parent as Panel;
|
||||
if (parent != null && parent.Children.Contains(visual))
|
||||
{
|
||||
parent.Children.Remove(visual);
|
||||
}
|
||||
else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(visual))
|
||||
{
|
||||
_mainWindow.inkCanvas.Children.Remove(visual);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从画布移除墨迹视觉元素失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
|
||||
_strokeVisuals.Remove(stroke);
|
||||
}
|
||||
|
||||
_strokeStartPoints.Remove(stroke);
|
||||
_strokeEndPoints.Remove(stroke);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"移除渐隐墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有渐隐墨迹
|
||||
/// </summary>
|
||||
public void ClearAllFadingStrokes()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var timer in _fadeTimers.Values)
|
||||
{
|
||||
timer.Stop();
|
||||
}
|
||||
|
||||
_fadeTimers.Clear();
|
||||
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_mainWindow.inkCanvas != null)
|
||||
{
|
||||
var parent = _mainWindow.inkCanvas.Parent as Panel;
|
||||
foreach (var visual in _strokeVisuals.Values)
|
||||
{
|
||||
if (parent != null && parent.Children.Contains(visual))
|
||||
{
|
||||
parent.Children.Remove(visual);
|
||||
}
|
||||
else if (_mainWindow.inkCanvas.Children.Contains(visual))
|
||||
{
|
||||
_mainWindow.inkCanvas.Children.Remove(visual);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清除所有墨迹视觉元素失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
|
||||
_strokeVisuals.Clear();
|
||||
_strokeStartPoints.Clear();
|
||||
_strokeEndPoints.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清除所有渐隐墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新渐隐时间设置
|
||||
/// </summary>
|
||||
/// <param name="fadeTime">新的渐隐时间(毫秒)</param>
|
||||
public void UpdateFadeTime(int fadeTime)
|
||||
{
|
||||
FadeTime = fadeTime;
|
||||
|
||||
foreach (var kvp in _fadeTimers)
|
||||
{
|
||||
var stroke = kvp.Key;
|
||||
var timer = kvp.Value;
|
||||
|
||||
timer.Stop();
|
||||
timer.Interval = TimeSpan.FromMilliseconds(FadeTime);
|
||||
timer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 启用墨迹渐隐功能
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
IsEnabled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用墨迹渐隐功能
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
IsEnabled = false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
/// <summary>
|
||||
/// 创建墨迹的视觉元素
|
||||
/// </summary>
|
||||
/// <param name="stroke">墨迹对象</param>
|
||||
/// <returns>视觉元素</returns>
|
||||
private UIElement CreateStrokeVisual(Stroke stroke)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建路径几何,使用墨迹的实际位置
|
||||
var geometry = stroke.GetGeometry();
|
||||
if (geometry == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取绘画属性
|
||||
var drawingAttribs = stroke.DrawingAttributes;
|
||||
|
||||
// 创建路径元素,确保使用正确的绘画属性
|
||||
var path = new Path
|
||||
{
|
||||
Data = geometry,
|
||||
Stroke = new SolidColorBrush(drawingAttribs.Color),
|
||||
StrokeThickness = drawingAttribs.Width, // 使用原始墨迹的粗细
|
||||
StrokeStartLineCap = PenLineCap.Round,
|
||||
StrokeEndLineCap = PenLineCap.Round,
|
||||
StrokeLineJoin = PenLineJoin.Round,
|
||||
Fill = drawingAttribs.IsHighlighter ? new SolidColorBrush(drawingAttribs.Color) : null, // 高亮笔需要填充
|
||||
Opacity = 0.95, // 初始透明度更高,显得更自然
|
||||
|
||||
// 优化渲染质量
|
||||
UseLayoutRounding = false,
|
||||
SnapsToDevicePixels = false
|
||||
};
|
||||
|
||||
// 如果是高亮笔,调整透明度和混合模式
|
||||
if (drawingAttribs.IsHighlighter)
|
||||
{
|
||||
path.Opacity = 0.4; // 高亮笔初始透明度更低,更符合荧光笔特性
|
||||
|
||||
// 为高亮笔添加特殊的混合效果
|
||||
// 使用更柔和的笔触样式
|
||||
path.StrokeStartLineCap = PenLineCap.Flat;
|
||||
path.StrokeEndLineCap = PenLineCap.Flat;
|
||||
path.StrokeLineJoin = PenLineJoin.Miter;
|
||||
|
||||
// 高亮笔通常需要更宽的笔触来覆盖下面的内容
|
||||
if (drawingAttribs.Width < 20)
|
||||
{
|
||||
path.StrokeThickness = Math.Max(drawingAttribs.Width * 1.5, 20);
|
||||
}
|
||||
|
||||
// 为高亮笔添加轻微的模糊效果,使渐隐更加自然
|
||||
path.Effect = new BlurEffect
|
||||
{
|
||||
Radius = 0.5, // 轻微的模糊效果
|
||||
KernelType = KernelType.Gaussian
|
||||
};
|
||||
}
|
||||
|
||||
// 不设置任何变换,保持墨迹原有粗细
|
||||
var bounds = geometry.Bounds;
|
||||
|
||||
// 设置墨迹的初始位置
|
||||
System.Windows.Controls.Canvas.SetLeft(path, bounds.Left);
|
||||
System.Windows.Controls.Canvas.SetTop(path, bounds.Top);
|
||||
|
||||
return path;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始渐隐动画
|
||||
/// </summary>
|
||||
/// <param name="stroke">要渐隐的墨迹</param>
|
||||
private void StartFadeAnimation(Stroke stroke)
|
||||
{
|
||||
if (!_strokeVisuals.TryGetValue(stroke, out var visual)) return;
|
||||
|
||||
try
|
||||
{
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
// 获取当前透明度和判断是否为高亮笔
|
||||
var currentOpacity = visual.Opacity;
|
||||
var isHighlighter = stroke.DrawingAttributes.IsHighlighter;
|
||||
|
||||
// 根据墨迹类型选择不同的动画效果
|
||||
if (isHighlighter)
|
||||
{
|
||||
StartHighlighterFadeAnimation(visual, stroke, currentOpacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
StartNormalStrokeFadeAnimation(visual, stroke, currentOpacity);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"开始渐隐动画失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始普通墨迹的渐隐动画
|
||||
/// </summary>
|
||||
private void StartNormalStrokeFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity)
|
||||
{
|
||||
try
|
||||
{
|
||||
StartProgressiveFadeAnimation(visual, stroke, currentOpacity, AnimationDuration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"开始普通墨迹渐隐动画失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一渐隐动画 - 整个墨迹作为一个整体进行渐隐,与擦除效果一致
|
||||
/// </summary>
|
||||
private void StartUnifiedFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建透明度动画,模拟擦除时的效果
|
||||
var fadeAnimation = new DoubleAnimation
|
||||
{
|
||||
From = currentOpacity,
|
||||
To = 0.0,
|
||||
Duration = TimeSpan.FromMilliseconds(duration),
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut }
|
||||
};
|
||||
|
||||
// 如果是高亮笔,添加轻微的缩放效果,使渐隐更加自然
|
||||
if (stroke.DrawingAttributes.IsHighlighter)
|
||||
{
|
||||
// 创建轻微的缩放动画,模拟墨迹"蒸发"的效果
|
||||
var scaleAnimation = new DoubleAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0.95, // 轻微缩小,增加自然感
|
||||
Duration = TimeSpan.FromMilliseconds(duration),
|
||||
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn }
|
||||
};
|
||||
|
||||
// 创建缩放变换
|
||||
var scaleTransform = new ScaleTransform();
|
||||
visual.RenderTransform = scaleTransform;
|
||||
visual.RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
|
||||
// 应用缩放动画
|
||||
scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation);
|
||||
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation);
|
||||
}
|
||||
|
||||
// 添加动画完成事件
|
||||
fadeAnimation.Completed += (sender, e) => OnAnimationCompleted(visual, stroke);
|
||||
|
||||
// 应用透明度动画
|
||||
visual.BeginAnimation(UIElement.OpacityProperty, fadeAnimation);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"统一渐隐动画失败: {ex}", LogHelper.LogType.Error);
|
||||
OnAnimationCompleted(visual, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始高亮笔的渐隐动画
|
||||
/// </summary>
|
||||
private void StartHighlighterFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 高亮笔使用统一的渐隐动画,与擦除效果一致
|
||||
StartUnifiedFadeAnimation(visual, stroke, currentOpacity, (int)(AnimationDuration * 1.2));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"开始高亮笔渐隐动画失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渐进式渐隐动画 - 从起点到终点逐渐消失
|
||||
/// </summary>
|
||||
private void StartProgressiveFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保所有墨迹都能显示动画,包括短墨迹
|
||||
if (stroke.StylusPoints.Count < 2)
|
||||
{
|
||||
// 只有1个点的墨迹也使用分段动画,确保视觉效果
|
||||
CreateSegmentedStroke(visual, stroke, currentOpacity, duration);
|
||||
return;
|
||||
}
|
||||
|
||||
// 将墨迹分段并创建多个 Path
|
||||
CreateSegmentedStroke(visual, stroke, currentOpacity, duration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"渐进式渐隐动画失败: {ex}", LogHelper.LogType.Error);
|
||||
// 失败时回退到简单动画
|
||||
StartSimpleFadeAnimation(visual, stroke, currentOpacity, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建分段墨迹并开始渐进消失
|
||||
/// </summary>
|
||||
private void CreateSegmentedStroke(UIElement originalVisual, Stroke stroke, double opacity, int duration)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stylusPoints = stroke.StylusPoints;
|
||||
var totalPoints = stylusPoints.Count;
|
||||
|
||||
// 分段算法 - 确保所有墨迹都有足够的动画效果
|
||||
var strokeLength = CalculateStrokeLength(stylusPoints);
|
||||
var segmentCount = CalculateOptimalSegmentCount(totalPoints, strokeLength);
|
||||
|
||||
// 强制最小分段数量,确保短墨迹也有动画效果
|
||||
segmentCount = Math.Max(segmentCount, 4);
|
||||
|
||||
var pointsPerSegment = Math.Max(1, totalPoints / segmentCount);
|
||||
|
||||
// 隐藏原始视觉元素
|
||||
originalVisual.Visibility = Visibility.Hidden;
|
||||
|
||||
var segments = new List<UIElement>();
|
||||
var parent = _mainWindow.inkCanvas?.Parent as Panel;
|
||||
if (parent == null)
|
||||
{
|
||||
// 如果父容器不是Panel,直接使用InkCanvas
|
||||
parent = null; // 稍后会检查并使用InkCanvas.Children
|
||||
}
|
||||
|
||||
// 创建各个分段 - 确保短墨迹也能正确分段
|
||||
for (int i = 0; i < segmentCount; i++)
|
||||
{
|
||||
var startIndex = i * pointsPerSegment;
|
||||
var endIndex = (i == segmentCount - 1) ? totalPoints - 1 : (i + 1) * pointsPerSegment;
|
||||
|
||||
// 确保有足够的点来创建分段,对于短墨迹特殊处理
|
||||
if (endIndex <= startIndex && totalPoints > 1)
|
||||
{
|
||||
// 短墨迹:每个点作为一个分段
|
||||
startIndex = i;
|
||||
endIndex = Math.Min(i + 1, totalPoints - 1);
|
||||
}
|
||||
|
||||
// 为每个分段添加重叠,确保连接处平滑
|
||||
var overlap = Math.Max(1, pointsPerSegment / 6); // 15%的重叠,平衡平滑与速度
|
||||
var actualStartIndex = Math.Max(0, startIndex - overlap);
|
||||
var actualEndIndex = Math.Min(totalPoints - 1, endIndex + overlap);
|
||||
|
||||
var segment = CreateStrokeSegment(stroke, actualStartIndex, actualEndIndex, opacity);
|
||||
if (segment != null)
|
||||
{
|
||||
segments.Add(segment);
|
||||
if (parent != null)
|
||||
{
|
||||
parent.Children.Add(segment);
|
||||
}
|
||||
else if (_mainWindow.inkCanvas != null)
|
||||
{
|
||||
_mainWindow.inkCanvas.Children.Add(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始分段渐隐动画
|
||||
StartSegmentedFadeAnimation(segments, stroke, originalVisual, duration);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
StartSimpleFadeAnimation(originalVisual, stroke, opacity, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建墨迹分段
|
||||
/// </summary>
|
||||
private UIElement CreateStrokeSegment(Stroke originalStroke, int startIndex, int endIndex, double opacity)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建分段的 StylusPoint 集合
|
||||
var segmentPoints = new StylusPointCollection();
|
||||
for (int i = startIndex; i <= endIndex && i < originalStroke.StylusPoints.Count; i++)
|
||||
{
|
||||
segmentPoints.Add(originalStroke.StylusPoints[i]);
|
||||
}
|
||||
|
||||
if (segmentPoints.Count < 2) return null;
|
||||
|
||||
// 创建分段墨迹
|
||||
var segmentStroke = new Stroke(segmentPoints)
|
||||
{
|
||||
DrawingAttributes = originalStroke.DrawingAttributes.Clone()
|
||||
};
|
||||
|
||||
// 创建分段的视觉元素
|
||||
var geometry = segmentStroke.GetGeometry();
|
||||
if (geometry == null) return null;
|
||||
|
||||
var drawingAttribs = segmentStroke.DrawingAttributes;
|
||||
var path = new Path
|
||||
{
|
||||
Data = geometry,
|
||||
Stroke = new SolidColorBrush(drawingAttribs.Color),
|
||||
StrokeThickness = drawingAttribs.Width,
|
||||
StrokeStartLineCap = drawingAttribs.IsHighlighter ? PenLineCap.Flat : PenLineCap.Round,
|
||||
StrokeEndLineCap = drawingAttribs.IsHighlighter ? PenLineCap.Flat : PenLineCap.Round,
|
||||
StrokeLineJoin = drawingAttribs.IsHighlighter ? PenLineJoin.Miter : PenLineJoin.Round,
|
||||
Fill = drawingAttribs.IsHighlighter ? new SolidColorBrush(drawingAttribs.Color) : null,
|
||||
Opacity = opacity,
|
||||
UseLayoutRounding = false,
|
||||
SnapsToDevicePixels = false
|
||||
};
|
||||
|
||||
// 设置位置
|
||||
var bounds = geometry.Bounds;
|
||||
System.Windows.Controls.Canvas.SetLeft(path, bounds.Left);
|
||||
System.Windows.Controls.Canvas.SetTop(path, bounds.Top);
|
||||
|
||||
return path;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始分段渐隐动画
|
||||
/// </summary>
|
||||
private void StartSegmentedFadeAnimation(List<UIElement> segments, Stroke originalStroke, UIElement originalVisual, int totalDuration)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 动画时序算法
|
||||
var segmentDuration = CalculateOptimalSegmentDuration(totalDuration, segments.Count);
|
||||
var animationCurve = CreateAppleStyleAnimationCurve(segments.Count, totalDuration);
|
||||
|
||||
// 跟踪动画完成状态
|
||||
var completedSegments = new HashSet<UIElement>();
|
||||
var totalSegments = segments.Count;
|
||||
|
||||
// 渐隐效果 - 使用自然的动画曲线
|
||||
for (int i = 0; i < segments.Count; i++)
|
||||
{
|
||||
var segment = segments[i];
|
||||
|
||||
// 使用预计算的动画曲线获取延迟时间
|
||||
var delay = animationCurve[i];
|
||||
|
||||
// 使用定时器延迟启动每个分段的动画
|
||||
var timer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(delay)
|
||||
};
|
||||
|
||||
int segmentIndex = i; // 捕获当前索引
|
||||
timer.Tick += (sender, e) =>
|
||||
{
|
||||
StartSingleSegmentFadeAnimation(segment, segmentDuration, () =>
|
||||
{
|
||||
// 动画完成回调
|
||||
lock (completedSegments)
|
||||
{
|
||||
completedSegments.Add(segment);
|
||||
|
||||
// 检查是否所有分段都完成了
|
||||
if (completedSegments.Count >= totalSegments)
|
||||
{
|
||||
CleanupSegmentedAnimation(segments, originalStroke, originalVisual);
|
||||
}
|
||||
}
|
||||
});
|
||||
timer.Stop();
|
||||
};
|
||||
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
// 设置一个安全超时定时器,防止无限等待
|
||||
var safetyTimeout = totalDuration + (segments.Count * segmentDuration) + 1200; // 额外1.2秒缓冲,确保动画完整
|
||||
var safetyTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(safetyTimeout)
|
||||
};
|
||||
|
||||
safetyTimer.Tick += (sender, e) =>
|
||||
{
|
||||
CleanupSegmentedAnimation(segments, originalStroke, originalVisual);
|
||||
safetyTimer.Stop();
|
||||
};
|
||||
|
||||
safetyTimer.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"分段渐隐动画失败: {ex}", LogHelper.LogType.Error);
|
||||
CleanupSegmentedAnimation(segments, originalStroke, originalVisual);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个分段的渐隐动画
|
||||
/// </summary>
|
||||
private void StartSingleSegmentFadeAnimation(UIElement segment, int duration, Action onCompleted = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 只使用透明度动画,保持墨迹原有粗细
|
||||
var fadeAnimation = new DoubleAnimation
|
||||
{
|
||||
From = segment.Opacity,
|
||||
To = 0.0,
|
||||
Duration = TimeSpan.FromMilliseconds(duration),
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut } // 更平滑的缓动
|
||||
};
|
||||
|
||||
// 添加动画完成事件
|
||||
if (onCompleted != null)
|
||||
{
|
||||
fadeAnimation.Completed += (sender, e) =>
|
||||
{
|
||||
onCompleted?.Invoke();
|
||||
};
|
||||
}
|
||||
|
||||
// 只应用透明度动画,不改变墨迹大小
|
||||
segment.BeginAnimation(UIElement.OpacityProperty, fadeAnimation);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"单个分段渐隐动画失败: {ex}", LogHelper.LogType.Error);
|
||||
// 即使失败也要调用完成回调
|
||||
onCompleted?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理分段动画
|
||||
/// </summary>
|
||||
private void CleanupSegmentedAnimation(List<UIElement> segments, Stroke originalStroke, UIElement originalVisual)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 移除所有分段
|
||||
var parent = _mainWindow.inkCanvas?.Parent as Panel;
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (parent != null && parent.Children.Contains(segment))
|
||||
{
|
||||
parent.Children.Remove(segment);
|
||||
}
|
||||
else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(segment))
|
||||
{
|
||||
_mainWindow.inkCanvas.Children.Remove(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理原始墨迹
|
||||
OnAnimationCompleted(originalVisual, originalStroke);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理分段动画失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 简单渐隐动画(备用方案)
|
||||
/// </summary>
|
||||
private void StartSimpleFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fadeAnimation = new DoubleAnimation
|
||||
{
|
||||
From = currentOpacity,
|
||||
To = 0.0,
|
||||
Duration = TimeSpan.FromMilliseconds(duration),
|
||||
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn }
|
||||
};
|
||||
|
||||
fadeAnimation.Completed += (sender, e) => OnAnimationCompleted(visual, stroke);
|
||||
visual.BeginAnimation(UIElement.OpacityProperty, fadeAnimation);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"简单渐隐动画失败: {ex}", LogHelper.LogType.Error);
|
||||
OnAnimationCompleted(visual, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算墨迹的实际长度
|
||||
/// </summary>
|
||||
private double CalculateStrokeLength(StylusPointCollection points)
|
||||
{
|
||||
if (points.Count < 2) return 0;
|
||||
|
||||
double totalLength = 0;
|
||||
for (int i = 1; i < points.Count; i++)
|
||||
{
|
||||
var p1 = points[i - 1].ToPoint();
|
||||
var p2 = points[i].ToPoint();
|
||||
totalLength += Math.Sqrt(Math.Pow(p2.X - p1.X, 2) + Math.Pow(p2.Y - p1.Y, 2));
|
||||
}
|
||||
return totalLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据墨迹特性计算最优分段数量 - 平衡速度与完整性
|
||||
/// </summary>
|
||||
private int CalculateOptimalSegmentCount(int pointCount, double strokeLength)
|
||||
{
|
||||
// 平衡速度与完整性,确保动画效果的同时提高速度
|
||||
const double PIXELS_PER_SEGMENT = 12.0; // 每段适中长度,平衡效果与速度
|
||||
const int MIN_SEGMENTS = 5; // 适当的最小分段数,确保动画效果
|
||||
const int MAX_SEGMENTS = 100; // 适中的最大分段数,平衡性能与效果
|
||||
|
||||
// 根据长度计算基础分段数
|
||||
var lengthBasedSegments = Math.Max(MIN_SEGMENTS, (int)(strokeLength / PIXELS_PER_SEGMENT));
|
||||
|
||||
// 根据点密度调整,平衡效果与速度
|
||||
var density = pointCount > 0 ? strokeLength / pointCount : 1;
|
||||
var densityFactor = Math.Max(0.4, Math.Min(2.5, density / 1.8));
|
||||
|
||||
var finalSegments = (int)(lengthBasedSegments * densityFactor);
|
||||
|
||||
// 对于短墨迹,确保至少有4个分段
|
||||
if (pointCount <= 5)
|
||||
{
|
||||
finalSegments = Math.Max(finalSegments, 4);
|
||||
}
|
||||
|
||||
// 限制在合理范围内
|
||||
return Math.Min(MAX_SEGMENTS, Math.Max(MIN_SEGMENTS, finalSegments));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算最优的单段动画持续时间 - 平衡速度与完整性
|
||||
/// </summary>
|
||||
private int CalculateOptimalSegmentDuration(int totalDuration, int segmentCount)
|
||||
{
|
||||
// 平衡速度与动画完整性
|
||||
var baseDuration = totalDuration / Math.Max(segmentCount, 1);
|
||||
var minDuration = 150; // 每段最少150ms,确保动画完整显示
|
||||
var maxDuration = 500; // 每段最多500ms,平衡速度与完整性
|
||||
|
||||
return Math.Max(minDuration, Math.Min(maxDuration, baseDuration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建优化的动画时间曲线 - 平衡速度与完整性
|
||||
/// </summary>
|
||||
private int[] CreateAppleStyleAnimationCurve(int segmentCount, int totalDuration)
|
||||
{
|
||||
var curve = new int[segmentCount];
|
||||
|
||||
// 平衡速度与完整性,确保动画有足够时间播放
|
||||
var availableTime = totalDuration * 0.6; // 使用60%的总时间,给动画留足够缓冲
|
||||
var delayBetweenSegments = Math.Max(60, availableTime / Math.Max(segmentCount, 1));
|
||||
|
||||
for (int i = 0; i < segmentCount; i++)
|
||||
{
|
||||
// 线性延迟,确保每个分段都有足够时间
|
||||
curve[i] = (int)(i * delayBetweenSegments);
|
||||
}
|
||||
|
||||
return curve;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 动画完成后的统一处理
|
||||
/// </summary>
|
||||
private void OnAnimationCompleted(UIElement visual, Stroke stroke)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从父容器中移除墨迹
|
||||
var parent = _mainWindow.inkCanvas?.Parent as Panel;
|
||||
if (parent != null && parent.Children.Contains(visual))
|
||||
{
|
||||
parent.Children.Remove(visual);
|
||||
}
|
||||
else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(visual))
|
||||
{
|
||||
_mainWindow.inkCanvas.Children.Remove(visual);
|
||||
}
|
||||
|
||||
RemoveStroke(stroke);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"渐隐动画完成后清理墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Ink;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public sealed class InkRecognitionManager
|
||||
{
|
||||
private static InkRecognitionManager _instance;
|
||||
private static readonly object _lock = new object();
|
||||
private readonly object _initSync = new object();
|
||||
|
||||
private ModernInkProcessor _modernProcessor;
|
||||
private bool _isModernSystemAvailable;
|
||||
private bool _isInitialized;
|
||||
|
||||
public static InkRecognitionManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_instance == null)
|
||||
_instance = new InkRecognitionManager();
|
||||
}
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private InkRecognitionManager() { }
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 启动阶段只做能力探测,不做 WinRT 组件实例化(避免冷启动延迟)
|
||||
_isModernSystemAvailable = WinRtInkShapeRecognizer.IsApiAvailable;
|
||||
_isInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("墨迹识别管理器初始化失败: " + ex.Message, LogHelper.LogType.Error);
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
lock (_initSync)
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureModernAnalyzerInitialized()
|
||||
{
|
||||
if (_modernProcessor != null || !_isModernSystemAvailable) return;
|
||||
|
||||
lock (_initSync)
|
||||
{
|
||||
if (_modernProcessor != null || !_isModernSystemAvailable) return;
|
||||
try
|
||||
{
|
||||
_modernProcessor ??= new ModernInkProcessor();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("WinRT 墨迹模块懒加载失败: " + ex.Message, LogHelper.LogType.Warning);
|
||||
_isModernSystemAvailable = false;
|
||||
_modernProcessor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<InkShapeRecognitionResult> RecognizeShapeAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (!_isInitialized || strokes == null || strokes.Count == 0)
|
||||
return Task.FromResult(InkShapeRecognitionResult.Empty);
|
||||
|
||||
try
|
||||
{
|
||||
if (ShapeRecognitionRouter.ResolveUseWinRt(mode)
|
||||
&& WinRtInkShapeRecognizer.IsApiAvailable)
|
||||
{
|
||||
return RecognizeShapeWinRtOnDispatcherContext(strokes);
|
||||
}
|
||||
|
||||
var legacy = InkRecognizeHelper.RecognizeShapeIACore(strokes);
|
||||
return Task.FromResult(InkRecognizeHelper.FromIACoreOrEmpty(legacy));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("墨迹形状识别失败: " + ex.Message, LogHelper.LogType.Error);
|
||||
return Task.FromResult(InkShapeRecognitionResult.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<InkShapeRecognitionResult> RecognizeShapeWinRtOnDispatcherContext(
|
||||
StrokeCollection strokes)
|
||||
{
|
||||
return await WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <param name="applyHandwritingBeautify">为 true 且走 WinRT 时,将识别成功的词替换为手写风格字体的轮廓墨迹(见设置中的字体列表)。</param>
|
||||
/// <param name="handwritingFontFamilyList">逗号分隔的字体回退列表(WPF FontFamily);null 时使用内置默认。</param>
|
||||
public Task<StrokeCollection> CorrectInkAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode,
|
||||
bool applyHandwritingBeautify = false,
|
||||
string handwritingFontFamilyList = null)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (!_isInitialized)
|
||||
{
|
||||
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:无笔画。", LogHelper.LogType.Info);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var useWinRt = ShapeRecognitionRouter.ResolveUseWinRt(mode);
|
||||
if (!applyHandwritingBeautify)
|
||||
{
|
||||
LogHelper.WriteLogToFile(
|
||||
"[手写体] CorrectInkAsync 跳过:未开启「识别转手写体字形」(applyHandwritingBeautify=false)。笔画数=" +
|
||||
strokes.Count,
|
||||
LogHelper.LogType.Info);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
if (!useWinRt)
|
||||
{
|
||||
LogHelper.WriteLogToFile(
|
||||
"[手写体] CorrectInkAsync 跳过:当前引擎非 WinRT(模式=" + mode + ")。笔画数=" + strokes.Count,
|
||||
LogHelper.LogType.Info);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
EnsureModernAnalyzerInitialized();
|
||||
if (_modernProcessor == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile(
|
||||
"[手写体] CorrectInkAsync 跳过:ModernInkProcessor 未就绪(WinRT 初始化失败?)。笔画数=" +
|
||||
strokes.Count,
|
||||
LogHelper.LogType.Warning);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile(
|
||||
"[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count +
|
||||
",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()),
|
||||
LogHelper.LogType.Info);
|
||||
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(strokes, handwritingFontFamilyList);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("墨迹纠正失败: " + ex.Message, LogHelper.LogType.Error);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WinRT 手写体识别(需 Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。
|
||||
/// </summary>
|
||||
public Task<HandwritingRecognitionResult> RecognizeHandwritingAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (!_isInitialized || strokes == null || strokes.Count == 0)
|
||||
return Task.FromResult(HandwritingRecognitionResult.Empty);
|
||||
|
||||
try
|
||||
{
|
||||
if (!ShapeRecognitionRouter.ResolveUseWinRt(mode)
|
||||
|| !WinRtHandwritingRecognizer.IsApiAvailable)
|
||||
return Task.FromResult(HandwritingRecognitionResult.Empty);
|
||||
|
||||
return WinRtHandwritingRecognizer.RecognizeHandwritingAsync(strokes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("手写识别失败: " + ex.Message, LogHelper.LogType.Error);
|
||||
return Task.FromResult(HandwritingRecognitionResult.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValidShapeType(string shapeName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(shapeName)
|
||||
&& (shapeName.Contains("Triangle") || shapeName.Contains("Circle")
|
||||
|| shapeName.Contains("Rectangle") || shapeName.Contains("Diamond")
|
||||
|| shapeName.Contains("Parallelogram") || shapeName.Contains("Square")
|
||||
|| shapeName.Contains("Ellipse") || shapeName.Contains("Line")
|
||||
|| shapeName.Contains("Arrow"));
|
||||
}
|
||||
|
||||
public string GetSystemInfo()
|
||||
{
|
||||
return _isModernSystemAvailable
|
||||
? $"现代化墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}"
|
||||
: $"传统墨迹识别系统 (IACore) - 进程架构: {Environment.Is64BitProcess}";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_modernProcessor?.Dispose();
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ModernInkProcessor : IDisposable
|
||||
{
|
||||
public ModernInkProcessor()
|
||||
{
|
||||
if (!WinRtInkShapeRecognizer.IsApiAvailable)
|
||||
throw new InvalidOperationException("WinRT 墨迹分析需要 Windows 10 及以上。");
|
||||
}
|
||||
|
||||
public Task<InkShapeRecognitionResult> RecognizeShapeAsync(StrokeCollection strokes)
|
||||
{
|
||||
return WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Media;
|
||||
@@ -7,8 +8,8 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public class InkRecognizeHelper
|
||||
{
|
||||
//识别形状
|
||||
public static ShapeRecognizeResult RecognizeShape(StrokeCollection strokes)
|
||||
/// <summary>IACore / IAWinFX 形状识别(典型用于 32 位进程)。</summary>
|
||||
public static ShapeRecognizeResult RecognizeShapeIACore(StrokeCollection strokes)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
return default;
|
||||
@@ -25,35 +26,158 @@ namespace Ink_Canvas.Helpers
|
||||
var alternates = analyzer.GetAlternates();
|
||||
if (alternates.Count > 0)
|
||||
{
|
||||
while ((!alternates[0].Strokes.Contains(strokes.Last()) ||
|
||||
!IsContainShapeType(((InkDrawingNode)alternates[0].AlternateNodes[0]).GetShapeName()))
|
||||
&& strokesCount >= 2)
|
||||
while (strokesCount >= 2)
|
||||
{
|
||||
var alt0 = alternates[0];
|
||||
if (alt0?.AlternateNodes == null || alt0.AlternateNodes.Count == 0)
|
||||
break;
|
||||
var drawNode = alt0.AlternateNodes[0] as InkDrawingNode;
|
||||
if (drawNode == null)
|
||||
break;
|
||||
var shapeOk = IsContainShapeType(drawNode.GetShapeName());
|
||||
if (alt0.Strokes.Contains(strokes.Last()) && shapeOk)
|
||||
break;
|
||||
analyzer.RemoveStroke(strokes[strokes.Count - strokesCount]);
|
||||
strokesCount--;
|
||||
sfsaf = analyzer.Analyze();
|
||||
if (sfsaf.Successful)
|
||||
{
|
||||
alternates = analyzer.GetAlternates();
|
||||
}
|
||||
else
|
||||
break;
|
||||
if (alternates.Count == 0)
|
||||
break;
|
||||
}
|
||||
if (alternates.Count > 0)
|
||||
{
|
||||
var altFinal = alternates[0];
|
||||
if (altFinal?.AlternateNodes != null && altFinal.AlternateNodes.Count > 0)
|
||||
analysisAlternate = altFinal;
|
||||
}
|
||||
analysisAlternate = alternates[0];
|
||||
}
|
||||
}
|
||||
|
||||
analyzer.Dispose();
|
||||
|
||||
if (analysisAlternate != null && analysisAlternate.AlternateNodes.Count > 0)
|
||||
if (analysisAlternate != null && analysisAlternate.AlternateNodes != null && analysisAlternate.AlternateNodes.Count > 0)
|
||||
{
|
||||
var node = analysisAlternate.AlternateNodes[0] as InkDrawingNode;
|
||||
if (node == null)
|
||||
return default;
|
||||
return new ShapeRecognizeResult(node.Centroid, node.HotPoints, analysisAlternate, node);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>兼容旧调用:等价于 <see cref="RecognizeShapeIACore"/>。</summary>
|
||||
public static ShapeRecognizeResult RecognizeShape(StrokeCollection strokes) =>
|
||||
RecognizeShapeIACore(strokes);
|
||||
|
||||
/// <summary>按设置选择 WinRT(<see cref="InkRecognitionManager"/>)或 IACore;WinRT 请用 <see cref="RecognizeShapeUnifiedAsync"/>。</summary>
|
||||
public static InkShapeRecognitionResult RecognizeShapeUnified(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var legacy = RecognizeShapeIACore(strokes);
|
||||
return FromIACoreOrEmpty(legacy);
|
||||
}
|
||||
|
||||
/// <summary>与 CE 反编译版 <c>InkRecognitionManager.RecognizeShapeAsync</c> 对齐的统一入口。</summary>
|
||||
public static Task<InkShapeRecognitionResult> RecognizeShapeUnifiedAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
return Task.FromResult(InkShapeRecognitionResult.Empty);
|
||||
|
||||
return InkRecognitionManager.Instance.RecognizeShapeAsync(strokes, mode);
|
||||
}
|
||||
|
||||
public static void WarmupShapeRecognition(ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
|
||||
{
|
||||
WinRtInkShapeRecognizer.Warmup();
|
||||
WinRtHandwritingRecognizer.Warmup();
|
||||
}
|
||||
else
|
||||
RecognizeShapeIACore(new StrokeCollection());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 预热失败不影响启动
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>WinRT 手写识别(Windows 10+)。</summary>
|
||||
public static Task<HandwritingRecognitionResult> RecognizeHandwritingUnifiedAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode) =>
|
||||
InkRecognitionManager.Instance.RecognizeHandwritingAsync(strokes, mode);
|
||||
|
||||
/// <summary>WinRT 下将识别成功的词替换为手写体字形墨迹;是否应用由设置「WinRT 识别转手写体字形」控制。</summary>
|
||||
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode) =>
|
||||
InkRecognitionManager.Instance.CorrectInkAsync(
|
||||
strokes,
|
||||
mode,
|
||||
MainWindow.Settings?.InkToShape?.EnableWinRtHandwritingStrokeBeautify ?? false,
|
||||
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
|
||||
|
||||
/// <summary>显式指定是否应用手写体字形替换(忽略开关);字体仍从设置读取。</summary>
|
||||
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode,
|
||||
bool applyHandwritingBeautify) =>
|
||||
InkRecognitionManager.Instance.CorrectInkAsync(
|
||||
strokes,
|
||||
mode,
|
||||
applyHandwritingBeautify,
|
||||
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
|
||||
|
||||
internal static InkShapeRecognitionResult FromIACoreOrEmpty(ShapeRecognizeResult legacy)
|
||||
{
|
||||
if (legacy?.InkDrawingNode == null)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var node = legacy.InkDrawingNode;
|
||||
var shape = node.GetShape();
|
||||
if (shape == null)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var hot = ClonePointCollection(node.HotPoints);
|
||||
return new InkShapeRecognitionResult(
|
||||
node.GetShapeName(),
|
||||
legacy.Centroid,
|
||||
hot,
|
||||
shape.Width,
|
||||
shape.Height,
|
||||
node.Strokes);
|
||||
}
|
||||
|
||||
private static PointCollection ClonePointCollection(PointCollection src)
|
||||
{
|
||||
var dst = new PointCollection();
|
||||
if (src == null) return dst;
|
||||
foreach (System.Windows.Point p in src)
|
||||
dst.Add(p);
|
||||
return dst;
|
||||
}
|
||||
|
||||
public static bool IsContainShapeType(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return false;
|
||||
|
||||
if (name.Contains("Triangle") || name.Contains("Circle") ||
|
||||
name.Contains("Rectangle") || name.Contains("Diamond") ||
|
||||
name.Contains("Parallelogram") || name.Contains("Square")
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using OSVersionExtension;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>墨迹形状识别后端:自动 / IACore / WinRT。</summary>
|
||||
public enum ShapeRecognitionEngineMode
|
||||
{
|
||||
Auto = 0,
|
||||
IACore = 1,
|
||||
WinRT = 2,
|
||||
}
|
||||
|
||||
public static class ShapeRecognitionRouter
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动模式:在 Windows 10 及以上系统默认使用 WinRT,否则使用 IACore。
|
||||
/// </summary>
|
||||
public static bool ResolveUseWinRt(ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
if (mode == ShapeRecognitionEngineMode.WinRT) return true;
|
||||
if (mode == ShapeRecognitionEngineMode.IACore) return false;
|
||||
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
|
||||
}
|
||||
|
||||
public static bool ShouldRunShapeRecognition(bool inkToShapeEnabled, ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
if (!inkToShapeEnabled) return false;
|
||||
if (ResolveUseWinRt(mode))
|
||||
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ShapeRecognitionEngineMode FromSettingsInt(int value)
|
||||
{
|
||||
if (value == (int)ShapeRecognitionEngineMode.IACore) return ShapeRecognitionEngineMode.IACore;
|
||||
if (value == (int)ShapeRecognitionEngineMode.WinRT) return ShapeRecognitionEngineMode.WinRT;
|
||||
return ShapeRecognitionEngineMode.Auto;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>与具体识别后端无关的形状识别结果,供统一纠正模块消费。</summary>
|
||||
public sealed class InkShapeRecognitionResult
|
||||
{
|
||||
public static readonly InkShapeRecognitionResult Empty = new InkShapeRecognitionResult();
|
||||
|
||||
private InkShapeRecognitionResult()
|
||||
{
|
||||
IsSuccess = false;
|
||||
ShapeName = string.Empty;
|
||||
Centroid = new Point();
|
||||
HotPoints = new PointCollection();
|
||||
StrokesToRemove = new StrokeCollection();
|
||||
}
|
||||
|
||||
public InkShapeRecognitionResult(
|
||||
string shapeName,
|
||||
Point centroid,
|
||||
PointCollection hotPoints,
|
||||
double shapeWidth,
|
||||
double shapeHeight,
|
||||
StrokeCollection strokesToRemove)
|
||||
{
|
||||
ShapeName = shapeName ?? string.Empty;
|
||||
Centroid = centroid;
|
||||
HotPoints = hotPoints ?? new PointCollection();
|
||||
ShapeWidth = shapeWidth;
|
||||
ShapeHeight = shapeHeight;
|
||||
StrokesToRemove = strokesToRemove ?? new StrokeCollection();
|
||||
IsSuccess = StrokesToRemove.Count > 0
|
||||
&& !string.IsNullOrEmpty(ShapeName)
|
||||
&& ShapeName != "Drawing";
|
||||
}
|
||||
|
||||
public bool IsSuccess { get; }
|
||||
public string ShapeName { get; }
|
||||
public Point Centroid { get; set; }
|
||||
public PointCollection HotPoints { get; }
|
||||
public double ShapeWidth { get; }
|
||||
public double ShapeHeight { get; }
|
||||
public StrokeCollection StrokesToRemove { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 墨迹平滑配置类
|
||||
/// </summary>
|
||||
public class InkSmoothingConfig
|
||||
{
|
||||
// 基本平滑参数
|
||||
public double SmoothingStrength { get; set; } = 0.4;
|
||||
public double ResampleInterval { get; set; } = 2.5;
|
||||
public int InterpolationSteps { get; set; } = 12;
|
||||
|
||||
// 贝塞尔曲线参数
|
||||
public bool UseAdaptiveInterpolation { get; set; } = true;
|
||||
public double CurveTension { get; set; } = 0.3;
|
||||
public double MinCurvatureThreshold { get; set; } = 0.1;
|
||||
public double MaxCurvatureThreshold { get; set; } = 0.8;
|
||||
|
||||
// 性能参数
|
||||
public bool UseHardwareAcceleration { get; set; } = true;
|
||||
public bool UseAsyncProcessing { get; set; } = true;
|
||||
public int MaxConcurrentTasks { get; set; } = Environment.ProcessorCount;
|
||||
public int MaxPointsPerStroke { get; set; } = 10000;
|
||||
|
||||
// 质量设置
|
||||
public SmoothingQuality Quality { get; set; } = SmoothingQuality.Balanced;
|
||||
|
||||
public enum SmoothingQuality
|
||||
{
|
||||
Performance, // 性能优先
|
||||
Balanced, // 平衡
|
||||
Quality // 质量优先
|
||||
}
|
||||
|
||||
// 兼容性枚举
|
||||
public enum InkSmoothingQuality
|
||||
{
|
||||
HighPerformance = 0, // 高性能低质量
|
||||
Balanced = 1, // 平衡
|
||||
HighQuality = 2 // 高质量低性能
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从设置中加载配置
|
||||
/// </summary>
|
||||
public static InkSmoothingConfig FromSettings()
|
||||
{
|
||||
var config = new InkSmoothingConfig();
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试从MainWindow.Settings加载配置(兼容性)
|
||||
if (MainWindow.Settings?.Canvas != null)
|
||||
{
|
||||
config.Quality = (SmoothingQuality)MainWindow.Settings.Canvas.InkSmoothingQuality;
|
||||
config.UseHardwareAcceleration = MainWindow.Settings.Canvas.UseHardwareAcceleration;
|
||||
config.UseAsyncProcessing = MainWindow.Settings.Canvas.UseAsyncInkSmoothing;
|
||||
config.MaxConcurrentTasks = MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks > 0 ?
|
||||
MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks : Environment.ProcessorCount;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"加载平滑配置失败: {ex.Message}");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用质量设置
|
||||
/// </summary>
|
||||
public void ApplyQualitySettings()
|
||||
{
|
||||
// 保存用户设置的异步处理偏好
|
||||
bool userAsyncPreference = UseAsyncProcessing;
|
||||
|
||||
switch (Quality)
|
||||
{
|
||||
case SmoothingQuality.Performance:
|
||||
SmoothingStrength = 0.15;
|
||||
ResampleInterval = 5.0;
|
||||
InterpolationSteps = 4;
|
||||
UseAdaptiveInterpolation = false;
|
||||
CurveTension = 0.15;
|
||||
MaxConcurrentTasks = Math.Max(1, Environment.ProcessorCount / 2);
|
||||
UseHardwareAcceleration = true;
|
||||
UseAsyncProcessing = userAsyncPreference;
|
||||
break;
|
||||
|
||||
case SmoothingQuality.Balanced:
|
||||
SmoothingStrength = 0.3;
|
||||
ResampleInterval = 3.0;
|
||||
InterpolationSteps = 8;
|
||||
UseAdaptiveInterpolation = true;
|
||||
CurveTension = 0.25;
|
||||
MaxConcurrentTasks = Environment.ProcessorCount;
|
||||
UseHardwareAcceleration = true;
|
||||
UseAsyncProcessing = userAsyncPreference;
|
||||
break;
|
||||
|
||||
case SmoothingQuality.Quality:
|
||||
SmoothingStrength = 0.5;
|
||||
ResampleInterval = 2.0;
|
||||
InterpolationSteps = 15;
|
||||
UseAdaptiveInterpolation = true;
|
||||
CurveTension = 0.35;
|
||||
MaxConcurrentTasks = Environment.ProcessorCount;
|
||||
UseHardwareAcceleration = true;
|
||||
UseAsyncProcessing = userAsyncPreference;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存配置到设置
|
||||
/// </summary>
|
||||
public void SaveToSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 尝试保存到MainWindow.Settings(兼容性)
|
||||
if (MainWindow.Settings?.Canvas != null)
|
||||
{
|
||||
MainWindow.Settings.Canvas.InkSmoothingQuality = (int)Quality;
|
||||
MainWindow.Settings.Canvas.UseHardwareAcceleration = UseHardwareAcceleration;
|
||||
MainWindow.Settings.Canvas.UseAsyncInkSmoothing = UseAsyncProcessing;
|
||||
MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks = MaxConcurrentTasks;
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"保存平滑配置失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证配置参数
|
||||
/// </summary>
|
||||
public bool Validate()
|
||||
{
|
||||
return SmoothingStrength >= 0.0 && SmoothingStrength <= 1.0 &&
|
||||
ResampleInterval > 0.0 &&
|
||||
InterpolationSteps > 0 && InterpolationSteps <= 50 &&
|
||||
CurveTension >= 0.0 && CurveTension <= 1.0 &&
|
||||
MaxConcurrentTasks > 0 &&
|
||||
MaxPointsPerStroke > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置摘要
|
||||
/// </summary>
|
||||
public string GetSummary()
|
||||
{
|
||||
return $"质量: {Quality}, 强度: {SmoothingStrength:F2}, 间隔: {ResampleInterval:F1}, " +
|
||||
$"步数: {InterpolationSteps}, 自适应: {UseAdaptiveInterpolation}, " +
|
||||
$"张力: {CurveTension:F2}, 硬件加速: {UseHardwareAcceleration}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +197,7 @@ namespace Ink_Canvas.Helpers
|
||||
if (processorCount >= 4 && isHardwareAccelerated)
|
||||
{
|
||||
// 降低高质量模式的门槛,4核以上且支持硬件加速就使用高质量
|
||||
config.Quality = InkSmoothingQuality.HighQuality;
|
||||
config.Quality = (InkSmoothingConfig.SmoothingQuality)InkSmoothingConfig.InkSmoothingQuality.HighQuality;
|
||||
config.UseHardwareAcceleration = true;
|
||||
config.UseAsyncProcessing = true;
|
||||
config.MaxConcurrentTasks = Math.Min(processorCount, 8);
|
||||
@@ -205,7 +205,7 @@ namespace Ink_Canvas.Helpers
|
||||
else if (processorCount >= 2)
|
||||
{
|
||||
// 2核以上使用平衡模式
|
||||
config.Quality = InkSmoothingQuality.Balanced;
|
||||
config.Quality = (InkSmoothingConfig.SmoothingQuality)InkSmoothingConfig.InkSmoothingQuality.Balanced;
|
||||
config.UseHardwareAcceleration = isHardwareAccelerated;
|
||||
config.UseAsyncProcessing = true;
|
||||
config.MaxConcurrentTasks = Math.Min(processorCount, 4);
|
||||
@@ -213,7 +213,7 @@ namespace Ink_Canvas.Helpers
|
||||
else
|
||||
{
|
||||
// 单核或性能较低的设备使用高性能模式
|
||||
config.Quality = InkSmoothingQuality.HighPerformance;
|
||||
config.Quality = (InkSmoothingConfig.SmoothingQuality)InkSmoothingConfig.InkSmoothingQuality.HighPerformance;
|
||||
config.UseHardwareAcceleration = false;
|
||||
config.UseAsyncProcessing = false;
|
||||
config.MaxConcurrentTasks = 1;
|
||||
|
||||
@@ -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,31 @@ 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))
|
||||
DebugConsoleManager.WriteLine(logLine);
|
||||
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 +133,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 +171,4 @@ namespace Ink_Canvas.Helpers
|
||||
Warning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
@@ -8,27 +9,53 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public class VisualCanvas : FrameworkElement
|
||||
{
|
||||
private readonly List<DrawingVisual> _visuals = new List<DrawingVisual>();
|
||||
|
||||
protected override Visual GetVisualChild(int index)
|
||||
{
|
||||
return Visual;
|
||||
if (index < 0 || index >= _visuals.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
return _visuals[index];
|
||||
}
|
||||
|
||||
protected override int VisualChildrenCount => 1;
|
||||
protected override int VisualChildrenCount => _visuals.Count;
|
||||
|
||||
public VisualCanvas(DrawingVisual visual)
|
||||
public VisualCanvas()
|
||||
{
|
||||
Visual = visual;
|
||||
CacheMode = new BitmapCache();
|
||||
|
||||
RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.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 int _lastDrawnPointCount = 0;
|
||||
private const int INCREMENTAL_DRAW_THRESHOLD = 2;
|
||||
private VisualCanvas _visualCanvas;
|
||||
|
||||
/// <summary>
|
||||
/// 创建显示笔迹的类
|
||||
/// </summary>
|
||||
@@ -43,7 +70,7 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建显示笔迹的类
|
||||
/// 创建显示笔迹的类
|
||||
/// </summary>
|
||||
/// <param name="drawingAttributes"></param>
|
||||
public StrokeVisual(DrawingAttributes drawingAttributes)
|
||||
@@ -52,12 +79,20 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置或获取显示的笔迹
|
||||
/// 设置或获取显示的笔迹
|
||||
/// </summary>
|
||||
public Stroke Stroke { set; get; }
|
||||
|
||||
/// <summary>
|
||||
/// 在笔迹中添加点
|
||||
/// 设置关联的VisualCanvas
|
||||
/// </summary>
|
||||
public void SetVisualCanvas(VisualCanvas visualCanvas)
|
||||
{
|
||||
_visualCanvas = visualCanvas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在笔迹中添加点
|
||||
/// </summary>
|
||||
/// <param name="point"></param>
|
||||
public void Add(StylusPoint point)
|
||||
@@ -74,25 +109,119 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新画出笔迹
|
||||
/// 绘制点段到新的DrawingVisual
|
||||
/// </summary>
|
||||
private static double PressureToVisualScale(float pressureFactor, bool ignorePressure)
|
||||
{
|
||||
if (ignorePressure)
|
||||
return 1.0;
|
||||
// 与 WPF 墨迹观感接近:0.5 为标称,压低变细、抬高变粗(预览此前固定 Pen 宽,等同忽略压感)
|
||||
return Math.Max(0.22, Math.Min(2.1, 0.42 + 1.16 * pressureFactor));
|
||||
}
|
||||
|
||||
private void DrawSegmentToNewVisual(int startIndex, int endIndex)
|
||||
{
|
||||
if (Stroke == null || Stroke.StylusPoints.Count == 0 || _visualCanvas == null) return;
|
||||
if (startIndex >= endIndex || startIndex < 0 || endIndex > Stroke.StylusPoints.Count) return;
|
||||
|
||||
var points = Stroke.StylusPoints;
|
||||
var drawingAttributes = Stroke.DrawingAttributes;
|
||||
var ignorePressure = drawingAttributes.IgnorePressure;
|
||||
|
||||
// 创建新的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())
|
||||
{
|
||||
// 绘制指定范围内的点段
|
||||
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);
|
||||
var s0 = PressureToVisualScale(points[i].PressureFactor, ignorePressure);
|
||||
var s1 = PressureToVisualScale(points[i + 1].PressureFactor, ignorePressure);
|
||||
var thickness = Math.Max(0.35, (drawingAttributes.Width * s0 + drawingAttributes.Width * s1) / 2.0);
|
||||
var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), thickness)
|
||||
{
|
||||
StartLineCap = PenLineCap.Round,
|
||||
EndLineCap = PenLineCap.Round,
|
||||
LineJoin = PenLineJoin.Round
|
||||
};
|
||||
dc.DrawLine(pen, startPoint, endPoint);
|
||||
}
|
||||
}
|
||||
else if (endIndex - startIndex == 1 && startIndex < points.Count)
|
||||
{
|
||||
// 只有一个点,绘制圆点
|
||||
var brush = new SolidColorBrush(drawingAttributes.Color);
|
||||
var point = points[startIndex];
|
||||
var s = PressureToVisualScale(point.PressureFactor, ignorePressure);
|
||||
dc.DrawEllipse(brush, null, new Point(point.X, point.Y),
|
||||
drawingAttributes.Width * s / 2, drawingAttributes.Height * s / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 将新的DrawingVisual添加到VisualCanvas中
|
||||
_visualCanvas.AddVisual(segmentVisual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新画出笔迹
|
||||
/// </summary>
|
||||
public void Redraw()
|
||||
{
|
||||
try
|
||||
if (Stroke == null || _visualCanvas == null) return;
|
||||
|
||||
var currentPointCount = Stroke.StylusPoints.Count;
|
||||
if (currentPointCount == 0) return;
|
||||
|
||||
// 计算新增的点数
|
||||
int newPointCount = currentPointCount - _lastDrawnPointCount;
|
||||
|
||||
// 如果新增点数达到阈值,才进行增量绘制
|
||||
if (newPointCount >= INCREMENTAL_DRAW_THRESHOLD || _lastDrawnPointCount == 0)
|
||||
{
|
||||
using (var dc = RenderOpen())
|
||||
try
|
||||
{
|
||||
Stroke.Draw(dc);
|
||||
if (_lastDrawnPointCount == 0)
|
||||
{
|
||||
// 首次绘制:绘制所有点
|
||||
DrawSegmentToNewVisual(0, currentPointCount);
|
||||
_lastDrawnPointCount = currentPointCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 从上次绘制的最后一个点开始
|
||||
int startIndex = Math.Max(0, _lastDrawnPointCount - 1);
|
||||
DrawSegmentToNewVisual(startIndex, currentPointCount);
|
||||
_lastDrawnPointCount = currentPointCount;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制重绘
|
||||
/// </summary>
|
||||
public void ForceRedraw()
|
||||
{
|
||||
if (_visualCanvas != null)
|
||||
{
|
||||
_visualCanvas.Clear();
|
||||
}
|
||||
_lastDrawnPointCount = 0;
|
||||
Redraw();
|
||||
}
|
||||
|
||||
private readonly DrawingAttributes _drawingAttributes;
|
||||
|
||||
public static implicit operator Stroke(StrokeVisual v)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// .NET Core / 5+ 未提供 <see cref="Marshal.GetActiveObject"/>,通过 OLE 实现等效行为。
|
||||
/// </summary>
|
||||
internal static class OleActiveObject
|
||||
{
|
||||
[DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
|
||||
private static extern int CLSIDFromProgID(string lpszProgId, out Guid lpclsid);
|
||||
|
||||
[DllImport("oleaut32.dll", PreserveSig = true)]
|
||||
private static extern int GetActiveObject(ref Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
|
||||
|
||||
public static object GetActiveObject(string progId)
|
||||
{
|
||||
int hr = CLSIDFromProgID(progId, out Guid clsid);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
hr = GetActiveObject(ref clsid, IntPtr.Zero, out object obj);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
+359
-151
@@ -1,14 +1,13 @@
|
||||
using Microsoft.Office.Interop.PowerPoint;
|
||||
using Microsoft.Office.Interop.PowerPoint;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Ink;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// PPT墨迹管理器 - 负责PPT中墨迹的保存、加载和同步
|
||||
/// PPT墨迹管理器 - 负责按幻灯片保存/加载墨迹、自动保存与内存管理。
|
||||
/// </summary>
|
||||
public class PPTInkManager : IDisposable
|
||||
{
|
||||
@@ -20,55 +19,102 @@ 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 = false;
|
||||
private bool _disposed;
|
||||
|
||||
// 墨迹锁定机制,防止翻页时的墨迹冲突
|
||||
private DateTime _inkLockUntil = DateTime.MinValue;
|
||||
private int _lockedSlideIndex = -1;
|
||||
private const int InkLockMilliseconds = 500;
|
||||
|
||||
// 添加快速切换保护机制
|
||||
private DateTime _lastSwitchTime = DateTime.MinValue;
|
||||
private int _lastSwitchSlideIndex = -1;
|
||||
private const int MinSwitchIntervalMs = 100;
|
||||
|
||||
private long _totalMemoryUsage;
|
||||
private const long MaxMemoryUsageBytes = 100 * 1024 * 1024; // 100MB
|
||||
private DateTime _lastMemoryCleanup = DateTime.MinValue;
|
||||
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
|
||||
{
|
||||
// 生成演示文稿唯一标识符
|
||||
ClearAllStrokesInternal();
|
||||
_inkLockUntil = DateTime.MinValue;
|
||||
_lockedSlideIndex = -1;
|
||||
_lastSwitchSlideIndex = -1;
|
||||
_lastSwitchTime = DateTime.MinValue;
|
||||
|
||||
_currentPresentationId = GeneratePresentationId(presentation);
|
||||
|
||||
// 重新初始化内存流数组
|
||||
var slideCount = presentation.Slides.Count;
|
||||
_memoryStreams = new MemoryStream[slideCount + 2];
|
||||
|
||||
// 如果启用自动保存,尝试加载已保存的墨迹
|
||||
if (IsAutoSaveEnabled && !string.IsNullOrEmpty(AutoSaveLocation))
|
||||
int slideCount = 0;
|
||||
try
|
||||
{
|
||||
LoadSavedStrokes();
|
||||
slideCount = presentation.Slides.Count;
|
||||
}
|
||||
catch (COMException comEx)
|
||||
{
|
||||
uint hr = (uint)comEx.HResult;
|
||||
if (hr == 0x80048010) return;
|
||||
throw;
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已初始化演示文稿墨迹管理: {presentation.Name}, 幻灯片数量: {slideCount}", LogHelper.LogType.Trace);
|
||||
int capacity = slideCount + 2;
|
||||
_maxSlides = Math.Max(_maxSlides, slideCount);
|
||||
_memoryStreams = new MemoryStream[capacity];
|
||||
|
||||
if (IsAutoSaveEnabled && !string.IsNullOrEmpty(AutoSaveLocation))
|
||||
LoadSavedStrokes();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -80,33 +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))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"墨迹写入被锁定,当前页:{slideIndex},锁定页:{_lockedSlideIndex}", LogHelper.LogType.Warning);
|
||||
return;
|
||||
}
|
||||
if (!CanWriteInk(slideIndex)) return;
|
||||
if (slideIndex >= _memoryStreams.Length) return;
|
||||
|
||||
if (slideIndex < _memoryStreams.Length)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
strokes.Save(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
// 释放旧的内存流
|
||||
_memoryStreams[slideIndex]?.Dispose();
|
||||
_memoryStreams[slideIndex] = ms;
|
||||
|
||||
LogHelper.WriteLogToFile($"已保存第{slideIndex}页墨迹,大小: {ms.Length} bytes", LogHelper.LogType.Trace);
|
||||
}
|
||||
ReplaceSlideStream(slideIndex, strokes);
|
||||
CheckAndPerformMemoryCleanup();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -115,11 +153,45 @@ 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) return;
|
||||
ReplaceSlideStream(slideIndex, strokes);
|
||||
LogHelper.WriteLogToFile($"已强制保存第{slideIndex}页墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"强制保存第{slideIndex}页墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
@@ -129,9 +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]);
|
||||
LogHelper.WriteLogToFile($"已加载第{slideIndex}页墨迹,笔画数量: {strokes.Count}", LogHelper.LogType.Trace);
|
||||
return strokes;
|
||||
return new StrokeCollection(_memoryStreams[slideIndex]);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -146,23 +216,31 @@ 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
|
||||
{
|
||||
// 如果有当前墨迹,先保存
|
||||
if (currentStrokes != null && currentStrokes.Count > 0)
|
||||
var now = DateTime.Now;
|
||||
if (now - _lastSwitchTime < TimeSpan.FromMilliseconds(MinSwitchIntervalMs) && _lastSwitchSlideIndex == slideIndex)
|
||||
{
|
||||
SaveCurrentSlideStrokes(_lockedSlideIndex > 0 ? _lockedSlideIndex : slideIndex, currentStrokes);
|
||||
LogHelper.WriteLogToFile($"快速切换保护:忽略重复请求 页{slideIndex}", LogHelper.LogType.Trace);
|
||||
return LoadSlideStrokes(slideIndex);
|
||||
}
|
||||
|
||||
// 设置墨迹锁定
|
||||
LockInkForSlide(slideIndex);
|
||||
|
||||
// 加载新页面的墨迹
|
||||
return LoadSlideStrokes(slideIndex);
|
||||
StrokeCollection newStrokes = LoadSlideStrokes(slideIndex);
|
||||
_lastSwitchTime = now;
|
||||
_lastSwitchSlideIndex = slideIndex;
|
||||
return newStrokes;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -175,68 +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
|
||||
int slideCount = 0;
|
||||
try { slideCount = presentation.Slides.Count; }
|
||||
catch (COMException comEx)
|
||||
{
|
||||
File.WriteAllText(Path.Combine(folderPath, "Position"), _lockedSlideIndex.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存位置信息失败: {ex}", LogHelper.LogType.Error);
|
||||
if ((uint)comEx.HResult == 0x80048010) return;
|
||||
throw;
|
||||
}
|
||||
|
||||
// 保存所有页面的墨迹
|
||||
int savedCount = 0;
|
||||
for (int i = 1; i <= presentation.Slides.Count && i < _memoryStreams.Length; i++)
|
||||
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++;
|
||||
|
||||
LogHelper.WriteLogToFile($"已保存第{i}页墨迹,大小: {byteLength} bytes", LogHelper.LogType.Trace);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已保存{savedCount}页墨迹到文件", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -248,42 +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);
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已从文件加载{loadedCount}页墨迹", LogHelper.LogType.Event);
|
||||
if (loadedCount > 0)
|
||||
LogHelper.WriteLogToFile($"已从磁盘加载 {loadedCount} 页墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -295,101 +379,225 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 清除所有墨迹
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 清除并释放当前演示文稿所有幻灯片的墨迹数据和相关内存资源。
|
||||
/// 该方法在内部加锁以保证线程安全;会处置并清空所有内部存储的墨迹流、重建内部流数组并清空 CurrentStrokes。
|
||||
/// </remarks>
|
||||
public void ClearAllStrokes()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < _memoryStreams.Length; i++)
|
||||
{
|
||||
_memoryStreams[i]?.Dispose();
|
||||
_memoryStreams[i] = null;
|
||||
}
|
||||
|
||||
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 false;
|
||||
if (currentSlideIndex != _lockedSlideIndex && _lockedSlideIndex > 0) return false;
|
||||
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;
|
||||
_lockedSlideIndex = -1;
|
||||
_lastSwitchTime = DateTime.MinValue;
|
||||
_lastSwitchSlideIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
#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();
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
long currentMemoryUsage = 0;
|
||||
if (_memoryStreams != null)
|
||||
{
|
||||
for (int i = 0; i < _memoryStreams.Length; i++)
|
||||
if (_memoryStreams[i] != null) currentMemoryUsage += _memoryStreams[i].Length;
|
||||
}
|
||||
_totalMemoryUsage = currentMemoryUsage;
|
||||
|
||||
if (currentMemoryUsage > MaxMemoryUsageBytes)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"墨迹内存超限 ({currentMemoryUsage / (1024 * 1024)}MB),执行清理", LogHelper.LogType.Warning);
|
||||
CleanupInactiveSlideStrokes();
|
||||
}
|
||||
_lastMemoryCleanup = now;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"内存清理检查失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理不活跃幻灯片的内存化墨迹数据以回收内存空间。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 将释放除当前锁定幻灯片与最近切换幻灯片之外的每页内存流(若存在),并将对应数组项设为 null;完成后若有释放,会记录已清理页数与释放的总大小(KB)。
|
||||
/// </remarks>
|
||||
private void CleanupInactiveSlideStrokes()
|
||||
{
|
||||
if (_memoryStreams == null) return;
|
||||
int cleaned = 0;
|
||||
long freed = 0;
|
||||
for (int i = 0; i < _memoryStreams.Length; i++)
|
||||
{
|
||||
if (i == _lockedSlideIndex || i == _lastSwitchSlideIndex) continue;
|
||||
if (_memoryStreams[i] != null)
|
||||
{
|
||||
long len = _memoryStreams[i].Length;
|
||||
try { _memoryStreams[i].Dispose(); freed += len; cleaned++; }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
finally { _memoryStreams[i] = null; }
|
||||
}
|
||||
}
|
||||
if (cleaned > 0)
|
||||
LogHelper.WriteLogToFile($"已清理 {cleaned} 页墨迹,释放 {freed / 1024}KB", LogHelper.LogType.Trace);
|
||||
}
|
||||
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
+703
-262
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)OleActiveObject.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)OleActiveObject.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
|
||||
}
|
||||
}
|
||||
+127
-113
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
@@ -18,7 +19,14 @@ namespace Ink_Canvas.Helpers
|
||||
public int PPTBButtonsOption { get; set; } = 121;
|
||||
public int PPTLSButtonPosition { get; set; } = 0;
|
||||
public int PPTRSButtonPosition { get; set; } = 0;
|
||||
public int PPTLBButtonPosition { get; set; } = 0;
|
||||
public int PPTRBButtonPosition { get; set; } = 0;
|
||||
public bool EnablePPTButtonPageClickable { get; set; } = true;
|
||||
public bool EnablePPTButtonLongPressPageTurn { get; set; } = true;
|
||||
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
|
||||
@@ -78,20 +86,55 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Collapsed;
|
||||
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Visible;
|
||||
|
||||
if (currentSlide > 0 && totalSlides > 0)
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
|
||||
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
|
||||
}
|
||||
// 同步页码到所有翻页条 + 兼容旧绑定的隐藏 placeholder
|
||||
SetPageNumberOnAllBars(currentSlide, totalSlides);
|
||||
|
||||
UpdateNavigationPanelsVisibility();
|
||||
UpdateNavigationButtonStyles();
|
||||
_mainWindow.UpdatePPTTimeCapsuleVisibility();
|
||||
_mainWindow.UpdatePPTQuickPanelVisibility();
|
||||
if (MainWindow.Settings.Advanced.IsEnableAvoidFullScreenHelper)
|
||||
{
|
||||
// 设置为画板模式,允许全屏操作
|
||||
AvoidFullScreenHelper.SetBoardMode(true);
|
||||
_dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
MainWindow.MoveWindow(new WindowInteropHelper(_mainWindow).Handle, 0, 0,
|
||||
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
|
||||
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height, true);
|
||||
|
||||
// MoveWindow 触发的 WM_WINDOWPOSCHANGING + 重绘会打断面板的 ShowWithFadeIn 动画,
|
||||
// 在窗口尺寸最终确定后重新评估一次翻页面板的可见性。
|
||||
UpdateNavigationPanelsVisibility();
|
||||
UpdateNavigationButtonStyles();
|
||||
}), DispatcherPriority.ApplicationIdle);
|
||||
|
||||
_mainWindow.isFullScreenApplied = true; // 标记已应用全屏处理
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Visible;
|
||||
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Collapsed;
|
||||
HideAllNavigationPanels();
|
||||
_mainWindow.UpdatePPTTimeCapsuleVisibility();
|
||||
_mainWindow.UpdatePPTQuickPanelVisibility();
|
||||
if (MainWindow.Settings.Advanced.IsEnableAvoidFullScreenHelper)
|
||||
{
|
||||
// 恢复为非画板模式,重新启用全屏限制
|
||||
AvoidFullScreenHelper.SetBoardMode(false);
|
||||
|
||||
_dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
// 退出PPT放映模式,恢复到工作区域大小
|
||||
var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
|
||||
MainWindow.MoveWindow(new WindowInteropHelper(_mainWindow).Handle,
|
||||
workingArea.X, workingArea.Y,
|
||||
workingArea.Width, workingArea.Height, true);
|
||||
}), DispatcherPriority.ApplicationIdle);
|
||||
|
||||
_mainWindow.isFullScreenApplied = false; // 标记全屏处理已还原
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -110,8 +153,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
|
||||
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
|
||||
SetPageNumberOnAllBars(currentSlide, totalSlides);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -120,6 +162,56 @@ namespace Ink_Canvas.Helpers
|
||||
});
|
||||
}
|
||||
|
||||
private void SetPageNumberOnAllBars(int currentSlide, int totalSlides)
|
||||
{
|
||||
var bars = new[]
|
||||
{
|
||||
_mainWindow.LeftBottomPanelForPPTNavigation,
|
||||
_mainWindow.RightBottomPanelForPPTNavigation,
|
||||
_mainWindow.LeftSidePanelForPPTNavigation,
|
||||
_mainWindow.RightSidePanelForPPTNavigation,
|
||||
};
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
if (bar == null) continue;
|
||||
bar.CurrentSlide = currentSlide;
|
||||
bar.TotalSlides = totalSlides;
|
||||
}
|
||||
// 兼容旧绑定(其它界面通过 ElementName 引用 PPTBtnPageNow / PPTBtnPageTotal)
|
||||
if (currentSlide > 0 && totalSlides > 0)
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
|
||||
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
|
||||
}
|
||||
else
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = "?";
|
||||
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理PPT放映状态变化
|
||||
/// </summary>
|
||||
public void OnSlideShowStateChanged(bool isInSlideShow)
|
||||
{
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!isInSlideShow)
|
||||
{
|
||||
// 如果不在放映模式,隐藏所有导航面板
|
||||
HideAllNavigationPanels();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理PPT放映状态变化失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新导航面板显示状态
|
||||
/// </summary>
|
||||
@@ -130,7 +222,16 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
// 检查是否应该显示PPT按钮
|
||||
bool shouldShowButtons = ShowPPTButton && _mainWindow.BtnPPTSlideShowEnd.Visibility == Visibility.Visible;
|
||||
// 不仅要检查按钮设置,还要确保确实在PPT放映模式下且页数有效
|
||||
bool isInSlideShow = _mainWindow.PPTManager?.IsInSlideShow == true;
|
||||
int slidesCount = _mainWindow.PPTManager?.SlidesCount ?? 0;
|
||||
bool hasValidPageCount = slidesCount > 0;
|
||||
|
||||
bool shouldShowButtons = ShowPPTButton &&
|
||||
_mainWindow.BtnPPTSlideShowEnd.Visibility == Visibility.Visible &&
|
||||
isInSlideShow &&
|
||||
hasValidPageCount &&
|
||||
!MainWindow.Settings.Automation.IsAutoFoldInPPTSlideShow;
|
||||
|
||||
if (!shouldShowButtons)
|
||||
{
|
||||
@@ -142,6 +243,10 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow.LeftSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTLSButtonPosition * 2);
|
||||
_mainWindow.RightSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTRSButtonPosition * 2);
|
||||
|
||||
// 设置底部按钮水平位置
|
||||
_mainWindow.LeftBottomPanelForPPTNavigation.Margin = new Thickness(6 + PPTLBButtonPosition, 0, 0, 6);
|
||||
_mainWindow.RightBottomPanelForPPTNavigation.Margin = new Thickness(0, 0, 6 + PPTRBButtonPosition, 6);
|
||||
|
||||
// 根据显示选项设置面板可见性
|
||||
var displayOption = PPTButtonsDisplayOption.ToString();
|
||||
if (displayOption.Length >= 4)
|
||||
@@ -293,17 +398,17 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
// 页码按钮显示
|
||||
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
|
||||
_mainWindow.PPTLSPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.PPTRSPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.LeftSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
|
||||
_mainWindow.RightSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
|
||||
|
||||
// 透明度设置
|
||||
var opacity = options[1] == '2' ? 0.5 : 1.0;
|
||||
_mainWindow.PPTBtnLSBorder.Opacity = opacity;
|
||||
_mainWindow.PPTBtnRSBorder.Opacity = opacity;
|
||||
// 透明度
|
||||
_mainWindow.LeftSidePanelForPPTNavigation.SetBarOpacity(PPTLSButtonOpacity);
|
||||
_mainWindow.RightSidePanelForPPTNavigation.SetBarOpacity(PPTRSButtonOpacity);
|
||||
|
||||
// 颜色主题
|
||||
bool isDarkTheme = options[2] == '2';
|
||||
ApplyButtonTheme(_mainWindow.PPTBtnLSBorder, _mainWindow.PPTBtnRSBorder, isDarkTheme, true);
|
||||
_mainWindow.LeftSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
|
||||
_mainWindow.RightSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -322,114 +427,23 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
// 页码按钮显示
|
||||
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
|
||||
_mainWindow.PPTLBPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.PPTRBPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.LeftBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
|
||||
_mainWindow.RightBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
|
||||
|
||||
// 透明度设置
|
||||
var opacity = options[1] == '2' ? 0.5 : 1.0;
|
||||
_mainWindow.PPTBtnLBBorder.Opacity = opacity;
|
||||
_mainWindow.PPTBtnRBBorder.Opacity = opacity;
|
||||
// 透明度
|
||||
_mainWindow.LeftBottomPanelForPPTNavigation.SetBarOpacity(PPTLBButtonOpacity);
|
||||
_mainWindow.RightBottomPanelForPPTNavigation.SetBarOpacity(PPTRBButtonOpacity);
|
||||
|
||||
// 颜色主题
|
||||
bool isDarkTheme = options[2] == '2';
|
||||
ApplyButtonTheme(_mainWindow.PPTBtnLBBorder, _mainWindow.PPTBtnRBBorder, isDarkTheme, false);
|
||||
_mainWindow.LeftBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
|
||||
_mainWindow.RightBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新底部按钮样式失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyButtonTheme(Border leftBorder, Border rightBorder, bool isDarkTheme, bool isSideButton)
|
||||
{
|
||||
try
|
||||
{
|
||||
Color backgroundColor, borderColor, foregroundColor, feedbackColor;
|
||||
|
||||
if (isDarkTheme)
|
||||
{
|
||||
backgroundColor = Color.FromRgb(39, 39, 42);
|
||||
borderColor = Color.FromRgb(82, 82, 91);
|
||||
foregroundColor = Colors.White;
|
||||
feedbackColor = Colors.White;
|
||||
}
|
||||
else
|
||||
{
|
||||
backgroundColor = Color.FromRgb(244, 244, 245);
|
||||
borderColor = Color.FromRgb(161, 161, 170);
|
||||
foregroundColor = Color.FromRgb(39, 39, 42);
|
||||
feedbackColor = Color.FromRgb(24, 24, 27);
|
||||
}
|
||||
|
||||
// 应用背景和边框颜色
|
||||
var backgroundBrush = new SolidColorBrush(backgroundColor);
|
||||
var borderBrush = new SolidColorBrush(borderColor);
|
||||
|
||||
leftBorder.Background = backgroundBrush;
|
||||
leftBorder.BorderBrush = borderBrush;
|
||||
rightBorder.Background = backgroundBrush;
|
||||
rightBorder.BorderBrush = borderBrush;
|
||||
|
||||
// 应用图标和文字颜色
|
||||
var foregroundBrush = new SolidColorBrush(foregroundColor);
|
||||
var feedbackBrush = new SolidColorBrush(feedbackColor);
|
||||
|
||||
if (isSideButton)
|
||||
{
|
||||
ApplySideButtonColors(foregroundBrush, feedbackBrush);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyBottomButtonColors(foregroundBrush, feedbackBrush);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用按钮主题失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySideButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
|
||||
{
|
||||
// 图标颜色
|
||||
_mainWindow.PPTLSPreviousButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTRSPreviousButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTLSNextButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTRSNextButtonGeometry.Brush = foregroundBrush;
|
||||
|
||||
// 反馈背景颜色
|
||||
_mainWindow.PPTLSPreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRSPreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTLSPageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRSPageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTLSNextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRSNextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
|
||||
// 文字颜色
|
||||
TextBlock.SetForeground(_mainWindow.PPTLSPageButton, foregroundBrush);
|
||||
TextBlock.SetForeground(_mainWindow.PPTRSPageButton, foregroundBrush);
|
||||
}
|
||||
|
||||
private void ApplyBottomButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
|
||||
{
|
||||
// 图标颜色
|
||||
_mainWindow.PPTLBPreviousButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTRBPreviousButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTLBNextButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTRBNextButtonGeometry.Brush = foregroundBrush;
|
||||
|
||||
// 反馈背景颜色
|
||||
_mainWindow.PPTLBPreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRBPreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTLBPageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRBPageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTLBNextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRBNextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
|
||||
// 文字颜色
|
||||
TextBlock.SetForeground(_mainWindow.PPTLBPageButton, foregroundBrush);
|
||||
TextBlock.SetForeground(_mainWindow.PPTRBPageButton, foregroundBrush);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Windows.Data.Pdf;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 Windows.Data.Pdf(WinRT)将 PDF 页渲染为 WPF 可用的位图。
|
||||
/// </summary>
|
||||
internal static class PdfWinRtHelper
|
||||
{
|
||||
public static async Task<uint> GetPageCountAsync(string pdfPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath))
|
||||
return 0;
|
||||
|
||||
var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask();
|
||||
var doc = await PdfDocument.LoadFromFileAsync(file).AsTask();
|
||||
if (doc.IsPasswordProtected)
|
||||
return 0;
|
||||
return doc.PageCount;
|
||||
}
|
||||
|
||||
public static async Task<BitmapSource> RenderPageToBitmapSourceAsync(string pdfPath, uint pageIndex)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath))
|
||||
return null;
|
||||
|
||||
var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask();
|
||||
var doc = await PdfDocument.LoadFromFileAsync(file).AsTask();
|
||||
if (doc.IsPasswordProtected)
|
||||
return null;
|
||||
if (pageIndex >= doc.PageCount)
|
||||
return null;
|
||||
|
||||
var page = doc.GetPage(pageIndex);
|
||||
try
|
||||
{
|
||||
using (var ras = new InMemoryRandomAccessStream())
|
||||
{
|
||||
await page.RenderToStreamAsync(ras).AsTask();
|
||||
ras.Seek(0);
|
||||
|
||||
var ms = new MemoryStream();
|
||||
using (var netStream = ras.AsStreamForRead())
|
||||
netStream.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
try
|
||||
{
|
||||
return await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var bi = new BitmapImage();
|
||||
bi.BeginInit();
|
||||
bi.StreamSource = ms;
|
||||
bi.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bi.EndInit();
|
||||
bi.Freeze();
|
||||
return (BitmapSource)bi;
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
ms.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
(page as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动台按钮控件
|
||||
/// </summary>
|
||||
public class LauncherButton
|
||||
{
|
||||
/// <summary>
|
||||
/// 父插件
|
||||
/// </summary>
|
||||
private readonly SuperLauncherPlugin _plugin;
|
||||
|
||||
/// <summary>
|
||||
/// 实际按钮控件
|
||||
/// </summary>
|
||||
private readonly SimpleStackPanel _panel;
|
||||
|
||||
/// <summary>
|
||||
/// 获取按钮UI元素
|
||||
/// </summary>
|
||||
public UIElement Element => _panel;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="plugin">父插件</param>
|
||||
public LauncherButton(SuperLauncherPlugin plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
_plugin = plugin;
|
||||
LogHelper.WriteLogToFile("开始创建启动台按钮");
|
||||
|
||||
// 创建SimpleStackPanel
|
||||
_panel = new SimpleStackPanel
|
||||
{
|
||||
Name = "Launcher_Icon",
|
||||
Orientation = Orientation.Vertical,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Width = 28,
|
||||
Margin = new Thickness(0, -2, 0, 0),
|
||||
Background = Brushes.Transparent
|
||||
};
|
||||
|
||||
LogHelper.WriteLogToFile("创建SimpleStackPanel完成");
|
||||
|
||||
// 添加图标
|
||||
var image = CreateIconImage();
|
||||
_panel.Children.Add(image);
|
||||
|
||||
// 添加文本
|
||||
TextBlock textBlock = new TextBlock
|
||||
{
|
||||
Text = "启动台",
|
||||
Foreground = Brushes.Black,
|
||||
FontSize = 8,
|
||||
Margin = new Thickness(0, 1, 0, 0),
|
||||
TextAlignment = TextAlignment.Center
|
||||
};
|
||||
_panel.Children.Add(textBlock);
|
||||
|
||||
// 设置鼠标事件
|
||||
_panel.MouseDown += Panel_MouseDown;
|
||||
_panel.MouseUp += Panel_MouseUp;
|
||||
_panel.MouseLeave += Panel_MouseLeave;
|
||||
|
||||
// 右键菜单支持
|
||||
_panel.ContextMenu = CreateContextMenu();
|
||||
|
||||
// 设置工具提示
|
||||
_panel.ToolTip = "启动台";
|
||||
|
||||
LogHelper.WriteLogToFile("启动台按钮创建完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建右键菜单
|
||||
/// </summary>
|
||||
private ContextMenu CreateContextMenu()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建菜单
|
||||
ContextMenu menu = new ContextMenu();
|
||||
|
||||
// 创建位置切换菜单项
|
||||
MenuItem positionMenuItem = new MenuItem();
|
||||
positionMenuItem.Header = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ?
|
||||
"移至右侧" : "移至左侧";
|
||||
positionMenuItem.Click += (s, e) =>
|
||||
{
|
||||
// 切换位置
|
||||
_plugin.Config.ButtonPosition = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ?
|
||||
LauncherButtonPosition.Right : LauncherButtonPosition.Left;
|
||||
|
||||
// 更新按钮位置
|
||||
_plugin.UpdateButtonPosition();
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
|
||||
LogHelper.WriteLogToFile($"通过右键菜单切换启动台按钮位置为: {_plugin.Config.ButtonPosition}");
|
||||
};
|
||||
menu.Items.Add(positionMenuItem);
|
||||
|
||||
// 添加设置菜单项
|
||||
MenuItem settingsMenuItem = new MenuItem();
|
||||
settingsMenuItem.Header = "打开设置";
|
||||
settingsMenuItem.Click += (s, e) =>
|
||||
{
|
||||
// 打开插件设置窗口
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用反射调用主窗口的ShowPluginSettings方法
|
||||
var method = mainWindow.GetType().GetMethod("ShowPluginSettings");
|
||||
if (method != null)
|
||||
{
|
||||
method.Invoke(mainWindow, null);
|
||||
LogHelper.WriteLogToFile("已打开插件设置窗口");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"打开插件设置窗口失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
};
|
||||
menu.Items.Add(settingsMenuItem);
|
||||
|
||||
return menu;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建右键菜单时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实际的UI元素
|
||||
/// </summary>
|
||||
[Obsolete("使用Element属性代替")]
|
||||
public UIElement GetUIElement()
|
||||
{
|
||||
return _panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建图标图像
|
||||
/// </summary>
|
||||
private Image CreateIconImage()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建图像
|
||||
Image image = new Image
|
||||
{
|
||||
Height = 17,
|
||||
Margin = new Thickness(0, 3, 0, 0)
|
||||
};
|
||||
|
||||
// 设置位图缩放模式
|
||||
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
|
||||
|
||||
// 创建绘图图像
|
||||
DrawingImage drawingImage = new DrawingImage();
|
||||
DrawingGroup drawingGroup = new DrawingGroup();
|
||||
drawingGroup.ClipGeometry = Geometry.Parse("M0,0 V24 H24 V0 H0 Z");
|
||||
|
||||
// 使用提供的应用网格图标
|
||||
GeometryDrawing geometryDrawing = new GeometryDrawing
|
||||
{
|
||||
Brush = new SolidColorBrush(Color.FromRgb(0x1B, 0x1B, 0x1B)),
|
||||
Geometry = Geometry.Parse("F0 M24,24z M0,0z M4.41721,4.29873C4.35178,4.29873,4.29873,4.35178,4.29873,4.41721L4.29873,9.15646C4.29873,9.22189,4.35178,9.27494,4.41721,9.27494L9.15646,9.27494C9.22189,9.27494,9.27494,9.22189,9.27494,9.15646L9.27494,4.41721C9.27494,4.35178,9.22189,4.29873,9.15646,4.29873L4.41721,4.29873z M2.64,4.41721C2.64,3.43569,3.43569,2.64,4.41721,2.64L9.15646,2.64C10.138,2.64,10.9337,3.43569,10.9337,4.41721L10.9337,9.15646C10.9337,10.138,10.138,10.9337,9.15646,10.9337L4.41721,10.9337C3.43569,10.9337,2.64,10.138,2.64,9.15646L2.64,4.41721z M14.8435,4.29873C14.7781,4.29873,14.7251,4.35178,14.7251,4.41721L14.7251,9.15646C14.7251,9.22189,14.7781,9.27494,14.8435,9.27494L19.5828,9.27494C19.6482,9.27494,19.7013,9.22189,19.7013,9.15646L19.7013,4.41721C19.7013,4.35178,19.6482,4.29873,19.5828,4.29873L14.8435,4.29873z M13.0663,4.41721C13.0663,3.43569,13.862,2.64,14.8435,2.64L19.5828,2.64C20.5643,2.64,21.36,3.43569,21.36,4.41721L21.36,9.15646C21.36,10.138,20.5643,10.9337,19.5828,10.9337L14.8435,10.9337C13.862,10.9337,13.0663,10.138,13.0663,9.15646L13.0663,4.41721z M14.8435,14.7251C14.7781,14.7251,14.7251,14.7781,14.7251,14.8435L14.7251,19.5828C14.7251,19.6482,14.7781,19.7013,14.8435,19.7013L19.5828,19.7013C19.6482,19.7013,19.7013,19.6482,19.7013,19.5828L19.7013,14.8435C19.7013,14.7781,19.6482,14.7251,19.5828,14.7251L14.8435,14.7251z M13.0663,14.8435C13.0663,13.862,13.862,13.0663,14.8435,13.0663L19.5828,13.0663C20.5643,13.0663,21.36,13.862,21.36,14.8435L21.36,19.5828C21.36,20.5643,20.5643,21.36,19.5828,21.36L14.8435,21.36C13.862,21.36,13.0663,20.5643,13.0663,19.5828L13.0663,14.8435z M4.41721,14.7251C4.35178,14.7251,4.29873,14.7781,4.29873,14.8435L4.29873,19.5828C4.29873,19.6482,4.35178,19.7013,4.41721,19.7013L9.15646,19.7013C9.22189,19.7013,9.27494,19.6482,9.27494,19.5828L9.27494,14.8435C9.27494,14.7781,9.22189,14.7251,9.15646,14.7251L4.41721,14.7251z M2.64,14.8435C2.64,13.862,3.43569,13.0663,4.41721,13.0663L9.15646,13.0663C10.138,13.0663,10.9337,13.862,10.9337,14.8435L10.9337,19.5828C10.9337,20.5643,10.138,21.36,9.15646,21.36L4.41721,21.36C3.43569,21.36,2.64,20.5643,2.64,19.5828L2.64,14.8435z")
|
||||
};
|
||||
|
||||
drawingGroup.Children.Add(geometryDrawing);
|
||||
|
||||
// 设置图像源
|
||||
drawingImage.Drawing = drawingGroup;
|
||||
image.Source = drawingImage;
|
||||
|
||||
return image;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建图标图像时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
|
||||
// 返回一个空图像
|
||||
return new Image();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标按下事件
|
||||
/// </summary>
|
||||
private void Panel_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 提供反馈
|
||||
_panel.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
|
||||
LogHelper.WriteLogToFile("启动台按钮鼠标按下");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动台按钮鼠标按下事件出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标抬起事件
|
||||
/// </summary>
|
||||
private void Panel_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 只有左键点击才显示启动台窗口
|
||||
if (e.ChangedButton != MouseButton.Left)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 恢复背景
|
||||
_panel.Background = Brushes.Transparent;
|
||||
LogHelper.WriteLogToFile("启动台按钮鼠标抬起,准备显示启动台窗口");
|
||||
|
||||
// 获取按钮在屏幕上的位置
|
||||
Point buttonPosition = _panel.PointToScreen(new Point(_panel.ActualWidth / 2, 0));
|
||||
|
||||
// 显示启动台窗口
|
||||
_plugin.ShowLauncherWindow(buttonPosition);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动台按钮鼠标抬起事件出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标离开事件
|
||||
/// </summary>
|
||||
private void Panel_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 恢复背景
|
||||
_panel.Background = Brushes.Transparent;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动台按钮鼠标离开事件出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动台按钮位置
|
||||
/// </summary>
|
||||
public enum LauncherButtonPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// 左侧
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// 右侧
|
||||
/// </summary>
|
||||
Right
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动台配置
|
||||
/// </summary>
|
||||
public class LauncherConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动台按钮位置
|
||||
/// </summary>
|
||||
public LauncherButtonPosition ButtonPosition { get; set; } = LauncherButtonPosition.Right;
|
||||
|
||||
/// <summary>
|
||||
/// 启动台应用程序列表
|
||||
/// </summary>
|
||||
public List<LauncherItem> Items { get; set; } = new List<LauncherItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动台应用项
|
||||
/// </summary>
|
||||
public class LauncherItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用程序名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序路径
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可见
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 在启动台中的位置(0-39)
|
||||
/// </summary>
|
||||
public int Position { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已固定位置
|
||||
/// </summary>
|
||||
public bool IsPositionFixed { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 图标缓存
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
private ImageSource _iconCache;
|
||||
|
||||
/// <summary>
|
||||
/// 获取应用程序图标
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ImageSource Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_iconCache != null)
|
||||
{
|
||||
return _iconCache;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(Path))
|
||||
{
|
||||
// 从文件中获取图标
|
||||
Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(Path);
|
||||
if (icon != null)
|
||||
{
|
||||
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
|
||||
icon.Handle,
|
||||
Int32Rect.Empty,
|
||||
BitmapSizeOptions.FromEmptyOptions());
|
||||
|
||||
icon.Dispose();
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 从注册表中获取文件类型关联图标
|
||||
string extension = System.IO.Path.GetExtension(Path);
|
||||
if (!string.IsNullOrEmpty(extension))
|
||||
{
|
||||
string fileType = Registry.ClassesRoot.OpenSubKey(extension)?.GetValue(string.Empty) as string;
|
||||
if (!string.IsNullOrEmpty(fileType))
|
||||
{
|
||||
string iconPath = Registry.ClassesRoot.OpenSubKey(fileType + "\\DefaultIcon")?.GetValue(string.Empty) as string;
|
||||
if (!string.IsNullOrEmpty(iconPath))
|
||||
{
|
||||
string[] parts = iconPath.Split(',');
|
||||
string iconFile = parts[0].Trim('"');
|
||||
int iconIndex = parts.Length > 1 ? Convert.ToInt32(parts[1]) : 0;
|
||||
|
||||
if (File.Exists(iconFile))
|
||||
{
|
||||
Icon icon = IconExtractor.Extract(iconFile, iconIndex, true);
|
||||
if (icon != null)
|
||||
{
|
||||
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
|
||||
icon.Handle,
|
||||
Int32Rect.Empty,
|
||||
BitmapSizeOptions.FromEmptyOptions());
|
||||
|
||||
icon.Dispose();
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取应用图标时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
// 返回默认图标
|
||||
return GetDefaultIcon();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认图标
|
||||
/// </summary>
|
||||
private ImageSource GetDefaultIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 对于资源管理器,使用特定图标
|
||||
if (Path.EndsWith("explorer.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 直接从C:\Windows\explorer.exe获取图标
|
||||
string explorerPath = @"C:\Windows\explorer.exe";
|
||||
if (File.Exists(explorerPath))
|
||||
{
|
||||
Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(explorerPath);
|
||||
if (icon != null)
|
||||
{
|
||||
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
|
||||
icon.Handle,
|
||||
Int32Rect.Empty,
|
||||
BitmapSizeOptions.FromEmptyOptions());
|
||||
|
||||
icon.Dispose();
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取资源管理器图标时出错: {ex.Message}", LogHelper.LogType.Warning);
|
||||
// 如果获取Windows图标失败,回退到默认图标
|
||||
}
|
||||
|
||||
// 回退到备用图标
|
||||
string explorerIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_folder_24_regular.png");
|
||||
if (File.Exists(explorerIconPath))
|
||||
{
|
||||
Uri uri = new Uri(explorerIconPath);
|
||||
BitmapImage image = new BitmapImage(uri);
|
||||
_iconCache = image;
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回一个简单的默认图标
|
||||
string iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-png", "icc.png");
|
||||
if (File.Exists(iconPath))
|
||||
{
|
||||
Uri uri = new Uri(iconPath);
|
||||
BitmapImage image = new BitmapImage(uri);
|
||||
_iconCache = image;
|
||||
return _iconCache;
|
||||
}
|
||||
|
||||
// 如果还是没有找到,尝试使用应用程序图标
|
||||
string appIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_apps_24_regular.png");
|
||||
if (File.Exists(appIconPath))
|
||||
{
|
||||
Uri uri = new Uri(appIconPath);
|
||||
BitmapImage image = new BitmapImage(uri);
|
||||
_iconCache = image;
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取默认图标时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动应用程序
|
||||
/// </summary>
|
||||
public void Launch()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(Path))
|
||||
{
|
||||
LogHelper.WriteLogToFile("无法启动应用程序:路径为空", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!File.Exists(Path) && !Path.Contains(":\\"))
|
||||
{
|
||||
// 可能是系统命令,如explorer.exe
|
||||
ProcessStartInfo psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path,
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(psi);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 使用Process.Start启动应用程序
|
||||
ProcessStartInfo psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path,
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(psi);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已启动应用程序: {Path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动应用程序时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"启动应用程序时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图标提取工具类
|
||||
/// </summary>
|
||||
public static class IconExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// 从文件中提取图标
|
||||
/// </summary>
|
||||
/// <param name="file">文件路径</param>
|
||||
/// <param name="index">图标索引</param>
|
||||
/// <param name="largeIcon">是否提取大图标</param>
|
||||
/// <returns>提取的图标</returns>
|
||||
public static Icon Extract(string file, int index, bool largeIcon)
|
||||
{
|
||||
try
|
||||
{
|
||||
IntPtr large;
|
||||
IntPtr small;
|
||||
ExtractIconEx(file, index, out large, out small, 1);
|
||||
|
||||
try
|
||||
{
|
||||
return Icon.FromHandle(largeIcon ? large : small);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (large != IntPtr.Zero)
|
||||
DestroyIcon(large);
|
||||
|
||||
if (small != IntPtr.Zero)
|
||||
DestroyIcon(small);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("Shell32.dll", EntryPoint = "ExtractIconEx")]
|
||||
private static extern int ExtractIconEx(
|
||||
[MarshalAs(UnmanagedType.LPStr)] string lpszFile,
|
||||
int nIconIndex,
|
||||
out IntPtr phiconLarge,
|
||||
out IntPtr phiconSmall,
|
||||
int nIcons);
|
||||
|
||||
[DllImport("User32.dll")]
|
||||
private static extern int DestroyIcon(IntPtr hIcon);
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
<UserControl x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherSettingsControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="500" d:DesignWidth="600">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- 自定义按钮样式 -->
|
||||
<Style x:Key="DefaultButtonStyle" TargetType="Button">
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextElement.Foreground="{TemplateBinding Foreground}"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Opacity" Value="0.8"/>
|
||||
<Setter Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect Color="Black" Direction="270" ShadowDepth="2" Opacity="0.3" BlurRadius="4"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="border" Property="Opacity" Value="0.6"/>
|
||||
<Setter Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="border" Property="Opacity" Value="0.4"/>
|
||||
<Setter Property="Cursor" Value="Arrow"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题 -->
|
||||
<TextBlock Grid.Row="0" Text="超级启动台设置" FontSize="16" FontWeight="Bold" Margin="0,0,0,15" Foreground="Black"/>
|
||||
|
||||
<!-- 基本设置 -->
|
||||
<StackPanel Grid.Row="1" Margin="0,0,0,15">
|
||||
<TextBlock Text="基本设置" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
|
||||
|
||||
<Grid Margin="10,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 按钮位置 -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="按钮位置:" VerticalAlignment="Center" Foreground="Black"/>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="0,5">
|
||||
<RadioButton x:Name="RbtnLeft" Content="浮动栏左侧" Margin="0,0,20,0" Checked="RbtnPosition_Checked" Foreground="Black"/>
|
||||
<RadioButton x:Name="RbtnRight" Content="浮动栏右侧" IsChecked="True" Checked="RbtnPosition_Checked" Foreground="Black"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 应用管理 -->
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Text="应用管理" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
|
||||
|
||||
<Border Grid.Row="1" BorderThickness="1" BorderBrush="#CCCCCC" CornerRadius="5">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 应用列表 -->
|
||||
<DataGrid Grid.Row="0" x:Name="DgApps" AutoGenerateColumns="False" Margin="5"
|
||||
CanUserAddRows="False" CanUserDeleteRows="False"
|
||||
HeadersVisibility="Column" SelectionMode="Single"
|
||||
SelectionChanged="DgApps_SelectionChanged">
|
||||
<DataGrid.Columns>
|
||||
<DataGridCheckBoxColumn Header="显示" Binding="{Binding IsVisible}" Width="50"/>
|
||||
<DataGridTextColumn Header="名称" Binding="{Binding Name}" Width="150"/>
|
||||
<DataGridTextColumn Header="路径" Binding="{Binding Path}" Width="*"/>
|
||||
<DataGridTextColumn Header="位置" Binding="{Binding Position}" Width="50"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="5">
|
||||
<Button x:Name="BtnAdd" Content="添加" Padding="10,5" Margin="0,5,5,5" Click="BtnAdd_Click"
|
||||
Background="#FF007ACC" Foreground="White" BorderBrush="#FF005A9B" BorderThickness="1"
|
||||
Style="{StaticResource DefaultButtonStyle}"/>
|
||||
<Button x:Name="BtnEdit" Content="编辑" Padding="10,5" Margin="5" Click="BtnEdit_Click"
|
||||
Background="#FF6C757D" Foreground="White" BorderBrush="#FF5A6268" BorderThickness="1"
|
||||
Style="{StaticResource DefaultButtonStyle}"/>
|
||||
<Button x:Name="BtnDelete" Content="删除" Padding="10,5" Margin="5" Click="BtnDelete_Click"
|
||||
Background="#FFDC3545" Foreground="White" BorderBrush="#FFBD2130" BorderThickness="1"
|
||||
Style="{StaticResource DefaultButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0">
|
||||
<Button x:Name="BtnSave" Content="保存设置" Padding="15,5" Click="BtnSave_Click"
|
||||
Background="#FF28A745" Foreground="White" BorderBrush="#FF1E7E34" BorderThickness="1"
|
||||
Style="{StaticResource DefaultButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,396 +0,0 @@
|
||||
using Ink_Canvas.Windows;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// LauncherSettingsControl.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class LauncherSettingsControl : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 父插件
|
||||
/// </summary>
|
||||
private readonly SuperLauncherPlugin _plugin;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="plugin">父插件</param>
|
||||
public LauncherSettingsControl(SuperLauncherPlugin plugin)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_plugin = plugin;
|
||||
|
||||
// 设置按钮位置
|
||||
RbtnLeft.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left;
|
||||
RbtnRight.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Right;
|
||||
|
||||
// 绑定应用列表
|
||||
DgApps.ItemsSource = _plugin.LauncherItems;
|
||||
|
||||
// 初始化按钮状态
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新按钮状态
|
||||
/// </summary>
|
||||
private void UpdateButtonStates()
|
||||
{
|
||||
bool hasSelection = DgApps.SelectedItem != null;
|
||||
BtnEdit.IsEnabled = hasSelection;
|
||||
BtnDelete.IsEnabled = hasSelection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 位置单选按钮选择事件
|
||||
/// </summary>
|
||||
private void RbtnPosition_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
|
||||
LauncherButtonPosition oldPosition = _plugin.Config.ButtonPosition;
|
||||
|
||||
if (sender == RbtnLeft)
|
||||
{
|
||||
_plugin.Config.ButtonPosition = LauncherButtonPosition.Left;
|
||||
}
|
||||
else if (sender == RbtnRight)
|
||||
{
|
||||
_plugin.Config.ButtonPosition = LauncherButtonPosition.Right;
|
||||
}
|
||||
|
||||
// 如果位置发生变化,更新按钮位置
|
||||
if (oldPosition != _plugin.Config.ButtonPosition)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 更新按钮位置
|
||||
_plugin.UpdateButtonPosition();
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
|
||||
LogHelper.WriteLogToFile($"启动台按钮位置已更改为: {_plugin.Config.ButtonPosition}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"更新启动台按钮位置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnAdd_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建新的启动项
|
||||
LauncherItem item = new LauncherItem
|
||||
{
|
||||
Name = "",
|
||||
Path = "",
|
||||
IsVisible = true,
|
||||
Position = -1 // 让插件管理器分配位置
|
||||
};
|
||||
|
||||
// 直接显示编辑对话框
|
||||
EditLauncherItem(item, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"添加启动项时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"添加启动项时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑应用按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnEdit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DgApps.SelectedItem is LauncherItem item)
|
||||
{
|
||||
EditLauncherItem(item, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除应用按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnDelete_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DgApps.SelectedItem is LauncherItem item)
|
||||
{
|
||||
// 确认删除
|
||||
MessageBoxResult result = MessageBox.Show(
|
||||
$"确定要删除 {item.Name} 吗?",
|
||||
"删除确认",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
// 从集合中移除
|
||||
_plugin.LauncherItems.Remove(item);
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存设置按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnSave_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
|
||||
// 如果插件已启用,重新加载启动台按钮
|
||||
if (_plugin.IsEnabled)
|
||||
{
|
||||
_plugin.Disable();
|
||||
_plugin.Enable();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果插件未启用,则启用它
|
||||
_plugin.Enable();
|
||||
|
||||
// 通知PluginSettingsWindow刷新插件列表
|
||||
var window = Window.GetWindow(this);
|
||||
if (window is PluginSettingsWindow pluginSettingsWindow)
|
||||
{
|
||||
// 触发刷新
|
||||
pluginSettingsWindow.RefreshPluginList();
|
||||
}
|
||||
}
|
||||
|
||||
MessageBox.Show("设置已保存并应用!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存设置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"保存设置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用项选择变更事件
|
||||
/// </summary>
|
||||
private void DgApps_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑启动项
|
||||
/// </summary>
|
||||
/// <param name="item">启动项</param>
|
||||
/// <param name="isNew">是否为新建</param>
|
||||
private void EditLauncherItem(LauncherItem item, bool isNew)
|
||||
{
|
||||
// 创建简单的编辑窗口
|
||||
Window editWindow = new Window
|
||||
{
|
||||
Title = isNew ? "添加" : "编辑应用",
|
||||
Width = 400,
|
||||
Height = 200,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
ResizeMode = ResizeMode.NoResize
|
||||
};
|
||||
|
||||
// 创建编辑表单
|
||||
Grid grid = new Grid
|
||||
{
|
||||
Margin = new Thickness(20)
|
||||
};
|
||||
|
||||
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
// 名称输入框
|
||||
TextBlock nameLabel = new TextBlock
|
||||
{
|
||||
Text = "名称:",
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
TextBox nameTextBox = new TextBox
|
||||
{
|
||||
Text = item.Name,
|
||||
Margin = new Thickness(0, 5, 0, 5)
|
||||
};
|
||||
|
||||
Grid.SetRow(nameLabel, 0);
|
||||
Grid.SetColumn(nameLabel, 0);
|
||||
Grid.SetRow(nameTextBox, 0);
|
||||
Grid.SetColumn(nameTextBox, 1);
|
||||
|
||||
grid.Children.Add(nameLabel);
|
||||
grid.Children.Add(nameTextBox);
|
||||
|
||||
// 路径输入框
|
||||
TextBlock pathLabel = new TextBlock
|
||||
{
|
||||
Text = "路径:",
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
Grid pathGrid = new Grid();
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength() });
|
||||
|
||||
TextBox pathTextBox = new TextBox
|
||||
{
|
||||
Text = item.Path,
|
||||
Margin = new Thickness(0, 5, 5, 5)
|
||||
};
|
||||
Button browseButton = new Button
|
||||
{
|
||||
Content = "浏览",
|
||||
Padding = new Thickness(5, 0, 5, 0),
|
||||
Margin = new Thickness(0, 5, 0, 5)
|
||||
};
|
||||
|
||||
browseButton.Click += (s, e) =>
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog
|
||||
{
|
||||
Title = "选择应用程序",
|
||||
Filter = "应用程序 (*.exe)|*.exe|所有文件 (*.*)|*.*",
|
||||
Multiselect = false,
|
||||
FileName = pathTextBox.Text
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
pathTextBox.Text = dialog.FileName;
|
||||
|
||||
// 如果选择的是.exe文件,自动获取文件名填入名称字段
|
||||
if (Path.GetExtension(dialog.FileName).ToLower() == ".exe")
|
||||
{
|
||||
string fileName = Path.GetFileNameWithoutExtension(dialog.FileName);
|
||||
// 只有在名称字段为空或者是新建项目时才自动填入
|
||||
if (string.IsNullOrWhiteSpace(nameTextBox.Text) || isNew)
|
||||
{
|
||||
nameTextBox.Text = fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Grid.SetColumn(pathTextBox, 0);
|
||||
Grid.SetColumn(browseButton, 1);
|
||||
pathGrid.Children.Add(pathTextBox);
|
||||
pathGrid.Children.Add(browseButton);
|
||||
|
||||
Grid.SetRow(pathLabel, 1);
|
||||
Grid.SetColumn(pathLabel, 0);
|
||||
Grid.SetRow(pathGrid, 1);
|
||||
Grid.SetColumn(pathGrid, 1);
|
||||
|
||||
grid.Children.Add(pathLabel);
|
||||
grid.Children.Add(pathGrid);
|
||||
|
||||
// 确认和取消按钮
|
||||
StackPanel buttonPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 10, 0, 0)
|
||||
};
|
||||
|
||||
Button okButton = new Button
|
||||
{
|
||||
Content = "确定",
|
||||
Padding = new Thickness(15, 5, 15, 5),
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
IsDefault = true
|
||||
};
|
||||
|
||||
Button cancelButton = new Button
|
||||
{
|
||||
Content = "取消",
|
||||
Padding = new Thickness(15, 5, 15, 5),
|
||||
IsCancel = true
|
||||
};
|
||||
|
||||
okButton.Click += (s, e) =>
|
||||
{
|
||||
// 验证输入
|
||||
if (string.IsNullOrWhiteSpace(nameTextBox.Text))
|
||||
{
|
||||
MessageBox.Show("请输入应用名称!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pathTextBox.Text))
|
||||
{
|
||||
MessageBox.Show("请输入应用路径!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新项目
|
||||
item.Name = nameTextBox.Text;
|
||||
item.Path = pathTextBox.Text;
|
||||
|
||||
// 如果是新建,添加到集合
|
||||
if (isNew)
|
||||
{
|
||||
_plugin.AddLauncherItem(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 触发属性变更通知,刷新DataGrid
|
||||
if (DgApps.ItemsSource is ICollectionView view)
|
||||
{
|
||||
view.Refresh();
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
|
||||
editWindow.DialogResult = true;
|
||||
editWindow.Close();
|
||||
};
|
||||
|
||||
cancelButton.Click += (s, e) =>
|
||||
{
|
||||
editWindow.DialogResult = false;
|
||||
editWindow.Close();
|
||||
};
|
||||
|
||||
buttonPanel.Children.Add(okButton);
|
||||
buttonPanel.Children.Add(cancelButton);
|
||||
|
||||
Grid.SetRow(buttonPanel, 2);
|
||||
Grid.SetColumnSpan(buttonPanel, 2);
|
||||
|
||||
grid.Children.Add(buttonPanel);
|
||||
|
||||
// 设置窗口内容
|
||||
editWindow.Content = grid;
|
||||
|
||||
// 显示窗口
|
||||
editWindow.ShowDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
<Window x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
|
||||
mc:Ignorable="d"
|
||||
Title="启动台"
|
||||
Width="400"
|
||||
Height="300"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="#80000000"
|
||||
ResizeMode="NoResize"
|
||||
Topmost="True"
|
||||
Deactivated="Window_Deactivated"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<Window.Resources>
|
||||
<!-- 应用项样式 -->
|
||||
<Style x:Key="LauncherItemStyle" TargetType="Button">
|
||||
<Setter Property="Width" Value="80"/>
|
||||
<Setter Property="Height" Value="80"/>
|
||||
<Setter Property="Margin" Value="5"/>
|
||||
<Setter Property="Background" Value="#40FFFFFF"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="border" Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="8">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Image Grid.Row="0" Source="{Binding Icon}" Width="32" Height="32" Margin="0,10,0,5"/>
|
||||
<TextBlock Grid.Row="1" Text="{Binding Name}" TextWrapping="Wrap" TextAlignment="Center"
|
||||
Margin="2,0,2,8" FontSize="11" Foreground="White"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#80FFFFFF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#C0FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Border CornerRadius="15" Background="#80000000" BorderThickness="1" BorderBrush="#40FFFFFF">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<Grid Grid.Row="0" Height="40">
|
||||
<TextBlock Text="启动台" Foreground="White" FontSize="18" FontWeight="Bold"
|
||||
VerticalAlignment="Center" Margin="15,0,0,0"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,10,0">
|
||||
<Button x:Name="BtnFixMode" Click="BtnFixMode_Click" Width="30" Height="30"
|
||||
Margin="5,0" Background="Transparent" BorderThickness="0"
|
||||
ToolTip="切换固定模式">
|
||||
<Path x:Name="FixModeIcon" Data="M7,2V13H10V22L17,10H13L17,2H7Z" Fill="White" Stretch="Uniform" Width="16" Height="16"/>
|
||||
</Button>
|
||||
<Button x:Name="BtnClose" Click="BtnClose_Click" Width="30" Height="30"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
ToolTip="关闭">
|
||||
<Path Data="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
|
||||
Fill="White" Stretch="Uniform" Width="16" Height="16"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 应用网格 -->
|
||||
<ScrollViewer Grid.Row="1" Margin="10" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<WrapPanel x:Name="AppPanel" Orientation="Horizontal" HorizontalAlignment="Center"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -1,466 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// LauncherWindow.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class LauncherWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 父插件
|
||||
/// </summary>
|
||||
private readonly SuperLauncherPlugin _plugin;
|
||||
|
||||
/// <summary>
|
||||
/// 是否处于固定模式
|
||||
/// </summary>
|
||||
private bool _isFixMode;
|
||||
|
||||
/// <summary>
|
||||
/// 应用项按钮列表
|
||||
/// </summary>
|
||||
private readonly Dictionary<Button, LauncherItem> _appButtons = new Dictionary<Button, LauncherItem>();
|
||||
|
||||
/// <summary>
|
||||
/// 拖拽中的按钮
|
||||
/// </summary>
|
||||
private Button _draggingButton;
|
||||
|
||||
/// <summary>
|
||||
/// 拖拽开始位置
|
||||
/// </summary>
|
||||
private Point _dragStartPoint;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public LauncherWindow(SuperLauncherPlugin plugin)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_plugin = plugin;
|
||||
|
||||
// 加载应用项
|
||||
LoadLauncherItems();
|
||||
|
||||
// 添加鼠标按下事件(用于拖动窗口)
|
||||
MouseDown += (s, e) =>
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed)
|
||||
{
|
||||
DragMove();
|
||||
}
|
||||
};
|
||||
|
||||
// 根据应用数量调整窗口大小
|
||||
AdjustWindowSize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载启动台应用项
|
||||
/// </summary>
|
||||
private void LoadLauncherItems()
|
||||
{
|
||||
// 清空现有应用项
|
||||
AppPanel.Children.Clear();
|
||||
_appButtons.Clear();
|
||||
|
||||
// 获取显示的应用项
|
||||
var visibleItems = _plugin.LauncherItems
|
||||
.Where(item => item.IsVisible)
|
||||
.OrderBy(item => item.Position)
|
||||
.ToList();
|
||||
|
||||
foreach (var item in visibleItems)
|
||||
{
|
||||
// 创建应用按钮
|
||||
Button appButton = new Button
|
||||
{
|
||||
Style = (Style)FindResource("LauncherItemStyle"),
|
||||
DataContext = item,
|
||||
Tag = item.Position
|
||||
};
|
||||
|
||||
// 添加点击事件
|
||||
appButton.Click += AppButton_Click;
|
||||
|
||||
// 在固定模式下,添加拖拽事件
|
||||
appButton.PreviewMouseDown += AppButton_PreviewMouseDown;
|
||||
appButton.PreviewMouseMove += AppButton_PreviewMouseMove;
|
||||
appButton.PreviewMouseUp += AppButton_PreviewMouseUp;
|
||||
|
||||
// 记录按钮和项目的对应关系
|
||||
_appButtons.Add(appButton, item);
|
||||
|
||||
// 添加到面板
|
||||
AppPanel.Children.Add(appButton);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据应用数量调整窗口大小
|
||||
/// </summary>
|
||||
private void AdjustWindowSize()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 每行最多显示4个应用
|
||||
const int appsPerRow = 4;
|
||||
|
||||
// 计算行数
|
||||
int visibleCount = _appButtons.Count;
|
||||
int rowCount = (int)Math.Ceiling(visibleCount / (double)appsPerRow);
|
||||
|
||||
// 设置窗口宽度(每个应用90像素宽 = 80 + 5*2)
|
||||
Width = Math.Min(appsPerRow * 90 + 40, 400); // 最大宽度400
|
||||
|
||||
// 设置窗口高度(每个应用90像素高 = 80 + 5*2)
|
||||
Height = Math.Min(rowCount * 90 + 60, 600); // 最大高度600,标题栏40 + 边距20
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"调整启动台窗口大小时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用按钮点击事件
|
||||
/// </summary>
|
||||
private void AppButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_isFixMode) return; // 在固定模式下,不响应点击事件
|
||||
|
||||
if (sender is Button button && _appButtons.TryGetValue(button, out LauncherItem item))
|
||||
{
|
||||
// 获取应用路径和名称,用于后续启动
|
||||
string appPath = item.Path;
|
||||
string appName = item.Name;
|
||||
|
||||
LogHelper.WriteLogToFile($"点击启动应用: {appName}, 路径: {appPath}");
|
||||
|
||||
// 首先标记窗口正在关闭
|
||||
IsClosing = true;
|
||||
|
||||
// 创建一个应用启动任务
|
||||
var launchTask = new Task(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 等待一段时间,确保窗口关闭流程已经开始
|
||||
Thread.Sleep(200);
|
||||
|
||||
// 使用UI线程启动应用
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查应用路径是否存在
|
||||
if (File.Exists(appPath) || !appPath.Contains(":\\"))
|
||||
{
|
||||
// 创建进程启动信息
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = appPath,
|
||||
UseShellExecute = true,
|
||||
};
|
||||
|
||||
// 启动应用程序
|
||||
var process = Process.Start(psi);
|
||||
LogHelper.WriteLogToFile($"应用程序 {appName} 已启动");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用路径不存在: {appPath}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"找不到应用程序: {appPath}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动应用程序失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"启动应用程序失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用启动任务出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
|
||||
// 关闭窗口
|
||||
try
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try { Close(); } catch { }
|
||||
|
||||
// 启动应用程序任务
|
||||
launchTask.Start();
|
||||
}), DispatcherPriority.Background);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"关闭窗口或启动任务时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
// 如果无法通过UI关闭窗口,直接启动任务
|
||||
launchTask.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用按钮点击事件出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
try { IsClosing = true; Close(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
#region 固定模式拖拽事件
|
||||
|
||||
/// <summary>
|
||||
/// 应用按钮鼠标按下事件
|
||||
/// </summary>
|
||||
private void AppButton_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_isFixMode) return;
|
||||
|
||||
if (e.ChangedButton == MouseButton.Left && sender is Button button)
|
||||
{
|
||||
_draggingButton = button;
|
||||
_dragStartPoint = e.GetPosition(AppPanel);
|
||||
button.CaptureMouse();
|
||||
button.Opacity = 0.7;
|
||||
|
||||
// 阻止事件冒泡,以避免触发按钮点击
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用按钮鼠标移动事件
|
||||
/// </summary>
|
||||
private void AppButton_PreviewMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_isFixMode || _draggingButton == null) return;
|
||||
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
{
|
||||
Point currentPosition = e.GetPosition(AppPanel);
|
||||
|
||||
// 移动按钮
|
||||
System.Windows.Controls.Canvas.SetLeft(_draggingButton, currentPosition.X - _draggingButton.ActualWidth / 2);
|
||||
System.Windows.Controls.Canvas.SetTop(_draggingButton, currentPosition.Y - _draggingButton.ActualHeight / 2);
|
||||
|
||||
// 将按钮移到最上层
|
||||
Panel.SetZIndex(_draggingButton, 100);
|
||||
|
||||
// 阻止事件冒泡
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用按钮鼠标释放事件
|
||||
/// </summary>
|
||||
private void AppButton_PreviewMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_isFixMode || _draggingButton == null) return;
|
||||
|
||||
// 释放鼠标捕获
|
||||
_draggingButton.ReleaseMouseCapture();
|
||||
|
||||
// 计算新位置
|
||||
Point releasePoint = e.GetPosition(AppPanel);
|
||||
int newPosition = CalculateGridPosition(releasePoint);
|
||||
|
||||
// 获取当前项目
|
||||
LauncherItem currentItem = _appButtons[_draggingButton];
|
||||
|
||||
// 重新排序
|
||||
ReorderItems(currentItem, newPosition);
|
||||
|
||||
// 重新加载应用项
|
||||
LoadLauncherItems();
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
|
||||
// 清除拖拽状态
|
||||
_draggingButton.Opacity = 1;
|
||||
Panel.SetZIndex(_draggingButton, 0);
|
||||
_draggingButton = null;
|
||||
|
||||
// 阻止事件冒泡
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算网格位置
|
||||
/// </summary>
|
||||
private int CalculateGridPosition(Point point)
|
||||
{
|
||||
// 计算行和列
|
||||
int columnCount = 4; // 每行最多4个应用
|
||||
int columnWidth = 90; // 应用宽度(包括边距)
|
||||
int rowHeight = 90; // 应用高度(包括边距)
|
||||
|
||||
int column = (int)(point.X / columnWidth);
|
||||
int row = (int)(point.Y / rowHeight);
|
||||
|
||||
// 确保在有效范围内
|
||||
column = Math.Max(0, Math.Min(column, columnCount - 1));
|
||||
row = Math.Max(0, row);
|
||||
|
||||
// 计算位置索引
|
||||
return row * columnCount + column;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新排序应用项
|
||||
/// </summary>
|
||||
private void ReorderItems(LauncherItem item, int newPosition)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 设置项目为固定位置
|
||||
item.IsPositionFixed = true;
|
||||
|
||||
// 如果位置相同,无需调整
|
||||
if (item.Position == newPosition)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有可见项目
|
||||
var visibleItems = _plugin.LauncherItems
|
||||
.Where(i => i.IsVisible)
|
||||
.OrderBy(i => i.Position)
|
||||
.ToList();
|
||||
|
||||
// 移除当前项目
|
||||
visibleItems.Remove(item);
|
||||
|
||||
// 查找插入位置
|
||||
int insertIndex = 0;
|
||||
for (int i = 0; i < visibleItems.Count; i++)
|
||||
{
|
||||
if (visibleItems[i].Position >= newPosition)
|
||||
{
|
||||
insertIndex = i;
|
||||
break;
|
||||
}
|
||||
insertIndex = i + 1;
|
||||
}
|
||||
|
||||
// 插入项目
|
||||
visibleItems.Insert(insertIndex, item);
|
||||
|
||||
// 重新分配位置
|
||||
for (int i = 0; i < visibleItems.Count; i++)
|
||||
{
|
||||
visibleItems[i].Position = i;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"重新排序应用项时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 窗口事件处理
|
||||
|
||||
/// <summary>
|
||||
/// 窗口失去焦点事件
|
||||
/// </summary>
|
||||
private void Window_Deactivated(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 只有在非固定模式、窗口已加载、未处于关闭状态且IsLoaded=true时关闭窗口
|
||||
if (!_isFixMode && IsLoaded && !IsClosing)
|
||||
{
|
||||
// 标记为正在关闭
|
||||
IsClosing = true;
|
||||
|
||||
// 使用Dispatcher.BeginInvoke而不是直接调用Close,避免冲突
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再次检查窗口状态
|
||||
if (IsLoaded && !IsClosing)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"延迟关闭窗口时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}), DispatcherPriority.Background);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"窗口失去焦点关闭时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口是否正在关闭
|
||||
/// </summary>
|
||||
private bool IsClosing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 重写OnClosing方法,标记窗口正在关闭
|
||||
/// </summary>
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
IsClosing = true;
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 固定模式按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnFixMode_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 切换固定模式
|
||||
_isFixMode = !_isFixMode;
|
||||
|
||||
// 更新固定模式按钮图标颜色
|
||||
FixModeIcon.Fill = _isFixMode ? Brushes.Yellow : Brushes.White;
|
||||
|
||||
// 显示提示
|
||||
if (_isFixMode)
|
||||
{
|
||||
MessageBox.Show("已进入固定模式,您可以拖动应用图标调整位置。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,589 +0,0 @@
|
||||
using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn
|
||||
{
|
||||
/// <summary>
|
||||
/// 超级启动台插件
|
||||
/// </summary>
|
||||
public class SuperLauncherPlugin : PluginBase
|
||||
{
|
||||
#region 插件基本信息
|
||||
|
||||
public override string Name => "超级启动台";
|
||||
|
||||
public override string Description => "在浮动栏添加一个启动台按钮,可快速启动常用应用程序。";
|
||||
|
||||
public override Version Version => new Version(1, 0, 1);
|
||||
|
||||
public override string Author => "ICC CE 团队";
|
||||
|
||||
public override bool IsBuiltIn => true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件属性和字段
|
||||
|
||||
/// <summary>
|
||||
/// 启动台配置
|
||||
/// </summary>
|
||||
public LauncherConfig Config { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动台应用程序列表
|
||||
/// </summary>
|
||||
public ObservableCollection<LauncherItem> LauncherItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动台按钮
|
||||
/// </summary>
|
||||
private LauncherButton _launcherButton;
|
||||
|
||||
/// <summary>
|
||||
/// 启动台窗口
|
||||
/// </summary>
|
||||
private LauncherWindow _launcherWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件路径
|
||||
/// </summary>
|
||||
private readonly string _configPath = Path.Combine(App.RootPath, "PluginConfigs", "SuperLauncher.json");
|
||||
|
||||
/// <summary>
|
||||
/// 标记是否已添加到浮动栏
|
||||
/// </summary>
|
||||
private bool _isAddedToFloatingBar;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件生命周期
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// 创建配置目录
|
||||
string configDir = Path.Combine(App.RootPath, "PluginConfigs");
|
||||
if (!Directory.Exists(configDir))
|
||||
{
|
||||
Directory.CreateDirectory(configDir);
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
LoadConfig();
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台插件已初始化");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsEnabled) return; // 防止重复启用
|
||||
|
||||
// 创建启动台按钮
|
||||
if (_launcherButton == null)
|
||||
{
|
||||
_launcherButton = new LauncherButton(this);
|
||||
LogHelper.WriteLogToFile("超级启动台按钮已创建");
|
||||
}
|
||||
|
||||
// 添加启动台按钮到浮动栏
|
||||
AddLauncherButtonToFloatingBar();
|
||||
|
||||
// 设置启用状态
|
||||
base.Enable();
|
||||
|
||||
// 保存插件配置
|
||||
SavePluginSettings();
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台插件已启用");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsEnabled) return; // 防止重复禁用
|
||||
|
||||
// 从浮动栏移除启动台按钮
|
||||
RemoveLauncherButtonFromFloatingBar();
|
||||
|
||||
// 如果启动台窗口打开,则关闭
|
||||
if (_launcherWindow != null && _launcherWindow.IsVisible)
|
||||
{
|
||||
_launcherWindow.Close();
|
||||
_launcherWindow = null;
|
||||
}
|
||||
|
||||
// 设置禁用状态
|
||||
base.Disable();
|
||||
|
||||
// 保存插件配置
|
||||
SavePluginSettings();
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台插件已禁用");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"禁用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override UserControl GetSettingsView()
|
||||
{
|
||||
return new LauncherSettingsControl(this);
|
||||
}
|
||||
|
||||
public override void Cleanup()
|
||||
{
|
||||
// 保存配置
|
||||
SaveConfig();
|
||||
|
||||
// 从浮动栏移除启动台按钮
|
||||
RemoveLauncherButtonFromFloatingBar();
|
||||
|
||||
// 如果启动台窗口打开,则关闭
|
||||
if (_launcherWindow != null && _launcherWindow.IsVisible)
|
||||
{
|
||||
_launcherWindow.Close();
|
||||
_launcherWindow = null;
|
||||
}
|
||||
|
||||
base.Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存插件设置
|
||||
/// </summary>
|
||||
public override void SavePluginSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保配置已加载
|
||||
if (Config == null)
|
||||
{
|
||||
LoadConfig();
|
||||
}
|
||||
|
||||
// 更新其他设置,但不更改插件启用状态
|
||||
|
||||
// 保存配置
|
||||
SaveConfig();
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台插件设置已保存");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存超级启动台插件设置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 配置管理
|
||||
|
||||
/// <summary>
|
||||
/// 加载配置
|
||||
/// </summary>
|
||||
private void LoadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_configPath))
|
||||
{
|
||||
string json = File.ReadAllText(_configPath);
|
||||
Config = JsonConvert.DeserializeObject<LauncherConfig>(json) ?? CreateDefaultConfig();
|
||||
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items ?? new List<LauncherItem>());
|
||||
|
||||
// 注意:不再根据配置更改插件启用状态
|
||||
// 插件状态由PluginManager统一管理
|
||||
}
|
||||
else
|
||||
{
|
||||
Config = CreateDefaultConfig();
|
||||
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items);
|
||||
SaveConfig();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
Config = CreateDefaultConfig();
|
||||
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存配置
|
||||
/// </summary>
|
||||
public void SaveConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 同步LauncherItems到Config
|
||||
Config.Items = new List<LauncherItem>(LauncherItems);
|
||||
|
||||
// 序列化并保存配置
|
||||
string json = JsonConvert.SerializeObject(Config, Formatting.Indented);
|
||||
File.WriteAllText(_configPath, json);
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台配置已保存");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建默认配置
|
||||
/// </summary>
|
||||
private LauncherConfig CreateDefaultConfig()
|
||||
{
|
||||
var config = new LauncherConfig
|
||||
{
|
||||
ButtonPosition = LauncherButtonPosition.Right,
|
||||
// 不再使用IsEnabled,插件状态由PluginManager管理
|
||||
Items = new List<LauncherItem>
|
||||
{
|
||||
new LauncherItem
|
||||
{
|
||||
Name = "资源管理器",
|
||||
Path = @"C:\Windows\explorer.exe",
|
||||
IsVisible = true,
|
||||
Position = 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 启动台按钮管理
|
||||
|
||||
/// <summary>
|
||||
/// 将启动台按钮添加到浮动栏
|
||||
/// </summary>
|
||||
private void AddLauncherButtonToFloatingBar()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果已经添加,先移除
|
||||
if (_isAddedToFloatingBar)
|
||||
{
|
||||
RemoveLauncherButtonFromFloatingBar();
|
||||
_isAddedToFloatingBar = false;
|
||||
}
|
||||
|
||||
// 获取主窗口实例
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到主窗口实例,无法添加启动台按钮", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建启动台按钮
|
||||
_launcherButton = new LauncherButton(this);
|
||||
var buttonElement = _launcherButton.Element;
|
||||
|
||||
// 查找浮动栏
|
||||
var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel;
|
||||
if (floatingBar == null)
|
||||
{
|
||||
// 如果直接查找失败,则尝试遍历可视树查找
|
||||
Panel floatingBarPanelFromTree = null;
|
||||
FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree);
|
||||
floatingBar = floatingBarPanelFromTree;
|
||||
}
|
||||
|
||||
if (floatingBar == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到浮动栏,无法添加启动台按钮", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加启动台按钮到浮动栏
|
||||
if (Config.ButtonPosition == LauncherButtonPosition.Left)
|
||||
{
|
||||
floatingBar.Children.Insert(0, buttonElement);
|
||||
LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏左侧");
|
||||
}
|
||||
else
|
||||
{
|
||||
floatingBar.Children.Add(buttonElement);
|
||||
LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏右侧");
|
||||
}
|
||||
|
||||
_isAddedToFloatingBar = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"添加启动台按钮到浮动栏时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归查找StackPanelFloatingBar
|
||||
/// </summary>
|
||||
private void FindStackPanelFloatingBar(DependencyObject parent, ref Panel result)
|
||||
{
|
||||
if (parent == null || result != null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 检查当前对象是否为我们要找的面板
|
||||
if (parent is Panel panel && panel.Name == "StackPanelFloatingBar")
|
||||
{
|
||||
result = panel;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取子元素数量
|
||||
int childCount = VisualTreeHelper.GetChildrenCount(parent);
|
||||
|
||||
// 遍历所有子元素
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
|
||||
FindStackPanelFloatingBar(child, ref result);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"查找StackPanelFloatingBar时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从浮动栏移除启动台按钮
|
||||
/// </summary>
|
||||
private void RemoveLauncherButtonFromFloatingBar()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_isAddedToFloatingBar || _launcherButton == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取主窗口实例
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到主窗口实例,无法移除启动台按钮", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取按钮元素
|
||||
var buttonElement = _launcherButton.Element;
|
||||
|
||||
// 查找浮动栏
|
||||
var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel;
|
||||
if (floatingBar == null)
|
||||
{
|
||||
// 如果直接查找失败,则尝试遍历可视树查找
|
||||
Panel floatingBarPanelFromTree = null;
|
||||
FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree);
|
||||
floatingBar = floatingBarPanelFromTree;
|
||||
}
|
||||
|
||||
if (floatingBar == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到浮动栏,无法移除启动台按钮", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 从浮动栏移除启动台按钮
|
||||
if (floatingBar.Children.Contains(buttonElement))
|
||||
{
|
||||
floatingBar.Children.Remove(buttonElement);
|
||||
LogHelper.WriteLogToFile("启动台按钮已从浮动栏移除");
|
||||
}
|
||||
|
||||
_isAddedToFloatingBar = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"移除启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新启动台按钮位置
|
||||
/// </summary>
|
||||
public void UpdateButtonPosition()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果按钮已添加到浮动栏,重新添加以更新位置
|
||||
if (_isAddedToFloatingBar)
|
||||
{
|
||||
RemoveLauncherButtonFromFloatingBar();
|
||||
AddLauncherButtonToFloatingBar();
|
||||
LogHelper.WriteLogToFile($"启动台按钮位置已更新为: {Config.ButtonPosition}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 启动台功能
|
||||
|
||||
/// <summary>
|
||||
/// 显示启动台窗口
|
||||
/// </summary>
|
||||
/// <param name="buttonPosition">按钮在屏幕上的位置</param>
|
||||
public void ShowLauncherWindow(Point buttonPosition)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果窗口已存在,关闭它
|
||||
if (_launcherWindow != null && _launcherWindow.IsVisible)
|
||||
{
|
||||
_launcherWindow.Close();
|
||||
_launcherWindow = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新的启动台窗口
|
||||
_launcherWindow = new LauncherWindow(this);
|
||||
|
||||
// 计算窗口位置,使其位于按钮上方
|
||||
PositionLauncherWindow(_launcherWindow, buttonPosition);
|
||||
|
||||
// 显示窗口
|
||||
_launcherWindow.Show();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"显示启动台窗口时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置启动台窗口位置
|
||||
/// </summary>
|
||||
/// <param name="window">启动台窗口</param>
|
||||
/// <param name="buttonPosition">按钮在屏幕上的位置</param>
|
||||
private void PositionLauncherWindow(LauncherWindow window, Point buttonPosition)
|
||||
{
|
||||
// 确保窗口已加载
|
||||
if (window.ActualWidth == 0 || window.ActualHeight == 0)
|
||||
{
|
||||
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
|
||||
// 设置窗口加载完成后的位置
|
||||
window.Loaded += (s, e) =>
|
||||
{
|
||||
// 窗口位于按钮上方居中
|
||||
double left = buttonPosition.X - (window.ActualWidth / 2);
|
||||
double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距
|
||||
|
||||
// 确保窗口在屏幕内
|
||||
left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth));
|
||||
top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight));
|
||||
|
||||
window.Left = left;
|
||||
window.Top = top;
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// 窗口位于按钮上方居中
|
||||
double left = buttonPosition.X - (window.ActualWidth / 2);
|
||||
double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距
|
||||
|
||||
// 确保窗口在屏幕内
|
||||
left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth));
|
||||
top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight));
|
||||
|
||||
window.Left = left;
|
||||
window.Top = top;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加应用到启动台
|
||||
/// </summary>
|
||||
/// <param name="item">启动台项</param>
|
||||
public void AddLauncherItem(LauncherItem item)
|
||||
{
|
||||
// 如果项目数量已达上限,则不添加
|
||||
if (LauncherItems.Count >= 40)
|
||||
{
|
||||
MessageBox.Show("启动台项目数量已达上限(40个)!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
// 寻找合适的位置
|
||||
if (item.Position < 0)
|
||||
{
|
||||
item.Position = FindNextAvailablePosition();
|
||||
}
|
||||
|
||||
// 添加项目并保存配置
|
||||
LauncherItems.Add(item);
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找下一个可用位置
|
||||
/// </summary>
|
||||
private int FindNextAvailablePosition()
|
||||
{
|
||||
// 获取已使用的位置列表
|
||||
var usedPositions = new HashSet<int>();
|
||||
foreach (var item in LauncherItems)
|
||||
{
|
||||
usedPositions.Add(item.Position);
|
||||
}
|
||||
|
||||
// 查找第一个可用位置
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
if (!usedPositions.Contains(i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有位置都已使用,则返回0
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// ICCPP 插件适配器,用于加载和管理 .iccpp 格式的插件
|
||||
/// </summary>
|
||||
public class ICCPPPluginAdapter : PluginBase
|
||||
{
|
||||
private readonly byte[] _pluginData;
|
||||
private readonly string _pluginPath;
|
||||
private readonly string _pluginName;
|
||||
private readonly Version _pluginVersion;
|
||||
private bool _isInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// 创建 ICCPP 插件适配器
|
||||
/// </summary>
|
||||
/// <param name="pluginPath">插件文件路径</param>
|
||||
/// <param name="pluginData">插件文件数据</param>
|
||||
public ICCPPPluginAdapter(string pluginPath, byte[] pluginData)
|
||||
{
|
||||
_pluginPath = pluginPath;
|
||||
_pluginData = pluginData;
|
||||
PluginPath = pluginPath;
|
||||
|
||||
// 从文件名获取插件名称
|
||||
_pluginName = Path.GetFileNameWithoutExtension(pluginPath);
|
||||
_pluginVersion = new Version(1, 0, 0); // 默认版本
|
||||
|
||||
// 尝试从插件数据中读取更多信息
|
||||
TryReadPluginMetadata();
|
||||
}
|
||||
|
||||
public ICCPPPluginAdapter()
|
||||
{
|
||||
_pluginPath = string.Empty;
|
||||
_pluginData = new byte[0];
|
||||
PluginPath = string.Empty;
|
||||
_pluginName = "ICCPPPlugin";
|
||||
_pluginVersion = new Version(1, 0, 0);
|
||||
// 可选:初始化其他字段
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从插件数据中读取元数据
|
||||
/// </summary>
|
||||
private void TryReadPluginMetadata()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 这里可以根据 .iccpp 文件的实际格式解析元数据
|
||||
// 例如,如果文件有特定的头部结构,可以在这里解析
|
||||
|
||||
// 示例:如果前100字节包含元数据
|
||||
if (_pluginData.Length > 100)
|
||||
{
|
||||
// 解析元数据的代码...
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"解析插件 {_pluginName} 元数据时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#region IPlugin 接口实现
|
||||
|
||||
/// <summary>
|
||||
/// 插件名称
|
||||
/// </summary>
|
||||
public override string Name => _pluginName;
|
||||
|
||||
/// <summary>
|
||||
/// 插件描述
|
||||
/// </summary>
|
||||
public override string Description => $"{_pluginName} (ICCPP 格式插件)";
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
public override Version Version => _pluginVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 插件作者
|
||||
/// </summary>
|
||||
public override string Author => "未知";
|
||||
|
||||
/// <summary>
|
||||
/// 是否为内置插件
|
||||
/// </summary>
|
||||
public override bool IsBuiltIn => false;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化插件
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 这里可以添加 .iccpp 插件的初始化逻辑
|
||||
// 例如,根据文件格式加载特定资源
|
||||
|
||||
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已初始化");
|
||||
_isInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// </summary>
|
||||
public override void Enable()
|
||||
{
|
||||
if (IsEnabled) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 这里可以添加 .iccpp 插件的启用逻辑
|
||||
// 例如,加载动态库、注册事件等
|
||||
|
||||
base.Enable(); // 设置启用状态并触发事件
|
||||
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已启用");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// </summary>
|
||||
public override void Disable()
|
||||
{
|
||||
if (!IsEnabled) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 这里可以添加 .iccpp 插件的禁用逻辑
|
||||
// 例如,卸载动态库、注销事件等
|
||||
|
||||
base.Disable(); // 设置禁用状态并触发事件
|
||||
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已禁用");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"禁用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理插件资源
|
||||
/// </summary>
|
||||
public override void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 这里可以添加 .iccpp 插件的清理逻辑
|
||||
// 例如,释放资源等
|
||||
|
||||
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已清理资源");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理 ICCPP 插件 {Name} 资源时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 定义插件的基本接口
|
||||
/// </summary>
|
||||
public interface IPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件名称
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件描述
|
||||
/// </summary>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件作者
|
||||
/// </summary>
|
||||
string Author { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为内置插件
|
||||
/// </summary>
|
||||
bool IsBuiltIn { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化插件
|
||||
/// 此方法在插件加载时被调用,用于执行一些初始化工作
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// 此方法在插件被用户或系统启用时调用,激活插件功能
|
||||
/// </summary>
|
||||
void Enable();
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// 此方法在插件被用户或系统禁用时调用,停用插件功能
|
||||
/// </summary>
|
||||
void Disable();
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件设置界面
|
||||
/// 此方法返回插件的设置界面控件,用于展示在设置窗口
|
||||
/// </summary>
|
||||
/// <returns>插件设置界面</returns>
|
||||
UserControl GetSettingsView();
|
||||
|
||||
/// <summary>
|
||||
/// 插件卸载时的清理工作
|
||||
/// 此方法在插件被卸载前调用,用于释放资源和执行清理
|
||||
/// </summary>
|
||||
void Cleanup();
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件基类,提供基本实现
|
||||
/// </summary>
|
||||
public abstract class PluginBase : IPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件状态(私有字段)
|
||||
/// </summary>
|
||||
private bool _isEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// 插件状态(公共属性)
|
||||
/// </summary>
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _isEnabled;
|
||||
protected set
|
||||
{
|
||||
if (_isEnabled != value)
|
||||
{
|
||||
_isEnabled = value;
|
||||
OnEnabledStateChanged(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件ID
|
||||
/// </summary>
|
||||
public string Id { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件路径
|
||||
/// </summary>
|
||||
public string PluginPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件名称
|
||||
/// </summary>
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件描述
|
||||
/// </summary>
|
||||
public abstract string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
public abstract Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件作者
|
||||
/// </summary>
|
||||
public abstract string Author { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为内置插件
|
||||
/// </summary>
|
||||
public virtual bool IsBuiltIn => false;
|
||||
|
||||
/// <summary>
|
||||
/// 状态变更事件
|
||||
/// </summary>
|
||||
public event EventHandler<bool> EnabledStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化插件
|
||||
/// </summary>
|
||||
public virtual void Initialize()
|
||||
{
|
||||
Id = GetType().FullName;
|
||||
|
||||
// 添加日志,记录插件名称
|
||||
try
|
||||
{
|
||||
string name = Name;
|
||||
LogHelper.WriteLogToFile($"初始化插件: ID={Id}, 名称={name ?? "未命名"}");
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"警告: 插件 {Id} 的名称为空", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取插件名称时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已初始化");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// </summary>
|
||||
public virtual void Enable()
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
IsEnabled = true;
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已启用");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// </summary>
|
||||
public virtual void Disable()
|
||||
{
|
||||
if (IsEnabled)
|
||||
{
|
||||
IsEnabled = false;
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已禁用");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件设置界面
|
||||
/// </summary>
|
||||
/// <returns>插件设置界面</returns>
|
||||
public virtual UserControl GetSettingsView()
|
||||
{
|
||||
// 默认返回空设置页面
|
||||
return new UserControl();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件卸载时的清理工作
|
||||
/// </summary>
|
||||
public virtual void Cleanup()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已卸载");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存插件自身的设置
|
||||
/// 注意:此方法仅用于保存插件的特定设置,不应影响插件启用/禁用状态
|
||||
/// 插件启用状态由PluginManager统一管理
|
||||
/// </summary>
|
||||
public virtual void SavePluginSettings()
|
||||
{
|
||||
// 默认实现不做任何事情
|
||||
// 子类可以重写此方法,将自身设置保存到配置文件中
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 设置已保存", LogHelper.LogType.Event);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发状态变更事件
|
||||
/// </summary>
|
||||
/// <param name="isEnabled">是否启用</param>
|
||||
protected virtual void OnEnabledStateChanged(bool isEnabled)
|
||||
{
|
||||
EnabledStateChanged?.Invoke(this, isEnabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,276 +0,0 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件模板,用于开发者参考
|
||||
/// 注意:实际开发时,请将此类移到单独的程序集中
|
||||
/// </summary>
|
||||
public class PluginTemplate : PluginBase
|
||||
{
|
||||
#region 插件基本信息
|
||||
|
||||
/// <summary>
|
||||
/// 插件名称
|
||||
/// </summary>
|
||||
public override string Name => "插件模板";
|
||||
|
||||
/// <summary>
|
||||
/// 插件描述
|
||||
/// </summary>
|
||||
public override string Description => "这是一个插件开发模板,用于开发者参考。";
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
public override Version Version => new Version(1, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 插件作者
|
||||
/// </summary>
|
||||
public override string Author => "Your Name";
|
||||
|
||||
/// <summary>
|
||||
/// 是否为内置插件(外部插件请返回false)
|
||||
/// </summary>
|
||||
public override bool IsBuiltIn => false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件生命周期
|
||||
|
||||
/// <summary>
|
||||
/// 插件初始化
|
||||
/// 在这里进行插件的初始化工作,如加载配置、注册事件等
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
// 先调用基类方法,这样会设置插件ID和记录日志
|
||||
base.Initialize();
|
||||
|
||||
// TODO: 在这里进行插件初始化工作
|
||||
|
||||
// 示例:记录初始化信息
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 开始初始化");
|
||||
|
||||
// 示例:加载配置
|
||||
LoadConfig();
|
||||
|
||||
// 示例:注册自定义事件
|
||||
// MainWindow.Instance.SomeEvent += OnSomeEvent;
|
||||
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 初始化完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// 在这里激活插件功能
|
||||
/// </summary>
|
||||
public override void Enable()
|
||||
{
|
||||
// 先调用基类方法,这样会设置插件状态和记录日志
|
||||
base.Enable();
|
||||
|
||||
// TODO: 在这里启用插件功能
|
||||
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已启用");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// 在这里停用插件功能
|
||||
/// </summary>
|
||||
public override void Disable()
|
||||
{
|
||||
// 先调用基类方法,这样会设置插件状态和记录日志
|
||||
base.Disable();
|
||||
|
||||
// TODO: 在这里禁用插件功能
|
||||
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已禁用");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理资源
|
||||
/// 在插件卸载时调用,清理资源
|
||||
/// </summary>
|
||||
public override void Cleanup()
|
||||
{
|
||||
// TODO: 在这里清理插件资源
|
||||
|
||||
// 示例:取消注册事件
|
||||
// MainWindow.Instance.SomeEvent -= OnSomeEvent;
|
||||
|
||||
// 示例:保存配置
|
||||
SaveConfig();
|
||||
|
||||
// 最后调用基类方法
|
||||
base.Cleanup();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件配置
|
||||
|
||||
/// <summary>
|
||||
/// 加载插件配置
|
||||
/// </summary>
|
||||
private void LoadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: 从文件或其他位置加载配置
|
||||
// 示例:
|
||||
// string configPath = Path.Combine(App.RootPath, "PluginConfigs", "YourPluginName.json");
|
||||
// if (File.Exists(configPath))
|
||||
// {
|
||||
// string json = File.ReadAllText(configPath);
|
||||
// YourConfig = Newtonsoft.Json.JsonConvert.DeserializeObject<YourConfigClass>(json);
|
||||
// }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存插件配置
|
||||
/// </summary>
|
||||
private void SaveConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: 保存配置到文件或其他位置
|
||||
// 示例:
|
||||
// string configDir = Path.Combine(App.RootPath, "PluginConfigs");
|
||||
// if (!Directory.Exists(configDir))
|
||||
// {
|
||||
// Directory.CreateDirectory(configDir);
|
||||
// }
|
||||
// string configPath = Path.Combine(configDir, "YourPluginName.json");
|
||||
// string json = Newtonsoft.Json.JsonConvert.SerializeObject(YourConfig, Newtonsoft.Json.Formatting.Indented);
|
||||
// File.WriteAllText(configPath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件设置界面
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件设置界面
|
||||
/// </summary>
|
||||
/// <returns>插件设置界面</returns>
|
||||
public override UserControl GetSettingsView()
|
||||
{
|
||||
// 创建插件设置界面
|
||||
return new PluginTemplateSettingsControl();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件功能方法
|
||||
|
||||
// TODO: 在这里添加插件的具体功能方法
|
||||
|
||||
/// <summary>
|
||||
/// 示例方法:执行一些功能
|
||||
/// </summary>
|
||||
public void DoSomething()
|
||||
{
|
||||
if (!IsEnabled) return;
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: 实现你的功能
|
||||
MessageBox.Show("插件功能执行示例", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"执行插件功能时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件设置控件
|
||||
/// </summary>
|
||||
public class PluginTemplateSettingsControl : UserControl
|
||||
{
|
||||
public PluginTemplateSettingsControl()
|
||||
{
|
||||
// 创建设置界面布局
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Margin = new Thickness(10)
|
||||
};
|
||||
|
||||
// 添加标题
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "插件模板设置",
|
||||
FontSize = 16,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Margin = new Thickness(0, 0, 0, 10)
|
||||
});
|
||||
|
||||
// 添加说明文字
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "这是一个示例设置界面,你可以在这里添加自己的设置控件。",
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 0, 0, 15)
|
||||
});
|
||||
|
||||
// 添加示例设置选项
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
Content = "启用某项功能",
|
||||
Margin = new Thickness(0, 0, 0, 10)
|
||||
};
|
||||
panel.Children.Add(checkBox);
|
||||
|
||||
// 添加文本输入框
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "设置项:",
|
||||
Margin = new Thickness(0, 5, 0, 5)
|
||||
});
|
||||
|
||||
panel.Children.Add(new TextBox
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
Width = 200,
|
||||
HorizontalAlignment = HorizontalAlignment.Left
|
||||
});
|
||||
|
||||
// 添加按钮
|
||||
var button = new Button
|
||||
{
|
||||
Content = "保存设置",
|
||||
Padding = new Thickness(10, 5, 10, 5),
|
||||
Margin = new Thickness(0, 10, 0, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Left
|
||||
};
|
||||
|
||||
button.Click += (sender, e) =>
|
||||
{
|
||||
MessageBox.Show("设置已保存!", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
};
|
||||
|
||||
panel.Children.Add(button);
|
||||
|
||||
// 设置控件内容
|
||||
Content = panel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
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` 的文件,以及应用根目录下的点名名单 `Names.txt`;
|
||||
/// 会跳过被 IsExcludedPath 判定为排除的路径。遇到任何 I/O 或访问错误时会静默忽略,不会抛出异常。</remarks>
|
||||
private static void LockFilesRecursive(string root)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rollCallNamesPath = NormalizePath(Path.Combine(root, "Names.txt"));
|
||||
foreach (var file in Directory.GetFiles(root, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (!IsExcludedPath(file))
|
||||
{
|
||||
var ext = Path.GetExtension(file);
|
||||
var normFile = NormalizePath(file);
|
||||
if (string.Equals(normFile, rollCallNamesPath, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".exe", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".config", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".manifest", StringComparison.OrdinalIgnoreCase) ||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user