Compare commits
774 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -16,16 +16,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 +37,10 @@
|
||||
"contributions": [
|
||||
"blog",
|
||||
"doc",
|
||||
"design"
|
||||
"design",
|
||||
"test",
|
||||
"tutorial",
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -98,6 +93,24 @@
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "doudou0720",
|
||||
"name": "doudou0720",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/98651603?v=4",
|
||||
"profile": "https://github.com/doudou0720",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ body:
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 期望结果 | Expected Behavior
|
||||
description: 你期望的正确行为或结果 | What did you expect to happen?
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
|
||||
@@ -20,7 +20,7 @@ body:
|
||||
label: 需求动机 | Motivation
|
||||
description: 为什么需要这个功能?| Why do you need this feature?
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: textarea
|
||||
id: design
|
||||
attributes:
|
||||
|
||||
@@ -1,35 +1,399 @@
|
||||
name: .NET Build
|
||||
name: .NET Build & PR Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main,beta ]
|
||||
branches: [ main, beta ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches: [ main, beta ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'docs/**'
|
||||
- 'Images/**'
|
||||
- 'Manual.md'
|
||||
- 'README.md'
|
||||
- 'UpdateLog.md'
|
||||
- 'CODE_OF_CONDUCT.md'
|
||||
- 'LICENSE'
|
||||
- 'privacy.txt'
|
||||
- 'icc.png'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.head_ref || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
find-or-create-pr-comment:
|
||||
name: Find or Create PR Comment
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
outputs:
|
||||
comment_id: ${{ steps.find-comment.outputs.comment_id }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Find existing bot comment
|
||||
id: find-comment
|
||||
run: |
|
||||
# 查找包含特定标记的现有评论
|
||||
COMMENTS=$(gh api \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
|
||||
--jq '.[] | select(.body | contains("<!-- github-action-pr-build -->")) | .id' | head -1)
|
||||
|
||||
if [ -n "$COMMENTS" ]; then
|
||||
echo "📝 找到现有评论 ID: $COMMENTS"
|
||||
echo "comment_id=$COMMENTS" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "📝 未找到现有评论,将创建新评论"
|
||||
echo "comment_id=" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Setup MSbuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Setup NuGet
|
||||
uses: NuGet/setup-nuget@v2.0.1
|
||||
pr-preview-comment:
|
||||
name: PR Preview (Building)
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
needs: find-or-create-pr-comment
|
||||
permissions:
|
||||
pull-requests: write
|
||||
outputs:
|
||||
comment_id: ${{ steps.create-preview-comment.outputs.comment-id || '' }}
|
||||
steps:
|
||||
- name: Prepare Preview Comment
|
||||
id: prepare-preview
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GH_RUN_ID: ${{ github.run_id }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
IS_READY_FOR_REVIEW: ${{ github.event.action == 'ready_for_review' }}
|
||||
run: |
|
||||
# 构建预览评论内容
|
||||
{
|
||||
if [ "$IS_READY_FOR_REVIEW" = "true" ]; then
|
||||
echo "# 🚀 PR 准备审查 - 构建预览"
|
||||
echo ""
|
||||
echo "**状态:** 🟡 正在构建(准备审查状态)..."
|
||||
else
|
||||
echo "# 🏗️ PR 构建预览"
|
||||
echo ""
|
||||
echo "**状态:** 🟡 正在构建..."
|
||||
fi
|
||||
|
||||
echo "**分支提交:** \`$PR_HEAD_SHA\`"
|
||||
echo "**操作:** [查看运行详情](https://github.com/$GH_REPO/actions/runs/$GH_RUN_ID)"
|
||||
echo ""
|
||||
|
||||
if [ "$IS_READY_FOR_REVIEW" = "true" ]; then
|
||||
echo "> 📋 此 PR 已标记为 **准备审查**,正在进行构建验证"
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
echo "<!-- github-action-pr-build -->"
|
||||
echo "<!-- build-id: $GH_RUN_ID -->"
|
||||
echo "<!-- event-type: ${{ github.event.action }} -->"
|
||||
echo "<!-- pr-head-sha: $PR_HEAD_SHA -->"
|
||||
echo "<!-- merge-sha: ${{ github.sha }} -->"
|
||||
echo ""
|
||||
echo "🤖 构建完成后,状态将自动更新"
|
||||
} > preview_comment.txt
|
||||
|
||||
preview_content=$(cat preview_comment.txt)
|
||||
echo "preview_body<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$preview_content" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore NuGet Packages
|
||||
run: nuget restore "Ink Canvas.sln"
|
||||
- name: Post/Update Preview Comment
|
||||
id: create-preview-comment
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
continue-on-error: true # 即使评论失败也继续执行
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ needs.find-or-create-pr-comment.outputs.comment_id }}
|
||||
body: ${{ steps.prepare-preview.outputs.preview_body }}
|
||||
edit-mode: replace
|
||||
|
||||
- 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"
|
||||
build-and-package:
|
||||
name: Build & Package
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
archive_name: ${{ steps.create-archive.outputs.archive_name }}
|
||||
build_result: ${{ steps.check-exe.outputs.build_success }}
|
||||
artifact_url: ${{ steps.upload-artifact.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Upload to artifact
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: InkCanvasForClass
|
||||
path: "Ink Canvas/bin/Any CPU/Release/net472/"
|
||||
- name: Setup NuGet
|
||||
uses: NuGet/setup-nuget@v2.0.1
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Cache NuGet global packages
|
||||
id: cache-nuget
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.nuget/packages
|
||||
~\AppData\Local\NuGet\Cache
|
||||
key: ${{ runner.os }}-nuget-v2-${{ hashFiles('**/*.csproj', '**/packages.config', '**/*.sln', '**/nuget.config') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-v2-
|
||||
|
||||
|
||||
|
||||
# Removed caching of obj/ folders to avoid cross-version incremental build issues
|
||||
# NuGet packages will still be cached; always run restore to ensure packages are present
|
||||
- name: Restore NuGet packages
|
||||
run: |
|
||||
Write-Host "📥 正在恢复 NuGet 包(始终运行以确保依赖可用)..." -ForegroundColor Yellow
|
||||
|
||||
# 恢复解决方案级别的包
|
||||
nuget restore "Ink Canvas.sln" -Verbosity minimal
|
||||
|
||||
# 恢复项目级别的包(兼容 packages.config)
|
||||
msbuild -t:restore "Ink Canvas/InkCanvasForClass.csproj" /p:GitFlow="Github Action" /p:RestorePackagesConfig=true /verbosity:minimal
|
||||
|
||||
Write-Host "✅ NuGet 包恢复完成" -ForegroundColor Green
|
||||
|
||||
- name: Build the Solution
|
||||
run: |
|
||||
Write-Host "🔨 正在构建项目..." -ForegroundColor Cyan
|
||||
|
||||
# 如果是 ready_for_review 事件,添加特殊标记
|
||||
if ("${{ github.event.action }}" -eq "ready_for_review") {
|
||||
Write-Host "🚀 PR 准备审查状态构建 - 进行完整验证" -ForegroundColor Magenta
|
||||
$GITFLOW = "Github Action - Ready For Review"
|
||||
} else {
|
||||
$GITFLOW = "Github Action"
|
||||
}
|
||||
|
||||
# 执行构建
|
||||
msbuild /p:platform="AnyCPU" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal
|
||||
|
||||
Write-Host "🏗️ 构建命令执行完成" -ForegroundColor Cyan
|
||||
|
||||
- name: Check if exe file is generated
|
||||
id: check-exe
|
||||
run: |
|
||||
Write-Host "🔍 检查是否生成可执行文件..." -ForegroundColor Cyan
|
||||
|
||||
$exePath = "Ink Canvas\bin\Debug\net472\InkCanvasForClass.exe"
|
||||
|
||||
if (Test-Path $exePath) {
|
||||
Write-Host "✅ 找到可执行文件: $exePath" -ForegroundColor Green
|
||||
$fileInfo = Get-Item $exePath
|
||||
Write-Host " 文件大小: $($fileInfo.Length) 字节" -ForegroundColor Gray
|
||||
Write-Host " 创建时间: $($fileInfo.CreationTime)" -ForegroundColor Gray
|
||||
echo "build_success=true" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
Write-Host "❌ 未找到可执行文件: $exePath" -ForegroundColor Red
|
||||
Write-Host " 检查目录内容:" -ForegroundColor Yellow
|
||||
if (Test-Path "Ink Canvas\bin\Debug\net472\") {
|
||||
Get-ChildItem "Ink Canvas\bin\Debug\net472\" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-Host " - $($_.Name)" -ForegroundColor Gray
|
||||
}
|
||||
} else {
|
||||
Write-Host " bin\Debug\net472 目录不存在" -ForegroundColor Red
|
||||
}
|
||||
echo "build_success=false" >> $env:GITHUB_OUTPUT
|
||||
|
||||
# 如果是直接触发,则抛出错误
|
||||
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
|
||||
Write-Host "🚨 工作流手动触发 - 构建失败,抛出错误!" -ForegroundColor Red -BackgroundColor Black
|
||||
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"
|
||||
$archiveName = "InkCanvasForClass.CE.$version.zip"
|
||||
Write-Host "📦 正在创建归档包: $archiveName" -ForegroundColor Cyan
|
||||
Write-Host " 使用合并提交哈希: $env:GITHUB_SHA" -ForegroundColor Gray
|
||||
Compress-Archive -Path "Ink Canvas\bin\Debug\net472\*" -DestinationPath $archiveName -Force
|
||||
echo "archive_name=$archiveName" >> $env:GITHUB_OUTPUT
|
||||
Write-Host "✅ 已创建归档包: $archiveName" -ForegroundColor Green
|
||||
|
||||
- name: Upload Artifact (if build succeeded)
|
||||
id: upload-artifact
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: app-package
|
||||
path: "*.zip"
|
||||
|
||||
pr-check-comment:
|
||||
name: PR Check & Comment (Final)
|
||||
needs: [build-and-package, pr-preview-comment]
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Prepare Final Comment Content
|
||||
id: prepare-final-comment
|
||||
run: |
|
||||
# 从构建作业获取构建结果
|
||||
BUILD_SUCCESS="${{ needs.build-and-package.outputs.build_result }}"
|
||||
ARTIFACT_URL="${{ needs.build-and-package.outputs.artifact_url }}"
|
||||
|
||||
# 使用 PR 分支的实际提交哈希
|
||||
PR_HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
||||
|
||||
# 确定构建状态
|
||||
if [ "$BUILD_SUCCESS" = "true" ]; then
|
||||
STATUS_ICON="✅"
|
||||
STATUS_TEXT="构建成功"
|
||||
COLOR="#00d26a"
|
||||
else
|
||||
STATUS_ICON="❌"
|
||||
STATUS_TEXT="构建失败"
|
||||
COLOR="#f85149"
|
||||
fi
|
||||
|
||||
# 检查是否是 ready_for_review 事件
|
||||
READY_FOR_REVIEW="${{ github.event.action == 'ready_for_review' }}"
|
||||
|
||||
# 构建最终评论内容
|
||||
{
|
||||
if [ "$READY_FOR_REVIEW" = "true" ]; then
|
||||
echo "# 🚀 PR 准备审查 - 构建结果"
|
||||
echo ""
|
||||
echo "> 📋 此 PR 已标记为 **准备审查**,构建验证完成"
|
||||
echo ""
|
||||
else
|
||||
echo "# 📋 构建结果摘要"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "## 本次构建状态"
|
||||
echo ""
|
||||
echo "| 项目 | 结果 |"
|
||||
echo "|------|------|"
|
||||
echo "| 状态 | $STATUS_ICON **$STATUS_TEXT** |"
|
||||
echo "| 事件类型 | \`${{ github.event.action }}\` |"
|
||||
echo "| 分支提交 | \`$PR_HEAD_SHA\` |"
|
||||
echo "| 工作流 | [运行 #${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) |"
|
||||
|
||||
# 如果有构建产物,显示下载链接
|
||||
if [ "$BUILD_SUCCESS" = "true" ]; then
|
||||
NIGHTLY_LINK="https://hk.gh-proxy.com/https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/app-package.zip"
|
||||
echo ""
|
||||
echo "## 构建产物"
|
||||
if [ -n "$ARTIFACT_URL" ]; then
|
||||
echo "- 📦 [下载构建产物]($ARTIFACT_URL)"
|
||||
else
|
||||
echo "- 📦 [下载构建产物](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||
fi
|
||||
echo "- 🌙 [直链下载 (nightly.link)]($NIGHTLY_LINK)"
|
||||
|
||||
if [ "$READY_FOR_REVIEW" = "true" ]; then
|
||||
echo ""
|
||||
echo "## 🎉 审查建议"
|
||||
echo "- ✅ 构建验证通过,代码可以正常编译"
|
||||
echo "- 🔍 请进行代码审查"
|
||||
echo "- 🧪 建议测试构建产物功能"
|
||||
fi
|
||||
else
|
||||
if [ "$READY_FOR_REVIEW" = "true" ]; then
|
||||
echo ""
|
||||
echo "## ⚠️ 审查阻塞"
|
||||
echo "- ❌ 构建失败,需要修复后才能继续审查"
|
||||
echo "- 🔧 请检查构建错误并修复"
|
||||
echo "- 📝 修复后重新标记为准备审查"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "---"
|
||||
echo "<!-- github-action-pr-build -->"
|
||||
echo "<!-- build-id: ${{ github.run_id }} -->"
|
||||
echo "<!-- event-type: ${{ github.event.action }} -->"
|
||||
echo "<!-- pr-head-sha: $PR_HEAD_SHA -->"
|
||||
echo "<!-- merge-sha: ${{ github.sha }} -->"
|
||||
echo ""
|
||||
echo "<sub>🤖 GitHub Actions 自动生成 • 最后更新: $(date -u +'%Y-%m-%d %H:%M:%S UTC')</sub>"
|
||||
} > final_comment.txt
|
||||
|
||||
# 输出多行内容
|
||||
final_content=$(cat final_comment.txt)
|
||||
echo "final_body<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$final_content" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "最终评论内容已生成"
|
||||
|
||||
- name: Write Final Comment to GitHub Summary
|
||||
run: |
|
||||
echo "# 🚀 最终构建结果 (GitHub Actions Summary)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "> ⚠️ 注意:由于权限限制,评论可能无法发布到 PR。这里是在 GitHub Actions 中的构建结果:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
cat final_comment.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**工作流信息:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 事件类型: ${{ github.event_name }} (${{ github.event.action }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 运行编号: #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 运行 ID: ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Update Final Comment
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
continue-on-error: true # 即使评论失败也继续执行
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ needs.pr-preview-comment.outputs.comment_id }}
|
||||
body: ${{ steps.prepare-final-comment.outputs.final_body }}
|
||||
edit-mode: replace
|
||||
|
||||
final-check:
|
||||
name: Final Check (Manual Trigger)
|
||||
if: always() && github.event_name == 'workflow_dispatch'
|
||||
needs: [build-and-package]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Build Result
|
||||
id: check-build
|
||||
run: |
|
||||
BUILD_SUCCESS="${{ needs.build-and-package.outputs.build_result }}"
|
||||
|
||||
if [ "$BUILD_SUCCESS" = "true" ]; then
|
||||
echo "✅ 构建成功 - 工作流完成"
|
||||
echo "status=success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ 构建失败 - 抛出错误"
|
||||
echo "status=failure" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create Summary (if successful)
|
||||
if: steps.check-build.outputs.status == 'success'
|
||||
run: |
|
||||
echo "# 🎉 手动构建完成" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**构建状态:** ✅ 成功" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**提交:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**运行编号:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[📦 下载构建产物](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**直链下载 (nightly.link):**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[🌙 nightly.link 下载链接](https://hk.gh-proxy.com/https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/app-package.zip)" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,508 @@
|
||||
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
|
||||
|
||||
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
|
||||
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: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup NuGet
|
||||
uses: NuGet/setup-nuget@v2.0.1
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Install Inno Setup Unofficial Language Files
|
||||
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
|
||||
|
||||
Write-Host "✅ Inno Setup unofficial language files installed successfully" -ForegroundColor Green
|
||||
|
||||
- name: Cache NuGet packages and obj files
|
||||
id: cache-nuget
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
# NuGet 全局包缓存(已下载的包)
|
||||
~/.nuget/packages
|
||||
# NuGet 缓存目录(包索引)
|
||||
~\AppData\Local\NuGet\Cache
|
||||
# 项目 obj 目录(包含 project.assets.json)
|
||||
Ink Canvas/obj/
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/packages.config', '**/*.sln') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: Restore NuGet packages (if cache missed)
|
||||
if: steps.cache-nuget.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
Write-Host "📥 缓存未命中,正在恢复 NuGet 包..." -ForegroundColor Yellow
|
||||
|
||||
# 恢复解决方案级别的包
|
||||
nuget restore "Ink Canvas.sln" -Verbosity minimal
|
||||
|
||||
# 恢复项目级别的包
|
||||
msbuild -t:restore "Ink Canvas/InkCanvasForClass.csproj" /p:GitFlow="Github Action" /p:RestorePackagesConfig=true /verbosity:minimal
|
||||
|
||||
Write-Host "✅ NuGet 包恢复完成" -ForegroundColor Green
|
||||
|
||||
- name: Display version info
|
||||
run: |
|
||||
echo "Building version: ${{ needs.prepare.outputs.version }}"
|
||||
echo "Tag: ${{ needs.prepare.outputs.tag_name }}"
|
||||
echo "Release type: ${{ needs.prepare.outputs.is_prerelease == 'true' && 'Pre-release' || 'Release' }}"
|
||||
|
||||
- name: Build the Solution
|
||||
run: |
|
||||
Write-Host "🔨 正在构建项目..." -ForegroundColor Cyan
|
||||
msbuild /p:platform="AnyCPU" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal
|
||||
|
||||
Write-Host "🏗️ 构建命令执行完成" -ForegroundColor Cyan
|
||||
|
||||
- name: Create Release Archive
|
||||
id: create_archive
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$archiveName = "InkCanvasForClass.CE.$version.zip"
|
||||
|
||||
# 创建发布目录
|
||||
New-Item -ItemType Directory -Path "release" -Force
|
||||
|
||||
# 复制发布文件
|
||||
Copy-Item "Ink Canvas\bin\Release\net472\*" "release\" -Recurse -Force
|
||||
|
||||
# 创建压缩包
|
||||
Compress-Archive -Path "release\*" -DestinationPath $archiveName -Force
|
||||
|
||||
echo "archive_name=$archiveName" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare Inno Setup script
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
|
||||
# 更新 ISS 文件中的版本信息
|
||||
$issPath = "build\InkCanvasForClass CE.iss"
|
||||
$issContent = Get-Content -Path $issPath -Raw
|
||||
|
||||
# 替换版本信息
|
||||
$issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`""
|
||||
|
||||
# 替换源文件路径为相对路径(考虑到ISS文件在build目录下,需要返回上级目录)
|
||||
$issContent = $issContent -replace 'Source: ".*\\{#MyAppExeName}";', 'Source: "..\release\{#MyAppExeName}";'
|
||||
$issContent = $issContent -replace 'Source: ".*\\InkCanvasForClass.exe.config";', 'Source: "..\release\InkCanvasForClass.exe.config";'
|
||||
|
||||
# 更新输出目录为当前目录
|
||||
$issContent = $issContent -replace 'OutputDir=.*', 'OutputDir=.'
|
||||
|
||||
# 更新默认安装目录
|
||||
$issContent = $issContent -replace 'DefaultDirName=.*', 'DefaultDirName={autopf}\{#MyAppName}'
|
||||
|
||||
# 更新许可证文件路径为相对路径(考虑到ISS文件在build目录下,需要返回上级目录)
|
||||
$issContent = $issContent -replace 'LicenseFile=.*', 'LicenseFile=..\LICENSE'
|
||||
|
||||
# 保存修改后的 ISS 文件
|
||||
$issContent | Set-Content -Path $issPath -Encoding UTF8
|
||||
|
||||
# 显示修改后的 ISS 文件内容
|
||||
Write-Host "Modified ISS file content:"
|
||||
Write-Host $issContent
|
||||
|
||||
- name: Build MSI installer with Inno Setup
|
||||
uses: Minionguyjpro/Inno-Setup-Action@v1.2.2
|
||||
with:
|
||||
path: build\InkCanvasForClass CE.iss
|
||||
options: /O.
|
||||
|
||||
- name: Rename installer file
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$setupFile = "InkCanvasForClass CE Setup.exe"
|
||||
$newSetupName = "InkCanvasForClass.CE.$version.Setup.exe"
|
||||
|
||||
if (Test-Path $setupFile) {
|
||||
Rename-Item -Path $setupFile -NewName $newSetupName
|
||||
Write-Host "Renamed setup file to: $newSetupName"
|
||||
} else {
|
||||
Write-Host "Setup file not found: $setupFile"
|
||||
}
|
||||
|
||||
- name: Calculate archive size
|
||||
id: calculate_size
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$archiveName = "InkCanvasForClass.CE.$version.zip"
|
||||
|
||||
# 获取文件大小(字节)
|
||||
$fileSize = (Get-Item $archiveName).Length
|
||||
|
||||
echo "zip_size=$fileSize" >> $env:GITHUB_OUTPUT
|
||||
|
||||
echo "Archive size: $fileSize bytes"
|
||||
|
||||
- name: Calculate installer size
|
||||
id: calculate_installer_size
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$installerName = "InkCanvasForClass.CE.$version.Setup.exe"
|
||||
|
||||
if (Test-Path $installerName) {
|
||||
# 获取文件大小(字节)
|
||||
$fileSize = (Get-Item $installerName).Length
|
||||
|
||||
echo "installer_size=$fileSize" >> $env:GITHUB_OUTPUT
|
||||
|
||||
echo "Installer size: $fileSize bytes"
|
||||
} else {
|
||||
echo "Installer file not found: $installerName"
|
||||
}
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
path: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
|
||||
sign:
|
||||
needs: [prepare, build]
|
||||
if: success()
|
||||
runs-on: ubuntu-latest # 改为 Ubuntu 以使用 Python 签名工具
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write # 需要这个权限来验证签名
|
||||
steps:
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Sign release artifacts with sigstore-python
|
||||
uses: sigstore/gh-action-sigstore-python@v3.2.0
|
||||
with:
|
||||
inputs: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
release-signing-artifacts: true
|
||||
upload-signing-artifacts: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Signed Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signed-files-${{ needs.prepare.outputs.version }}
|
||||
path: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe.sigstore.json
|
||||
|
||||
release:
|
||||
needs: [prepare, build, sign]
|
||||
if: success()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
|
||||
- name: Download Signed Artifacts (if exists)
|
||||
uses: actions/download-artifact@v4
|
||||
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 }}"
|
||||
|
||||
# 构建文件信息表格
|
||||
fileTable=$'\n## 文件信息 (File Information)\n'
|
||||
fileTable+=$'| 文件名 | 大小 |\n'
|
||||
fileTable+=$'|--------|------|\n'
|
||||
|
||||
# ZIP 文件信息
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"
|
||||
fileTable+=$'.zip | ${{ needs.build.outputs.zip_size }} bytes |\n'
|
||||
|
||||
# 安装包文件信息
|
||||
installerSize="${{ needs.build.outputs.installer_size }}"
|
||||
if [ -n "$installerSize" ]; then
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe | '"$installerSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
# 检查是否有签名文件
|
||||
if [ -f "InkCanvasForClass.CE.$version.zip.sigstore.json" ]; then
|
||||
sigstoreSize=$(stat -c%s "InkCanvasForClass.CE.$version.zip.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
# 检查安装程序签名文件
|
||||
if [ -f "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json" ]; then
|
||||
sigstoreSize=$(stat -c%s "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
fileTable+=$'\n*文件大小信息由GitHub Actions自动生成*\n'
|
||||
|
||||
# 将表格附加到原始changelog
|
||||
enhancedChangelog="${originalChangelog}${fileTable}"
|
||||
|
||||
# 输出增强版changelog内容
|
||||
echo "enhanced_changelog<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$enhancedChangelog" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Enhanced changelog created with file information table"
|
||||
|
||||
- name: Display Release Info
|
||||
run: |
|
||||
echo "=== Creating Release ==="
|
||||
echo "Version: ${{ needs.prepare.outputs.version }}"
|
||||
echo "Tag: ${{ needs.prepare.outputs.tag_name }}"
|
||||
echo "Pre-release: ${{ needs.prepare.outputs.is_prerelease }}"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.prepare.outputs.tag_name }}
|
||||
name: ICC CE ${{ needs.prepare.outputs.version }}
|
||||
body: |
|
||||
${{ steps.enhanced_changelog.outputs.enhanced_changelog }}
|
||||
draft: false
|
||||
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
|
||||
files: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe.sigstore.json
|
||||
fail_on_unmatched_files: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,4 +1,428 @@
|
||||
obj/
|
||||
bin/
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.env
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
|
||||
[Dd]ebug/x64/
|
||||
[Dd]ebugPublic/x64/
|
||||
[Rr]elease/x64/
|
||||
[Rr]eleases/x64/
|
||||
bin/x64/
|
||||
obj/x64/
|
||||
|
||||
[Dd]ebug/x86/
|
||||
[Dd]ebugPublic/x86/
|
||||
[Rr]elease/x86/
|
||||
[Rr]eleases/x86/
|
||||
bin/x86/
|
||||
obj/x86/
|
||||
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
[Aa][Rr][Mm]64[Ee][Cc]/
|
||||
bld/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Build results on 'Bin' directories
|
||||
**/[Bb]in/*
|
||||
# Uncomment if you have tasks that rely on *.refresh files to move binaries
|
||||
# (https://github.com/github/gitignore/pull/3736)
|
||||
#!**/[Bb]in/*.refresh
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
/Ink Canvas/obj
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
*.trx
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Approval Tests result files
|
||||
*.received.*
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.idb
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
**/.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
**/.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
**/.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
#tools/**
|
||||
#!tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
MSBuild_Logs/
|
||||
|
||||
# AWS SAM Build and Temporary Artifacts folder
|
||||
.aws-sam
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
**/.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
**/.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
**/.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
@@ -1 +1 @@
|
||||
1.7.9.0
|
||||
1.7.18.0
|
||||
|
||||
@@ -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>
|
||||
@@ -10,7 +10,7 @@
|
||||
<Style TargetType="ui:ScrollViewerEx">
|
||||
<EventSetter Event="PreviewMouseWheel" Handler="ScrollViewer_PreviewMouseWheel"/>
|
||||
</Style>
|
||||
<ContextMenu Opened="SysTrayMenu_Opened" x:Shared="false" x:Key="SysTrayMenu" Padding="6" ui:ThemeManager.RequestedTheme="Light">
|
||||
<ContextMenu Opened="SysTrayMenu_Opened" Closed="SysTrayMenu_Closed" x:Shared="false" x:Key="SysTrayMenu" Padding="6" ui:ThemeManager.RequestedTheme="Light">
|
||||
<MenuItem IsCheckable="True" IsChecked="False" Checked="HideICCMainWindowTrayIconMenuItem_Checked" Unchecked="HideICCMainWindowTrayIconMenuItem_UnChecked" Name="HideICCMainWindowTrayIconMenuItem">
|
||||
<MenuItem.Header>
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
@@ -32,7 +32,7 @@
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator Margin="0,3" />
|
||||
<MenuItem>
|
||||
<MenuItem Name="DisableAllHotkeysMenuItem" Click="DisableAllHotkeysMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="禁用所有快捷键" />
|
||||
@@ -232,12 +232,11 @@
|
||||
ContextMenu="{StaticResource SysTrayMenu}"
|
||||
IconSource="/Resources/icc.ico"/>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemeResources RequestedTheme="Light"/>
|
||||
<ui:ThemeResources/>
|
||||
<ui:XamlControlsResources />
|
||||
<ResourceDictionary Source="Resources/SeewoImageDictionary.xaml"/>
|
||||
<ResourceDictionary Source="Resources/DrawShapeImageDictionary.xaml"/>
|
||||
<ResourceDictionary Source="Resources/IconImageDictionary.xaml"/>
|
||||
<ResourceDictionary Source="Resources/Styles/Light.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
@@ -10,7 +10,7 @@ using System.Windows;
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("CJK_mkp")]
|
||||
[assembly: AssemblyProduct("InkCanvasForClass")]
|
||||
[assembly: AssemblyCopyright("Copyright © HARKOTEK Studio 2024")]
|
||||
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025-2026")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
@@ -49,5 +49,5 @@ using System.Windows;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.7.9.0")]
|
||||
[assembly: AssemblyFileVersion("1.7.9.0")]
|
||||
[assembly: AssemblyVersion("1.7.18.4")]
|
||||
[assembly: AssemblyFileVersion("1.7.18.4")]
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<UserControl x:Class="Ink_Canvas.Controls.QuickDrawFloatingButtonControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Width="65" Height="45">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<!-- 悬浮按钮资源 -->
|
||||
<SolidColorBrush x:Key="QuickDrawFloatingButtonBackground" Color="#80000000"/>
|
||||
<SolidColorBrush x:Key="QuickDrawFloatingButtonBorderBrush" Color="#40000000"/>
|
||||
<SolidColorBrush x:Key="QuickDrawFloatingButtonIconForeground" Color="White"/>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Background="{DynamicResource QuickDrawFloatingButtonBackground}"
|
||||
CornerRadius="8"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource QuickDrawFloatingButtonBorderBrush}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" Direction="315" ShadowDepth="3" Opacity="0.3" BlurRadius="5"/>
|
||||
</Border.Effect>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="22"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 拖动区域 -->
|
||||
<Border Grid.Column="0"
|
||||
MouseLeftButtonDown="DragArea_MouseLeftButtonDown"
|
||||
MouseMove="DragArea_MouseMove"
|
||||
MouseLeftButtonUp="DragArea_MouseLeftButtonUp"
|
||||
Cursor="SizeAll"
|
||||
Background="Transparent">
|
||||
<Grid VerticalAlignment="Center" Height="14" IsHitTestVisible="False">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="4"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 三个白色横线 -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
|
||||
HorizontalAlignment="Center"
|
||||
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
|
||||
<Border Grid.Row="2" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
|
||||
HorizontalAlignment="Center"
|
||||
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
|
||||
<Border Grid.Row="4" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
|
||||
HorizontalAlignment="Center"
|
||||
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 半透明分割线 -->
|
||||
<Rectangle Grid.Column="1" Width="1" Fill="#20FFFFFF" Margin="0,8,0,8"/>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<Border Grid.Column="2"
|
||||
MouseLeftButtonDown="FloatingButton_Click"
|
||||
Cursor="Hand"
|
||||
Background="Transparent">
|
||||
<Grid IsHitTestVisible="False">
|
||||
<Path Data="M5 7C5 8.06087 5.42143 9.07828 6.17157 9.82843C6.92172 10.5786 7.93913 11 9 11C10.0609 11 11.0783 10.5786 11.8284 9.82843C12.5786 9.07828 13 8.06087 13 7C13 5.93913 12.5786 4.92172 11.8284 4.17157C11.0783 3.42143 10.0609 3 9 3C7.93913 3 6.92172 3.42143 6.17157 4.17157C5.42143 4.92172 5 5.93913 5 7Z M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21 M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88 M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15"
|
||||
Stroke="{DynamicResource QuickDrawFloatingButtonIconForeground}"
|
||||
StrokeThickness="2"
|
||||
StrokeLineJoin="Round"
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using HorizontalAlignment = System.Windows.HorizontalAlignment;
|
||||
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
|
||||
using VerticalAlignment = System.Windows.VerticalAlignment;
|
||||
|
||||
namespace Ink_Canvas.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// 快抽悬浮按钮控件
|
||||
/// </summary>
|
||||
public partial class QuickDrawFloatingButtonControl : UserControl
|
||||
{
|
||||
private bool _isDragging = false;
|
||||
private Point _dragStartPoint;
|
||||
private Point _controlStartPoint;
|
||||
|
||||
public QuickDrawFloatingButtonControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快抽按钮点击事件
|
||||
/// </summary>
|
||||
private void FloatingButton_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果正在拖动,不触发点击事件
|
||||
if (_isDragging) return;
|
||||
|
||||
// 打开快抽窗口
|
||||
var quickDrawWindow = new QuickDrawWindow();
|
||||
quickDrawWindow.ShowDialog();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Helpers.LogHelper.WriteLogToFile($"打开快抽窗口失败: {ex.Message}", Helpers.LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拖动区域鼠标按下事件
|
||||
/// </summary>
|
||||
private void DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_isDragging = false;
|
||||
|
||||
// 记录鼠标在屏幕上的初始位置
|
||||
_dragStartPoint = this.PointToScreen(e.GetPosition(this));
|
||||
|
||||
// 记录控件的初始位置
|
||||
var parent = this.Parent as FrameworkElement;
|
||||
if (parent != null)
|
||||
{
|
||||
var transform = this.TransformToVisual(parent);
|
||||
var currentPos = transform.Transform(new Point(0, 0));
|
||||
_controlStartPoint = currentPos;
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentMargin = this.Margin;
|
||||
_controlStartPoint = new Point(
|
||||
double.IsNaN(currentMargin.Left) ? 0 : currentMargin.Left,
|
||||
double.IsNaN(currentMargin.Top) ? 0 : currentMargin.Top);
|
||||
}
|
||||
|
||||
((UIElement)sender).CaptureMouse();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拖动区域鼠标移动事件
|
||||
/// </summary>
|
||||
private void DragArea_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed && ((UIElement)sender).IsMouseCaptured)
|
||||
{
|
||||
// 获取鼠标在屏幕上的当前位置
|
||||
Point currentScreenPoint = this.PointToScreen(e.GetPosition(this));
|
||||
Vector diff = currentScreenPoint - _dragStartPoint;
|
||||
|
||||
if (!_isDragging && (Math.Abs(diff.X) > 3 || Math.Abs(diff.Y) > 3))
|
||||
{
|
||||
_isDragging = true;
|
||||
// 切换到绝对定位模式
|
||||
this.HorizontalAlignment = HorizontalAlignment.Left;
|
||||
this.VerticalAlignment = VerticalAlignment.Top;
|
||||
}
|
||||
|
||||
if (_isDragging)
|
||||
{
|
||||
// 计算新位置
|
||||
var parent = this.Parent as FrameworkElement;
|
||||
if (parent != null)
|
||||
{
|
||||
// 计算屏幕坐标相对于父容器的位置
|
||||
var parentPoint = parent.PointFromScreen(currentScreenPoint);
|
||||
var startParentPoint = parent.PointFromScreen(_dragStartPoint);
|
||||
|
||||
// 计算相对于初始位置的偏移
|
||||
double offsetX = parentPoint.X - startParentPoint.X;
|
||||
double offsetY = parentPoint.Y - startParentPoint.Y;
|
||||
|
||||
// 新位置 = 初始位置 + 偏移
|
||||
double newLeft = _controlStartPoint.X + offsetX;
|
||||
double newTop = _controlStartPoint.Y + offsetY;
|
||||
|
||||
// 限制在父容器范围内
|
||||
newLeft = Math.Max(0, Math.Min(newLeft, parent.ActualWidth - this.ActualWidth));
|
||||
newTop = Math.Max(0, Math.Min(newTop, parent.ActualHeight - this.ActualHeight));
|
||||
|
||||
// 更新Margin
|
||||
this.Margin = new Thickness(newLeft, newTop, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拖动区域鼠标释放事件
|
||||
/// </summary>
|
||||
private void DragArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (((UIElement)sender).IsMouseCaptured)
|
||||
{
|
||||
((UIElement)sender).ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
if (_isDragging)
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() => { _isDragging = false; }),
|
||||
DispatcherPriority.Background);
|
||||
}
|
||||
else
|
||||
{
|
||||
_isDragging = false;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -95,18 +95,22 @@ namespace Ink_Canvas.Helpers
|
||||
// 使用改进的贝塞尔曲线拟合
|
||||
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 * 1.5)
|
||||
// 进一步放宽最终检查
|
||||
if (smoothedPoints.Length > originalPoints.Length * 2.5)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 重采样后点数仍然过多,返回原始笔画");
|
||||
// 如果仍然太多点,使用原始笔画
|
||||
return stroke;
|
||||
}
|
||||
@@ -117,6 +121,7 @@ namespace Ink_Canvas.Helpers
|
||||
DrawingAttributes = stroke.DrawingAttributes.Clone()
|
||||
};
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 成功创建平滑笔画");
|
||||
return smoothedStroke;
|
||||
}
|
||||
|
||||
@@ -125,33 +130,42 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
private StylusPoint[] ApplyImprovedBezierSmoothing(StylusPoint[] points)
|
||||
{
|
||||
if (points.Length < 4) return points;
|
||||
if (points.Length < 6) return points; // 5次贝塞尔需要6个点
|
||||
|
||||
var result = new List<StylusPoint>();
|
||||
|
||||
|
||||
// 添加第一个点
|
||||
result.Add(points[0]);
|
||||
|
||||
// 使用非重叠的窗口进行贝塞尔曲线拟合
|
||||
for (int i = 0; i < points.Length - 3; i += 3) // 每次移动3个点,避免重叠
|
||||
// 使用5次贝塞尔曲线,每次移动1个点确保连续性
|
||||
for (int i = 0; i < points.Length - 5; i++)
|
||||
{
|
||||
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 + 3, points.Length - 1)];
|
||||
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];
|
||||
|
||||
// 计算改进的控制点
|
||||
var controlPoints = CalculateImprovedControlPoints(p0, p1, p2, p3);
|
||||
|
||||
// 限制插值步数,避免点数爆炸
|
||||
int steps = Math.Min(UseAdaptiveInterpolation ?
|
||||
CalculateAdaptiveSteps(p0, p1, p2, p3) : InterpolationSteps, 16);
|
||||
// 计算5次贝塞尔的控制点
|
||||
var controlPoints = CalculateQuinticControlPoints(p0, p1, p2, p3, p4, p5);
|
||||
|
||||
// 生成贝塞尔曲线点,但跳过第一个点避免重复
|
||||
for (int j = 1; j <= steps; j++)
|
||||
// 生成插值点
|
||||
if (i == 0)
|
||||
{
|
||||
double t = (double)j / steps;
|
||||
var bezierPoint = CubicBezierWithControlPoints(controlPoints, t, p0, p3);
|
||||
// 第一个窗口:生成更多插值点
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -159,8 +173,209 @@ namespace Ink_Canvas.Helpers
|
||||
// 添加最后一个点
|
||||
result.Add(points[points.Length - 1]);
|
||||
|
||||
// 去重和优化点数
|
||||
return RemoveDuplicatePoints(result.ToArray());
|
||||
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>
|
||||
@@ -179,7 +394,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 计算控制点距离(基于点间距离)
|
||||
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;
|
||||
|
||||
@@ -214,7 +429,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 基于长度和曲率计算步数
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -251,7 +466,7 @@ namespace Ink_Canvas.Helpers
|
||||
var result = new List<StylusPoint>();
|
||||
result.Add(points[0]);
|
||||
|
||||
double minDistance = ResampleInterval * 0.5; // 最小距离阈值
|
||||
double minDistance = 0.3; // 进一步减少最小距离阈值,保留更多平滑点
|
||||
|
||||
for (int i = 1; i < points.Length; i++)
|
||||
{
|
||||
@@ -270,6 +485,7 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"RemoveDuplicatePoints: 输入点数={points.Length}, 输出点数={result.Count}");
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
@@ -516,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; // 如果点数增加太多,返回原始笔画
|
||||
}
|
||||
|
||||
@@ -540,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>
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动备份管理器
|
||||
/// 负责管理配置文件的自动备份功能
|
||||
/// </summary>
|
||||
public static class AutoBackupManager
|
||||
{
|
||||
private static readonly string BackupDir = Path.Combine(App.RootPath, "Backups");
|
||||
private static readonly string SettingsFile = Path.Combine(App.RootPath, "Configs", "Settings.json");
|
||||
private static readonly string BackupPrefix = "Settings_AutoBackup_";
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否需要执行自动备份
|
||||
/// </summary>
|
||||
/// <param name="settings">设置对象</param>
|
||||
/// <returns>如果需要备份返回true,否则返回false</returns>
|
||||
public static bool ShouldPerformAutoBackup(Settings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果自动备份功能未启用,不执行备份
|
||||
if (!settings.Advanced.IsAutoBackupEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果从未备份过,需要创建首次备份
|
||||
if (settings.Advanced.LastAutoBackupTime == DateTime.MinValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否已超过备份间隔
|
||||
var daysSinceLastBackup = (DateTime.Now - settings.Advanced.LastAutoBackupTime).TotalDays;
|
||||
return daysSinceLastBackup >= settings.Advanced.AutoBackupIntervalDays;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"检查自动备份条件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行自动备份
|
||||
/// </summary>
|
||||
/// <param name="settings">设置对象</param>
|
||||
/// <returns>备份是否成功</returns>
|
||||
public static bool PerformAutoBackup(Settings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保备份目录存在
|
||||
if (!Directory.Exists(BackupDir))
|
||||
{
|
||||
Directory.CreateDirectory(BackupDir);
|
||||
}
|
||||
|
||||
// 检查主配置文件是否存在
|
||||
if (!File.Exists(SettingsFile))
|
||||
{
|
||||
LogHelper.WriteLogToFile("主配置文件不存在,跳过自动备份", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建备份文件名(使用当前日期时间)
|
||||
string backupFileName = $"{BackupPrefix}{DateTime.Now:yyyyMMdd_HHmmss}.json";
|
||||
string backupPath = Path.Combine(BackupDir, backupFileName);
|
||||
|
||||
// 复制主配置文件到备份位置
|
||||
File.Copy(SettingsFile, backupPath, true);
|
||||
|
||||
// 更新最后备份时间
|
||||
settings.Advanced.LastAutoBackupTime = DateTime.Now;
|
||||
MainWindow.SaveSettingsToFile();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"执行自动备份时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从备份恢复配置文件
|
||||
/// </summary>
|
||||
/// <returns>恢复是否成功</returns>
|
||||
public static bool TryRestoreFromBackup()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保备份目录存在
|
||||
if (!Directory.Exists(BackupDir))
|
||||
{
|
||||
LogHelper.WriteLogToFile("备份目录不存在,无法从备份恢复", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查找最新的备份文件
|
||||
var backupFiles = Directory.GetFiles(BackupDir, $"{BackupPrefix}*.json")
|
||||
.OrderByDescending(f => File.GetCreationTime(f))
|
||||
.ToArray();
|
||||
|
||||
if (backupFiles.Length == 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile("没有找到可用的备份文件", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试使用最新的备份文件
|
||||
string latestBackup = backupFiles[0];
|
||||
|
||||
// 验证备份文件是否有效
|
||||
try
|
||||
{
|
||||
string backupJson = File.ReadAllText(latestBackup);
|
||||
var testSettings = JsonConvert.DeserializeObject<Settings>(backupJson);
|
||||
if (testSettings == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("备份文件内容无效,无法恢复", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"备份文件验证失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 备份当前损坏的配置文件(如果存在)
|
||||
if (File.Exists(SettingsFile))
|
||||
{
|
||||
string corruptedBackup = Path.Combine(BackupDir, $"Settings_Corrupted_{DateTime.Now:yyyyMMdd_HHmmss}.json");
|
||||
File.Copy(SettingsFile, corruptedBackup, true);
|
||||
}
|
||||
|
||||
// 从备份恢复配置文件
|
||||
File.Copy(latestBackup, SettingsFile, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从备份恢复配置文件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理过期的备份文件
|
||||
/// 保留最近30天的备份文件
|
||||
/// </summary>
|
||||
public static void CleanupOldBackups()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(BackupDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cutoffDate = DateTime.Now.AddDays(-30);
|
||||
var backupFiles = Directory.GetFiles(BackupDir, $"{BackupPrefix}*.json");
|
||||
|
||||
int deletedCount = 0;
|
||||
foreach (var file in backupFiles)
|
||||
{
|
||||
if (File.GetCreationTime(file) < cutoffDate)
|
||||
{
|
||||
File.Delete(file);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0)
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理过期备份文件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化自动备份功能
|
||||
/// 在应用程序启动时调用
|
||||
/// </summary>
|
||||
/// <param name="settings">设置对象</param>
|
||||
public static void Initialize(Settings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查是否需要执行自动备份
|
||||
if (ShouldPerformAutoBackup(settings))
|
||||
{
|
||||
PerformAutoBackup(settings);
|
||||
}
|
||||
|
||||
// 清理过期备份
|
||||
CleanupOldBackups();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化自动备份功能时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@@ -15,8 +17,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
@@ -74,6 +74,34 @@ namespace Ink_Canvas.Helpers
|
||||
GroupName = "inkeys",
|
||||
DownloadUrlFormat = "https://iccce.inkeys.top/Release/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "gh-proxy",
|
||||
VersionUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "hk.gh-proxy",
|
||||
VersionUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://hk.gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "cdn.gh-proxy",
|
||||
VersionUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://cdn.gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "edgeone.gh-proxy",
|
||||
VersionUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://edgeone.gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -111,6 +139,34 @@ namespace Ink_Canvas.Helpers
|
||||
GroupName = "inkeys",
|
||||
DownloadUrlFormat = "https://iccce.inkeys.top/Beta/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "gh-proxy",
|
||||
VersionUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "hk.gh-proxy",
|
||||
VersionUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://hk.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "cdn.gh-proxy",
|
||||
VersionUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://cdn.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
|
||||
},
|
||||
new UpdateLineGroup
|
||||
{
|
||||
GroupName = "edgeone.gh-proxy",
|
||||
VersionUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
|
||||
DownloadUrlFormat = "https://edgeone.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
|
||||
LogUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,14 +244,45 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
// 跳过"智教联盟"和"inkeys"线路组,不参与延迟检测和排序
|
||||
string testUrl = null;
|
||||
if (group.GroupName == "智教联盟" || group.GroupName == "inkeys")
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 跳过{group.GroupName}线路组延迟检测");
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(group.DownloadUrlFormat))
|
||||
{
|
||||
testUrl = group.DownloadUrlFormat.Replace("{0}", "test");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
testUrl = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
testUrl = group.VersionUrl;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(testUrl))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 缺少可用测速地址,跳过", LogHelper.LogType.Warning);
|
||||
continue;
|
||||
}
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({group.VersionUrl})");
|
||||
var delay = await GetUrlDelay(group.VersionUrl);
|
||||
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({testUrl})");
|
||||
|
||||
long delay;
|
||||
|
||||
if (group.GroupName == "智教联盟" || group.GroupName == "inkeys")
|
||||
{
|
||||
delay = await GetDownloadUrlDelay(testUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
delay = await GetUrlDelay(testUrl);
|
||||
}
|
||||
|
||||
if (delay >= 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 延迟: {delay}ms");
|
||||
@@ -213,20 +300,12 @@ namespace Ink_Canvas.Helpers
|
||||
.Select(x => x.group)
|
||||
.ToList();
|
||||
|
||||
// 将"智教联盟"线路组插入到最前面(如果存在)
|
||||
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
|
||||
if (zhiJiaoGroup != null)
|
||||
{
|
||||
orderedGroups.Insert(0, zhiJiaoGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 智教联盟线路组已插入到首位");
|
||||
}
|
||||
|
||||
// 将"inkeys"线路组插入到第二位(如果存在)
|
||||
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
|
||||
var inkeysGroup = orderedGroups.FirstOrDefault(g => g.GroupName == "inkeys");
|
||||
if (inkeysGroup != null)
|
||||
{
|
||||
orderedGroups.Insert(1, inkeysGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | inkeys线路组已插入到第二位");
|
||||
orderedGroups.Remove(inkeysGroup);
|
||||
orderedGroups.Insert(0, inkeysGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | inkeys线路组已默认优先");
|
||||
}
|
||||
|
||||
if (orderedGroups.Count > 0)
|
||||
@@ -245,6 +324,47 @@ namespace Ink_Canvas.Helpers
|
||||
return orderedGroups;
|
||||
}
|
||||
|
||||
private static async Task<long> GetDownloadUrlDelay(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var osVersion = Environment.OSVersion;
|
||||
bool isWindows7 = osVersion.Version.Major == 6 && osVersion.Version.Minor == 1;
|
||||
|
||||
if (isWindows7)
|
||||
{
|
||||
using (var handler = new HttpClientHandler())
|
||||
{
|
||||
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
|
||||
|
||||
using (var client = new HttpClient(handler))
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(5);
|
||||
var sw = Stopwatch.StartNew();
|
||||
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));
|
||||
sw.Stop();
|
||||
return sw.ElapsedMilliseconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(5);
|
||||
var sw = Stopwatch.StartNew();
|
||||
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));
|
||||
sw.Stop();
|
||||
return sw.ElapsedMilliseconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取远程版本号
|
||||
private static async Task<string> GetRemoteVersion(string fileUrl)
|
||||
{
|
||||
@@ -513,7 +633,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 尝试获取当前版本的发布时间
|
||||
DateTime? currentVersionReleaseTime = await GetVersionReleaseTime(localVersion, channel);
|
||||
|
||||
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(apiVersion, releaseTime, true, currentVersionReleaseTime); // 明确标记为自动更新
|
||||
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(apiVersion, releaseTime, true, currentVersionReleaseTime, localVersion); // 明确标记为自动更新
|
||||
if (!shouldPush)
|
||||
{
|
||||
var priority = DeviceIdentifier.GetUpdatePriority();
|
||||
@@ -569,7 +689,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 尝试获取当前版本的发布时间
|
||||
DateTime? currentVersionReleaseTime = await GetVersionReleaseTime(localVersion, channel);
|
||||
|
||||
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(remoteVersion, DateTime.Now, true, currentVersionReleaseTime); // 明确标记为自动更新
|
||||
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(remoteVersion, DateTime.Now, true, currentVersionReleaseTime, localVersion); // 明确标记为自动更新
|
||||
if (!shouldPush)
|
||||
{
|
||||
var priority = DeviceIdentifier.GetUpdatePriority();
|
||||
@@ -671,23 +791,13 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
SaveDownloadStatus(false);
|
||||
|
||||
// 优先尝试“智教联盟”线路组
|
||||
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
|
||||
// 优先尝试"inkeys"线路组
|
||||
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
|
||||
if (zhiJiaoGroup != null || inkeysGroup != null)
|
||||
if (inkeysGroup != null)
|
||||
{
|
||||
var priorityGroups = new List<UpdateLineGroup>();
|
||||
if (zhiJiaoGroup != null)
|
||||
{
|
||||
priorityGroups.Add(zhiJiaoGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试智教联盟线路组");
|
||||
}
|
||||
if (inkeysGroup != null)
|
||||
{
|
||||
priorityGroups.Add(inkeysGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试inkeys线路组");
|
||||
}
|
||||
groups = priorityGroups.Concat(groups.Where(g => g.GroupName != "智教联盟" && g.GroupName != "inkeys")).ToList();
|
||||
groups.Remove(inkeysGroup);
|
||||
groups.Insert(0, inkeysGroup);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试inkeys线路组");
|
||||
}
|
||||
|
||||
// 依次尝试每个线路组
|
||||
@@ -1238,7 +1348,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 查找解压后的主程序文件
|
||||
string newAppPath = null;
|
||||
string[] possibleExeNames = { "InkCanvasForClass.exe", "Ink Canvas.exe", "InkCanvas.exe" };
|
||||
|
||||
|
||||
foreach (string exeName in possibleExeNames)
|
||||
{
|
||||
string testPath = Path.Combine(extractPath, exeName);
|
||||
@@ -1260,12 +1370,12 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 准备启动新版本进程: {newAppPath}");
|
||||
|
||||
|
||||
// 启动新版本进程(以更新模式)
|
||||
string arguments = $"--update-mode --old-process-id={currentProcessId} --extract-path=\"{extractPath}\" --target-path=\"{currentAppDir}\" --is-silence={isInSilence}";
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 启动新进程的命令行: {newAppPath} {arguments}");
|
||||
|
||||
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = newAppPath,
|
||||
@@ -1276,10 +1386,10 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
Process.Start(startInfo);
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 新版本进程启动命令已执行");
|
||||
|
||||
|
||||
// 等待一小段时间确保新进程启动
|
||||
Thread.Sleep(2000);
|
||||
|
||||
|
||||
// 关闭当前旧软件进程
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 关闭当前旧软件进程");
|
||||
App.IsAppExitByUser = true;
|
||||
@@ -1312,7 +1422,7 @@ namespace Ink_Canvas.Helpers
|
||||
if (args.Contains("--update-mode"))
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 检测到更新模式启动");
|
||||
|
||||
|
||||
// 解析命令行参数
|
||||
int oldProcessId = -1;
|
||||
string extractPath = null;
|
||||
@@ -1326,7 +1436,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
string arg = args[i];
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 处理参数 {i}: {arg}");
|
||||
|
||||
|
||||
if (arg.StartsWith("--old-process-id="))
|
||||
{
|
||||
string processIdStr = arg.Substring("--old-process-id=".Length);
|
||||
@@ -1365,7 +1475,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 启动更新任务
|
||||
Task.Run(async () => await PerformUpdate(oldProcessId, extractPath, targetPath, isSilence));
|
||||
return true; // 返回true表示是更新模式
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 参数验证失败 - 老进程ID: {oldProcessId}, 解压路径: {extractPath}, 目标路径: {targetPath}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
@@ -1437,7 +1547,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
// 复制文件到目标目录
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 开始复制文件从 {extractPath} 到 {targetPath}");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// 使用递归复制方法,支持重试机制
|
||||
@@ -1445,11 +1555,11 @@ namespace Ink_Canvas.Helpers
|
||||
if (copySuccess)
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 文件复制完成");
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 文件复制失败,部分文件可能无法覆盖", LogHelper.LogType.Error);
|
||||
|
||||
|
||||
if (!isSilence)
|
||||
{
|
||||
MessageBox.Show("更新失败:部分文件无法覆盖,可能是文件正在使用中。\n请关闭所有相关程序后重试。", "更新失败", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
@@ -1460,7 +1570,7 @@ namespace Ink_Canvas.Helpers
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 文件复制失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
|
||||
|
||||
if (!isSilence)
|
||||
{
|
||||
MessageBox.Show($"更新失败:文件复制时出错\n{ex.Message}", "更新失败", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
@@ -1472,7 +1582,7 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 清理临时文件");
|
||||
|
||||
|
||||
// 删除解压目录
|
||||
if (Directory.Exists(extractPath))
|
||||
{
|
||||
@@ -1503,23 +1613,23 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 更新操作完成");
|
||||
|
||||
// 启动更新后的应用程序
|
||||
// 启动更新后的应用程序
|
||||
string newAppPath = Path.Combine(targetPath, "InkCanvasForClass.exe");
|
||||
if (File.Exists(newAppPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 准备启动更新后的应用程序: {newAppPath}");
|
||||
|
||||
|
||||
// 获取当前更新进程ID
|
||||
int currentUpdateProcessId = Process.GetCurrentProcess().Id;
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 当前更新进程ID: {currentUpdateProcessId}");
|
||||
|
||||
|
||||
// 创建一个临时标记文件,用于新进程检测更新状态
|
||||
string updateMarkerFile = Path.Combine(targetPath, "update_in_progress.tmp");
|
||||
File.WriteAllText(updateMarkerFile, currentUpdateProcessId.ToString());
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 创建更新标记文件: {updateMarkerFile}");
|
||||
|
||||
|
||||
// 启动更新后的应用程序(标记为最终应用,不受相同进程影响)
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
{
|
||||
@@ -1528,24 +1638,24 @@ namespace Ink_Canvas.Helpers
|
||||
WorkingDirectory = targetPath,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
|
||||
Process newProcess = Process.Start(startInfo);
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 最终应用程序启动成功,PID: {newProcess?.Id},已标记为最终应用");
|
||||
|
||||
|
||||
// 等待一小段时间确保最终应用程序启动
|
||||
Thread.Sleep(2000);
|
||||
|
||||
|
||||
// 结束当前更新进程
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 更新流程完成,结束更新进程");
|
||||
|
||||
|
||||
// 强制结束当前更新进程
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 强制结束更新进程");
|
||||
|
||||
|
||||
// 标记为应用主动退出,避免看门狗重启
|
||||
App.IsAppExitByUser = true;
|
||||
|
||||
|
||||
// 写入退出信号文件,确保看门狗不会重启
|
||||
try
|
||||
{
|
||||
@@ -1557,7 +1667,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 写入看门狗退出信号文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1569,7 +1679,7 @@ namespace Ink_Canvas.Helpers
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 启动更新后的应用程序失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
|
||||
|
||||
if (!isSilence)
|
||||
{
|
||||
MessageBox.Show($"更新完成,但启动应用程序失败:{ex.Message}\n请手动启动应用程序。", "启动失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
@@ -1579,7 +1689,7 @@ namespace Ink_Canvas.Helpers
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 更新后的应用程序文件不存在: {newAppPath}", LogHelper.LogType.Error);
|
||||
|
||||
|
||||
if (!isSilence)
|
||||
{
|
||||
MessageBox.Show($"更新完成,但未找到应用程序文件:{newAppPath}\n请检查更新是否成功。", "文件缺失", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
@@ -1589,7 +1699,7 @@ namespace Ink_Canvas.Helpers
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 执行更新操作时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
|
||||
|
||||
if (!isSilence)
|
||||
{
|
||||
MessageBox.Show($"更新失败:{ex.Message}", "更新失败", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
@@ -1610,12 +1720,24 @@ namespace Ink_Canvas.Helpers
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
}
|
||||
|
||||
// 定义需要覆盖的文件列表(仅覆盖主程序和配置文件)
|
||||
string[] filesToOverwrite = { "InkCanvasForClass.exe", "InkCanvasForClass.exe.config" };
|
||||
|
||||
// 复制文件
|
||||
foreach (FileInfo file in dir.GetFiles())
|
||||
{
|
||||
// 只覆盖指定的文件,跳过其他文件
|
||||
if (!filesToOverwrite.Contains(file.Name))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 跳过文件(不在覆盖列表中): {file.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
string targetFilePath = Path.Combine(destinationDir, file.Name);
|
||||
bool fileCopied = false;
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 开始覆盖文件: {file.Name}");
|
||||
|
||||
// 重试机制,最多重试3次
|
||||
for (int retry = 0; retry < 3; retry++)
|
||||
{
|
||||
@@ -1638,22 +1760,23 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await Task.Run(() => file.CopyTo(targetFilePath));
|
||||
fileCopied = true;
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 文件覆盖成功: {file.Name}");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 复制文件失败 (重试 {retry + 1}/3) {file.FullName} -> {targetFilePath}: {ex.Message}", LogHelper.LogType.Warning);
|
||||
|
||||
|
||||
if (retry < 2)
|
||||
{
|
||||
Thread.Sleep(1000); // 等待1秒后重试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!fileCopied)
|
||||
{
|
||||
allFilesCopied = false;
|
||||
@@ -1687,19 +1810,32 @@ namespace Ink_Canvas.Helpers
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
}
|
||||
|
||||
// 定义需要覆盖的文件列表(仅覆盖主程序和配置文件)
|
||||
string[] filesToOverwrite = { "InkCanvasForClass.exe", "InkCanvasForClass.exe.config" };
|
||||
|
||||
// 复制文件
|
||||
foreach (FileInfo file in dir.GetFiles())
|
||||
{
|
||||
// 只覆盖指定的文件,跳过其他文件
|
||||
if (!filesToOverwrite.Contains(file.Name))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 跳过文件(不在覆盖列表中): {file.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
string targetFilePath = Path.Combine(destinationDir, file.Name);
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 开始覆盖文件: {file.Name}");
|
||||
|
||||
// 如果目标文件存在且正在使用,先删除
|
||||
if (File.Exists(targetFilePath))
|
||||
{
|
||||
File.Delete(targetFilePath);
|
||||
}
|
||||
|
||||
|
||||
await Task.Run(() => file.CopyTo(targetFilePath));
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 文件覆盖成功: {file.Name}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1866,7 +2002,7 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
// 执行安装,静默模式
|
||||
InstallNewVersionApp(remoteVersion, true);
|
||||
InstallNewVersionApp(remoteVersion, true);
|
||||
App.IsAppExitByUser = true;
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
@@ -1996,7 +2132,7 @@ namespace Ink_Canvas.Helpers
|
||||
return false;
|
||||
}
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 手动安装版本: {version}");
|
||||
InstallNewVersionApp(version, true);
|
||||
InstallNewVersionApp(version, true);
|
||||
App.IsAppExitByUser = true;
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
using AForge.Video;
|
||||
using AForge.Video.DirectShow;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public class CameraService : IDisposable
|
||||
{
|
||||
private VideoCaptureDevice _videoSource;
|
||||
private bool _isCapturing;
|
||||
private Bitmap _currentFrame;
|
||||
private readonly object _frameLock = new object();
|
||||
private Dispatcher _dispatcher;
|
||||
|
||||
// 新增属性
|
||||
private int _rotationAngle = 0; // 0=0度,1=90度,2=180度,3=270度
|
||||
private int _resolutionWidth = 640;
|
||||
private int _resolutionHeight = 480;
|
||||
|
||||
public event EventHandler<Bitmap> FrameReceived;
|
||||
public event EventHandler<string> ErrorOccurred;
|
||||
|
||||
public bool IsCapturing => _isCapturing;
|
||||
public List<FilterInfo> AvailableCameras { get; private set; }
|
||||
public FilterInfo CurrentCamera { get; private set; }
|
||||
|
||||
// 新增属性
|
||||
public int RotationAngle
|
||||
{
|
||||
get => _rotationAngle;
|
||||
set => _rotationAngle = Math.Max(0, Math.Min(3, value));
|
||||
}
|
||||
|
||||
public int ResolutionWidth
|
||||
{
|
||||
get => _resolutionWidth;
|
||||
set => _resolutionWidth = Math.Max(320, Math.Min(1920, value));
|
||||
}
|
||||
|
||||
public int ResolutionHeight
|
||||
{
|
||||
get => _resolutionHeight;
|
||||
set => _resolutionHeight = Math.Max(240, Math.Min(1080, value));
|
||||
}
|
||||
|
||||
public CameraService()
|
||||
{
|
||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||
AvailableCameras = new List<FilterInfo>();
|
||||
RefreshCameraList();
|
||||
}
|
||||
|
||||
public CameraService(int rotationAngle, int resolutionWidth, int resolutionHeight)
|
||||
{
|
||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||
AvailableCameras = new List<FilterInfo>();
|
||||
_rotationAngle = rotationAngle;
|
||||
_resolutionWidth = resolutionWidth;
|
||||
_resolutionHeight = resolutionHeight;
|
||||
RefreshCameraList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新可用摄像头列表
|
||||
/// </summary>
|
||||
public void RefreshCameraList()
|
||||
{
|
||||
try
|
||||
{
|
||||
AvailableCameras.Clear();
|
||||
var videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
|
||||
|
||||
foreach (FilterInfo device in videoDevices)
|
||||
{
|
||||
AvailableCameras.Add(device);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"刷新摄像头列表失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
ErrorOccurred?.Invoke(this, $"刷新摄像头列表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始摄像头预览
|
||||
/// </summary>
|
||||
/// <param name="cameraIndex">摄像头索引</param>
|
||||
public bool StartPreview(int cameraIndex = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (AvailableCameras.Count == 0)
|
||||
{
|
||||
RefreshCameraList();
|
||||
if (AvailableCameras.Count == 0)
|
||||
{
|
||||
ErrorOccurred?.Invoke(this, "未找到可用的摄像头设备");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
|
||||
{
|
||||
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 停止当前预览
|
||||
StopPreview();
|
||||
|
||||
CurrentCamera = AvailableCameras[cameraIndex];
|
||||
_videoSource = new VideoCaptureDevice(CurrentCamera.MonikerString);
|
||||
|
||||
// 设置视频源事件处理
|
||||
_videoSource.NewFrame += VideoSource_NewFrame;
|
||||
|
||||
// 启动视频源
|
||||
_videoSource.Start();
|
||||
|
||||
_isCapturing = true;
|
||||
LogHelper.WriteLogToFile($"开始摄像头预览: {CurrentCamera.Name}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
ErrorOccurred?.Invoke(this, $"启动摄像头预览失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止摄像头预览
|
||||
/// </summary>
|
||||
public void StopPreview()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_videoSource != null && _videoSource.IsRunning)
|
||||
{
|
||||
_videoSource.SignalToStop();
|
||||
_videoSource.WaitForStop();
|
||||
_videoSource.NewFrame -= VideoSource_NewFrame;
|
||||
_videoSource = null;
|
||||
}
|
||||
|
||||
_isCapturing = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"停止摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定摄像头
|
||||
/// </summary>
|
||||
/// <param name="cameraIndex">摄像头索引</param>
|
||||
public bool SwitchCamera(int cameraIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
|
||||
{
|
||||
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
|
||||
return false;
|
||||
}
|
||||
|
||||
return StartPreview(cameraIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"切换摄像头失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
ErrorOccurred?.Invoke(this, $"切换摄像头失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前帧的BitmapSource(WPF格式),直接返回可用的WPF位图
|
||||
/// </summary>
|
||||
public BitmapSource GetCurrentFrameAsBitmapSource()
|
||||
{
|
||||
lock (_frameLock)
|
||||
{
|
||||
if (_currentFrame == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// 验证位图有效性
|
||||
if (_currentFrame.Width <= 0 || _currentFrame.Height <= 0)
|
||||
return null;
|
||||
|
||||
// 使用更安全的方法转换位图
|
||||
var bitmapData = _currentFrame.LockBits(
|
||||
new Rectangle(0, 0, _currentFrame.Width, _currentFrame.Height),
|
||||
ImageLockMode.ReadOnly,
|
||||
_currentFrame.PixelFormat);
|
||||
|
||||
try
|
||||
{
|
||||
// 根据像素格式选择合适的WPF像素格式
|
||||
System.Windows.Media.PixelFormat wpfPixelFormat;
|
||||
switch (_currentFrame.PixelFormat)
|
||||
{
|
||||
case PixelFormat.Format24bppRgb:
|
||||
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
|
||||
break;
|
||||
case PixelFormat.Format32bppArgb:
|
||||
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgra32;
|
||||
break;
|
||||
case PixelFormat.Format32bppRgb:
|
||||
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr32;
|
||||
break;
|
||||
default:
|
||||
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
|
||||
break;
|
||||
}
|
||||
|
||||
var bitmapSource = BitmapSource.Create(
|
||||
bitmapData.Width,
|
||||
bitmapData.Height,
|
||||
_currentFrame.HorizontalResolution,
|
||||
_currentFrame.VerticalResolution,
|
||||
wpfPixelFormat,
|
||||
null,
|
||||
bitmapData.Scan0,
|
||||
bitmapData.Stride * bitmapData.Height,
|
||||
bitmapData.Stride);
|
||||
|
||||
bitmapSource.Freeze();
|
||||
return bitmapSource;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentFrame.UnlockBits(bitmapData);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"转换帧为BitmapSource失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 视频源新帧事件处理
|
||||
/// </summary>
|
||||
private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_frameLock)
|
||||
{
|
||||
// 释放之前的帧
|
||||
_currentFrame?.Dispose();
|
||||
|
||||
// 创建新的位图,避免Clone的问题
|
||||
var sourceFrame = eventArgs.Frame;
|
||||
|
||||
if (sourceFrame != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var width = sourceFrame.Width;
|
||||
var height = sourceFrame.Height;
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
// 应用旋转
|
||||
Bitmap rotatedFrame = ApplyRotation(sourceFrame);
|
||||
|
||||
int targetWidth = _resolutionWidth;
|
||||
int targetHeight = _resolutionHeight;
|
||||
|
||||
if (_rotationAngle == 1 || _rotationAngle == 3)
|
||||
{
|
||||
targetWidth = _resolutionHeight;
|
||||
targetHeight = _resolutionWidth;
|
||||
}
|
||||
|
||||
_currentFrame = ResizeImageWithAspectRatio(rotatedFrame, targetWidth, targetHeight);
|
||||
|
||||
rotatedFrame?.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentFrame = null;
|
||||
}
|
||||
}
|
||||
catch (Exception frameEx)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理源帧失败: {frameEx.Message}", LogHelper.LogType.Error);
|
||||
_currentFrame = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 在UI线程中触发事件
|
||||
_dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
FrameReceived?.Invoke(this, _currentFrame);
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理新帧失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
ErrorOccurred?.Invoke(this, $"处理新帧失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取摄像头名称列表
|
||||
/// </summary>
|
||||
public List<string> GetCameraNames()
|
||||
{
|
||||
return AvailableCameras.Select(camera => camera.Name).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有可用摄像头
|
||||
/// </summary>
|
||||
public bool HasAvailableCameras()
|
||||
{
|
||||
if (AvailableCameras.Count == 0)
|
||||
{
|
||||
RefreshCameraList();
|
||||
}
|
||||
return AvailableCameras.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用旋转到图像
|
||||
/// </summary>
|
||||
private Bitmap ApplyRotation(Bitmap source)
|
||||
{
|
||||
if (_rotationAngle == 0)
|
||||
return new Bitmap(source);
|
||||
|
||||
var rotationType = RotateFlipType.RotateNoneFlipNone;
|
||||
switch (_rotationAngle)
|
||||
{
|
||||
case 1: rotationType = RotateFlipType.Rotate90FlipNone; break;
|
||||
case 2: rotationType = RotateFlipType.Rotate180FlipNone; break;
|
||||
case 3: rotationType = RotateFlipType.Rotate270FlipNone; break;
|
||||
}
|
||||
|
||||
var rotated = new Bitmap(source);
|
||||
rotated.RotateFlip(rotationType);
|
||||
return rotated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调整图像大小
|
||||
/// </summary>
|
||||
private Bitmap ResizeImageWithAspectRatio(Bitmap source, int targetWidth, int targetHeight)
|
||||
{
|
||||
if (source.Width == targetWidth && source.Height == targetHeight)
|
||||
return new Bitmap(source);
|
||||
|
||||
double scaleX = (double)targetWidth / source.Width;
|
||||
double scaleY = (double)targetHeight / source.Height;
|
||||
double scale = Math.Min(scaleX, scaleY);
|
||||
|
||||
// 计算实际尺寸
|
||||
int actualWidth = (int)(source.Width * scale);
|
||||
int actualHeight = (int)(source.Height * scale);
|
||||
|
||||
var resized = new Bitmap(actualWidth, actualHeight, PixelFormat.Format24bppRgb);
|
||||
using (var graphics = Graphics.FromImage(resized))
|
||||
{
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
|
||||
graphics.DrawImage(source, 0, 0, actualWidth, actualHeight);
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调整图像大小
|
||||
/// </summary>
|
||||
private Bitmap ResizeImage(Bitmap source, int width, int height)
|
||||
{
|
||||
if (source.Width == width && source.Height == height)
|
||||
return new Bitmap(source);
|
||||
|
||||
var resized = new Bitmap(width, height, PixelFormat.Format24bppRgb);
|
||||
using (var graphics = Graphics.FromImage(resized))
|
||||
{
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
|
||||
graphics.DrawImage(source, 0, 0, width, height);
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopPreview();
|
||||
|
||||
lock (_frameLock)
|
||||
{
|
||||
_currentFrame?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,4 +112,44 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -5,7 +6,6 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
@@ -17,7 +17,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 文件路径策略
|
||||
private static readonly string DeviceIdFilePath = Path.Combine(App.RootPath, "device_id.dat");
|
||||
private static readonly string UsageStatsFilePath = Path.Combine(App.RootPath, "usage_stats.enc");
|
||||
private static readonly string UsageStatsBackupPath = Path.Combine(App.RootPath, "saves", "usage_stats_backup.enc");
|
||||
private static readonly string UsageStatsBackupPath = Path.Combine(App.RootPath, "Saves", "usage_stats_backup.enc");
|
||||
|
||||
private static readonly string DeviceId;
|
||||
private static readonly object fileLock = new object();
|
||||
@@ -758,26 +758,26 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
|
||||
// 如果所有文件都不存在或损坏,返回新的统计对象
|
||||
var newStats = new UsageStats
|
||||
{
|
||||
DeviceId = DeviceId,
|
||||
LastLaunchTime = DateTime.Now,
|
||||
LaunchCount = 0,
|
||||
TotalUsageSeconds = 0,
|
||||
AverageSessionSeconds = 0,
|
||||
LastUpdateCheck = DateTime.MinValue,
|
||||
UpdatePriority = UpdatePriority.Medium,
|
||||
UsageFrequency = UsageFrequency.Medium
|
||||
};
|
||||
var newStats = new UsageStats
|
||||
{
|
||||
DeviceId = DeviceId,
|
||||
LastLaunchTime = DateTime.Now,
|
||||
LaunchCount = 0,
|
||||
TotalUsageSeconds = 0,
|
||||
AverageSessionSeconds = 0,
|
||||
LastUpdateCheck = DateTime.MinValue,
|
||||
UpdatePriority = UpdatePriority.Medium,
|
||||
UsageFrequency = UsageFrequency.Medium
|
||||
};
|
||||
|
||||
// 保存新统计到文件
|
||||
SaveUsageStatsToFile(UsageStatsFilePath, newStats);
|
||||
return newStats;
|
||||
return newStats;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 加载使用统计失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
|
||||
|
||||
// 返回默认统计对象
|
||||
return new UsageStats
|
||||
{
|
||||
@@ -800,7 +800,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
// 保存到主文件
|
||||
SaveUsageStatsToFile(UsageStatsFilePath, stats);
|
||||
|
||||
|
||||
// 保存到备份文件
|
||||
SaveUsageStatsToFile(UsageStatsBackupPath, stats);
|
||||
}
|
||||
@@ -822,7 +822,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
return stats;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -843,39 +843,39 @@ namespace Ink_Canvas.Helpers
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
byte[] encryptedData = File.ReadAllBytes(filePath);
|
||||
|
||||
|
||||
if (encryptedData.Length < 32) // SHA256校验和长度为32字节
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 加密文件格式错误: {filePath}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 提取校验和和加密数据
|
||||
byte[] checksum = new byte[32];
|
||||
byte[] data = new byte[encryptedData.Length - 32];
|
||||
Array.Copy(encryptedData, 0, checksum, 0, 32);
|
||||
Array.Copy(encryptedData, 32, data, 0, data.Length);
|
||||
|
||||
|
||||
// 使用SHA256生成解密密钥
|
||||
using (var sha256 = SHA256.Create())
|
||||
{
|
||||
byte[] keyBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(DeviceId + "ICC_Usage_Stats_Salt"));
|
||||
|
||||
|
||||
// XOR解密
|
||||
byte[] decryptedData = new byte[data.Length];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
decryptedData[i] = (byte)(data[i] ^ keyBytes[i % keyBytes.Length]);
|
||||
}
|
||||
|
||||
// 验证校验和
|
||||
|
||||
// 验证校验
|
||||
byte[] computedChecksum = sha256.ComputeHash(decryptedData);
|
||||
if (!checksum.SequenceEqual(computedChecksum))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 加密文件校验和验证失败: {filePath}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
string json = Encoding.UTF8.GetString(decryptedData);
|
||||
var stats = JsonConvert.DeserializeObject<UsageStats>(json);
|
||||
if (stats != null && !string.IsNullOrEmpty(stats.DeviceId))
|
||||
@@ -893,8 +893,9 @@ namespace Ink_Canvas.Helpers
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 保存使用统计到文件(加密)
|
||||
/// 保存使用统计到文件
|
||||
/// </summary>
|
||||
private static void SaveUsageStatsToFile(string filePath, UsageStats stats)
|
||||
{
|
||||
@@ -909,27 +910,27 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
string json = JsonConvert.SerializeObject(stats, Formatting.Indented);
|
||||
byte[] data = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
|
||||
// 使用SHA256生成加密密钥(基于设备ID)
|
||||
using (var sha256 = SHA256.Create())
|
||||
{
|
||||
byte[] keyBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(DeviceId + "ICC_Usage_Stats_Salt"));
|
||||
|
||||
|
||||
// 简单的XOR加密
|
||||
byte[] encryptedData = new byte[data.Length];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
encryptedData[i] = (byte)(data[i] ^ keyBytes[i % keyBytes.Length]);
|
||||
}
|
||||
|
||||
|
||||
// 添加SHA256校验和
|
||||
byte[] checksum = sha256.ComputeHash(data);
|
||||
byte[] finalData = new byte[checksum.Length + encryptedData.Length];
|
||||
checksum.CopyTo(finalData, 0);
|
||||
encryptedData.CopyTo(finalData, checksum.Length);
|
||||
|
||||
|
||||
File.WriteAllBytes(filePath, finalData);
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 加密使用统计已保存到: {filePath}");
|
||||
}
|
||||
}
|
||||
@@ -958,7 +959,7 @@ namespace Ink_Canvas.Helpers
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 记录更新检查失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 从备份文件恢复使用统计数据
|
||||
@@ -997,7 +998,7 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
var status = new List<string>();
|
||||
|
||||
|
||||
// 检查主文件
|
||||
if (File.Exists(UsageStatsFilePath))
|
||||
{
|
||||
@@ -1054,7 +1055,7 @@ namespace Ink_Canvas.Helpers
|
||||
/// <param name="isAutoUpdate">是否为自动更新检查(默认true,false表示版本修复)</param>
|
||||
/// <param name="currentVersionReleaseTime">当前版本发布时间</param>
|
||||
/// <returns>是否应该推送更新</returns>
|
||||
public static bool ShouldPushUpdate(string updateVersion, DateTime releaseTime, bool isAutoUpdate = true, DateTime? currentVersionReleaseTime = null)
|
||||
public static bool ShouldPushUpdate(string updateVersion, DateTime releaseTime, bool isAutoUpdate = true, DateTime? currentVersionReleaseTime = null, string localVersion = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -1064,7 +1065,6 @@ namespace Ink_Canvas.Helpers
|
||||
// 如果不是自动更新(即版本修复),则应用不同的策略
|
||||
if (!isAutoUpdate)
|
||||
{
|
||||
// 版本修复:立即允许,不受分级策略影响
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本修复 - 版本: {updateVersion}, 类型: {updateType}, 结果: 允许");
|
||||
return true;
|
||||
}
|
||||
@@ -1086,6 +1086,23 @@ namespace Ink_Canvas.Helpers
|
||||
daysBetweenVersions = (DateTime.Now - releaseTime).TotalDays;
|
||||
}
|
||||
|
||||
// 当无法获取版本发布时间时,判断版本号差异
|
||||
if (!currentVersionReleaseTime.HasValue && !string.IsNullOrEmpty(localVersion))
|
||||
{
|
||||
int versionDiff = CalculateVersionGenerationDifference(localVersion, updateVersion);
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 无法获取版本发布时间,使用版本号差异判断 - 本地版本: {localVersion}, 远程版本: {updateVersion}, 代数差异: {versionDiff}");
|
||||
|
||||
if (versionDiff >= 1)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=1,允许更新");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})<1,可能是相同版本或降级,暂不更新");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算最近活跃度(最后一次使用距今的天数)
|
||||
var daysSinceLastUse = (DateTime.Now - stats.LastLaunchTime).TotalDays;
|
||||
|
||||
@@ -1265,6 +1282,77 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算版本号代数差异
|
||||
/// </summary>
|
||||
/// <param name="localVersion">本地版本号</param>
|
||||
/// <param name="remoteVersion">远程版本号</param>
|
||||
/// <returns>版本号代数差异,如果无法计算则返回0</returns>
|
||||
private static int CalculateVersionGenerationDifference(string localVersion, string remoteVersion)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(localVersion) || string.IsNullOrEmpty(remoteVersion))
|
||||
return 0;
|
||||
|
||||
// 移除可能的前缀(如 "v")
|
||||
var cleanLocal = localVersion.TrimStart('v', 'V');
|
||||
var cleanRemote = remoteVersion.TrimStart('v', 'V');
|
||||
|
||||
// 解析版本号 (格式: X.X.X.X)
|
||||
var localParts = cleanLocal.Split('.');
|
||||
var remoteParts = cleanRemote.Split('.');
|
||||
|
||||
if (localParts.Length < 4 || remoteParts.Length < 4)
|
||||
return 0;
|
||||
|
||||
// 解析四个版本号部分
|
||||
if (int.TryParse(localParts[0], out int localMajor) &&
|
||||
int.TryParse(localParts[1], out int localMinor) &&
|
||||
int.TryParse(localParts[2], out int localBuild) &&
|
||||
int.TryParse(localParts[3], out int localRevision) &&
|
||||
int.TryParse(remoteParts[0], out int remoteMajor) &&
|
||||
int.TryParse(remoteParts[1], out int remoteMinor) &&
|
||||
int.TryParse(remoteParts[2], out int remoteBuild) &&
|
||||
int.TryParse(remoteParts[3], out int remoteRevision))
|
||||
{
|
||||
// 计算代数差异:主版本号差异 * 1000 + 次版本号差异 * 100 + 构建号差异 * 10 + 修订号差异
|
||||
int majorDiff = remoteMajor - localMajor;
|
||||
int minorDiff = remoteMinor - localMinor;
|
||||
int buildDiff = remoteBuild - localBuild;
|
||||
int revisionDiff = remoteRevision - localRevision;
|
||||
|
||||
// 如果主版本号不同,则代数差异很大
|
||||
if (majorDiff != 0)
|
||||
{
|
||||
return majorDiff * 1000 + minorDiff * 100 + buildDiff * 10 + revisionDiff;
|
||||
}
|
||||
|
||||
// 如果次版本号不同,则代数差异中等
|
||||
if (minorDiff != 0)
|
||||
{
|
||||
return minorDiff * 100 + buildDiff * 10 + revisionDiff;
|
||||
}
|
||||
|
||||
// 如果构建号不同,则代数差异较小
|
||||
if (buildDiff != 0)
|
||||
{
|
||||
return buildDiff * 10 + revisionDiff;
|
||||
}
|
||||
|
||||
// 只有修订号不同,代数差异最小
|
||||
return revisionDiff;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 计算版本号代数差异失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据更新类型获取紧急程度倍数(仅用于自动更新分级)
|
||||
/// </summary>
|
||||
@@ -1339,7 +1427,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
return descriptions.Count > 0 ? string.Join(", ", descriptions) : "普通用户";
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 关机时保存使用时间数据
|
||||
/// </summary>
|
||||
@@ -1360,7 +1448,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 2. 计算本次会话时长(防止异常值)
|
||||
TimeSpan sessionDuration = DateTime.Now - App.appStartTime;
|
||||
long sessionSeconds = Math.Max(0, (long)sessionDuration.TotalSeconds);
|
||||
|
||||
|
||||
// 防止异常大的会话时长(超过24小时)
|
||||
if (sessionSeconds > 86400)
|
||||
{
|
||||
@@ -1373,10 +1461,10 @@ namespace Ink_Canvas.Helpers
|
||||
stats.LaunchCount++;
|
||||
stats.AverageSessionSeconds = stats.TotalUsageSeconds / (double)Math.Max(1, stats.LaunchCount);
|
||||
stats.LastLaunchTime = DateTime.Now;
|
||||
|
||||
|
||||
// 4. 保存数据
|
||||
SaveUsageStats(stats);
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile("DeviceIdentifier | 关机保存完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Dlass API 客户端,用于与服务端通信
|
||||
/// </summary>
|
||||
public class DlassApiClient : IDisposable
|
||||
{
|
||||
private const string DEFAULT_BASE_URL = "https://dlass.tech";
|
||||
private readonly string _appId;
|
||||
private readonly string _appSecret;
|
||||
private readonly string _baseUrl;
|
||||
private HttpClient _httpClient;
|
||||
private string _accessToken;
|
||||
private DateTime _tokenExpiresAt;
|
||||
|
||||
private string _userToken;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 Dlass API 客户端
|
||||
/// </summary>
|
||||
/// <param name="appId">应用ID</param>
|
||||
/// <param name="appSecret">应用密钥</param>
|
||||
/// <param name="baseUrl">API基础URL,如果为空则使用默认URL</param>
|
||||
/// <param name="userToken">用户Token,如果提供则优先使用用户token而不是App Secret</param>
|
||||
public DlassApiClient(string appId, string appSecret, string baseUrl = null, string userToken = null)
|
||||
{
|
||||
_appId = appId ?? throw new ArgumentNullException(nameof(appId));
|
||||
_appSecret = appSecret ?? throw new ArgumentNullException(nameof(appSecret));
|
||||
_userToken = userToken;
|
||||
_baseUrl = baseUrl ?? DEFAULT_BASE_URL;
|
||||
|
||||
_baseUrl = _baseUrl.TrimEnd('/');
|
||||
if (!_baseUrl.StartsWith("http://") && !_baseUrl.StartsWith("https://"))
|
||||
{
|
||||
_baseUrl = "https://" + _baseUrl;
|
||||
}
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_baseUrl),
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
_httpClient.DefaultRequestHeaders.Add("User-Agent", "InkCanvas/1.0");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取访问令牌(Access Token)
|
||||
/// </summary>
|
||||
public async Task<string> GetAccessTokenAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_userToken))
|
||||
{
|
||||
return _userToken;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_accessToken) && DateTime.Now < _tokenExpiresAt.AddMinutes(-5))
|
||||
{
|
||||
return _accessToken;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var requestData = new
|
||||
{
|
||||
app_id = _appId,
|
||||
app_secret = _appSecret,
|
||||
grant_type = "client_credentials"
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(requestData);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await _httpClient.PostAsync("/oauth/token", content);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
|
||||
_accessToken = tokenResponse.AccessToken;
|
||||
_tokenExpiresAt = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn ?? 3600);
|
||||
return _accessToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"获取Access Token失败: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"获取Access Token时网络错误: {httpEx.Message}", httpEx);
|
||||
}
|
||||
catch (TaskCanceledException timeoutEx)
|
||||
{
|
||||
throw new Exception("获取Access Token时请求超时", timeoutEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"获取Access Token时出错: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送GET请求
|
||||
/// </summary>
|
||||
public async Task<T> GetAsync<T>(string endpoint, bool requireAuth = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
string token = null;
|
||||
if (requireAuth)
|
||||
{
|
||||
token = await GetAccessTokenAsync();
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
|
||||
|
||||
if (requireAuth && !string.IsNullOrEmpty(token))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_userToken))
|
||||
{
|
||||
request.Headers.Add("X-User-Token", token);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
return JsonConvert.DeserializeObject<T>(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
|
||||
}
|
||||
catch (TaskCanceledException timeoutEx)
|
||||
{
|
||||
throw new Exception($"请求超时: {endpoint}", timeoutEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送POST请求
|
||||
/// </summary>
|
||||
public async Task<T> PostAsync<T>(string endpoint, object data = null, bool requireAuth = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
string token = null;
|
||||
if (requireAuth)
|
||||
{
|
||||
token = await GetAccessTokenAsync();
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||
|
||||
if (requireAuth && !string.IsNullOrEmpty(token))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_userToken))
|
||||
{
|
||||
request.Headers.Add("X-User-Token", token);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
}
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(data);
|
||||
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
return JsonConvert.DeserializeObject<T>(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
|
||||
}
|
||||
catch (TaskCanceledException timeoutEx)
|
||||
{
|
||||
throw new Exception($"请求超时: {endpoint}", timeoutEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送PUT请求
|
||||
/// </summary>
|
||||
public async Task<T> PutAsync<T>(string endpoint, object data = null, bool requireAuth = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
string token = null;
|
||||
if (requireAuth)
|
||||
{
|
||||
token = await GetAccessTokenAsync();
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
|
||||
|
||||
if (requireAuth && !string.IsNullOrEmpty(token))
|
||||
{
|
||||
// 如果是用户token,使用X-User-Token header
|
||||
if (!string.IsNullOrEmpty(_userToken))
|
||||
{
|
||||
request.Headers.Add("X-User-Token", token);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
}
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(data);
|
||||
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
return JsonConvert.DeserializeObject<T>(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
|
||||
}
|
||||
catch (TaskCanceledException timeoutEx)
|
||||
{
|
||||
throw new Exception($"请求超时: {endpoint}", timeoutEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"发送请求时出错: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送DELETE请求
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteAsync(string endpoint, bool requireAuth = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
string token = null;
|
||||
if (requireAuth)
|
||||
{
|
||||
token = await GetAccessTokenAsync();
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
|
||||
|
||||
if (requireAuth && !string.IsNullOrEmpty(token))
|
||||
{
|
||||
// 如果是用户token,使用X-User-Token header
|
||||
if (!string.IsNullOrEmpty(_userToken))
|
||||
{
|
||||
request.Headers.Add("X-User-Token", token);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传笔记文件
|
||||
/// </summary>
|
||||
/// <param name="endpoint">上传端点</param>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="boardId">白板ID</param>
|
||||
/// <param name="secretKey">白板密钥</param>
|
||||
/// <param name="title">笔记标题(可选)</param>
|
||||
/// <param name="description">笔记描述(可选)</param>
|
||||
/// <param name="tags">笔记标签(可选)</param>
|
||||
public async Task<T> UploadNoteAsync<T>(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"文件不存在: {filePath}");
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||
|
||||
// 设置白板认证头
|
||||
request.Headers.Add("X-Board-ID", boardId);
|
||||
request.Headers.Add("X-Secret-Key", secretKey);
|
||||
|
||||
// 创建multipart/form-data内容
|
||||
var content = new MultipartFormDataContent();
|
||||
|
||||
// 添加文件
|
||||
var fileContent = new ByteArrayContent(File.ReadAllBytes(filePath));
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
content.Add(fileContent, "file", fileName);
|
||||
|
||||
// 添加可选参数
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
content.Add(new StringContent(title), "title");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(description))
|
||||
{
|
||||
content.Add(new StringContent(description), "description");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(tags))
|
||||
{
|
||||
content.Add(new StringContent(tags), "tags");
|
||||
}
|
||||
|
||||
request.Content = content;
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(responseContent))
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
return JsonConvert.DeserializeObject<T>(responseContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"上传文件失败: {response.StatusCode} - {responseContent}");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
throw new Exception($"上传文件时网络错误: {httpEx.Message}", httpEx);
|
||||
}
|
||||
catch (TaskCanceledException timeoutEx)
|
||||
{
|
||||
throw new Exception($"上传文件超时: {endpoint}", timeoutEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"上传文件时出错: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
}
|
||||
|
||||
#region 内部类
|
||||
|
||||
/// <summary>
|
||||
/// Token响应模型
|
||||
/// </summary>
|
||||
private class TokenResponse
|
||||
{
|
||||
[JsonProperty("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[JsonProperty("expires_in")]
|
||||
public int? ExpiresIn { get; set; }
|
||||
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,762 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Dlass笔记自动上传辅助类
|
||||
/// </summary>
|
||||
public class DlassNoteUploader
|
||||
{
|
||||
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
|
||||
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
|
||||
private const int BATCH_SIZE = 10; // 批量上传大小
|
||||
private const int MAX_RETRY_COUNT = 3; // 最大重试次数
|
||||
private const string QUEUE_FILE_NAME = "DlassUploadQueue.json";
|
||||
|
||||
/// <summary>
|
||||
/// 上传队列项
|
||||
/// </summary>
|
||||
private class UploadQueueItemData
|
||||
{
|
||||
[JsonProperty("file_path")]
|
||||
public string FilePath { get; set; }
|
||||
|
||||
[JsonProperty("retry_count")]
|
||||
public int RetryCount { get; set; }
|
||||
|
||||
[JsonProperty("added_time")]
|
||||
public DateTime AddedTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传队列项
|
||||
/// </summary>
|
||||
private class UploadQueueItem
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
public int RetryCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传队列
|
||||
/// </summary>
|
||||
private static readonly ConcurrentQueue<UploadQueueItem> _uploadQueue = new ConcurrentQueue<UploadQueueItem>();
|
||||
|
||||
/// <summary>
|
||||
/// 队列处理锁,防止并发处理
|
||||
/// </summary>
|
||||
private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// 队列保存锁,防止并发保存
|
||||
/// </summary>
|
||||
private static readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// 是否已初始化队列
|
||||
/// </summary>
|
||||
private static bool _isQueueInitialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取队列文件路径
|
||||
/// </summary>
|
||||
private static string GetQueueFilePath()
|
||||
{
|
||||
var configsDir = Path.Combine(App.RootPath, "Configs");
|
||||
if (!Directory.Exists(configsDir))
|
||||
{
|
||||
Directory.CreateDirectory(configsDir);
|
||||
}
|
||||
return Path.Combine(configsDir, QUEUE_FILE_NAME);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化上传队列
|
||||
/// </summary>
|
||||
public static void InitializeQueue()
|
||||
{
|
||||
if (_isQueueInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var queueFilePath = GetQueueFilePath();
|
||||
if (!File.Exists(queueFilePath))
|
||||
{
|
||||
_isQueueInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var jsonContent = File.ReadAllText(queueFilePath);
|
||||
if (string.IsNullOrWhiteSpace(jsonContent))
|
||||
{
|
||||
_isQueueInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var queueData = JsonConvert.DeserializeObject<List<UploadQueueItemData>>(jsonContent);
|
||||
if (queueData == null || queueData.Count == 0)
|
||||
{
|
||||
_isQueueInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int restoredCount = 0;
|
||||
int skippedCount = 0;
|
||||
|
||||
foreach (var item in queueData)
|
||||
{
|
||||
// 验证文件是否存在
|
||||
if (!File.Exists(item.FilePath))
|
||||
{
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 验证文件格式和大小
|
||||
var fileExtension = Path.GetExtension(item.FilePath).ToLower();
|
||||
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
|
||||
{
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(item.FilePath);
|
||||
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
|
||||
if (fileInfo.Length > maxSize)
|
||||
{
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 恢复队列项
|
||||
_uploadQueue.Enqueue(new UploadQueueItem
|
||||
{
|
||||
FilePath = item.FilePath,
|
||||
RetryCount = item.RetryCount
|
||||
});
|
||||
restoredCount++;
|
||||
}
|
||||
|
||||
_isQueueInitialized = true;
|
||||
|
||||
if (restoredCount > 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
|
||||
// 如果恢复了队列,触发处理
|
||||
_ = ProcessUploadQueueAsync();
|
||||
}
|
||||
else if (skippedCount > 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
_isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存队列到文件
|
||||
/// </summary>
|
||||
private static async Task SaveQueueToFileAsync()
|
||||
{
|
||||
if (!await _queueSaveLock.WaitAsync(1000)) // 最多等待1秒
|
||||
{
|
||||
return; // 如果无法获取锁,跳过保存(避免阻塞)
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var queueData = new List<UploadQueueItemData>();
|
||||
|
||||
// 将队列转换为可序列化的格式
|
||||
foreach (var item in _uploadQueue)
|
||||
{
|
||||
queueData.Add(new UploadQueueItemData
|
||||
{
|
||||
FilePath = item.FilePath,
|
||||
RetryCount = item.RetryCount,
|
||||
AddedTime = DateTime.Now
|
||||
});
|
||||
}
|
||||
|
||||
var queueFilePath = GetQueueFilePath();
|
||||
|
||||
// 如果队列为空,清空文件
|
||||
if (queueData.Count == 0)
|
||||
{
|
||||
ClearQueueFile();
|
||||
return;
|
||||
}
|
||||
|
||||
var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented);
|
||||
|
||||
// 使用临时文件写入,然后替换,确保原子性
|
||||
var tempFilePath = queueFilePath + ".tmp";
|
||||
File.WriteAllText(tempFilePath, jsonContent);
|
||||
|
||||
// 如果原文件存在,先删除
|
||||
if (File.Exists(queueFilePath))
|
||||
{
|
||||
File.Delete(queueFilePath);
|
||||
}
|
||||
|
||||
// 重命名临时文件
|
||||
File.Move(tempFilePath, queueFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_queueSaveLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空队列文件
|
||||
/// </summary>
|
||||
private static void ClearQueueFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
var queueFilePath = GetQueueFilePath();
|
||||
if (File.Exists(queueFilePath))
|
||||
{
|
||||
File.WriteAllText(queueFilePath, "[]");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传笔记响应模型
|
||||
/// </summary>
|
||||
public class UploadNoteResponse
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
[JsonProperty("note_id")]
|
||||
public int? NoteId { get; set; }
|
||||
|
||||
[JsonProperty("filename")]
|
||||
public string Filename { get; set; }
|
||||
|
||||
[JsonProperty("file_path")]
|
||||
public string FilePath { get; set; }
|
||||
|
||||
[JsonProperty("file_url")]
|
||||
public string FileUrl { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 白板信息模型(用于查找白板)
|
||||
/// </summary>
|
||||
private class WhiteboardInfo
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("board_id")]
|
||||
public string BoardId { get; set; }
|
||||
|
||||
[JsonProperty("secret_key")]
|
||||
public string SecretKey { get; set; }
|
||||
|
||||
[JsonProperty("class_name")]
|
||||
public string ClassName { get; set; }
|
||||
|
||||
[JsonProperty("class_id")]
|
||||
public int ClassId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 认证响应模型
|
||||
/// </summary>
|
||||
private class AuthWithTokenResponse
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonProperty("whiteboards")]
|
||||
public List<WhiteboardInfo> Whiteboards { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步上传笔记文件到Dlass(支持PNG、ICSTK、XML和ZIP格式)
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径(支持PNG、ICSTK、XML和ZIP)</param>
|
||||
/// <returns>是否成功加入队列(不等待实际上传完成)</returns>
|
||||
public static async Task<bool> UploadNoteFileAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查是否启用自动上传
|
||||
if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 基本验证
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
var fileExtension = Path.GetExtension(filePath).ToLower();
|
||||
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
|
||||
if (fileInfo.Length > maxSize)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取上传延迟时间(分钟)
|
||||
var delayMinutes = MainWindow.Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
|
||||
|
||||
// 如果设置了延迟时间,在后台任务中等待后再加入队列
|
||||
if (delayMinutes > 0)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
|
||||
EnqueueFile(filePath);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
EnqueueFile(filePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将文件加入上传队列
|
||||
/// </summary>
|
||||
private static void EnqueueFile(string filePath, int retryCount = 0)
|
||||
{
|
||||
_uploadQueue.Enqueue(new UploadQueueItem
|
||||
{
|
||||
FilePath = filePath,
|
||||
RetryCount = retryCount
|
||||
});
|
||||
|
||||
// 异步保存队列到文件
|
||||
_ = Task.Run(async () => await SaveQueueToFileAsync());
|
||||
|
||||
// 如果队列达到批量大小,触发批量上传
|
||||
if (_uploadQueue.Count >= BATCH_SIZE)
|
||||
{
|
||||
_ = ProcessUploadQueueAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理上传队列,批量上传文件
|
||||
/// </summary>
|
||||
private static async Task ProcessUploadQueueAsync()
|
||||
{
|
||||
// 使用信号量防止并发处理
|
||||
if (!await _queueProcessingLock.WaitAsync(0))
|
||||
{
|
||||
return; // 已有处理任务在运行
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filesToUpload = new List<UploadQueueItem>();
|
||||
|
||||
// 从队列中取出最多BATCH_SIZE个文件
|
||||
while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
|
||||
{
|
||||
// 再次检查文件是否存在
|
||||
if (File.Exists(item.FilePath))
|
||||
{
|
||||
filesToUpload.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToUpload.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取共享的白板信息(同一批次的所有文件共享认证信息)
|
||||
WhiteboardInfo sharedWhiteboard = null;
|
||||
string apiBaseUrl = null;
|
||||
string userToken = null;
|
||||
|
||||
try
|
||||
{
|
||||
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
|
||||
if (string.IsNullOrEmpty(selectedClassName))
|
||||
{
|
||||
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
|
||||
// 将文件重新加入队列
|
||||
foreach (var item in filesToUpload)
|
||||
{
|
||||
EnqueueFile(item.FilePath, item.RetryCount);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
userToken = MainWindow.Settings?.Dlass?.UserToken;
|
||||
if (string.IsNullOrEmpty(userToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
|
||||
// 将文件重新加入队列
|
||||
foreach (var item in filesToUpload)
|
||||
{
|
||||
EnqueueFile(item.FilePath, item.RetryCount);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
|
||||
|
||||
// 获取白板信息(只获取一次,所有文件共享)
|
||||
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
|
||||
{
|
||||
var authData = new
|
||||
{
|
||||
app_id = APP_ID,
|
||||
app_secret = APP_SECRET,
|
||||
user_token = userToken
|
||||
};
|
||||
|
||||
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
|
||||
|
||||
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
|
||||
// 将文件重新加入队列
|
||||
foreach (var item in filesToUpload)
|
||||
{
|
||||
EnqueueFile(item.FilePath, item.RetryCount);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sharedWhiteboard = authResult.Whiteboards
|
||||
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
|
||||
|
||||
if (sharedWhiteboard == null || string.IsNullOrEmpty(sharedWhiteboard.BoardId) || string.IsNullOrEmpty(sharedWhiteboard.SecretKey))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
|
||||
// 将文件重新加入队列
|
||||
foreach (var item in filesToUpload)
|
||||
{
|
||||
EnqueueFile(item.FilePath, item.RetryCount);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
// 将文件重新加入队列
|
||||
foreach (var item in filesToUpload)
|
||||
{
|
||||
EnqueueFile(item.FilePath, item.RetryCount);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发上传所有文件(共享白板信息),并处理失败重试
|
||||
var uploadTasks = filesToUpload.Select(async item =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await UploadFileInternalAsync(item.FilePath, sharedWhiteboard, apiBaseUrl, userToken);
|
||||
if (!success)
|
||||
{
|
||||
// 检查是否是可重试的错误
|
||||
if (IsRetryableError(item.FilePath))
|
||||
{
|
||||
// 检查重试次数
|
||||
if (item.RetryCount < MAX_RETRY_COUNT)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
|
||||
EnqueueFile(item.FilePath, item.RetryCount + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 检查是否是可重试的错误(超时、网络错误等)
|
||||
var errorMessage = ex.Message.ToLower();
|
||||
bool isRetryable = errorMessage.Contains("超时") ||
|
||||
errorMessage.Contains("timeout") ||
|
||||
errorMessage.Contains("网络错误") ||
|
||||
errorMessage.Contains("network");
|
||||
|
||||
if (isRetryable && IsRetryableError(item.FilePath))
|
||||
{
|
||||
// 检查重试次数
|
||||
if (item.RetryCount < MAX_RETRY_COUNT)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
|
||||
EnqueueFile(item.FilePath, item.RetryCount + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
await Task.WhenAll(uploadTasks);
|
||||
|
||||
// 上传完成后保存队列状态
|
||||
await SaveQueueToFileAsync();
|
||||
|
||||
// 如果队列达到批量大小,继续处理
|
||||
if (_uploadQueue.Count >= BATCH_SIZE)
|
||||
{
|
||||
_ = ProcessUploadQueueAsync();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_queueProcessingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部上传方法,执行实际上传操作
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="whiteboard">白板信息(如果为null则重新获取)</param>
|
||||
/// <param name="apiBaseUrl">API基础URL(如果为null则从设置获取)</param>
|
||||
/// <param name="userToken">用户Token(如果为null则从设置获取)</param>
|
||||
private static async Task<bool> UploadFileInternalAsync(string filePath, WhiteboardInfo whiteboard = null, string apiBaseUrl = null, string userToken = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再次检查文件是否存在(可能在队列等待时被删除)
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
var fileExtension = Path.GetExtension(filePath).ToLower();
|
||||
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件大小(最大10MB,ZIP文件可能更大,允许50MB)
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
|
||||
if (fileInfo.Length > maxSize)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果白板信息未提供,则重新获取
|
||||
if (whiteboard == null)
|
||||
{
|
||||
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
|
||||
if (string.IsNullOrEmpty(selectedClassName))
|
||||
{
|
||||
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
|
||||
if (string.IsNullOrEmpty(userToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
|
||||
|
||||
// 创建API客户端并获取白板信息
|
||||
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
|
||||
{
|
||||
var authData = new
|
||||
{
|
||||
app_id = APP_ID,
|
||||
app_secret = APP_SECRET,
|
||||
user_token = userToken
|
||||
};
|
||||
|
||||
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
|
||||
|
||||
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查找匹配班级的白板
|
||||
whiteboard = authResult.Whiteboards
|
||||
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
|
||||
|
||||
if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取API基础URL和用户Token(如果未提供)
|
||||
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
|
||||
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
|
||||
|
||||
// 准备上传参数
|
||||
var fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||
var title = fileName;
|
||||
string fileType;
|
||||
string tags;
|
||||
if (fileExtension == ".zip")
|
||||
{
|
||||
fileType = "多页面墨迹压缩包";
|
||||
tags = "自动上传,多页面,zip,压缩包";
|
||||
}
|
||||
else if (fileExtension == ".icstk")
|
||||
{
|
||||
fileType = "墨迹文件";
|
||||
tags = "自动上传,墨迹,icstk";
|
||||
}
|
||||
else if (fileExtension == ".xml")
|
||||
{
|
||||
fileType = "XML文件";
|
||||
tags = "自动上传,xml";
|
||||
}
|
||||
else
|
||||
{
|
||||
fileType = "笔记";
|
||||
tags = "自动上传,笔记,png";
|
||||
}
|
||||
var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
|
||||
|
||||
// 创建API客户端并上传文件
|
||||
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
|
||||
{
|
||||
var uploadResult = await apiClient.UploadNoteAsync<UploadNoteResponse>(
|
||||
"/api/whiteboard/upload_note",
|
||||
filePath,
|
||||
whiteboard.BoardId,
|
||||
whiteboard.SecretKey,
|
||||
title,
|
||||
description,
|
||||
tags);
|
||||
|
||||
if (uploadResult != null && uploadResult.Success)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"笔记上传成功:{fileName} -> {uploadResult.FileUrl}", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"上传失败:服务器响应失败 - {uploadResult?.Message ?? "未知错误"}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误信息,抛出异常以便调用方判断是否可重试
|
||||
LogHelper.WriteLogToFile($"上传笔记时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断错误是否可重试(超时、网络错误等)
|
||||
/// </summary>
|
||||
private static bool IsRetryableError(string filePath)
|
||||
{
|
||||
// 检查文件是否存在
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return false; // 文件不存在,不可重试
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
var fileExtension = Path.GetExtension(filePath).ToLower();
|
||||
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
|
||||
{
|
||||
return false; // 文件格式错误,不可重试
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
|
||||
if (fileInfo.Length > maxSize)
|
||||
{
|
||||
return false; // 文件过大,不可重试
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false; // 无法读取文件信息,不可重试
|
||||
}
|
||||
|
||||
// 其他错误(超时、网络错误等)可以重试
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件关联管理器,用于注册和处理.icstk文件的关联
|
||||
/// </summary>
|
||||
public static class FileAssociationManager
|
||||
{
|
||||
private const string FileExtension = ".icstk";
|
||||
private const string FileTypeName = "InkCanvasStrokesFile";
|
||||
private const string AppName = "Ink Canvas";
|
||||
private const string AppDescription = "Ink Canvas Strokes File";
|
||||
|
||||
// IPC相关常量
|
||||
private const string IpcMutexName = "InkCanvasFileAssociationIpc";
|
||||
private const string IpcEventName = "InkCanvasFileAssociationEvent";
|
||||
private const string IpcFilePrefix = "InkCanvasFileAssociation_";
|
||||
private const string IpcBoardModePrefix = "InkCanvasBoardMode_";
|
||||
private const string IpcShowModePrefix = "InkCanvasShowMode_";
|
||||
private const int IpcTimeout = 5000; // 5秒超时
|
||||
|
||||
/// <summary>
|
||||
/// 注册.icstk文件关联
|
||||
/// </summary>
|
||||
public static bool RegisterFileAssociation()
|
||||
{
|
||||
try
|
||||
{
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
|
||||
// 注册文件类型
|
||||
using (RegistryKey fileTypeKey = Registry.ClassesRoot.CreateSubKey(FileTypeName))
|
||||
{
|
||||
fileTypeKey.SetValue("", AppDescription);
|
||||
fileTypeKey.SetValue("FriendlyTypeName", AppDescription);
|
||||
|
||||
// 设置默认图标
|
||||
using (RegistryKey defaultIconKey = fileTypeKey.CreateSubKey("DefaultIcon"))
|
||||
{
|
||||
defaultIconKey.SetValue("", $"\"{exePath}\",0");
|
||||
}
|
||||
|
||||
// 设置打开命令
|
||||
using (RegistryKey shellKey = fileTypeKey.CreateSubKey("shell"))
|
||||
using (RegistryKey openKey = shellKey.CreateSubKey("open"))
|
||||
using (RegistryKey commandKey = openKey.CreateSubKey("command"))
|
||||
{
|
||||
commandKey.SetValue("", $"\"{exePath}\" \"%1\"");
|
||||
}
|
||||
}
|
||||
|
||||
// 注册文件扩展名
|
||||
using (RegistryKey extensionKey = Registry.ClassesRoot.CreateSubKey(FileExtension))
|
||||
{
|
||||
extensionKey.SetValue("", FileTypeName);
|
||||
}
|
||||
|
||||
// 刷新系统文件关联缓存
|
||||
RefreshSystemFileAssociations();
|
||||
|
||||
LogHelper.WriteLogToFile($"成功注册{FileExtension}文件关联", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (SecurityException ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册文件关联时权限不足: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册文件关联时访问被拒绝: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注销.icstk文件关联
|
||||
/// </summary>
|
||||
public static bool UnregisterFileAssociation()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 删除文件扩展名关联
|
||||
Registry.ClassesRoot.DeleteSubKeyTree(FileExtension, false);
|
||||
|
||||
// 删除文件类型定义
|
||||
Registry.ClassesRoot.DeleteSubKeyTree(FileTypeName, false);
|
||||
|
||||
// 刷新系统文件关联缓存
|
||||
RefreshSystemFileAssociations();
|
||||
|
||||
LogHelper.WriteLogToFile($"成功注销{FileExtension}文件关联", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注销文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查文件关联是否已注册
|
||||
/// </summary>
|
||||
public static bool IsFileAssociationRegistered()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (RegistryKey extensionKey = Registry.ClassesRoot.OpenSubKey(FileExtension))
|
||||
{
|
||||
if (extensionKey == null) return false;
|
||||
|
||||
string fileType = extensionKey.GetValue("") as string;
|
||||
if (string.IsNullOrEmpty(fileType)) return false;
|
||||
|
||||
using (RegistryKey fileTypeKey = Registry.ClassesRoot.OpenSubKey(fileType))
|
||||
{
|
||||
if (fileTypeKey == null) return false;
|
||||
|
||||
using (RegistryKey shellKey = fileTypeKey.OpenSubKey("shell\\open\\command"))
|
||||
{
|
||||
if (shellKey == null) return false;
|
||||
|
||||
string command = shellKey.GetValue("") as string;
|
||||
if (string.IsNullOrEmpty(command)) return false;
|
||||
|
||||
// 检查命令是否指向当前应用程序
|
||||
string currentExePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
return command.Contains(currentExePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"检查文件关联状态时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示文件关联状态
|
||||
/// </summary>
|
||||
public static void ShowFileAssociationStatus()
|
||||
{
|
||||
bool isRegistered = IsFileAssociationRegistered();
|
||||
LogHelper.WriteLogToFile($"{FileExtension}文件关联状态: {(isRegistered ? "已注册" : "未注册")}", LogHelper.LogType.Event);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新系统文件关联缓存
|
||||
/// </summary>
|
||||
private static void RefreshSystemFileAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 通知系统文件关联已更改
|
||||
SHChangeNotify(0x08000000, 0, IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"刷新文件关联缓存时出错: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理命令行参数中的文件路径
|
||||
/// </summary>
|
||||
/// <param name="args">命令行参数</param>
|
||||
/// <returns>找到的.icstk文件路径,如果没有找到则返回null</returns>
|
||||
public static string GetIcstkFileFromArgs(string[] args)
|
||||
{
|
||||
if (args == null || args.Length == 0) return null;
|
||||
|
||||
foreach (string arg in args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(arg)) continue;
|
||||
|
||||
// 检查是否为.icstk文件
|
||||
if (Path.GetExtension(arg).Equals(FileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 检查文件是否存在
|
||||
if (File.Exists(arg))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从命令行参数中找到.icstk文件: {arg}", LogHelper.LogType.Event);
|
||||
return arg;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"命令行参数中的.icstk文件不存在: {arg}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过IPC将文件路径发送给已运行的实例
|
||||
/// </summary>
|
||||
/// <param name="filePath">要打开的文件路径</param>
|
||||
/// <returns>是否成功发送</returns>
|
||||
public static bool TrySendFileToExistingInstance(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"尝试通过IPC发送文件路径给已运行实例: {filePath}", LogHelper.LogType.Event);
|
||||
|
||||
// 创建IPC文件
|
||||
string tempDir = Path.GetTempPath();
|
||||
string ipcFileName = IpcFilePrefix + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
|
||||
|
||||
// 写入文件路径到IPC文件
|
||||
File.WriteAllText(ipcFilePath, filePath, Encoding.UTF8);
|
||||
|
||||
// 创建事件通知已运行实例
|
||||
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
|
||||
{
|
||||
ipcEvent.Set();
|
||||
}
|
||||
|
||||
// 等待一段时间让已运行实例处理文件
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// 清理IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFilePath))
|
||||
{
|
||||
File.Delete(ipcFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("IPC文件路径发送完成", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"通过IPC发送文件路径失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过IPC将白板模式命令发送给已运行的实例
|
||||
/// </summary>
|
||||
/// <returns>是否成功发送</returns>
|
||||
public static bool TrySendBoardModeCommandToExistingInstance()
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("尝试通过IPC发送白板模式命令给已运行实例", LogHelper.LogType.Event);
|
||||
|
||||
// 创建IPC文件
|
||||
string tempDir = Path.GetTempPath();
|
||||
string ipcFileName = IpcBoardModePrefix + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
|
||||
|
||||
// 写入白板模式命令到IPC文件
|
||||
File.WriteAllText(ipcFilePath, "BOARD_MODE", Encoding.UTF8);
|
||||
|
||||
// 创建事件通知已运行实例
|
||||
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
|
||||
{
|
||||
ipcEvent.Set();
|
||||
}
|
||||
|
||||
// 等待一段时间让已运行实例处理命令
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// 清理IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFilePath))
|
||||
{
|
||||
File.Delete(ipcFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("IPC白板模式命令发送完成", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"通过IPC发送白板模式命令失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过IPC将展开浮动栏命令发送给已运行的实例
|
||||
/// </summary>
|
||||
/// <returns>是否成功发送</returns>
|
||||
public static bool TrySendShowModeCommandToExistingInstance()
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("尝试通过IPC发送展开浮动栏命令给已运行实例", LogHelper.LogType.Event);
|
||||
|
||||
// 创建IPC文件
|
||||
string tempDir = Path.GetTempPath();
|
||||
string ipcFileName = IpcShowModePrefix + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
|
||||
|
||||
// 写入展开浮动栏命令到IPC文件
|
||||
File.WriteAllText(ipcFilePath, "SHOW_MODE", Encoding.UTF8);
|
||||
|
||||
// 创建事件通知已运行实例
|
||||
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
|
||||
{
|
||||
ipcEvent.Set();
|
||||
}
|
||||
|
||||
// 等待一段时间让已运行实例处理命令
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// 清理IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFilePath))
|
||||
{
|
||||
File.Delete(ipcFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("IPC展开浮动栏命令发送完成", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"通过IPC发送展开浮动栏命令失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动IPC监听器,等待其他实例发送文件路径
|
||||
/// </summary>
|
||||
public static void StartIpcListener()
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread ipcThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("启动IPC监听器", LogHelper.LogType.Event);
|
||||
|
||||
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// 等待IPC事件
|
||||
if (ipcEvent.WaitOne(IpcTimeout))
|
||||
{
|
||||
// 处理IPC文件
|
||||
ProcessIpcFiles();
|
||||
|
||||
// 重置事件
|
||||
ipcEvent.Reset();
|
||||
}
|
||||
|
||||
// 检查应用是否还在运行
|
||||
if (Application.Current == null || Application.Current.Dispatcher == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC监听器出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcThread.IsBackground = true;
|
||||
ipcThread.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动IPC监听器失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理IPC文件
|
||||
/// </summary>
|
||||
private static void ProcessIpcFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
string tempDir = Path.GetTempPath();
|
||||
|
||||
// 处理文件路径IPC文件
|
||||
string[] ipcFiles = Directory.GetFiles(tempDir, IpcFilePrefix + "*.tmp");
|
||||
foreach (string ipcFile in ipcFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取文件路径
|
||||
string filePath = File.ReadAllText(ipcFile, Encoding.UTF8);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC接收到文件路径: {filePath}", LogHelper.LogType.Event);
|
||||
|
||||
// 在UI线程中处理文件打开
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取主窗口并打开文件
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.OpenSingleStrokeFile(filePath);
|
||||
mainWindow.ShowNotification($"已加载墨迹文件: {Path.GetFileName(filePath)}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC处理文件打开失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 删除IPC文件
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
|
||||
// 尝试删除损坏的IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFile))
|
||||
{
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// 处理白板模式命令IPC文件
|
||||
string[] boardModeFiles = Directory.GetFiles(tempDir, IpcBoardModePrefix + "*.tmp");
|
||||
foreach (string ipcFile in boardModeFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取命令内容
|
||||
string command = File.ReadAllText(ipcFile, Encoding.UTF8);
|
||||
|
||||
if (command == "BOARD_MODE")
|
||||
{
|
||||
LogHelper.WriteLogToFile("IPC接收到白板模式命令", LogHelper.LogType.Event);
|
||||
|
||||
// 在UI线程中处理白板模式切换
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取主窗口并切换到白板模式
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.SwitchToBoardMode();
|
||||
mainWindow.ShowNotification("已切换到白板模式");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC处理白板模式切换失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 删除IPC文件
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理白板模式IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
|
||||
// 尝试删除损坏的IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFile))
|
||||
{
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// 处理展开浮动栏命令IPC文件
|
||||
string[] showModeFiles = Directory.GetFiles(tempDir, IpcShowModePrefix + "*.tmp");
|
||||
foreach (string ipcFile in showModeFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取命令内容
|
||||
string command = File.ReadAllText(ipcFile, Encoding.UTF8);
|
||||
|
||||
if (command == "SHOW_MODE")
|
||||
{
|
||||
LogHelper.WriteLogToFile("IPC接收到展开浮动栏命令", LogHelper.LogType.Event);
|
||||
|
||||
// 在UI线程中处理展开浮动栏
|
||||
Application.Current.Dispatcher.BeginInvoke(new Action(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取主窗口并展开浮动栏
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
// 如果当前处于收纳模式,则展开浮动栏
|
||||
if (mainWindow.isFloatingBarFolded)
|
||||
{
|
||||
await mainWindow.UnFoldFloatingBar(new object());
|
||||
}
|
||||
mainWindow.ShowNotification("已退出收纳模式并恢复浮动栏");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"IPC处理展开浮动栏失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 删除IPC文件
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理展开浮动栏IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
|
||||
// 尝试删除损坏的IPC文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(ipcFile))
|
||||
{
|
||||
File.Delete(ipcFile);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理IPC文件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("shell32.dll")]
|
||||
private static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,24 @@ namespace Ink_Canvas.Helpers
|
||||
public int Height => Bottom - Top;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MONITORINFO
|
||||
{
|
||||
public uint cbSize;
|
||||
public RECT rcMonitor;
|
||||
public RECT rcWork;
|
||||
public uint dwFlags;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromRect(ref RECT lprc, uint dwFlags);
|
||||
|
||||
public static string WindowTitle()
|
||||
{
|
||||
IntPtr foregroundWindowHandle = GetForegroundWindow();
|
||||
@@ -106,10 +124,28 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
public static double GetTaskbarHeight(Screen screen, double dpiScaleY)
|
||||
{
|
||||
// 获取工作区和屏幕高度的差值
|
||||
var workingArea = screen.WorkingArea;
|
||||
var bounds = screen.Bounds;
|
||||
int taskbarHeight = bounds.Height - workingArea.Height;
|
||||
// 创建RECT结构体表示屏幕边界
|
||||
RECT screenRect = new RECT
|
||||
{
|
||||
Left = screen.Bounds.Left,
|
||||
Top = screen.Bounds.Top,
|
||||
Right = screen.Bounds.Right,
|
||||
Bottom = screen.Bounds.Bottom
|
||||
};
|
||||
|
||||
// 获取屏幕句柄
|
||||
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
|
||||
IntPtr hMonitor = MonitorFromRect(ref screenRect, MONITOR_DEFAULTTONEAREST);
|
||||
|
||||
// 初始化MONITORINFO结构体
|
||||
MONITORINFO monitorInfo = new MONITORINFO();
|
||||
monitorInfo.cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFO));
|
||||
|
||||
// 获取监视器信息
|
||||
GetMonitorInfo(hMonitor, ref monitorInfo);
|
||||
|
||||
// 计算任务栏高度:monitorInfo.rcMonitor.bottom减去monitorInfo.rcWork.bottom的值
|
||||
int taskbarHeight = monitorInfo.rcMonitor.Bottom - monitorInfo.rcWork.Bottom;
|
||||
// 考虑 DPI 缩放
|
||||
return taskbarHeight / dpiScaleY;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
using NHotkey.Wpf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Input;
|
||||
using Newtonsoft.Json;
|
||||
using NHotkey.Wpf;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
@@ -19,9 +21,19 @@ namespace Ink_Canvas.Helpers
|
||||
private readonly MainWindow _mainWindow;
|
||||
private bool _isDisposed;
|
||||
private bool _hotkeysShouldBeRegistered = true; // 启动时注册热键
|
||||
|
||||
|
||||
// 多屏幕支持相关字段
|
||||
private Screen _currentScreen;
|
||||
private bool _isMultiScreenMode = false;
|
||||
private bool _enableScreenSpecificHotkeys = true; // 是否启用基于屏幕的热键注册
|
||||
|
||||
// 智能热键管理相关字段
|
||||
private bool _isWindowFocused = false;
|
||||
private bool _isMouseOverWindow = false;
|
||||
private System.Windows.Threading.DispatcherTimer _mousePositionTimer;
|
||||
|
||||
// 配置文件路径
|
||||
private static readonly string HotkeyConfigFile = Path.Combine(App.RootPath, "HotkeyConfig.json");
|
||||
private static readonly string HotkeyConfigFile = Path.Combine(App.RootPath, "Configs", "HotkeyConfig.json");
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
@@ -30,6 +42,12 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow));
|
||||
_registeredHotkeys = new Dictionary<string, HotkeyInfo>();
|
||||
_hotkeysShouldBeRegistered = true; // 启动时注册热键
|
||||
|
||||
// 初始化多屏幕支持
|
||||
InitializeMultiScreenSupport();
|
||||
|
||||
// 启动时确保配置文件存在
|
||||
EnsureConfigFileExists();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -49,11 +67,27 @@ namespace Ink_Canvas.Helpers
|
||||
if (_isDisposed)
|
||||
return false;
|
||||
|
||||
// 检查是否应该注册热键(基于屏幕和模式)
|
||||
if (!ShouldRegisterHotkeys())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果快捷键已存在,先注销
|
||||
if (_registeredHotkeys.ContainsKey(hotkeyName))
|
||||
{
|
||||
UnregisterHotkey(hotkeyName);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
HotkeyManager.Current.Remove(hotkeyName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// 创建快捷键信息
|
||||
var hotkeyInfo = new HotkeyInfo
|
||||
@@ -82,12 +116,14 @@ namespace Ink_Canvas.Helpers
|
||||
});
|
||||
|
||||
_registeredHotkeys[hotkeyName] = hotkeyInfo;
|
||||
// 成功注册全局快捷键
|
||||
|
||||
// 记录注册信息
|
||||
var screenInfo = _isMultiScreenMode ? $" (屏幕: {_currentScreen?.DeviceName})" : "";
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册全局快捷键 {hotkeyName} 失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -176,7 +212,6 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
if (!File.Exists(HotkeyConfigFile))
|
||||
{
|
||||
LogHelper.WriteLogToFile("快捷键配置文件不存在");
|
||||
return new List<HotkeyInfo>();
|
||||
}
|
||||
|
||||
@@ -209,7 +244,6 @@ namespace Ink_Canvas.Helpers
|
||||
});
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"从配置文件读取到 {hotkeyList.Count} 个快捷键信息");
|
||||
return hotkeyList;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -227,7 +261,7 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
// 开始注册默认快捷键集合
|
||||
|
||||
|
||||
// 基本操作快捷键
|
||||
RegisterHotkey("Undo", Key.Z, ModifierKeys.Control, () => _mainWindow.SymbolIconUndo_MouseUp(null, null));
|
||||
RegisterHotkey("Redo", Key.Y, ModifierKeys.Control, () => _mainWindow.SymbolIconRedo_MouseUp(null, null));
|
||||
@@ -272,36 +306,35 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
// 开始从配置文件加载快捷键设置
|
||||
|
||||
|
||||
// 检查是否应该注册快捷键
|
||||
if (!_hotkeysShouldBeRegistered)
|
||||
{
|
||||
// 当前状态不允许注册快捷键,跳过加载
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 如果配置文件不存在,先创建默认配置文件
|
||||
if (!File.Exists(HotkeyConfigFile))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"快捷键配置文件不存在: {HotkeyConfigFile}", LogHelper.LogType.Warning);
|
||||
CreateDefaultConfigFile();
|
||||
RegisterDefaultHotkeys();
|
||||
_hotkeysShouldBeRegistered = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试从配置文件加载
|
||||
if (LoadHotkeysFromConfigFile())
|
||||
{
|
||||
// 成功从配置文件加载快捷键设置
|
||||
_hotkeysShouldBeRegistered = true;
|
||||
LogHelper.WriteLogToFile("成功从配置文件加载快捷键设置");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果配置文件不存在或加载失败,使用默认快捷键
|
||||
if (!File.Exists(HotkeyConfigFile))
|
||||
{
|
||||
LogHelper.WriteLogToFile("配置文件不存在,注册默认快捷键");
|
||||
RegisterDefaultHotkeys();
|
||||
_hotkeysShouldBeRegistered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile("配置文件存在但加载失败,回退到默认快捷键", LogHelper.LogType.Warning);
|
||||
RegisterDefaultHotkeys();
|
||||
_hotkeysShouldBeRegistered = true;
|
||||
}
|
||||
LogHelper.WriteLogToFile("配置文件存在但加载失败,回退到默认快捷键", LogHelper.LogType.Warning);
|
||||
RegisterDefaultHotkeys();
|
||||
_hotkeysShouldBeRegistered = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -318,11 +351,9 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("开始保存快捷键配置到配置文件", LogHelper.LogType.Event);
|
||||
|
||||
|
||||
if (SaveHotkeysToConfigFile())
|
||||
{
|
||||
LogHelper.WriteLogToFile("快捷键配置已成功保存到配置文件", LogHelper.LogType.Event);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -346,16 +377,28 @@ namespace Ink_Canvas.Helpers
|
||||
if (!_hotkeysShouldBeRegistered)
|
||||
{
|
||||
_hotkeysShouldBeRegistered = true;
|
||||
LogHelper.WriteLogToFile("启用快捷键注册功能");
|
||||
|
||||
// 立即加载快捷键设置
|
||||
LoadHotkeysFromSettings();
|
||||
|
||||
// 启动鼠标位置监控定时器
|
||||
if (_isMultiScreenMode && _enableScreenSpecificHotkeys && _mousePositionTimer != null)
|
||||
{
|
||||
_mousePositionTimer.Start();
|
||||
}
|
||||
|
||||
// 根据上下文决定是否立即加载快捷键
|
||||
if (ShouldEnableHotkeysBasedOnContext())
|
||||
{
|
||||
LoadHotkeysFromSettings();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile("快捷键注册功能已经启用,重新加载快捷键设置");
|
||||
// 即使已经启用,也要重新加载快捷键设置以确保快捷键正常工作
|
||||
LoadHotkeysFromSettings();
|
||||
if (_registeredHotkeys.Count == 0)
|
||||
{
|
||||
if (ShouldEnableHotkeysBasedOnContext())
|
||||
{
|
||||
LoadHotkeysFromSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -375,14 +418,18 @@ namespace Ink_Canvas.Helpers
|
||||
if (_hotkeysShouldBeRegistered)
|
||||
{
|
||||
_hotkeysShouldBeRegistered = false;
|
||||
LogHelper.WriteLogToFile("禁用快捷键注册功能");
|
||||
|
||||
|
||||
// 停止鼠标位置监控定时器
|
||||
if (_mousePositionTimer != null && _mousePositionTimer.IsEnabled)
|
||||
{
|
||||
_mousePositionTimer.Stop();
|
||||
}
|
||||
|
||||
// 注销所有快捷键
|
||||
UnregisterAllHotkeys();
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile("快捷键注册功能已经禁用");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -402,15 +449,32 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
if (isMouseMode)
|
||||
{
|
||||
// 鼠标模式下禁用快捷键,让键盘操作放行
|
||||
DisableHotkeyRegistration();
|
||||
LogHelper.WriteLogToFile("切换到鼠标模式,禁用快捷键以放行键盘操作");
|
||||
// 检查设置中是否允许在鼠标模式下启用快捷键
|
||||
if (MainWindow.Settings.Appearance.EnableHotkeysInMouseMode)
|
||||
{
|
||||
// 如果设置允许,则在鼠标模式下也启用快捷键
|
||||
EnableHotkeyRegistration();
|
||||
|
||||
if (_hotkeysShouldBeRegistered && _registeredHotkeys.Count == 0)
|
||||
{
|
||||
LoadHotkeysFromSettings();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 鼠标模式下禁用快捷键,让键盘操作放行
|
||||
DisableHotkeyRegistration();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非鼠标模式下启用快捷键
|
||||
EnableHotkeyRegistration();
|
||||
LogHelper.WriteLogToFile("切换到非鼠标模式,启用快捷键");
|
||||
|
||||
if (_hotkeysShouldBeRegistered && _registeredHotkeys.Count == 0)
|
||||
{
|
||||
LoadHotkeysFromSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -438,20 +502,19 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
// 获取原有的动作
|
||||
var originalAction = _registeredHotkeys[hotkeyName].Action;
|
||||
|
||||
|
||||
// 注销原有快捷键
|
||||
UnregisterHotkey(hotkeyName);
|
||||
|
||||
|
||||
// 注册新的快捷键
|
||||
var success = RegisterHotkey(hotkeyName, key, modifiers, originalAction);
|
||||
|
||||
|
||||
if (success)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"成功更新快捷键 {hotkeyName}: {modifiers}+{key}", LogHelper.LogType.Event);
|
||||
// 自动保存配置
|
||||
SaveHotkeysToSettings();
|
||||
}
|
||||
|
||||
|
||||
return success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -460,9 +523,424 @@ namespace Ink_Canvas.Helpers
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用基于屏幕的热键注册
|
||||
/// </summary>
|
||||
public void EnableScreenSpecificHotkeys()
|
||||
{
|
||||
try
|
||||
{
|
||||
_enableScreenSpecificHotkeys = true;
|
||||
|
||||
// 如果当前在多屏幕环境下,刷新热键注册
|
||||
if (_isMultiScreenMode)
|
||||
{
|
||||
RefreshHotkeysForCurrentScreen();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启用基于屏幕的热键注册时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用基于屏幕的热键注册
|
||||
/// </summary>
|
||||
public void DisableScreenSpecificHotkeys()
|
||||
{
|
||||
try
|
||||
{
|
||||
_enableScreenSpecificHotkeys = false;
|
||||
|
||||
// 重新注册热键(全局模式)
|
||||
if (_hotkeysShouldBeRegistered)
|
||||
{
|
||||
RefreshHotkeysForCurrentScreen();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"禁用基于屏幕的热键注册时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前屏幕信息
|
||||
/// </summary>
|
||||
/// <returns>当前屏幕信息</returns>
|
||||
public string GetCurrentScreenInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_isMultiScreenMode && _currentScreen != null)
|
||||
{
|
||||
return $"多屏幕环境 - 当前屏幕: {_currentScreen.DeviceName} ({_currentScreen.Bounds.Width}x{_currentScreen.Bounds.Height})";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "单屏幕环境";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取当前屏幕信息时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return "无法获取屏幕信息";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否启用了基于屏幕的热键注册
|
||||
/// </summary>
|
||||
/// <returns>是否启用</returns>
|
||||
public bool IsScreenSpecificHotkeysEnabled()
|
||||
{
|
||||
return _enableScreenSpecificHotkeys && _isMultiScreenMode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动刷新当前屏幕的热键注册
|
||||
/// </summary>
|
||||
public void RefreshCurrentScreenHotkeys()
|
||||
{
|
||||
try
|
||||
{
|
||||
RefreshHotkeysForCurrentScreen();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"刷新当前屏幕热键时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Helper Methods
|
||||
/// <summary>
|
||||
/// 初始化多屏幕支持
|
||||
/// </summary>
|
||||
private void InitializeMultiScreenSupport()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检测是否有多个屏幕
|
||||
_isMultiScreenMode = ScreenDetectionHelper.HasMultipleScreens();
|
||||
|
||||
if (_isMultiScreenMode)
|
||||
{
|
||||
// 获取当前窗口所在的屏幕
|
||||
_currentScreen = ScreenDetectionHelper.GetWindowScreen(_mainWindow);
|
||||
|
||||
// 监听窗口位置变化事件
|
||||
_mainWindow.LocationChanged += OnWindowLocationChanged;
|
||||
|
||||
// 初始化智能热键管理
|
||||
InitializeSmartHotkeyManagement();
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentScreen = ScreenDetectionHelper.GetPrimaryScreen();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化多屏幕支持时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
_isMultiScreenMode = false;
|
||||
_currentScreen = ScreenDetectionHelper.GetPrimaryScreen();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化智能热键管理
|
||||
/// </summary>
|
||||
private void InitializeSmartHotkeyManagement()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 监听窗口焦点事件
|
||||
_mainWindow.GotFocus += OnWindowGotFocus;
|
||||
_mainWindow.LostFocus += OnWindowLostFocus;
|
||||
|
||||
// 监听鼠标进入/离开事件
|
||||
_mainWindow.MouseEnter += OnMouseEnterWindow;
|
||||
_mainWindow.MouseLeave += OnMouseLeaveWindow;
|
||||
|
||||
// 初始化鼠标位置监控定时器
|
||||
_mousePositionTimer = new System.Windows.Threading.DispatcherTimer();
|
||||
_mousePositionTimer.Interval = TimeSpan.FromMilliseconds(500); // 每500ms检查一次
|
||||
_mousePositionTimer.Tick += OnMousePositionTimerTick;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化热键管理时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口位置变化事件处理
|
||||
/// </summary>
|
||||
private void OnWindowLocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
|
||||
return;
|
||||
|
||||
var newScreen = ScreenDetectionHelper.GetWindowScreen(_mainWindow);
|
||||
if (newScreen != null && newScreen != _currentScreen)
|
||||
{
|
||||
_currentScreen = newScreen;
|
||||
|
||||
// 重新注册热键以适应新屏幕
|
||||
RefreshHotkeysForCurrentScreen();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理窗口位置变化时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为当前屏幕刷新热键注册
|
||||
/// </summary>
|
||||
private void RefreshHotkeysForCurrentScreen()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_hotkeysShouldBeRegistered)
|
||||
return;
|
||||
|
||||
// 注销所有现有热键
|
||||
UnregisterAllHotkeys();
|
||||
|
||||
// 重新注册热键
|
||||
LoadHotkeysFromSettings();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"刷新当前屏幕热键时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口获得焦点事件处理
|
||||
/// </summary>
|
||||
private void OnWindowGotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_isWindowFocused = true;
|
||||
UpdateHotkeyStateBasedOnContext();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理窗口获得焦点事件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口失去焦点事件处理
|
||||
/// </summary>
|
||||
private void OnWindowLostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_isWindowFocused = false;
|
||||
UpdateHotkeyStateBasedOnContext();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理窗口失去焦点事件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标进入窗口事件处理
|
||||
/// </summary>
|
||||
private void OnMouseEnterWindow(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_isMouseOverWindow = true;
|
||||
UpdateHotkeyStateBasedOnContext();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理鼠标进入窗口事件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标离开窗口事件处理
|
||||
/// </summary>
|
||||
private void OnMouseLeaveWindow(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_isMouseOverWindow = false;
|
||||
UpdateHotkeyStateBasedOnContext();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理鼠标离开窗口事件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标位置定时器事件处理
|
||||
/// </summary>
|
||||
private void OnMousePositionTimerTick(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
|
||||
return;
|
||||
|
||||
// 检查鼠标是否在当前窗口所在的屏幕上
|
||||
var mousePosition = Control.MousePosition;
|
||||
var currentScreen = Screen.FromPoint(mousePosition);
|
||||
|
||||
// 无论屏幕是否变化,都检查热键状态
|
||||
// 这样可以确保热键状态始终与当前上下文保持一致
|
||||
bool shouldEnableHotkeys = ShouldEnableHotkeysBasedOnContext();
|
||||
bool currentlyHasHotkeys = _registeredHotkeys.Count > 0;
|
||||
|
||||
if (shouldEnableHotkeys && !currentlyHasHotkeys)
|
||||
{
|
||||
UpdateHotkeyStateBasedOnContext();
|
||||
}
|
||||
else if (!shouldEnableHotkeys && currentlyHasHotkeys)
|
||||
{
|
||||
UpdateHotkeyStateBasedOnContext();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理鼠标位置定时器事件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据上下文更新热键状态
|
||||
/// </summary>
|
||||
private void UpdateHotkeyStateBasedOnContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_hotkeysShouldBeRegistered)
|
||||
return;
|
||||
|
||||
bool shouldEnableHotkeys = ShouldEnableHotkeysBasedOnContext();
|
||||
bool currentlyHasHotkeys = _registeredHotkeys.Count > 0;
|
||||
|
||||
if (shouldEnableHotkeys && !currentlyHasHotkeys)
|
||||
{
|
||||
// 需要注册快捷键
|
||||
LoadHotkeysFromSettings();
|
||||
}
|
||||
else if (!shouldEnableHotkeys && currentlyHasHotkeys)
|
||||
{
|
||||
// 需要注销快捷键
|
||||
UnregisterAllHotkeys();
|
||||
}
|
||||
// 如果状态没有变化,则不进行任何操作
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"根据上下文更新热键状态时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否应该注册热键(基于屏幕和模式)
|
||||
/// </summary>
|
||||
/// <returns>是否应该注册热键</returns>
|
||||
private bool ShouldRegisterHotkeys()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果禁用热键注册,则不注册
|
||||
if (!_hotkeysShouldBeRegistered)
|
||||
return false;
|
||||
|
||||
// 如果启用基于屏幕的热键注册
|
||||
if (_enableScreenSpecificHotkeys && _isMultiScreenMode)
|
||||
{
|
||||
return ShouldEnableHotkeysBasedOnContext();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"检查是否应该注册热键时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return true; // 出错时默认注册
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据上下文检查是否应该启用热键
|
||||
/// </summary>
|
||||
/// <returns>是否应该启用热键</returns>
|
||||
private bool ShouldEnableHotkeysBasedOnContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查当前是否处于鼠标模式
|
||||
bool isMouseMode = IsInSelectMode();
|
||||
|
||||
if (isMouseMode)
|
||||
{
|
||||
// 鼠标模式下,根据设置决定是否启用快捷键
|
||||
return MainWindow.Settings.Appearance.EnableHotkeysInMouseMode;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非鼠标模式下,需要检查焦点和屏幕位置
|
||||
|
||||
// 策略1:鼠标在窗口上时启用热键(最高优先级)
|
||||
if (_isMouseOverWindow)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 策略2:在多屏幕环境下,检查鼠标是否在当前窗口所在的屏幕上
|
||||
if (_isMultiScreenMode && _enableScreenSpecificHotkeys)
|
||||
{
|
||||
var mousePosition = Control.MousePosition;
|
||||
var mouseScreen = Screen.FromPoint(mousePosition);
|
||||
|
||||
if (mouseScreen == _currentScreen)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 策略3:单屏幕环境下,窗口有焦点时启用热键
|
||||
if (_isWindowFocused)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 策略4:如果以上都不满足,但在非鼠标模式下,仍然启用快捷键
|
||||
// 这样可以确保在批注模式下快捷键始终可用
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"检查是否应该启用热键时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return true; // 出错时默认启用
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定笔类型
|
||||
/// </summary>
|
||||
@@ -472,17 +950,17 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
// 通过反射访问主窗口的penType字段
|
||||
var penTypeField = _mainWindow.GetType().GetField("penType",
|
||||
var penTypeField = _mainWindow.GetType().GetField("penType",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
|
||||
if (penTypeField != null)
|
||||
{
|
||||
penTypeField.SetValue(_mainWindow, penTypeIndex);
|
||||
|
||||
|
||||
// 调用CheckPenTypeUIState方法更新UI状态
|
||||
var checkPenTypeMethod = _mainWindow.GetType().GetMethod("CheckPenTypeUIState",
|
||||
var checkPenTypeMethod = _mainWindow.GetType().GetMethod("CheckPenTypeUIState",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
|
||||
if (checkPenTypeMethod != null)
|
||||
{
|
||||
checkPenTypeMethod.Invoke(_mainWindow, null);
|
||||
@@ -495,6 +973,88 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保配置文件存在,如果不存在则创建
|
||||
/// </summary>
|
||||
private void EnsureConfigFileExists()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果配置文件不存在,创建默认配置文件
|
||||
if (!File.Exists(HotkeyConfigFile))
|
||||
{
|
||||
CreateDefaultConfigFile();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"确保快捷键配置文件存在时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建默认的快捷键配置文件
|
||||
/// </summary>
|
||||
private void CreateDefaultConfigFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保配置目录存在
|
||||
string configDir = Path.GetDirectoryName(HotkeyConfigFile);
|
||||
if (!Directory.Exists(configDir))
|
||||
{
|
||||
Directory.CreateDirectory(configDir);
|
||||
}
|
||||
|
||||
// 创建默认配置对象
|
||||
var config = new HotkeyConfig
|
||||
{
|
||||
Version = "1.0",
|
||||
LastModified = DateTime.Now,
|
||||
Hotkeys = new List<HotkeyConfigItem>()
|
||||
};
|
||||
|
||||
// 添加默认快捷键配置
|
||||
config.Hotkeys.AddRange(new[]
|
||||
{
|
||||
new HotkeyConfigItem { Name = "Undo", Key = Key.Z, Modifiers = ModifierKeys.Control },
|
||||
new HotkeyConfigItem { Name = "Redo", Key = Key.Y, Modifiers = ModifierKeys.Control },
|
||||
new HotkeyConfigItem { Name = "Clear", Key = Key.E, Modifiers = ModifierKeys.Control },
|
||||
new HotkeyConfigItem { Name = "Paste", Key = Key.V, Modifiers = ModifierKeys.Control },
|
||||
new HotkeyConfigItem { Name = "SelectTool", Key = Key.S, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "DrawTool", Key = Key.D, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "EraserTool", Key = Key.E, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "BlackboardTool", Key = Key.B, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "QuitDrawTool", Key = Key.Q, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Pen1", Key = Key.D1, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Pen2", Key = Key.D2, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Pen3", Key = Key.D3, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Pen4", Key = Key.D4, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Pen5", Key = Key.D5, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "DrawLine", Key = Key.L, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Screenshot", Key = Key.C, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Hide", Key = Key.V, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Exit", Key = Key.Escape, Modifiers = ModifierKeys.None }
|
||||
});
|
||||
|
||||
// 序列化为JSON
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.Indented
|
||||
};
|
||||
|
||||
string jsonContent = JsonConvert.SerializeObject(config, settings);
|
||||
|
||||
// 写入配置文件
|
||||
File.WriteAllText(HotkeyConfigFile, jsonContent, Encoding.UTF8);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建默认快捷键配置文件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从配置文件加载快捷键设置
|
||||
/// </summary>
|
||||
@@ -551,7 +1111,6 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"成功加载 {successCount}/{config.Hotkeys.Count} 个快捷键配置", LogHelper.LogType.Event);
|
||||
if (successCount > 0)
|
||||
{
|
||||
_hotkeysShouldBeRegistered = true;
|
||||
@@ -604,13 +1163,12 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
Formatting = Formatting.Indented
|
||||
};
|
||||
|
||||
|
||||
string jsonContent = JsonConvert.SerializeObject(config, settings);
|
||||
|
||||
// 直接写入原文件,覆盖原有内容
|
||||
File.WriteAllText(HotkeyConfigFile, jsonContent, Encoding.UTF8);
|
||||
|
||||
LogHelper.WriteLogToFile($"快捷键配置已保存到: {HotkeyConfigFile}", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -688,9 +1246,9 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
// 通过反射访问主窗口的FloatingbarSelectionBG字段
|
||||
var floatingbarSelectionBGField = _mainWindow.GetType().GetField("FloatingbarSelectionBG",
|
||||
var floatingbarSelectionBGField = _mainWindow.GetType().GetField("FloatingbarSelectionBG",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
|
||||
if (floatingbarSelectionBGField != null)
|
||||
{
|
||||
var floatingbarSelectionBG = floatingbarSelectionBGField.GetValue(_mainWindow);
|
||||
@@ -707,7 +1265,7 @@ namespace Ink_Canvas.Helpers
|
||||
return true; // 返回true表示应该注销快捷键
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 通过反射访问Canvas.GetLeft方法来获取高光位置
|
||||
var canvasType = Type.GetType("System.Windows.Controls.Canvas, PresentationFramework");
|
||||
if (canvasType != null)
|
||||
@@ -719,7 +1277,7 @@ namespace Ink_Canvas.Helpers
|
||||
if (leftPosition != null)
|
||||
{
|
||||
var position = Convert.ToDouble(leftPosition);
|
||||
|
||||
|
||||
// 根据高光位置判断当前选中的工具
|
||||
// 位置计算基于SetFloatingBarHighlightPosition方法中的逻辑
|
||||
bool isMouseMode;
|
||||
@@ -739,20 +1297,20 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
isMouseMode = false;
|
||||
}
|
||||
|
||||
|
||||
return isMouseMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果无法获取高光状态,则回退到inkCanvas.EditingMode判断
|
||||
|
||||
|
||||
// 通过反射访问主窗口的inkCanvas字段
|
||||
var inkCanvasField = _mainWindow.GetType().GetField("inkCanvas",
|
||||
var inkCanvasField = _mainWindow.GetType().GetField("inkCanvas",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
|
||||
if (inkCanvasField != null)
|
||||
{
|
||||
var inkCanvas = inkCanvasField.GetValue(_mainWindow);
|
||||
@@ -768,23 +1326,23 @@ namespace Ink_Canvas.Helpers
|
||||
// 检查是否为批注模式
|
||||
var isInkMode = editingMode.ToString().Contains("Ink");
|
||||
var isSelectMode = editingMode.ToString().Contains("Select");
|
||||
|
||||
|
||||
// 如果是批注模式或选择模式,则应该注册快捷键(返回false)
|
||||
// 如果是橡皮擦模式或其他模式,则不应该注册快捷键(返回true)
|
||||
var shouldNotRegisterHotkeys = !isInkMode && !isSelectMode;
|
||||
|
||||
|
||||
return shouldNotRegisterHotkeys;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果无法获取任何状态信息,则回退到原来的判断逻辑
|
||||
|
||||
|
||||
// 通过反射访问主窗口的currentMode字段(作为最后的备用方案)
|
||||
var currentModeField = _mainWindow.GetType().GetField("currentMode",
|
||||
var currentModeField = _mainWindow.GetType().GetField("currentMode",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
|
||||
if (currentModeField != null)
|
||||
{
|
||||
var currentMode = currentModeField.GetValue(_mainWindow);
|
||||
@@ -798,7 +1356,7 @@ namespace Ink_Canvas.Helpers
|
||||
return isSelectMode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false; // 默认允许快捷键
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -816,7 +1374,30 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
|
||||
// 注销所有快捷键
|
||||
UnregisterAllHotkeys();
|
||||
|
||||
// 停止定时器
|
||||
if (_mousePositionTimer != null)
|
||||
{
|
||||
_mousePositionTimer.Stop();
|
||||
_mousePositionTimer = null;
|
||||
}
|
||||
|
||||
// 移除事件监听器
|
||||
if (_mainWindow != null)
|
||||
{
|
||||
if (_isMultiScreenMode)
|
||||
{
|
||||
_mainWindow.LocationChanged -= OnWindowLocationChanged;
|
||||
}
|
||||
|
||||
_mainWindow.GotFocus -= OnWindowGotFocus;
|
||||
_mainWindow.LostFocus -= OnWindowLostFocus;
|
||||
_mainWindow.MouseEnter -= OnMouseEnterWindow;
|
||||
_mainWindow.MouseLeave -= OnMouseLeaveWindow;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
@@ -861,4 +1442,4 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@ namespace Ink_Canvas.Helpers
|
||||
return originalStroke;
|
||||
|
||||
var originalPoints = originalStroke.StylusPoints.ToArray();
|
||||
|
||||
|
||||
// 预处理:去除噪声点
|
||||
var cleanedPoints = RemoveNoisePoints(originalPoints);
|
||||
|
||||
|
||||
// 使用改进的贝塞尔曲线拟合
|
||||
var smoothedPoints = ApplyCubicBezierSmoothing(cleanedPoints);
|
||||
|
||||
|
||||
// 后处理:重采样和优化
|
||||
var finalPoints = PostProcessPoints(smoothedPoints);
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Ink_Canvas.Helpers
|
||||
var next = points[i + 1];
|
||||
|
||||
// 计算到前一个点的距离
|
||||
double distToPrev = Math.Sqrt((curr.X - prev.X) * (curr.X - prev.X) +
|
||||
double distToPrev = Math.Sqrt((curr.X - prev.X) * (curr.X - prev.X) +
|
||||
(curr.Y - prev.Y) * (curr.Y - prev.Y));
|
||||
|
||||
// 如果距离太近,跳过这个点
|
||||
@@ -148,7 +148,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 计算控制点距离
|
||||
double dist1 = CalculateDistance(p0, p1);
|
||||
double dist2 = CalculateDistance(p2, p3);
|
||||
|
||||
|
||||
double controlDist1 = dist1 * _config.CurveTension;
|
||||
double controlDist2 = dist2 * _config.CurveTension;
|
||||
|
||||
@@ -322,4 +322,4 @@ namespace Ink_Canvas.Helpers
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
|
||||
@@ -63,13 +64,18 @@ namespace Ink_Canvas.Helpers
|
||||
/// <param name="endPoint">抬笔点</param>
|
||||
public void AddFadingStroke(Stroke stroke, Point startPoint, Point endPoint)
|
||||
{
|
||||
if (!IsEnabled || stroke == null)
|
||||
if (!IsEnabled || stroke == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 确保主窗口的InkCanvas保持Ink编辑模式,防止墨迹渐隐时切换到鼠标模式
|
||||
if (_mainWindow.inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
|
||||
{
|
||||
_mainWindow.inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
|
||||
}
|
||||
|
||||
// 记录墨迹的起点和终点
|
||||
_strokeStartPoints[stroke] = startPoint;
|
||||
@@ -238,12 +244,12 @@ namespace Ink_Canvas.Helpers
|
||||
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();
|
||||
@@ -283,14 +289,14 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
// 创建路径几何,使用墨迹的实际位置
|
||||
var geometry = stroke.GetGeometry();
|
||||
if (geometry == null)
|
||||
if (geometry == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取绘画属性
|
||||
var drawingAttribs = stroke.DrawingAttributes;
|
||||
|
||||
|
||||
// 创建路径元素,确保使用正确的绘画属性
|
||||
var path = new Path
|
||||
{
|
||||
@@ -302,7 +308,7 @@ namespace Ink_Canvas.Helpers
|
||||
StrokeLineJoin = PenLineJoin.Round,
|
||||
Fill = drawingAttribs.IsHighlighter ? new SolidColorBrush(drawingAttribs.Color) : null, // 高亮笔需要填充
|
||||
Opacity = 0.95, // 初始透明度更高,显得更自然
|
||||
|
||||
|
||||
// 优化渲染质量
|
||||
UseLayoutRounding = false,
|
||||
SnapsToDevicePixels = false
|
||||
@@ -312,19 +318,26 @@ namespace Ink_Canvas.Helpers
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 高亮笔通常需要更宽的笔触来覆盖下面的内容
|
||||
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;
|
||||
@@ -356,7 +369,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 获取当前透明度和判断是否为高亮笔
|
||||
var currentOpacity = visual.Opacity;
|
||||
var isHighlighter = stroke.DrawingAttributes.IsHighlighter;
|
||||
|
||||
|
||||
// 根据墨迹类型选择不同的动画效果
|
||||
if (isHighlighter)
|
||||
{
|
||||
@@ -389,6 +402,57 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -396,7 +460,8 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
StartProgressiveFadeAnimation(visual, stroke, currentOpacity, (int)(AnimationDuration * 1.5));
|
||||
// 高亮笔使用统一的渐隐动画,与擦除效果一致
|
||||
StartUnifiedFadeAnimation(visual, stroke, currentOpacity, (int)(AnimationDuration * 1.2));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -439,19 +504,19 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
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)
|
||||
@@ -465,7 +530,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
var startIndex = i * pointsPerSegment;
|
||||
var endIndex = (i == segmentCount - 1) ? totalPoints - 1 : (i + 1) * pointsPerSegment;
|
||||
|
||||
|
||||
// 确保有足够的点来创建分段,对于短墨迹特殊处理
|
||||
if (endIndex <= startIndex && totalPoints > 1)
|
||||
{
|
||||
@@ -473,12 +538,12 @@ namespace Ink_Canvas.Helpers
|
||||
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)
|
||||
{
|
||||
@@ -576,10 +641,10 @@ namespace Ink_Canvas.Helpers
|
||||
for (int i = 0; i < segments.Count; i++)
|
||||
{
|
||||
var segment = segments[i];
|
||||
|
||||
|
||||
// 使用预计算的动画曲线获取延迟时间
|
||||
var delay = animationCurve[i];
|
||||
|
||||
|
||||
// 使用定时器延迟启动每个分段的动画
|
||||
var timer = new DispatcherTimer
|
||||
{
|
||||
@@ -595,7 +660,7 @@ namespace Ink_Canvas.Helpers
|
||||
lock (completedSegments)
|
||||
{
|
||||
completedSegments.Add(segment);
|
||||
|
||||
|
||||
// 检查是否所有分段都完成了
|
||||
if (completedSegments.Count >= totalSegments)
|
||||
{
|
||||
@@ -676,7 +741,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
// 移除所有分段
|
||||
var parent = _mainWindow.inkCanvas?.Parent as Panel;
|
||||
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (parent != null && parent.Children.Contains(segment))
|
||||
@@ -729,7 +794,7 @@ namespace Ink_Canvas.Helpers
|
||||
private double CalculateStrokeLength(StylusPointCollection points)
|
||||
{
|
||||
if (points.Count < 2) return 0;
|
||||
|
||||
|
||||
double totalLength = 0;
|
||||
for (int i = 1; i < points.Count; i++)
|
||||
{
|
||||
@@ -749,22 +814,22 @@ namespace Ink_Canvas.Helpers
|
||||
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));
|
||||
}
|
||||
@@ -778,7 +843,7 @@ namespace Ink_Canvas.Helpers
|
||||
var baseDuration = totalDuration / Math.Max(segmentCount, 1);
|
||||
var minDuration = 150; // 每段最少150ms,确保动画完整显示
|
||||
var maxDuration = 500; // 每段最多500ms,平衡速度与完整性
|
||||
|
||||
|
||||
return Math.Max(minDuration, Math.Min(maxDuration, baseDuration));
|
||||
}
|
||||
|
||||
@@ -788,17 +853,17 @@ namespace Ink_Canvas.Helpers
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -829,4 +894,4 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,22 +12,22 @@ namespace Ink_Canvas.Helpers
|
||||
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, // 性能优先
|
||||
@@ -49,7 +49,7 @@ namespace Ink_Canvas.Helpers
|
||||
public static InkSmoothingConfig FromSettings()
|
||||
{
|
||||
var config = new InkSmoothingConfig();
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试从MainWindow.Settings加载配置(兼容性)
|
||||
@@ -66,42 +66,51 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
Debug.WriteLine($"加载平滑配置失败: {ex.Message}");
|
||||
}
|
||||
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用质量设置
|
||||
/// 应用质量设置
|
||||
/// </summary>
|
||||
public void ApplyQualitySettings()
|
||||
{
|
||||
// 保存用户设置的异步处理偏好
|
||||
bool userAsyncPreference = UseAsyncProcessing;
|
||||
|
||||
switch (Quality)
|
||||
{
|
||||
case SmoothingQuality.Performance:
|
||||
SmoothingStrength = 0.2;
|
||||
ResampleInterval = 4.0;
|
||||
InterpolationSteps = 6;
|
||||
SmoothingStrength = 0.15;
|
||||
ResampleInterval = 5.0;
|
||||
InterpolationSteps = 4;
|
||||
UseAdaptiveInterpolation = false;
|
||||
CurveTension = 0.2;
|
||||
CurveTension = 0.15;
|
||||
MaxConcurrentTasks = Math.Max(1, Environment.ProcessorCount / 2);
|
||||
UseHardwareAcceleration = true;
|
||||
UseAsyncProcessing = userAsyncPreference;
|
||||
break;
|
||||
|
||||
|
||||
case SmoothingQuality.Balanced:
|
||||
SmoothingStrength = 0.4;
|
||||
ResampleInterval = 2.5;
|
||||
InterpolationSteps = 12;
|
||||
SmoothingStrength = 0.3;
|
||||
ResampleInterval = 3.0;
|
||||
InterpolationSteps = 8;
|
||||
UseAdaptiveInterpolation = true;
|
||||
CurveTension = 0.3;
|
||||
CurveTension = 0.25;
|
||||
MaxConcurrentTasks = Environment.ProcessorCount;
|
||||
UseHardwareAcceleration = true;
|
||||
UseAsyncProcessing = userAsyncPreference;
|
||||
break;
|
||||
|
||||
|
||||
case SmoothingQuality.Quality:
|
||||
SmoothingStrength = 0.6;
|
||||
ResampleInterval = 1.5;
|
||||
InterpolationSteps = 20;
|
||||
SmoothingStrength = 0.5;
|
||||
ResampleInterval = 2.0;
|
||||
InterpolationSteps = 15;
|
||||
UseAdaptiveInterpolation = true;
|
||||
CurveTension = 0.4;
|
||||
CurveTension = 0.35;
|
||||
MaxConcurrentTasks = Environment.ProcessorCount;
|
||||
UseHardwareAcceleration = true;
|
||||
UseAsyncProcessing = userAsyncPreference;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -120,7 +129,7 @@ namespace Ink_Canvas.Helpers
|
||||
MainWindow.Settings.Canvas.UseHardwareAcceleration = UseHardwareAcceleration;
|
||||
MainWindow.Settings.Canvas.UseAsyncInkSmoothing = UseAsyncProcessing;
|
||||
MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks = MaxConcurrentTasks;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -152,4 +161,4 @@ namespace Ink_Canvas.Helpers
|
||||
$"张力: {CurveTension:F2}, 硬件加速: {UseHardwareAcceleration}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,18 +109,102 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新画出笔迹
|
||||
/// 绘制点段到新的DrawingVisual
|
||||
/// </summary>
|
||||
private void DrawSegmentToNewVisual(int startIndex, int endIndex)
|
||||
{
|
||||
if (Stroke == null || Stroke.StylusPoints.Count == 0 || _visualCanvas == null) return;
|
||||
if (startIndex >= endIndex || startIndex < 0 || endIndex > Stroke.StylusPoints.Count) return;
|
||||
|
||||
var points = Stroke.StylusPoints;
|
||||
var drawingAttributes = Stroke.DrawingAttributes;
|
||||
|
||||
// 创建新的DrawingVisual用于绘制这个点段
|
||||
var segmentVisual = new DrawingVisual();
|
||||
|
||||
RenderOptions.SetBitmapScalingMode(segmentVisual, BitmapScalingMode.HighQuality);
|
||||
RenderOptions.SetEdgeMode(segmentVisual, EdgeMode.Aliased);
|
||||
RenderOptions.SetCachingHint(segmentVisual, CachingHint.Cache);
|
||||
|
||||
using (var dc = segmentVisual.RenderOpen())
|
||||
{
|
||||
var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), drawingAttributes.Width);
|
||||
pen.StartLineCap = PenLineCap.Round;
|
||||
pen.EndLineCap = PenLineCap.Round;
|
||||
pen.LineJoin = PenLineJoin.Round;
|
||||
|
||||
// 绘制指定范围内的点段
|
||||
if (endIndex - startIndex >= 2)
|
||||
{
|
||||
// 多个点,绘制线段
|
||||
for (int i = startIndex; i < endIndex - 1 && i < points.Count - 1; i++)
|
||||
{
|
||||
var startPoint = new Point(points[i].X, points[i].Y);
|
||||
var endPoint = new Point(points[i + 1].X, points[i + 1].Y);
|
||||
dc.DrawLine(pen, startPoint, endPoint);
|
||||
}
|
||||
}
|
||||
else if (endIndex - startIndex == 1 && startIndex < points.Count)
|
||||
{
|
||||
// 只有一个点,绘制圆点
|
||||
var brush = new SolidColorBrush(drawingAttributes.Color);
|
||||
var point = points[startIndex];
|
||||
dc.DrawEllipse(brush, null, new Point(point.X, point.Y),
|
||||
drawingAttributes.Width / 2, drawingAttributes.Height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 将新的DrawingVisual添加到VisualCanvas中
|
||||
_visualCanvas.AddVisual(segmentVisual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新画出笔迹
|
||||
/// </summary>
|
||||
public void Redraw()
|
||||
{
|
||||
try
|
||||
if (Stroke == null || _visualCanvas == null) return;
|
||||
|
||||
var currentPointCount = Stroke.StylusPoints.Count;
|
||||
if (currentPointCount == 0) return;
|
||||
|
||||
// 计算新增的点数
|
||||
int newPointCount = currentPointCount - _lastDrawnPointCount;
|
||||
|
||||
// 如果新增点数达到阈值,才进行增量绘制
|
||||
if (newPointCount >= INCREMENTAL_DRAW_THRESHOLD || _lastDrawnPointCount == 0)
|
||||
{
|
||||
using (var dc = RenderOpen())
|
||||
try
|
||||
{
|
||||
Stroke.Draw(dc);
|
||||
if (_lastDrawnPointCount == 0)
|
||||
{
|
||||
// 首次绘制:绘制所有点
|
||||
DrawSegmentToNewVisual(0, currentPointCount);
|
||||
_lastDrawnPointCount = currentPointCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 从上次绘制的最后一个点开始
|
||||
int startIndex = Math.Max(0, _lastDrawnPointCount - 1);
|
||||
DrawSegmentToNewVisual(startIndex, currentPointCount);
|
||||
_lastDrawnPointCount = currentPointCount;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制重绘
|
||||
/// </summary>
|
||||
public void ForceRedraw()
|
||||
{
|
||||
if (_visualCanvas != null)
|
||||
{
|
||||
_visualCanvas.Clear();
|
||||
}
|
||||
_lastDrawnPointCount = 0;
|
||||
Redraw();
|
||||
}
|
||||
|
||||
private readonly DrawingAttributes _drawingAttributes;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using Microsoft.Office.Interop.PowerPoint;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Windows.Ink;
|
||||
using Microsoft.Office.Interop.PowerPoint;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
@@ -29,6 +30,17 @@ namespace Ink_Canvas.Helpers
|
||||
private DateTime _inkLockUntil = DateTime.MinValue;
|
||||
private int _lockedSlideIndex = -1;
|
||||
private const int InkLockMilliseconds = 500;
|
||||
|
||||
// 添加快速切换保护机制
|
||||
private DateTime _lastSwitchTime = DateTime.MinValue;
|
||||
private int _lastSwitchSlideIndex = -1;
|
||||
private const int MinSwitchIntervalMs = 100; // 最小切换间隔100毫秒
|
||||
|
||||
// 内存管理相关字段
|
||||
private long _totalMemoryUsage = 0;
|
||||
private const long MaxMemoryUsageBytes = 100 * 1024 * 1024; // 100MB限制
|
||||
private DateTime _lastMemoryCleanup = DateTime.MinValue;
|
||||
private const int MemoryCleanupIntervalMinutes = 5; // 5分钟清理一次
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
@@ -57,16 +69,29 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
// 完全清理之前的墨迹状态
|
||||
ClearAllStrokes();
|
||||
|
||||
|
||||
// 重置墨迹锁定状态
|
||||
_inkLockUntil = DateTime.MinValue;
|
||||
_lockedSlideIndex = -1;
|
||||
|
||||
|
||||
// 生成演示文稿唯一标识符
|
||||
_currentPresentationId = GeneratePresentationId(presentation);
|
||||
|
||||
// 重新初始化内存流数组
|
||||
var slideCount = presentation.Slides.Count;
|
||||
int slideCount = 0;
|
||||
try
|
||||
{
|
||||
slideCount = presentation.Slides.Count;
|
||||
}
|
||||
catch (COMException comEx)
|
||||
{
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr == 0x80048010)
|
||||
{
|
||||
return;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
_memoryStreams = new MemoryStream[slideCount + 2];
|
||||
|
||||
// 如果启用自动保存,尝试加载已保存的墨迹
|
||||
@@ -75,7 +100,6 @@ namespace Ink_Canvas.Helpers
|
||||
LoadSavedStrokes();
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已初始化演示文稿墨迹管理: {presentation.Name}, 幻灯片数量: {slideCount}", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -95,24 +119,39 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查墨迹锁定
|
||||
// 检查墨迹锁定
|
||||
if (!CanWriteInk(slideIndex))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"墨迹写入被锁定,当前页:{slideIndex},锁定页:{_lockedSlideIndex}", LogHelper.LogType.Warning);
|
||||
if (DateTime.Now < _inkLockUntil)
|
||||
{
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (slideIndex < _memoryStreams.Length)
|
||||
{
|
||||
// 先释放旧的内存流,防止内存泄漏
|
||||
try
|
||||
{
|
||||
_memoryStreams[slideIndex]?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"释放旧内存流失败: {ex}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
// 创建新的内存流
|
||||
var ms = new MemoryStream();
|
||||
strokes.Save(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
// 释放旧的内存流
|
||||
_memoryStreams[slideIndex]?.Dispose();
|
||||
_memoryStreams[slideIndex] = ms;
|
||||
|
||||
LogHelper.WriteLogToFile($"已保存第{slideIndex}页墨迹,大小: {ms.Length} bytes", LogHelper.LogType.Trace);
|
||||
if (ms.Length > 0)
|
||||
{
|
||||
}
|
||||
|
||||
// 检查内存使用情况
|
||||
CheckAndPerformMemoryCleanup();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -122,6 +161,45 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制保存指定页面的墨迹
|
||||
/// </summary>
|
||||
public void ForceSaveSlideStrokes(int slideIndex, StrokeCollection strokes)
|
||||
{
|
||||
if (slideIndex <= 0 || strokes == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (slideIndex < _memoryStreams.Length)
|
||||
{
|
||||
// 先释放旧的内存流,防止内存泄漏
|
||||
try
|
||||
{
|
||||
_memoryStreams[slideIndex]?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"释放旧内存流失败: {ex}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
// 创建新的内存流
|
||||
var ms = new MemoryStream();
|
||||
strokes.Save(ms);
|
||||
ms.Position = 0;
|
||||
_memoryStreams[slideIndex] = ms;
|
||||
|
||||
LogHelper.WriteLogToFile($"已强制保存第{slideIndex}页墨迹,大小: {ms.Length} bytes", LogHelper.LogType.Trace);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"强制保存第{slideIndex}页墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载指定页面的墨迹
|
||||
/// </summary>
|
||||
@@ -137,7 +215,6 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
_memoryStreams[slideIndex].Position = 0;
|
||||
var strokes = new StrokeCollection(_memoryStreams[slideIndex]);
|
||||
LogHelper.WriteLogToFile($"已加载第{slideIndex}页墨迹,笔画数量: {strokes.Count}", LogHelper.LogType.Trace);
|
||||
return strokes;
|
||||
}
|
||||
}
|
||||
@@ -159,27 +236,30 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果有当前墨迹,先保存到正确的页面
|
||||
if (currentStrokes != null && currentStrokes.Count > 0)
|
||||
// 检查快速切换保护
|
||||
var now = DateTime.Now;
|
||||
if (now - _lastSwitchTime < TimeSpan.FromMilliseconds(MinSwitchIntervalMs) &&
|
||||
_lastSwitchSlideIndex == slideIndex)
|
||||
{
|
||||
// 确定要保存的页面索引
|
||||
int saveToSlideIndex = _lockedSlideIndex > 0 ? _lockedSlideIndex : slideIndex;
|
||||
|
||||
// 确保页面索引有效
|
||||
if (saveToSlideIndex > 0 && saveToSlideIndex < _memoryStreams.Length)
|
||||
{
|
||||
SaveCurrentSlideStrokes(saveToSlideIndex, currentStrokes);
|
||||
LogHelper.WriteLogToFile($"已保存第{saveToSlideIndex}页墨迹,墨迹数量: {currentStrokes.Count}", LogHelper.LogType.Trace);
|
||||
}
|
||||
LogHelper.WriteLogToFile($"快速切换保护:忽略重复的页面切换请求 {slideIndex}", LogHelper.LogType.Warning);
|
||||
return LoadSlideStrokes(slideIndex);
|
||||
}
|
||||
|
||||
|
||||
// 设置墨迹锁定
|
||||
LockInkForSlide(slideIndex);
|
||||
|
||||
// 加载新页面的墨迹
|
||||
var newStrokes = LoadSlideStrokes(slideIndex);
|
||||
LogHelper.WriteLogToFile($"已切换到第{slideIndex}页,加载墨迹数量: {newStrokes.Count}", LogHelper.LogType.Trace);
|
||||
|
||||
|
||||
// 更新切换记录
|
||||
_lastSwitchTime = now;
|
||||
_lastSwitchSlideIndex = slideIndex;
|
||||
|
||||
if (newStrokes.Count > 0)
|
||||
{
|
||||
}
|
||||
|
||||
return newStrokes;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -193,7 +273,9 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 保存所有墨迹到文件
|
||||
/// </summary>
|
||||
public void SaveAllStrokesToFile(Presentation presentation)
|
||||
/// <param name="presentation">演示文稿对象</param>
|
||||
/// <param name="currentSlideIndex">当前播放的页码,如果提供则使用此值保存位置,否则使用_lockedSlideIndex</param>
|
||||
public void SaveAllStrokesToFile(Presentation presentation, int currentSlideIndex = -1)
|
||||
{
|
||||
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return;
|
||||
|
||||
@@ -210,7 +292,18 @@ namespace Ink_Canvas.Helpers
|
||||
// 保存位置信息
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(folderPath, "Position"), _lockedSlideIndex.ToString());
|
||||
// 优先使用传入的当前页码,否则使用锁定的页码
|
||||
int positionToSave = currentSlideIndex > 0 ? currentSlideIndex : _lockedSlideIndex;
|
||||
// 如果都没有有效值,尝试使用最后切换的页码
|
||||
if (positionToSave <= 0 && _lastSwitchSlideIndex > 0)
|
||||
{
|
||||
positionToSave = _lastSwitchSlideIndex;
|
||||
}
|
||||
|
||||
if (positionToSave > 0)
|
||||
{
|
||||
File.WriteAllText(Path.Combine(folderPath, "Position"), positionToSave.ToString());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -219,7 +312,23 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
// 保存所有页面的墨迹
|
||||
int savedCount = 0;
|
||||
for (int i = 1; i <= presentation.Slides.Count && i < _memoryStreams.Length; i++)
|
||||
int slideCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
slideCount = presentation.Slides.Count;
|
||||
}
|
||||
catch (COMException comEx)
|
||||
{
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr == 0x80048010)
|
||||
{
|
||||
return;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
for (int i = 1; i <= slideCount && i < _memoryStreams.Length; i++)
|
||||
{
|
||||
if (_memoryStreams[i] != null)
|
||||
{
|
||||
@@ -235,7 +344,6 @@ namespace Ink_Canvas.Helpers
|
||||
File.WriteAllBytes(filePath, srcBuf);
|
||||
savedCount++;
|
||||
|
||||
LogHelper.WriteLogToFile($"已保存第{i}页墨迹,大小: {byteLength} bytes", LogHelper.LogType.Trace);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -254,7 +362,6 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已保存{savedCount}页墨迹到文件", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -301,7 +408,6 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已从文件加载{loadedCount}页墨迹", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -319,13 +425,30 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < _memoryStreams.Length; i++)
|
||||
// 安全释放所有内存流
|
||||
if (_memoryStreams != null)
|
||||
{
|
||||
_memoryStreams[i]?.Dispose();
|
||||
_memoryStreams[i] = null;
|
||||
for (int i = 0; i < _memoryStreams.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
_memoryStreams[i]?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"释放内存流{i}失败: {ex}", LogHelper.LogType.Warning);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_memoryStreams[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 重新初始化数组
|
||||
_memoryStreams = new MemoryStream[_maxSlides + 2];
|
||||
}
|
||||
|
||||
CurrentStrokes.Clear();
|
||||
CurrentStrokes?.Clear();
|
||||
LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -349,9 +472,145 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
public bool CanWriteInk(int currentSlideIndex)
|
||||
{
|
||||
if (DateTime.Now < _inkLockUntil) return false;
|
||||
if (currentSlideIndex != _lockedSlideIndex && _lockedSlideIndex > 0) return false;
|
||||
return true;
|
||||
// 如果锁定时间已过,允许写入
|
||||
if (DateTime.Now >= _inkLockUntil)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果当前页面与锁定页面相同,允许写入(用户在当前页面绘制)
|
||||
if (currentSlideIndex == _lockedSlideIndex)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果当前页面不是锁定页面,但锁定时间很短(小于50ms),允许写入
|
||||
// 这样可以确保旧页面的墨迹能够及时保存
|
||||
if (DateTime.Now - (_inkLockUntil.AddMilliseconds(-InkLockMilliseconds)) < TimeSpan.FromMilliseconds(50))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 只有在快速切换且页面不同时才锁定
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置墨迹锁定状态
|
||||
/// </summary>
|
||||
public void ResetLockState()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_inkLockUntil = DateTime.MinValue;
|
||||
_lockedSlideIndex = -1;
|
||||
_lastSwitchTime = DateTime.MinValue;
|
||||
_lastSwitchSlideIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并执行内存清理
|
||||
/// </summary>
|
||||
private void CheckAndPerformMemoryCleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
|
||||
// 检查是否需要执行内存清理
|
||||
if (now - _lastMemoryCleanup < TimeSpan.FromMinutes(MemoryCleanupIntervalMinutes))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算当前内存使用量
|
||||
long currentMemoryUsage = 0;
|
||||
if (_memoryStreams != null)
|
||||
{
|
||||
for (int i = 0; i < _memoryStreams.Length; i++)
|
||||
{
|
||||
if (_memoryStreams[i] != null)
|
||||
{
|
||||
currentMemoryUsage += _memoryStreams[i].Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_totalMemoryUsage = currentMemoryUsage;
|
||||
|
||||
// 如果内存使用量超过限制,执行清理
|
||||
if (currentMemoryUsage > MaxMemoryUsageBytes)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"内存使用量超限 ({currentMemoryUsage / 1024 / 1024}MB),开始清理", LogHelper.LogType.Warning);
|
||||
|
||||
// 清理非当前页面的墨迹
|
||||
CleanupInactiveSlideStrokes();
|
||||
|
||||
_lastMemoryCleanup = now;
|
||||
LogHelper.WriteLogToFile($"内存清理完成,当前使用量: {_totalMemoryUsage / 1024 / 1024}MB", LogHelper.LogType.Trace);
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastMemoryCleanup = now;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"内存清理检查失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理非活跃页面的墨迹
|
||||
/// </summary>
|
||||
private void CleanupInactiveSlideStrokes()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_memoryStreams == null) return;
|
||||
|
||||
int cleanedCount = 0;
|
||||
long freedMemory = 0;
|
||||
|
||||
for (int i = 0; i < _memoryStreams.Length; i++)
|
||||
{
|
||||
// 保留当前锁定页面和最近访问的页面
|
||||
if (i == _lockedSlideIndex || i == _lastSwitchSlideIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_memoryStreams[i] != null)
|
||||
{
|
||||
long memorySize = _memoryStreams[i].Length;
|
||||
|
||||
try
|
||||
{
|
||||
_memoryStreams[i].Dispose();
|
||||
freedMemory += memorySize;
|
||||
cleanedCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理页面{i}墨迹失败: {ex}", LogHelper.LogType.Warning);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_memoryStreams[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"已清理{cleanedCount}个页面的墨迹,释放内存: {freedMemory / 1024}KB", LogHelper.LogType.Trace);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理非活跃页面墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
using Microsoft.Office.Interop.PowerPoint;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
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 _, isSupportWPS);
|
||||
|
||||
if (bestApp != null && bestPriority > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
Microsoft.Office.Interop.PowerPoint.Application pptApp = bestApp as Microsoft.Office.Interop.PowerPoint.Application;
|
||||
|
||||
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 连接验证失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
SafeReleaseComObject(bestApp);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SafeReleaseComObject(bestApp);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ROT 连接验证失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
SafeReleaseComObject(bestApp);
|
||||
}
|
||||
}
|
||||
else if (bestApp != null)
|
||||
{
|
||||
SafeReleaseComObject(bestApp);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ROT 连接过程发生异常: {ex}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
private static object GetAnyActivePowerPoint(object targetApp, out int bestPriority, out int targetPriority, bool isSupportWPS)
|
||||
{
|
||||
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
|
||||
{
|
||||
candidateApp = comObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 { }
|
||||
|
||||
if (activePres != null)
|
||||
{
|
||||
currentPriority = 1;
|
||||
|
||||
try
|
||||
{
|
||||
ssWindow = activePres.SlideShowWindow;
|
||||
}
|
||||
catch { }
|
||||
|
||||
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 { }
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
currentPriority = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsSlideShowWindowActive(ssWindow, isSupportWPS))
|
||||
{
|
||||
currentPriority = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
private static bool IsSlideShowWindowActive(object sswObj, bool isSupportWPS)
|
||||
{
|
||||
try
|
||||
{
|
||||
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;
|
||||
if (isSupportWPS)
|
||||
{
|
||||
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 { }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr GetPptHwndFromSlideShowWindow(object pptSlideShowWindowObj)
|
||||
{
|
||||
if (pptSlideShowWindowObj == null) return IntPtr.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
dynamic ssw = pptSlideShowWindowObj;
|
||||
object hwndObj = ssw.HWND;
|
||||
|
||||
if (hwndObj is int)
|
||||
{
|
||||
return new IntPtr((int)hwndObj);
|
||||
}
|
||||
else if (hwndObj is IntPtr)
|
||||
{
|
||||
return (IntPtr)hwndObj;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SafeReleaseComObject(object comObj)
|
||||
{
|
||||
if (comObj == null) return;
|
||||
|
||||
if (Marshal.IsComObject(comObj))
|
||||
{
|
||||
try
|
||||
{
|
||||
Marshal.ReleaseComObject(comObj);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,6 +23,10 @@ namespace Ink_Canvas.Helpers
|
||||
public int PPTRBButtonPosition { get; set; } = 0;
|
||||
public bool EnablePPTButtonPageClickable { get; set; } = true;
|
||||
public bool EnablePPTButtonLongPressPageTurn { get; set; } = true;
|
||||
public double PPTLSButtonOpacity { get; set; } = 0.5;
|
||||
public double PPTRSButtonOpacity { get; set; } = 0.5;
|
||||
public double PPTLBButtonOpacity { get; set; } = 0.5;
|
||||
public double PPTRBButtonOpacity { get; set; } = 0.5;
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
@@ -81,20 +86,60 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Collapsed;
|
||||
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Visible;
|
||||
|
||||
// 只有在页数有效时才更新页码显示
|
||||
if (currentSlide > 0 && totalSlides > 0)
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
|
||||
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 页数无效时清空页码显示
|
||||
_mainWindow.PPTBtnPageNow.Text = "?";
|
||||
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
|
||||
}
|
||||
|
||||
UpdateNavigationPanelsVisibility();
|
||||
UpdateNavigationButtonStyles();
|
||||
_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);
|
||||
}), 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)
|
||||
@@ -113,8 +158,18 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
|
||||
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
|
||||
// 只有在页数有效时才更新页码显示
|
||||
if (currentSlide > 0 && totalSlides > 0)
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
|
||||
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 页数无效时清空页码显示
|
||||
_mainWindow.PPTBtnPageNow.Text = "?";
|
||||
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -136,7 +191,6 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
// 如果不在放映模式,隐藏所有导航面板
|
||||
HideAllNavigationPanels();
|
||||
LogHelper.WriteLogToFile("PPT放映状态变化:隐藏导航面板", LogHelper.LogType.Trace);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -156,10 +210,16 @@ namespace Ink_Canvas.Helpers
|
||||
try
|
||||
{
|
||||
// 检查是否应该显示PPT按钮
|
||||
// 不仅要检查按钮设置,还要确保确实在PPT放映模式下
|
||||
bool shouldShowButtons = ShowPPTButton &&
|
||||
// 不仅要检查按钮设置,还要确保确实在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 &&
|
||||
_mainWindow.PPTManager?.IsInSlideShow == true;
|
||||
isInSlideShow &&
|
||||
hasValidPageCount &&
|
||||
!MainWindow.Settings.Automation.IsAutoFoldInPPTSlideShow;
|
||||
|
||||
if (!shouldShowButtons)
|
||||
{
|
||||
@@ -329,10 +389,9 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow.PPTLSPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.PPTRSPageButton.Visibility = pageButtonVisibility;
|
||||
|
||||
// 透明度设置
|
||||
var opacity = options[1] == '2' ? 0.5 : 1.0;
|
||||
_mainWindow.PPTBtnLSBorder.Opacity = opacity;
|
||||
_mainWindow.PPTBtnRSBorder.Opacity = opacity;
|
||||
// 透明度设置 - 直接使用用户设置的透明度值
|
||||
_mainWindow.PPTBtnLSBorder.Opacity = PPTLSButtonOpacity;
|
||||
_mainWindow.PPTBtnRSBorder.Opacity = PPTRSButtonOpacity;
|
||||
|
||||
// 颜色主题
|
||||
bool isDarkTheme = options[2] == '2';
|
||||
@@ -358,10 +417,9 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow.PPTLBPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.PPTRBPageButton.Visibility = pageButtonVisibility;
|
||||
|
||||
// 透明度设置
|
||||
var opacity = options[1] == '2' ? 0.5 : 1.0;
|
||||
_mainWindow.PPTBtnLBBorder.Opacity = opacity;
|
||||
_mainWindow.PPTBtnRBBorder.Opacity = opacity;
|
||||
// 透明度设置 - 直接使用用户设置的透明度值
|
||||
_mainWindow.PPTBtnLBBorder.Opacity = PPTLBButtonOpacity;
|
||||
_mainWindow.PPTBtnRBBorder.Opacity = PPTRBButtonOpacity;
|
||||
|
||||
// 颜色主题
|
||||
bool isDarkTheme = options[2] == '2';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -8,8 +10,6 @@ using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Ink_Canvas.Windows;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Ink_Canvas.Windows;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -5,8 +7,6 @@ using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn
|
||||
{
|
||||
|
||||
@@ -89,4 +89,4 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
base.Cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 增强的插件基类 V2,提供对三个专门服务接口的访问
|
||||
/// 插件开发者可以根据需要选择性地使用这些服务
|
||||
/// </summary>
|
||||
public abstract class EnhancedPluginBaseV2 : PluginBase, IEnhancedPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取服务实例
|
||||
/// </summary>
|
||||
public IGetService GetService { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 窗口服务实例
|
||||
/// </summary>
|
||||
public IWindowService WindowService { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作服务实例
|
||||
/// </summary>
|
||||
public IActionService ActionService { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件服务实例(兼容性)
|
||||
/// </summary>
|
||||
public IPluginService PluginService { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
protected EnhancedPluginBaseV2()
|
||||
{
|
||||
// 初始化所有服务实例
|
||||
PluginService = PluginServiceManager.Instance;
|
||||
GetService = PluginServiceManager.Instance;
|
||||
WindowService = PluginServiceManager.Instance;
|
||||
ActionService = PluginServiceManager.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件启动时调用,在Initialize之后
|
||||
/// </summary>
|
||||
public virtual void OnStartup()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已启动");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件关闭时调用,在Cleanup之前
|
||||
/// </summary>
|
||||
public virtual void OnShutdown()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 正在关闭");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的菜单项
|
||||
/// </summary>
|
||||
/// <returns>菜单项集合</returns>
|
||||
public virtual MenuItem[] GetMenuItems()
|
||||
{
|
||||
return new MenuItem[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的工具栏按钮
|
||||
/// </summary>
|
||||
/// <returns>工具栏按钮集合</returns>
|
||||
public virtual Button[] GetToolbarButtons()
|
||||
{
|
||||
return new Button[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的状态栏信息
|
||||
/// </summary>
|
||||
/// <returns>状态栏信息</returns>
|
||||
public virtual string GetStatusBarInfo()
|
||||
{
|
||||
return $"{Name} v{Version} - {(IsEnabled ? "已启用" : "已禁用")}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件配置变更时调用
|
||||
/// </summary>
|
||||
public virtual void OnConfigurationChanged()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 配置已变更");
|
||||
}
|
||||
|
||||
#region 便捷方法
|
||||
|
||||
/// <summary>
|
||||
/// 显示通知消息
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="type">消息类型</param>
|
||||
protected void ShowNotification(string message, NotificationType type = NotificationType.Info)
|
||||
{
|
||||
WindowService.ShowNotification(message, type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示确认对话框
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <returns>用户选择结果</returns>
|
||||
protected bool ShowConfirmDialog(string message, string title = "确认")
|
||||
{
|
||||
return WindowService.ShowConfirmDialog(message, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示输入对话框
|
||||
/// </summary>
|
||||
/// <param name="message">提示消息</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>用户输入内容</returns>
|
||||
protected string ShowInputDialog(string message, string title = "输入", string defaultValue = "")
|
||||
{
|
||||
return WindowService.ShowInputDialog(message, title, defaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>设置值</returns>
|
||||
protected T GetSetting<T>(string key, T defaultValue = default(T))
|
||||
{
|
||||
return GetService.GetSetting(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="value">设置值</param>
|
||||
protected void SetSetting<T>(string key, T value)
|
||||
{
|
||||
ActionService.SetSetting(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存设置
|
||||
/// </summary>
|
||||
protected void SaveSettings()
|
||||
{
|
||||
ActionService.SaveSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除当前画布
|
||||
/// </summary>
|
||||
protected void ClearCanvas()
|
||||
{
|
||||
ActionService.ClearCanvas();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 撤销操作
|
||||
/// </summary>
|
||||
protected void Undo()
|
||||
{
|
||||
ActionService.Undo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重做操作
|
||||
/// </summary>
|
||||
protected void Redo()
|
||||
{
|
||||
ActionService.Redo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以撤销
|
||||
/// </summary>
|
||||
protected bool CanUndo => GetService.CanUndo;
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以重做
|
||||
/// </summary>
|
||||
protected bool CanRedo => GetService.CanRedo;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前绘制模式
|
||||
/// </summary>
|
||||
protected int CurrentDrawingMode => GetService.CurrentDrawingMode;
|
||||
|
||||
/// <summary>
|
||||
/// 设置绘制模式
|
||||
/// </summary>
|
||||
/// <param name="mode">绘制模式</param>
|
||||
protected void SetDrawingMode(int mode)
|
||||
{
|
||||
ActionService.SetDrawingMode(mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
protected void RegisterEventHandler(string eventName, System.EventHandler handler)
|
||||
{
|
||||
ActionService.RegisterEventHandler(eventName, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注销事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
protected void UnregisterEventHandler(string eventName, System.EventHandler handler)
|
||||
{
|
||||
ActionService.UnregisterEventHandler(eventName, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发事件
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="args">事件参数</param>
|
||||
protected void TriggerEvent(string eventName, object sender, System.EventArgs args)
|
||||
{
|
||||
ActionService.TriggerEvent(eventName, sender, args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using System;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 操作服务接口,统一所有执行操作相关的方法
|
||||
/// </summary>
|
||||
public interface IActionService
|
||||
{
|
||||
#region 画布操作
|
||||
|
||||
/// <summary>
|
||||
/// 清除当前画布
|
||||
/// </summary>
|
||||
void ClearCanvas();
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有画布
|
||||
/// </summary>
|
||||
void ClearAllCanvases();
|
||||
|
||||
/// <summary>
|
||||
/// 添加新页面
|
||||
/// </summary>
|
||||
void AddNewPage();
|
||||
|
||||
/// <summary>
|
||||
/// 删除当前页面
|
||||
/// </summary>
|
||||
void DeleteCurrentPage();
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定页面
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">页面索引</param>
|
||||
void SwitchToPage(int pageIndex);
|
||||
|
||||
/// <summary>
|
||||
/// 切换到下一页
|
||||
/// </summary>
|
||||
void NextPage();
|
||||
|
||||
/// <summary>
|
||||
/// 切换到上一页
|
||||
/// </summary>
|
||||
void PreviousPage();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 绘制操作
|
||||
|
||||
/// <summary>
|
||||
/// 设置绘制模式
|
||||
/// </summary>
|
||||
/// <param name="mode">绘制模式</param>
|
||||
void SetDrawingMode(int mode);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触宽度
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
void SetInkWidth(double width);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触颜色
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
void SetInkColor(Color color);
|
||||
|
||||
/// <summary>
|
||||
/// 设置高亮笔宽度
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
void SetHighlighterWidth(double width);
|
||||
|
||||
/// <summary>
|
||||
/// 设置橡皮擦大小
|
||||
/// </summary>
|
||||
/// <param name="size">大小</param>
|
||||
void SetEraserSize(int size);
|
||||
|
||||
/// <summary>
|
||||
/// 设置橡皮擦类型
|
||||
/// </summary>
|
||||
/// <param name="type">类型</param>
|
||||
void SetEraserType(int type);
|
||||
|
||||
/// <summary>
|
||||
/// 设置橡皮擦形状
|
||||
/// </summary>
|
||||
/// <param name="shape">形状</param>
|
||||
void SetEraserShape(int shape);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触透明度
|
||||
/// </summary>
|
||||
/// <param name="alpha">透明度</param>
|
||||
void SetInkAlpha(double alpha);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触样式
|
||||
/// </summary>
|
||||
/// <param name="style">样式</param>
|
||||
void SetInkStyle(int style);
|
||||
|
||||
/// <summary>
|
||||
/// 设置背景颜色
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
void SetBackgroundColor(string color);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文件操作
|
||||
|
||||
/// <summary>
|
||||
/// 保存画布内容
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
void SaveCanvas(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// 加载画布内容
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
void LoadCanvas(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// 导出为图片
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="format">图片格式</param>
|
||||
void ExportAsImage(string filePath, string format);
|
||||
|
||||
/// <summary>
|
||||
/// 导出为PDF
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
void ExportAsPDF(string filePath);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 撤销重做操作
|
||||
|
||||
/// <summary>
|
||||
/// 撤销操作
|
||||
/// </summary>
|
||||
void Undo();
|
||||
|
||||
/// <summary>
|
||||
/// 重做操作
|
||||
/// </summary>
|
||||
void Redo();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 选择操作
|
||||
|
||||
/// <summary>
|
||||
/// 全选
|
||||
/// </summary>
|
||||
void SelectAll();
|
||||
|
||||
/// <summary>
|
||||
/// 取消选择
|
||||
/// </summary>
|
||||
void DeselectAll();
|
||||
|
||||
/// <summary>
|
||||
/// 删除选中内容
|
||||
/// </summary>
|
||||
void DeleteSelected();
|
||||
|
||||
/// <summary>
|
||||
/// 复制选中内容
|
||||
/// </summary>
|
||||
void CopySelected();
|
||||
|
||||
/// <summary>
|
||||
/// 剪切选中内容
|
||||
/// </summary>
|
||||
void CutSelected();
|
||||
|
||||
/// <summary>
|
||||
/// 粘贴内容
|
||||
/// </summary>
|
||||
void Paste();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 系统设置操作
|
||||
|
||||
/// <summary>
|
||||
/// 设置系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="value">设置值</param>
|
||||
void SetSetting<T>(string key, T value);
|
||||
|
||||
/// <summary>
|
||||
/// 保存设置到文件
|
||||
/// </summary>
|
||||
void SaveSettings();
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载设置
|
||||
/// </summary>
|
||||
void LoadSettings();
|
||||
|
||||
/// <summary>
|
||||
/// 重置设置为默认值
|
||||
/// </summary>
|
||||
void ResetSettings();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件管理操作
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
void EnablePlugin(string pluginName);
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
void DisablePlugin(string pluginName);
|
||||
|
||||
/// <summary>
|
||||
/// 卸载插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
void UnloadPlugin(string pluginName);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件系统操作
|
||||
|
||||
/// <summary>
|
||||
/// 注册事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
void RegisterEventHandler(string eventName, EventHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// 注销事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
void UnregisterEventHandler(string eventName, EventHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// 触发事件
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="args">事件参数</param>
|
||||
void TriggerEvent(string eventName, object sender, EventArgs args);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 应用程序操作
|
||||
|
||||
/// <summary>
|
||||
/// 重启应用程序
|
||||
/// </summary>
|
||||
void RestartApplication();
|
||||
|
||||
/// <summary>
|
||||
/// 退出应用程序
|
||||
/// </summary>
|
||||
void ExitApplication();
|
||||
|
||||
/// <summary>
|
||||
/// 检查更新
|
||||
/// </summary>
|
||||
void CheckForUpdates();
|
||||
|
||||
/// <summary>
|
||||
/// 打开帮助文档
|
||||
/// </summary>
|
||||
void OpenHelpDocument();
|
||||
|
||||
/// <summary>
|
||||
/// 打开关于页面
|
||||
/// </summary>
|
||||
void OpenAboutPage();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -45,4 +45,4 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
/// </summary>
|
||||
void OnConfigurationChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取服务接口,统一所有获取类的方法
|
||||
/// </summary>
|
||||
public interface IGetService
|
||||
{
|
||||
#region 窗口和UI获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取主窗口引用
|
||||
/// </summary>
|
||||
Window MainWindow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前画布
|
||||
/// </summary>
|
||||
InkCanvas CurrentCanvas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有画布页面
|
||||
/// </summary>
|
||||
List<Canvas> AllCanvasPages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前页面索引
|
||||
/// </summary>
|
||||
int CurrentPageIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前页面数量
|
||||
/// </summary>
|
||||
int TotalPageCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取浮动工具栏
|
||||
/// </summary>
|
||||
FrameworkElement FloatingToolBar { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取左侧面板
|
||||
/// </summary>
|
||||
FrameworkElement LeftPanel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取右侧面板
|
||||
/// </summary>
|
||||
FrameworkElement RightPanel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取顶部面板
|
||||
/// </summary>
|
||||
FrameworkElement TopPanel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取底部面板
|
||||
/// </summary>
|
||||
FrameworkElement BottomPanel { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 绘制工具状态获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前绘制模式
|
||||
/// </summary>
|
||||
int CurrentDrawingMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触宽度
|
||||
/// </summary>
|
||||
double CurrentInkWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触颜色
|
||||
/// </summary>
|
||||
Color CurrentInkColor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前高亮笔宽度
|
||||
/// </summary>
|
||||
double CurrentHighlighterWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前橡皮擦大小
|
||||
/// </summary>
|
||||
int CurrentEraserSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前橡皮擦类型
|
||||
/// </summary>
|
||||
int CurrentEraserType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前橡皮擦形状
|
||||
/// </summary>
|
||||
int CurrentEraserShape { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触透明度
|
||||
/// </summary>
|
||||
double CurrentInkAlpha { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触样式
|
||||
/// </summary>
|
||||
int CurrentInkStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前背景颜色
|
||||
/// </summary>
|
||||
string CurrentBackgroundColor { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 应用状态获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前主题模式
|
||||
/// </summary>
|
||||
bool IsDarkTheme { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为白板模式
|
||||
/// </summary>
|
||||
bool IsWhiteboardMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为PPT模式
|
||||
/// </summary>
|
||||
bool IsPPTMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为全屏模式
|
||||
/// </summary>
|
||||
bool IsFullScreenMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为画板模式
|
||||
/// </summary>
|
||||
bool IsCanvasMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为选择模式
|
||||
/// </summary>
|
||||
bool IsSelectionMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为擦除模式
|
||||
/// </summary>
|
||||
bool IsEraserMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为形状绘制模式
|
||||
/// </summary>
|
||||
bool IsShapeDrawingMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为高亮模式
|
||||
/// </summary>
|
||||
bool IsHighlighterMode { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 撤销重做状态获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否可以撤销
|
||||
/// </summary>
|
||||
bool CanUndo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否可以重做
|
||||
/// </summary>
|
||||
bool CanRedo { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 系统设置获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>设置值</returns>
|
||||
T GetSetting<T>(string key, T defaultValue = default(T));
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件信息获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有已加载的插件
|
||||
/// </summary>
|
||||
/// <returns>插件列表</returns>
|
||||
List<IPlugin> GetAllPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
/// <returns>插件实例</returns>
|
||||
IPlugin GetPlugin(string pluginName);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,532 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件服务接口,提供对软件内部功能的访问
|
||||
/// 继承自三个专门的服务接口:获取服务、窗口服务、操作服务
|
||||
/// </summary>
|
||||
public interface IPluginService
|
||||
public interface IPluginService : IGetService, IWindowService, IActionService
|
||||
{
|
||||
#region 窗口和UI访问
|
||||
|
||||
/// <summary>
|
||||
/// 获取主窗口引用
|
||||
/// </summary>
|
||||
Window MainWindow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前画布
|
||||
/// </summary>
|
||||
InkCanvas CurrentCanvas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有画布页面
|
||||
/// </summary>
|
||||
List<Canvas> AllCanvasPages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前页面索引
|
||||
/// </summary>
|
||||
int CurrentPageIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前页面数量
|
||||
/// </summary>
|
||||
int TotalPageCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取浮动工具栏
|
||||
/// </summary>
|
||||
FrameworkElement FloatingToolBar { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取左侧面板
|
||||
/// </summary>
|
||||
FrameworkElement LeftPanel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取右侧面板
|
||||
/// </summary>
|
||||
FrameworkElement RightPanel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取顶部面板
|
||||
/// </summary>
|
||||
FrameworkElement TopPanel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取底部面板
|
||||
/// </summary>
|
||||
FrameworkElement BottomPanel { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 绘制工具状态
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前绘制模式
|
||||
/// </summary>
|
||||
int CurrentDrawingMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触宽度
|
||||
/// </summary>
|
||||
double CurrentInkWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触颜色
|
||||
/// </summary>
|
||||
Color CurrentInkColor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前高亮笔宽度
|
||||
/// </summary>
|
||||
double CurrentHighlighterWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前橡皮擦大小
|
||||
/// </summary>
|
||||
int CurrentEraserSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前橡皮擦类型
|
||||
/// </summary>
|
||||
int CurrentEraserType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前橡皮擦形状
|
||||
/// </summary>
|
||||
int CurrentEraserShape { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触透明度
|
||||
/// </summary>
|
||||
double CurrentInkAlpha { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触样式
|
||||
/// </summary>
|
||||
int CurrentInkStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前背景颜色
|
||||
/// </summary>
|
||||
string CurrentBackgroundColor { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 应用状态
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前主题模式
|
||||
/// </summary>
|
||||
bool IsDarkTheme { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为白板模式
|
||||
/// </summary>
|
||||
bool IsWhiteboardMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为PPT模式
|
||||
/// </summary>
|
||||
bool IsPPTMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为全屏模式
|
||||
/// </summary>
|
||||
bool IsFullScreenMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为画板模式
|
||||
/// </summary>
|
||||
bool IsCanvasMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为选择模式
|
||||
/// </summary>
|
||||
bool IsSelectionMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为擦除模式
|
||||
/// </summary>
|
||||
bool IsEraserMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为形状绘制模式
|
||||
/// </summary>
|
||||
bool IsShapeDrawingMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为高亮模式
|
||||
/// </summary>
|
||||
bool IsHighlighterMode { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 画布操作
|
||||
|
||||
/// <summary>
|
||||
/// 清除当前画布
|
||||
/// </summary>
|
||||
void ClearCanvas();
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有画布
|
||||
/// </summary>
|
||||
void ClearAllCanvases();
|
||||
|
||||
/// <summary>
|
||||
/// 添加新页面
|
||||
/// </summary>
|
||||
void AddNewPage();
|
||||
|
||||
/// <summary>
|
||||
/// 删除当前页面
|
||||
/// </summary>
|
||||
void DeleteCurrentPage();
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定页面
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">页面索引</param>
|
||||
void SwitchToPage(int pageIndex);
|
||||
|
||||
/// <summary>
|
||||
/// 切换到下一页
|
||||
/// </summary>
|
||||
void NextPage();
|
||||
|
||||
/// <summary>
|
||||
/// 切换到上一页
|
||||
/// </summary>
|
||||
void PreviousPage();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 绘制操作
|
||||
|
||||
/// <summary>
|
||||
/// 设置绘制模式
|
||||
/// </summary>
|
||||
/// <param name="mode">绘制模式</param>
|
||||
void SetDrawingMode(int mode);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触宽度
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
void SetInkWidth(double width);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触颜色
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
void SetInkColor(Color color);
|
||||
|
||||
/// <summary>
|
||||
/// 设置高亮笔宽度
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
void SetHighlighterWidth(double width);
|
||||
|
||||
/// <summary>
|
||||
/// 设置橡皮擦大小
|
||||
/// </summary>
|
||||
/// <param name="size">大小</param>
|
||||
void SetEraserSize(int size);
|
||||
|
||||
/// <summary>
|
||||
/// 设置橡皮擦类型
|
||||
/// </summary>
|
||||
/// <param name="type">类型</param>
|
||||
void SetEraserType(int type);
|
||||
|
||||
/// <summary>
|
||||
/// 设置橡皮擦形状
|
||||
/// </summary>
|
||||
/// <param name="shape">形状</param>
|
||||
void SetEraserShape(int shape);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触透明度
|
||||
/// </summary>
|
||||
/// <param name="alpha">透明度</param>
|
||||
void SetInkAlpha(double alpha);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触样式
|
||||
/// </summary>
|
||||
/// <param name="style">样式</param>
|
||||
void SetInkStyle(int style);
|
||||
|
||||
/// <summary>
|
||||
/// 设置背景颜色
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
void SetBackgroundColor(string color);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文件操作
|
||||
|
||||
/// <summary>
|
||||
/// 保存画布内容
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
void SaveCanvas(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// 加载画布内容
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
void LoadCanvas(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// 导出为图片
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="format">图片格式</param>
|
||||
void ExportAsImage(string filePath, string format);
|
||||
|
||||
/// <summary>
|
||||
/// 导出为PDF
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
void ExportAsPDF(string filePath);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 撤销重做
|
||||
|
||||
/// <summary>
|
||||
/// 撤销操作
|
||||
/// </summary>
|
||||
void Undo();
|
||||
|
||||
/// <summary>
|
||||
/// 重做操作
|
||||
/// </summary>
|
||||
void Redo();
|
||||
|
||||
/// <summary>
|
||||
/// 是否可以撤销
|
||||
/// </summary>
|
||||
bool CanUndo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可以重做
|
||||
/// </summary>
|
||||
bool CanRedo { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 选择操作
|
||||
|
||||
/// <summary>
|
||||
/// 全选
|
||||
/// </summary>
|
||||
void SelectAll();
|
||||
|
||||
/// <summary>
|
||||
/// 取消选择
|
||||
/// </summary>
|
||||
void DeselectAll();
|
||||
|
||||
/// <summary>
|
||||
/// 删除选中内容
|
||||
/// </summary>
|
||||
void DeleteSelected();
|
||||
|
||||
/// <summary>
|
||||
/// 复制选中内容
|
||||
/// </summary>
|
||||
void CopySelected();
|
||||
|
||||
/// <summary>
|
||||
/// 剪切选中内容
|
||||
/// </summary>
|
||||
void CutSelected();
|
||||
|
||||
/// <summary>
|
||||
/// 粘贴内容
|
||||
/// </summary>
|
||||
void Paste();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 窗口管理
|
||||
|
||||
/// <summary>
|
||||
/// 显示设置窗口
|
||||
/// </summary>
|
||||
void ShowSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏设置窗口
|
||||
/// </summary>
|
||||
void HideSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 显示插件设置窗口
|
||||
/// </summary>
|
||||
void ShowPluginSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏插件设置窗口
|
||||
/// </summary>
|
||||
void HidePluginSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 显示帮助窗口
|
||||
/// </summary>
|
||||
void ShowHelpWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏帮助窗口
|
||||
/// </summary>
|
||||
void HideHelpWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 显示关于窗口
|
||||
/// </summary>
|
||||
void ShowAboutWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏关于窗口
|
||||
/// </summary>
|
||||
void HideAboutWindow();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 通知和消息
|
||||
|
||||
/// <summary>
|
||||
/// 显示通知消息
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="type">消息类型</param>
|
||||
void ShowNotification(string message, NotificationType type = NotificationType.Info);
|
||||
|
||||
/// <summary>
|
||||
/// 显示确认对话框
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <returns>用户选择结果</returns>
|
||||
bool ShowConfirmDialog(string message, string title = "确认");
|
||||
|
||||
/// <summary>
|
||||
/// 显示输入对话框
|
||||
/// </summary>
|
||||
/// <param name="message">提示消息</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>用户输入内容</returns>
|
||||
string ShowInputDialog(string message, string title = "输入", string defaultValue = "");
|
||||
|
||||
#endregion
|
||||
|
||||
#region 系统功能
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>设置值</returns>
|
||||
T GetSetting<T>(string key, T defaultValue = default(T));
|
||||
|
||||
/// <summary>
|
||||
/// 设置系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="value">设置值</param>
|
||||
void SetSetting<T>(string key, T value);
|
||||
|
||||
/// <summary>
|
||||
/// 保存设置到文件
|
||||
/// </summary>
|
||||
void SaveSettings();
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载设置
|
||||
/// </summary>
|
||||
void LoadSettings();
|
||||
|
||||
/// <summary>
|
||||
/// 重置设置为默认值
|
||||
/// </summary>
|
||||
void ResetSettings();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件管理
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有已加载的插件
|
||||
/// </summary>
|
||||
/// <returns>插件列表</returns>
|
||||
List<IPlugin> GetAllPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
/// <returns>插件实例</returns>
|
||||
IPlugin GetPlugin(string pluginName);
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
void EnablePlugin(string pluginName);
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
void DisablePlugin(string pluginName);
|
||||
|
||||
/// <summary>
|
||||
/// 卸载插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
void UnloadPlugin(string pluginName);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件系统
|
||||
|
||||
/// <summary>
|
||||
/// 注册事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
void RegisterEventHandler(string eventName, EventHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// 注销事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
void UnregisterEventHandler(string eventName, EventHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// 触发事件
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="args">事件参数</param>
|
||||
void TriggerEvent(string eventName, object sender, EventArgs args);
|
||||
|
||||
#endregion
|
||||
// 这个接口现在继承自三个专门的服务接口
|
||||
// 所有方法都在子接口中定义,这里不需要重复定义
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -554,4 +35,4 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 窗口服务接口,统一所有窗口操作相关的方法
|
||||
/// </summary>
|
||||
public interface IWindowService
|
||||
{
|
||||
#region 窗口显示和隐藏
|
||||
|
||||
/// <summary>
|
||||
/// 显示设置窗口
|
||||
/// </summary>
|
||||
void ShowSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏设置窗口
|
||||
/// </summary>
|
||||
void HideSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 显示插件设置窗口
|
||||
/// </summary>
|
||||
void ShowPluginSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏插件设置窗口
|
||||
/// </summary>
|
||||
void HidePluginSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 显示帮助窗口
|
||||
/// </summary>
|
||||
void ShowHelpWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏帮助窗口
|
||||
/// </summary>
|
||||
void HideHelpWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 显示关于窗口
|
||||
/// </summary>
|
||||
void ShowAboutWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏关于窗口
|
||||
/// </summary>
|
||||
void HideAboutWindow();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 对话框和通知
|
||||
|
||||
/// <summary>
|
||||
/// 显示通知消息
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="type">消息类型</param>
|
||||
void ShowNotification(string message, NotificationType type = NotificationType.Info);
|
||||
|
||||
/// <summary>
|
||||
/// 显示确认对话框
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <returns>用户选择结果</returns>
|
||||
bool ShowConfirmDialog(string message, string title = "确认");
|
||||
|
||||
/// <summary>
|
||||
/// 显示输入对话框
|
||||
/// </summary>
|
||||
/// <param name="message">提示消息</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>用户输入内容</returns>
|
||||
string ShowInputDialog(string message, string title = "输入", string defaultValue = "");
|
||||
|
||||
#endregion
|
||||
|
||||
#region 窗口状态控制
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口全屏状态
|
||||
/// </summary>
|
||||
/// <param name="isFullScreen">是否全屏</param>
|
||||
void SetFullScreen(bool isFullScreen);
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口置顶状态
|
||||
/// </summary>
|
||||
/// <param name="isTopMost">是否置顶</param>
|
||||
void SetTopMost(bool isTopMost);
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口可见性
|
||||
/// </summary>
|
||||
/// <param name="isVisible">是否可见</param>
|
||||
void SetWindowVisibility(bool isVisible);
|
||||
|
||||
/// <summary>
|
||||
/// 最小化窗口
|
||||
/// </summary>
|
||||
void MinimizeWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 最大化窗口
|
||||
/// </summary>
|
||||
void MaximizeWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 恢复窗口
|
||||
/// </summary>
|
||||
void RestoreWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 关闭窗口
|
||||
/// </summary>
|
||||
void CloseWindow();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 窗口位置和大小
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口位置
|
||||
/// </summary>
|
||||
/// <param name="x">X坐标</param>
|
||||
/// <param name="y">Y坐标</param>
|
||||
void SetWindowPosition(double x, double y);
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口大小
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
/// <param name="height">高度</param>
|
||||
void SetWindowSize(double width, double height);
|
||||
|
||||
/// <summary>
|
||||
/// 获取窗口位置
|
||||
/// </summary>
|
||||
/// <returns>窗口位置</returns>
|
||||
(double x, double y) GetWindowPosition();
|
||||
|
||||
/// <summary>
|
||||
/// 获取窗口大小
|
||||
/// </summary>
|
||||
/// <returns>窗口大小</returns>
|
||||
(double width, double height) GetWindowSize();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
@@ -270,4 +270,4 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Ink_Canvas.Windows;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -8,8 +10,6 @@ using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Ink_Canvas.Windows;
|
||||
using Newtonsoft.Json;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
@@ -20,8 +20,8 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
public class PluginManager
|
||||
{
|
||||
private static readonly string PluginsDirectory = Path.Combine(App.RootPath, "Plugins");
|
||||
private static readonly string PluginConfigFile = Path.Combine(App.RootPath, "PluginConfig.json");
|
||||
private static readonly string PluginConfigBackupFile = Path.Combine(App.RootPath, "PluginConfig.json.bak");
|
||||
private static readonly string PluginConfigFile = Path.Combine(App.RootPath, "Configs", "PluginConfig.json");
|
||||
private static readonly string PluginConfigBackupFile = Path.Combine(App.RootPath, "Configs", "PluginConfig.json.bak");
|
||||
|
||||
private static PluginManager _instance;
|
||||
private static SemaphoreSlim _configLock = new SemaphoreSlim(1, 1);
|
||||
@@ -79,8 +79,6 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
Directory.CreateDirectory(PluginsDirectory);
|
||||
}
|
||||
|
||||
// 加载插件配置
|
||||
LoadConfig();
|
||||
|
||||
// 初始化自动保存计时器(3秒)
|
||||
_autoSaveTimer = new Timer(3000);
|
||||
|
||||
@@ -115,7 +115,149 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
|
||||
#endregion
|
||||
|
||||
#region 画布操作
|
||||
#region IGetService 实现
|
||||
|
||||
public bool CanUndo => false; // 暂时返回默认值
|
||||
|
||||
public bool CanRedo => false; // 暂时返回默认值
|
||||
|
||||
public T GetSetting<T>(string key, T defaultValue = default(T))
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public List<IPlugin> GetAllPlugins()
|
||||
{
|
||||
return new List<IPlugin>(PluginManager.Instance.Plugins);
|
||||
}
|
||||
|
||||
public IPlugin GetPlugin(string pluginName)
|
||||
{
|
||||
return PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IWindowService 实现
|
||||
|
||||
public void ShowSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HideSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowPluginSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HidePluginSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowHelpWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HideHelpWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowAboutWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HideAboutWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowNotification(string message, NotificationType type = NotificationType.Info)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public bool ShowConfirmDialog(string message, string title = "确认")
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return false;
|
||||
}
|
||||
|
||||
public string ShowInputDialog(string message, string title = "输入", string defaultValue = "")
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void SetFullScreen(bool isFullScreen)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetTopMost(bool isTopMost)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetWindowVisibility(bool isVisible)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void MinimizeWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void MaximizeWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void RestoreWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void CloseWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetWindowPosition(double x, double y)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetWindowSize(double width, double height)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public (double x, double y) GetWindowPosition()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
public (double width, double height) GetWindowSize()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return (800, 600);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IActionService 实现
|
||||
|
||||
public void ClearCanvas()
|
||||
{
|
||||
@@ -152,10 +294,6 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 绘制操作
|
||||
|
||||
public void SetDrawingMode(int mode)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
@@ -206,10 +344,6 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文件操作
|
||||
|
||||
public void SaveCanvas(string filePath)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
@@ -230,10 +364,6 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 撤销重做
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
@@ -244,14 +374,6 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public bool CanUndo => false; // 暂时返回默认值
|
||||
|
||||
public bool CanRedo => false; // 暂时返回默认值
|
||||
|
||||
#endregion
|
||||
|
||||
#region 选择操作
|
||||
|
||||
public void SelectAll()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
@@ -282,81 +404,6 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 窗口管理
|
||||
|
||||
public void ShowSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HideSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowPluginSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HidePluginSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowHelpWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HideHelpWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowAboutWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HideAboutWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 通知和消息
|
||||
|
||||
public void ShowNotification(string message, NotificationType type = NotificationType.Info)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public bool ShowConfirmDialog(string message, string title = "确认")
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return false;
|
||||
}
|
||||
|
||||
public string ShowInputDialog(string message, string title = "输入", string defaultValue = "")
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 系统功能
|
||||
|
||||
public T GetSetting<T>(string key, T defaultValue = default(T))
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void SetSetting<T>(string key, T value)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
@@ -377,20 +424,6 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件管理
|
||||
|
||||
public List<IPlugin> GetAllPlugins()
|
||||
{
|
||||
return new List<IPlugin>(PluginManager.Instance.Plugins);
|
||||
}
|
||||
|
||||
public IPlugin GetPlugin(string pluginName)
|
||||
{
|
||||
return PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName);
|
||||
}
|
||||
|
||||
public void EnablePlugin(string pluginName)
|
||||
{
|
||||
var plugin = GetPlugin(pluginName);
|
||||
@@ -418,10 +451,6 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件系统
|
||||
|
||||
public void RegisterEventHandler(string eventName, EventHandler handler)
|
||||
{
|
||||
if (!_eventHandlers.ContainsKey(eventName))
|
||||
@@ -450,6 +479,31 @@ namespace Ink_Canvas.Helpers.Plugins
|
||||
}
|
||||
}
|
||||
|
||||
public void RestartApplication()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ExitApplication()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void CheckForUpdates()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void OpenHelpDocument()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void OpenAboutPage()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Interop;
|
||||
using Point = System.Windows.Point;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 屏幕检测帮助类 - 用于检测窗口所在的屏幕和屏幕信息
|
||||
/// </summary>
|
||||
public static class ScreenDetectionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取窗口所在的屏幕
|
||||
/// </summary>
|
||||
/// <param name="window">要检测的窗口</param>
|
||||
/// <returns>窗口所在的屏幕,如果无法检测则返回主屏幕</returns>
|
||||
public static Screen GetWindowScreen(Window window)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (window == null)
|
||||
return Screen.PrimaryScreen;
|
||||
|
||||
// 获取窗口的句柄
|
||||
var hwndSource = PresentationSource.FromVisual(window) as HwndSource;
|
||||
if (hwndSource == null)
|
||||
return Screen.PrimaryScreen;
|
||||
|
||||
// 获取窗口在屏幕上的位置
|
||||
var windowRect = GetWindowScreenBounds(window);
|
||||
|
||||
// 查找与窗口重叠最多的屏幕
|
||||
Screen targetScreen = null;
|
||||
double maxIntersection = 0;
|
||||
|
||||
foreach (var screen in Screen.AllScreens)
|
||||
{
|
||||
var intersection = Rectangle.Intersect(windowRect, screen.Bounds);
|
||||
if (intersection.Width * intersection.Height > maxIntersection)
|
||||
{
|
||||
maxIntersection = intersection.Width * intersection.Height;
|
||||
targetScreen = screen;
|
||||
}
|
||||
}
|
||||
|
||||
return targetScreen ?? Screen.PrimaryScreen;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"检测窗口屏幕时出错: {ex.Message}", LogHelper.LogType.Warning);
|
||||
return Screen.PrimaryScreen;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取窗口在屏幕坐标系中的边界
|
||||
/// </summary>
|
||||
/// <param name="window">要检测的窗口</param>
|
||||
/// <returns>窗口的屏幕边界</returns>
|
||||
private static Rectangle GetWindowScreenBounds(Window window)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取窗口左上角在屏幕上的位置
|
||||
var topLeft = window.PointToScreen(new Point(0, 0));
|
||||
|
||||
// 获取窗口右下角在屏幕上的位置
|
||||
var bottomRight = window.PointToScreen(new Point(window.ActualWidth, window.ActualHeight));
|
||||
|
||||
return new Rectangle(
|
||||
(int)topLeft.X,
|
||||
(int)topLeft.Y,
|
||||
(int)(bottomRight.X - topLeft.X),
|
||||
(int)(bottomRight.Y - topLeft.Y));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果无法获取精确位置,返回窗口的Left和Top
|
||||
return new Rectangle(
|
||||
(int)window.Left,
|
||||
(int)window.Top,
|
||||
(int)window.Width,
|
||||
(int)window.Height);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有多个屏幕
|
||||
/// </summary>
|
||||
/// <returns>如果有多个屏幕返回true,否则返回false</returns>
|
||||
public static bool HasMultipleScreens()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Screen.AllScreens.Length > 1;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取主屏幕
|
||||
/// </summary>
|
||||
/// <returns>主屏幕</returns>
|
||||
public static Screen GetPrimaryScreen()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Screen.PrimaryScreen;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有屏幕信息
|
||||
/// </summary>
|
||||
/// <returns>所有屏幕的数组</returns>
|
||||
public static Screen[] GetAllScreens()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Screen.AllScreens;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new Screen[] { Screen.PrimaryScreen };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查窗口是否在主屏幕上
|
||||
/// </summary>
|
||||
/// <param name="window">要检查的窗口</param>
|
||||
/// <returns>如果窗口在主屏幕上返回true,否则返回false</returns>
|
||||
public static bool IsWindowOnPrimaryScreen(Window window)
|
||||
{
|
||||
try
|
||||
{
|
||||
var windowScreen = GetWindowScreen(window);
|
||||
return windowScreen == Screen.PrimaryScreen;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; // 出错时假设在主屏幕
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// UIAccess DLL释放器
|
||||
/// </summary>
|
||||
public static class UIAccessDllExtractor
|
||||
{
|
||||
private static readonly string[] RequiredDlls = {
|
||||
"UIAccessDLL_x64.dll",
|
||||
"UIAccessDLL_x86.dll"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 在应用启动时释放UIAccess相关DLL
|
||||
/// </summary>
|
||||
public static void ExtractUIAccessDlls()
|
||||
{
|
||||
try
|
||||
{
|
||||
string appDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
LogHelper.WriteLogToFile("开始检查并释放UIAccess相关DLL文件");
|
||||
|
||||
foreach (string dllName in RequiredDlls)
|
||||
{
|
||||
string targetPath = Path.Combine(appDirectory, dllName);
|
||||
|
||||
// 检查文件是否已存在且有效
|
||||
if (File.Exists(targetPath) && IsValidDll(targetPath))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"{dllName} 已存在且有效,跳过释放");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 从嵌入资源中释放DLL
|
||||
if (ExtractDllFromResource(dllName, targetPath))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"成功释放 {dllName} 到 {targetPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"警告:无法释放 {dllName},可能影响UIA置顶功能", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("UIAccess DLL释放检查完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"释放UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从嵌入资源中提取DLL文件
|
||||
/// </summary>
|
||||
private static bool ExtractDllFromResource(string dllName, string targetPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
string resourceName = $"Ink_Canvas.{dllName}";
|
||||
|
||||
using (Stream resourceStream = assembly.GetManifestResourceStream(resourceName))
|
||||
{
|
||||
if (resourceStream == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"未找到嵌入资源: {resourceName}", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
string targetDirectory = Path.GetDirectoryName(targetPath);
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
using (FileStream fileStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
resourceStream.CopyTo(fileStream);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从资源提取 {dllName} 失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查DLL文件是否有效
|
||||
/// </summary>
|
||||
private static bool IsValidDll(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
return false;
|
||||
|
||||
FileInfo fileInfo = new FileInfo(filePath);
|
||||
|
||||
// 检查文件大小(空文件或过小的文件可能无效)
|
||||
if (fileInfo.Length < 1024) // 小于1KB可能无效
|
||||
return false;
|
||||
|
||||
// 简单检查PE头(DLL文件应该以MZ开头)
|
||||
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
byte[] buffer = new byte[2];
|
||||
if (fs.Read(buffer, 0, 2) == 2)
|
||||
{
|
||||
return buffer[0] == 0x4D && buffer[1] == 0x5A; // "MZ"
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理释放的DLL文件(可选,在应用退出时调用)
|
||||
/// </summary>
|
||||
public static void CleanupExtractedDlls()
|
||||
{
|
||||
try
|
||||
{
|
||||
string appDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
foreach (string dllName in RequiredDlls)
|
||||
{
|
||||
string filePath = Path.Combine(appDirectory, dllName);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
LogHelper.WriteLogToFile($"已清理 {dllName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理 {dllName} 失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩形结构体(用于窗口位置和大小)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct WindowRect
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
|
||||
public int Width => Right - Left;
|
||||
public int Height => Bottom - Top;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口信息结构
|
||||
/// </summary>
|
||||
public class WindowInfo
|
||||
{
|
||||
public IntPtr Handle { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string ClassName { get; set; }
|
||||
public string ProcessName { get; set; }
|
||||
public string ProcessPath { get; set; }
|
||||
public WindowRect Rect { get; set; }
|
||||
public bool IsVisible { get; set; }
|
||||
public bool IsMinimized { get; set; }
|
||||
public bool IsMaximized { get; set; }
|
||||
public int ZOrder { get; set; }
|
||||
public uint ProcessId { get; set; }
|
||||
public bool IsFullScreen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计算窗口是否覆盖指定区域
|
||||
/// </summary>
|
||||
public bool CoversArea(WindowRect area)
|
||||
{
|
||||
if (!IsVisible || IsMinimized) return false;
|
||||
|
||||
// 计算交集
|
||||
int left = Math.Max(Rect.Left, area.Left);
|
||||
int top = Math.Max(Rect.Top, area.Top);
|
||||
int right = Math.Min(Rect.Right, area.Right);
|
||||
int bottom = Math.Min(Rect.Bottom, area.Bottom);
|
||||
|
||||
// 如果有交集,说明窗口覆盖了该区域
|
||||
return left < right && top < bottom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算窗口覆盖指定区域的比例
|
||||
/// </summary>
|
||||
public double GetCoverageRatio(WindowRect area)
|
||||
{
|
||||
if (!IsVisible || IsMinimized) return 0.0;
|
||||
|
||||
// 计算交集
|
||||
int left = Math.Max(Rect.Left, area.Left);
|
||||
int top = Math.Max(Rect.Top, area.Top);
|
||||
int right = Math.Min(Rect.Right, area.Right);
|
||||
int bottom = Math.Min(Rect.Bottom, area.Bottom);
|
||||
|
||||
if (left >= right || top >= bottom) return 0.0;
|
||||
|
||||
// 计算交集面积
|
||||
double intersectionArea = (right - left) * (bottom - top);
|
||||
// 计算目标区域面积
|
||||
double targetArea = area.Width * area.Height;
|
||||
|
||||
if (targetArea == 0) return 0.0;
|
||||
|
||||
return intersectionArea / targetArea;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口概览模型 - 实时监控桌面所有可见窗口并计算遮挡情况
|
||||
/// </summary>
|
||||
public class WindowOverviewModel : IDisposable
|
||||
{
|
||||
#region Win32 API Declarations
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetWindowRect(IntPtr hWnd, out WindowRect lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsIconic(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsZoomed(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
|
||||
|
||||
private const uint GW_HWNDNEXT = 2;
|
||||
private const uint GW_HWNDPREV = 3;
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
#endregion
|
||||
|
||||
private readonly object _lockObject = new object();
|
||||
private List<WindowInfo> _windows = new List<WindowInfo>();
|
||||
private Timer _updateTimer;
|
||||
private bool _isDisposed = false;
|
||||
private readonly int _updateInterval = 1000; // 更新间隔(毫秒)
|
||||
|
||||
private readonly Dictionary<uint, ProcessCacheInfo> _processCache = new Dictionary<uint, ProcessCacheInfo>();
|
||||
private readonly object _processCacheLock = new object();
|
||||
private DateTime _lastProcessCacheCleanup = DateTime.Now;
|
||||
private const int PROCESS_CACHE_CLEANUP_INTERVAL_MS = 30000;
|
||||
|
||||
// 窗口缓存,用于增量更新
|
||||
private readonly Dictionary<IntPtr, WindowInfo> _windowCache = new Dictionary<IntPtr, WindowInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// 进程缓存信息
|
||||
/// </summary>
|
||||
private class ProcessCacheInfo
|
||||
{
|
||||
public string ProcessName { get; set; }
|
||||
public string ProcessPath { get; set; }
|
||||
public DateTime LastAccessTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口列表更新事件
|
||||
/// </summary>
|
||||
public event EventHandler<List<WindowInfo>> WindowsUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// 当前窗口列表(按Z顺序排序,最上层在前)
|
||||
/// </summary>
|
||||
public List<WindowInfo> Windows
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return new List<WindowInfo>(_windows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public WindowOverviewModel()
|
||||
{
|
||||
// 立即执行一次更新
|
||||
UpdateWindows();
|
||||
|
||||
// 启动定时器,定期更新窗口列表
|
||||
_updateTimer = new Timer(OnUpdateTimer, null, _updateInterval, _updateInterval);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定时器回调
|
||||
/// </summary>
|
||||
private void OnUpdateTimer(object state)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
UpdateWindows();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"窗口概览模型更新失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取进程信息
|
||||
/// </summary>
|
||||
private (string processName, string processPath) GetProcessInfo(uint processId)
|
||||
{
|
||||
lock (_processCacheLock)
|
||||
{
|
||||
// 定期清理缓存
|
||||
var now = DateTime.Now;
|
||||
if ((now - _lastProcessCacheCleanup).TotalMilliseconds > PROCESS_CACHE_CLEANUP_INTERVAL_MS)
|
||||
{
|
||||
var keysToRemove = _processCache
|
||||
.Where(kvp => (now - kvp.Value.LastAccessTime).TotalMilliseconds > PROCESS_CACHE_CLEANUP_INTERVAL_MS)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_processCache.Remove(key);
|
||||
}
|
||||
|
||||
_lastProcessCacheCleanup = now;
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
if (_processCache.TryGetValue(processId, out var cachedInfo))
|
||||
{
|
||||
cachedInfo.LastAccessTime = now;
|
||||
return (cachedInfo.ProcessName, cachedInfo.ProcessPath);
|
||||
}
|
||||
|
||||
// 缓存未命中,获取进程信息
|
||||
string processName = "Unknown";
|
||||
string processPath = "Unknown";
|
||||
|
||||
try
|
||||
{
|
||||
Process process = Process.GetProcessById((int)processId);
|
||||
processName = process.ProcessName;
|
||||
try
|
||||
{
|
||||
processPath = process.MainModule?.FileName ?? "Unknown";
|
||||
}
|
||||
catch
|
||||
{
|
||||
processPath = "Unknown";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 进程可能已退出
|
||||
}
|
||||
|
||||
// 添加到缓存
|
||||
_processCache[processId] = new ProcessCacheInfo
|
||||
{
|
||||
ProcessName = processName,
|
||||
ProcessPath = processPath,
|
||||
LastAccessTime = now
|
||||
};
|
||||
|
||||
return (processName, processPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查窗口信息是否发生变化
|
||||
/// </summary>
|
||||
private bool HasWindowChanged(IntPtr hWnd, WindowRect rect, bool isMinimized, bool isMaximized, bool isFullScreen)
|
||||
{
|
||||
if (!_windowCache.TryGetValue(hWnd, out var cachedWindow))
|
||||
{
|
||||
return true; // 新窗口
|
||||
}
|
||||
|
||||
// 检查关键属性是否变化
|
||||
return cachedWindow.Rect.Left != rect.Left ||
|
||||
cachedWindow.Rect.Top != rect.Top ||
|
||||
cachedWindow.Rect.Right != rect.Right ||
|
||||
cachedWindow.Rect.Bottom != rect.Bottom ||
|
||||
cachedWindow.IsMinimized != isMinimized ||
|
||||
cachedWindow.IsMaximized != isMaximized ||
|
||||
cachedWindow.IsFullScreen != isFullScreen;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新窗口列表
|
||||
/// </summary>
|
||||
public void UpdateWindows()
|
||||
{
|
||||
var windows = new List<WindowInfo>();
|
||||
var zOrder = 0;
|
||||
var currentWindowHandles = new HashSet<IntPtr>();
|
||||
|
||||
EnumWindows((hWnd, lParam) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查窗口是否可见
|
||||
if (!IsWindowVisible(hWnd)) return true;
|
||||
|
||||
// 检查窗口是否最小化
|
||||
bool isMinimized = IsIconic(hWnd);
|
||||
if (isMinimized) return true;
|
||||
|
||||
// 获取窗口矩形
|
||||
if (!GetWindowRect(hWnd, out WindowRect rect)) return true;
|
||||
|
||||
// 过滤掉无效的窗口
|
||||
if (rect.Width <= 0 || rect.Height <= 0) return true;
|
||||
if (rect.Right < rect.Left || rect.Bottom < rect.Top) return true;
|
||||
|
||||
// 检查是否最大化
|
||||
bool isMaximized = IsZoomed(hWnd);
|
||||
|
||||
// 检查是否全屏(窗口大小接近屏幕大小)
|
||||
bool isFullScreen = false;
|
||||
try
|
||||
{
|
||||
var screen = System.Windows.Forms.Screen.FromHandle(hWnd);
|
||||
var screenBounds = screen.Bounds;
|
||||
// 如果窗口大小接近屏幕大小(允许10像素误差),认为是全屏
|
||||
isFullScreen = rect.Width >= screenBounds.Width - 10 &&
|
||||
rect.Height >= screenBounds.Height - 10 &&
|
||||
Math.Abs(rect.Left - screenBounds.Left) <= 10 &&
|
||||
Math.Abs(rect.Top - screenBounds.Top) <= 10;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 无法获取屏幕信息,使用默认值
|
||||
}
|
||||
|
||||
// 检查窗口是否发生变化
|
||||
bool windowChanged = HasWindowChanged(hWnd, rect, isMinimized, isMaximized, isFullScreen);
|
||||
|
||||
// 获取进程信息
|
||||
GetWindowThreadProcessId(hWnd, out uint processId);
|
||||
|
||||
// 使用缓存的进程信息
|
||||
var (processName, processPath) = GetProcessInfo(processId);
|
||||
|
||||
// 跳过当前应用程序的窗口(避免检测到自己)
|
||||
if (processName == "InkCanvasForClass" || processName == "Ink Canvas")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果窗口信息未变化且已缓存,尝试重用缓存的数据
|
||||
WindowInfo windowInfo;
|
||||
if (!windowChanged && _windowCache.TryGetValue(hWnd, out var cachedInfo))
|
||||
{
|
||||
// 重用缓存的窗口信息,只更新Z顺序和可能变化的状态
|
||||
windowInfo = new WindowInfo
|
||||
{
|
||||
Handle = hWnd,
|
||||
Title = cachedInfo.Title,
|
||||
ClassName = cachedInfo.ClassName,
|
||||
ProcessName = cachedInfo.ProcessName,
|
||||
ProcessPath = cachedInfo.ProcessPath,
|
||||
Rect = rect, // 使用最新的rect(虽然理论上应该相同)
|
||||
IsVisible = true,
|
||||
IsMinimized = false,
|
||||
IsMaximized = isMaximized,
|
||||
ZOrder = zOrder++,
|
||||
ProcessId = processId,
|
||||
IsFullScreen = isFullScreen
|
||||
};
|
||||
|
||||
// 更新缓存以保持ZOrder等属性最新
|
||||
_windowCache[hWnd] = windowInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 窗口信息变化或新窗口,需要获取完整信息
|
||||
// 获取窗口标题
|
||||
const int nChars = 256;
|
||||
StringBuilder windowTitle = new StringBuilder(nChars);
|
||||
GetWindowText(hWnd, windowTitle, nChars);
|
||||
string title = windowTitle.ToString();
|
||||
|
||||
// 获取窗口类名
|
||||
StringBuilder className = new StringBuilder(nChars);
|
||||
GetClassName(hWnd, className, nChars);
|
||||
string classNameStr = className.ToString();
|
||||
|
||||
windowInfo = new WindowInfo
|
||||
{
|
||||
Handle = hWnd,
|
||||
Title = title,
|
||||
ClassName = classNameStr,
|
||||
ProcessName = processName,
|
||||
ProcessPath = processPath,
|
||||
Rect = rect,
|
||||
IsVisible = true,
|
||||
IsMinimized = false,
|
||||
IsMaximized = isMaximized,
|
||||
ZOrder = zOrder++,
|
||||
ProcessId = processId,
|
||||
IsFullScreen = isFullScreen
|
||||
};
|
||||
|
||||
// 更新缓存
|
||||
_windowCache[hWnd] = windowInfo;
|
||||
}
|
||||
|
||||
windows.Add(windowInfo);
|
||||
currentWindowHandles.Add(hWnd);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略单个窗口的错误,继续枚举其他窗口
|
||||
}
|
||||
|
||||
return true; // 继续枚举
|
||||
}, IntPtr.Zero);
|
||||
|
||||
// 清理已关闭的窗口缓存
|
||||
var handlesToRemove = _windowCache.Keys.Where(h => !currentWindowHandles.Contains(h)).ToList();
|
||||
foreach (var handle in handlesToRemove)
|
||||
{
|
||||
_windowCache.Remove(handle);
|
||||
}
|
||||
|
||||
windows = windows.OrderByDescending(w => w.ZOrder).ToList();
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
_windows = windows;
|
||||
}
|
||||
|
||||
// 触发更新事件
|
||||
WindowsUpdated?.Invoke(this, windows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定区域是否被其他窗口覆盖
|
||||
/// </summary>
|
||||
/// <param name="area">要检查的区域</param>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表(例如当前应用程序)</param>
|
||||
/// <param name="coverageThreshold">覆盖阈值(0.0-1.0),超过此阈值认为被覆盖</param>
|
||||
/// <returns>如果被覆盖返回true</returns>
|
||||
public bool IsAreaCovered(WindowRect area, List<string> excludeProcessNames = null, double coverageThreshold = 0.5)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
|
||||
// 从最上层窗口开始检查
|
||||
foreach (var window in _windows)
|
||||
{
|
||||
// 跳过排除的进程
|
||||
if (excludeProcessNames.Contains(window.ProcessName)) continue;
|
||||
|
||||
// 计算覆盖比例
|
||||
double coverage = window.GetCoverageRatio(area);
|
||||
if (coverage >= coverageThreshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定区域是否被全屏窗口覆盖
|
||||
/// </summary>
|
||||
/// <param name="area">要检查的区域</param>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表</param>
|
||||
/// <returns>如果被全屏窗口覆盖返回true</returns>
|
||||
public bool IsAreaCoveredByFullScreenWindow(WindowRect area, List<string> excludeProcessNames = null)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
|
||||
// 查找全屏窗口
|
||||
foreach (var window in _windows)
|
||||
{
|
||||
// 跳过排除的进程
|
||||
if (excludeProcessNames.Contains(window.ProcessName)) continue;
|
||||
|
||||
// 只检查全屏窗口
|
||||
if (window.IsFullScreen && window.CoversArea(area))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取覆盖指定区域的所有窗口
|
||||
/// </summary>
|
||||
/// <param name="area">要检查的区域</param>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表</param>
|
||||
/// <param name="coverageThreshold">覆盖阈值</param>
|
||||
/// <returns>覆盖该区域的窗口列表(按Z顺序,最上层在前)</returns>
|
||||
public List<WindowInfo> GetCoveringWindows(WindowRect area, List<string> excludeProcessNames = null, double coverageThreshold = 0.1)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
var coveringWindows = new List<WindowInfo>();
|
||||
|
||||
foreach (var window in _windows)
|
||||
{
|
||||
// 跳过排除的进程
|
||||
if (excludeProcessNames.Contains(window.ProcessName)) continue;
|
||||
|
||||
// 计算覆盖比例
|
||||
double coverage = window.GetCoverageRatio(area);
|
||||
if (coverage >= coverageThreshold)
|
||||
{
|
||||
coveringWindows.Add(window);
|
||||
}
|
||||
}
|
||||
|
||||
return coveringWindows;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有全屏窗口
|
||||
/// </summary>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表</param>
|
||||
/// <returns>如果有全屏窗口返回true</returns>
|
||||
public bool HasFullScreenWindow(List<string> excludeProcessNames = null)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
|
||||
return _windows.Any(w => !excludeProcessNames.Contains(w.ProcessName) && w.IsFullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有全屏窗口
|
||||
/// </summary>
|
||||
/// <param name="excludeProcessNames">要排除的进程名列表</param>
|
||||
/// <returns>全屏窗口列表</returns>
|
||||
public List<WindowInfo> GetFullScreenWindows(List<string> excludeProcessNames = null)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
excludeProcessNames = excludeProcessNames ?? new List<string>();
|
||||
|
||||
return _windows.Where(w => !excludeProcessNames.Contains(w.ProcessName) && w.IsFullScreen).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据进程名查找窗口
|
||||
/// </summary>
|
||||
/// <param name="processName">进程名</param>
|
||||
/// <returns>匹配的窗口列表</returns>
|
||||
public List<WindowInfo> FindWindowsByProcessName(string processName)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _windows.Where(w => w.ProcessName.Equals(processName, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据窗口标题查找窗口
|
||||
/// </summary>
|
||||
/// <param name="title">窗口标题(支持部分匹配)</param>
|
||||
/// <returns>匹配的窗口列表</returns>
|
||||
public List<WindowInfo> FindWindowsByTitle(string title)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _windows.Where(w => w.Title.IndexOf(title, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
_isDisposed = true;
|
||||
_updateTimer?.Dispose();
|
||||
_updateTimer = null;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
_windows.Clear();
|
||||
}
|
||||
|
||||
lock (_processCacheLock)
|
||||
{
|
||||
_processCache.Clear();
|
||||
}
|
||||
|
||||
_windowCache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 窗口Z-Order管理器,用于管理窗口的层级顺序
|
||||
/// 在无焦点模式下,确保后打开的窗口能够置顶于先打开的窗口
|
||||
/// </summary>
|
||||
public static class WindowZOrderManager
|
||||
{
|
||||
#region Win32 API 声明
|
||||
private const int GWL_EXSTYLE = -20;
|
||||
private const int WS_EX_TOPMOST = 0x00000008;
|
||||
private const int WS_EX_NOACTIVATE = 0x08000000;
|
||||
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
|
||||
private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
|
||||
private const uint SWP_NOMOVE = 0x0002;
|
||||
private const uint SWP_NOSIZE = 0x0001;
|
||||
private const uint SWP_NOACTIVATE = 0x0010;
|
||||
private const uint SWP_SHOWWINDOW = 0x0040;
|
||||
private const uint SWP_NOOWNERZORDER = 0x0200;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool IsWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool IsIconic(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern uint GetCurrentProcessId();
|
||||
#endregion
|
||||
|
||||
// 窗口层级管理
|
||||
private static readonly List<WindowInfo> _windowStack = new List<WindowInfo>();
|
||||
private static readonly object _lockObject = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 窗口信息类
|
||||
/// </summary>
|
||||
private class WindowInfo
|
||||
{
|
||||
public IntPtr Handle { get; set; }
|
||||
public Window Window { get; set; }
|
||||
public DateTime CreatedTime { get; set; }
|
||||
public bool IsTopmost { get; set; }
|
||||
public bool IsNoFocusMode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册窗口到Z-Order管理器
|
||||
/// </summary>
|
||||
/// <param name="window">要注册的窗口</param>
|
||||
/// <param name="isTopmost">是否置顶</param>
|
||||
/// <param name="isNoFocusMode">是否无焦点模式</param>
|
||||
public static void RegisterWindow(Window window, bool isTopmost = false, bool isNoFocusMode = false)
|
||||
{
|
||||
if (window == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(window).Handle;
|
||||
if (hwnd == IntPtr.Zero) return;
|
||||
|
||||
// 移除已存在的记录
|
||||
_windowStack.RemoveAll(w => w.Handle == hwnd);
|
||||
|
||||
// 添加新记录
|
||||
var windowInfo = new WindowInfo
|
||||
{
|
||||
Handle = hwnd,
|
||||
Window = window,
|
||||
CreatedTime = DateTime.Now,
|
||||
IsTopmost = isTopmost,
|
||||
IsNoFocusMode = isNoFocusMode
|
||||
};
|
||||
|
||||
_windowStack.Add(windowInfo);
|
||||
|
||||
// 应用Z-Order
|
||||
ApplyZOrder();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从Z-Order管理器中移除窗口
|
||||
/// </summary>
|
||||
/// <param name="window">要移除的窗口</param>
|
||||
public static void UnregisterWindow(Window window)
|
||||
{
|
||||
if (window == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(window).Handle;
|
||||
_windowStack.RemoveAll(w => w.Handle == hwnd);
|
||||
ApplyZOrder();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口为置顶状态
|
||||
/// </summary>
|
||||
/// <param name="window">要置顶的窗口</param>
|
||||
/// <param name="isTopmost">是否置顶</param>
|
||||
public static void SetWindowTopmost(Window window, bool isTopmost)
|
||||
{
|
||||
if (window == null) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
var windowInfo = _windowStack.FirstOrDefault(w => w.Window == window);
|
||||
if (windowInfo != null)
|
||||
{
|
||||
windowInfo.IsTopmost = isTopmost;
|
||||
ApplyZOrder();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将窗口移到最顶层
|
||||
/// </summary>
|
||||
/// <param name="window">要移到最顶层的窗口</param>
|
||||
public static void BringToTop(Window window)
|
||||
{
|
||||
if (window == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(window).Handle;
|
||||
if (hwnd == IntPtr.Zero) return;
|
||||
|
||||
// 使用更直接的方法:先激活窗口,再置顶
|
||||
window.Activate();
|
||||
window.Focus();
|
||||
|
||||
// 设置窗口为置顶
|
||||
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
|
||||
|
||||
// 确保窗口样式正确
|
||||
int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
|
||||
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
|
||||
|
||||
// 再次确保置顶
|
||||
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"BringToTop失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用Z-Order排序
|
||||
/// </summary>
|
||||
private static void ApplyZOrder()
|
||||
{
|
||||
// 简化逻辑:直接设置所有窗口为置顶,让Windows系统自然处理层级
|
||||
foreach (var windowInfo in _windowStack.ToList())
|
||||
{
|
||||
if (windowInfo.IsTopmost && IsWindow(windowInfo.Handle) && IsWindowVisible(windowInfo.Handle) && !IsIconic(windowInfo.Handle))
|
||||
{
|
||||
// 设置窗口为置顶
|
||||
SetWindowPos(windowInfo.Handle, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
|
||||
|
||||
// 确保窗口样式正确
|
||||
int exStyle = GetWindowLong(windowInfo.Handle, GWL_EXSTYLE);
|
||||
if ((exStyle & WS_EX_TOPMOST) == 0)
|
||||
{
|
||||
SetWindowLong(windowInfo.Handle, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有子窗口在前景
|
||||
/// </summary>
|
||||
/// <returns>如果有子窗口在前景返回true</returns>
|
||||
public static bool HasChildWindowInForeground()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
var foregroundWindow = GetForegroundWindow();
|
||||
if (foregroundWindow == IntPtr.Zero) return false;
|
||||
|
||||
return _windowStack.Any(w => w.Handle == foregroundWindow);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理无效的窗口记录
|
||||
/// </summary>
|
||||
public static void CleanupInvalidWindows()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_windowStack.RemoveAll(w => !IsWindow(w.Handle) || !IsWindowVisible(w.Handle));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前注册的窗口数量
|
||||
/// </summary>
|
||||
/// <returns>窗口数量</returns>
|
||||
public static int GetWindowCount()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _windowStack.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制刷新所有窗口的置顶状态
|
||||
/// </summary>
|
||||
public static void ForceRefreshAllWindows()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
foreach (var windowInfo in _windowStack.ToList())
|
||||
{
|
||||
if (windowInfo.IsTopmost && IsWindow(windowInfo.Handle))
|
||||
{
|
||||
// 强制设置窗口为置顶
|
||||
SetWindowPos(windowInfo.Handle, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
|
||||
|
||||
// 确保窗口样式正确
|
||||
int exStyle = GetWindowLong(windowInfo.Handle, GWL_EXSTYLE);
|
||||
SetWindowLong(windowInfo.Handle, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
internal static class WindowsNotificationHelper
|
||||
{
|
||||
private const string APP_ID = "InkCanvasForClass.CE";
|
||||
|
||||
public static void ShowNewVersionToast(string version)
|
||||
{
|
||||
try
|
||||
{
|
||||
var os = Environment.OSVersion.Version;
|
||||
|
||||
if (os.Major == 6 && os.Minor == 1)
|
||||
{
|
||||
ShowBalloonForWin7(version);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowToastForModernWindows(version);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowBalloonForWin7(string version)
|
||||
{
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var taskbar = Application.Current.Resources["TaskbarTrayIcon"] as TaskbarIcon;
|
||||
if (taskbar == null) return;
|
||||
|
||||
taskbar.Visibility = Visibility.Visible;
|
||||
|
||||
taskbar.ShowBalloonTip(
|
||||
"InkCanvasForClass CE",
|
||||
$"发现新版本!:{version}",
|
||||
BalloonIcon.Info);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void ShowToastForModernWindows(string version)
|
||||
{
|
||||
new ToastContentBuilder()
|
||||
.AddText("InkCanvasForClass CE")
|
||||
.AddText($"发现新版本!:{version}")
|
||||
.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="UIAutomationClient" />
|
||||
@@ -152,11 +153,15 @@
|
||||
<PackageReference Include="MdXaml" Version="1.27.0" />
|
||||
<PackageReference Include="Microsoft.Office.Interop.PowerPoint" Version="15.0.4420.1018" />
|
||||
<PackageReference Include="MicrosoftOfficeCore" Version="15.0.0" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<PackageReference Include="Microsoft.International.Converters.PinYinConverter" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
|
||||
<PackageReference Include="OSVersionExt" Version="3.0.0" />
|
||||
<PackageReference Include="AForge.Video" Version="2.2.5" />
|
||||
<PackageReference Include="AForge.Video.DirectShow" Version="2.2.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<COMReference Include="IWshRuntimeLibrary">
|
||||
@@ -189,11 +194,14 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Resources\TimerDownNotice.wav" />
|
||||
<None Include="Resources\ProgressiveAudio.wav" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\IACore\IACore.dll" />
|
||||
<EmbeddedResource Include="Resources\IACore\IALoader.dll" />
|
||||
<EmbeddedResource Include="Resources\IACore\IAWinFX.dll" />
|
||||
<EmbeddedResource Include="UIAccessDLL_x64.dll" />
|
||||
<EmbeddedResource Include="UIAccessDLL_x86.dll" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Cursors\Cursor.cur" />
|
||||
@@ -205,6 +213,7 @@
|
||||
<Resource Include="Resources\DeveloperAvatars\RaspberryKan.jpg" />
|
||||
<Resource Include="Resources\DeveloperAvatars\wwei.png" />
|
||||
<Resource Include="Resources\DeveloperAvatars\yuwenhui2020.png" />
|
||||
<Resource Include="Resources\DeveloperAvatars\CJKmkp.jpg" />
|
||||
<Resource Include="Resources\icc.ico" />
|
||||
<Resource Include="Resources\Icons-png\AdmoxBooth.png" />
|
||||
<Resource Include="Resources\Icons-png\AdmoxWhiteboard.png" />
|
||||
@@ -219,6 +228,10 @@
|
||||
<Resource Include="Resources\Icons-png\icc-transparent-dark.png" />
|
||||
<Resource Include="Resources\Icons-png\icc-transparent.png" />
|
||||
<Resource Include="Resources\Icons-png\icc.png" />
|
||||
<Resource Include="Resources\Icons-png\icc-dark.png" />
|
||||
<Resource Include="Resources\Icons-png\icc-noshadow.png" />
|
||||
<Resource Include="Resources\Icons-png\icc-sharpdark.png" />
|
||||
<Resource Include="Resources\Icons-png\icc-transparent-light-small.png" />
|
||||
<Resource Include="Resources\Icons-png\InkCanvas.png" />
|
||||
<Resource Include="Resources\Icons-png\kuanciya.png" />
|
||||
<Resource Include="Resources\Icons-png\kuandogeyuanliangwo.png" />
|
||||
@@ -235,6 +248,10 @@
|
||||
<Resource Include="Resources\Icons-png\undo.png" />
|
||||
<Resource Include="Resources\Icons-png\minimize.png" />
|
||||
<Resource Include="Resources\Icons-png\penUpright.png" />
|
||||
<Resource Include="Resources\new-icons\multi-touch_white.png" />
|
||||
<Resource Include="Resources\new-icons\hand-move_white.png" />
|
||||
<Resource Include="Resources\new-icons\zoom_white.png" />
|
||||
<Resource Include="Resources\new-icons\rotate_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\twoFingelMove-Blue.png" />
|
||||
@@ -251,6 +268,7 @@
|
||||
<Resource Include="Resources\DeveloperAvatars\kengwang.png" />
|
||||
<Resource Include="Resources\DeveloperAvatars\STBBRD.png" />
|
||||
<Resource Include="Resources\DeveloperAvatars\WXRIW.png" />
|
||||
<Resource Include="Resources\DeveloperAvatars\PrefacedCorg.jpg" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\down.png" />
|
||||
@@ -283,7 +301,9 @@
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_arrow_circle_left_24_regular.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_arrow_circle_right_24_regular.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_weather_moon_24_regular.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_weather_moon_24_regular_white.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_weather_sunny_24_regular.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_weather_sunny_24_regular_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_signature_24_regular.png" />
|
||||
@@ -318,6 +338,7 @@
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_arrow_rotate_clockwise_24_regular.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_scale_fit_24_regular.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_scale_fit_24_regular_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_scales_24_regular.png" />
|
||||
@@ -335,9 +356,6 @@
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_clock_24_regular.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_book_question_mark_24_regular.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_keyboard_24_regular.png" />
|
||||
</ItemGroup>
|
||||
@@ -405,6 +423,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\new-icons\gesture.png" />
|
||||
<Resource Include="Resources\new-icons\gesture_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\new-icons\gesture-enabled.png" />
|
||||
@@ -452,6 +471,37 @@
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\cube.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\arrow_white.png" />
|
||||
<Resource Include="Resources\Icons-png\geo-icons\dashed-line_white.png" />
|
||||
<Resource Include="Resources\Icons-png\geo-icons\dotted-line_white.png" />
|
||||
<Resource Include="Resources\Icons-png\geo-icons\line_white.png" />
|
||||
<Resource Include="Resources\Icons-png\geo-icons\paralle-lines_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\centered-square_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\centered-circle_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\centered-circle-dashed_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\centered-oval_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\square_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\cylinder_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\cone_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\geo-icons\cube_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\EasiNote.png" />
|
||||
<Resource Include="Resources\Icons-png\EasiNote3C.png" />
|
||||
@@ -488,6 +538,19 @@
|
||||
<Resource Include="Resources\new-icons\unfold-chevron.png" />
|
||||
<Resource Include="Resources\new-icons\zoom.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_person_money_24_regular-light.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_people_money_24_regular-light.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_timer_24_regular-light.png" />
|
||||
<Resource Include="Resources\new-icons\blackboard-light.png" />
|
||||
<Resource Include="Resources\new-icons\end-slides-show-light.png" />
|
||||
<Resource Include="Resources\new-icons\eye-light.png" />
|
||||
<Resource Include="Resources\new-icons\chevron-left-light.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_keyboard_24_regular_white.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_control_button_24_regular_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
@@ -514,6 +577,10 @@
|
||||
<None Remove="Resources\Icons-png\icc-transparent-dark.png" />
|
||||
<None Remove="Resources\Icons-png\icc-transparent.png" />
|
||||
<None Remove="Resources\Icons-png\icc.png" />
|
||||
<None Remove="Resources\Icons-png\icc-dark.png" />
|
||||
<None Remove="Resources\Icons-png\icc-noshadow.png" />
|
||||
<None Remove="Resources\Icons-png\icc-sharpdark.png" />
|
||||
<None Remove="Resources\Icons-png\icc-transparent-light-small.png" />
|
||||
<None Remove="Resources\Icons-png\idt.png" />
|
||||
<None Remove="Resources\Icons-png\InkCanvas.png" />
|
||||
<None Remove="Resources\Icons-png\kuanciya.png" />
|
||||
@@ -550,14 +617,31 @@
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\idt.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Fonts\LXGWWenKaiTC-Regular.ttf" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons-png\HiteAnnotation.png" />
|
||||
<Resource Include="Resources\Icons-png\AiClass.png" />
|
||||
<Resource Include="Resources\Icons-png\天喻教育云.png" />
|
||||
<Resource Include="Resources\Icons-png\畅言智慧课堂.png" />
|
||||
<Resource Include="Resources\PresentationExample\bottombar-dark.png" />
|
||||
<Resource Include="Resources\PresentationExample\bottombar-white.png" />
|
||||
<Resource Include="Resources\PresentationExample\page.jpg" />
|
||||
<Resource Include="Resources\PresentationExample\sidebar-dark.png" />
|
||||
<Resource Include="Resources\PresentationExample\sidebar-white.png" />
|
||||
<Resource Include="Resources\PresentationExample\toolbar.png" />
|
||||
<Resource Include="Resources\Startup-animation\ICC Spring.png" />
|
||||
<Resource Include="Resources\Startup-animation\ICC Summer.png" />
|
||||
<Resource Include="Resources\Startup-animation\ICC Autumn.png" />
|
||||
<Resource Include="Resources\Startup-animation\ICC Winter.png" />
|
||||
<Resource Include="Resources\Startup-animation\ICC Horse.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_copy_24_regular_white.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_copy_add_24_regular_white.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_flip_horizontal_24_regular_white.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_flip_vertical_24_regular_white.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_edit_24_regular_white.png" />
|
||||
<Resource Include="Resources\Icons-Fluent\ic_fluent_delete_24_regular_white.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Settings.Designer.cs">
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<_LastSelectedProfileId>D:\vs\ica\Ink Canvas\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
@@ -6,8 +8,6 @@ using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -103,7 +103,6 @@ namespace Ink_Canvas
|
||||
HideSubPanels("cursor");
|
||||
SidePannelMarginAnimation(-10);
|
||||
});
|
||||
isFloatingBarChangingHideMode = false;
|
||||
}
|
||||
|
||||
private async void LeftUnFoldButtonDisplayQuickPanel_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
@@ -259,7 +258,11 @@ namespace Ink_Canvas
|
||||
PenIcon_Click(null, null);
|
||||
}
|
||||
|
||||
if (StackPanelPPTControls.Visibility == Visibility.Visible)
|
||||
// 只有在PPT放映模式下且页数有效时才显示翻页按钮
|
||||
if (StackPanelPPTControls.Visibility == Visibility.Visible &&
|
||||
BtnPPTSlideShowEnd.Visibility == Visibility.Visible &&
|
||||
PPTManager?.IsInSlideShow == true &&
|
||||
PPTManager?.SlidesCount > 0)
|
||||
{
|
||||
var dops = Settings.PowerPointSettings.PPTButtonsDisplayOption.ToString();
|
||||
var dopsc = dops.ToCharArray();
|
||||
@@ -268,10 +271,31 @@ namespace Ink_Canvas
|
||||
if (dopsc[2] == '2' && !isDisplayingOrHidingBlackboard) AnimationsHelper.ShowWithFadeIn(LeftSidePanelForPPTNavigation);
|
||||
if (dopsc[3] == '2' && !isDisplayingOrHidingBlackboard) AnimationsHelper.ShowWithFadeIn(RightSidePanelForPPTNavigation);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果条件不满足,确保隐藏翻页按钮
|
||||
LeftBottomPanelForPPTNavigation.Visibility = Visibility.Collapsed;
|
||||
RightBottomPanelForPPTNavigation.Visibility = Visibility.Collapsed;
|
||||
LeftSidePanelForPPTNavigation.Visibility = Visibility.Collapsed;
|
||||
RightSidePanelForPPTNavigation.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// 新增:只在屏幕模式下显示浮动栏
|
||||
// 新只在屏幕模式下显示浮动栏
|
||||
if (currentMode == 0)
|
||||
{
|
||||
// 强制更新布局以确保ActualWidth正确
|
||||
ViewboxFloatingBar.UpdateLayout();
|
||||
|
||||
// 等待一小段时间让布局完全更新
|
||||
Task.Delay(50);
|
||||
|
||||
// 再次强制更新布局
|
||||
ViewboxFloatingBar.UpdateLayout();
|
||||
|
||||
// 强制重新测量和排列
|
||||
ViewboxFloatingBar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
ViewboxFloatingBar.Arrange(new Rect(ViewboxFloatingBar.DesiredSize));
|
||||
|
||||
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible)
|
||||
ViewboxFloatingBarMarginAnimation(60);
|
||||
else
|
||||
@@ -280,7 +304,26 @@ namespace Ink_Canvas
|
||||
SidePannelMarginAnimation(-50, !unfoldFloatingBarByUser);
|
||||
});
|
||||
|
||||
isFloatingBarChangingHideMode = false;
|
||||
await Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 等待UI完全更新
|
||||
await Task.Delay(100);
|
||||
|
||||
// 获取当前选中的模式并重新设置高光位置
|
||||
string selectedToolMode = GetCurrentSelectedMode();
|
||||
if (!string.IsNullOrEmpty(selectedToolMode))
|
||||
{
|
||||
SetFloatingBarHighlightPosition(selectedToolMode);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"浮动栏展开后重新设置按钮高亮状态失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private async void SidePannelMarginAnimation(int MarginFromEdge, bool isNoAnimation = false) // Possible value: -50, -10
|
||||
@@ -319,4 +362,4 @@ namespace Ink_Canvas
|
||||
isFloatingBarChangingHideMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using IWshRuntimeLibrary;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using IWshRuntimeLibrary;
|
||||
using Application = System.Windows.Forms.Application;
|
||||
using File = System.IO.File;
|
||||
|
||||
|
||||
@@ -1,24 +1,45 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using Application = System.Windows.Application;
|
||||
using ui = iNKORE.UI.WPF.Modern.Controls;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private Color FloatBarForegroundColor = Color.FromRgb(102, 102, 102);
|
||||
private Color FloatBarForegroundColor;
|
||||
|
||||
private void SetTheme(string theme)
|
||||
private void SetTheme(string theme, bool autoSwitchIcon = false)
|
||||
{
|
||||
// 清理现有的主题资源
|
||||
var resourcesToRemove = new List<ResourceDictionary>();
|
||||
foreach (var dict in Application.Current.Resources.MergedDictionaries)
|
||||
{
|
||||
if (dict.Source != null &&
|
||||
(dict.Source.ToString().Contains("Light.xaml") ||
|
||||
dict.Source.ToString().Contains("Dark.xaml")))
|
||||
{
|
||||
resourcesToRemove.Add(dict);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dict in resourcesToRemove)
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Remove(dict);
|
||||
}
|
||||
|
||||
if (theme == "Light")
|
||||
{
|
||||
var rd1 = new ResourceDictionary
|
||||
{ Source = new Uri("Resources/Styles/Light.xaml", UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd1);
|
||||
|
||||
// 在主题资源之后添加其他资源
|
||||
var rd2 = new ResourceDictionary
|
||||
{ Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd2);
|
||||
@@ -33,13 +54,39 @@ namespace Ink_Canvas
|
||||
|
||||
ThemeManager.SetRequestedTheme(window, ElementTheme.Light);
|
||||
|
||||
FloatBarForegroundColor = (Color)Application.Current.FindResource("FloatBarForegroundColor");
|
||||
InitializeFloatBarForegroundColor();
|
||||
|
||||
// 刷新快速面板图标
|
||||
RefreshQuickPanelIcons();
|
||||
|
||||
// 刷新墨迹选中栏图标
|
||||
RefreshStrokeSelectionIcons();
|
||||
|
||||
// 刷新图片选中栏图标
|
||||
RefreshImageSelectionIcons();
|
||||
|
||||
// 刷新手势按钮图标
|
||||
RefreshGestureButtonIcon();
|
||||
|
||||
RefreshFloatingBarHighlightColors();
|
||||
|
||||
if (autoSwitchIcon)
|
||||
{
|
||||
AutoSwitchFloatingBarIconForTheme("Light");
|
||||
}
|
||||
|
||||
// 强制刷新UI
|
||||
window.InvalidateVisual();
|
||||
|
||||
// 通知其他窗口刷新主题
|
||||
RefreshOtherWindowsTheme();
|
||||
}
|
||||
else if (theme == "Dark")
|
||||
{
|
||||
var rd1 = new ResourceDictionary { Source = new Uri("Resources/Styles/Dark.xaml", UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd1);
|
||||
|
||||
// 在主题资源之后添加其他资源
|
||||
var rd2 = new ResourceDictionary
|
||||
{ Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd2);
|
||||
@@ -54,7 +101,198 @@ namespace Ink_Canvas
|
||||
|
||||
ThemeManager.SetRequestedTheme(window, ElementTheme.Dark);
|
||||
|
||||
InitializeFloatBarForegroundColor();
|
||||
|
||||
// 刷新快速面板图标
|
||||
RefreshQuickPanelIcons();
|
||||
|
||||
// 刷新墨迹选中栏图标
|
||||
RefreshStrokeSelectionIcons();
|
||||
|
||||
// 刷新图片选中栏图标
|
||||
RefreshImageSelectionIcons();
|
||||
|
||||
// 刷新手势按钮图标
|
||||
RefreshGestureButtonIcon();
|
||||
|
||||
RefreshFloatingBarHighlightColors();
|
||||
|
||||
if (autoSwitchIcon)
|
||||
{
|
||||
AutoSwitchFloatingBarIconForTheme("Dark");
|
||||
}
|
||||
|
||||
// 强制刷新UI
|
||||
window.InvalidateVisual();
|
||||
|
||||
// 通知其他窗口刷新主题
|
||||
RefreshOtherWindowsTheme();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化FloatBarForegroundColor,从当前主题资源中加载颜色
|
||||
/// </summary>
|
||||
private void InitializeFloatBarForegroundColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
FloatBarForegroundColor = (Color)Application.Current.FindResource("FloatBarForegroundColor");
|
||||
|
||||
// 强制刷新浮动工具栏按钮颜色
|
||||
RefreshFloatingBarButtonColors();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 如果无法从资源中加载,使用默认颜色
|
||||
FloatBarForegroundColor = Color.FromRgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新快速面板图标
|
||||
/// </summary>
|
||||
private void RefreshQuickPanelIcons()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (LeftUnFoldButtonQuickPanel != null)
|
||||
{
|
||||
LeftUnFoldButtonQuickPanel.InvalidateVisual();
|
||||
}
|
||||
if (RightUnFoldButtonQuickPanel != null)
|
||||
{
|
||||
RightUnFoldButtonQuickPanel.InvalidateVisual();
|
||||
}
|
||||
if (LeftSidePanel != null)
|
||||
{
|
||||
LeftSidePanel.InvalidateVisual();
|
||||
}
|
||||
if (RightSidePanel != null)
|
||||
{
|
||||
RightSidePanel.InvalidateVisual();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新浮动栏高光条颜色
|
||||
/// </summary>
|
||||
private void RefreshFloatingBarHighlightColors()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (FloatingbarSelectionBG != null && FloatingbarSelectionBG.Visibility == Visibility.Visible)
|
||||
{
|
||||
// 根据主题设置高光颜色
|
||||
Color highlightBackgroundColor;
|
||||
Color highlightBarColor;
|
||||
bool isDarkTheme = Settings.Appearance.Theme == 1 ||
|
||||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
|
||||
|
||||
if (isDarkTheme)
|
||||
{
|
||||
highlightBackgroundColor = Color.FromArgb(21, 102, 204, 255);
|
||||
highlightBarColor = Color.FromRgb(102, 204, 255);
|
||||
}
|
||||
else
|
||||
{
|
||||
highlightBackgroundColor = Color.FromArgb(21, 59, 130, 246);
|
||||
highlightBarColor = Color.FromRgb(37, 99, 235);
|
||||
}
|
||||
|
||||
// 设置高光背景颜色
|
||||
FloatingbarSelectionBG.Background = new SolidColorBrush(highlightBackgroundColor);
|
||||
if (FloatingbarSelectionBG.Child is System.Windows.Controls.Canvas canvas && canvas.Children.Count > 0)
|
||||
{
|
||||
var firstChild = canvas.Children[0];
|
||||
if (firstChild is Border innerBorder)
|
||||
{
|
||||
innerBorder.Background = new SolidColorBrush(highlightBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新浮动工具栏按钮颜色
|
||||
/// </summary>
|
||||
private void RefreshFloatingBarButtonColors()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 根据主题选择高光颜色
|
||||
Color selectedColor;
|
||||
bool isDarkTheme = Settings.Appearance.Theme == 1 ||
|
||||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
|
||||
|
||||
if (isDarkTheme)
|
||||
{
|
||||
selectedColor = Color.FromRgb(102, 204, 255);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedColor = Color.FromRgb(30, 58, 138);
|
||||
}
|
||||
|
||||
// 根据当前模式设置按钮颜色
|
||||
switch (_currentToolMode)
|
||||
{
|
||||
case "cursor":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
break;
|
||||
case "pen":
|
||||
case "color":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
break;
|
||||
case "eraser":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
break;
|
||||
case "eraserByStrokes":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
break;
|
||||
case "select":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
break;
|
||||
default:
|
||||
// 默认情况,所有按钮都使用主题颜色
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,5 +329,228 @@ namespace Ink_Canvas
|
||||
|
||||
return light;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据主题自动切换浮动栏图标
|
||||
/// </summary>
|
||||
private void AutoSwitchFloatingBarIconForTheme(string theme)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (theme == "Light")
|
||||
{
|
||||
Settings.Appearance.FloatingBarImg = 0;
|
||||
}
|
||||
else if (theme == "Dark")
|
||||
{
|
||||
Settings.Appearance.FloatingBarImg = 3;
|
||||
}
|
||||
|
||||
UpdateFloatingBarIcon();
|
||||
UpdateFloatingBarIconComboBox();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新设置界面中的浮动栏图标选择下拉框显示
|
||||
/// </summary>
|
||||
private void UpdateFloatingBarIconComboBox()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ComboBoxFloatingBarImg != null)
|
||||
{
|
||||
ComboBoxFloatingBarImg.SelectedIndex = Settings.Appearance.FloatingBarImg;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新墨迹选中栏图标
|
||||
/// </summary>
|
||||
private void RefreshStrokeSelectionIcons()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (BorderStrokeSelectionControl != null)
|
||||
{
|
||||
// 强制刷新墨迹选中栏的视觉状态
|
||||
BorderStrokeSelectionControl.InvalidateVisual();
|
||||
|
||||
// 刷新墨迹选中栏内的所有图标
|
||||
var viewbox = BorderStrokeSelectionControl.Child as Viewbox;
|
||||
if (viewbox?.Child is ui.SimpleStackPanel stackPanel)
|
||||
{
|
||||
RefreshStrokeSelectionIconsRecursive(stackPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 忽略异常,确保主题切换不会因为图标刷新失败而中断
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归刷新墨迹选中栏内的图标
|
||||
/// </summary>
|
||||
private void RefreshStrokeSelectionIconsRecursive(System.Windows.Controls.Panel panel)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var child in panel.Children)
|
||||
{
|
||||
if (child is Image image)
|
||||
{
|
||||
// 强制刷新图像
|
||||
image.InvalidateVisual();
|
||||
}
|
||||
else if (child is System.Windows.Controls.Panel childPanel)
|
||||
{
|
||||
// 递归处理子面板
|
||||
RefreshStrokeSelectionIconsRecursive(childPanel);
|
||||
}
|
||||
else if (child is Border border && border.Child is System.Windows.Controls.Panel borderPanel)
|
||||
{
|
||||
// 处理Border内的面板
|
||||
RefreshStrokeSelectionIconsRecursive(borderPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 忽略异常
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新图片选中栏图标
|
||||
/// </summary>
|
||||
private void RefreshImageSelectionIcons()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (BorderImageSelectionControl != null)
|
||||
{
|
||||
// 强制刷新图片选中栏的视觉状态
|
||||
BorderImageSelectionControl.InvalidateVisual();
|
||||
|
||||
// 刷新图片选中栏内的所有图标
|
||||
var viewbox = BorderImageSelectionControl.Child as Viewbox;
|
||||
if (viewbox?.Child is ui.SimpleStackPanel stackPanel)
|
||||
{
|
||||
RefreshImageSelectionIconsRecursive(stackPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 忽略异常,确保主题切换不会因为图标刷新失败而中断
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归刷新图片选中栏内的图标
|
||||
/// </summary>
|
||||
private void RefreshImageSelectionIconsRecursive(System.Windows.Controls.Panel panel)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var child in panel.Children)
|
||||
{
|
||||
if (child is Image image)
|
||||
{
|
||||
// 强制刷新图像
|
||||
image.InvalidateVisual();
|
||||
}
|
||||
else if (child is System.Windows.Controls.Panel childPanel)
|
||||
{
|
||||
// 递归处理子面板
|
||||
RefreshImageSelectionIconsRecursive(childPanel);
|
||||
}
|
||||
else if (child is Border border && border.Child is System.Windows.Controls.Panel borderPanel)
|
||||
{
|
||||
// 处理Border内的面板
|
||||
RefreshImageSelectionIconsRecursive(borderPanel);
|
||||
}
|
||||
else if (child is Grid grid)
|
||||
{
|
||||
// 处理Grid内的子元素
|
||||
foreach (var gridChild in grid.Children)
|
||||
{
|
||||
if (gridChild is Image gridImage)
|
||||
{
|
||||
gridImage.InvalidateVisual();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 忽略异常
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新手势按钮图标
|
||||
/// </summary>
|
||||
private void RefreshGestureButtonIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 调用手势按钮颜色和图标更新方法,该方法会根据当前主题和手势状态设置正确的图标
|
||||
CheckEnableTwoFingerGestureBtnColorPrompt();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新其他窗口的主题
|
||||
/// </summary>
|
||||
private void RefreshOtherWindowsTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 刷新所有打开的窗口
|
||||
foreach (Window window in Application.Current.Windows)
|
||||
{
|
||||
if (window is CountdownTimerWindow timerWindow)
|
||||
{
|
||||
timerWindow.RefreshTheme();
|
||||
}
|
||||
else if (window is RandWindow randWindow)
|
||||
{
|
||||
randWindow.RefreshTheme();
|
||||
}
|
||||
else if (window is OperatingGuideWindow operatingGuideWindow)
|
||||
{
|
||||
operatingGuideWindow.RefreshTheme();
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新计时器控件
|
||||
if (TimerControl != null)
|
||||
{
|
||||
TimerControl.RefreshTheme();
|
||||
}
|
||||
|
||||
if (MinimizedTimerControl != null)
|
||||
{
|
||||
MinimizedTimerControl.RefreshTheme();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
@@ -6,7 +7,6 @@ using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Media;
|
||||
using Ink_Canvas.Helpers;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -15,10 +15,10 @@ namespace Ink_Canvas
|
||||
private StrokeCollection[] strokeCollections = new StrokeCollection[101];
|
||||
private bool[] whiteboadLastModeIsRedo = new bool[101];
|
||||
private StrokeCollection lastTouchDownStrokeCollection = new StrokeCollection();
|
||||
|
||||
private int CurrentWhiteboardIndex = 1;
|
||||
private int WhiteboardTotalCount = 1;
|
||||
private TimeMachineHistory[][] TimeMachineHistories = new TimeMachineHistory[101][]; //最多99页,0用来存储非白板时的墨迹以便还原
|
||||
private TimeMachineHistory[][] TimeMachineHistories = new TimeMachineHistory[101][];
|
||||
private bool[] savedMultiTouchModeStates = new bool[101];
|
||||
|
||||
// 保存每页白板图片信息
|
||||
private void SaveStrokes(bool isBackupMain = false)
|
||||
@@ -97,6 +97,8 @@ namespace Ink_Canvas
|
||||
{
|
||||
var timeMachineHistory = timeMachine.ExportTimeMachineHistory();
|
||||
TimeMachineHistories[0] = timeMachineHistory;
|
||||
// 保存多指书写模式状态
|
||||
savedMultiTouchModeStates[0] = isInMultiTouchMode;
|
||||
timeMachine.ClearStrokeHistory();
|
||||
|
||||
|
||||
@@ -105,6 +107,8 @@ namespace Ink_Canvas
|
||||
{
|
||||
var timeMachineHistory = timeMachine.ExportTimeMachineHistory();
|
||||
TimeMachineHistories[CurrentWhiteboardIndex] = timeMachineHistory;
|
||||
// 保存多指书写模式状态
|
||||
savedMultiTouchModeStates[CurrentWhiteboardIndex] = isInMultiTouchMode;
|
||||
timeMachine.ClearStrokeHistory();
|
||||
|
||||
|
||||
@@ -116,15 +120,25 @@ namespace Ink_Canvas
|
||||
_currentCommitType = CommitReason.ClearingCanvas;
|
||||
if (isErasedByCode) _currentCommitType = CommitReason.CodeInput;
|
||||
|
||||
|
||||
|
||||
// 只清除笔画,不清除图片元素
|
||||
// 图片元素的清除由调用方决定
|
||||
inkCanvas.Strokes.Clear();
|
||||
|
||||
// 执行内存清理
|
||||
PerformLightweightMemoryCleanup();
|
||||
|
||||
_currentCommitType = CommitReason.UserInput;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行内存清理
|
||||
/// </summary>
|
||||
private void PerformLightweightMemoryCleanup()
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
GC.Collect();
|
||||
});
|
||||
}
|
||||
|
||||
// 恢复每页白板图片信息
|
||||
private void RestoreStrokes(bool isBackupMain = false)
|
||||
{
|
||||
@@ -140,7 +154,7 @@ namespace Ink_Canvas
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
|
||||
|
||||
var targetIndex = isBackupMain ? 0 : CurrentWhiteboardIndex;
|
||||
|
||||
// 先清空当前画布的墨迹
|
||||
@@ -161,12 +175,16 @@ namespace Ink_Canvas
|
||||
{
|
||||
timeMachine.ImportTimeMachineHistory(TimeMachineHistories[0]);
|
||||
foreach (var item in TimeMachineHistories[0]) ApplyHistoryToCanvas(item);
|
||||
// 恢复多指书写模式状态
|
||||
RestoreMultiTouchModeState(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
timeMachine.ImportTimeMachineHistory(TimeMachineHistories[CurrentWhiteboardIndex]);
|
||||
// 通过时间机器历史恢复所有内容(墨迹和图片)
|
||||
foreach (var item in TimeMachineHistories[CurrentWhiteboardIndex]) ApplyHistoryToCanvas(item);
|
||||
// 恢复多指书写模式状态
|
||||
RestoreMultiTouchModeState(CurrentWhiteboardIndex);
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +195,39 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复多指书写模式状态
|
||||
/// </summary>
|
||||
private void RestoreMultiTouchModeState(int pageIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查是否保存了多指书写模式状态
|
||||
if (savedMultiTouchModeStates[pageIndex])
|
||||
{
|
||||
// 更新UI状态
|
||||
if (ToggleSwitchEnableMultiTouchMode != null)
|
||||
{
|
||||
ToggleSwitchEnableMultiTouchMode.IsOn = true;
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"恢复多指书写模式状态 - 页面索引: {pageIndex}", LogHelper.LogType.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 更新UI状态
|
||||
if (ToggleSwitchEnableMultiTouchMode != null)
|
||||
{
|
||||
ToggleSwitchEnableMultiTouchMode.IsOn = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"恢复多指书写模式状态失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void BtnWhiteBoardPageIndex_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (sender == BtnLeftPageListWB)
|
||||
@@ -191,9 +242,12 @@ namespace Ink_Canvas
|
||||
RefreshBlackBoardSidePageListView();
|
||||
AnimationsHelper.ShowWithSlideFromBottomAndFade(BoardBorderLeftPageListView);
|
||||
await Task.Delay(1);
|
||||
ScrollViewToVerticalTop(
|
||||
(ListViewItem)BlackBoardLeftSidePageListView.ItemContainerGenerator.ContainerFromIndex(
|
||||
CurrentWhiteboardIndex - 1), BlackBoardLeftSidePageListScrollViewer);
|
||||
var leftContainer = BlackBoardLeftSidePageListView.ItemContainerGenerator.ContainerFromIndex(
|
||||
CurrentWhiteboardIndex - 1) as ListViewItem;
|
||||
if (leftContainer != null)
|
||||
{
|
||||
ScrollViewToVerticalTop(leftContainer, BlackBoardLeftSidePageListScrollViewer);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (sender == BtnRightPageListWB)
|
||||
@@ -208,9 +262,12 @@ namespace Ink_Canvas
|
||||
RefreshBlackBoardSidePageListView();
|
||||
AnimationsHelper.ShowWithSlideFromBottomAndFade(BoardBorderRightPageListView);
|
||||
await Task.Delay(1);
|
||||
ScrollViewToVerticalTop(
|
||||
(ListViewItem)BlackBoardRightSidePageListView.ItemContainerGenerator.ContainerFromIndex(
|
||||
CurrentWhiteboardIndex - 1), BlackBoardRightSidePageListScrollViewer);
|
||||
var rightContainer = BlackBoardRightSidePageListView.ItemContainerGenerator.ContainerFromIndex(
|
||||
CurrentWhiteboardIndex - 1) as ListViewItem;
|
||||
if (rightContainer != null)
|
||||
{
|
||||
ScrollViewToVerticalTop(rightContainer, BlackBoardRightSidePageListScrollViewer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +310,7 @@ namespace Ink_Canvas
|
||||
BtnWhiteBoardAdd_Click(sender, e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 隐藏图片选择工具栏
|
||||
if (currentSelectedElement != null)
|
||||
{
|
||||
@@ -280,7 +337,7 @@ namespace Ink_Canvas
|
||||
if (WhiteboardTotalCount >= 99) return;
|
||||
if (Settings.Automation.IsAutoSaveStrokesAtClear &&
|
||||
inkCanvas.Strokes.Count > Settings.Automation.MinimumAutomationStrokeNumber) SaveScreenShot(true);
|
||||
|
||||
|
||||
// 隐藏图片选择工具栏
|
||||
if (currentSelectedElement != null)
|
||||
{
|
||||
@@ -291,7 +348,7 @@ namespace Ink_Canvas
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
|
||||
|
||||
SaveStrokes();
|
||||
ClearStrokes(true);
|
||||
|
||||
@@ -330,7 +387,7 @@ namespace Ink_Canvas
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
|
||||
|
||||
ClearStrokes(true);
|
||||
|
||||
if (CurrentWhiteboardIndex != WhiteboardTotalCount)
|
||||
@@ -360,13 +417,25 @@ namespace Ink_Canvas
|
||||
BtnLeftWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
|
||||
BtnRightWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
|
||||
|
||||
// 始终允许点击“下一页/新页面”按钮(除非已达最大页数)
|
||||
BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage;
|
||||
if (isLastPage)
|
||||
{
|
||||
BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage;
|
||||
}
|
||||
else
|
||||
{
|
||||
BtnWhiteBoardSwitchNext.IsEnabled = true;
|
||||
}
|
||||
|
||||
// 保持按钮常亮(高亮)
|
||||
BtnLeftWhiteBoardSwitchNextGeometry.Brush = new SolidColorBrush(Color.FromArgb(255, 24, 24, 27));
|
||||
// 获取主题颜色资源
|
||||
var iconForegroundBrush = Application.Current.FindResource("IconForeground") as SolidColorBrush;
|
||||
|
||||
// 设置下一页按钮颜色
|
||||
if (iconForegroundBrush != null)
|
||||
{
|
||||
BtnLeftWhiteBoardSwitchNextGeometry.Brush = iconForegroundBrush;
|
||||
BtnRightWhiteBoardSwitchNextGeometry.Brush = iconForegroundBrush;
|
||||
}
|
||||
BtnLeftWhiteBoardSwitchNextLabel.Opacity = 1;
|
||||
BtnRightWhiteBoardSwitchNextGeometry.Brush = new SolidColorBrush(Color.FromArgb(255, 24, 24, 27));
|
||||
BtnRightWhiteBoardSwitchNextLabel.Opacity = 1;
|
||||
|
||||
BtnWhiteBoardSwitchPrevious.IsEnabled = true;
|
||||
@@ -374,16 +443,23 @@ namespace Ink_Canvas
|
||||
if (CurrentWhiteboardIndex == 1)
|
||||
{
|
||||
BtnWhiteBoardSwitchPrevious.IsEnabled = false;
|
||||
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = new SolidColorBrush(Color.FromArgb(127, 24, 24, 27));
|
||||
if (iconForegroundBrush != null)
|
||||
{
|
||||
var disabledBrush = new SolidColorBrush(Color.FromArgb(127, iconForegroundBrush.Color.R, iconForegroundBrush.Color.G, iconForegroundBrush.Color.B));
|
||||
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = disabledBrush;
|
||||
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = disabledBrush;
|
||||
}
|
||||
BtnLeftWhiteBoardSwitchPreviousLabel.Opacity = 0.5;
|
||||
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = new SolidColorBrush(Color.FromArgb(127, 24, 24, 27));
|
||||
BtnRightWhiteBoardSwitchPreviousLabel.Opacity = 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = new SolidColorBrush(Color.FromArgb(255, 24, 24, 27));
|
||||
if (iconForegroundBrush != null)
|
||||
{
|
||||
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = iconForegroundBrush;
|
||||
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = iconForegroundBrush;
|
||||
}
|
||||
BtnLeftWhiteBoardSwitchPreviousLabel.Opacity = 1;
|
||||
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = new SolidColorBrush(Color.FromArgb(255, 24, 24, 27));
|
||||
BtnRightWhiteBoardSwitchPreviousLabel.Opacity = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -6,7 +7,6 @@ using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Ink_Canvas.Helpers;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -61,7 +61,7 @@ namespace Ink_Canvas
|
||||
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
|
||||
|
||||
// 设置为白板默认背景色
|
||||
Color defaultWhiteboardColor = Color.FromRgb(234, 235, 237);
|
||||
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
|
||||
|
||||
if (currentMode == 1) // 白板模式
|
||||
{
|
||||
@@ -135,7 +135,7 @@ namespace Ink_Canvas
|
||||
{
|
||||
Name = "BackgroundPalette",
|
||||
Visibility = Visibility.Collapsed,
|
||||
Background = new SolidColorBrush(Colors.White),
|
||||
Background = (SolidColorBrush)Application.Current.FindResource("SettingsPageBackground"),
|
||||
Opacity = 1,
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)),
|
||||
BorderThickness = new Thickness(1),
|
||||
@@ -166,7 +166,7 @@ namespace Ink_Canvas
|
||||
var titleText = new TextBlock
|
||||
{
|
||||
Text = "背景设置",
|
||||
Foreground = new SolidColorBrush(Colors.White),
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("FloatBarForeground"),
|
||||
Padding = new Thickness(0, 5, 0, 0),
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.Bold,
|
||||
@@ -198,7 +198,7 @@ namespace Ink_Canvas
|
||||
var modeTitle = new TextBlock
|
||||
{
|
||||
Text = "白板模式",
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x17, 0x25, 0x54)),
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground"),
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
@@ -233,7 +233,7 @@ namespace Ink_Canvas
|
||||
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
|
||||
|
||||
// 设置为白板默认背景色
|
||||
Color defaultWhiteboardColor = Color.FromRgb(234, 235, 237);
|
||||
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
|
||||
|
||||
if (currentMode == 1) // 白板模式
|
||||
{
|
||||
@@ -319,7 +319,7 @@ namespace Ink_Canvas
|
||||
var separator = new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = new SolidColorBrush(Color.FromRgb(0xd4, 0xd4, 0xd8)),
|
||||
Background = (SolidColorBrush)Application.Current.FindResource("SettingsPageBorderBrush"),
|
||||
Margin = new Thickness(0, 12, 0, 12)
|
||||
};
|
||||
contentPanel.Children.Add(separator);
|
||||
@@ -328,7 +328,7 @@ namespace Ink_Canvas
|
||||
var colorTitle = new TextBlock
|
||||
{
|
||||
Text = "背景颜色",
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x17, 0x25, 0x54)),
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground"),
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
@@ -342,7 +342,7 @@ namespace Ink_Canvas
|
||||
Width = 100,
|
||||
Height = 40,
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0xd4, 0xd4, 0xd8)),
|
||||
BorderBrush = (SolidColorBrush)Application.Current.FindResource("SettingsPageBorderBrush"),
|
||||
Background = new SolidColorBrush(Colors.White),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
@@ -378,7 +378,7 @@ namespace Ink_Canvas
|
||||
// 先创建所有滑块控件
|
||||
// R滑块和文本框
|
||||
var rPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 0, 10, 5) };
|
||||
var rLabel = new TextBlock { Text = "R:", Width = 20, VerticalAlignment = VerticalAlignment.Center };
|
||||
var rLabel = new TextBlock { Text = "R:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
|
||||
var rSlider = new Slider
|
||||
{
|
||||
Minimum = 0,
|
||||
@@ -393,12 +393,13 @@ namespace Ink_Canvas
|
||||
Text = currentBackgroundColor.R.ToString(),
|
||||
Width = 30,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Right
|
||||
TextAlignment = TextAlignment.Right,
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
|
||||
};
|
||||
|
||||
// G滑块和文本框
|
||||
var gPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 0, 10, 5) };
|
||||
var gLabel = new TextBlock { Text = "G:", Width = 20, VerticalAlignment = VerticalAlignment.Center };
|
||||
var gLabel = new TextBlock { Text = "G:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
|
||||
var gSlider = new Slider
|
||||
{
|
||||
Minimum = 0,
|
||||
@@ -413,12 +414,13 @@ namespace Ink_Canvas
|
||||
Text = currentBackgroundColor.G.ToString(),
|
||||
Width = 30,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Right
|
||||
TextAlignment = TextAlignment.Right,
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
|
||||
};
|
||||
|
||||
// B滑块和文本框
|
||||
var bPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 0, 10, 5) };
|
||||
var bLabel = new TextBlock { Text = "B:", Width = 20, VerticalAlignment = VerticalAlignment.Center };
|
||||
var bLabel = new TextBlock { Text = "B:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
|
||||
var bSlider = new Slider
|
||||
{
|
||||
Minimum = 0,
|
||||
@@ -433,7 +435,8 @@ namespace Ink_Canvas
|
||||
Text = currentBackgroundColor.B.ToString(),
|
||||
Width = 30,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Right
|
||||
TextAlignment = TextAlignment.Right,
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
|
||||
};
|
||||
|
||||
// 现在添加事件处理程序
|
||||
@@ -716,7 +719,7 @@ namespace Ink_Canvas
|
||||
//}
|
||||
//else {
|
||||
// 禁用高级橡皮擦系统
|
||||
DisableAdvancedEraserSystem();
|
||||
DisableEraserOverlay();
|
||||
|
||||
forceEraser = true;
|
||||
forcePointEraser = false;
|
||||
@@ -726,7 +729,6 @@ namespace Ink_Canvas
|
||||
SetCurrentToolMode(InkCanvasEditingMode.EraseByStroke);
|
||||
drawingShapeMode = 0;
|
||||
|
||||
// 修复:切换到线擦时,确保重置笔的状态
|
||||
penType = 0;
|
||||
drawingAttributes.IsHighlighter = false;
|
||||
drawingAttributes.StylusTip = StylusTip.Ellipse;
|
||||
@@ -855,4 +857,4 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -9,7 +10,6 @@ using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Clipboard = System.Windows.Clipboard;
|
||||
using ContextMenu = System.Windows.Controls.ContextMenu;
|
||||
using Cursors = System.Windows.Input.Cursors;
|
||||
@@ -151,7 +151,7 @@ namespace Ink_Canvas
|
||||
// 设置图片属性,避免被InkCanvas选择系统处理
|
||||
image.IsHitTestVisible = true;
|
||||
image.Focusable = false;
|
||||
|
||||
|
||||
// 初始化InkCanvas选择设置
|
||||
if (inkCanvas != null)
|
||||
{
|
||||
@@ -173,7 +173,7 @@ namespace Ink_Canvas
|
||||
{
|
||||
// 先进行缩放居中处理
|
||||
CenterAndScaleElement(image);
|
||||
|
||||
|
||||
// 如果有指定位置,调整到指定位置
|
||||
if (position.HasValue)
|
||||
{
|
||||
@@ -181,7 +181,7 @@ namespace Ink_Canvas
|
||||
InkCanvas.SetLeft(image, position.Value.X - image.Width / 2);
|
||||
InkCanvas.SetTop(image, position.Value.Y - image.Height / 2);
|
||||
}
|
||||
|
||||
|
||||
// 绑定事件处理器
|
||||
if (image is FrameworkElement elementForEvents)
|
||||
{
|
||||
@@ -192,6 +192,8 @@ namespace Ink_Canvas
|
||||
elementForEvents.MouseWheel += Element_MouseWheel;
|
||||
|
||||
// 触摸事件
|
||||
elementForEvents.TouchDown += Element_TouchDown;
|
||||
elementForEvents.TouchUp += Element_TouchUp;
|
||||
elementForEvents.IsManipulationEnabled = true;
|
||||
elementForEvents.ManipulationDelta += Element_ManipulationDelta;
|
||||
elementForEvents.ManipulationCompleted += Element_ManipulationCompleted;
|
||||
@@ -205,6 +207,11 @@ namespace Ink_Canvas
|
||||
// 提交到历史记录
|
||||
timeMachine.CommitElementInsertHistory(image);
|
||||
|
||||
// 插入图片后切换到选择模式并刷新浮动栏高光显示
|
||||
SetCurrentToolMode(InkCanvasEditingMode.Select);
|
||||
UpdateCurrentToolMode("select");
|
||||
HideSubPanels("select");
|
||||
|
||||
ShowNotification("图片已从剪贴板粘贴");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
@@ -8,7 +9,6 @@ using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Ink_Canvas.Helpers;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -16,9 +16,12 @@ namespace Ink_Canvas
|
||||
{
|
||||
private int inkColor = 1;
|
||||
|
||||
private void ColorSwitchCheck()
|
||||
private void ColorSwitchCheck(bool hidePanels = true)
|
||||
{
|
||||
HideSubPanels("color");
|
||||
if (hidePanels)
|
||||
{
|
||||
HideSubPanels("color");
|
||||
}
|
||||
if (GridTransparencyFakeBackground.Background == Brushes.Transparent)
|
||||
{
|
||||
if (currentMode == 1)
|
||||
@@ -28,7 +31,7 @@ namespace Ink_Canvas
|
||||
AnimationsHelper.HideWithSlideAndFade(BlackboardLeftSide);
|
||||
AnimationsHelper.HideWithSlideAndFade(BlackboardCenterSide);
|
||||
AnimationsHelper.HideWithSlideAndFade(BlackboardRightSide);
|
||||
|
||||
|
||||
// 在PPT模式下隐藏手势面板和手势按钮
|
||||
AnimationsHelper.HideWithSlideAndFade(TwoFingerGestureBorder);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardTwoFingerGestureBorder);
|
||||
@@ -423,7 +426,7 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
// 更新快捷调色盘选择指示器
|
||||
if (penType == 0)
|
||||
if (penType == 0)
|
||||
{
|
||||
UpdateQuickColorPaletteIndicator(inkCanvas.DefaultDrawingAttributes.Color);
|
||||
}
|
||||
@@ -601,9 +604,9 @@ namespace Ink_Canvas
|
||||
drawingAttributes.Height = Settings.Canvas.HighlighterWidth;
|
||||
drawingAttributes.StylusTip = StylusTip.Rectangle;
|
||||
drawingAttributes.IsHighlighter = true;
|
||||
|
||||
|
||||
// 确保荧光笔模式切换后正确更新颜色和快捷调色板指示器
|
||||
ColorSwitchCheck();
|
||||
ColorSwitchCheck(false);
|
||||
}
|
||||
|
||||
private void BtnColorBlack_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -9,9 +11,9 @@ using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Microsoft.Win32;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -42,10 +44,10 @@ namespace Ink_Canvas
|
||||
// 设置图片属性,避免被InkCanvas选择系统处理
|
||||
image.IsHitTestVisible = true;
|
||||
image.Focusable = false;
|
||||
|
||||
|
||||
// 初始化InkCanvas选择设置
|
||||
InitializeInkCanvasSelectionSettings();
|
||||
|
||||
|
||||
// 先添加到画布
|
||||
inkCanvas.Children.Add(image);
|
||||
|
||||
@@ -62,12 +64,17 @@ namespace Ink_Canvas
|
||||
|
||||
// 最后绑定事件处理器
|
||||
BindElementEvents(image);
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile($"图片插入完成: {image.Name}");
|
||||
}), DispatcherPriority.Loaded);
|
||||
};
|
||||
|
||||
timeMachine.CommitElementInsertHistory(image);
|
||||
|
||||
// 插入图片后切换到选择模式并刷新浮动栏高光显示
|
||||
SetCurrentToolMode(InkCanvasEditingMode.Select);
|
||||
UpdateCurrentToolMode("select");
|
||||
HideSubPanels("select");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +105,7 @@ namespace Ink_Canvas
|
||||
|
||||
// 设置光标
|
||||
element.Cursor = Cursors.Hand;
|
||||
|
||||
|
||||
// 禁用InkCanvas对图片的选择处理
|
||||
element.IsHitTestVisible = true;
|
||||
element.Focusable = false;
|
||||
@@ -109,6 +116,12 @@ namespace Ink_Canvas
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
if (inkCanvas.EditingMode != InkCanvasEditingMode.Select)
|
||||
{
|
||||
e.Handled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 取消之前选中的元素
|
||||
if (currentSelectedElement != null && currentSelectedElement != element)
|
||||
{
|
||||
@@ -121,7 +134,7 @@ namespace Ink_Canvas
|
||||
|
||||
// 选中当前元素
|
||||
SelectElement(element);
|
||||
|
||||
|
||||
// 开始拖动
|
||||
isDragging = true;
|
||||
dragStartPoint = e.GetPosition(inkCanvas);
|
||||
@@ -145,13 +158,26 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸释放事件
|
||||
private void Element_TouchUp(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
isDragging = false;
|
||||
element.ReleaseTouchCapture(e.TouchDevice);
|
||||
element.Cursor = Cursors.Hand;
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标移动事件
|
||||
private void Element_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element && isDragging && element.IsMouseCaptured)
|
||||
{
|
||||
var currentPoint = e.GetPosition(inkCanvas);
|
||||
|
||||
|
||||
// 使用鼠标拖动的完整实现机制
|
||||
ApplyMouseDragTransform(element, currentPoint, dragStartPoint);
|
||||
|
||||
@@ -161,6 +187,12 @@ namespace Ink_Canvas
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
}
|
||||
|
||||
// 如果是图片元素,更新选择点位置
|
||||
if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
}
|
||||
|
||||
dragStartPoint = currentPoint;
|
||||
e.Handled = true;
|
||||
}
|
||||
@@ -172,7 +204,7 @@ namespace Ink_Canvas
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
|
||||
|
||||
|
||||
// 使用滚轮缩放的核心机制
|
||||
ApplyWheelScaleTransform(element, e);
|
||||
|
||||
@@ -182,6 +214,46 @@ namespace Ink_Canvas
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
}
|
||||
|
||||
// 如果是图片元素,更新选择点位置
|
||||
if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸按下事件
|
||||
private void Element_TouchDown(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
if (inkCanvas.EditingMode != InkCanvasEditingMode.Select)
|
||||
{
|
||||
e.Handled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 取消之前选中的元素
|
||||
if (currentSelectedElement != null && currentSelectedElement != element)
|
||||
{
|
||||
// 保存当前编辑模式
|
||||
var previousEditingMode = inkCanvas.EditingMode;
|
||||
UnselectElement(currentSelectedElement);
|
||||
// 恢复编辑模式
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
}
|
||||
|
||||
// 选中当前元素
|
||||
SelectElement(element);
|
||||
|
||||
// 开始拖动
|
||||
isDragging = true;
|
||||
dragStartPoint = e.GetTouchPoint(inkCanvas).Position;
|
||||
element.CaptureTouch(e.TouchDevice);
|
||||
element.Cursor = Cursors.SizeAll;
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -191,7 +263,16 @@ namespace Ink_Canvas
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
// 使用触摸拖动的完整实现
|
||||
// 检查是否是双指手势
|
||||
if (e.Manipulators.Count() >= 2)
|
||||
{
|
||||
// 双指手势时,不处理单个元素的手势,让画布级别的手势处理
|
||||
// 这样可以实现图片与墨迹的同步移动
|
||||
e.Handled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 单指手势时,使用触摸拖动的完整实现
|
||||
ApplyTouchManipulationTransform(element, e);
|
||||
|
||||
// 如果是图片元素,更新工具栏位置
|
||||
@@ -200,6 +281,12 @@ namespace Ink_Canvas
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
}
|
||||
|
||||
// 如果是图片元素,更新选择点位置
|
||||
if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -264,7 +351,7 @@ namespace Ink_Canvas
|
||||
private void SelectElement(FrameworkElement element)
|
||||
{
|
||||
currentSelectedElement = element;
|
||||
|
||||
|
||||
// 根据元素类型显示不同的选择工具栏
|
||||
if (element is Image)
|
||||
{
|
||||
@@ -275,41 +362,41 @@ namespace Ink_Canvas
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
BorderImageSelectionControl.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
// 隐藏笔画选择工具栏
|
||||
if (BorderStrokeSelectionControl != null)
|
||||
{
|
||||
BorderStrokeSelectionControl.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// 显示图片缩放选择点
|
||||
ShowImageResizeHandles(element);
|
||||
|
||||
// 墨迹选择工具栏通过GridInkCanvasSelectionCover的可见性来控制
|
||||
// 不需要直接设置BorderStrokeSelectionControl.Visibility
|
||||
}
|
||||
else
|
||||
{
|
||||
// 显示笔画选择工具栏
|
||||
if (BorderStrokeSelectionControl != null)
|
||||
{
|
||||
BorderStrokeSelectionControl.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
// 隐藏图片选择工具栏
|
||||
if (BorderImageSelectionControl != null)
|
||||
{
|
||||
BorderImageSelectionControl.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// 隐藏图片缩放选择点
|
||||
HideImageResizeHandles();
|
||||
|
||||
// 墨迹选择工具栏通过GridInkCanvasSelectionCover的可见性来控制
|
||||
// 不需要直接设置BorderStrokeSelectionControl.Visibility
|
||||
}
|
||||
|
||||
|
||||
// 确保选择框不显示,避免蓝色边框
|
||||
if (GridInkCanvasSelectionCover != null)
|
||||
{
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
|
||||
// 禁用InkCanvas的选择功能,去除控制点
|
||||
if (inkCanvas != null)
|
||||
{
|
||||
// 清除当前选择
|
||||
inkCanvas.Select(new StrokeCollection());
|
||||
// 设置编辑模式为非选择模式
|
||||
inkCanvas.EditingMode = InkCanvasEditingMode.None;
|
||||
// 保持选择模式,这样用户可以直接点击墨迹来选择
|
||||
inkCanvas.EditingMode = InkCanvasEditingMode.Select;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,25 +404,30 @@ namespace Ink_Canvas
|
||||
private void UnselectElement(FrameworkElement element)
|
||||
{
|
||||
// 去除选中效果
|
||||
|
||||
// 隐藏所有选择工具栏
|
||||
|
||||
// 隐藏图片选择工具栏
|
||||
if (BorderImageSelectionControl != null)
|
||||
{
|
||||
BorderImageSelectionControl.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (BorderStrokeSelectionControl != null)
|
||||
{
|
||||
BorderStrokeSelectionControl.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
|
||||
// 隐藏图片缩放选择点
|
||||
HideImageResizeHandles();
|
||||
|
||||
// 墨迹选择工具栏通过GridInkCanvasSelectionCover的可见性来控制
|
||||
// 不需要直接设置BorderStrokeSelectionControl.Visibility
|
||||
|
||||
// 确保选择框隐藏
|
||||
if (GridInkCanvasSelectionCover != null)
|
||||
{
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
|
||||
// 确保InkCanvas处于选择模式,这样用户可以直接点击墨迹来选择
|
||||
if (inkCanvas != null)
|
||||
{
|
||||
inkCanvas.EditingMode = InkCanvasEditingMode.Select;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用矩阵变换到元素
|
||||
@@ -345,7 +437,7 @@ namespace Ink_Canvas
|
||||
{
|
||||
// 创建MatrixTransform
|
||||
var matrixTransform = new MatrixTransform(matrix);
|
||||
|
||||
|
||||
// 将MatrixTransform添加到TransformGroup
|
||||
transformGroup.Children.Add(matrixTransform);
|
||||
}
|
||||
@@ -358,25 +450,25 @@ namespace Ink_Canvas
|
||||
{
|
||||
// 根据滚轮方向确定缩放比例(向上1.1倍,向下0.9倍)
|
||||
double scaleFactor = e.Delta > 0 ? 1.1 : 0.9;
|
||||
|
||||
|
||||
// 计算选中元素的中心点作为缩放中心
|
||||
var elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
|
||||
|
||||
|
||||
// 创建 Matrix 对象并应用 ScaleAt 变换
|
||||
var matrix = new Matrix();
|
||||
matrix.ScaleAt(scaleFactor, scaleFactor, elementCenter.X, elementCenter.Y);
|
||||
|
||||
|
||||
// 对选中的图片元素调用 ApplyElementMatrixTransform
|
||||
ApplyElementMatrixTransform(element, matrix);
|
||||
|
||||
|
||||
// 对选中的笔画应用 Transform 方法(如果有选中的笔画)
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -399,20 +491,20 @@ namespace Ink_Canvas
|
||||
|
||||
// 保存初始变换状态用于历史记录
|
||||
var initialTransform = transformGroup.Clone();
|
||||
|
||||
|
||||
// 创建新的 TransformGroup 并添加 MatrixTransform
|
||||
var newTransformGroup = new TransformGroup();
|
||||
newTransformGroup.Children.Add(new MatrixTransform(matrix));
|
||||
|
||||
|
||||
// 将新的变换组添加到现有的变换组中
|
||||
transformGroup.Children.Add(newTransformGroup);
|
||||
|
||||
|
||||
// 如果启用了历史记录,提交变换历史
|
||||
if (saveHistory)
|
||||
{
|
||||
CommitTransformHistory(element, initialTransform, transformGroup);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -428,24 +520,24 @@ namespace Ink_Canvas
|
||||
{
|
||||
// 计算鼠标移动的位移向量
|
||||
var delta = currentPoint - startPoint;
|
||||
|
||||
|
||||
// 创建 Matrix 对象并应用 Translate 变换
|
||||
var matrix = new Matrix();
|
||||
matrix.Translate(delta.X, delta.Y);
|
||||
|
||||
|
||||
// 对选中的图片元素应用矩阵变换
|
||||
ApplyMatrixTransformToElement(element, matrix, false);
|
||||
|
||||
|
||||
// 对选中的笔画应用变换
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
|
||||
|
||||
// 更新选择框的位置(如果有选择框)
|
||||
UpdateSelectionBorderPosition(delta);
|
||||
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -503,7 +595,7 @@ namespace Ink_Canvas
|
||||
|
||||
// 支持单指拖动和多指手势
|
||||
// 可以同时进行平移、旋转和缩放
|
||||
|
||||
|
||||
// 通过 ManipulationDelta 获取手势变化信息
|
||||
var translation = md.Translation;
|
||||
var rotation = md.Rotation;
|
||||
@@ -519,13 +611,13 @@ namespace Ink_Canvas
|
||||
if (e.Manipulators.Count() >= 2)
|
||||
{
|
||||
var center = e.ManipulationOrigin;
|
||||
|
||||
|
||||
// 应用缩放
|
||||
if (scale.X != 1.0 || scale.Y != 1.0)
|
||||
{
|
||||
matrix.ScaleAt(scale.X, scale.Y, center.X, center.Y);
|
||||
}
|
||||
|
||||
|
||||
// 应用旋转
|
||||
if (rotation != 0)
|
||||
{
|
||||
@@ -535,15 +627,15 @@ namespace Ink_Canvas
|
||||
|
||||
// 应用变换到元素
|
||||
ApplyMatrixTransformToElement(element, matrix, false);
|
||||
|
||||
|
||||
// 应用变换到选中的笔画
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1087,19 +1179,19 @@ namespace Ink_Canvas
|
||||
{
|
||||
// 创建克隆图片
|
||||
Image clonedImage = CloneImage(originalImage);
|
||||
|
||||
|
||||
// 添加到画布
|
||||
inkCanvas.Children.Add(clonedImage);
|
||||
|
||||
|
||||
// 初始化变换
|
||||
InitializeElementTransform(clonedImage);
|
||||
|
||||
|
||||
// 绑定事件
|
||||
BindElementEvents(clonedImage);
|
||||
|
||||
|
||||
// 记录历史
|
||||
timeMachine.CommitElementInsertHistory(clonedImage);
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile($"图片克隆完成: {clonedImage.Name}");
|
||||
}
|
||||
}
|
||||
@@ -1118,10 +1210,10 @@ namespace Ink_Canvas
|
||||
{
|
||||
// 创建新页面
|
||||
BtnWhiteBoardAdd_Click(null, null);
|
||||
|
||||
|
||||
// 创建克隆图片(不添加到当前画布,因为已经创建了新页面)
|
||||
Image clonedImage = CreateClonedImage(originalImage);
|
||||
|
||||
|
||||
if (clonedImage != null)
|
||||
{
|
||||
// 设置图片属性,避免被InkCanvas选择系统处理
|
||||
@@ -1139,7 +1231,7 @@ namespace Ink_Canvas
|
||||
|
||||
// 记录历史
|
||||
timeMachine.CommitElementInsertHistory(clonedImage);
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile($"图片克隆到新页面完成: {clonedImage.Name}");
|
||||
}
|
||||
}
|
||||
@@ -1158,13 +1250,13 @@ namespace Ink_Canvas
|
||||
if (currentSelectedElement != null)
|
||||
{
|
||||
ApplyRotateTransform(currentSelectedElement, -45);
|
||||
|
||||
|
||||
// 更新工具栏位置
|
||||
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
}
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile("图片左旋转完成");
|
||||
}
|
||||
}
|
||||
@@ -1182,13 +1274,13 @@ namespace Ink_Canvas
|
||||
if (currentSelectedElement != null)
|
||||
{
|
||||
ApplyRotateTransform(currentSelectedElement, 45);
|
||||
|
||||
|
||||
// 更新工具栏位置
|
||||
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
}
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile("图片右旋转完成");
|
||||
}
|
||||
}
|
||||
@@ -1207,13 +1299,13 @@ namespace Ink_Canvas
|
||||
{
|
||||
var elementCenter = new Point(currentSelectedElement.ActualWidth / 2, currentSelectedElement.ActualHeight / 2);
|
||||
ApplyScaleTransform(currentSelectedElement, 0.9, elementCenter);
|
||||
|
||||
|
||||
// 更新工具栏位置
|
||||
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
}
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile("图片缩放减小完成");
|
||||
}
|
||||
}
|
||||
@@ -1229,23 +1321,23 @@ namespace Ink_Canvas
|
||||
try
|
||||
{
|
||||
if (currentSelectedElement != null)
|
||||
{
|
||||
var elementCenter = new Point(currentSelectedElement.ActualWidth / 2, currentSelectedElement.ActualHeight / 2);
|
||||
ApplyScaleTransform(currentSelectedElement, 1.1, elementCenter);
|
||||
|
||||
// 更新工具栏位置
|
||||
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
var elementCenter = new Point(currentSelectedElement.ActualWidth / 2, currentSelectedElement.ActualHeight / 2);
|
||||
ApplyScaleTransform(currentSelectedElement, 1.1, elementCenter);
|
||||
|
||||
// 更新工具栏位置
|
||||
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("图片缩放增大完成");
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("图片缩放增大完成");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"图片缩放增大失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"图片缩放增大失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 图片删除
|
||||
@@ -1257,20 +1349,20 @@ namespace Ink_Canvas
|
||||
{
|
||||
// 保存删除前的编辑模式
|
||||
var previousEditingMode = inkCanvas.EditingMode;
|
||||
|
||||
|
||||
// 记录删除历史
|
||||
timeMachine.CommitElementRemoveHistory(currentSelectedElement);
|
||||
|
||||
|
||||
// 从画布中移除
|
||||
inkCanvas.Children.Remove(currentSelectedElement);
|
||||
|
||||
|
||||
// 清除选中状态
|
||||
UnselectElement(currentSelectedElement);
|
||||
currentSelectedElement = null;
|
||||
|
||||
|
||||
// 恢复到删除前的编辑模式
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile($"图片删除完成,已恢复到编辑模式: {previousEditingMode}");
|
||||
}
|
||||
}
|
||||
@@ -1286,35 +1378,35 @@ namespace Ink_Canvas
|
||||
try
|
||||
{
|
||||
Image clonedImage = new Image();
|
||||
|
||||
|
||||
// 复制图片源
|
||||
if (originalImage.Source is BitmapSource bitmapSource)
|
||||
{
|
||||
clonedImage.Source = bitmapSource;
|
||||
}
|
||||
|
||||
|
||||
// 复制属性
|
||||
clonedImage.Width = originalImage.Width;
|
||||
clonedImage.Height = originalImage.Height;
|
||||
clonedImage.Stretch = originalImage.Stretch;
|
||||
clonedImage.StretchDirection = originalImage.StretchDirection;
|
||||
|
||||
|
||||
// 复制位置(在新页面中居中显示)
|
||||
double left = InkCanvas.GetLeft(originalImage);
|
||||
double top = InkCanvas.GetTop(originalImage);
|
||||
InkCanvas.SetLeft(clonedImage, left + 20); // 稍微偏移位置
|
||||
InkCanvas.SetTop(clonedImage, top + 20);
|
||||
|
||||
|
||||
// 复制变换
|
||||
if (originalImage.RenderTransform is TransformGroup originalTransformGroup)
|
||||
{
|
||||
clonedImage.RenderTransform = originalTransformGroup.Clone();
|
||||
}
|
||||
|
||||
|
||||
// 设置名称
|
||||
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
|
||||
clonedImage.Name = timestamp;
|
||||
|
||||
|
||||
return clonedImage;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1324,6 +1416,323 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 克隆墨迹集合
|
||||
/// </summary>
|
||||
/// <param name="strokes">要克隆的墨迹集合</param>
|
||||
/// <returns>克隆后的墨迹集合</returns>
|
||||
private StrokeCollection CloneStrokes(StrokeCollection strokes)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0) return new StrokeCollection();
|
||||
|
||||
try
|
||||
{
|
||||
// 创建墨迹集合的副本
|
||||
var clonedStrokes = strokes.Clone();
|
||||
|
||||
// 为每个墨迹添加位置偏移以避免重叠
|
||||
foreach (var stroke in clonedStrokes)
|
||||
{
|
||||
var offsetPoints = new StylusPointCollection();
|
||||
foreach (var point in stroke.StylusPoints)
|
||||
{
|
||||
offsetPoints.Add(new StylusPoint(point.X + 20, point.Y + 20, point.PressureFactor));
|
||||
}
|
||||
stroke.StylusPoints = offsetPoints;
|
||||
}
|
||||
|
||||
// 添加到画布
|
||||
inkCanvas.Strokes.Add(clonedStrokes);
|
||||
|
||||
// 提交到时间机器以支持撤销
|
||||
timeMachine.CommitStrokeUserInputHistory(clonedStrokes);
|
||||
|
||||
LogHelper.WriteLogToFile($"墨迹克隆完成: {clonedStrokes.Count} 个墨迹");
|
||||
return clonedStrokes;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误但不中断程序
|
||||
LogHelper.WriteLogToFile($"克隆墨迹时发生错误: {ex.Message}", LogHelper.LogType.Error);
|
||||
return new StrokeCollection();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 克隆墨迹集合到新页面
|
||||
/// </summary>
|
||||
/// <param name="strokes">要克隆的墨迹集合</param>
|
||||
private void CloneStrokesToNewBoard(StrokeCollection strokes)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 创建墨迹集合的副本
|
||||
var clonedStrokes = strokes.Clone();
|
||||
|
||||
// 为每个墨迹添加位置偏移以避免重叠
|
||||
foreach (var stroke in clonedStrokes)
|
||||
{
|
||||
var offsetPoints = new StylusPointCollection();
|
||||
foreach (var point in stroke.StylusPoints)
|
||||
{
|
||||
offsetPoints.Add(new StylusPoint(point.X + 20, point.Y + 20, point.PressureFactor));
|
||||
}
|
||||
stroke.StylusPoints = offsetPoints;
|
||||
}
|
||||
|
||||
// 创建新页面
|
||||
BtnWhiteBoardAdd_Click(null, null);
|
||||
|
||||
// 添加到新页面的画布
|
||||
inkCanvas.Strokes.Add(clonedStrokes);
|
||||
|
||||
// 提交到时间机器以支持撤销
|
||||
timeMachine.CommitStrokeUserInputHistory(clonedStrokes);
|
||||
|
||||
LogHelper.WriteLogToFile($"墨迹克隆到新页面完成: {clonedStrokes.Count} 个墨迹");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误但不中断程序
|
||||
LogHelper.WriteLogToFile($"克隆墨迹到新页面时发生错误: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Image Resize Handles
|
||||
|
||||
// 图片缩放选择点相关变量
|
||||
private bool isResizingImage = false;
|
||||
private Point imageResizeStartPoint;
|
||||
private string activeResizeHandle = "";
|
||||
|
||||
// 显示图片缩放选择点
|
||||
private void ShowImageResizeHandles(FrameworkElement element)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ImageResizeHandlesCanvas == null || element == null) return;
|
||||
|
||||
// 获取元素的实际边界
|
||||
Rect elementBounds = GetElementActualBounds(element);
|
||||
|
||||
// 设置选择点位置
|
||||
UpdateImageResizeHandlesPosition(elementBounds);
|
||||
|
||||
// 显示选择点
|
||||
ImageResizeHandlesCanvas.Visibility = Visibility.Visible;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"显示图片缩放选择点失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏图片缩放选择点
|
||||
private void HideImageResizeHandles()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ImageResizeHandlesCanvas != null)
|
||||
{
|
||||
ImageResizeHandlesCanvas.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"隐藏图片缩放选择点失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图片缩放选择点位置
|
||||
private void UpdateImageResizeHandlesPosition(Rect elementBounds)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ImageResizeHandlesCanvas == null) return;
|
||||
|
||||
ImageResizeHandlesCanvas.Margin = new Thickness(elementBounds.Left, elementBounds.Top, 0, 0);
|
||||
|
||||
// 四个角控制点
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageTopLeftHandle, -4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageTopLeftHandle, -4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageTopRightHandle, elementBounds.Width - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageTopRightHandle, -4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageBottomLeftHandle, -4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageBottomLeftHandle, elementBounds.Height - 4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageBottomRightHandle, elementBounds.Width - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageBottomRightHandle, elementBounds.Height - 4);
|
||||
|
||||
// 四个边控制点
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageTopHandle, elementBounds.Width / 2 - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageTopHandle, -4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageBottomHandle, elementBounds.Width / 2 - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageBottomHandle, elementBounds.Height - 4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageLeftHandle, -4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageLeftHandle, elementBounds.Height / 2 - 4);
|
||||
|
||||
System.Windows.Controls.Canvas.SetLeft(ImageRightHandle, elementBounds.Width - 4);
|
||||
System.Windows.Controls.Canvas.SetTop(ImageRightHandle, elementBounds.Height / 2 - 4);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新图片缩放选择点位置失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 图片缩放选择点鼠标按下事件
|
||||
private void ImageResizeHandle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (currentSelectedElement is Image image && sender is Ellipse ellipse)
|
||||
{
|
||||
isResizingImage = true;
|
||||
imageResizeStartPoint = e.GetPosition(inkCanvas);
|
||||
|
||||
// 确定是哪个控制点
|
||||
activeResizeHandle = ellipse.Name;
|
||||
|
||||
// 捕获鼠标
|
||||
ellipse.CaptureMouse();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"图片缩放选择点鼠标按下事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 图片缩放选择点鼠标释放事件
|
||||
private void ImageResizeHandle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isResizingImage && sender is Ellipse ellipse)
|
||||
{
|
||||
isResizingImage = false;
|
||||
ellipse.ReleaseMouseCapture();
|
||||
activeResizeHandle = "";
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"图片缩放选择点鼠标释放事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 图片缩放选择点鼠标移动事件
|
||||
private void ImageResizeHandle_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isResizingImage && currentSelectedElement is Image image && sender is Ellipse ellipse)
|
||||
{
|
||||
var currentPoint = e.GetPosition(inkCanvas);
|
||||
ResizeImageByHandle(image, imageResizeStartPoint, currentPoint, activeResizeHandle);
|
||||
imageResizeStartPoint = currentPoint;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"图片缩放选择点鼠标移动事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据控制点缩放图片
|
||||
private void ResizeImageByHandle(Image image, Point startPoint, Point currentPoint, string handleName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (image.RenderTransform is TransformGroup transformGroup)
|
||||
{
|
||||
var scaleTransform = transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault();
|
||||
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
|
||||
|
||||
if (scaleTransform == null || translateTransform == null) return;
|
||||
|
||||
// 获取图片的当前边界
|
||||
Rect currentBounds = GetElementActualBounds(image);
|
||||
double deltaX = currentPoint.X - startPoint.X;
|
||||
double deltaY = currentPoint.Y - startPoint.Y;
|
||||
|
||||
// 计算缩放比例
|
||||
double scaleX = 1.0;
|
||||
double scaleY = 1.0;
|
||||
double translateX = 0;
|
||||
double translateY = 0;
|
||||
|
||||
switch (handleName)
|
||||
{
|
||||
case "ImageTopLeftHandle":
|
||||
scaleX = (currentBounds.Width - deltaX) / currentBounds.Width;
|
||||
scaleY = (currentBounds.Height - deltaY) / currentBounds.Height;
|
||||
translateX = deltaX;
|
||||
translateY = deltaY;
|
||||
break;
|
||||
case "ImageTopRightHandle":
|
||||
scaleX = (currentBounds.Width + deltaX) / currentBounds.Width;
|
||||
scaleY = (currentBounds.Height - deltaY) / currentBounds.Height;
|
||||
translateY = deltaY;
|
||||
break;
|
||||
case "ImageBottomLeftHandle":
|
||||
scaleX = (currentBounds.Width - deltaX) / currentBounds.Width;
|
||||
scaleY = (currentBounds.Height + deltaY) / currentBounds.Height;
|
||||
translateX = deltaX;
|
||||
break;
|
||||
case "ImageBottomRightHandle":
|
||||
scaleX = (currentBounds.Width + deltaX) / currentBounds.Width;
|
||||
scaleY = (currentBounds.Height + deltaY) / currentBounds.Height;
|
||||
break;
|
||||
case "ImageTopHandle":
|
||||
scaleY = (currentBounds.Height - deltaY) / currentBounds.Height;
|
||||
translateY = deltaY;
|
||||
break;
|
||||
case "ImageBottomHandle":
|
||||
scaleY = (currentBounds.Height + deltaY) / currentBounds.Height;
|
||||
break;
|
||||
case "ImageLeftHandle":
|
||||
scaleX = (currentBounds.Width - deltaX) / currentBounds.Width;
|
||||
translateX = deltaX;
|
||||
break;
|
||||
case "ImageRightHandle":
|
||||
scaleX = (currentBounds.Width + deltaX) / currentBounds.Width;
|
||||
break;
|
||||
}
|
||||
|
||||
// 限制缩放范围
|
||||
scaleX = Math.Max(0.1, Math.Min(scaleX, 5.0));
|
||||
scaleY = Math.Max(0.1, Math.Min(scaleY, 5.0));
|
||||
|
||||
// 应用缩放
|
||||
scaleTransform.ScaleX *= scaleX;
|
||||
scaleTransform.ScaleY *= scaleY;
|
||||
|
||||
// 应用平移
|
||||
translateTransform.X += translateX;
|
||||
translateTransform.Y += translateY;
|
||||
|
||||
// 更新选择点位置
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(image));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"根据控制点缩放图片失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<DrawingGroup x:Key="EraserDrawingGroup" ClipGeometry="M0,0 V56 H38 V0 H0 Z">
|
||||
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M38,56z M0,0z M0,4C0,1.79086,1.79086,0,4,0L34,0C36.2091,0,38,1.79086,38,4L38,52C38,54.2091,36.2091,56,34,56L4,56C1.79086,56,0,54.2091,0,52L0,4z" />
|
||||
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M38,56z M0,0z M34,1L4,1C2.34315,1,1,2.34315,1,4L1,52C1,53.6569,2.34315,55,4,55L34,55C35.6569,55,37,53.6569,37,52L37,4C37,2.34315,35.6569,1,34,1z M4,0C1.79086,0,0,1.79086,0,4L0,52C0,54.2091,1.79086,56,4,56L34,56C36.2091,56,38,54.2091,38,52L38,4C38,1.79086,36.2091,0,34,0L4,0z" />
|
||||
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M12,19.5C12,18.1193,13.1193,17,14.5,17L14.5,17C15.8807,17,17,18.1193,17,19.5L17,36.5C17,37.8807,15.8807,39,14.5,39L14.5,39C13.1193,39,12,37.8807,12,36.5L12,19.5z" />
|
||||
<GeometryDrawing Geometry="F0 M38,56z M0,0z M11.5,19.5C11.5,17.8431 12.8431,16.5 14.5,16.5 16.1569,16.5 17.5,17.8431 17.5,19.5L17.5,36.5C17.5,38.1569 16.1569,39.5 14.5,39.5 12.8431,39.5 11.5,38.1569 11.5,36.5L11.5,19.5z M14.5,17.5C13.3954,17.5,12.5,18.3954,12.5,19.5L12.5,36.5C12.5,37.6046 13.3954,38.5 14.5,38.5 15.6046,38.5 16.5,37.6046 16.5,36.5L16.5,19.5C16.5,18.3954,15.6046,17.5,14.5,17.5z">
|
||||
<GeometryDrawing.Brush>
|
||||
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
|
||||
</GeometryDrawing.Brush>
|
||||
</GeometryDrawing>
|
||||
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
|
||||
<GeometryDrawing Geometry="F0 M38,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
|
||||
<GeometryDrawing.Brush>
|
||||
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
|
||||
</GeometryDrawing.Brush>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- 矩形橡皮擦图像资源 -->
|
||||
<DrawingImage x:Key="RectangleEraserImageSource">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V56 H38 V0 H0 Z">
|
||||
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M38,56z M0,0z M0,4C0,1.79086,1.79086,0,4,0L34,0C36.2091,0,38,1.79086,38,4L38,52C38,54.2091,36.2091,56,34,56L4,56C1.79086,56,0,54.2091,0,52L0,4z" />
|
||||
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M38,56z M0,0z M34,1L4,1C2.34315,1,1,2.34315,1,4L1,52C1,53.6569,2.34315,55,4,55L34,55C35.6569,55,37,53.6569,37,52L37,4C37,2.34315,35.6569,1,34,1z M4,0C1.79086,0,0,1.79086,0,4L0,52C0,54.2091,1.79086,56,4,56L34,56C36.2091,56,38,54.2091,38,52L38,4C38,1.79086,36.2091,0,34,0L4,0z" />
|
||||
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M12,19.5C12,18.1193,13.1193,17,14.5,17L14.5,17C15.8807,17,17,18.1193,17,19.5L17,36.5C17,37.8807,15.8807,39,14.5,39L14.5,39C13.1193,39,12,37.8807,12,36.5L12,19.5z" />
|
||||
<GeometryDrawing Geometry="F0 M38,56z M0,0z M11.5,19.5C11.5,17.8431 12.8431,16.5 14.5,16.5 16.1569,16.5 17.5,17.8431 17.5,19.5L17.5,36.5C17.5,38.1569 16.1569,39.5 14.5,39.5 12.8431,39.5 11.5,38.1569 11.5,36.5L11.5,19.5z M14.5,17.5C13.3954,17.5,12.5,18.3954,12.5,19.5L12.5,36.5C12.5,37.6046 13.3954,38.5 14.5,38.5 15.6046,38.5 16.5,37.6046 16.5,36.5L16.5,19.5C16.5,18.3954,15.6046,17.5,14.5,17.5z">
|
||||
<GeometryDrawing.Brush>
|
||||
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
|
||||
</GeometryDrawing.Brush>
|
||||
</GeometryDrawing>
|
||||
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
|
||||
<GeometryDrawing Geometry="F0 M38,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
|
||||
<GeometryDrawing.Brush>
|
||||
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
|
||||
</GeometryDrawing.Brush>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
|
||||
<DrawingGroup x:Key="EraserCircleDrawingGroup" ClipGeometry="M0,0 V56 H56 V0 H0 Z">
|
||||
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M56,56z M0,0z M0,28C0,12.536,12.536,0,28,0L28,0C43.464,0,56,12.536,56,28L56,28C56,43.464,43.464,56,28,56L28,56C12.536,56,0,43.464,0,28L0,28z" />
|
||||
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M56,56z M0,0z M1,28C1,42.9117 13.0883,55 28,55 42.9117,55 55,42.9117 55,28 55,13.0883 42.9117,1 28,1 13.0883,1 1,13.0883 1,28z M28,0C12.536,0 0,12.536 0,28 0,43.464 12.536,56 28,56 43.464,56 56,43.464 56,28 56,12.536 43.464,0 28,0z" />
|
||||
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
|
||||
<GeometryDrawing Geometry="F0 M56,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
|
||||
<GeometryDrawing.Brush>
|
||||
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
|
||||
</GeometryDrawing.Brush>
|
||||
</GeometryDrawing>
|
||||
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M30,19.5C30,18.1193,31.1193,17,32.5,17L32.5,17C33.8807,17,35,18.1193,35,19.5L35,36.5C35,37.8807,33.8807,39,32.5,39L32.5,39C31.1193,39,30,37.8807,30,36.5L30,19.5z" />
|
||||
<GeometryDrawing Geometry="F0 M56,56z M0,0z M29.5,19.5C29.5,17.8431 30.8431,16.5 32.5,16.5 34.1569,16.5 35.5,17.8431 35.5,19.5L35.5,36.5C35.5,38.1569 34.1569,39.5 32.5,39.5 30.8431,39.5 29.5,38.1569 29.5,36.5L29.5,19.5z M32.5,17.5C31.3954,17.5,30.5,18.3954,30.5,19.5L30.5,36.5C30.5,37.6046 31.3954,38.5 32.5,38.5 33.6046,38.5 34.5,37.6046 34.5,36.5L34.5,19.5C34.5,18.3954,33.6046,17.5,32.5,17.5z">
|
||||
<GeometryDrawing.Brush>
|
||||
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
|
||||
</GeometryDrawing.Brush>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
<!-- 圆形橡皮擦图像资源 -->
|
||||
<DrawingImage x:Key="EllipseEraserImageSource">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V56 H56 V0 H0 Z">
|
||||
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M56,56z M0,0z M0,28C0,12.536,12.536,0,28,0L28,0C43.464,0,56,12.536,56,28L56,28C56,43.464,43.464,56,28,56L28,56C12.536,56,0,43.464,0,28L0,28z" />
|
||||
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M56,56z M0,0z M1,28C1,42.9117 13.0883,55 28,55 42.9117,55 55,42.9117 55,28 55,13.0883 42.9117,1 28,1 13.0883,1 1,13.0883 1,28z M28,0C12.536,0 0,12.536 0,28 0,43.464 12.536,56 28,56 43.464,56 56,43.464 56,28 56,12.536 43.464,0 28,0z" />
|
||||
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
|
||||
<GeometryDrawing Geometry="F0 M56,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
|
||||
<GeometryDrawing.Brush>
|
||||
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
|
||||
</GeometryDrawing.Brush>
|
||||
</GeometryDrawing>
|
||||
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M30,19.5C30,18.1193,31.1193,17,32.5,17L32.5,17C33.8807,17,35,18.1193,35,19.5L35,36.5C35,37.8807,33.8807,39,32.5,39L32.5,39C31.1193,39,30,37.8807,30,36.5L30,19.5z" />
|
||||
<GeometryDrawing Geometry="F0 M56,56z M0,0z M29.5,19.5C29.5,17.8431 30.8431,16.5 32.5,16.5 34.1569,16.5 35.5,17.8431 35.5,19.5L35.5,36.5C35.5,38.1569 34.1569,39.5 32.5,39.5 30.8431,39.5 29.5,38.1569 29.5,36.5L29.5,19.5z M32.5,17.5C31.3954,17.5,30.5,18.3954,30.5,19.5L30.5,36.5C30.5,37.6046 31.3954,38.5 32.5,38.5 33.6046,38.5 34.5,37.6046 34.5,36.5L34.5,19.5C34.5,18.3954,33.6046,17.5,32.5,17.5z">
|
||||
<GeometryDrawing.Brush>
|
||||
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
|
||||
</GeometryDrawing.Brush>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,358 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
#region 悬浮窗拦截功能
|
||||
|
||||
/// <summary>
|
||||
/// 初始化悬浮窗拦截管理器
|
||||
/// </summary>
|
||||
private void InitializeFloatingWindowInterceptor()
|
||||
{
|
||||
try
|
||||
{
|
||||
_floatingWindowInterceptorManager = new FloatingWindowInterceptorManager();
|
||||
|
||||
// 订阅事件
|
||||
_floatingWindowInterceptorManager.WindowIntercepted += OnFloatingWindowIntercepted;
|
||||
_floatingWindowInterceptorManager.WindowRestored += OnFloatingWindowRestored;
|
||||
|
||||
// 初始化拦截器
|
||||
_floatingWindowInterceptorManager.Initialize(Settings.Automation.FloatingWindowInterceptor);
|
||||
|
||||
// 加载UI状态
|
||||
LoadFloatingWindowInterceptorUI();
|
||||
|
||||
LogHelper.WriteLogToFile("悬浮窗拦截管理器初始化完成", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化悬浮窗拦截管理器失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载悬浮窗拦截UI状态
|
||||
/// </summary>
|
||||
private void LoadFloatingWindowInterceptorUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
|
||||
// 设置主开关状态
|
||||
ToggleSwitchFloatingWindowInterceptorEnabled.IsOn = Settings.Automation.FloatingWindowInterceptor.IsEnabled;
|
||||
|
||||
// 设置各个拦截规则的状态
|
||||
foreach (var kvp in Settings.Automation.FloatingWindowInterceptor.InterceptRules)
|
||||
{
|
||||
var toggleName = $"ToggleSwitch{kvp.Key}";
|
||||
var toggle = FindName(toggleName) as ToggleSwitch;
|
||||
if (toggle != null)
|
||||
{
|
||||
toggle.IsOn = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新UI可见性
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载悬浮窗拦截UI状态失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新悬浮窗拦截UI
|
||||
/// </summary>
|
||||
private void UpdateFloatingWindowInterceptorUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
var isEnabled = Settings.Automation.FloatingWindowInterceptor.IsEnabled;
|
||||
FloatingWindowInterceptorGrid.Visibility = isEnabled ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// 计算启用的规则数量
|
||||
var enabledRulesCount = Settings.Automation.FloatingWindowInterceptor.InterceptRules.Where(kvp => kvp.Value).Count();
|
||||
var totalRulesCount = Settings.Automation.FloatingWindowInterceptor.InterceptRules.Count;
|
||||
|
||||
// 更新状态文本
|
||||
if (_floatingWindowInterceptorManager != null)
|
||||
{
|
||||
var stats = _floatingWindowInterceptorManager.GetStatistics();
|
||||
TextBlockFloatingWindowInterceptorStatus.Text = stats.IsRunning
|
||||
? $"拦截器运行中 - 已启用 {enabledRulesCount}/{totalRulesCount} 个规则"
|
||||
: $"拦截器未启动 - 已启用 {enabledRulesCount}/{totalRulesCount} 个规则";
|
||||
}
|
||||
else
|
||||
{
|
||||
TextBlockFloatingWindowInterceptorStatus.Text = $"拦截器未初始化 - 已启用 {enabledRulesCount}/{totalRulesCount} 个规则";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新悬浮窗拦截UI失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口被拦截事件处理
|
||||
/// </summary>
|
||||
private void OnFloatingWindowIntercepted(object sender, FloatingWindowInterceptor.WindowInterceptedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 在UI线程中更新状态
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理窗口拦截事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口被恢复事件处理
|
||||
/// </summary>
|
||||
private void OnFloatingWindowRestored(object sender, FloatingWindowInterceptor.WindowRestoredEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 在UI线程中更新状态
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理窗口恢复事件失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 悬浮窗拦截事件处理
|
||||
|
||||
/// <summary>
|
||||
/// 主开关切换事件
|
||||
/// </summary>
|
||||
private void ToggleSwitchFloatingWindowInterceptorEnabled_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
|
||||
try
|
||||
{
|
||||
Settings.Automation.FloatingWindowInterceptor.IsEnabled = ToggleSwitchFloatingWindowInterceptorEnabled.IsOn;
|
||||
|
||||
if (_floatingWindowInterceptorManager != null)
|
||||
{
|
||||
if (Settings.Automation.FloatingWindowInterceptor.IsEnabled)
|
||||
{
|
||||
_floatingWindowInterceptorManager.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
_floatingWindowInterceptorManager.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
SaveSettingsToFile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"切换悬浮窗拦截主开关失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃白板3拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoWhiteboard3Floating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard3Floating, ToggleSwitchSeewoWhiteboard3Floating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃白板5拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoWhiteboard5Floating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5Floating, ToggleSwitchSeewoWhiteboard5Floating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃白板5C拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoWhiteboard5CFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5CFloating, ToggleSwitchSeewoWhiteboard5CFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃品课侧栏拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoPincoSideBarFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoSideBarFloating, ToggleSwitchSeewoPincoSideBarFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃品课画笔拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoPincoDrawingFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoDrawingFloating, ToggleSwitchSeewoPincoDrawingFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃PPT小工具拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoPPTFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPPTFloating, ToggleSwitchSeewoPPTFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AiClass拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchAiClassFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.AiClassFloating, ToggleSwitchAiClassFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鸿合屏幕书写拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchHiteAnnotationFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.HiteAnnotationFloating, ToggleSwitchHiteAnnotationFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 畅言智慧课堂拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchChangYanFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanFloating, ToggleSwitchChangYanFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 畅言PPT拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchChangYanPptFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanPptFloating, ToggleSwitchChangYanPptFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 天喻教育云拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchIntelligentClassFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.IntelligentClassFloating, ToggleSwitchIntelligentClassFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃桌面画笔拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoDesktopAnnotationFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopAnnotationFloating, ToggleSwitchSeewoDesktopAnnotationFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃桌面侧栏拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoDesktopSideBarFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopSideBarFloating, ToggleSwitchSeewoDesktopSideBarFloating.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置拦截规则
|
||||
/// </summary>
|
||||
private void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_floatingWindowInterceptorManager != null)
|
||||
{
|
||||
_floatingWindowInterceptorManager.SetInterceptRule(type, enabled);
|
||||
}
|
||||
|
||||
// 更新设置
|
||||
var ruleName = type.ToString();
|
||||
if (Settings.Automation.FloatingWindowInterceptor.InterceptRules.ContainsKey(ruleName))
|
||||
{
|
||||
Settings.Automation.FloatingWindowInterceptor.InterceptRules[ruleName] = enabled;
|
||||
}
|
||||
|
||||
// 获取规则信息以处理父子关系
|
||||
var rule = _floatingWindowInterceptorManager?.GetInterceptRule(type);
|
||||
if (rule != null)
|
||||
{
|
||||
// 如果是父规则,更新所有子规则的设置
|
||||
if (rule.ChildTypes.Count > 0)
|
||||
{
|
||||
foreach (var childType in rule.ChildTypes)
|
||||
{
|
||||
var childRuleName = childType.ToString();
|
||||
if (Settings.Automation.FloatingWindowInterceptor.InterceptRules.ContainsKey(childRuleName))
|
||||
{
|
||||
Settings.Automation.FloatingWindowInterceptor.InterceptRules[childRuleName] = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果是子规则,更新父规则的设置
|
||||
else if (rule.ParentType.HasValue)
|
||||
{
|
||||
var parentRule = _floatingWindowInterceptorManager?.GetInterceptRule(rule.ParentType.Value);
|
||||
if (parentRule != null)
|
||||
{
|
||||
var parentRuleName = rule.ParentType.Value.ToString();
|
||||
if (Settings.Automation.FloatingWindowInterceptor.InterceptRules.ContainsKey(parentRuleName))
|
||||
{
|
||||
Settings.Automation.FloatingWindowInterceptor.InterceptRules[parentRuleName] = parentRule.IsEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新UI显示
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
|
||||
SaveSettingsToFile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"设置拦截规则失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -7,41 +7,31 @@ namespace Ink_Canvas
|
||||
{
|
||||
private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (StackPanelPPTControls.Visibility != Visibility.Visible || currentMode != 0) return;
|
||||
|
||||
// 直接发送翻页请求到PPT放映软件,不通过软件处理
|
||||
if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return;
|
||||
if (e.Delta >= 120)
|
||||
{
|
||||
// 上一页 - 发送PageUp键到PPT放映窗口
|
||||
SendKeyToPPTSlideShow(true);
|
||||
BtnPPTSlidesUp_Click(null, null);
|
||||
}
|
||||
else if (e.Delta <= -120)
|
||||
{
|
||||
// 下一页 - 发送PageDown键到PPT放映窗口
|
||||
SendKeyToPPTSlideShow(false);
|
||||
BtnPPTSlidesDown_Click(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void Main_Grid_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (StackPanelPPTControls.Visibility != Visibility.Visible || currentMode != 0) return;
|
||||
if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return;
|
||||
|
||||
// 直接发送翻页请求到PPT放映软件,不通过软件处理
|
||||
if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N ||
|
||||
e.Key == Key.Space)
|
||||
if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N || e.Key == Key.Space)
|
||||
{
|
||||
e.Handled = true; // 阻止事件继续传播
|
||||
SendKeyToPPTSlideShow(false); // 下一页
|
||||
BtnPPTSlidesDown_Click(null, null);
|
||||
}
|
||||
else if (e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Left || e.Key == Key.P)
|
||||
if (e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Left || e.Key == Key.P)
|
||||
{
|
||||
e.Handled = true; // 阻止事件继续传播
|
||||
SendKeyToPPTSlideShow(true); // 上一页
|
||||
BtnPPTSlidesUp_Click(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// 保留PPT翻页快捷键处理
|
||||
// 以下方法保留供全局快捷键调用
|
||||
|
||||
private void HotKey_Undo(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
public static string LinedCursorIcon =
|
||||
"F1 M24,24z M0,0z M5.72106,15.9716L3.71327,3.00395C3.6389,2.6693 3.65747,2.41831 3.76902,2.25099 3.88057,2.08366 4.0479,2 4.271,2 4.4941,2 4.71711,2.07437 4.94021,2.2231 6.72502,3.39438 9.28149,5.10481 12.6094,7.3544 15.677,9.45526 18.1125,11.1285 19.9159,12.3742 20.1204,12.5229 20.2505,12.6995 20.3062,12.904 20.362,13.1085 20.3249,13.2944 20.1947,13.4618 20.0832,13.6105 19.8973,13.6849 19.637,13.6849L13.3902,13.6849 17.6291,19.7365C17.722,19.8666 17.75,20.0153 17.7128,20.1827 17.6942,20.3314 17.6198,20.4522 17.4897,20.5452L15.5654,21.8838C15.4353,21.9768 15.2865,22.0139 15.1192,21.9953 14.9704,21.9582 14.8496,21.8745 14.7566,21.7444L10.2389,15.2745 7.58956,19.9038C7.45942,20.1269 7.30144,20.2756 7.11552,20.35 6.92961,20.4058 6.75292,20.3872 6.5856,20.2942 6.43686,20.2013 6.34392,20.0339 6.30673,19.7922L6.00007,17.8959C5.88852,17.0779,5.79543,16.4364,5.72106,15.9716z";
|
||||
|
||||
public static string SolidCursorIcon =
|
||||
"F1 M24,24z M0,0z M5.72106,15.9716L3.71327,3.00395C3.6389,2.6693 3.65747,2.41831 3.76902,2.25099 3.88057,2.08366 4.0479,2 4.271,2 4.4941,2 4.71711,2.07437 4.94021,2.2231 6.72502,3.39438 9.28149,5.10481 12.6094,7.3544 15.677,9.45526 18.1125,11.1285 19.9159,12.3742 20.1204,12.5229 20.2505,12.6995 20.3062,12.904 20.362,13.1085 20.3249,13.2944 20.1947,13.4618 20.0832,13.6105 19.8973,13.6849 19.637,13.6849L13.3902,13.6849 17.6291,19.7365C17.722,19.8666 17.75,20.0153 17.7128,20.1827 17.6942,20.3314 17.6198,20.4522 17.4897,20.5452L15.5654,21.8838C15.4353,21.9768 15.2865,22.0139 15.1192,21.9953 14.9704,21.9582 14.8496,21.8745 14.7566,21.7444L10.2389,15.2745 7.58956,19.9038C7.45942,20.1269 7.30144,20.2756 7.11552,20.35 6.92961,20.4058 6.75292,20.3872 6.5856,20.2942 6.43686,20.2013 6.34392,20.0339 6.30673,19.7922L6.00007,17.8959C5.88852,17.0779,5.79543,16.4364,5.72106,15.9716z";
|
||||
|
||||
public static string LinedPenIcon =
|
||||
"F1 M24,24z M0,0z M16.996,2.34419L21.6823,7.00397C21.8941,7.23343 22,7.49819 22,7.79825 22,8.09831 21.8941,8.35425 21.6823,8.56606L10.8271,19.4212 4.57877,13.1994 15.4339,2.34419C15.6457,2.11473 15.9017,2 16.2018,2 16.5195,2 16.7842,2.11473 16.996,2.34419z M9.63571,20.5862L9.50333,20.6391 2.6725,21.9894C2.47834,22.0247 2.31066,21.9718 2.16946,21.8306 2.02825,21.707 1.97529,21.5481 2.01059,21.354L3.38736,14.5232C3.38736,14.4879,3.40502,14.4349,3.44032,14.3643L9.63571,20.5862z";
|
||||
|
||||
@@ -37,5 +40,36 @@
|
||||
|
||||
public static string EnabledGestureIconBadgeCheck =
|
||||
"M22.74,18.2234C22.74,20.8888 20.5793,23.0494 17.914,23.0494 15.2487,23.0494 13.088,20.8888 13.088,18.2234 13.088,15.5581 15.2487,13.3975 17.914,13.3975 20.5793,13.3975 22.74,15.5581 22.74,18.2234z M21.1673,15.8009C21.4651,16.0889,21.473,16.5637,21.1851,16.8614L17.5425,20.6282C17.4012,20.7743 17.2066,20.8568 17.0034,20.8568 16.8001,20.8568 16.6055,20.7743 16.4642,20.6282L14.6429,18.7448C14.355,18.447 14.3629,17.9722 14.6607,17.6843 14.9585,17.3963 15.4333,17.4043 15.7212,17.7021L17.0034,19.0279 20.1068,15.8187C20.3947,15.5209,20.8695,15.513,21.1673,15.8009z";
|
||||
|
||||
// 老版浮动栏按钮图标
|
||||
public static string LegacyLinedCursorIcon =
|
||||
"F0 M24,24z M0,0z M3.85151,2.7073C3.52422,2.57095 3.147,2.64558 2.89629,2.89629 2.64558,3.147 2.57095,3.52422 2.7073,3.85151L9.7773,20.8215C9.91729,21.1575 10.2507,21.3718 10.6145,21.3595 10.9783,21.3473 11.2965,21.1111 11.4135,20.7664L13.4711,14.7085 18.8963,20.1337C19.238,20.4754 19.792,20.4754 20.1337,20.1337 20.4754,19.792 20.4754,19.238 20.1337,18.8963L14.7085,13.4711 20.7664,11.4135C21.1111,11.2965 21.3473,10.9783 21.3595,10.6145 21.3718,10.2507 21.1575,9.91729 20.8215,9.7773L3.85151,2.7073z M10.5017,18.0097L5.13984,5.13984 18.0097,10.5017 12.8136,12.2665C12.5561,12.3539,12.3539,12.5561,12.2665,12.8136L10.5017,18.0097z";
|
||||
|
||||
public static string LegacySolidCursorIcon =
|
||||
"F0 M24,24z M0,0z M2.89629,2.89629C3.147,2.64558,3.52422,2.57095,3.85151,2.7073L20.8215,9.7773C21.1575,9.91729 21.3718,10.2507 21.3595,10.6145 21.3473,10.9783 21.1111,11.2965 20.7664,11.4135L14.7085,13.4711 20.1337,18.8963C20.4754,19.238 20.4754,19.792 20.1337,20.1337 19.792,20.4754 19.238,20.4754 18.8963,20.1337L13.4711,14.7085 11.4135,20.7664C11.2965,21.1111 10.9783,21.3473 10.6145,21.3595 10.2507,21.3718 9.91729,21.1575 9.7773,20.8215L2.7073,3.85151C2.57095,3.52422,2.64558,3.147,2.89629,2.89629z";
|
||||
|
||||
public static string LegacyLinedPenIcon =
|
||||
"F0 M24,24z M0,0z M18.7033,4.39761C18.4948,4.31644 18.2714,4.27922 18.0473,4.28846 17.8233,4.29771 17.6038,4.3532 17.403,4.4512 17.2022,4.54919 17.0246,4.68744 16.8813,4.8568 16.8665,4.87422 16.8511,4.89102 16.8349,4.90716L15.7108,6.03131 17.9591,8.27962 19.0832,7.15546C19.1021,7.13662 19.1218,7.11869 19.1424,7.10176 19.3143,6.96037 19.4543,6.7853 19.5537,6.58793 19.6531,6.39058 19.7099,6.1751 19.7207,5.95519 19.7314,5.73528 19.6959,5.51545 19.6163,5.30962 19.5367,5.10378 19.4147,4.91625 19.2576,4.75914 19.1004,4.60201 18.9117,4.47877 18.7033,4.39761z M16.7944,9.44428L14.5461,7.19597 5.47079,16.2713 4.62767,19.3627 7.7191,18.5196 16.7944,9.44428z M13.9636,5.44913L4.15148,15.2613C4.05014,15.3626,3.977,15.4886,3.93929,15.6269L2.65942,20.3198C2.58166,20.6049 2.66264,20.9098 2.87161,21.1188 3.08059,21.3277 3.38551,21.4087 3.67063,21.331L8.36347,20.0511C8.50174,20.0134,8.62777,19.9402,8.72911,19.8389L20.2217,8.34636C20.5551,8.06468 20.8283,7.71873 21.0247,7.3289 21.2275,6.92628 21.3437,6.48586 21.3658,6.03572 21.3878,5.58559 21.3151,5.13594 21.1525,4.71552 20.99,4.29512 20.7411,3.91338 20.4222,3.59447 20.1033,3.27558 19.7214,3.0265 19.3009,2.86277 18.8804,2.69905 18.4304,2.62417 17.9794,2.64278 17.5285,2.66139 17.0862,2.77308 16.6807,2.97095 16.2862,3.16344 15.9348,3.43348 15.6478,3.76494L13.9636,5.44913z";
|
||||
|
||||
public static string LegacySolidPenIcon =
|
||||
"F1 M24,24z M0,0z M19.3332,2.85933C18.9193,2.69814 18.4762,2.62442 18.0322,2.64274 17.5882,2.66106 17.1527,2.77103 16.7535,2.96583 16.3643,3.15575 16.0177,3.42232 15.7349,3.74956L14.5672,4.91725 19.0731,9.4231 20.2373,8.25888C20.5666,7.98121 20.8364,7.63993 21.0302,7.25528 21.2298,6.85899 21.3442,6.42551 21.3659,5.98249 21.3876,5.53947 21.3161,5.09692 21.1561,4.68313 20.996,4.26934 20.7511,3.89359 20.4372,3.57966 20.1232,3.26574 19.7472,3.02052 19.3332,2.85933z M18.0085,10.4877L13.5026,5.98183 4.14128,15.3432C4.04864,15.4358,3.98179,15.551,3.94732,15.6774L2.65684,20.4091C2.58577,20.6698 2.65979,20.9485 2.8508,21.1395 3.04182,21.3305 3.32054,21.4045 3.58117,21.3335L8.3129,20.043C8.43929,20.0085,8.5545,19.9417,8.64713,19.849L18.0085,10.4877z";
|
||||
|
||||
public static string LegacyLinedEraserStrokeIcon =
|
||||
"F0 M25,24z M0,0z M7.32029,21.36L13.0098,21.36 13.0122,21.36 21.5471,21.36C21.989,21.36 22.3473,21.0017 22.3473,20.5598 22.3473,20.1179 21.989,19.7596 21.5471,19.7596L14.9429,19.7596 21.4352,13.2673C22.7372,12.0786,22.6872,10.1353,21.449,8.89707L16.1515,3.59952C14.9628,2.29751,13.0195,2.34754,11.7813,3.58572L2.68992,12.6771C1.3879,13.8657,1.43793,15.8091,2.67611,17.0473L6.75447,21.1256C6.90453,21.2757,7.10807,21.36,7.32029,21.36z M14.9771,4.68685C14.4571,4.10907,13.5664,4.06392,12.9129,4.71737L6.55503,11.0753 13.9595,18.4797 20.3174,12.1218C20.3273,12.1119 20.3375,12.1022 20.3479,12.0929 20.9257,11.5729 20.9708,10.6822 20.3174,10.0287L15.006,4.71737C14.9961,4.70745,14.9864,4.69727,14.9771,4.68685z M12.8278,19.6114L5.42338,12.2069 3.80776,13.8225C3.79784,13.8324 3.78766,13.8421 3.77724,13.8515 3.19947,14.3715 3.15431,15.2622 3.80776,15.9156L7.65174,19.7596 12.6796,19.7596 12.8278,19.6114z";
|
||||
|
||||
public static string LegacySolidEraserStrokeIcon =
|
||||
"F1 M24,24z M0,0z M11.6199,3.61372C12.8916,2.34202,14.8995,2.2837,16.1307,3.62964L21.3433,8.84225C22.615,10.1139,22.6733,12.1218,21.3274,13.353L15.1877,19.4927 5.46434,9.76928 11.6199,3.61372z M7.33167,21.36C7.08919,21.36 6.86831,21.2676 6.70232,21.116 6.69184,21.1064 6.68155,21.0966 6.67147,21.0865L2.65671,17.0718C1.385,15.8001,1.32668,13.7922,2.67262,12.561L4.14394,11.0897 12.5469,19.4927 21.3367,19.4927C21.8523,19.4927 22.2703,19.9107 22.2703,20.4263 22.2703,20.942 21.8523,21.36 21.3367,21.36L7.33167,21.36z";
|
||||
|
||||
public static string LegacyLinedEraserCircleIcon =
|
||||
"F0 M25,24z M0,0z M2.47995,17.1206L6.56736,21.208C6.57733,21.218 6.58749,21.2277 6.59783,21.237 6.66429,21.2971 6.7405,21.3466 6.82381,21.3829L6.83712,21.3885C6.84698,21.3926 6.85693,21.3965 6.86698,21.4003 6.86818,21.4007 6.86937,21.4011 6.87057,21.4016 6.94576,21.4289 7.02412,21.4451 7.10303,21.45L7.12183,21.451 7.13076,21.4513 7.13345,21.4514 7.15549,21.4517 17.0847,21.4517C17.5973,22.3438 18.5597,22.9445 19.6624,22.9445 21.3031,22.9445 22.6332,21.6144 22.6332,19.9737 22.6332,18.3329 21.3031,17.0028 19.6624,17.0028 18.0839,17.0028 16.793,18.2338 16.6972,19.7882L14.8669,19.7882 21.3224,13.3327C22.6404,12.1289,22.5884,10.1619,21.3367,8.91021L16.0278,3.60138C14.8241,2.28336,12.8571,2.33535,11.6053,3.58706L2.49426,12.6981C1.17625,13.9019,1.22824,15.8689,2.47995,17.1206z M14.8072,4.7316C14.2984,4.16633,13.4255,4.11939,12.7816,4.76332L6.43063,11.1143 13.8094,18.4931 20.1604,12.1421C20.1707,12.1318 20.1813,12.1218 20.1921,12.112 20.7574,11.6033 20.8043,10.7304 20.1604,10.0865L14.8373,4.76332C14.8269,4.75301,14.8169,4.74243,14.8072,4.7316z M3.65621,13.8887C3.6459,13.899 3.63532,13.9091 3.62448,13.9188 3.05922,14.4276 3.01228,15.3004 3.65621,15.9444L7.50001,19.7882 12.752,19.7882 5.25437,12.2906 3.65621,13.8887z";
|
||||
|
||||
public static string LegacySolidEraserCircleIcon =
|
||||
"F1 M24,24z M0,0z M15.0919,19.6686L21.4282,13.3322C22.7462,12.1285,22.6942,10.1616,21.4426,8.90993L16.134,3.60133C14.9303,2.28338,12.9633,2.33537,11.7117,3.58702L5.36097,9.93771 15.0919,19.6686z M6.67201,21.2053C6.82267,21.3569,7.03137,21.4508,7.26201,21.4508L17.1907,21.4508C17.7033,22.3429 18.6657,22.9437 19.7683,22.9437 21.409,22.9437 22.7391,21.6136 22.7391,19.9729 22.7391,18.3322 21.409,17.0022 19.7683,17.0022 18.19,17.0022 16.8991,18.2331 16.8033,19.7874L12.8583,19.7874 4.18476,11.1139 2.60098,12.6977C1.28303,13.9014,1.33502,15.8683,2.58667,17.12L6.67201,21.2053z";
|
||||
|
||||
public static string LegacyLinedLassoSelectIcon =
|
||||
"F0 M24,24z M0,0z M14.4715,12.7092L14.4715,18.7882 15.8291,16.7546C15.9688,16.5453,16.2038,16.4196,16.4554,16.4196L19.0106,16.4196 14.4715,12.7092z M14.6618,10.9193C14.4981,10.784 14.2733,10.6788 14.0025,10.6788 13.9951,10.6788 13.9877,10.6789 13.9803,10.6791 13.7083,10.6872 13.4502,10.8008 13.2607,10.9961 13.0712,11.1913 12.9653,11.4526 12.9653,11.7246L12.9653,20.3314C12.9653,20.3403 12.9655,20.3491 12.9658,20.358 12.9734,20.5733 13.0468,20.7811 13.176,20.9534 13.3053,21.1258 13.4842,21.2544 13.6888,21.3219 13.765,21.3471 13.8447,21.36 13.925,21.36L14.0025,21.36C14.1661,21.36 14.3276,21.3218 14.474,21.2486 14.6204,21.1754 14.7477,21.0692 14.8459,20.9382 14.8542,20.9272 14.8622,20.9159 14.8698,20.9045L16.8582,17.9258 20.3145,17.9258C20.5287,17.9281 20.7384,17.8641 20.9149,17.7424 21.0941,17.6187 21.2299,17.4417 21.3029,17.2365 21.3759,17.0313 21.3825,16.8084 21.3217,16.5993 21.262,16.3936 21.14,16.2117 20.9729,16.0782L14.6618,10.9193z M8.14548,20.0044C7.70454,19.6737 7.34665,19.2448 7.10016,18.7519 6.94658,18.4447 6.83887,18.1179 6.7795,17.7818 7.131,17.6605 7.45404,17.4604 7.72196,17.1924 7.74959,17.1648 7.7765,17.1366 7.80267,17.1078 8.5567,17.4118 9.3392,17.6365 10.1444,17.7694 10.5548,17.8372 10.9424,17.5594 11.0101,17.149 11.0779,16.7387 10.8001,16.3511 10.3897,16.2833 9.72172,16.1731 9.06686,15.9883 8.42926,15.7362 8.44084,15.6393 8.44672,15.5413 8.44672,15.4427 8.44672,14.7865 8.18602,14.1571 7.72196,13.693 7.25791,13.229 6.62852,12.9682 5.97224,12.9682 5.65536,12.9682 5.34474,13.029 5.05598,13.1441 4.47073,12.3026 4.15196,11.303 4.14328,10.2756 4.14532,7.03688 7.49758,4.1462 11.9971,4.1462 16.4941,4.1462 19.8451,7.03371 19.8508,10.2703 19.8388,10.7807 19.7549,11.2869 19.6016,11.7739 19.4767,12.1706 19.697,12.5934 20.0938,12.7183 20.4905,12.8432 20.9134,12.6228 21.0383,12.2261 21.2351,11.6008 21.3424,10.9507 21.3568,10.2952L21.357,10.2786C21.357,5.91009 16.9982,2.64 11.9971,2.64 6.9959,2.64 2.63705,5.91009 2.63705,10.2786L2.6371,10.2845C2.6479,11.6579 3.08647,12.993 3.89074,14.1047 3.63615,14.5007 3.49777,14.9646 3.49777,15.4427 3.49777,16.099 3.75847,16.7284 4.22252,17.1924 4.51465,17.4846 4.87229,17.6961 5.26092,17.8128 5.33333,18.3726 5.49917,18.9179 5.75297,19.4255 6.10404,20.1276 6.61375,20.7383 7.24176,21.2093 7.5745,21.4589 8.04654,21.3915 8.2961,21.0587 8.54566,20.726 8.47822,20.2539 8.14548,20.0044z M5.97224,14.4745C5.71544,14.4745 5.46916,14.5765 5.28757,14.7581 5.10598,14.9396 5.00397,15.1859 5.00397,15.4427 5.00397,15.6995 5.10598,15.9458 5.28757,16.1274 5.46916,16.309 5.71544,16.411 5.97224,16.411 6.22904,16.411 6.47533,16.309 6.65692,16.1274 6.8385,15.9458 6.94052,15.6995 6.94052,15.4427 6.94052,15.1859 6.8385,14.9396 6.65692,14.7581 6.47533,14.5765 6.22904,14.4745 5.97224,14.4745z";
|
||||
|
||||
public static string LegacySolidLassoSelectIcon =
|
||||
"F1 M24,24z M0,0z M14.6618,10.9193C14.4981,10.784 14.2733,10.6788 14.0025,10.6788 13.9951,10.6788 13.9877,10.6789 13.9803,10.6791 13.7083,10.6872 13.4502,10.8008 13.2607,10.9961 13.0712,11.1913 12.9653,11.4526 12.9653,11.7246L12.9653,20.3314C12.9653,20.3403 12.9655,20.3491 12.9658,20.358 12.9734,20.5733 13.0468,20.7811 13.176,20.9534 13.3053,21.1258 13.4842,21.2544 13.6888,21.3219 13.765,21.3471 13.8447,21.36 13.925,21.36L14.0025,21.36C14.1661,21.36 14.3276,21.3218 14.474,21.2486 14.6204,21.1754 14.7477,21.0692 14.8459,20.9382 14.8542,20.9272 14.8622,20.9159 14.8698,20.9045L16.8582,17.9258 20.3145,17.9258C20.5287,17.9281 20.7384,17.8641 20.9149,17.7424 21.0941,17.6187 21.2299,17.4417 21.3029,17.2365 21.3759,17.0313 21.3825,16.8084 21.3217,16.5993 21.262,16.3936 21.14,16.2117 20.9729,16.0782L14.6618,10.9193z M8.14548,20.0044C7.70454,19.6737 7.34665,19.2448 7.10016,18.7519 6.94658,18.4447 6.83888,18.1179 6.7795,17.7818 7.131,17.6605 7.45404,17.4604 7.72196,17.1924 7.74959,17.1648 7.77649,17.1366 7.80267,17.1078 8.5567,17.4118 9.3392,17.6365 10.1444,17.7694 10.5548,17.8372 10.9424,17.5594 11.0101,17.149 11.0779,16.7387 10.8001,16.3511 10.3897,16.2833 9.72172,16.1731 9.06686,15.9883 8.42926,15.7362 8.44084,15.6393 8.44672,15.5413 8.44672,15.4427 8.44672,14.7865 8.18602,14.1571 7.72196,13.693 7.25791,13.229 6.62852,12.9682 5.97224,12.9682 5.65536,12.9682 5.34474,13.029 5.05598,13.1441 4.47073,12.3026 4.15196,11.303 4.14328,10.2756 4.14532,7.03688 7.49758,4.1462 11.9971,4.1462 16.4941,4.1462 19.8451,7.03371 19.8508,10.2703 19.8388,10.7807 19.7549,11.2869 19.6016,11.7739 19.4767,12.1706 19.697,12.5934 20.0938,12.7183 20.4905,12.8432 20.9134,12.6228 21.0383,12.2261 21.2351,11.6008 21.3424,10.9507 21.3568,10.2952L21.357,10.2786C21.357,5.91009 16.9982,2.64 11.9971,2.64 6.9959,2.64 2.63705,5.91009 2.63705,10.2786L2.6371,10.2845C2.6479,11.6579 3.08647,12.993 3.89074,14.1047 3.63615,14.5007 3.49777,14.9646 3.49777,15.4427 3.49777,16.099 3.75847,16.7284 4.22252,17.1924 4.51465,17.4846 4.87229,17.6961 5.26092,17.8128 5.33333,18.3726 5.49917,18.9178 5.75297,19.4255 6.10404,20.1276 6.61375,20.7383 7.24176,21.2093 7.5745,21.4589 8.04654,21.3915 8.2961,21.0587 8.54566,20.726 8.47822,20.2539 8.14548,20.0044z";
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
@@ -11,7 +12,6 @@ using System.Windows.Forms;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Application = System.Windows.Application;
|
||||
using Color = System.Drawing.Color;
|
||||
using Cursors = System.Windows.Input.Cursors;
|
||||
@@ -27,11 +27,15 @@ namespace Ink_Canvas
|
||||
{
|
||||
public Rectangle Area;
|
||||
public List<Point> Path;
|
||||
public Bitmap CameraImage;
|
||||
public BitmapSource CameraBitmapSource;
|
||||
|
||||
public ScreenshotResult(Rectangle area, List<Point> path = null)
|
||||
public ScreenshotResult(Rectangle area, List<Point> path = null, Bitmap cameraImage = null, BitmapSource cameraBitmapSource = null)
|
||||
{
|
||||
Area = area;
|
||||
Path = path;
|
||||
CameraImage = cameraImage;
|
||||
CameraBitmapSource = cameraBitmapSource;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,34 +59,48 @@ namespace Ink_Canvas
|
||||
// 恢复窗口显示
|
||||
Visibility = originalVisibility;
|
||||
|
||||
if (screenshotResult.HasValue && screenshotResult.Value.Area.Width > 0 && screenshotResult.Value.Area.Height > 0)
|
||||
if (screenshotResult.HasValue)
|
||||
{
|
||||
// 截取选定区域
|
||||
using (var originalBitmap = CaptureScreenArea(screenshotResult.Value.Area))
|
||||
// 检查是否是摄像头截图
|
||||
if (screenshotResult.Value.CameraBitmapSource != null)
|
||||
{
|
||||
if (originalBitmap != null)
|
||||
// 摄像头截图(使用BitmapSource)
|
||||
await InsertBitmapSourceToCanvas(screenshotResult.Value.CameraBitmapSource);
|
||||
}
|
||||
else if (screenshotResult.Value.CameraImage != null)
|
||||
{
|
||||
// 摄像头截图(使用Bitmap)
|
||||
await InsertScreenshotToCanvas(screenshotResult.Value.CameraImage);
|
||||
}
|
||||
else if (screenshotResult.Value.Area.Width > 0 && screenshotResult.Value.Area.Height > 0)
|
||||
{
|
||||
// 屏幕截图
|
||||
using (var originalBitmap = CaptureScreenArea(screenshotResult.Value.Area))
|
||||
{
|
||||
Bitmap finalBitmap = originalBitmap;
|
||||
bool needDisposeFinalBitmap = false;
|
||||
|
||||
try
|
||||
if (originalBitmap != null)
|
||||
{
|
||||
// 如果有路径信息,应用形状遮罩
|
||||
if (screenshotResult.Value.Path != null && screenshotResult.Value.Path.Count > 0)
|
||||
Bitmap finalBitmap = originalBitmap;
|
||||
bool needDisposeFinalBitmap = false;
|
||||
|
||||
try
|
||||
{
|
||||
finalBitmap = ApplyShapeMask(originalBitmap, screenshotResult.Value.Path, screenshotResult.Value.Area);
|
||||
needDisposeFinalBitmap = true; // 标记需要释放新创建的位图
|
||||
// 如果有路径信息,应用形状遮罩
|
||||
if (screenshotResult.Value.Path != null && screenshotResult.Value.Path.Count > 0)
|
||||
{
|
||||
finalBitmap = ApplyShapeMask(originalBitmap, screenshotResult.Value.Path, screenshotResult.Value.Area);
|
||||
needDisposeFinalBitmap = true; // 标记需要释放新创建的位图
|
||||
}
|
||||
|
||||
// 将截图转换为WPF Image并插入到画布
|
||||
await InsertScreenshotToCanvas(finalBitmap);
|
||||
}
|
||||
|
||||
// 将截图转换为WPF Image并插入到画布
|
||||
await InsertScreenshotToCanvas(finalBitmap);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 如果创建了新的位图,需要释放它
|
||||
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
|
||||
finally
|
||||
{
|
||||
finalBitmap.Dispose();
|
||||
// 如果创建了新的位图,需要释放它
|
||||
if (needDisposeFinalBitmap && finalBitmap != originalBitmap)
|
||||
{
|
||||
finalBitmap.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,6 +118,46 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 直接全屏截图并插入到画布
|
||||
private async Task CaptureFullScreenAndInsert()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 隐藏主窗口以避免截图包含窗口本身
|
||||
var originalVisibility = Visibility;
|
||||
Visibility = Visibility.Hidden;
|
||||
|
||||
// 等待窗口隐藏
|
||||
await Task.Delay(200);
|
||||
|
||||
// 获取虚拟屏幕边界
|
||||
var virtualScreen = SystemInformation.VirtualScreen;
|
||||
var fullScreenArea = new Rectangle(virtualScreen.X, virtualScreen.Y, virtualScreen.Width, virtualScreen.Height);
|
||||
|
||||
// 截取全屏
|
||||
using (var fullScreenBitmap = CaptureScreenArea(fullScreenArea))
|
||||
{
|
||||
if (fullScreenBitmap != null)
|
||||
{
|
||||
// 将截图转换为WPF Image并插入到画布
|
||||
await InsertScreenshotToCanvas(fullScreenBitmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowNotification("全屏截图失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复窗口显示
|
||||
Visibility = originalVisibility;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowNotification($"全屏截图失败: {ex.Message}");
|
||||
Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示截图区域选择器
|
||||
private async Task<ScreenshotResult?> ShowScreenshotSelector()
|
||||
{
|
||||
@@ -112,10 +170,31 @@ namespace Ink_Canvas
|
||||
var selectorWindow = new ScreenshotSelectorWindow();
|
||||
if (selectorWindow.ShowDialog() == true)
|
||||
{
|
||||
result = new ScreenshotResult(
|
||||
selectorWindow.SelectedArea.Value,
|
||||
selectorWindow.SelectedPath
|
||||
);
|
||||
// 检查是否是摄像头截图
|
||||
if (selectorWindow.CameraBitmapSource != null)
|
||||
{
|
||||
result = new ScreenshotResult(
|
||||
Rectangle.Empty, // 摄像头截图不需要区域
|
||||
null, // 摄像头截图不需要路径
|
||||
null, // 不再使用Bitmap
|
||||
selectorWindow.CameraBitmapSource // 摄像头BitmapSource
|
||||
);
|
||||
}
|
||||
else if (selectorWindow.CameraImage != null)
|
||||
{
|
||||
result = new ScreenshotResult(
|
||||
Rectangle.Empty, // 摄像头截图不需要区域
|
||||
null, // 摄像头截图不需要路径
|
||||
selectorWindow.CameraImage // 摄像头图像
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = new ScreenshotResult(
|
||||
selectorWindow.SelectedArea.Value,
|
||||
selectorWindow.SelectedPath
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -158,7 +237,6 @@ namespace Ink_Canvas
|
||||
graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"成功截取区域: X={x}, Y={y}, Width={width}, Height={height}");
|
||||
return bitmap;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -173,9 +251,22 @@ namespace Ink_Canvas
|
||||
{
|
||||
try
|
||||
{
|
||||
// 验证位图有效性
|
||||
if (bitmap == null || bitmap.Width <= 0 || bitmap.Height <= 0)
|
||||
{
|
||||
ShowNotification("无效的截图");
|
||||
return;
|
||||
}
|
||||
|
||||
// 将Bitmap转换为WPF BitmapSource
|
||||
var bitmapSource = ConvertBitmapToBitmapSource(bitmap);
|
||||
|
||||
if (bitmapSource == null)
|
||||
{
|
||||
ShowNotification("转换截图失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建WPF Image控件
|
||||
var image = new Image
|
||||
{
|
||||
@@ -190,11 +281,11 @@ namespace Ink_Canvas
|
||||
|
||||
// 初始化TransformGroup
|
||||
InitializeScreenshotTransform(image);
|
||||
|
||||
|
||||
// 设置截图属性,避免被InkCanvas选择系统处理
|
||||
image.IsHitTestVisible = true;
|
||||
image.Focusable = false;
|
||||
|
||||
|
||||
// 初始化InkCanvas选择设置
|
||||
InitializeInkCanvasSelectionSettings();
|
||||
|
||||
@@ -216,6 +307,11 @@ namespace Ink_Canvas
|
||||
// 提交历史记录
|
||||
timeMachine.CommitElementInsertHistory(image);
|
||||
|
||||
// 插入图片后切换到选择模式并刷新浮动栏高光显示
|
||||
SetCurrentToolMode(InkCanvasEditingMode.Select);
|
||||
UpdateCurrentToolMode("select");
|
||||
HideSubPanels("select");
|
||||
|
||||
ShowNotification("截图已插入到画布");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -223,6 +319,69 @@ namespace Ink_Canvas
|
||||
ShowNotification($"插入截图失败: {ex.Message}");
|
||||
LogHelper.WriteLogToFile($"插入截图失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
bitmap?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// 将BitmapSource插入到画布(用于摄像头截图)
|
||||
private async Task InsertBitmapSourceToCanvas(BitmapSource bitmapSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建WPF Image控件
|
||||
var image = new Image
|
||||
{
|
||||
Source = bitmapSource,
|
||||
Stretch = Stretch.Uniform
|
||||
};
|
||||
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
|
||||
|
||||
// 生成唯一名称
|
||||
string timestamp = "camera_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
|
||||
image.Name = timestamp;
|
||||
|
||||
// 初始化TransformGroup
|
||||
InitializeScreenshotTransform(image);
|
||||
|
||||
// 设置截图属性,避免被InkCanvas选择系统处理
|
||||
image.IsHitTestVisible = true;
|
||||
image.Focusable = false;
|
||||
|
||||
// 初始化InkCanvas选择设置
|
||||
InitializeInkCanvasSelectionSettings();
|
||||
|
||||
// 等待图片加载完成后再进行居中处理
|
||||
image.Loaded += (sender, e) =>
|
||||
{
|
||||
// 确保在UI线程中执行
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
CenterAndScaleScreenshot(image);
|
||||
// 绑定事件处理器
|
||||
BindScreenshotEvents(image);
|
||||
}), DispatcherPriority.Loaded);
|
||||
};
|
||||
|
||||
// 添加到画布
|
||||
inkCanvas.Children.Add(image);
|
||||
|
||||
// 提交历史记录
|
||||
timeMachine.CommitElementInsertHistory(image);
|
||||
|
||||
// 插入图片后切换到选择模式并刷新浮动栏高光显示
|
||||
SetCurrentToolMode(InkCanvasEditingMode.Select);
|
||||
UpdateCurrentToolMode("select");
|
||||
HideSubPanels("select");
|
||||
|
||||
ShowNotification("摄像头截图已插入到画布");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowNotification($"插入摄像头截图失败: {ex.Message}");
|
||||
LogHelper.WriteLogToFile($"插入摄像头截图失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化截图的TransformGroup
|
||||
@@ -245,13 +404,15 @@ namespace Ink_Canvas
|
||||
image.MouseWheel += Element_MouseWheel;
|
||||
|
||||
// 触摸事件
|
||||
image.TouchDown += Element_TouchDown;
|
||||
image.TouchUp += Element_TouchUp;
|
||||
image.IsManipulationEnabled = true;
|
||||
image.ManipulationDelta += Element_ManipulationDelta;
|
||||
image.ManipulationCompleted += Element_ManipulationCompleted;
|
||||
|
||||
// 设置光标
|
||||
image.Cursor = Cursors.Hand;
|
||||
|
||||
|
||||
// 禁用InkCanvas对截图的选择处理
|
||||
image.IsHitTestVisible = true;
|
||||
image.Focusable = false;
|
||||
@@ -332,7 +493,6 @@ namespace Ink_Canvas
|
||||
InitializeScreenshotTransform(image);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"截图居中完成: 位置({centerX}, {centerY}), 尺寸({newWidth}x{newHeight})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -360,13 +520,13 @@ namespace Ink_Canvas
|
||||
|
||||
// 创建结果位图,确保支持透明度
|
||||
var resultBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb);
|
||||
|
||||
|
||||
// 首先将整个位图设置为透明
|
||||
using (var resultGraphics = Graphics.FromImage(resultBitmap))
|
||||
{
|
||||
// 清除位图,设置为完全透明
|
||||
resultGraphics.Clear(Color.Transparent);
|
||||
|
||||
|
||||
// 设置高质量渲染
|
||||
resultGraphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
resultGraphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||
@@ -406,7 +566,7 @@ namespace Ink_Canvas
|
||||
|
||||
// 在裁剪区域内绘制原始图像
|
||||
resultGraphics.DrawImage(bitmap, 0, 0);
|
||||
|
||||
|
||||
// 重置裁剪区域,确保后续操作不受影响
|
||||
resultGraphics.ResetClip();
|
||||
}
|
||||
@@ -433,24 +593,169 @@ namespace Ink_Canvas
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var memory = new MemoryStream())
|
||||
// 验证位图有效性
|
||||
if (bitmap == null)
|
||||
return null;
|
||||
|
||||
// 验证位图尺寸
|
||||
if (bitmap.Width <= 0 || bitmap.Height <= 0)
|
||||
return null;
|
||||
|
||||
// 使用更安全的方法转换位图
|
||||
var bitmapData = bitmap.LockBits(
|
||||
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
|
||||
ImageLockMode.ReadOnly,
|
||||
bitmap.PixelFormat);
|
||||
|
||||
try
|
||||
{
|
||||
bitmap.Save(memory, ImageFormat.Png);
|
||||
memory.Position = 0;
|
||||
// 根据像素格式选择合适的WPF像素格式
|
||||
System.Windows.Media.PixelFormat wpfPixelFormat;
|
||||
switch (bitmap.PixelFormat)
|
||||
{
|
||||
case PixelFormat.Format24bppRgb:
|
||||
wpfPixelFormat = PixelFormats.Bgr24;
|
||||
break;
|
||||
case PixelFormat.Format32bppArgb:
|
||||
wpfPixelFormat = PixelFormats.Bgra32;
|
||||
break;
|
||||
case PixelFormat.Format32bppRgb:
|
||||
wpfPixelFormat = PixelFormats.Bgr32;
|
||||
break;
|
||||
default:
|
||||
wpfPixelFormat = PixelFormats.Bgr24;
|
||||
break;
|
||||
}
|
||||
|
||||
var bitmapImage = new BitmapImage();
|
||||
bitmapImage.BeginInit();
|
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmapImage.StreamSource = memory;
|
||||
bitmapImage.EndInit();
|
||||
bitmapImage.Freeze();
|
||||
var bitmapSource = BitmapSource.Create(
|
||||
bitmapData.Width,
|
||||
bitmapData.Height,
|
||||
bitmap.HorizontalResolution,
|
||||
bitmap.VerticalResolution,
|
||||
wpfPixelFormat,
|
||||
null,
|
||||
bitmapData.Scan0,
|
||||
bitmapData.Stride * bitmapData.Height,
|
||||
bitmapData.Stride);
|
||||
|
||||
return bitmapImage;
|
||||
bitmapSource.Freeze();
|
||||
return bitmapSource;
|
||||
}
|
||||
finally
|
||||
{
|
||||
bitmap.UnlockBits(bitmapData);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"转换位图失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
|
||||
// 尝试使用备用方法:内存流转换
|
||||
try
|
||||
{
|
||||
return ConvertBitmapToBitmapSourceFallback(bitmap);
|
||||
}
|
||||
catch (Exception fallbackEx)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"备用转换方法也失败: {fallbackEx.Message}", LogHelper.LogType.Error);
|
||||
|
||||
// 最后尝试:使用最简单的转换方法
|
||||
try
|
||||
{
|
||||
return ConvertBitmapToBitmapSourceSimple(bitmap);
|
||||
}
|
||||
catch (Exception simpleEx)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"简单转换方法也失败: {simpleEx.Message}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 备用的位图转换方法(使用内存流)
|
||||
private BitmapSource ConvertBitmapToBitmapSourceFallback(Bitmap bitmap)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 验证位图有效性
|
||||
if (bitmap == null || bitmap.Width <= 0 || bitmap.Height <= 0)
|
||||
return null;
|
||||
|
||||
// 创建一个新的位图,确保格式正确
|
||||
using (var convertedBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb))
|
||||
{
|
||||
using (var graphics = Graphics.FromImage(convertedBitmap))
|
||||
{
|
||||
graphics.DrawImage(bitmap, 0, 0);
|
||||
}
|
||||
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
convertedBitmap.Save(memory, ImageFormat.Png);
|
||||
memory.Position = 0;
|
||||
|
||||
var bitmapImage = new BitmapImage();
|
||||
bitmapImage.BeginInit();
|
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmapImage.StreamSource = memory;
|
||||
bitmapImage.EndInit();
|
||||
bitmapImage.Freeze();
|
||||
|
||||
return bitmapImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"备用转换方法失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// 最简单的位图转换方法
|
||||
private BitmapSource ConvertBitmapToBitmapSourceSimple(Bitmap bitmap)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (bitmap == null)
|
||||
return null;
|
||||
|
||||
// 使用最基础的方法:直接保存为PNG然后加载
|
||||
var tempFile = Path.GetTempFileName() + ".png";
|
||||
|
||||
try
|
||||
{
|
||||
bitmap.Save(tempFile, ImageFormat.Png);
|
||||
|
||||
var bitmapImage = new BitmapImage();
|
||||
bitmapImage.BeginInit();
|
||||
bitmapImage.UriSource = new Uri(tempFile);
|
||||
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmapImage.EndInit();
|
||||
bitmapImage.Freeze();
|
||||
|
||||
return bitmapImage;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 清理临时文件
|
||||
try
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
catch (Exception deleteEx)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"删除临时文件失败: {deleteEx.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"简单转换方法失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -466,4 +771,4 @@ namespace Ink_Canvas
|
||||
return 1.0; // 默认DPI
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using Ink_Canvas.Helpers;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using Ink_Canvas.Helpers;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -68,9 +68,20 @@ namespace Ink_Canvas
|
||||
|
||||
public static void ScrollViewToVerticalTop(FrameworkElement element, ScrollViewer scrollViewer)
|
||||
{
|
||||
if (element == null || scrollViewer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scrollViewerOffset = scrollViewer.VerticalOffset;
|
||||
var point = new Point(0, scrollViewerOffset);
|
||||
var tarPos = element.TransformToVisual(scrollViewer).Transform(point);
|
||||
var transform = element.TransformToVisual(scrollViewer);
|
||||
if (transform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tarPos = transform.Transform(point);
|
||||
scrollViewer.ScrollToVerticalOffset(tarPos.Y);
|
||||
}
|
||||
|
||||
@@ -96,7 +107,7 @@ namespace Ink_Canvas
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
|
||||
|
||||
SaveStrokes();
|
||||
ClearStrokes(true);
|
||||
CurrentWhiteboardIndex = index + 1;
|
||||
@@ -129,7 +140,7 @@ namespace Ink_Canvas
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
currentSelectedElement = null;
|
||||
}
|
||||
|
||||
|
||||
SaveStrokes();
|
||||
ClearStrokes(true);
|
||||
CurrentWhiteboardIndex = index + 1;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Forms;
|
||||
@@ -12,8 +16,8 @@ using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Color = System.Drawing.Color;
|
||||
using File = System.IO.File;
|
||||
using Image = System.Windows.Controls.Image;
|
||||
@@ -78,7 +82,7 @@ namespace Ink_Canvas
|
||||
|
||||
for (int i = 1; i <= totalSlides; i++)
|
||||
{
|
||||
var slideStrokes = _pptInkManager?.LoadSlideStrokes(i);
|
||||
var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i);
|
||||
if (slideStrokes != null && slideStrokes.Count > 0)
|
||||
{
|
||||
allPageStrokes.Add(slideStrokes);
|
||||
@@ -126,12 +130,103 @@ namespace Ink_Canvas
|
||||
SaveSinglePageStrokesAsImage(savePathWithName, newNotice);
|
||||
}
|
||||
}
|
||||
else if (Settings.Automation.IsSaveStrokesAsXML)
|
||||
{
|
||||
// XML保存模式 - 检查是否存在多页面墨迹
|
||||
bool hasMultiplePages = false;
|
||||
List<StrokeCollection> allPageStrokes = new List<StrokeCollection>();
|
||||
|
||||
// 检查PPT放映模式下的多页面墨迹
|
||||
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible && _pptManager?.IsConnected == true)
|
||||
{
|
||||
hasMultiplePages = true;
|
||||
var totalSlides = _pptManager.SlidesCount;
|
||||
var currentSlide = _pptManager.GetCurrentSlideNumber();
|
||||
|
||||
for (int i = 1; i <= totalSlides; i++)
|
||||
{
|
||||
var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i);
|
||||
if (slideStrokes != null && slideStrokes.Count > 0)
|
||||
{
|
||||
allPageStrokes.Add(slideStrokes);
|
||||
}
|
||||
else if (i == currentSlide && inkCanvas.Strokes.Count > 0)
|
||||
{
|
||||
allPageStrokes.Add(inkCanvas.Strokes.Clone());
|
||||
}
|
||||
else
|
||||
{
|
||||
allPageStrokes.Add(new StrokeCollection());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查白板模式下的多页面墨迹
|
||||
else if (currentMode != 0 && WhiteboardTotalCount > 1)
|
||||
{
|
||||
hasMultiplePages = true;
|
||||
for (int i = 1; i <= WhiteboardTotalCount; i++)
|
||||
{
|
||||
if (TimeMachineHistories[i] != null)
|
||||
{
|
||||
var strokes = ApplyHistoriesToNewStrokeCollection(TimeMachineHistories[i]);
|
||||
allPageStrokes.Add(strokes);
|
||||
}
|
||||
else
|
||||
{
|
||||
allPageStrokes.Add(new StrokeCollection());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMultiplePages && allPageStrokes.Count > 0)
|
||||
{
|
||||
// 多页面XML保存为压缩包
|
||||
string zipFileName = Path.ChangeExtension(savePathWithName, "zip");
|
||||
SaveMultiPageStrokesAsXMLZip(allPageStrokes, zipFileName, newNotice);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 单页面XML保存
|
||||
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
|
||||
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
|
||||
if (newNotice) ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 常规保存模式 - 仅保存墨迹对象
|
||||
var fs = new FileStream(savePathWithName, FileMode.Create);
|
||||
inkCanvas.Strokes.Save(fs);
|
||||
fs.Close();
|
||||
if (Settings.Automation.IsSaveStrokesAsXML)
|
||||
{
|
||||
// 保存为XML格式
|
||||
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
|
||||
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
|
||||
if (newNotice) ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 保存为二进制格式
|
||||
var fs = new FileStream(savePathWithName, FileMode.Create);
|
||||
inkCanvas.Strokes.Save(fs);
|
||||
fs.Close();
|
||||
if (newNotice) ShowNotification("墨迹成功保存至 " + savePathWithName);
|
||||
}
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
|
||||
if (delayMinutes > 0)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
|
||||
}
|
||||
|
||||
await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePathWithName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
// 保存元素信息
|
||||
var elementInfos = new List<CanvasElementInfo>();
|
||||
foreach (var child in inkCanvas.Children)
|
||||
@@ -150,8 +245,7 @@ namespace Ink_Canvas
|
||||
});
|
||||
}
|
||||
}
|
||||
File.WriteAllText(Path.ChangeExtension(savePathWithName, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Formatting.Indented));
|
||||
if (newNotice) ShowNotification("墨迹成功保存至 " + savePathWithName);
|
||||
File.WriteAllText(Path.ChangeExtension(savePathWithName, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -161,6 +255,201 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将StrokeCollection保存为XML格式
|
||||
/// </summary>
|
||||
private void SaveStrokesAsXML(StrokeCollection strokes, string xmlPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用XDocument创建XML文档
|
||||
XDocument doc = new XDocument(
|
||||
new XDeclaration("1.0", "utf-8", "yes"),
|
||||
new XElement("InkCanvasStrokes",
|
||||
new XAttribute("Version", "1.0"),
|
||||
new XAttribute("StrokeCount", strokes.Count),
|
||||
new XAttribute("SaveTime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")),
|
||||
from stroke in strokes
|
||||
select new XElement("Stroke",
|
||||
new XAttribute("DrawingAttributes", SerializeDrawingAttributes(stroke.DrawingAttributes)),
|
||||
new XElement("StylusPoints",
|
||||
from point in stroke.StylusPoints
|
||||
select new XElement("StylusPoint",
|
||||
new XAttribute("X", point.X),
|
||||
new XAttribute("Y", point.Y),
|
||||
new XAttribute("PressureFactor", point.PressureFactor)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// 保存XML文件
|
||||
using (var writer = new XmlTextWriter(xmlPath, Encoding.UTF8))
|
||||
{
|
||||
writer.Formatting = System.Xml.Formatting.Indented;
|
||||
doc.Save(writer);
|
||||
}
|
||||
|
||||
// 同时保存元素信息
|
||||
var elementInfos = new List<CanvasElementInfo>();
|
||||
foreach (var child in inkCanvas.Children)
|
||||
{
|
||||
if (child is Image img && img.Source is BitmapImage bmp)
|
||||
{
|
||||
elementInfos.Add(new CanvasElementInfo
|
||||
{
|
||||
Type = "Image",
|
||||
SourcePath = bmp.UriSource?.LocalPath ?? "",
|
||||
Left = InkCanvas.GetLeft(img),
|
||||
Top = InkCanvas.GetTop(img),
|
||||
Width = img.Width,
|
||||
Height = img.Height,
|
||||
Stretch = img.Stretch.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
File.WriteAllText(Path.ChangeExtension(xmlPath, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented));
|
||||
|
||||
// 异步上传到Dlass
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
|
||||
if (delayMinutes > 0)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
|
||||
}
|
||||
|
||||
await Helpers.DlassNoteUploader.UploadNoteFileAsync(xmlPath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存XML格式墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序列化DrawingAttributes为字符串
|
||||
/// </summary>
|
||||
private string SerializeDrawingAttributes(DrawingAttributes da)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"Color={da.Color};");
|
||||
sb.Append($"Width={da.Width};");
|
||||
sb.Append($"Height={da.Height};");
|
||||
sb.Append($"FitToCurve={da.FitToCurve};");
|
||||
sb.Append($"IsHighlighter={da.IsHighlighter};");
|
||||
sb.Append($"IgnorePressure={da.IgnorePressure};");
|
||||
sb.Append($"StylusTip={da.StylusTip};");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将多页面墨迹保存为XML格式压缩包
|
||||
/// </summary>
|
||||
private void SaveMultiPageStrokesAsXMLZip(List<StrokeCollection> allPageStrokes, string zipFileName, bool newNotice)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建临时目录来存放文件
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), $"InkCanvas_MultiPage_XML_{DateTime.Now:yyyyMMdd_HHmmss}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
// 保存所有页面的XML文件到临时目录
|
||||
for (int i = 0; i < allPageStrokes.Count; i++)
|
||||
{
|
||||
var strokes = allPageStrokes[i];
|
||||
if (strokes.Count > 0)
|
||||
{
|
||||
// 保存XML文件
|
||||
string xmlFileName = Path.Combine(tempDir, $"page_{i + 1:D4}.xml");
|
||||
SaveStrokesAsXML(strokes, xmlFileName);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存元数据信息
|
||||
string metadataFile = Path.Combine(tempDir, "metadata.txt");
|
||||
using (var writer = new StreamWriter(metadataFile, false, Encoding.UTF8))
|
||||
{
|
||||
writer.WriteLine($"保存时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
writer.WriteLine($"总页数: {allPageStrokes.Count}");
|
||||
writer.WriteLine($"模式: {(currentMode == 0 ? "PPT放映" : "白板")}");
|
||||
writer.WriteLine($"格式: XML");
|
||||
if (currentMode != 0)
|
||||
{
|
||||
writer.WriteLine($"当前页面: {CurrentWhiteboardIndex}");
|
||||
writer.WriteLine($"总页面数: {WhiteboardTotalCount}");
|
||||
}
|
||||
else if (pptApplication != null)
|
||||
{
|
||||
writer.WriteLine($"PPT名称: {pptApplication.SlideShowWindows[1].Presentation.Name}");
|
||||
writer.WriteLine($"PPT总页数: {pptApplication.SlideShowWindows[1].Presentation.Slides.Count}");
|
||||
writer.WriteLine($"PPT文件路径: {pptApplication.SlideShowWindows[1].Presentation.FullName}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < allPageStrokes.Count; i++)
|
||||
{
|
||||
writer.WriteLine($"页面 {i + 1}: {allPageStrokes[i].Count} 条墨迹");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建ZIP文件
|
||||
if (File.Exists(zipFileName))
|
||||
File.Delete(zipFileName);
|
||||
|
||||
ZipFile.CreateFromDirectory(tempDir, zipFileName);
|
||||
|
||||
// 异步上传ZIP文件到Dlass
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
|
||||
if (delayMinutes > 0)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
|
||||
}
|
||||
|
||||
await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
if (newNotice) ShowNotification($"多页面XML墨迹成功保存至压缩包 {zipFileName}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 清理临时目录
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理临时目录失败: {ex}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存多页面XML墨迹压缩包失败: {ex}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将多页面墨迹保存为压缩包
|
||||
/// </summary>
|
||||
@@ -228,6 +517,24 @@ namespace Ink_Canvas
|
||||
// 使用System.IO.Compression.FileSystem来创建ZIP
|
||||
ZipFile.CreateFromDirectory(tempDir, zipFileName);
|
||||
|
||||
// 异步上传ZIP文件到Dlass
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
|
||||
if (delayMinutes > 0)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
|
||||
}
|
||||
|
||||
await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
if (newNotice) ShowNotification($"多页面墨迹成功保存至压缩包 {zipFileName}");
|
||||
}
|
||||
finally
|
||||
@@ -310,6 +617,23 @@ namespace Ink_Canvas
|
||||
var fs = new FileStream(savePathWithName, FileMode.Create);
|
||||
inkCanvas.Strokes.Save(fs);
|
||||
fs.Close();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
|
||||
if (delayMinutes > 0)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
|
||||
}
|
||||
|
||||
await Helpers.DlassNoteUploader.UploadNoteFileAsync(imagePathWithName);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +682,7 @@ namespace Ink_Canvas
|
||||
var openFileDialog = new OpenFileDialog();
|
||||
openFileDialog.InitialDirectory = Settings.Automation.AutoSavedStrokesLocation;
|
||||
openFileDialog.Title = "打开墨迹文件";
|
||||
openFileDialog.Filter = "Ink Canvas Strokes File (*.icstk)|*.icstk|ICC压缩包 (*.zip)|*.zip";
|
||||
openFileDialog.Filter = "Ink Canvas Strokes File (*.icstk)|*.icstk|XML墨迹文件 (*.xml)|*.xml|ICC压缩包 (*.zip)|*.zip|所有支持的文件 (*.icstk;*.xml;*.zip)|*.icstk;*.xml;*.zip";
|
||||
if (openFileDialog.ShowDialog() != true) return;
|
||||
LogHelper.WriteLogToFile($"Strokes Insert: Name: {openFileDialog.FileName}",
|
||||
LogHelper.LogType.Event);
|
||||
@@ -369,12 +693,17 @@ namespace Ink_Canvas
|
||||
|
||||
if (fileExtension == ".zip")
|
||||
{
|
||||
// 处理ICC压缩包
|
||||
// 处理ICC压缩包(可能包含XML格式)
|
||||
OpenICCZipFile(openFileDialog.FileName);
|
||||
}
|
||||
else if (fileExtension == ".xml")
|
||||
{
|
||||
// 处理XML格式墨迹文件
|
||||
OpenXMLStrokeFile(openFileDialog.FileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 处理单个墨迹文件
|
||||
// 处理单个墨迹文件(二进制格式)
|
||||
OpenSingleStrokeFile(openFileDialog.FileName);
|
||||
}
|
||||
|
||||
@@ -528,23 +857,41 @@ namespace Ink_Canvas
|
||||
timeMachine.ClearStrokeHistory();
|
||||
|
||||
// 重置PPT墨迹存储
|
||||
_pptInkManager?.ClearAllStrokes();
|
||||
_singlePPTInkManager?.ClearAllStrokes();
|
||||
|
||||
// 读取所有页面的墨迹文件
|
||||
var files = Directory.GetFiles(tempDir, "page_*.icstk");
|
||||
foreach (var file in files)
|
||||
// 读取所有页面的墨迹文件(支持.icstk和.xml格式)
|
||||
var icstkFiles = Directory.GetFiles(tempDir, "page_*.icstk");
|
||||
var xmlFiles = Directory.GetFiles(tempDir, "page_*.xml");
|
||||
var allFiles = new List<string>();
|
||||
allFiles.AddRange(icstkFiles);
|
||||
allFiles.AddRange(xmlFiles);
|
||||
|
||||
foreach (var file in allFiles)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(file);
|
||||
if (fileName.StartsWith("page_") && int.TryParse(fileName.Substring(5), out int pageNumber))
|
||||
{
|
||||
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read))
|
||||
StrokeCollection strokes = null;
|
||||
string extension = Path.GetExtension(file).ToLower();
|
||||
|
||||
if (extension == ".xml")
|
||||
{
|
||||
var strokes = new StrokeCollection(fs);
|
||||
if (strokes.Count > 0)
|
||||
// 从XML文件加载
|
||||
strokes = LoadStrokesFromXML(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 从二进制文件加载
|
||||
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
_pptInkManager?.SaveCurrentSlideStrokes(pageNumber, strokes);
|
||||
strokes = new StrokeCollection(fs);
|
||||
}
|
||||
}
|
||||
|
||||
if (strokes != null && strokes.Count > 0)
|
||||
{
|
||||
_singlePPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,14 +899,14 @@ namespace Ink_Canvas
|
||||
if (_pptManager?.IsInSlideShow == true)
|
||||
{
|
||||
int currentSlide = _pptManager.GetCurrentSlideNumber();
|
||||
var currentStrokes = _pptInkManager?.LoadSlideStrokes(currentSlide);
|
||||
var currentStrokes = _singlePPTInkManager?.LoadSlideStrokes(currentSlide);
|
||||
if (currentStrokes != null && currentStrokes.Count > 0)
|
||||
{
|
||||
inkCanvas.Strokes.Add(currentStrokes);
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"成功恢复PPT墨迹,共{files.Length}页");
|
||||
LogHelper.WriteLogToFile($"成功恢复PPT墨迹,共{allFiles.Count}页");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -640,10 +987,177 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开XML格式的墨迹文件
|
||||
/// </summary>
|
||||
public void OpenXMLStrokeFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
XDocument doc = XDocument.Load(filePath);
|
||||
var root = doc.Root;
|
||||
if (root == null || root.Name != "InkCanvasStrokes")
|
||||
{
|
||||
throw new Exception("无效的XML墨迹文件格式");
|
||||
}
|
||||
|
||||
var strokes = new StrokeCollection();
|
||||
foreach (var strokeElement in root.Elements("Stroke"))
|
||||
{
|
||||
var drawingAttributesStr = strokeElement.Attribute("DrawingAttributes")?.Value ?? "";
|
||||
var da = ParseDrawingAttributes(drawingAttributesStr);
|
||||
|
||||
var stylusPoints = new StylusPointCollection();
|
||||
var stylusPointsElement = strokeElement.Element("StylusPoints");
|
||||
if (stylusPointsElement != null)
|
||||
{
|
||||
foreach (var pointElement in stylusPointsElement.Elements("StylusPoint"))
|
||||
{
|
||||
double x = double.Parse(pointElement.Attribute("X")?.Value ?? "0");
|
||||
double y = double.Parse(pointElement.Attribute("Y")?.Value ?? "0");
|
||||
float pressure = float.Parse(pointElement.Attribute("PressureFactor")?.Value ?? "0.5");
|
||||
stylusPoints.Add(new StylusPoint(x, y, pressure));
|
||||
}
|
||||
}
|
||||
|
||||
if (stylusPoints.Count > 0)
|
||||
{
|
||||
var stroke = new Stroke(stylusPoints) { DrawingAttributes = da };
|
||||
strokes.Add(stroke);
|
||||
}
|
||||
}
|
||||
|
||||
ClearStrokes(true);
|
||||
timeMachine.ClearStrokeHistory();
|
||||
inkCanvas.Strokes.Add(strokes);
|
||||
LogHelper.NewLog($"XML Strokes Insert: Strokes Count: {inkCanvas.Strokes.Count}");
|
||||
|
||||
// 恢复元素信息
|
||||
var elementsFile = Path.ChangeExtension(filePath, ".elements.json");
|
||||
if (File.Exists(elementsFile))
|
||||
{
|
||||
var elementInfos = JsonConvert.DeserializeObject<List<CanvasElementInfo>>(File.ReadAllText(elementsFile));
|
||||
foreach (var info in elementInfos)
|
||||
{
|
||||
if (info.Type == "Image" && File.Exists(info.SourcePath))
|
||||
{
|
||||
var img = new Image
|
||||
{
|
||||
Source = new BitmapImage(new Uri(info.SourcePath)),
|
||||
Width = info.Width,
|
||||
Height = info.Height,
|
||||
Stretch = Enum.TryParse<Stretch>(info.Stretch, out var stretch) ? stretch : Stretch.Fill
|
||||
};
|
||||
InkCanvas.SetLeft(img, info.Left);
|
||||
InkCanvas.SetTop(img, info.Top);
|
||||
inkCanvas.Children.Add(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"打开XML墨迹文件失败: {ex}", LogHelper.LogType.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从XML文件加载StrokeCollection(辅助方法,用于ZIP文件恢复)
|
||||
/// </summary>
|
||||
private StrokeCollection LoadStrokesFromXML(string xmlPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
XDocument doc = XDocument.Load(xmlPath);
|
||||
var root = doc.Root;
|
||||
if (root == null || root.Name != "InkCanvasStrokes")
|
||||
{
|
||||
return new StrokeCollection();
|
||||
}
|
||||
|
||||
var strokes = new StrokeCollection();
|
||||
foreach (var strokeElement in root.Elements("Stroke"))
|
||||
{
|
||||
var drawingAttributesStr = strokeElement.Attribute("DrawingAttributes")?.Value ?? "";
|
||||
var da = ParseDrawingAttributes(drawingAttributesStr);
|
||||
|
||||
var stylusPoints = new StylusPointCollection();
|
||||
var stylusPointsElement = strokeElement.Element("StylusPoints");
|
||||
if (stylusPointsElement != null)
|
||||
{
|
||||
foreach (var pointElement in stylusPointsElement.Elements("StylusPoint"))
|
||||
{
|
||||
double x = double.Parse(pointElement.Attribute("X")?.Value ?? "0");
|
||||
double y = double.Parse(pointElement.Attribute("Y")?.Value ?? "0");
|
||||
float pressure = float.Parse(pointElement.Attribute("PressureFactor")?.Value ?? "0.5");
|
||||
stylusPoints.Add(new StylusPoint(x, y, pressure));
|
||||
}
|
||||
}
|
||||
|
||||
if (stylusPoints.Count > 0)
|
||||
{
|
||||
var stroke = new Stroke(stylusPoints) { DrawingAttributes = da };
|
||||
strokes.Add(stroke);
|
||||
}
|
||||
}
|
||||
|
||||
return strokes;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从XML加载墨迹失败: {ex}", LogHelper.LogType.Error);
|
||||
return new StrokeCollection();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从字符串解析DrawingAttributes
|
||||
/// </summary>
|
||||
private DrawingAttributes ParseDrawingAttributes(string attributesStr)
|
||||
{
|
||||
var da = new DrawingAttributes();
|
||||
var parts = attributesStr.Split(';');
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var kv = part.Split('=');
|
||||
if (kv.Length == 2)
|
||||
{
|
||||
var key = kv[0].Trim();
|
||||
var value = kv[1].Trim();
|
||||
switch (key)
|
||||
{
|
||||
case "Color":
|
||||
da.Color = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(value);
|
||||
break;
|
||||
case "Width":
|
||||
da.Width = double.Parse(value);
|
||||
break;
|
||||
case "Height":
|
||||
da.Height = double.Parse(value);
|
||||
break;
|
||||
case "FitToCurve":
|
||||
da.FitToCurve = bool.Parse(value);
|
||||
break;
|
||||
case "IsHighlighter":
|
||||
da.IsHighlighter = bool.Parse(value);
|
||||
break;
|
||||
case "IgnorePressure":
|
||||
da.IgnorePressure = bool.Parse(value);
|
||||
break;
|
||||
case "StylusTip":
|
||||
da.StylusTip = Enum.TryParse<StylusTip>(value, out var tip) ? tip : StylusTip.Ellipse;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return da;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开单个墨迹文件
|
||||
/// </summary>
|
||||
private void OpenSingleStrokeFile(string filePath)
|
||||
public void OpenSingleStrokeFile(string filePath)
|
||||
{
|
||||
var fileStreamHasNoStroke = false;
|
||||
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||
@@ -695,3 +1209,4 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
|
||||
@@ -65,6 +66,22 @@ namespace Ink_Canvas
|
||||
{
|
||||
ShowNotification($"截图成功保存至 {savePath}");
|
||||
}
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
|
||||
if (delayMinutes > 0)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
|
||||
}
|
||||
|
||||
await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取日期文件夹路径
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -6,7 +8,7 @@ using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using System.Windows.Shapes;
|
||||
using Point = System.Windows.Point;
|
||||
|
||||
namespace Ink_Canvas
|
||||
@@ -32,23 +34,24 @@ namespace Ink_Canvas
|
||||
lastBorderMouseDownObject = sender;
|
||||
}
|
||||
|
||||
private bool isStrokeSelectionCloneOn;
|
||||
|
||||
private void BorderStrokeSelectionClone_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (lastBorderMouseDownObject != sender) return;
|
||||
|
||||
if (isStrokeSelectionCloneOn)
|
||||
try
|
||||
{
|
||||
BorderStrokeSelectionClone.Background = Brushes.Transparent;
|
||||
|
||||
isStrokeSelectionCloneOn = false;
|
||||
var strokes = inkCanvas.GetSelectedStrokes();
|
||||
if (strokes.Count > 0)
|
||||
{
|
||||
// 直接执行克隆操作,与图片克隆保持一致
|
||||
CloneStrokes(strokes);
|
||||
LogHelper.WriteLogToFile($"墨迹克隆完成: {strokes.Count} 个墨迹");
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
BorderStrokeSelectionClone.Background = new SolidColorBrush(StringToColor("#FF1ED760"));
|
||||
|
||||
isStrokeSelectionCloneOn = true;
|
||||
LogHelper.WriteLogToFile($"墨迹克隆失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +61,7 @@ namespace Ink_Canvas
|
||||
|
||||
var strokes = inkCanvas.GetSelectedStrokes();
|
||||
inkCanvas.Select(new StrokeCollection());
|
||||
strokes = strokes.Clone();
|
||||
BtnWhiteBoardAdd_Click(null, null);
|
||||
inkCanvas.Strokes.Add(strokes);
|
||||
CloneStrokesToNewBoard(strokes);
|
||||
}
|
||||
|
||||
private void BorderStrokeSelectionDelete_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
@@ -254,23 +255,105 @@ namespace Ink_Canvas
|
||||
#endregion
|
||||
|
||||
private bool isGridInkCanvasSelectionCoverMouseDown;
|
||||
private bool isStrokeDragging = false;
|
||||
private Point strokeDragStartPoint;
|
||||
private StrokeCollection StrokesSelectionClone = new StrokeCollection();
|
||||
|
||||
// 选择框和选择点相关变量
|
||||
private bool isResizing = false;
|
||||
private string currentResizeHandle = "";
|
||||
private Point resizeStartPoint;
|
||||
private Rect originalSelectionBounds;
|
||||
|
||||
private void GridInkCanvasSelectionCover_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
isGridInkCanvasSelectionCoverMouseDown = true;
|
||||
|
||||
// 检查是否有选中的墨迹
|
||||
if (inkCanvas.GetSelectedStrokes().Count > 0)
|
||||
{
|
||||
// 获取鼠标点击位置
|
||||
var clickPoint = e.GetPosition(inkCanvas);
|
||||
var selectionBounds = inkCanvas.GetSelectionBounds();
|
||||
|
||||
// 检查点击位置是否在选择框边界内
|
||||
if (clickPoint.X >= selectionBounds.Left &&
|
||||
clickPoint.X <= selectionBounds.Right &&
|
||||
clickPoint.Y >= selectionBounds.Top &&
|
||||
clickPoint.Y <= selectionBounds.Bottom)
|
||||
{
|
||||
// 只有在选择框边界内才允许拖动
|
||||
isStrokeDragging = true;
|
||||
strokeDragStartPoint = clickPoint;
|
||||
GridInkCanvasSelectionCover.CaptureMouse();
|
||||
GridInkCanvasSelectionCover.Cursor = Cursors.SizeAll;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 点击在选择框外,取消选择
|
||||
inkCanvas.Select(new StrokeCollection());
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GridInkCanvasSelectionCover_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!isGridInkCanvasSelectionCoverMouseDown) return;
|
||||
|
||||
// 如果正在拖动墨迹,执行拖动操作
|
||||
if (isStrokeDragging && GridInkCanvasSelectionCover.IsMouseCaptured)
|
||||
{
|
||||
var currentPoint = e.GetPosition(inkCanvas);
|
||||
var delta = currentPoint - strokeDragStartPoint;
|
||||
|
||||
// 创建变换矩阵
|
||||
var matrix = new Matrix();
|
||||
matrix.Translate(delta.X, delta.Y);
|
||||
|
||||
// 对选中的墨迹应用变换
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
|
||||
// 更新选中栏位置
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
|
||||
// 更新起始点
|
||||
strokeDragStartPoint = currentPoint;
|
||||
}
|
||||
else if (inkCanvas.GetSelectedStrokes().Count > 0)
|
||||
{
|
||||
// 当鼠标在选中区域移动时,更新墨迹选中栏位置
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
}
|
||||
}
|
||||
|
||||
private void GridInkCanvasSelectionCover_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!isGridInkCanvasSelectionCoverMouseDown) return;
|
||||
|
||||
// 结束墨迹拖动
|
||||
if (isStrokeDragging)
|
||||
{
|
||||
isStrokeDragging = false;
|
||||
GridInkCanvasSelectionCover.ReleaseMouseCapture();
|
||||
GridInkCanvasSelectionCover.Cursor = Cursors.Arrow;
|
||||
}
|
||||
|
||||
isGridInkCanvasSelectionCoverMouseDown = false;
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
|
||||
// 只有在没有选中墨迹时才隐藏选中栏
|
||||
if (inkCanvas.GetSelectedStrokes().Count == 0)
|
||||
{
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnSelect_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExitMultiTouchModeIfNeeded();
|
||||
forceEraser = true;
|
||||
drawingShapeMode = 0;
|
||||
inkCanvas.IsManipulationEnabled = false;
|
||||
@@ -305,29 +388,52 @@ namespace Ink_Canvas
|
||||
private void inkCanvas_SelectionChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (isProgramChangeStrokeSelection) return;
|
||||
|
||||
// 检查是否有图片元素被选中
|
||||
|
||||
// 优先检查墨迹选择状态
|
||||
if (inkCanvas.GetSelectedStrokes().Count > 0)
|
||||
{
|
||||
// 有墨迹被选中,清除图片选择状态
|
||||
if (currentSelectedElement != null)
|
||||
{
|
||||
currentSelectedElement = null;
|
||||
// 隐藏图片选择工具栏
|
||||
if (BorderImageSelectionControl != null)
|
||||
{
|
||||
BorderImageSelectionControl.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示墨迹选择栏和选择框
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Visible;
|
||||
BorderStrokeSelectionClone.Background = Brushes.Transparent;
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
UpdateSelectionDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有图片元素被选中(通过InkCanvas的选中元素)
|
||||
var selectedElements = inkCanvas.GetSelectedElements();
|
||||
bool hasImageElement = selectedElements.Any(element => element is Image);
|
||||
|
||||
|
||||
// 如果有图片元素被选中,不显示选择框
|
||||
if (hasImageElement)
|
||||
{
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
HideSelectionDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
if (inkCanvas.GetSelectedStrokes().Count == 0)
|
||||
|
||||
// 检查是否有图片元素被选中(通过currentSelectedElement)
|
||||
if (currentSelectedElement != null && currentSelectedElement is Image)
|
||||
{
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
HideSelectionDisplay();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Visible;
|
||||
BorderStrokeSelectionClone.Background = Brushes.Transparent;
|
||||
isStrokeSelectionCloneOn = false;
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
}
|
||||
|
||||
// 没有选中任何内容,隐藏选择框
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
HideSelectionDisplay();
|
||||
}
|
||||
|
||||
|
||||
@@ -336,7 +442,7 @@ namespace Ink_Canvas
|
||||
{
|
||||
var borderLeft = (inkCanvas.GetSelectionBounds().Left + inkCanvas.GetSelectionBounds().Right -
|
||||
BorderStrokeSelectionControlWidth) / 2;
|
||||
var borderTop = inkCanvas.GetSelectionBounds().Bottom + 1;
|
||||
var borderTop = inkCanvas.GetSelectionBounds().Bottom + 10; // 在墨迹下方10像素处显示
|
||||
if (borderLeft < 0) borderLeft = 0;
|
||||
if (borderTop < 0) borderTop = 0;
|
||||
if (Width - borderLeft < BorderStrokeSelectionControlWidth || double.IsNaN(borderLeft))
|
||||
@@ -344,7 +450,14 @@ namespace Ink_Canvas
|
||||
if (Height - borderTop < BorderStrokeSelectionControlHeight || double.IsNaN(borderTop))
|
||||
borderTop = Height - BorderStrokeSelectionControlHeight;
|
||||
|
||||
if (borderTop > 60) borderTop -= 60;
|
||||
// 确保墨迹选中栏始终显示在墨迹下方
|
||||
// 如果选中栏会超出屏幕底部,则显示在墨迹上方
|
||||
if (borderTop + BorderStrokeSelectionControlHeight > Height)
|
||||
{
|
||||
borderTop = inkCanvas.GetSelectionBounds().Top - BorderStrokeSelectionControlHeight - 10;
|
||||
if (borderTop < 0) borderTop = 10; // 如果上方也没有空间,则显示在顶部
|
||||
}
|
||||
|
||||
BorderStrokeSelectionControl.Margin = new Thickness(borderLeft, borderTop, 0, 0);
|
||||
}
|
||||
|
||||
@@ -408,25 +521,19 @@ namespace Ink_Canvas
|
||||
strokes = StrokesSelectionClone;
|
||||
else if (Settings.Gesture.IsEnableTwoFingerRotationOnSelection)
|
||||
m.RotateAt(rotate, center.X, center.Y); // 旋转
|
||||
|
||||
// 应用变换到选中的墨迹
|
||||
foreach (var stroke in strokes)
|
||||
{
|
||||
stroke.Transform(m, false);
|
||||
|
||||
try
|
||||
{
|
||||
stroke.DrawingAttributes.Width *= md.Scale.X;
|
||||
stroke.DrawingAttributes.Height *= md.Scale.Y;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"墨迹ManipulationDelta错误: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,6 +545,78 @@ namespace Ink_Canvas
|
||||
{
|
||||
}
|
||||
|
||||
private void GridInkCanvasSelectionCover_TouchMove(object sender, TouchEventArgs e)
|
||||
{
|
||||
// 处理触摸移动事件 - 用于拖动选中的墨迹
|
||||
if (inkCanvas.GetSelectedStrokes().Count > 0 && dec.Count == 1)
|
||||
{
|
||||
var currentTouchPoint = e.GetTouchPoint(inkCanvas).Position;
|
||||
|
||||
// 检查是否有有效的起始触摸点
|
||||
if (lastTouchPointOnGridInkCanvasCover != new Point(0, 0))
|
||||
{
|
||||
var delta = currentTouchPoint - lastTouchPointOnGridInkCanvasCover;
|
||||
|
||||
// 只有当移动距离足够大时才进行拖动(避免微小移动造成的抖动)
|
||||
if (Math.Abs(delta.X) > 1 || Math.Abs(delta.Y) > 1)
|
||||
{
|
||||
// 创建变换矩阵
|
||||
var matrix = new Matrix();
|
||||
matrix.Translate(delta.X, delta.Y);
|
||||
|
||||
// 对选中的墨迹应用变换
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
|
||||
// 更新选中栏位置
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
|
||||
// 更新最后触摸点
|
||||
lastTouchPointOnGridInkCanvasCover = currentTouchPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GridInkCanvasSelectionCover_PreviewTouchMove(object sender, TouchEventArgs e)
|
||||
{
|
||||
// 预览触摸移动事件 - 用于更精确的触摸处理
|
||||
if (inkCanvas.GetSelectedStrokes().Count > 0 && dec.Count == 1)
|
||||
{
|
||||
var currentTouchPoint = e.GetTouchPoint(inkCanvas).Position;
|
||||
|
||||
// 检查是否有有效的起始触摸点
|
||||
if (lastTouchPointOnGridInkCanvasCover != new Point(0, 0))
|
||||
{
|
||||
var delta = currentTouchPoint - lastTouchPointOnGridInkCanvasCover;
|
||||
|
||||
// 只有当移动距离足够大时才进行拖动(避免微小移动造成的抖动)
|
||||
if (Math.Abs(delta.X) > 1 || Math.Abs(delta.Y) > 1)
|
||||
{
|
||||
// 创建变换矩阵
|
||||
var matrix = new Matrix();
|
||||
matrix.Translate(delta.X, delta.Y);
|
||||
|
||||
// 对选中的墨迹应用变换
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
|
||||
// 更新选中栏位置
|
||||
updateBorderStrokeSelectionControlLocation();
|
||||
|
||||
// 更新最后触摸点
|
||||
lastTouchPointOnGridInkCanvasCover = currentTouchPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Point lastTouchPointOnGridInkCanvasCover = new Point(0, 0);
|
||||
|
||||
private void GridInkCanvasSelectionCover_PreviewTouchDown(object sender, TouchEventArgs e)
|
||||
@@ -449,24 +628,6 @@ namespace Ink_Canvas
|
||||
var touchPoint = e.GetTouchPoint(null);
|
||||
centerPoint = touchPoint.Position;
|
||||
lastTouchPointOnGridInkCanvasCover = touchPoint.Position;
|
||||
|
||||
if (isStrokeSelectionCloneOn)
|
||||
{
|
||||
var strokes = inkCanvas.GetSelectedStrokes();
|
||||
isProgramChangeStrokeSelection = true;
|
||||
inkCanvas.Select(new StrokeCollection());
|
||||
StrokesSelectionClone = strokes.Clone();
|
||||
inkCanvas.Select(strokes);
|
||||
isProgramChangeStrokeSelection = false;
|
||||
inkCanvas.Strokes.Add(StrokesSelectionClone);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 新增:启动套索选择模式
|
||||
// 使用集中化的工具模式切换方法
|
||||
SetCurrentToolMode(InkCanvasEditingMode.Select);
|
||||
inkCanvas.Select(new StrokeCollection());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,13 +636,24 @@ namespace Ink_Canvas
|
||||
dec.Remove(e.TouchDevice.Id);
|
||||
if (dec.Count >= 1) return;
|
||||
isProgramChangeStrokeSelection = false;
|
||||
if (lastTouchPointOnGridInkCanvasCover == e.GetTouchPoint(null).Position)
|
||||
|
||||
var touchUpPoint = e.GetTouchPoint(null).Position;
|
||||
if (lastTouchPointOnGridInkCanvasCover == touchUpPoint)
|
||||
{
|
||||
if (!(lastTouchPointOnGridInkCanvasCover.X < inkCanvas.GetSelectionBounds().Left) &&
|
||||
!(lastTouchPointOnGridInkCanvasCover.Y < inkCanvas.GetSelectionBounds().Top) &&
|
||||
!(lastTouchPointOnGridInkCanvasCover.X > inkCanvas.GetSelectionBounds().Right) &&
|
||||
!(lastTouchPointOnGridInkCanvasCover.Y > inkCanvas.GetSelectionBounds().Bottom)) return;
|
||||
var touchPointInCanvas = e.GetTouchPoint(inkCanvas).Position;
|
||||
var selectionBounds = inkCanvas.GetSelectionBounds();
|
||||
|
||||
if (!(touchPointInCanvas.X < selectionBounds.Left) &&
|
||||
!(touchPointInCanvas.Y < selectionBounds.Top) &&
|
||||
!(touchPointInCanvas.X > selectionBounds.Right) &&
|
||||
!(touchPointInCanvas.Y > selectionBounds.Bottom))
|
||||
{
|
||||
return;
|
||||
}
|
||||
isProgramChangeStrokeSelection = true;
|
||||
inkCanvas.Select(new StrokeCollection());
|
||||
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
|
||||
isProgramChangeStrokeSelection = false;
|
||||
StrokesSelectionClone = new StrokeCollection();
|
||||
}
|
||||
else if (inkCanvas.GetSelectedStrokes().Count == 0)
|
||||
@@ -498,7 +670,6 @@ namespace Ink_Canvas
|
||||
|
||||
private void LassoSelect_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExitMultiTouchModeIfNeeded();
|
||||
forceEraser = false;
|
||||
forcePointEraser = false;
|
||||
drawingShapeMode = 0;
|
||||
@@ -509,7 +680,6 @@ namespace Ink_Canvas
|
||||
|
||||
private void BtnLassoSelect_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExitMultiTouchModeIfNeeded();
|
||||
forceEraser = false;
|
||||
forcePointEraser = false;
|
||||
drawingShapeMode = 0;
|
||||
@@ -520,7 +690,7 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
#region UIElement Selection and Resize
|
||||
|
||||
|
||||
private Rect GetUIElementBounds(UIElement element)
|
||||
{
|
||||
if (element is FrameworkElement fe)
|
||||
@@ -558,7 +728,177 @@ namespace Ink_Canvas
|
||||
|
||||
return new Rect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Selection Display and Resize Handles
|
||||
|
||||
private void UpdateSelectionDisplay()
|
||||
{
|
||||
if (inkCanvas.GetSelectedStrokes().Count == 0)
|
||||
{
|
||||
HideSelectionDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
var selectionBounds = inkCanvas.GetSelectionBounds();
|
||||
|
||||
// 更新选择框
|
||||
SelectionRectangle.Visibility = Visibility.Visible;
|
||||
SelectionRectangle.Margin = new Thickness(selectionBounds.Left, selectionBounds.Top, 0, 0);
|
||||
SelectionRectangle.Width = selectionBounds.Width;
|
||||
SelectionRectangle.Height = selectionBounds.Height;
|
||||
|
||||
// 更新选择点位置
|
||||
UpdateSelectionHandles(selectionBounds);
|
||||
SelectionHandlesCanvas.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void HideSelectionDisplay()
|
||||
{
|
||||
SelectionRectangle.Visibility = Visibility.Collapsed;
|
||||
SelectionHandlesCanvas.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void UpdateSelectionHandles(Rect bounds)
|
||||
{
|
||||
// 四个角选择点
|
||||
TopLeftHandle.Margin = new Thickness(bounds.Left - 4, bounds.Top - 4, 0, 0);
|
||||
TopRightHandle.Margin = new Thickness(bounds.Right - 4, bounds.Top - 4, 0, 0);
|
||||
BottomLeftHandle.Margin = new Thickness(bounds.Left - 4, bounds.Bottom - 4, 0, 0);
|
||||
BottomRightHandle.Margin = new Thickness(bounds.Right - 4, bounds.Bottom - 4, 0, 0);
|
||||
|
||||
// 四个边选择点
|
||||
TopHandle.Margin = new Thickness(bounds.Left + bounds.Width / 2 - 4, bounds.Top - 4, 0, 0);
|
||||
BottomHandle.Margin = new Thickness(bounds.Left + bounds.Width / 2 - 4, bounds.Bottom - 4, 0, 0);
|
||||
LeftHandle.Margin = new Thickness(bounds.Left - 4, bounds.Top + bounds.Height / 2 - 4, 0, 0);
|
||||
RightHandle.Margin = new Thickness(bounds.Right - 4, bounds.Top + bounds.Height / 2 - 4, 0, 0);
|
||||
}
|
||||
|
||||
private void SelectionHandle_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Rectangle handle)
|
||||
{
|
||||
isResizing = true;
|
||||
currentResizeHandle = handle.Name;
|
||||
resizeStartPoint = e.GetPosition(inkCanvas);
|
||||
originalSelectionBounds = inkCanvas.GetSelectionBounds();
|
||||
handle.CaptureMouse();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectionHandle_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!isResizing || !(sender is Rectangle handle)) return;
|
||||
|
||||
var currentPoint = e.GetPosition(inkCanvas);
|
||||
var delta = new Point(currentPoint.X - resizeStartPoint.X, currentPoint.Y - resizeStartPoint.Y);
|
||||
|
||||
var newBounds = CalculateNewBounds(originalSelectionBounds, delta, currentResizeHandle);
|
||||
|
||||
// 应用新的边界到选中的墨迹
|
||||
ApplyBoundsToStrokes(newBounds);
|
||||
|
||||
// 更新选择框显示
|
||||
UpdateSelectionDisplay();
|
||||
}
|
||||
|
||||
private void SelectionHandle_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Rectangle handle)
|
||||
{
|
||||
isResizing = false;
|
||||
currentResizeHandle = "";
|
||||
handle.ReleaseMouseCapture();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private Rect CalculateNewBounds(Rect originalBounds, Point delta, string handleName)
|
||||
{
|
||||
var newBounds = originalBounds;
|
||||
double newWidth = originalBounds.Width;
|
||||
double newHeight = originalBounds.Height;
|
||||
double newX = originalBounds.X;
|
||||
double newY = originalBounds.Y;
|
||||
|
||||
switch (handleName)
|
||||
{
|
||||
case "TopLeftHandle":
|
||||
newX = originalBounds.X + delta.X;
|
||||
newY = originalBounds.Y + delta.Y;
|
||||
newWidth = originalBounds.Width - delta.X;
|
||||
newHeight = originalBounds.Height - delta.Y;
|
||||
break;
|
||||
case "TopRightHandle":
|
||||
newY = originalBounds.Y + delta.Y;
|
||||
newWidth = originalBounds.Width + delta.X;
|
||||
newHeight = originalBounds.Height - delta.Y;
|
||||
break;
|
||||
case "BottomLeftHandle":
|
||||
newX = originalBounds.X + delta.X;
|
||||
newWidth = originalBounds.Width - delta.X;
|
||||
newHeight = originalBounds.Height + delta.Y;
|
||||
break;
|
||||
case "BottomRightHandle":
|
||||
newWidth = originalBounds.Width + delta.X;
|
||||
newHeight = originalBounds.Height + delta.Y;
|
||||
break;
|
||||
case "TopHandle":
|
||||
newY = originalBounds.Y + delta.Y;
|
||||
newHeight = originalBounds.Height - delta.Y;
|
||||
break;
|
||||
case "BottomHandle":
|
||||
newHeight = originalBounds.Height + delta.Y;
|
||||
break;
|
||||
case "LeftHandle":
|
||||
newX = originalBounds.X + delta.X;
|
||||
newWidth = originalBounds.Width - delta.X;
|
||||
break;
|
||||
case "RightHandle":
|
||||
newWidth = originalBounds.Width + delta.X;
|
||||
break;
|
||||
}
|
||||
|
||||
// 确保最小尺寸和正值
|
||||
if (newWidth < 10) newWidth = 10;
|
||||
if (newHeight < 10) newHeight = 10;
|
||||
|
||||
// 创建新的Rect,确保所有值都是有效的
|
||||
newBounds = new Rect(newX, newY, newWidth, newHeight);
|
||||
|
||||
return newBounds;
|
||||
}
|
||||
|
||||
private void ApplyBoundsToStrokes(Rect newBounds)
|
||||
{
|
||||
var selectedStrokes = inkCanvas.GetSelectedStrokes();
|
||||
if (selectedStrokes.Count == 0) return;
|
||||
|
||||
var originalBounds = inkCanvas.GetSelectionBounds();
|
||||
|
||||
// 计算缩放比例
|
||||
var scaleX = newBounds.Width / originalBounds.Width;
|
||||
var scaleY = newBounds.Height / originalBounds.Height;
|
||||
|
||||
// 计算平移量
|
||||
var translateX = newBounds.X - originalBounds.X;
|
||||
var translateY = newBounds.Y - originalBounds.Y;
|
||||
|
||||
// 创建变换矩阵
|
||||
var matrix = new Matrix();
|
||||
matrix.Translate(translateX, translateY);
|
||||
matrix.ScaleAt(scaleX, scaleY, originalBounds.X + originalBounds.Width / 2, originalBounds.Y + originalBounds.Height / 2);
|
||||
|
||||
// 应用变换到选中的墨迹
|
||||
foreach (var stroke in selectedStrokes)
|
||||
{
|
||||
stroke.Transform(matrix, false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
using System;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OSVersionExtension;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -6,12 +12,10 @@ using System.Windows.Ink;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using OSVersionExtension;
|
||||
using System.Windows.Threading;
|
||||
using File = System.IO.File;
|
||||
using OperatingSystem = OSVersionExtension.OperatingSystem;
|
||||
using WinForms = System.Windows.Forms;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -28,12 +32,103 @@ namespace Ink_Canvas
|
||||
{
|
||||
string text = File.ReadAllText(App.RootPath + settingsFileName);
|
||||
Settings = JsonConvert.DeserializeObject<Settings>(text);
|
||||
|
||||
if (Settings != null)
|
||||
{
|
||||
CleanupObsoleteSettings(text);
|
||||
}
|
||||
|
||||
// 验证设置是否成功加载
|
||||
if (Settings == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("配置文件解析失败,尝试从备份恢复", LogHelper.LogType.Warning);
|
||||
if (AutoBackupManager.TryRestoreFromBackup())
|
||||
{
|
||||
// 重新尝试加载
|
||||
text = File.ReadAllText(App.RootPath + settingsFileName);
|
||||
Settings = JsonConvert.DeserializeObject<Settings>(text);
|
||||
if (Settings != null)
|
||||
{
|
||||
// 清理过期配置项
|
||||
CleanupObsoleteSettings(text);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然失败,使用默认设置
|
||||
if (Settings == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("从备份恢复失败,使用默认设置", LogHelper.LogType.Warning);
|
||||
BtnResetToSuggestion_Click(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"配置文件加载失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
|
||||
// 尝试从备份恢复
|
||||
LogHelper.WriteLogToFile("尝试从备份恢复配置文件", LogHelper.LogType.Warning);
|
||||
if (AutoBackupManager.TryRestoreFromBackup())
|
||||
{
|
||||
try
|
||||
{
|
||||
string text = File.ReadAllText(App.RootPath + settingsFileName);
|
||||
Settings = JsonConvert.DeserializeObject<Settings>(text);
|
||||
if (Settings != null)
|
||||
{
|
||||
// 清理过期配置项
|
||||
CleanupObsoleteSettings(text);
|
||||
}
|
||||
}
|
||||
catch (Exception restoreEx)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从备份恢复后重新加载失败: {restoreEx.Message}", LogHelper.LogType.Error);
|
||||
BtnResetToSuggestion_Click(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然失败,使用默认设置
|
||||
if (Settings == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("从备份恢复失败,使用默认设置", LogHelper.LogType.Warning);
|
||||
BtnResetToSuggestion_Click(null, null);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
else
|
||||
{
|
||||
BtnResetToSuggestion_Click(null, null);
|
||||
LogHelper.WriteLogToFile("配置文件不存在,尝试从备份恢复", LogHelper.LogType.Warning);
|
||||
if (AutoBackupManager.TryRestoreFromBackup())
|
||||
{
|
||||
try
|
||||
{
|
||||
string text = File.ReadAllText(App.RootPath + settingsFileName);
|
||||
Settings = JsonConvert.DeserializeObject<Settings>(text);
|
||||
if (Settings != null)
|
||||
{
|
||||
// 清理过期配置项
|
||||
CleanupObsoleteSettings(text);
|
||||
}
|
||||
}
|
||||
catch (Exception restoreEx)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从备份恢复后加载失败: {restoreEx.Message}", LogHelper.LogType.Error);
|
||||
BtnResetToSuggestion_Click(null, null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 备份恢复失败(备份目录不存在等),使用默认设置
|
||||
LogHelper.WriteLogToFile("备份恢复失败,使用默认设置", LogHelper.LogType.Warning);
|
||||
BtnResetToSuggestion_Click(null, null);
|
||||
}
|
||||
|
||||
// 如果仍然失败,使用默认设置
|
||||
if (Settings == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("从备份恢复失败,使用默认设置", LogHelper.LogType.Warning);
|
||||
BtnResetToSuggestion_Click(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -75,7 +170,7 @@ namespace Ink_Canvas
|
||||
Settings.Automation.AutoDelSavedFilesDaysThreshold);
|
||||
}
|
||||
|
||||
if (Settings.Startup.IsFoldAtStartup)
|
||||
if (Settings.Startup.IsFoldAtStartup && !App.StartWithBoardMode)
|
||||
{
|
||||
FoldFloatingBar_MouseUp(Fold_Icon, null);
|
||||
}
|
||||
@@ -113,7 +208,7 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// ToggleSwitchIsAutoUpdateWithSilence.Visibility = Settings.Startup.IsAutoUpdate ? Visibility.Visible : Visibility.Collapsed;
|
||||
ToggleSwitchIsAutoUpdateWithSilence.Visibility = Settings.Startup.IsAutoUpdate ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (Settings.Startup.IsAutoUpdateWithSilence)
|
||||
{
|
||||
ToggleSwitchIsAutoUpdateWithSilence.IsOn = true;
|
||||
@@ -245,10 +340,17 @@ namespace Ink_Canvas
|
||||
break;
|
||||
}
|
||||
|
||||
// 设置主题下拉框
|
||||
ComboBoxTheme.SelectedIndex = Settings.Appearance.Theme;
|
||||
|
||||
ComboBoxChickenSoupSource.SelectedIndex = Settings.Appearance.ChickenSoupSource;
|
||||
|
||||
ToggleSwitchEnableQuickPanel.IsOn = Settings.Appearance.IsShowQuickPanel;
|
||||
|
||||
ToggleSwitchEnableSplashScreen.IsOn = Settings.Appearance.EnableSplashScreen;
|
||||
|
||||
ComboBoxSplashScreenStyle.SelectedIndex = Settings.Appearance.SplashScreenStyle;
|
||||
|
||||
ToggleSwitchEnableTrayIcon.IsOn = Settings.Appearance.EnableTrayIcon;
|
||||
ICCTrayIconExampleImage.Visibility =
|
||||
Settings.Appearance.EnableTrayIcon ? Visibility.Visible : Visibility.Collapsed;
|
||||
@@ -257,6 +359,10 @@ namespace Ink_Canvas
|
||||
|
||||
ViewboxFloatingBar.Opacity = Settings.Appearance.ViewboxFloatingBarOpacityValue;
|
||||
|
||||
// 初始化浮动栏透明度滑块值
|
||||
ViewboxFloatingBarOpacityValueSlider.Value = Settings.Appearance.ViewboxFloatingBarOpacityValue;
|
||||
ViewboxFloatingBarOpacityInPPTValueSlider.Value = Settings.Appearance.ViewboxFloatingBarOpacityInPPTValue;
|
||||
|
||||
if (Settings.Appearance.EnableViewboxBlackBoardScaleTransform) // 画板 UI 缩放 80%
|
||||
{
|
||||
//ViewboxBlackboardLeftSideScaleTransform.ScaleX = 0.8;
|
||||
@@ -316,6 +422,7 @@ namespace Ink_Canvas
|
||||
Settings.Appearance.EnableChickenSoupInWhiteboardMode;
|
||||
|
||||
// 浮动栏按钮显示控制开关初始化
|
||||
CheckBoxUseLegacyFloatingBarUI.IsChecked = Settings.Appearance.UseLegacyFloatingBarUI;
|
||||
CheckBoxShowShapeButton.IsChecked = Settings.Appearance.IsShowShapeButton;
|
||||
CheckBoxShowUndoButton.IsChecked = Settings.Appearance.IsShowUndoButton;
|
||||
CheckBoxShowRedoButton.IsChecked = Settings.Appearance.IsShowRedoButton;
|
||||
@@ -327,13 +434,16 @@ namespace Ink_Canvas
|
||||
CheckBoxShowClearAndMouseButton.IsChecked = Settings.Appearance.IsShowClearAndMouseButton;
|
||||
ComboBoxEraserDisplayOption.SelectedIndex = Settings.Appearance.EraserDisplayOption;
|
||||
ComboBoxQuickColorPaletteDisplayMode.SelectedIndex = Settings.Appearance.QuickColorPaletteDisplayMode;
|
||||
|
||||
|
||||
// 初始化快捷调色盘指示器
|
||||
UpdateQuickColorPaletteIndicator(inkCanvas.DefaultDrawingAttributes.Color);
|
||||
|
||||
|
||||
// 应用浮动栏按钮可见性设置
|
||||
UpdateFloatingBarButtonsVisibility();
|
||||
|
||||
// 更新浮动栏图标
|
||||
UpdateFloatingBarIcons();
|
||||
|
||||
SystemEvents_UserPreferenceChanged(null, null);
|
||||
}
|
||||
else
|
||||
@@ -370,6 +480,21 @@ namespace Ink_Canvas
|
||||
|
||||
ToggleSwitchNotifyPreviousPage.IsOn = Settings.PowerPointSettings.IsNotifyPreviousPage;
|
||||
|
||||
// PPT时间显示胶囊设置
|
||||
if (ToggleSwitchEnablePPTTimeCapsule != null)
|
||||
{
|
||||
ToggleSwitchEnablePPTTimeCapsule.IsOn = Settings.PowerPointSettings.EnablePPTTimeCapsule;
|
||||
}
|
||||
if (ComboBoxPPTTimeCapsulePosition != null)
|
||||
{
|
||||
int position = Settings.PowerPointSettings.PPTTimeCapsulePosition;
|
||||
if (position < 0 || position > 2)
|
||||
{
|
||||
position = 1; // 默认右上角
|
||||
}
|
||||
ComboBoxPPTTimeCapsulePosition.SelectedIndex = position;
|
||||
}
|
||||
|
||||
// -- new --
|
||||
ToggleSwitchShowPPTButton.IsOn = Settings.PowerPointSettings.ShowPPTButton;
|
||||
|
||||
@@ -443,6 +568,38 @@ namespace Ink_Canvas
|
||||
|
||||
PPTButtonRBPositionValueSlider.Value = Settings.PowerPointSettings.PPTRBButtonPosition;
|
||||
|
||||
// 初始化PPT翻页按钮透明度滑块值,根据半透明选项设置默认值
|
||||
// 重用之前定义的sopsc和bopsc变量
|
||||
bool isSideHalfOpacity = sopsc.Length >= 2 && sopsc[1] == '2';
|
||||
// 如果透明度为0或未设置,根据半透明选项设置默认值
|
||||
if (Settings.PowerPointSettings.PPTLSButtonOpacity == 0.0 ||
|
||||
(Settings.PowerPointSettings.PPTLSButtonOpacity == 1.0 && isSideHalfOpacity))
|
||||
{
|
||||
Settings.PowerPointSettings.PPTLSButtonOpacity = isSideHalfOpacity ? 0.5 : 1.0;
|
||||
}
|
||||
if (Settings.PowerPointSettings.PPTRSButtonOpacity == 0.0 ||
|
||||
(Settings.PowerPointSettings.PPTRSButtonOpacity == 1.0 && isSideHalfOpacity))
|
||||
{
|
||||
Settings.PowerPointSettings.PPTRSButtonOpacity = isSideHalfOpacity ? 0.5 : 1.0;
|
||||
}
|
||||
PPTLSButtonOpacityValueSlider.Value = Settings.PowerPointSettings.PPTLSButtonOpacity;
|
||||
PPTRSButtonOpacityValueSlider.Value = Settings.PowerPointSettings.PPTRSButtonOpacity;
|
||||
|
||||
bool isBottomHalfOpacity = bopsc.Length >= 2 && bopsc[1] == '2';
|
||||
// 如果透明度为0或未设置,根据半透明选项设置默认值
|
||||
if (Settings.PowerPointSettings.PPTLBButtonOpacity == 0.0 ||
|
||||
(Settings.PowerPointSettings.PPTLBButtonOpacity == 1.0 && isBottomHalfOpacity))
|
||||
{
|
||||
Settings.PowerPointSettings.PPTLBButtonOpacity = isBottomHalfOpacity ? 0.5 : 1.0;
|
||||
}
|
||||
if (Settings.PowerPointSettings.PPTRBButtonOpacity == 0.0 ||
|
||||
(Settings.PowerPointSettings.PPTRBButtonOpacity == 1.0 && isBottomHalfOpacity))
|
||||
{
|
||||
Settings.PowerPointSettings.PPTRBButtonOpacity = isBottomHalfOpacity ? 0.5 : 1.0;
|
||||
}
|
||||
PPTLBButtonOpacityValueSlider.Value = Settings.PowerPointSettings.PPTLBButtonOpacity;
|
||||
PPTRBButtonOpacityValueSlider.Value = Settings.PowerPointSettings.PPTRBButtonOpacity;
|
||||
|
||||
UpdatePPTBtnSlidersStatus();
|
||||
|
||||
UpdatePPTBtnPreview();
|
||||
@@ -455,6 +612,8 @@ namespace Ink_Canvas
|
||||
|
||||
ToggleSwitchSupportWPS.IsOn = Settings.PowerPointSettings.IsSupportWPS;
|
||||
|
||||
ToggleSwitchPowerPointEnhancement.IsOn = Settings.PowerPointSettings.EnablePowerPointEnhancement;
|
||||
|
||||
ToggleSwitchAutoSaveScreenShotInPowerPoint.IsOn =
|
||||
Settings.PowerPointSettings.IsAutoSaveScreenShotInPowerPoint;
|
||||
ToggleSwitchEnableWppProcessKill.IsOn = Settings.PowerPointSettings.EnableWppProcessKill;
|
||||
@@ -493,7 +652,7 @@ namespace Ink_Canvas
|
||||
ToggleSwitchEnableTwoFingerTranslate.IsOn = false;
|
||||
BoardToggleSwitchEnableTwoFingerTranslate.IsOn = false;
|
||||
Settings.Gesture.IsEnableTwoFingerTranslate = false;
|
||||
if (!isInMultiTouchMode) ToggleSwitchEnableMultiTouchMode.IsOn = true;
|
||||
// if (!isInMultiTouchMode) ToggleSwitchEnableMultiTouchMode.IsOn = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -555,6 +714,7 @@ namespace Ink_Canvas
|
||||
|
||||
// 初始化屏蔽压感开关状态
|
||||
ToggleSwitchDisablePressure.IsOn = Settings.Canvas.DisablePressure;
|
||||
inkCanvas.DefaultDrawingAttributes.IgnorePressure = Settings.Canvas.DisablePressure;
|
||||
|
||||
ComboBoxPenStyle.SelectedIndex = Settings.Canvas.InkStyle;
|
||||
BoardComboBoxPenStyle.SelectedIndex = Settings.Canvas.InkStyle;
|
||||
@@ -695,6 +855,7 @@ namespace Ink_Canvas
|
||||
ToggleSwitchIsLogEnabled.IsOn = Settings.Advanced.IsLogEnabled;
|
||||
ToggleSwitchIsSaveLogByDate.IsOn = Settings.Advanced.IsSaveLogByDate;
|
||||
ToggleSwitchIsSecondConfimeWhenShutdownApp.IsOn = Settings.Advanced.IsSecondConfirmWhenShutdownApp;
|
||||
ToggleSwitchWindowMode.IsOn = Settings.Advanced.WindowMode;
|
||||
ToggleSwitchIsSpecialScreen.IsOn = Settings.Advanced.IsSpecialScreen;
|
||||
ToggleSwitchIsQuadIR.IsOn = Settings.Advanced.IsQuadIR;
|
||||
ToggleSwitchEraserBindTouchMultiplier.IsOn = Settings.Advanced.EraserBindTouchMultiplier;
|
||||
@@ -705,6 +866,17 @@ namespace Ink_Canvas
|
||||
ToggleSwitchIsEnableDPIChangeDetection.IsOn = Settings.Advanced.IsEnableDPIChangeDetection;
|
||||
ToggleSwitchIsEnableAvoidFullScreenHelper.IsOn = Settings.Advanced.IsEnableAvoidFullScreenHelper;
|
||||
ToggleSwitchIsAutoBackupBeforeUpdate.IsOn = Settings.Advanced.IsAutoBackupBeforeUpdate;
|
||||
ToggleSwitchIsAutoBackupEnabled.IsOn = Settings.Advanced.IsAutoBackupEnabled;
|
||||
|
||||
// 设置备份间隔下拉框
|
||||
foreach (ComboBoxItem item in ComboBoxAutoBackupInterval.Items)
|
||||
{
|
||||
if (item.Tag != null && int.TryParse(item.Tag.ToString(), out int interval) && interval == Settings.Advanced.AutoBackupIntervalDays)
|
||||
{
|
||||
ComboBoxAutoBackupInterval.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Settings.Advanced.IsEnableFullScreenHelper)
|
||||
{
|
||||
FullScreenHelper.MarkFullscreenWindowTaskbarList(new WindowInteropHelper(this).Handle, true);
|
||||
@@ -712,6 +884,14 @@ namespace Ink_Canvas
|
||||
if (Settings.Advanced.IsEnableAvoidFullScreenHelper)
|
||||
{
|
||||
AvoidFullScreenHelper.StartAvoidFullScreen(this);
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (isLoaded)
|
||||
{
|
||||
MoveWindow(new WindowInteropHelper(this).Handle, 0, 0,
|
||||
WinForms.Screen.PrimaryScreen.Bounds.Width, WinForms.Screen.PrimaryScreen.Bounds.Height, true);
|
||||
}
|
||||
}), DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
if (Settings.Advanced.IsEnableEdgeGestureUtil)
|
||||
{
|
||||
@@ -757,10 +937,36 @@ namespace Ink_Canvas
|
||||
RandWindowOnceCloseLatencySlider.Value = Settings.RandSettings.RandWindowOnceCloseLatency;
|
||||
RandWindowOnceMaxStudentsSlider.Value = Settings.RandSettings.RandWindowOnceMaxStudents;
|
||||
ToggleSwitchShowRandomAndSingleDraw.IsOn = Settings.RandSettings.ShowRandomAndSingleDraw;
|
||||
ToggleSwitchDirectCallCiRand.IsOn = Settings.RandSettings.DirectCallCiRand;
|
||||
ToggleSwitchEnableQuickDraw.IsOn = Settings.RandSettings.EnableQuickDraw;
|
||||
ToggleSwitchExternalCaller.IsOn = Settings.RandSettings.DirectCallCiRand;
|
||||
ComboBoxExternalCallerType.SelectedIndex = Settings.RandSettings.ExternalCallerType;
|
||||
RandomDrawPanel.Visibility = Settings.RandSettings.ShowRandomAndSingleDraw ? Visibility.Visible : Visibility.Collapsed;
|
||||
SingleDrawPanel.Visibility = Settings.RandSettings.ShowRandomAndSingleDraw ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// 计时器设置
|
||||
ToggleSwitchUseLegacyTimerUI.IsOn = Settings.RandSettings.UseLegacyTimerUI;
|
||||
ToggleSwitchUseNewStyleUI.IsOn = Settings.RandSettings.UseNewStyleUI;
|
||||
ToggleSwitchEnableOvertimeCountUp.IsOn = Settings.RandSettings.EnableOvertimeCountUp;
|
||||
|
||||
// 新点名UI设置
|
||||
ToggleSwitchUseNewRollCallUI.IsOn = Settings.RandSettings.UseNewRollCallUI;
|
||||
ToggleSwitchEnableMLAvoidance.IsOn = Settings.RandSettings.EnableMLAvoidance;
|
||||
MLAvoidanceHistorySlider.Value = Settings.RandSettings.MLAvoidanceHistoryCount;
|
||||
MLAvoidanceWeightSlider.Value = Settings.RandSettings.MLAvoidanceWeight;
|
||||
|
||||
bool canEnableRedText = Settings.RandSettings.EnableOvertimeCountUp && Settings.RandSettings.EnableOvertimeRedText;
|
||||
ToggleSwitchEnableOvertimeRedText.IsOn = canEnableRedText;
|
||||
if (!canEnableRedText)
|
||||
{
|
||||
Settings.RandSettings.EnableOvertimeRedText = false;
|
||||
}
|
||||
|
||||
TimerVolumeSlider.Value = Settings.RandSettings.TimerVolume;
|
||||
|
||||
// 渐进提醒设置
|
||||
ToggleSwitchEnableProgressiveReminder.IsOn = Settings.RandSettings.EnableProgressiveReminder;
|
||||
ProgressiveReminderVolumeSlider.Value = Settings.RandSettings.ProgressiveReminderVolume;
|
||||
|
||||
// 加载自定义点名背景
|
||||
UpdatePickNameBackgroundsInComboBox();
|
||||
|
||||
@@ -777,7 +983,44 @@ namespace Ink_Canvas
|
||||
ToggleSwitchDisplayRandWindowNamesInputBtn.IsOn = Settings.RandSettings.DisplayRandWindowNamesInputBtn;
|
||||
RandWindowOnceCloseLatencySlider.Value = Settings.RandSettings.RandWindowOnceCloseLatency;
|
||||
RandWindowOnceMaxStudentsSlider.Value = Settings.RandSettings.RandWindowOnceMaxStudents;
|
||||
ToggleSwitchDirectCallCiRand.IsOn = Settings.RandSettings.DirectCallCiRand;
|
||||
ToggleSwitchEnableQuickDraw.IsOn = Settings.RandSettings.EnableQuickDraw;
|
||||
ToggleSwitchExternalCaller.IsOn = Settings.RandSettings.DirectCallCiRand;
|
||||
ComboBoxExternalCallerType.SelectedIndex = Settings.RandSettings.ExternalCallerType;
|
||||
ToggleSwitchUseLegacyTimerUI.IsOn = Settings.RandSettings.UseLegacyTimerUI;
|
||||
ToggleSwitchUseNewStyleUI.IsOn = Settings.RandSettings.UseNewStyleUI;
|
||||
ToggleSwitchEnableOvertimeCountUp.IsOn = Settings.RandSettings.EnableOvertimeCountUp;
|
||||
|
||||
bool canEnableRedText = Settings.RandSettings.EnableOvertimeCountUp && Settings.RandSettings.EnableOvertimeRedText;
|
||||
ToggleSwitchEnableOvertimeRedText.IsOn = canEnableRedText;
|
||||
if (!canEnableRedText)
|
||||
{
|
||||
Settings.RandSettings.EnableOvertimeRedText = false;
|
||||
}
|
||||
|
||||
TimerVolumeSlider.Value = Settings.RandSettings.TimerVolume;
|
||||
|
||||
// 渐进提醒设置
|
||||
ToggleSwitchEnableProgressiveReminder.IsOn = Settings.RandSettings.EnableProgressiveReminder;
|
||||
ProgressiveReminderVolumeSlider.Value = Settings.RandSettings.ProgressiveReminderVolume;
|
||||
}
|
||||
|
||||
// ModeSettings
|
||||
if (Settings.ModeSettings != null)
|
||||
{
|
||||
ToggleSwitchMode.IsOn = Settings.ModeSettings.IsPPTOnlyMode;
|
||||
|
||||
// 根据加载的配置状态执行相应的窗口显示/隐藏逻辑
|
||||
if (isStartup && Settings.ModeSettings.IsPPTOnlyMode)
|
||||
{
|
||||
// 启动时如果是仅PPT模式,隐藏主窗口
|
||||
Hide();
|
||||
LogHelper.WriteLogToFile("启动时检测到仅PPT模式,主窗口已隐藏", LogHelper.LogType.Event);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings.ModeSettings = new ModeSettings();
|
||||
ToggleSwitchMode.IsOn = false;
|
||||
}
|
||||
|
||||
// Automation
|
||||
@@ -831,6 +1074,8 @@ namespace Ink_Canvas
|
||||
|
||||
ToggleSwitchAutoFoldAfterPPTSlideShow.IsOn = Settings.Automation.IsAutoFoldAfterPPTSlideShow;
|
||||
|
||||
ToggleSwitchKeepFoldAfterSoftwareExit.IsOn = Settings.Automation.KeepFoldAfterSoftwareExit;
|
||||
|
||||
if (Settings.Automation.IsAutoKillEasiNote || Settings.Automation.IsAutoKillPptService ||
|
||||
Settings.Automation.IsAutoKillHiteAnnotation || Settings.Automation.IsAutoKillInkCanvas
|
||||
|| Settings.Automation.IsAutoKillICA || Settings.Automation.IsAutoKillIDT ||
|
||||
@@ -869,6 +1114,25 @@ namespace Ink_Canvas
|
||||
|
||||
ToggleSwitchSaveFullPageStrokes.IsOn = Settings.Automation.IsSaveFullPageStrokes;
|
||||
|
||||
ToggleSwitchSaveStrokesAsXML.IsOn = Settings.Automation.IsSaveStrokesAsXML;
|
||||
|
||||
// 加载定时保存墨迹设置
|
||||
ToggleSwitchEnableAutoSaveStrokes.IsOn = Settings.Automation.IsEnableAutoSaveStrokes;
|
||||
// 初始化保存间隔下拉框
|
||||
if (ComboBoxAutoSaveStrokesInterval != null)
|
||||
{
|
||||
int intervalMinutes = Settings.Automation.AutoSaveStrokesIntervalMinutes;
|
||||
if (intervalMinutes < 1) intervalMinutes = 5; // 默认5分钟
|
||||
foreach (System.Windows.Controls.ComboBoxItem item in ComboBoxAutoSaveStrokesInterval.Items)
|
||||
{
|
||||
if (item.Tag != null && int.TryParse(item.Tag.ToString(), out int tagValue) && tagValue == intervalMinutes)
|
||||
{
|
||||
ComboBoxAutoSaveStrokesInterval.SelectedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SideControlMinimumAutomationSlider.Value = Settings.Automation.MinimumAutomationStrokeNumber;
|
||||
|
||||
AutoSavedStrokesLocation.Text = Settings.Automation.AutoSavedStrokesLocation;
|
||||
@@ -878,6 +1142,9 @@ namespace Ink_Canvas
|
||||
|
||||
// 加载退出收纳模式自动切换至批注模式设置
|
||||
ToggleSwitchAutoEnterAnnotationModeWhenExitFoldMode.IsOn = Settings.Automation.IsAutoEnterAnnotationModeWhenExitFoldMode;
|
||||
|
||||
// 加载退出白板时自动收纳设置
|
||||
ToggleSwitchAutoFoldWhenExitWhiteboard.IsOn = Settings.Automation.IsAutoFoldWhenExitWhiteboard;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -945,5 +1212,102 @@ namespace Ink_Canvas
|
||||
LogHelper.WriteLogToFile($"加载墨迹渐隐设置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <param name="userConfigJson">用户配置的JSON字符串</param>
|
||||
private void CleanupObsoleteSettings(string userConfigJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建默认配置对象
|
||||
Settings defaultSettings = new Settings();
|
||||
|
||||
// 将默认配置和用户配置都序列化为JObject
|
||||
JObject defaultConfigObj = JObject.FromObject(defaultSettings);
|
||||
JObject userConfigObj = JObject.Parse(userConfigJson);
|
||||
|
||||
// 记录是否有清理操作
|
||||
bool hasChanges = false;
|
||||
|
||||
// 递归比较并删除用户配置中多余的键
|
||||
RemoveObsoleteProperties(userConfigObj, defaultConfigObj, ref hasChanges);
|
||||
|
||||
// 如果有清理操作,重新反序列化并保存
|
||||
if (hasChanges)
|
||||
{
|
||||
string cleanedJson = userConfigObj.ToString(Formatting.Indented);
|
||||
Settings = JsonConvert.DeserializeObject<Settings>(cleanedJson);
|
||||
SaveSettingsToFile();
|
||||
LogHelper.WriteLogToFile("已清理过期配置项", LogHelper.LogType.Event);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理过期配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <param name="userObj">用户配置的JObject</param>
|
||||
/// <param name="defaultObj">默认配置的JObject</param>
|
||||
/// <param name="hasChanges">是否有变更的引用标志</param>
|
||||
private void RemoveObsoleteProperties(JObject userObj, JObject defaultObj, ref bool hasChanges)
|
||||
{
|
||||
if (userObj == null || defaultObj == null)
|
||||
return;
|
||||
|
||||
// 获取需要删除的键列表(避免在遍历时修改集合)
|
||||
List<string> keysToRemove = new List<string>();
|
||||
|
||||
foreach (var property in userObj.Properties())
|
||||
{
|
||||
string propertyName = property.Name;
|
||||
|
||||
// 如果默认配置中不存在该属性,标记为删除
|
||||
if (!defaultObj.ContainsKey(propertyName))
|
||||
{
|
||||
keysToRemove.Add(propertyName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果两个属性都是对象类型,递归比较
|
||||
JToken userValue = property.Value;
|
||||
JToken defaultValue = defaultObj[propertyName];
|
||||
|
||||
if (userValue != null && defaultValue != null)
|
||||
{
|
||||
if (userValue.Type == JTokenType.Object && defaultValue.Type == JTokenType.Object)
|
||||
{
|
||||
RemoveObsoleteProperties(userValue as JObject, defaultValue as JObject, ref hasChanges);
|
||||
}
|
||||
// 处理数组中的对象(如自定义图标列表等)
|
||||
else if (userValue.Type == JTokenType.Array && defaultValue.Type == JTokenType.Array)
|
||||
{
|
||||
JArray userArray = userValue as JArray;
|
||||
JArray defaultArray = defaultValue as JArray;
|
||||
|
||||
if (userArray != null && defaultArray != null && userArray.Count > 0 && defaultArray.Count > 0)
|
||||
{
|
||||
// 如果数组元素是对象,比较第一个元素的属性结构
|
||||
if (userArray[0].Type == JTokenType.Object && defaultArray[0].Type == JTokenType.Object)
|
||||
{
|
||||
for (int i = 0; i < userArray.Count; i++)
|
||||
{
|
||||
if (userArray[i] is JObject userItemObj && defaultArray[0] is JObject defaultItemObj)
|
||||
{
|
||||
RemoveObsoleteProperties(userItemObj, defaultItemObj, ref hasChanges);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除标记的键
|
||||
foreach (string key in keysToRemove)
|
||||
{
|
||||
userObj.Remove(key);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
@@ -6,7 +7,6 @@ using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Ink_Canvas.Helpers;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -193,13 +193,13 @@ namespace Ink_Canvas
|
||||
// 检查图片是否有位置信息,如果没有则应用居中
|
||||
double left = InkCanvas.GetLeft(img);
|
||||
double top = InkCanvas.GetTop(img);
|
||||
|
||||
|
||||
if (double.IsNaN(left) || double.IsNaN(top))
|
||||
{
|
||||
// 图片没有位置信息,应用居中
|
||||
CenterAndScaleElement(img);
|
||||
}
|
||||
|
||||
|
||||
// 重新绑定事件处理器
|
||||
BindElementEvents(img);
|
||||
}
|
||||
@@ -208,13 +208,13 @@ namespace Ink_Canvas
|
||||
// 检查媒体元素是否有位置信息,如果没有则应用居中
|
||||
double left = InkCanvas.GetLeft(media);
|
||||
double top = InkCanvas.GetTop(media);
|
||||
|
||||
|
||||
if (double.IsNaN(left) || double.IsNaN(top))
|
||||
{
|
||||
// 媒体元素没有位置信息,应用居中
|
||||
CenterAndScaleElement(media);
|
||||
}
|
||||
|
||||
|
||||
// 重新绑定事件处理器
|
||||
BindElementEvents(media);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@@ -11,7 +12,7 @@ using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -61,12 +62,22 @@ namespace Ink_Canvas
|
||||
private Timer timerCheckAutoFold = new Timer();
|
||||
private string AvailableLatestVersion;
|
||||
private Timer timerCheckAutoUpdateWithSilence = new Timer();
|
||||
private Timer timerCheckAutoUpdateRetry = new Timer();
|
||||
private bool isHidingSubPanelsWhenInking; // 避免书写时触发二次关闭二级菜单导致动画不连续
|
||||
|
||||
private int updateCheckRetryCount = 0;
|
||||
private const int MAX_UPDATE_CHECK_RETRIES = 6;
|
||||
private Timer timerDisplayTime = new Timer();
|
||||
private Timer timerDisplayDate = new Timer();
|
||||
private Timer timerNtpSync = new Timer();
|
||||
|
||||
private TimeViewModel nowTimeVM = new TimeViewModel();
|
||||
private DateTime cachedNetworkTime = DateTime.Now;
|
||||
private DateTime lastNtpSyncTime = DateTime.MinValue;
|
||||
private string lastDisplayedTime = "";
|
||||
private bool useNetworkTime = false;
|
||||
private TimeSpan networkTimeOffset = TimeSpan.Zero;
|
||||
private DateTime lastLocalTime = DateTime.Now; // 记录上次的本地时间,用于检测时间跳跃
|
||||
private bool isNtpSyncing = false; // 防止重复NTP同步的标志
|
||||
|
||||
private async Task<DateTime> GetNetworkTimeAsync()
|
||||
{
|
||||
@@ -79,7 +90,7 @@ namespace Ink_Canvas
|
||||
var ipEndPoint = new IPEndPoint(addresses[0], 123);
|
||||
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
|
||||
{
|
||||
socket.ReceiveTimeout = 2000;
|
||||
socket.ReceiveTimeout = 5000;
|
||||
socket.Connect(ipEndPoint);
|
||||
await Task.Factory.FromAsync(socket.BeginSend(ntpData, 0, ntpData.Length, SocketFlags.None, null, socket), socket.EndSend);
|
||||
await Task.Factory.FromAsync(socket.BeginReceive(ntpData, 0, ntpData.Length, SocketFlags.None, null, socket), socket.EndReceive);
|
||||
@@ -91,7 +102,7 @@ namespace Ink_Canvas
|
||||
var networkDateTime = (new DateTime(1900, 1, 1)).AddMilliseconds((long)milliseconds);
|
||||
return networkDateTime.ToLocalTime();
|
||||
}
|
||||
catch
|
||||
catch (Exception)
|
||||
{
|
||||
return DateTime.Now;
|
||||
}
|
||||
@@ -109,28 +120,189 @@ namespace Ink_Canvas
|
||||
timerCheckAutoFold.Interval = 500;
|
||||
timerCheckAutoUpdateWithSilence.Elapsed += timerCheckAutoUpdateWithSilence_Elapsed;
|
||||
timerCheckAutoUpdateWithSilence.Interval = 1000 * 60 * 10;
|
||||
timerCheckAutoUpdateRetry.Elapsed += timerCheckAutoUpdateRetry_Elapsed;
|
||||
timerCheckAutoUpdateRetry.Interval = 1000 * 60 * 10;
|
||||
WaterMarkTime.DataContext = nowTimeVM;
|
||||
WaterMarkDate.DataContext = nowTimeVM;
|
||||
timerDisplayTime.Elapsed += async (s, e) => await TimerDisplayTime_ElapsedAsync();
|
||||
timerDisplayTime.Elapsed += TimerDisplayTime_Elapsed;
|
||||
timerDisplayTime.Interval = 1000;
|
||||
timerDisplayTime.Start();
|
||||
timerDisplayDate.Elapsed += TimerDisplayDate_Elapsed;
|
||||
timerDisplayDate.Interval = 1000 * 60 * 60 * 1;
|
||||
timerDisplayDate.Start();
|
||||
timerNtpSync.Elapsed += async (s, e) => await TimerNtpSync_ElapsedAsync();
|
||||
timerNtpSync.Interval = 1000 * 60 * 60 * 2; // 每2小时同步一次
|
||||
timerNtpSync.Start();
|
||||
timerKillProcess.Start();
|
||||
nowTimeVM.nowDate = DateTime.Now.ToString("yyyy'年'MM'月'dd'日' dddd");
|
||||
nowTimeVM.nowTime = DateTime.Now.ToString("tt hh'时'mm'分'ss'秒'");
|
||||
|
||||
// 程序启动时立即进行一次NTP同步
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await TimerNtpSync_ElapsedAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"程序启动时NTP同步失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化定时保存墨迹定时器
|
||||
InitAutoSaveStrokesTimer();
|
||||
}
|
||||
|
||||
// 修改TimerDisplayTime_ElapsedAsync方法中的时间格式
|
||||
private async Task TimerDisplayTime_ElapsedAsync()
|
||||
// 初始化定时保存墨迹定时器
|
||||
private void InitAutoSaveStrokesTimer()
|
||||
{
|
||||
DateTime now = await GetNetworkTimeAsync();
|
||||
// 只更新时间,日期由原有逻辑定时更新即可
|
||||
Dispatcher.Invoke(() =>
|
||||
if (autoSaveStrokesTimer == null)
|
||||
{
|
||||
nowTimeVM.nowTime = now.ToString("tt hh'时'mm'分'ss'秒'");
|
||||
});
|
||||
autoSaveStrokesTimer = new DispatcherTimer();
|
||||
autoSaveStrokesTimer.Tick += AutoSaveStrokesTimer_Tick;
|
||||
}
|
||||
|
||||
// 根据设置更新定时器间隔和启动状态
|
||||
UpdateAutoSaveStrokesTimer();
|
||||
}
|
||||
|
||||
// 更新定时保存墨迹定时器状态
|
||||
private void UpdateAutoSaveStrokesTimer()
|
||||
{
|
||||
if (autoSaveStrokesTimer == null) return;
|
||||
|
||||
autoSaveStrokesTimer.Stop();
|
||||
|
||||
if (Settings.Automation.IsEnableAutoSaveStrokes)
|
||||
{
|
||||
int intervalMinutes = Settings.Automation.AutoSaveStrokesIntervalMinutes;
|
||||
if (intervalMinutes < 1) intervalMinutes = 1; // 最小间隔1分钟
|
||||
autoSaveStrokesTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
|
||||
autoSaveStrokesTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
// 定时保存墨迹定时器事件处理
|
||||
private void AutoSaveStrokesTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 只有在画布可见且有墨迹时才保存
|
||||
if (inkCanvas.Visibility == Visibility.Visible && inkCanvas.Strokes.Count > 0)
|
||||
{
|
||||
// 静默保存
|
||||
SaveInkCanvasStrokes(false, false);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// NTP同步定时器事件处理
|
||||
private async Task TimerNtpSync_ElapsedAsync()
|
||||
{
|
||||
// 防止重复同步
|
||||
if (isNtpSyncing) return;
|
||||
|
||||
isNtpSyncing = true;
|
||||
try
|
||||
{
|
||||
|
||||
// 添加超时机制,最多等待10秒
|
||||
var timeoutTask = Task.Delay(10000);
|
||||
var ntpTask = GetNetworkTimeAsync();
|
||||
|
||||
var completedTask = await Task.WhenAny(ntpTask, timeoutTask);
|
||||
|
||||
if (completedTask == timeoutTask)
|
||||
{
|
||||
cachedNetworkTime = DateTime.Now;
|
||||
lastNtpSyncTime = DateTime.Now;
|
||||
useNetworkTime = false;
|
||||
networkTimeOffset = TimeSpan.Zero;
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime networkTime = await ntpTask;
|
||||
DateTime localTime = DateTime.Now;
|
||||
|
||||
cachedNetworkTime = networkTime;
|
||||
lastNtpSyncTime = localTime;
|
||||
|
||||
// 计算网络时间与本地时间的偏移量
|
||||
networkTimeOffset = networkTime - localTime;
|
||||
|
||||
// 如果时间差超过3分钟,则使用网络时间
|
||||
useNetworkTime = Math.Abs(networkTimeOffset.TotalMinutes) > 3.0;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// NTP同步失败时,保持使用本地时间
|
||||
cachedNetworkTime = DateTime.Now;
|
||||
lastNtpSyncTime = DateTime.Now;
|
||||
useNetworkTime = false;
|
||||
networkTimeOffset = TimeSpan.Zero;
|
||||
|
||||
LogHelper.WriteLogToFile($"NTP同步失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isNtpSyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 优化后的时间显示方法,仅在NTP同步时计算网络时间偏移
|
||||
private void TimerDisplayTime_Elapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
DateTime localTime = DateTime.Now;
|
||||
DateTime displayTime = localTime; // 默认使用本地时间
|
||||
|
||||
// 检测系统时间是否发生重大跳跃(超过2分钟)
|
||||
TimeSpan timeJump = localTime - lastLocalTime;
|
||||
double timeJumpMinutes = Math.Abs(timeJump.TotalMinutes);
|
||||
|
||||
if (timeJumpMinutes > 3 && !isNtpSyncing)
|
||||
{
|
||||
// 系统时间发生重大变化(超过3分钟),立即触发NTP同步
|
||||
// 使用异步方式触发NTP同步,避免阻塞主线程
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await TimerNtpSync_ElapsedAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"时间跳跃触发的NTP同步失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
lastLocalTime = localTime;
|
||||
|
||||
// 如果启用网络时间且偏移量已计算,则应用偏移量
|
||||
if (useNetworkTime && networkTimeOffset != TimeSpan.Zero)
|
||||
{
|
||||
displayTime = localTime + networkTimeOffset;
|
||||
}
|
||||
|
||||
// 格式化时间字符串
|
||||
string timeString = displayTime.ToString("tt hh'时'mm'分'ss'秒'");
|
||||
|
||||
|
||||
// 只有当时间字符串发生变化时才更新UI,避免不必要的UI刷新
|
||||
if (timeString != lastDisplayedTime)
|
||||
{
|
||||
lastDisplayedTime = timeString;
|
||||
|
||||
// 使用BeginInvoke异步更新UI,避免阻塞
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
nowTimeVM.nowTime = timeString;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 修改TimerDisplayDate_Elapsed方法中的日期格式
|
||||
@@ -157,6 +329,8 @@ namespace Ink_Canvas
|
||||
{
|
||||
var processes = Process.GetProcessesByName("EasiNote");
|
||||
if (processes.Length > 0) arg += " /IM EasiNote.exe";
|
||||
var seewoStartProcesses = Process.GetProcessesByName("SeewoStart");
|
||||
if (seewoStartProcesses.Length > 0) arg += " /IM SeewoStart.exe";
|
||||
}
|
||||
|
||||
if (Settings.Automation.IsAutoKillHiteAnnotation)
|
||||
@@ -220,8 +394,18 @@ namespace Ink_Canvas
|
||||
ShowNotification("“鸿合屏幕书写”已自动关闭");
|
||||
if (Settings.Automation.IsAutoKillHiteAnnotation && Settings.Automation.IsAutoEnterAnnotationAfterKillHite)
|
||||
{
|
||||
// 自动进入批注状态
|
||||
PenIcon_Click(null, null);
|
||||
// 检查是否处于收纳状态,如果是则先展开浮动栏
|
||||
if (isFloatingBarFolded)
|
||||
{
|
||||
// 先展开浮动栏,然后进入批注状态
|
||||
// UnFoldFloatingBar 方法内部会根据设置自动进入批注模式
|
||||
UnFoldFloatingBar(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果已经展开,直接进入批注状态
|
||||
PenIcon_Click(null, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -274,13 +458,300 @@ namespace Ink_Canvas
|
||||
private bool foldFloatingBarByUser, // 保持收纳操作不受自动收纳的控制
|
||||
unfoldFloatingBarByUser; // 允许用户在希沃软件内进行展开操作
|
||||
|
||||
/// <summary>
|
||||
/// 检测是否为批注窗口(窗口标题为空且高度小于500像素)
|
||||
/// </summary>
|
||||
/// <returns>如果是批注窗口返回true,否则返回false</returns>
|
||||
private bool IsAnnotationWindow()
|
||||
{
|
||||
var windowTitle = ForegroundWindowInfo.WindowTitle();
|
||||
var windowRect = ForegroundWindowInfo.WindowRect();
|
||||
var windowProcessName = ForegroundWindowInfo.ProcessName();
|
||||
|
||||
// 检测希沃白板五的批注面板
|
||||
// 希沃白板五的批注面板通常具有以下特征:
|
||||
// 1. 窗口标题为空或包含特定关键词
|
||||
// 2. 窗口高度较小(批注工具栏)
|
||||
// 3. 窗口宽度适中(工具栏宽度)
|
||||
if (windowProcessName == "BoardService" || windowProcessName == "seewoPincoTeacher")
|
||||
{
|
||||
// 检测希沃白板五的批注工具栏
|
||||
// 批注工具栏通常高度在50-200像素之间,宽度在200-800像素之间
|
||||
if (windowRect.Height >= 50 && windowRect.Height <= 200 &&
|
||||
windowRect.Width >= 200 && windowRect.Width <= 800)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检测希沃白板五的二级菜单面板
|
||||
// 二级菜单面板通常高度在100-400像素之间,宽度在150-400像素之间
|
||||
if (windowRect.Height >= 100 && windowRect.Height <= 400 &&
|
||||
windowRect.Width >= 150 && windowRect.Width <= 400)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检测鸿合软件的批注面板
|
||||
if (windowProcessName == "HiteCamera" || windowProcessName == "HiteTouchPro" || windowProcessName == "HiteLightBoard")
|
||||
{
|
||||
// 鸿合软件的批注面板特征
|
||||
if (windowRect.Height >= 50 && windowRect.Height <= 300 &&
|
||||
windowRect.Width >= 200 && windowRect.Width <= 600)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 原有的检测逻辑(保持向后兼容)
|
||||
return windowTitle.Length == 0 && windowRect.Height < 500;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用窗口预览模型检测前台窗口是否符合自动收纳要求(仅用于检测,不执行任何操作)
|
||||
/// </summary>
|
||||
/// <returns>如果符合自动收纳要求返回true,否则返回false</returns>
|
||||
private bool CheckShouldAutoFoldByWindowPreview()
|
||||
{
|
||||
if (_windowOverviewModel == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
// 从窗口预览模型中获取窗口列表(已按ZOrder排序,最上层在前)
|
||||
var windows = _windowOverviewModel.Windows;
|
||||
if (windows == null || windows.Count == 0) return false;
|
||||
|
||||
// 获取前台窗口(ZOrder最小的窗口,即最上层)
|
||||
var foregroundWindow = windows.FirstOrDefault();
|
||||
if (foregroundWindow == null) return false;
|
||||
|
||||
var windowProcessName = foregroundWindow.ProcessName;
|
||||
var windowTitle = foregroundWindow.Title;
|
||||
var windowRect = foregroundWindow.Rect;
|
||||
|
||||
// 检查EasiNote
|
||||
if (windowProcessName == "EasiNote")
|
||||
{
|
||||
if (foregroundWindow.ProcessPath != "Unknown")
|
||||
{
|
||||
try
|
||||
{
|
||||
var versionInfo = FileVersionInfo.GetVersionInfo(foregroundWindow.ProcessPath);
|
||||
string version = versionInfo.FileVersion;
|
||||
string prodName = versionInfo.ProductName;
|
||||
|
||||
if (version.StartsWith("5.") && Settings.Automation.IsAutoFoldInEasiNote)
|
||||
{
|
||||
bool isAnnotationWindow = windowTitle.Length == 0 && windowRect.Height < 500;
|
||||
if (Settings.Automation.IsAutoFoldInEasiNoteIgnoreDesktopAnno && isAnnotationWindow)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (!isAnnotationWindow)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (version.StartsWith("3.") && Settings.Automation.IsAutoFoldInEasiNote3)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (prodName.Contains("3C") && Settings.Automation.IsAutoFoldInEasiNote3C &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
// 检查EasiCamera
|
||||
else if (Settings.Automation.IsAutoFoldInEasiCamera && windowProcessName == "EasiCamera" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查EasiNote5C
|
||||
else if (Settings.Automation.IsAutoFoldInEasiNote5C && windowProcessName == "EasiNote5C" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查SeewoPinco
|
||||
else if (Settings.Automation.IsAutoFoldInSeewoPincoTeacher &&
|
||||
(windowProcessName == "BoardService" || windowProcessName == "seewoPincoTeacher"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查HiteCamera
|
||||
else if (Settings.Automation.IsAutoFoldInHiteCamera && windowProcessName == "HiteCamera" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查HiteTouchPro
|
||||
else if (Settings.Automation.IsAutoFoldInHiteTouchPro && windowProcessName == "HiteTouchPro" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查WxBoardMain
|
||||
else if (Settings.Automation.IsAutoFoldInWxBoardMain && windowProcessName == "WxBoardMain" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查MSWhiteboard
|
||||
else if (Settings.Automation.IsAutoFoldInMSWhiteboard &&
|
||||
(windowProcessName == "MicrosoftWhiteboard" || windowProcessName == "msedgewebview2"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查OldZyBoard
|
||||
else if (Settings.Automation.IsAutoFoldInOldZyBoard &&
|
||||
(WinTabWindowsChecker.IsWindowExisted("WhiteBoard - DrawingWindow") ||
|
||||
WinTabWindowsChecker.IsWindowExisted("InstantAnnotationWindow")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查HiteLightBoard
|
||||
else if (Settings.Automation.IsAutoFoldInHiteLightBoard && windowProcessName == "HiteLightBoard" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查AdmoxWhiteboard
|
||||
else if (Settings.Automation.IsAutoFoldInAdmoxWhiteboard && windowProcessName == "Amdox.WhiteBoard" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查AdmoxBooth
|
||||
else if (Settings.Automation.IsAutoFoldInAdmoxBooth && windowProcessName == "Amdox.Booth" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查QPoint
|
||||
else if (Settings.Automation.IsAutoFoldInQPoint && windowProcessName == "QPoint" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查YiYunVisualPresenter
|
||||
else if (Settings.Automation.IsAutoFoldInYiYunVisualPresenter && windowProcessName == "YiYunVisualPresenter" &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 检查MaxHubWhiteboard
|
||||
else if (Settings.Automation.IsAutoFoldInMaxHubWhiteboard && windowProcessName == "WhiteBoard" &&
|
||||
WinTabWindowsChecker.IsWindowExisted("白板书写") &&
|
||||
windowRect.Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
windowRect.Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
if (foregroundWindow.ProcessPath != "Unknown")
|
||||
{
|
||||
try
|
||||
{
|
||||
var versionInfo = FileVersionInfo.GetVersionInfo(foregroundWindow.ProcessPath);
|
||||
var version = versionInfo.FileVersion;
|
||||
var prodName = versionInfo.ProductName;
|
||||
if (version.StartsWith("6.") && prodName == "WhiteBoard")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"窗口预览模型检测失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void timerCheckAutoFold_Elapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
if (isFloatingBarChangingHideMode) return;
|
||||
try
|
||||
{
|
||||
bool shouldAutoFold = CheckShouldAutoFoldByWindowPreview();
|
||||
var windowProcessName = ForegroundWindowInfo.ProcessName();
|
||||
var windowTitle = ForegroundWindowInfo.WindowTitle();
|
||||
|
||||
if (shouldAutoFold)
|
||||
{
|
||||
if (windowProcessName == "EasiNote")
|
||||
{
|
||||
if (ForegroundWindowInfo.ProcessPath() != "Unknown")
|
||||
{
|
||||
var versionInfo = FileVersionInfo.GetVersionInfo(ForegroundWindowInfo.ProcessPath());
|
||||
string version = versionInfo.FileVersion;
|
||||
string prodName = versionInfo.ProductName;
|
||||
|
||||
if (version.StartsWith("5.") && Settings.Automation.IsAutoFoldInEasiNote)
|
||||
{
|
||||
bool isAnnotationWindow = windowTitle.Length == 0 && ForegroundWindowInfo.WindowRect().Height < 500;
|
||||
if (Settings.Automation.IsAutoFoldInEasiNoteIgnoreDesktopAnno && isAnnotationWindow)
|
||||
{
|
||||
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
else if (!isAnnotationWindow)
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
}
|
||||
else if (version.StartsWith("3.") && Settings.Automation.IsAutoFoldInEasiNote3)
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
else if (prodName.Contains("3C") && Settings.Automation.IsAutoFoldInEasiNote3C &&
|
||||
ForegroundWindowInfo.WindowRect().Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
ForegroundWindowInfo.WindowRect().Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理其他需要检测批注窗口的情况
|
||||
else if ((Settings.Automation.IsAutoFoldInEasiCamera && windowProcessName == "EasiCamera") ||
|
||||
(Settings.Automation.IsAutoFoldInSeewoPincoTeacher && (windowProcessName == "BoardService" || windowProcessName == "seewoPincoTeacher")) ||
|
||||
(Settings.Automation.IsAutoFoldInHiteCamera && windowProcessName == "HiteCamera") ||
|
||||
(Settings.Automation.IsAutoFoldInHiteTouchPro && windowProcessName == "HiteTouchPro") ||
|
||||
(Settings.Automation.IsAutoFoldInHiteLightBoard && windowProcessName == "HiteLightBoard"))
|
||||
{
|
||||
if (IsAnnotationWindow())
|
||||
{
|
||||
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
}
|
||||
// 处理其他普通情况
|
||||
else
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//LogHelper.WriteLogToFile("windowTitle | " + windowTitle + " | windowProcessName | " + windowProcessName);
|
||||
|
||||
if (windowProcessName == "EasiNote")
|
||||
@@ -294,10 +765,22 @@ namespace Ink_Canvas
|
||||
Trace.WriteLine(ForegroundWindowInfo.ProcessPath());
|
||||
Trace.WriteLine(version);
|
||||
Trace.WriteLine(prodName);
|
||||
if (version.StartsWith("5.") && Settings.Automation.IsAutoFoldInEasiNote && (!(windowTitle.Length == 0 && ForegroundWindowInfo.WindowRect().Height < 500) ||
|
||||
!Settings.Automation.IsAutoFoldInEasiNoteIgnoreDesktopAnno))
|
||||
if (version.StartsWith("5.") && Settings.Automation.IsAutoFoldInEasiNote)
|
||||
{ // EasiNote5
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
// 检查是否是桌面批注窗口
|
||||
bool isAnnotationWindow = windowTitle.Length == 0 && ForegroundWindowInfo.WindowRect().Height < 500;
|
||||
|
||||
// 如果启用了忽略桌面批注窗口功能,且当前是批注窗口
|
||||
if (Settings.Automation.IsAutoFoldInEasiNoteIgnoreDesktopAnno && isAnnotationWindow)
|
||||
{
|
||||
// 强制保持收纳状态
|
||||
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
else if (!isAnnotationWindow)
|
||||
{
|
||||
// 非批注窗口时正常收纳
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
}
|
||||
else if (version.StartsWith("3.") && Settings.Automation.IsAutoFoldInEasiNote3)
|
||||
{ // EasiNote3
|
||||
@@ -316,7 +799,17 @@ namespace Ink_Canvas
|
||||
ForegroundWindowInfo.WindowRect().Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
ForegroundWindowInfo.WindowRect().Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
// 检测到批注窗口时保持收纳状态
|
||||
if (IsAnnotationWindow())
|
||||
{
|
||||
// 批注窗口打开时,如果当前是展开状态则收纳
|
||||
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非批注窗口时正常处理
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
// EasiNote5C
|
||||
}
|
||||
else if (Settings.Automation.IsAutoFoldInEasiNote5C && windowProcessName == "EasiNote5C" &&
|
||||
@@ -328,21 +821,51 @@ namespace Ink_Canvas
|
||||
}
|
||||
else if (Settings.Automation.IsAutoFoldInSeewoPincoTeacher && (windowProcessName == "BoardService" || windowProcessName == "seewoPincoTeacher"))
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
// 检测到希沃白板五的批注窗口时保持收纳状态
|
||||
if (IsAnnotationWindow())
|
||||
{
|
||||
// 批注窗口打开时,如果当前是展开状态则收纳
|
||||
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非批注窗口时正常处理
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
// HiteCamera
|
||||
}
|
||||
else if (Settings.Automation.IsAutoFoldInHiteCamera && windowProcessName == "HiteCamera" &&
|
||||
ForegroundWindowInfo.WindowRect().Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
ForegroundWindowInfo.WindowRect().Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
// 检测到批注窗口时保持收纳状态
|
||||
if (IsAnnotationWindow())
|
||||
{
|
||||
// 批注窗口打开时,如果当前是展开状态则收纳
|
||||
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非批注窗口时正常处理
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
// HiteTouchPro
|
||||
}
|
||||
else if (Settings.Automation.IsAutoFoldInHiteTouchPro && windowProcessName == "HiteTouchPro" &&
|
||||
ForegroundWindowInfo.WindowRect().Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
ForegroundWindowInfo.WindowRect().Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
// 检测到批注窗口时保持收纳状态
|
||||
if (IsAnnotationWindow())
|
||||
{
|
||||
// 批注窗口打开时,如果当前是展开状态则收纳
|
||||
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非批注窗口时正常处理
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
// WxBoardMain
|
||||
}
|
||||
else if (Settings.Automation.IsAutoFoldInWxBoardMain && windowProcessName == "WxBoardMain" &&
|
||||
@@ -369,7 +892,17 @@ namespace Ink_Canvas
|
||||
ForegroundWindowInfo.WindowRect().Height >= SystemParameters.WorkArea.Height - 16 &&
|
||||
ForegroundWindowInfo.WindowRect().Width >= SystemParameters.WorkArea.Width - 16)
|
||||
{
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
// 检测到批注窗口时保持收纳状态
|
||||
if (IsAnnotationWindow())
|
||||
{
|
||||
// 批注窗口打开时,如果当前是展开状态则收纳
|
||||
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非批注窗口时正常处理
|
||||
if (!unfoldFloatingBarByUser && !isFloatingBarFolded) FoldFloatingBar_MouseUp(null, null);
|
||||
}
|
||||
// AdmoxWhiteboard
|
||||
}
|
||||
else if (Settings.Automation.IsAutoFoldInAdmoxWhiteboard && windowProcessName == "Amdox.WhiteBoard" &&
|
||||
@@ -420,8 +953,18 @@ namespace Ink_Canvas
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isFloatingBarFolded && !foldFloatingBarByUser) UnFoldFloatingBar_MouseUp(new object(), null);
|
||||
unfoldFloatingBarByUser = false;
|
||||
// 检查是否启用了软件退出后保持收纳模式
|
||||
if (Settings.Automation.KeepFoldAfterSoftwareExit)
|
||||
{
|
||||
// 如果启用了保持收纳模式,则不自动展开浮动栏
|
||||
unfoldFloatingBarByUser = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 原有的逻辑:软件退出后自动展开浮动栏
|
||||
if (isFloatingBarFolded && !foldFloatingBarByUser) UnFoldFloatingBar_MouseUp(new object(), null);
|
||||
unfoldFloatingBarByUser = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
@@ -579,5 +1122,92 @@ namespace Ink_Canvas
|
||||
timerCheckAutoUpdateWithSilence.Start();
|
||||
}
|
||||
}
|
||||
|
||||
// 检查更新失败重试定时器事件处理
|
||||
private async void timerCheckAutoUpdateRetry_Elapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
// 停止定时器,避免重复触发
|
||||
timerCheckAutoUpdateRetry.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
// 检查是否启用了自动更新
|
||||
if (!Settings.Startup.IsAutoUpdate)
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | Auto update is disabled, stopping retry timer");
|
||||
return;
|
||||
}
|
||||
|
||||
// 增加重试计数
|
||||
updateCheckRetryCount++;
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | Retry check attempt {updateCheckRetryCount}/{MAX_UPDATE_CHECK_RETRIES}");
|
||||
|
||||
// 检查是否超过最大重试次数
|
||||
if (updateCheckRetryCount > MAX_UPDATE_CHECK_RETRIES)
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | Maximum retry attempts reached, stopping retry timer", LogHelper.LogType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行更新检查
|
||||
LogHelper.WriteLogToFile("AutoUpdate | Retrying update check after failure");
|
||||
|
||||
// 清除之前的更新状态
|
||||
AvailableLatestVersion = null;
|
||||
AvailableLatestLineGroup = null;
|
||||
|
||||
// 使用当前选择的更新通道检查更新
|
||||
var (remoteVersion, lineGroup, apiReleaseNotes) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel);
|
||||
AvailableLatestVersion = remoteVersion;
|
||||
AvailableLatestLineGroup = lineGroup;
|
||||
|
||||
if (AvailableLatestVersion != null)
|
||||
{
|
||||
// 检查更新成功,重置重试计数
|
||||
updateCheckRetryCount = 0;
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | Retry successful, found new version: {AvailableLatestVersion}");
|
||||
|
||||
// 停止重试定时器,因为已经找到了更新
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 检查更新仍然失败,继续重试
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | Retry {updateCheckRetryCount} failed, will retry in 10 minutes");
|
||||
|
||||
// 重新启动定时器,10分钟后再次尝试
|
||||
timerCheckAutoUpdateRetry.Start();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | Error in retry check: {ex.Message}", LogHelper.LogType.Error);
|
||||
|
||||
// 出错时也重新启动定时器,稍后再检查
|
||||
if (updateCheckRetryCount <= MAX_UPDATE_CHECK_RETRIES)
|
||||
{
|
||||
timerCheckAutoUpdateRetry.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置更新检查重试状态
|
||||
public void ResetUpdateCheckRetry()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 停止重试定时器
|
||||
timerCheckAutoUpdateRetry.Stop();
|
||||
|
||||
// 重置重试计数
|
||||
updateCheckRetryCount = 0;
|
||||
|
||||
LogHelper.WriteLogToFile("AutoUpdate | Update check retry state reset");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | Error resetting retry state: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Interop;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using Application = System.Windows.Application;
|
||||
using ContextMenu = System.Windows.Controls.ContextMenu;
|
||||
using MenuItem = System.Windows.Controls.MenuItem;
|
||||
@@ -30,6 +30,12 @@ namespace Ink_Canvas
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
if (mainWin.IsLoaded)
|
||||
{
|
||||
// 在无焦点模式下,暂时取消主窗口置顶,让系统菜单能够正常显示
|
||||
if (Ink_Canvas.MainWindow.Settings.Advanced.IsAlwaysOnTop && Ink_Canvas.MainWindow.Settings.Advanced.IsNoFocusMode)
|
||||
{
|
||||
mainWin.Topmost = false;
|
||||
}
|
||||
|
||||
// 判斷是否在收納模式中
|
||||
if (mainWin.isFloatingBarFolded)
|
||||
{
|
||||
@@ -57,6 +63,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
private void SysTrayMenu_Closed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
if (mainWin.IsLoaded)
|
||||
{
|
||||
// 菜单关闭后,恢复主窗口的置顶状态
|
||||
if (Ink_Canvas.MainWindow.Settings.Advanced.IsAlwaysOnTop && Ink_Canvas.MainWindow.Settings.Advanced.IsNoFocusMode)
|
||||
{
|
||||
mainWin.Topmost = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseAppTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
@@ -182,5 +201,60 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableAllHotkeysMenuItem_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mainWin = (MainWindow)Current.MainWindow;
|
||||
if (mainWin.IsLoaded)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取全局快捷键管理器
|
||||
var hotkeyManagerField = typeof(MainWindow).GetField("_globalHotkeyManager",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var hotkeyManager = hotkeyManagerField?.GetValue(mainWin) as GlobalHotkeyManager;
|
||||
|
||||
if (hotkeyManager != null)
|
||||
{
|
||||
// 禁用所有快捷键
|
||||
hotkeyManager.DisableHotkeyRegistration();
|
||||
|
||||
// 更新菜单项文本和状态
|
||||
var menuItem = sender as MenuItem;
|
||||
if (menuItem != null)
|
||||
{
|
||||
var headerPanel = menuItem.Header as SimpleStackPanel;
|
||||
if (headerPanel != null)
|
||||
{
|
||||
var textBlock = headerPanel.Children[0] as TextBlock;
|
||||
if (textBlock != null)
|
||||
{
|
||||
if (textBlock.Text == "禁用所有快捷键")
|
||||
{
|
||||
textBlock.Text = "启用所有快捷键";
|
||||
LogHelper.WriteLogToFile("已禁用所有快捷键", LogHelper.LogType.Event);
|
||||
}
|
||||
else
|
||||
{
|
||||
textBlock.Text = "禁用所有快捷键";
|
||||
// 重新启用快捷键
|
||||
hotkeyManager.EnableHotkeyRegistration();
|
||||
LogHelper.WriteLogToFile("已启用所有快捷键", LogHelper.LogType.Event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile("无法获取全局快捷键管理器", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"禁用/启用快捷键时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using System.Windows;
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("CJK_mkp")]
|
||||
[assembly: AssemblyProduct("InkCanvasForClass")]
|
||||
[assembly: AssemblyCopyright("Copyright © HARKOTEK Studio 2024")]
|
||||
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025-2026")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
@@ -49,5 +49,5 @@ using System.Windows;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.7.9.0")]
|
||||
[assembly: AssemblyFileVersion("1.7.9.0")]
|
||||
[assembly: AssemblyVersion("1.7.18.4")]
|
||||
[assembly: AssemblyFileVersion("1.7.18.4")]
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>dist\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
</Project>
|
||||
@@ -74,5 +74,14 @@ namespace Ink_Canvas.Properties {
|
||||
return ResourceManager.GetStream("TimerDownNotice", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.IO.UnmanagedMemoryStream similar to System.IO.MemoryStream.
|
||||
/// </summary>
|
||||
internal static UnmanagedMemoryStream ProgressiveAudio {
|
||||
get {
|
||||
return ResourceManager.GetStream("ProgressiveAudio", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,4 +121,7 @@
|
||||
<data name="TimerDownNotice" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\TimerDownNotice.wav;System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
<data name="ProgressiveAudio" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\ProgressiveAudio.wav;System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
</root>
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |