Compare commits
367 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8383002b5c | |||
| 8c37b91b5b | |||
| 5a387eef96 | |||
| c27759189d | |||
| a33a399989 | |||
| 6c7c76958f | |||
| 6980abe331 | |||
| 5fc92cdd10 | |||
| 4bcc39a7ae | |||
| a044a8bc21 | |||
| f15a293a69 | |||
| 448a695503 | |||
| 3915893ee6 | |||
| 914e6eea2e | |||
| 3434ab6227 | |||
| 90ba3f7fa6 | |||
| f05bcf6cc8 | |||
| 21d5ee25ea | |||
| 13c73fbfe4 | |||
| 97dbedfed5 | |||
| e5ef1c6472 | |||
| 17945a9298 | |||
| 17c0ecc0f5 | |||
| 8c2fc15d81 | |||
| ac399773d0 | |||
| 199674feca | |||
| cb6af3e21f | |||
| efe0bb6ae2 | |||
| 4a1403eed3 | |||
| 77a7628453 | |||
| 21186d1edb | |||
| 1cb77b4f48 | |||
| a6f92a883c | |||
| 2c0b09a9ad | |||
| 003fffca2d | |||
| 28b719e9db | |||
| 4d8fbef455 | |||
| e55f8e8ea1 | |||
| 8ad987a64b | |||
| b0aff1e378 | |||
| 6239ac0b88 | |||
| 875fa9c6f0 | |||
| a7b020b0ff | |||
| b614517728 | |||
| 329e9bd933 | |||
| 73f27a9423 | |||
| ffee3aa564 | |||
| 9268ebb580 | |||
| 40fc4e89e0 | |||
| 6fd4a4f036 | |||
| 483991b85c | |||
| a0d24ea6cc | |||
| 9613285513 | |||
| 649a939a57 | |||
| f20e360c0b | |||
| c1e599971e | |||
| 21c93d38c7 | |||
| 4902559cfa | |||
| 49b22dc184 | |||
| f1f75ff015 | |||
| 6e68fa9cfc | |||
| c68596b91e | |||
| 7d52573595 | |||
| 6ac34ba8aa | |||
| 9136f1dbe3 | |||
| 98cfdb53c3 | |||
| e17387c99e | |||
| d30ed9e726 | |||
| 988af60a30 | |||
| 8b53456b5d | |||
| d23193527e | |||
| b58376847e | |||
| d0551db70a | |||
| e416151a56 | |||
| b02b496b88 | |||
| e31dbbcedc | |||
| 09ad1bac86 | |||
| a2ac761f70 | |||
| 9e5ad7e2b4 | |||
| 4f015fb155 | |||
| b4526473fa | |||
| 3ab9fcc857 | |||
| 7ef231a4c7 | |||
| da3a1f7dea | |||
| 81001542b8 | |||
| aacf5b696c | |||
| 062c0feb65 | |||
| 92f97eab78 | |||
| 31120eab09 | |||
| 9b7d718f78 | |||
| 9132c9eb14 | |||
| adebf8ec38 | |||
| 854e14f9b0 | |||
| d7faac257c | |||
| 1b3c5294d9 | |||
| f7802ba4d0 | |||
| 8e84dce73f | |||
| 620da0e645 | |||
| c4657ebb86 | |||
| 24cbaf69ea | |||
| f3ef2f7aec | |||
| 77dff81217 | |||
| 004364c3a9 | |||
| 877d702978 | |||
| a8bc9f442b | |||
| f09f05ab3c | |||
| ef5377f85c | |||
| 5dbdd7bf12 | |||
| d73e3eb0ea | |||
| b7d03d694c | |||
| b7d4983af5 | |||
| 919cf8a736 | |||
| 8a7d2ddf30 | |||
| b1640f44c2 | |||
| 6e61538dec | |||
| f789a919e1 | |||
| b9651240df | |||
| 5266368f79 | |||
| 42854ff924 | |||
| 48b0e09278 | |||
| f05062f902 | |||
| 532aa03c56 | |||
| b891cb6fe3 | |||
| 0683779e09 | |||
| b949b1651a | |||
| 259ce3dd11 | |||
| cab703b98e | |||
| a57e0496ee | |||
| df66a49ba5 | |||
| 245a29f797 | |||
| 4e8f574fdb | |||
| a5d6135f0c | |||
| 12c7fb1713 | |||
| 4dd56a4e5d | |||
| abe5992d21 | |||
| 8d74b6ee30 | |||
| 8b5797ac66 | |||
| 231b850f74 | |||
| 7003bb8426 | |||
| c4b5be783e | |||
| c38829f7d8 | |||
| fdf3633d8c | |||
| 3916476af7 | |||
| edc94547ab | |||
| 8a8cf9a679 | |||
| 21f3b99838 | |||
| 3bfa0b1d85 | |||
| de55fd43a0 | |||
| 6e68e53b1e | |||
| 4b8d89854e | |||
| 8298f7d5bb | |||
| 816153a1db | |||
| 1e8ddf4754 | |||
| d73e87b980 | |||
| b64b0a3618 | |||
| d52ae90b56 | |||
| 0045f97569 | |||
| 41be1e901d | |||
| 0a14d96e10 | |||
| 57c3fe358b | |||
| c255db6ff3 | |||
| f6484f759e | |||
| b16636d06b | |||
| b50049c822 | |||
| 220a316d70 | |||
| dbac80509a | |||
| bb82eb4891 | |||
| c1d584e3e7 | |||
| f776dac04d | |||
| b56f99fff9 | |||
| 4151dfaf1f | |||
| 836e0b0fde | |||
| 6e7a0e36f4 | |||
| a7fe7937a5 | |||
| dba59b92ac | |||
| 0d6ffa6de6 | |||
| 9edf5ee36c | |||
| 585b712c4c | |||
| d6f20a365e | |||
| f7befe387c | |||
| b1a34b7a64 | |||
| d556cf7741 | |||
| 76a046cca6 | |||
| d6385aea1c | |||
| 31bf2b5af1 | |||
| de613c8a4b | |||
| 84b626d344 | |||
| 2ca7d2a337 | |||
| ea03e8e7c7 | |||
| 778894482f | |||
| 1035f7ef92 | |||
| 6e4f8bc982 | |||
| 36e47cec43 | |||
| 8b2bc352a6 | |||
| 256b3c0887 | |||
| 77cf91e802 | |||
| ea55eb1738 | |||
| 302ef307fe | |||
| 3f3a10de7d | |||
| c73123cd23 | |||
| 73935e0f22 | |||
| 0dfe835eef | |||
| 1ec07421b5 | |||
| 441e600b5d | |||
| bd4b4bd233 | |||
| f0343c98b5 | |||
| 36ff945384 | |||
| 057fb35d00 | |||
| bcbece5bd6 | |||
| 1b01e4a5c5 | |||
| 5aad206e0d | |||
| 74344c4782 | |||
| f280358f56 | |||
| 243201502b | |||
| 8c090218d1 | |||
| 6ca1c598d4 | |||
| 5eff424b2d | |||
| 0c078ef863 | |||
| fea6576dfb | |||
| 998d829783 | |||
| 226a6942dc | |||
| 52a3d184c7 | |||
| 44cdab063c | |||
| 24bc2cf138 | |||
| e6b707ea51 | |||
| 6183011952 | |||
| d80a59556e | |||
| ebbe018bae | |||
| bad05f77b5 | |||
| ea375a9ce6 | |||
| d3382d7856 | |||
| acf0c17d7a | |||
| 83568c8b33 | |||
| cc80529498 | |||
| 002970921d | |||
| ea74592e89 | |||
| fa38d3d664 | |||
| cffedb8cb7 | |||
| c77beb662e | |||
| 77dd83c2d6 | |||
| ceb99cea2b | |||
| 04a8224484 | |||
| 2f54e9d7b3 | |||
| 1165e5bbf2 | |||
| 24c6ca60a3 | |||
| 8f3b65c6d4 | |||
| 4835e3db50 | |||
| 7a30c93ff5 | |||
| 1fca17d557 | |||
| 67a70fd6e4 | |||
| 2e8660de26 | |||
| 069a478559 | |||
| c70d8e1c4e | |||
| 379d514bb5 | |||
| 25dc6a00d3 | |||
| eb2f65e6e5 | |||
| 16c86cd02d | |||
| f5a657d5c3 | |||
| dfc23b4428 | |||
| b62055e705 | |||
| 3190211f9a | |||
| e138f600a1 | |||
| 2173b82fb4 | |||
| ceeb9bffba | |||
| ba70d67c89 | |||
| 74341cc162 | |||
| fc4a3a1194 | |||
| c33ac03255 | |||
| 66afe271c5 | |||
| 9279783fc3 | |||
| 3cf1ea438b | |||
| 277e46030d | |||
| af19ffb736 | |||
| 1f00962c47 | |||
| 742a62b6ff | |||
| 140e92eeda | |||
| 34c2dab82a | |||
| fa2366c373 | |||
| e2d898df14 | |||
| cacc67b11d | |||
| aef860a8cc | |||
| c890f95092 | |||
| 7c9a5f8265 | |||
| 7eafcb02cb | |||
| d4517d3c53 | |||
| 62a9a097aa | |||
| e7fa1caf6c | |||
| e347443a0a | |||
| b77e540863 | |||
| df4168e8fa | |||
| 576f86ce48 | |||
| c3335c0c24 | |||
| 26f79da2f9 | |||
| 11cb1815ad | |||
| a6fdb07e8b | |||
| ffdb3cd431 | |||
| 5f9b01ef04 | |||
| ca53624149 | |||
| 6484450ad3 | |||
| 7c97b683ea | |||
| 378b3e73f2 | |||
| a2f21357b6 | |||
| 79e03dc0d5 | |||
| e0f35450e1 | |||
| cc9f58fb6a | |||
| e8be85141b | |||
| 1bf57ea2f5 | |||
| 18b737b22b | |||
| 4ec3332808 | |||
| b2290ecf65 | |||
| 55c336daab | |||
| f2ed3f619c | |||
| 114f400bd9 | |||
| 506d1118b2 | |||
| 20441543f0 | |||
| 2dbc47ac3c | |||
| bbe99649cd | |||
| 04806d2004 | |||
| 759a635026 | |||
| e68bd9286f | |||
| a6b31e82c0 | |||
| bc3e37e541 | |||
| 1124bb6bfa | |||
| b19c19d73c | |||
| feb3fad4da | |||
| 3db745d684 | |||
| b4089ae62e | |||
| 97bdf78b08 | |||
| dc9fb26260 | |||
| 97b0972fdf | |||
| ea23145349 | |||
| f7013196f7 | |||
| 9d0baa0799 | |||
| 91c2fa4eee | |||
| 837311b2b7 | |||
| bc53eab669 | |||
| 55eb811193 | |||
| f7aa107a62 | |||
| 56a65af9a7 | |||
| de3f5d16a2 | |||
| d325a58f17 | |||
| fd137ae787 | |||
| bb1b893961 | |||
| 01c247ac29 | |||
| 1fa75640ee | |||
| 6e0191af6b | |||
| 9ac4070e4e | |||
| 2590ea5bdb | |||
| 290e031f77 | |||
| 9bc9af5eec | |||
| 8faffe9d4e | |||
| 95dfad64ce | |||
| c9dd98fa8d | |||
| 3ae99c82cc | |||
| 016abafee4 | |||
| dca606cf5f | |||
| e7c77b173d | |||
| 68b588ee7b | |||
| b4c753dfb4 | |||
| 037bcf6007 | |||
| b2e0e7b9e2 | |||
| 03c1399768 | |||
| 9b3a59f089 | |||
| e8f824a046 | |||
| 4772b9cce7 | |||
| 74b64bd21f | |||
| 3934270ed2 |
+17
-4
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/all-contributors.json",
|
||||
"projectName": "community",
|
||||
"projectOwner": "InkCanvasForClass",
|
||||
"files": [
|
||||
@@ -6,7 +7,7 @@
|
||||
],
|
||||
"commitType": "docs",
|
||||
"commitConvention": "angular",
|
||||
"contributorsPerLine": 7,
|
||||
"contributorsPerLine": 5,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "CJKmkp",
|
||||
@@ -100,7 +101,8 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/156585442?v=4",
|
||||
"profile": "https://github.com/Tayasui-rainnya",
|
||||
"contributions": [
|
||||
"design"
|
||||
"design",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -110,7 +112,8 @@
|
||||
"profile": "https://github.com/doudou0720",
|
||||
"contributions": [
|
||||
"code",
|
||||
"blog"
|
||||
"blog",
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -140,6 +143,16 @@
|
||||
"infra",
|
||||
"blog"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Hao3288",
|
||||
"name": "NoobHao",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/119276078?v=4",
|
||||
"profile": "https://github.com/Hao3288",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"repoType": "github"
|
||||
}
|
||||
|
||||
@@ -2,12 +2,7 @@ name: .NET Build & Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, beta ]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened ]
|
||||
branches: [ main, beta ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
branches: [ net6 ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -21,6 +16,10 @@ jobs:
|
||||
build-and-package:
|
||||
name: Build & Package
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture: [AnyCPU, x86]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
@@ -28,7 +27,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5
|
||||
@@ -42,12 +41,12 @@ jobs:
|
||||
- name: Build the Solution
|
||||
env:
|
||||
DLASS_SENTRY_DSN: ${{ secrets.DLASS_SENTRY_DSN }}
|
||||
run: msbuild /p:platform="AnyCPU" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
run: msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
|
||||
- name: Check if exe file is generated
|
||||
id: check-exe
|
||||
run: |
|
||||
$exePath = "Ink Canvas\bin\Debug\net472\InkCanvasForClass.exe"
|
||||
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
|
||||
|
||||
if (Test-Path $exePath) {
|
||||
echo "build_success=true" >> $env:GITHUB_OUTPUT
|
||||
@@ -74,8 +73,8 @@ jobs:
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: InkCanvasForClass.CE.debug
|
||||
path: "Ink Canvas/bin/Debug/net472/*"
|
||||
name: InkCanvasForClass.CE.debug.${{ matrix.architecture }}
|
||||
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
|
||||
|
||||
- name: Create Summary
|
||||
if: always()
|
||||
@@ -90,15 +89,17 @@ jobs:
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Download Artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.zip)\)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip)\)" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "## ❌ Build Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
name: PR Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
branches: [ main, net6 ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.head_ref || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-package:
|
||||
name: Build & Package
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture: [AnyCPU, x86]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5
|
||||
|
||||
- name: Restore Package
|
||||
run: dotnet restore "Ink Canvas.sln" --locked-mode
|
||||
|
||||
- name: Build the Solution
|
||||
run: msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
|
||||
- name: Check if exe file is generated
|
||||
id: check-exe
|
||||
run: |
|
||||
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
|
||||
|
||||
if (Test-Path $exePath) {
|
||||
echo "build_success=true" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "build_success=false" >> $env:GITHUB_OUTPUT
|
||||
|
||||
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
- name: Create Package (if build succeeded)
|
||||
id: create-archive
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
env:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||
run: |
|
||||
$shortSha = $env:GITHUB_SHA.Substring(0, 7)
|
||||
$version = "debug-$shortSha-$env:GITHUB_RUN_NUMBER"
|
||||
echo "archive_name=$version" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Artifact (if build succeeded)
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: InkCanvasForClass.CE.debug.${{ matrix.architecture }}
|
||||
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
|
||||
|
||||
|
||||
- name: Create Summary
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo "# Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then
|
||||
echo "## ✅ Build Successful" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Download Artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip)\)" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "## ❌ Build Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -200,6 +200,10 @@ jobs:
|
||||
needs: prepare
|
||||
if: success()
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture: [AnyCPU, x86]
|
||||
outputs:
|
||||
archive_name: ${{ steps.create_archive.outputs.archive_name }}
|
||||
zip_size: ${{ steps.calculate_size.outputs.zip_size }}
|
||||
@@ -212,7 +216,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v5
|
||||
@@ -227,12 +231,12 @@ jobs:
|
||||
env:
|
||||
DLASS_SENTRY_DSN: ${{ secrets.DLASS_SENTRY_DSN }}
|
||||
run: |
|
||||
msbuild /p:platform="AnyCPU" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
|
||||
|
||||
- name: Check if exe file is generated
|
||||
id: check-exe
|
||||
run: |
|
||||
$exePath = "Ink Canvas/bin/Release/net472/InkCanvasForClass.exe"
|
||||
$exePath = "Ink Canvas\bin\Release\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
|
||||
|
||||
if (Test-Path $exePath) {
|
||||
echo "build_success=true" >> $env:GITHUB_OUTPUT
|
||||
@@ -265,13 +269,22 @@ jobs:
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$archiveName = "InkCanvasForClass.CE.$version.zip"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$archiveName = "InkCanvasForClass.CE.$version$suffix.zip"
|
||||
|
||||
# 创建发布目录
|
||||
New-Item -ItemType Directory -Path "release" -Force
|
||||
|
||||
# 复制发布文件
|
||||
Copy-Item "Ink Canvas/bin/Release/net472/*" "release/" -Recurse -Force
|
||||
# 复制发布文件(使用架构特定的路径)
|
||||
Copy-Item "Ink Canvas\bin\Release\$architecture\net6.0-windows10.0.19041.0\*" "release/" -Recurse -Force
|
||||
|
||||
# 创建压缩包
|
||||
Compress-Archive -Path "release/*" -DestinationPath $archiveName -Force
|
||||
@@ -282,6 +295,7 @@ jobs:
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 更新 ISS 文件中的版本信息
|
||||
$issPath = "build/InkCanvasForClass CE.iss"
|
||||
@@ -317,8 +331,17 @@ jobs:
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$setupFile = "InkCanvasForClass CE Setup.exe"
|
||||
$newSetupName = "InkCanvasForClass.CE.$version.Setup.exe"
|
||||
$newSetupName = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
|
||||
|
||||
if (Test-Path $setupFile) {
|
||||
Rename-Item -Path $setupFile -NewName $newSetupName
|
||||
@@ -331,7 +354,16 @@ jobs:
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$archiveName = "InkCanvasForClass.CE.$version.zip"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$archiveName = "InkCanvasForClass.CE.$version$suffix.zip"
|
||||
|
||||
# 获取文件大小(字节)
|
||||
$fileSize = (Get-Item $archiveName).Length
|
||||
@@ -343,7 +375,16 @@ jobs:
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$installerName = "InkCanvasForClass.CE.$version.Setup.exe"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$installerName = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
|
||||
|
||||
if (Test-Path $installerName) {
|
||||
# 获取文件大小(字节)
|
||||
@@ -354,14 +395,34 @@ jobs:
|
||||
Write-Error "Installer file not found: $installerName"
|
||||
}
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
run: |
|
||||
$version = "${{ needs.prepare.outputs.version }}"
|
||||
$architecture = "${{ matrix.architecture }}"
|
||||
|
||||
# 根据架构生成文件名后缀
|
||||
if ($architecture -eq "AnyCPU") {
|
||||
$suffix = "-x64"
|
||||
} else {
|
||||
$suffix = ""
|
||||
}
|
||||
|
||||
$zipFile = "InkCanvasForClass.CE.$version$suffix.zip"
|
||||
$setupFile = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
|
||||
|
||||
echo "zip_file=$zipFile" >> $env:GITHUB_OUTPUT
|
||||
echo "setup_file=$setupFile" >> $env:GITHUB_OUTPUT
|
||||
id: get_file_names
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
if: steps.check-exe.outputs.build_success == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
name: build-files-${{ needs.prepare.outputs.version }}-${{ matrix.architecture }}
|
||||
path: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
${{ steps.get_file_names.outputs.zip_file }}
|
||||
${{ steps.get_file_names.outputs.setup_file }}
|
||||
|
||||
- name: Create Build Summary
|
||||
if: always()
|
||||
@@ -376,6 +437,7 @@ jobs:
|
||||
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Release Type:** ${{ needs.prepare.outputs.is_prerelease == 'true' && 'Pre-release' || 'Release' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -393,6 +455,7 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -411,7 +474,8 @@ jobs:
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
pattern: build-files-${{ needs.prepare.outputs.version }}-*
|
||||
merge-multiple: false
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
@@ -419,11 +483,13 @@ jobs:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Sign release artifacts with sigstore-python
|
||||
uses: sigstore/gh-action-sigstore-python@v3.2.0
|
||||
uses: sigstore/gh-action-sigstore-python@v3.3.0
|
||||
with:
|
||||
inputs: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.zip
|
||||
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.exe
|
||||
build-files-${{ needs.prepare.outputs.version }}-x86/*.zip
|
||||
build-files-${{ needs.prepare.outputs.version }}-x86/*.exe
|
||||
release-signing-artifacts: true
|
||||
upload-signing-artifacts: true
|
||||
env:
|
||||
@@ -434,8 +500,8 @@ jobs:
|
||||
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
|
||||
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.sigstore.json
|
||||
build-files-${{ needs.prepare.outputs.version }}-x86/*.sigstore.json
|
||||
|
||||
release:
|
||||
needs: [prepare, build, sign]
|
||||
@@ -450,7 +516,8 @@ jobs:
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
pattern: build-files-${{ needs.prepare.outputs.version }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download Signed Artifacts (if exists)
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -461,13 +528,13 @@ jobs:
|
||||
- name: Create enhanced changelog with file table
|
||||
id: enhanced_changelog
|
||||
run: |
|
||||
version="${{ needs.prepare.outputs.version }}"
|
||||
version='${{ needs.prepare.outputs.version }}'
|
||||
|
||||
# 读取git-cliff生成的changelog内容
|
||||
originalChangelog="${{ needs.prepare.outputs.changelog }}"
|
||||
originalChangelog='${{ needs.prepare.outputs.changelog }}'
|
||||
|
||||
# 检查是否为预发布版本,如果是则添加警告提示
|
||||
if [ "${{ needs.prepare.outputs.is_prerelease }}" = "true" ]; then
|
||||
if [ '${{ needs.prepare.outputs.is_prerelease }}' = "true" ]; then
|
||||
warningText=$'\n> [!CAUTION]\n'
|
||||
warningText+=$'> **注意:此版本为预览或测试版**\n'
|
||||
warningText+=$'> \n'
|
||||
@@ -480,25 +547,45 @@ jobs:
|
||||
fileTable+=$'| 文件名 | 大小 |\n'
|
||||
fileTable+=$'|--------|------|\n'
|
||||
|
||||
# ZIP 文件信息
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"
|
||||
fileTable+=$'.zip | ${{ needs.build.outputs.zip_size }} bytes |\n'
|
||||
# AnyCPU (x64) 架构文件
|
||||
if [ -f "InkCanvasForClass.CE.$version-x64.zip" ]; then
|
||||
zipSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip | '"$zipSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
# 安装包文件信息
|
||||
installerSize="${{ needs.build.outputs.installer_size }}"
|
||||
if [ -n "$installerSize" ]; then
|
||||
if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe" ]; then
|
||||
installerSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe | '"$installerSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
if [ -f "InkCanvasForClass.CE.$version-x64.zip.sigstore.json" ]; then
|
||||
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json" ]; then
|
||||
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
# x86 架构文件
|
||||
if [ -f "InkCanvasForClass.CE.$version.zip" ]; then
|
||||
zipSize=$(wc -c < "InkCanvasForClass.CE.$version.zip")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip | '"$zipSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
if [ -f "InkCanvasForClass.CE.$version.Setup.exe" ]; then
|
||||
installerSize=$(wc -c < "InkCanvasForClass.CE.$version.Setup.exe")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe | '"$installerSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
# 检查是否有签名文件
|
||||
if [ -f "InkCanvasForClass.CE.$version.zip.sigstore.json" ]; then
|
||||
sigstoreSize=$(stat -c%s "InkCanvasForClass.CE.$version.zip.sigstore.json")
|
||||
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version.zip.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
# 检查安装程序签名文件
|
||||
if [ -f "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json" ]; then
|
||||
sigstoreSize=$(stat -c%s "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json")
|
||||
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json")
|
||||
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n'
|
||||
fi
|
||||
|
||||
@@ -531,6 +618,10 @@ jobs:
|
||||
draft: ${{ github.event.inputs.draft || false }}
|
||||
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
|
||||
files: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip.sigstore.json
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe.sigstore.json
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json
|
||||
@@ -541,7 +632,7 @@ jobs:
|
||||
|
||||
post_release:
|
||||
needs: [prepare, release]
|
||||
if: success()
|
||||
if: success() && github.event.inputs.draft != 'true'
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -550,7 +641,8 @@ jobs:
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: build-files-${{ needs.prepare.outputs.version }}
|
||||
pattern: build-files-${{ needs.prepare.outputs.version }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Get beta token
|
||||
uses: octo-sts/action@main
|
||||
@@ -580,18 +672,32 @@ jobs:
|
||||
cd $REPO_DIR
|
||||
IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}"
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION.zip"
|
||||
X64_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION-x64.zip"
|
||||
X86_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION.zip"
|
||||
|
||||
if [ "$IS_PRERELEASE" == "true" ]; then
|
||||
mkdir -p Beta
|
||||
cp "$ZIP_FILE" Beta/
|
||||
git add Beta/InkCanvasForClass.CE.$VERSION.zip
|
||||
if [ -f "$X64_ZIP_FILE" ]; then
|
||||
cp "$X64_ZIP_FILE" Beta/
|
||||
git add Beta/InkCanvasForClass.CE.$VERSION-x64.zip
|
||||
fi
|
||||
if [ -f "$X86_ZIP_FILE" ]; then
|
||||
cp "$X86_ZIP_FILE" Beta/
|
||||
git add Beta/InkCanvasForClass.CE.$VERSION.zip
|
||||
fi
|
||||
git commit -m "Add $VERSION PreRelease"
|
||||
else
|
||||
mkdir -p Release Beta
|
||||
cp "$ZIP_FILE" Release/
|
||||
cp "$ZIP_FILE" Beta/
|
||||
git add Release/InkCanvasForClass.CE.$VERSION.zip Beta/InkCanvasForClass.CE.$VERSION.zip
|
||||
if [ -f "$X64_ZIP_FILE" ]; then
|
||||
cp "$X64_ZIP_FILE" Release/
|
||||
cp "$X64_ZIP_FILE" Beta/
|
||||
git add Release/InkCanvasForClass.CE.$VERSION-x64.zip Beta/InkCanvasForClass.CE.$VERSION-x64.zip
|
||||
fi
|
||||
if [ -f "$X86_ZIP_FILE" ]; then
|
||||
cp "$X86_ZIP_FILE" Release/
|
||||
cp "$X86_ZIP_FILE" Beta/
|
||||
git add Release/InkCanvasForClass.CE.$VERSION.zip Beta/InkCanvasForClass.CE.$VERSION.zip
|
||||
fi
|
||||
git commit -m "Add $VERSION Release"
|
||||
fi
|
||||
git push origin main
|
||||
@@ -622,9 +728,11 @@ jobs:
|
||||
draft: false
|
||||
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
|
||||
files: |
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip
|
||||
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
|
||||
fail_on_unmatched_files: false
|
||||
repository: "InkCanvasForClass/community-beta"
|
||||
token: ${{ steps.octo-sts-beta.outputs.token }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }}
|
||||
|
||||
@@ -642,5 +750,5 @@ jobs:
|
||||
/repos/InkCanvasForClass/community/contents/AutomaticUpdateVersionControl.txt \
|
||||
-f message="Update AutomaticUpdateVersionControl.txt" \
|
||||
-f content="$CONTENT" \
|
||||
-f branch="beta" \
|
||||
-f branch="net6" \
|
||||
${SHA:+-f sha="$SHA"}
|
||||
|
||||
+2
-1
@@ -429,4 +429,5 @@ FodyWeavers.xsd
|
||||
|
||||
# Telemetry DSN configuration file (contains sensitive information)
|
||||
telemetry_dsn.txt
|
||||
**/telemetry_dsn.txt
|
||||
**/telemetry_dsn.txt
|
||||
.trae/skills/migrate-toggle-switch/SKILL.md
|
||||
|
||||
+57
-13
@@ -1,10 +1,14 @@
|
||||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.33530.505
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.4.11626.88 stable
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InkCanvasForClass", "Ink Canvas\InkCanvasForClass.csproj", "{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.PluginSdk", "InkCanvas.PluginSdk\InkCanvas.PluginSdk.csproj", "{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.Controls", "InkCanvas.Controls\InkCanvas.Controls.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -23,22 +27,62 @@ Global
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x64.Build.0 = Debug|x64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.Build.0 = Debug|x86
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM.Build.0 = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.ActiveCfg = Release|x64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.Build.0 = Release|x64
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.ActiveCfg = Release|x86
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.Build.0 = Release|x86
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.Build.0 = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM64.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
+34
-3
@@ -1,8 +1,9 @@
|
||||
<Application x:Class="Ink_Canvas.App"
|
||||
<Application x:Class="Ink_Canvas.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Ink_Canvas"
|
||||
xmlns:tb="http://www.hardcodet.net/taskbar"
|
||||
xmlns:props="clr-namespace:Ink_Canvas.Properties"
|
||||
xmlns:tb="clr-namespace:H.NotifyIcon;assembly=H.NotifyIcon.Wpf"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
|
||||
>
|
||||
@@ -32,6 +33,36 @@
|
||||
</Image>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Name="TempShowMainWindowTrayIconMenuItem" Click="TempShowMainWindowTrayIconMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="{x:Static props:Strings.Tray_TempShowMainWindow}" />
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="28" Height="28" Margin="-2">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
|
||||
<GeometryDrawing Brush="#27272a" Geometry="F0 M24,24z M0,0z M5,6C4.73478,6 4.48043,6.10536 4.29289,6.29289 4.10536,6.48043 4,6.73478 4,7L4,17C4,17.2652 4.10536,17.5196 4.29289,17.7071 4.48043,17.8946 4.73478,18 5,18L19,18C19.2652,18 19.5196,17.8946 19.7071,17.7071 19.8946,17.5196 20,17.2652 20,17L20,7C20,6.73478 19.8946,6.48043 19.7071,6.29289 19.5196,6.10536 19.2652,6 19,6L5,6z M2.87868,4.87868C3.44129,4.31607,4.20435,4,5,4L19,4C19.7957,4 20.5587,4.31607 21.1213,4.87868 21.6839,5.44129 22,6.20435 22,7L22,17C22,17.7957 21.6839,18.5587 21.1213,19.1213 20.5587,19.6839 19.7957,20 19,20L5,20C4.20435,20 3.44129,19.6839 2.87868,19.1213 2.31607,18.5587 2,17.7956 2,17L2,7C2,6.20435,2.31607,5.44129,2.87868,4.87868z M5,8C5,7.44772,5.44772,7,6,7L6.01,7C6.56228,7 7.01,7.44772 7.01,8 7.01,8.55228 6.56228,9 6.01,9L6,9C5.44772,9,5,8.55228,5,8z M9,7C8.44772,7 8,7.44772 8,8 8,8.55228 8.44772,9 9,9L9.01,9C9.56228,9 10.01,8.55228 10.01,8 10.01,7.44772 9.56228,7 9.01,7L9,7z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Name="OpenSettingsTrayIconMenuItem" Click="OpenSettingsTrayIconMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="{x:Static props:Strings.Tray_OpenSettings}" />
|
||||
</ikw:SimpleStackPanel>
|
||||
</MenuItem.Header>
|
||||
<MenuItem.Icon>
|
||||
<Image Width="28" Height="28" Margin="-2" Source="/Resources/Icons-Fluent/ic_fluent_settings_24_regular.png" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator Margin="0,3" />
|
||||
<MenuItem Name="DisableAllHotkeysMenuItem" Click="DisableAllHotkeysMenuItem_Clicked">
|
||||
<MenuItem.Header>
|
||||
@@ -232,7 +263,7 @@
|
||||
ToolTipText="InkCanvasForClass"
|
||||
ContextMenu="{StaticResource SysTrayMenu}"
|
||||
IconSource="/Resources/icc.ico"/>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemeResources/>
|
||||
<ui:XamlControlsResources />
|
||||
<ResourceDictionary Source="Resources/SeewoImageDictionary.xaml"/>
|
||||
|
||||
+301
-133
@@ -1,10 +1,14 @@
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using H.NotifyIcon;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Ink_Canvas.Plugins;
|
||||
using Ink_Canvas.Properties;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
using Sentry;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -15,13 +19,12 @@ using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Threading;
|
||||
using Application = System.Windows.Application;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
using Ink_Canvas.Properties;
|
||||
using SplashScreen = Ink_Canvas.Windows.SplashScreen;
|
||||
using Timer = System.Threading.Timer;
|
||||
using Sentry;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -32,6 +35,20 @@ namespace Ink_Canvas
|
||||
{
|
||||
Mutex mutex;
|
||||
|
||||
public void ReleaseMutexForRestart()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (mutex != null)
|
||||
{
|
||||
mutex.ReleaseMutex();
|
||||
mutex.Dispose();
|
||||
mutex = null;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static string[] StartArgs;
|
||||
public static string RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
|
||||
|
||||
@@ -61,9 +78,16 @@ namespace Ink_Canvas
|
||||
private static string lastErrorMessage = string.Empty;
|
||||
// 新增:是否已初始化崩溃监听器
|
||||
private static bool crashListenersInitialized;
|
||||
private IntPtr processDestroyHook = IntPtr.Zero;
|
||||
private IntPtr monitoredMainWindowHandle = IntPtr.Zero;
|
||||
private bool mainWindowDestroyedLogged;
|
||||
private WinEventDelegate processDestroyHookCallback;
|
||||
// 新增:启动画面相关
|
||||
private static SplashScreen _splashScreen;
|
||||
private static bool _isSplashScreenShown = false;
|
||||
private static System.Resources.ResourceSet _pendingLocalizedResourceSet;
|
||||
private static readonly Stopwatch startupStopwatch = new Stopwatch();
|
||||
private static readonly Stopwatch splashStopwatch = new Stopwatch();
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern int SetCurrentProcessExplicitAppUserModelID(string appId);
|
||||
@@ -192,15 +216,13 @@ namespace Ink_Canvas
|
||||
// 尝试注册Windows关闭消息监听
|
||||
SetConsoleCtrlHandler(ConsoleCtrlHandler, true);
|
||||
|
||||
// 如果系统支持,添加Windows Management Instrumentation监听器
|
||||
try
|
||||
{
|
||||
// 使用反射动态加载和调用WMI
|
||||
TrySetupWmiMonitoring();
|
||||
TrySetupTerminationMonitoring();
|
||||
}
|
||||
catch (Exception wmiEx)
|
||||
catch (Exception monitorEx)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"设置WMI进程监控失败: {wmiEx.Message}", LogHelper.LogType.Warning);
|
||||
LogHelper.WriteLogToFile($"设置终止监控失败: {monitorEx.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
|
||||
crashListenersInitialized = true;
|
||||
@@ -212,80 +234,114 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 动态加载WMI监控
|
||||
private void TrySetupWmiMonitoring()
|
||||
private void TrySetupTerminationMonitoring()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查System.Management程序集是否可用
|
||||
var assemblyName = "System.Management, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
|
||||
var assembly = Assembly.Load(assemblyName);
|
||||
if (assembly == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到System.Management程序集,跳过WMI监控", LogHelper.LogType.Warning);
|
||||
return;
|
||||
}
|
||||
processDestroyHookCallback = OnWinEventMainWindowDestroyed;
|
||||
|
||||
// 使用反射创建WMI查询
|
||||
var watcherType = assembly.GetType("System.Management.ManagementEventWatcher");
|
||||
if (watcherType == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到ManagementEventWatcher类型,跳过WMI监控", LogHelper.LogType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建WMI查询字符串
|
||||
string queryString = $"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.ProcessId = {currentProcessId}";
|
||||
|
||||
// 创建ManagementEventWatcher实例
|
||||
object watcher = Activator.CreateInstance(watcherType, queryString);
|
||||
|
||||
// 获取EventArrived事件信息
|
||||
var eventInfo = watcherType.GetEvent("EventArrived");
|
||||
if (eventInfo == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到EventArrived事件,跳过WMI监控", LogHelper.LogType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建委托并订阅事件
|
||||
Type delegateType = eventInfo.EventHandlerType;
|
||||
var handler = Delegate.CreateDelegate(delegateType, this, GetType().GetMethod("WmiEventHandler", BindingFlags.NonPublic | BindingFlags.Instance));
|
||||
eventInfo.AddEventHandler(watcher, handler);
|
||||
|
||||
// 启动监听
|
||||
var startMethod = watcherType.GetMethod("Start");
|
||||
startMethod.Invoke(watcher, null);
|
||||
|
||||
LogHelper.WriteLogToFile("已成功启动WMI进程监控");
|
||||
// 等主窗口句柄可用后再开始监听
|
||||
Dispatcher.BeginInvoke(new Action(BindMainWindowLifecycle), DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"动态加载WMI监控失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
LogHelper.WriteLogToFile($"初始化终止监控失败: {ex.GetType().FullName}: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
// WMI事件处理方法
|
||||
private void WmiEventHandler(object sender, EventArgs e)
|
||||
private void BindMainWindowLifecycle()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 尝试从事件参数中提取信息
|
||||
dynamic eventArgs = e;
|
||||
dynamic newEvent = eventArgs.NewEvent;
|
||||
if (newEvent != null)
|
||||
if (Current?.MainWindow == null)
|
||||
{
|
||||
dynamic targetInstance = newEvent["TargetInstance"];
|
||||
if (targetInstance != null)
|
||||
{
|
||||
string processName = targetInstance["Name"]?.ToString() ?? "未知进程";
|
||||
WriteCrashLog($"WMI检测到进程{processName}(ID:{currentProcessId})已终止");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Current.MainWindow.SourceInitialized -= MainWindow_SourceInitialized;
|
||||
Current.MainWindow.SourceInitialized += MainWindow_SourceInitialized;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void MainWindow_SourceInitialized(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!(sender is Window window))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
monitoredMainWindowHandle = new WindowInteropHelper(window).Handle;
|
||||
if (monitoredMainWindowHandle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RegisterMainWindowDestroyHook();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterMainWindowDestroyHook()
|
||||
{
|
||||
if (processDestroyHook != IntPtr.Zero || monitoredMainWindowHandle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
processDestroyHook = SetWinEventHook(
|
||||
EVENT_OBJECT_DESTROY,
|
||||
EVENT_OBJECT_DESTROY,
|
||||
IntPtr.Zero,
|
||||
processDestroyHookCallback,
|
||||
(uint)currentProcessId,
|
||||
0,
|
||||
WINEVENT_OUTOFCONTEXT);
|
||||
|
||||
if (processDestroyHook == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWinEventMainWindowDestroyed(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
{
|
||||
if (eventType != EVENT_OBJECT_DESTROY || mainWindowDestroyedLogged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (idObject != OBJID_WINDOW || idChild != CHILDID_SELF)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (hwnd != monitoredMainWindowHandle || hwnd == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindowDestroyedLogged = true;
|
||||
}
|
||||
|
||||
private void CleanupTerminationMonitoring()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (processDestroyHook != IntPtr.Zero)
|
||||
{
|
||||
UnhookWinEvent(processDestroyHook);
|
||||
processDestroyHook = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
LogHelper.WriteLogToFile($"处理WMI事件时出错: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,6 +350,19 @@ namespace Ink_Canvas
|
||||
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate handler, bool add);
|
||||
|
||||
private delegate bool ConsoleCtrlDelegate(int ctrlType);
|
||||
private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
||||
|
||||
private const uint EVENT_OBJECT_DESTROY = 0x8001;
|
||||
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
|
||||
private const int OBJID_WINDOW = 0;
|
||||
private const int CHILDID_SELF = 0;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
|
||||
|
||||
private static bool ConsoleCtrlHandler(int ctrlType)
|
||||
{
|
||||
@@ -428,6 +497,7 @@ namespace Ink_Canvas
|
||||
// 处理进程退出事件
|
||||
private void CurrentDomain_ProcessExit(object sender, EventArgs e)
|
||||
{
|
||||
CleanupTerminationMonitoring();
|
||||
TimeSpan runDuration = DateTime.Now - appStartTime;
|
||||
string durationText = FormatTimeSpan(runDuration);
|
||||
WriteCrashLog($"应用程序退出,运行时长: {durationText}");
|
||||
@@ -476,6 +546,7 @@ namespace Ink_Canvas
|
||||
_splashScreen.Show();
|
||||
_isSplashScreenShown = true;
|
||||
splashScreenStartTime = DateTime.Now;
|
||||
splashStopwatch.Restart();
|
||||
LogHelper.WriteLogToFile("启动画面已显示");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -695,20 +766,25 @@ namespace Ink_Canvas
|
||||
{
|
||||
appStartTime = DateTime.Now;
|
||||
appStartupStartTime = DateTime.Now;
|
||||
startupStopwatch.Restart();
|
||||
|
||||
TryApplyPreferredLanguageFromSettings();
|
||||
|
||||
_pendingLocalizedResourceSet = Strings.ResourceManager.GetResourceSet(CultureInfo.CurrentUICulture, true, true);
|
||||
|
||||
// 根据设置决定是否显示启动画面
|
||||
if (ShouldShowSplashScreen() && !IsLaunchByFileOrUri(e.Args))
|
||||
{
|
||||
await Task.Delay(100);
|
||||
ShowSplashScreen();
|
||||
SetSplashMessage(Strings.GetString("Splash_Starting"));
|
||||
SetSplashProgress(20);
|
||||
await Task.Delay(500);
|
||||
|
||||
// 强制刷新UI,确保启动画面显示
|
||||
Application.Current.Dispatcher.Invoke(() => { }, DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
await Task.Delay(500);
|
||||
await Task.Delay(100);
|
||||
RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
|
||||
|
||||
LogHelper.NewLog(string.Format("Ink Canvas Starting (Version: {0})", Assembly.GetExecutingAssembly().GetName().Version));
|
||||
@@ -744,15 +820,6 @@ namespace Ink_Canvas
|
||||
{
|
||||
SetSplashMessage("正在初始化组件...");
|
||||
SetSplashProgress(40);
|
||||
await Task.Delay(500);
|
||||
}
|
||||
try
|
||||
{
|
||||
IACoreDllExtractor.ExtractIACoreDlls();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"释放IACore DLL时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
// 释放UIAccess DLL
|
||||
@@ -760,43 +827,13 @@ namespace Ink_Canvas
|
||||
{
|
||||
SetSplashMessage("正在初始化组件...");
|
||||
SetSplashProgress(50);
|
||||
await Task.Delay(300);
|
||||
}
|
||||
try
|
||||
{
|
||||
UIAccessDllExtractor.ExtractUIAccessDlls();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"释放UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
// 记录应用启动(设备标识符)
|
||||
if (_isSplashScreenShown)
|
||||
{
|
||||
SetSplashMessage("正在加载配置...");
|
||||
SetSplashProgress(60);
|
||||
await Task.Delay(500);
|
||||
}
|
||||
DeviceIdentifier.RecordAppLaunch();
|
||||
try
|
||||
{
|
||||
var systemVersion = DeviceIdentifier.GetSystemVersion();
|
||||
if (!string.IsNullOrWhiteSpace(systemVersion))
|
||||
{
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.SetTag("system_version", systemVersion);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"App | 初始化系统版本遥测标签失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
LogHelper.WriteLogToFile($"App | 设备ID: {DeviceIdentifier.GetDeviceId()}");
|
||||
LogHelper.WriteLogToFile($"App | 使用频率: {DeviceIdentifier.GetUsageFrequency()}");
|
||||
LogHelper.WriteLogToFile($"App | 更新优先级: {DeviceIdentifier.GetUpdatePriority()}");
|
||||
|
||||
// 处理更新模式启动
|
||||
bool isUpdateMode = AutoUpdateHelper.HandleUpdateModeStartup(e.Args);
|
||||
@@ -1069,26 +1106,48 @@ namespace Ink_Canvas
|
||||
{
|
||||
SetSplashMessage("正在初始化主界面...");
|
||||
SetSplashProgress(80);
|
||||
await Task.Delay(500);
|
||||
}
|
||||
var mainWindow = new MainWindow();
|
||||
MainWindow = mainWindow;
|
||||
|
||||
// 注册 InkCanvas 服务供插件使用
|
||||
try
|
||||
{
|
||||
var inkCanvasService = new Plugins.InkCanvasService(mainWindow);
|
||||
Plugins.PluginManager.Instance.RegisterService<Plugins.IInkCanvasService>(inkCanvasService);
|
||||
LogHelper.WriteLogToFile("InkCanvasService registered for plugins");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"Failed to register InkCanvasService: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var appRestartService = new Plugins.AppRestartService();
|
||||
Plugins.PluginManager.Instance.RegisterService<Plugins.IAppRestartService>(appRestartService);
|
||||
LogHelper.WriteLogToFile("AppRestartService registered for plugins");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"Failed to register AppRestartService: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
// 主窗口加载完成后关闭启动画面
|
||||
mainWindow.Loaded += (s, args) =>
|
||||
{
|
||||
isStartupComplete = true;
|
||||
startupCompleteHeartbeat = DateTime.Now;
|
||||
if (_isSplashScreenShown && splashScreenStartTime != DateTime.MinValue)
|
||||
if (_isSplashScreenShown && splashStopwatch.IsRunning)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动完成心跳已记录,启动画面显示时长: {(startupCompleteHeartbeat - splashScreenStartTime).TotalSeconds:F2}秒");
|
||||
LogHelper.WriteLogToFile($"启动完成心跳已记录,启动画面显示时长: {splashStopwatch.Elapsed.TotalSeconds:F2}秒");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动完成心跳已记录");
|
||||
}
|
||||
LogHelper.WriteLogToFile($"启动时长: {(startupCompleteHeartbeat - appStartupStartTime).TotalSeconds:F2}秒");
|
||||
|
||||
LogHelper.WriteLogToFile($"启动时长: {startupStopwatch.Elapsed.TotalSeconds:F2}秒");
|
||||
|
||||
if (_isSplashScreenShown)
|
||||
{
|
||||
SetSplashMessage("完成初始化...");
|
||||
@@ -1110,6 +1169,19 @@ namespace Ink_Canvas
|
||||
};
|
||||
|
||||
mainWindow.Show();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(600);
|
||||
Dispatcher.Invoke(() => _taskbar?.ForceCreate());
|
||||
});
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (_pendingLocalizedResourceSet != null)
|
||||
{
|
||||
LoadLocalizedResources(_pendingLocalizedResourceSet);
|
||||
_pendingLocalizedResourceSet = null;
|
||||
}
|
||||
}), DispatcherPriority.ApplicationIdle);
|
||||
|
||||
// 处理启动时的URI参数
|
||||
string startupUriArg = e.Args.FirstOrDefault(a => a.StartsWith("icc:", StringComparison.OrdinalIgnoreCase));
|
||||
@@ -1126,40 +1198,123 @@ namespace Ink_Canvas
|
||||
});
|
||||
}
|
||||
|
||||
// 注册.icstk文件关联
|
||||
_ = RunDeferredStartupTasksAsync();
|
||||
|
||||
}
|
||||
|
||||
private async Task RunDeferredStartupTasksAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("开始注册.icstk文件关联");
|
||||
FileAssociationManager.RegisterFileAssociation();
|
||||
FileAssociationManager.ShowFileAssociationStatus();
|
||||
await Task.Delay(1200);
|
||||
|
||||
try
|
||||
{
|
||||
IACoreDllExtractor.ExtractIACoreDlls();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"释放IACore DLL时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("开始注册.icstk文件关联");
|
||||
FileAssociationManager.RegisterFileAssociation();
|
||||
FileAssociationManager.ShowFileAssociationStatus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("启动IPC监听器");
|
||||
FileAssociationManager.StartIpcListener();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动IPC监听器时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("初始化上传帮助类");
|
||||
Helpers.UploadHelper.Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化上传帮助类时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("开始加载插件");
|
||||
await PluginManager.Instance.LoadAllAsync();
|
||||
LogHelper.WriteLogToFile(string.Format("插件加载完成,共加载 {0} 个插件", PluginManager.Instance.Plugins.Count));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile(string.Format("加载插件时出错: {0}", ex.Message), LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(1500);
|
||||
DeviceIdentifier.RecordAppLaunch();
|
||||
var systemVersion = DeviceIdentifier.GetSystemVersion();
|
||||
if (!string.IsNullOrWhiteSpace(systemVersion))
|
||||
{
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.SetTag("system_version", systemVersion);
|
||||
});
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"App | 设备ID: {DeviceIdentifier.GetDeviceId()}");
|
||||
LogHelper.WriteLogToFile($"App | 使用频率: {DeviceIdentifier.GetUsageFrequency()}");
|
||||
LogHelper.WriteLogToFile($"App | 更新优先级: {DeviceIdentifier.GetUpdatePriority()}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"App | 初始化设备统计与遥测标签失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"注册文件关联时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.WriteLogToFile($"启动阶段任务执行失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动IPC监听器
|
||||
private void TryApplyPreferredLanguageFromSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("启动IPC监听器");
|
||||
FileAssociationManager.StartIpcListener();
|
||||
var settingsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configs", "Settings.json");
|
||||
if (!File.Exists(settingsPath)) return;
|
||||
|
||||
var json = File.ReadAllText(settingsPath);
|
||||
dynamic obj = JsonConvert.DeserializeObject(json);
|
||||
string preferredLanguage = obj?["appearance"]?["language"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(preferredLanguage))
|
||||
{
|
||||
LocalizationHelper.TrySetCulture(preferredLanguage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动IPC监听器时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.WriteLogToFile($"启动时预加载语言失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化上传帮助类
|
||||
try
|
||||
private void LoadLocalizedResources(System.Resources.ResourceSet resourceSet)
|
||||
{
|
||||
foreach (System.Collections.DictionaryEntry entry in resourceSet)
|
||||
{
|
||||
LogHelper.WriteLogToFile("初始化上传帮助类");
|
||||
Helpers.UploadHelper.Initialize();
|
||||
if (entry.Key is string key && entry.Value is string value)
|
||||
Current.Resources[key] = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化上传帮助类时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||||
@@ -1223,8 +1378,8 @@ namespace Ink_Canvas
|
||||
|
||||
if (!isStartupComplete && appStartupStartTime != DateTime.MinValue)
|
||||
{
|
||||
DateTime startTime = _isSplashScreenShown && splashScreenStartTime != DateTime.MinValue
|
||||
? splashScreenStartTime
|
||||
DateTime startTime = _isSplashScreenShown && splashScreenStartTime != DateTime.MinValue
|
||||
? splashScreenStartTime
|
||||
: appStartupStartTime;
|
||||
TimeSpan elapsedSinceStart = DateTime.Now - startTime;
|
||||
if (elapsedSinceStart.TotalMinutes >= 2)
|
||||
@@ -1405,7 +1560,7 @@ namespace Ink_Canvas
|
||||
|
||||
string assemblyLocation = Assembly.GetExecutingAssembly().Location;
|
||||
string currentDir = Path.GetDirectoryName(assemblyLocation);
|
||||
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
string dsnFilePath = Path.Combine(currentDir, "telemetry_dsn.txt");
|
||||
@@ -1417,7 +1572,7 @@ namespace Ink_Canvas
|
||||
return dsn;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DirectoryInfo parentDir = Directory.GetParent(currentDir);
|
||||
if (parentDir == null)
|
||||
{
|
||||
@@ -1436,6 +1591,19 @@ namespace Ink_Canvas
|
||||
|
||||
private void App_Exit(object sender, ExitEventArgs e)
|
||||
{
|
||||
CleanupTerminationMonitoring();
|
||||
// 卸载所有插件
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile("正在卸载插件...");
|
||||
PluginManager.Instance.UnloadAll();
|
||||
LogHelper.WriteLogToFile("插件卸载完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"卸载插件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
// 仅在软件内主动退出时关闭看门狗,并写入退出信号
|
||||
try
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
|
||||
[assembly: System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
@@ -43,5 +44,5 @@ using System.Windows;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.7.18.8")]
|
||||
[assembly: AssemblyFileVersion("1.7.18.8")]
|
||||
[assembly: AssemblyVersion("1.7.18.10")]
|
||||
[assembly: AssemblyFileVersion("1.7.18.10")]
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Controls
|
||||
{
|
||||
[TemplatePart(Name = PartCloseImage, Type = typeof(UIElement))]
|
||||
[TemplatePart(Name = PartAnimationRoot, Type = typeof(UIElement))]
|
||||
public class BoardMenuFrame : ContentControl
|
||||
{
|
||||
private const string PartCloseImage = "PART_CloseImage";
|
||||
private const string PartAnimationRoot = "PART_AnimationRoot";
|
||||
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register(nameof(Title), typeof(object), typeof(BoardMenuFrame), new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty TitleFontSizeProperty =
|
||||
DependencyProperty.Register(nameof(TitleFontSize), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(11d));
|
||||
|
||||
public static readonly DependencyProperty HeaderHeightProperty =
|
||||
DependencyProperty.Register(nameof(HeaderHeight), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(48d));
|
||||
|
||||
public static readonly DependencyProperty PanelCornerRadiusProperty =
|
||||
DependencyProperty.Register(nameof(PanelCornerRadius), typeof(CornerRadius), typeof(BoardMenuFrame), new PropertyMetadata(new CornerRadius(5)));
|
||||
|
||||
public static readonly DependencyProperty HeaderCornerRadiusProperty =
|
||||
DependencyProperty.Register(nameof(HeaderCornerRadius), typeof(CornerRadius), typeof(BoardMenuFrame), new PropertyMetadata(new CornerRadius(6, 6, 0, 0)));
|
||||
|
||||
public static readonly DependencyProperty PanelBackgroundProperty =
|
||||
DependencyProperty.Register(nameof(PanelBackground), typeof(Brush), typeof(BoardMenuFrame), new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty HeaderBackgroundProperty =
|
||||
DependencyProperty.Register(nameof(HeaderBackground), typeof(Brush), typeof(BoardMenuFrame),
|
||||
new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2563eb"))));
|
||||
|
||||
public static readonly DependencyProperty HeaderBorderBrushProperty =
|
||||
DependencyProperty.Register(nameof(HeaderBorderBrush), typeof(Brush), typeof(BoardMenuFrame),
|
||||
new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1e3a8a"))));
|
||||
|
||||
public static readonly DependencyProperty IsOpenProperty =
|
||||
DependencyProperty.Register(nameof(IsOpen), typeof(bool), typeof(BoardMenuFrame), new PropertyMetadata(false));
|
||||
|
||||
public static readonly DependencyProperty PlacementTargetProperty =
|
||||
DependencyProperty.Register(nameof(PlacementTarget), typeof(UIElement), typeof(BoardMenuFrame), new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty PlacementProperty =
|
||||
DependencyProperty.Register(nameof(Placement), typeof(PlacementMode), typeof(BoardMenuFrame), new PropertyMetadata(PlacementMode.Custom));
|
||||
|
||||
public static readonly DependencyProperty CustomPopupPlacementCallbackProperty =
|
||||
DependencyProperty.Register(nameof(CustomPopupPlacementCallback), typeof(CustomPopupPlacementCallback), typeof(BoardMenuFrame),
|
||||
new PropertyMetadata((CustomPopupPlacementCallback)PlaceCenteredAbove));
|
||||
|
||||
private static CustomPopupPlacement[] PlaceCenteredAbove(Size popupSize, Size targetSize, Point offset)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new CustomPopupPlacement(
|
||||
new Point((targetSize.Width - popupSize.Width) / 2 + offset.X,
|
||||
-popupSize.Height + offset.Y),
|
||||
PopupPrimaryAxis.Horizontal),
|
||||
new CustomPopupPlacement(
|
||||
new Point((targetSize.Width - popupSize.Width) / 2 + offset.X,
|
||||
targetSize.Height - offset.Y),
|
||||
PopupPrimaryAxis.Horizontal)
|
||||
};
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PopupHorizontalOffsetProperty =
|
||||
DependencyProperty.Register(nameof(PopupHorizontalOffset), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(0d));
|
||||
|
||||
public static readonly DependencyProperty PopupVerticalOffsetProperty =
|
||||
DependencyProperty.Register(nameof(PopupVerticalOffset), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(-4d));
|
||||
|
||||
public object Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
|
||||
public double TitleFontSize { get => (double)GetValue(TitleFontSizeProperty); set => SetValue(TitleFontSizeProperty, value); }
|
||||
public double HeaderHeight { get => (double)GetValue(HeaderHeightProperty); set => SetValue(HeaderHeightProperty, value); }
|
||||
public CornerRadius PanelCornerRadius { get => (CornerRadius)GetValue(PanelCornerRadiusProperty); set => SetValue(PanelCornerRadiusProperty, value); }
|
||||
public CornerRadius HeaderCornerRadius { get => (CornerRadius)GetValue(HeaderCornerRadiusProperty); set => SetValue(HeaderCornerRadiusProperty, value); }
|
||||
public Brush PanelBackground { get => (Brush)GetValue(PanelBackgroundProperty); set => SetValue(PanelBackgroundProperty, value); }
|
||||
public Brush HeaderBackground { get => (Brush)GetValue(HeaderBackgroundProperty); set => SetValue(HeaderBackgroundProperty, value); }
|
||||
public Brush HeaderBorderBrush { get => (Brush)GetValue(HeaderBorderBrushProperty); set => SetValue(HeaderBorderBrushProperty, value); }
|
||||
public bool IsOpen { get => (bool)GetValue(IsOpenProperty); set => SetValue(IsOpenProperty, value); }
|
||||
public UIElement PlacementTarget { get => (UIElement)GetValue(PlacementTargetProperty); set => SetValue(PlacementTargetProperty, value); }
|
||||
public PlacementMode Placement { get => (PlacementMode)GetValue(PlacementProperty); set => SetValue(PlacementProperty, value); }
|
||||
public CustomPopupPlacementCallback CustomPopupPlacementCallback
|
||||
{
|
||||
get => (CustomPopupPlacementCallback)GetValue(CustomPopupPlacementCallbackProperty);
|
||||
set => SetValue(CustomPopupPlacementCallbackProperty, value);
|
||||
}
|
||||
public double PopupHorizontalOffset { get => (double)GetValue(PopupHorizontalOffsetProperty); set => SetValue(PopupHorizontalOffsetProperty, value); }
|
||||
public double PopupVerticalOffset { get => (double)GetValue(PopupVerticalOffsetProperty); set => SetValue(PopupVerticalOffsetProperty, value); }
|
||||
|
||||
public event MouseButtonEventHandler CloseMouseDown;
|
||||
public event MouseButtonEventHandler CloseMouseUp;
|
||||
|
||||
public UIElement AnimationTarget { get; private set; }
|
||||
|
||||
private UIElement _closeImage;
|
||||
|
||||
static BoardMenuFrame()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(BoardMenuFrame), new FrameworkPropertyMetadata(typeof(BoardMenuFrame)));
|
||||
VisibilityProperty.OverrideMetadata(typeof(BoardMenuFrame),
|
||||
new FrameworkPropertyMetadata(Visibility.Collapsed, OnVisibilityChanged));
|
||||
}
|
||||
|
||||
private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((BoardMenuFrame)d).IsOpen = (Visibility)e.NewValue == Visibility.Visible;
|
||||
}
|
||||
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
if (_closeImage != null)
|
||||
{
|
||||
_closeImage.MouseDown -= CloseImage_MouseDown;
|
||||
_closeImage.MouseUp -= CloseImage_MouseUp;
|
||||
}
|
||||
_closeImage = GetTemplateChild(PartCloseImage) as UIElement;
|
||||
if (_closeImage != null)
|
||||
{
|
||||
_closeImage.MouseDown += CloseImage_MouseDown;
|
||||
_closeImage.MouseUp += CloseImage_MouseUp;
|
||||
}
|
||||
AnimationTarget = GetTemplateChild(PartAnimationRoot) as UIElement;
|
||||
}
|
||||
|
||||
private void CloseImage_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
CloseMouseDown?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void CloseImage_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
CloseMouseUp?.Invoke(sender, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Ink_Canvas.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// 画布上的多页 PDF:仅显示当前页;翻页与页码由主窗口 PDF 侧栏控制(无 XAML 文件)。
|
||||
/// </summary>
|
||||
public class PdfEmbeddedView : UserControl
|
||||
{
|
||||
private readonly Image _pageImage;
|
||||
|
||||
private string _pdfPath;
|
||||
private uint _pageCount;
|
||||
private uint _currentIndex;
|
||||
private bool _compressLargePictures;
|
||||
private bool _isPagingBusy;
|
||||
private bool _layoutSizeCommitted;
|
||||
|
||||
/// <summary>页码或可翻页状态变化(用于更新侧栏)。</summary>
|
||||
public event EventHandler PageNavigationStateChanged;
|
||||
|
||||
public PdfEmbeddedView()
|
||||
{
|
||||
MinWidth = 80;
|
||||
MinHeight = 60;
|
||||
|
||||
var grid = new Grid { ClipToBounds = true };
|
||||
_pageImage = new Image
|
||||
{
|
||||
Stretch = Stretch.Uniform,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
grid.Children.Add(_pageImage);
|
||||
Content = grid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化并显示指定页;由 MainWindow 在 UI 线程创建后调用。
|
||||
/// </summary>
|
||||
/// <param name="initialPageIndex">从 0 开始的页码,超出范围时夹紧到合法区间。</param>
|
||||
public async Task InitializeAsync(string pdfFilePath, uint pageCount, bool compressLargePictures, uint initialPageIndex = 0)
|
||||
{
|
||||
_pdfPath = pdfFilePath ?? throw new ArgumentNullException(nameof(pdfFilePath));
|
||||
_pageCount = pageCount;
|
||||
_compressLargePictures = compressLargePictures;
|
||||
if (_pageCount == 0)
|
||||
_currentIndex = 0;
|
||||
else
|
||||
_currentIndex = initialPageIndex >= _pageCount ? _pageCount - 1 : initialPageIndex;
|
||||
|
||||
await ShowPageAsync(_currentIndex);
|
||||
}
|
||||
|
||||
public string PdfPath => _pdfPath;
|
||||
|
||||
public uint PageCount => _pageCount;
|
||||
|
||||
public uint CurrentPageIndex => _currentIndex;
|
||||
|
||||
public string PageLabelText => _pageCount == 0 ? "" : $"{_currentIndex + 1} / {_pageCount}";
|
||||
|
||||
public bool CanGoPrevious => !_isPagingBusy && _pageCount > 1 && _currentIndex > 0;
|
||||
|
||||
public bool CanGoNext => !_isPagingBusy && _pageCount > 1 && _currentIndex < _pageCount - 1;
|
||||
|
||||
public async Task GoToPreviousPageAsync()
|
||||
{
|
||||
await GoRelativeAsync(-1);
|
||||
}
|
||||
|
||||
public async Task GoToNextPageAsync()
|
||||
{
|
||||
await GoRelativeAsync(1);
|
||||
}
|
||||
|
||||
private void NotifyPageNavigationStateChanged()
|
||||
{
|
||||
PageNavigationStateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private async Task GoRelativeAsync(int delta)
|
||||
{
|
||||
if (_isPagingBusy || _pageCount <= 1)
|
||||
return;
|
||||
int next = (int)_currentIndex + delta;
|
||||
if (next < 0 || next >= _pageCount)
|
||||
return;
|
||||
_currentIndex = (uint)next;
|
||||
await ShowPageAsync(_currentIndex);
|
||||
}
|
||||
|
||||
private async Task ShowPageAsync(uint pageIndex)
|
||||
{
|
||||
_isPagingBusy = true;
|
||||
NotifyPageNavigationStateChanged();
|
||||
try
|
||||
{
|
||||
BitmapSource raw = await PdfWinRtHelper.RenderPageToBitmapSourceAsync(_pdfPath, pageIndex);
|
||||
if (raw == null)
|
||||
return;
|
||||
|
||||
BitmapSource display = ApplyCompressionIfNeeded(raw);
|
||||
_pageImage.Source = display;
|
||||
if (!_layoutSizeCommitted)
|
||||
{
|
||||
bool callerSized = !double.IsNaN(Width) && Width > 0 && !double.IsNaN(Height) && Height > 0;
|
||||
if (!callerSized)
|
||||
{
|
||||
Width = display.PixelWidth;
|
||||
Height = display.PixelHeight;
|
||||
}
|
||||
|
||||
_layoutSizeCommitted = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isPagingBusy = false;
|
||||
NotifyPageNavigationStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapSource ApplyCompressionIfNeeded(BitmapSource rendered)
|
||||
{
|
||||
int width = rendered.PixelWidth;
|
||||
int height = rendered.PixelHeight;
|
||||
if (_compressLargePictures && (width > 1920 || height > 1080))
|
||||
{
|
||||
double scaleX = 1920.0 / width;
|
||||
double scaleY = 1080.0 / height;
|
||||
double scale = Math.Min(scaleX, scaleY);
|
||||
return new TransformedBitmap(rendered, new ScaleTransform(scale, scale));
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<UserControl x:Class="Ink_Canvas.Controls.PptNavBar"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
|
||||
xmlns:local="clr-namespace:Ink_Canvas.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="50" d:DesignWidth="200">
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="PptNavBarItemForeground" Color="#71717a"/>
|
||||
<DataTemplate x:Key="PptPreviewItemTemplate">
|
||||
<Grid Margin="2,4">
|
||||
<Border CornerRadius="8" Background="Transparent" Padding="6">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PptNavBarItemForeground}"
|
||||
Text="{Binding SlideNumber}"/>
|
||||
<Image Grid.Column="1" Source="{Binding Thumbnail}" Stretch="Uniform"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<Style x:Key="PptPreviewItemContainerStyle" TargetType="ListBoxItem">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd" CornerRadius="8" Background="Transparent"
|
||||
BorderBrush="Transparent" BorderThickness="2">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#1affffff"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#66CCFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border x:Name="RootBorder" BorderThickness="1" BorderBrush="#a1a1aa"
|
||||
Background="#f4f4f5" Opacity="1" CornerRadius="6">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="#000000" BlurRadius="14" ShadowDepth="2" Opacity="0.16"/>
|
||||
</Border.Effect>
|
||||
<DockPanel x:Name="LayoutRoot" LastChildFill="True">
|
||||
<ListBox x:Name="PreviewList"
|
||||
Visibility="Collapsed"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Padding="6,8,6,4"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ItemTemplate="{StaticResource PptPreviewItemTemplate}"
|
||||
ItemContainerStyle="{StaticResource PptPreviewItemContainerStyle}"
|
||||
MouseUp="PreviewList_MouseUp"/>
|
||||
<ikw:SimpleStackPanel x:Name="ButtonRow" Orientation="Horizontal"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Border x:Name="PreviousButtonBorder" Width="50" Height="50"
|
||||
MouseDown="PreviousButton_MouseDown"
|
||||
MouseUp="PreviousButton_MouseUp"
|
||||
MouseLeave="PreviousButton_MouseLeave"
|
||||
CornerRadius="5 5 0 0" Background="Transparent">
|
||||
<Grid>
|
||||
<Border x:Name="PreviousButtonFeedbackBorder" Margin="3" CornerRadius="5"
|
||||
Background="#18181b" Opacity="0"/>
|
||||
<Image Height="28" Width="28">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
|
||||
<GeometryDrawing x:Name="PreviousButtonGeometry" Brush="#27272a"/>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border x:Name="PageButtonBorder" Visibility="Visible" TextBlock.Foreground="#171717"
|
||||
MouseDown="PageButton_MouseDown"
|
||||
MouseUp="PageButton_MouseUp"
|
||||
MouseLeave="PageButton_MouseLeave"
|
||||
MinWidth="50" MinHeight="50" Background="Transparent">
|
||||
<Grid>
|
||||
<Border x:Name="PageButtonFeedbackBorder" Margin="0,3" CornerRadius="5"
|
||||
Background="#18181b" Opacity="0"/>
|
||||
<ikw:SimpleStackPanel VerticalAlignment="Center" Orientation="Vertical"
|
||||
Spacing="0.5" Margin="12,0">
|
||||
<TextBlock x:Name="PageNowText" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontSize="17" FontWeight="Bold" Text="?"/>
|
||||
<TextBlock x:Name="PageTotalText" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontSize="10" Text="/ ?"/>
|
||||
</ikw:SimpleStackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border x:Name="NextButtonBorder" Width="50" Height="50"
|
||||
MouseDown="NextButton_MouseDown"
|
||||
MouseUp="NextButton_MouseUp"
|
||||
MouseLeave="NextButton_MouseLeave"
|
||||
CornerRadius="0 0 5 5" Background="Transparent">
|
||||
<Grid>
|
||||
<Border x:Name="NextButtonFeedbackBorder" Margin="3" CornerRadius="5"
|
||||
Background="#18181b" Opacity="0"/>
|
||||
<Image VerticalAlignment="Center" HorizontalAlignment="Center" Height="28" Width="28">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
|
||||
<GeometryDrawing x:Name="NextButtonGeometry" Brush="#27272a"/>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ikw:SimpleStackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,393 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// PPT 翻页 + 增强预览一体化控件。
|
||||
/// 通过 <see cref="Direction"/> 切换底部条 (LB/RB) 与侧边条 (LS/RS) 布局,
|
||||
/// 预览列表内嵌于同一个 Border,展开时占据按钮组之外的剩余空间。
|
||||
/// </summary>
|
||||
public partial class PptNavBar : UserControl
|
||||
{
|
||||
public sealed class PreviewItem
|
||||
{
|
||||
public int SlideNumber { get; set; }
|
||||
public BitmapImage Thumbnail { get; set; }
|
||||
}
|
||||
|
||||
public enum NavDirection
|
||||
{
|
||||
LeftBottom,
|
||||
RightBottom,
|
||||
LeftSide,
|
||||
RightSide
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register(
|
||||
nameof(Direction), typeof(NavDirection), typeof(PptNavBar),
|
||||
new PropertyMetadata(NavDirection.LeftBottom, OnDirectionChanged));
|
||||
|
||||
public static readonly DependencyProperty CurrentSlideProperty = DependencyProperty.Register(
|
||||
nameof(CurrentSlide), typeof(int), typeof(PptNavBar),
|
||||
new PropertyMetadata(0, OnPageChanged));
|
||||
|
||||
public static readonly DependencyProperty TotalSlidesProperty = DependencyProperty.Register(
|
||||
nameof(TotalSlides), typeof(int), typeof(PptNavBar),
|
||||
new PropertyMetadata(0, OnPageChanged));
|
||||
|
||||
public static readonly DependencyProperty PreviewItemsProperty = DependencyProperty.Register(
|
||||
nameof(PreviewItems), typeof(IList<PreviewItem>), typeof(PptNavBar),
|
||||
new PropertyMetadata(null, OnPreviewItemsChanged));
|
||||
|
||||
public static readonly DependencyProperty IsPreviewExpandedProperty = DependencyProperty.Register(
|
||||
nameof(IsPreviewExpanded), typeof(bool), typeof(PptNavBar),
|
||||
new PropertyMetadata(false, OnIsPreviewExpandedChanged));
|
||||
|
||||
public NavDirection Direction
|
||||
{
|
||||
get => (NavDirection)GetValue(DirectionProperty);
|
||||
set => SetValue(DirectionProperty, value);
|
||||
}
|
||||
|
||||
public int CurrentSlide
|
||||
{
|
||||
get => (int)GetValue(CurrentSlideProperty);
|
||||
set => SetValue(CurrentSlideProperty, value);
|
||||
}
|
||||
|
||||
public int TotalSlides
|
||||
{
|
||||
get => (int)GetValue(TotalSlidesProperty);
|
||||
set => SetValue(TotalSlidesProperty, value);
|
||||
}
|
||||
|
||||
public IList<PreviewItem> PreviewItems
|
||||
{
|
||||
get => (IList<PreviewItem>)GetValue(PreviewItemsProperty);
|
||||
set => SetValue(PreviewItemsProperty, value);
|
||||
}
|
||||
|
||||
public bool IsPreviewExpanded
|
||||
{
|
||||
get => (bool)GetValue(IsPreviewExpandedProperty);
|
||||
set => SetValue(IsPreviewExpandedProperty, value);
|
||||
}
|
||||
|
||||
public event EventHandler PreviousClick;
|
||||
public event EventHandler NextClick;
|
||||
public event EventHandler PageClick;
|
||||
public event EventHandler<int> SlideSelected;
|
||||
public event EventHandler PreviousPressedDown;
|
||||
public event EventHandler NextPressedDown;
|
||||
public event EventHandler PressEnded;
|
||||
public event EventHandler<bool> PreviewExpandedChanged;
|
||||
|
||||
// 静态几何(左下/右下:水平箭头;左侧/右侧:垂直箭头)
|
||||
private static readonly Geometry HArrowLeft = Geometry.Parse("F0 M24,24z M0,0z M3.3994,12.9642C2.86687,12.4317,2.86687,11.5683,3.3994,11.0358L9.94485,4.49031C10.4774,3.95777 11.3408,3.95777 11.8733,4.49031 12.4059,5.02284 12.4059,5.88625 11.8733,6.41878L7.65575,10.6364 19.6364,10.6364C20.3895,10.6364 21,11.2469 21,12 21,12.7531 20.3895,13.3636 19.6364,13.3636L7.65575,13.3636 11.8733,17.5812C12.4059,18.1137 12.4059,18.9772 11.8733,19.5097 11.3408,20.0422 10.4774,20.0422 9.94485,19.5097L3.3994,12.9642z");
|
||||
private static readonly Geometry HArrowRight = Geometry.Parse("F0 M24,24z M0,0z M20.6006,12.9642C21.1331,12.4317,21.1331,11.5683,20.6006,11.0358L14.0551,4.49031C13.5226,3.95777 12.6592,3.95777 12.1267,4.49031 11.5941,5.02284 11.5941,5.88625 12.1267,6.41878L16.3443,10.6364 4.36364,10.6364C3.61052,10.6364 3,11.2469 3,12 3,12.7531 3.61052,13.3636 4.36364,13.3636L16.3443,13.3636 12.1267,17.5812C11.5941,18.1137 11.5941,18.9772 12.1267,19.5097 12.6592,20.0422 13.5226,20.0422 14.0551,19.5097L20.6006,12.9642z");
|
||||
private static readonly Geometry VArrowUp = Geometry.Parse("F0 M24,24z M0,0z M11.0357,3.3994C11.5682,2.86687,12.4316,2.86687,12.9641,3.3994L19.5096,9.94485C20.0421,10.4774 20.0421,11.3408 19.5096,11.8733 18.9771,12.4059 18.1137,12.4059 17.5811,11.8733L13.3635,7.65575 13.3635,19.6364C13.3635,20.3895 12.753,21 11.9999,21 11.2468,21 10.6363,20.3895 10.6363,19.6364L10.6363,7.65575 6.41869,11.8733C5.88616,12.4059 5.02275,12.4059 4.49022,11.8733 3.95769,11.3408 3.95769,10.4774 4.49022,9.94485L11.0357,3.3994z");
|
||||
private static readonly Geometry VArrowDown = Geometry.Parse("F0 M24,24z M0,0z M11.0357,20.6006C11.5682,21.1331,12.4316,21.1331,12.9641,20.6006L19.5096,14.0551C20.0421,13.5226 20.0421,12.6592 19.5096,12.1267 18.9771,11.5941 18.1137,11.5941 17.5811,12.1267L13.3635,16.3443 13.3635,4.36364C13.3635,3.61052 12.753,3 11.9999,3 11.2468,3 10.6363,3.61052 10.6363,4.36364L10.6363,16.3443 6.41869,12.1267C5.88616,11.5941 5.02275,11.5941 4.49022,12.1267 3.95769,12.6592 3.95769,13.5226 4.49022,14.0551L11.0357,20.6006z");
|
||||
|
||||
public PptNavBar()
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplyDirection(Direction);
|
||||
}
|
||||
|
||||
private static void OnDirectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is PptNavBar bar) bar.ApplyDirection((NavDirection)e.NewValue);
|
||||
}
|
||||
|
||||
private static void OnPageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is PptNavBar bar) bar.RefreshPageText();
|
||||
}
|
||||
|
||||
private static void OnPreviewItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is PptNavBar bar)
|
||||
{
|
||||
bar.PreviewList.ItemsSource = e.NewValue as IList<PreviewItem>;
|
||||
bar.SyncPreviewSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnIsPreviewExpandedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is PptNavBar bar)
|
||||
{
|
||||
bool expanded = (bool)e.NewValue;
|
||||
bar.PreviewList.Visibility = expanded ? Visibility.Visible : Visibility.Collapsed;
|
||||
bar.ApplyLayout();
|
||||
if (expanded)
|
||||
{
|
||||
bar.SyncPreviewSelection();
|
||||
bar.HookOutsideClick();
|
||||
}
|
||||
else
|
||||
{
|
||||
bar.UnhookOutsideClick();
|
||||
}
|
||||
bar.PreviewExpandedChanged?.Invoke(bar, expanded);
|
||||
}
|
||||
}
|
||||
|
||||
private Window _hookedWindow;
|
||||
private void HookOutsideClick()
|
||||
{
|
||||
if (_hookedWindow != null) return;
|
||||
_hookedWindow = Window.GetWindow(this);
|
||||
if (_hookedWindow != null)
|
||||
{
|
||||
_hookedWindow.PreviewMouseDown += OnWindowPreviewMouseDown;
|
||||
}
|
||||
}
|
||||
private void UnhookOutsideClick()
|
||||
{
|
||||
if (_hookedWindow != null)
|
||||
{
|
||||
_hookedWindow.PreviewMouseDown -= OnWindowPreviewMouseDown;
|
||||
_hookedWindow = null;
|
||||
}
|
||||
}
|
||||
private void OnWindowPreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.OriginalSource is DependencyObject d && !IsDescendantOf(d, this))
|
||||
{
|
||||
IsPreviewExpanded = false;
|
||||
}
|
||||
}
|
||||
private static bool IsDescendantOf(DependencyObject child, DependencyObject ancestor)
|
||||
{
|
||||
while (child != null)
|
||||
{
|
||||
if (ReferenceEquals(child, ancestor)) return true;
|
||||
child = System.Windows.Media.VisualTreeHelper.GetParent(child)
|
||||
?? System.Windows.LogicalTreeHelper.GetParent(child);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ApplyDirection(NavDirection dir) => ApplyLayout();
|
||||
|
||||
private void ApplyLayout()
|
||||
{
|
||||
var dir = Direction;
|
||||
bool expanded = IsPreviewExpanded;
|
||||
|
||||
// 重置可能在不同状态下被设置的属性
|
||||
ButtonRow.ClearValue(WidthProperty);
|
||||
ButtonRow.ClearValue(HeightProperty);
|
||||
ButtonRow.ClearValue(HorizontalAlignmentProperty);
|
||||
PreviewList.ClearValue(WidthProperty);
|
||||
PreviewList.ClearValue(HeightProperty);
|
||||
PreviewList.ClearValue(MaxHeightProperty);
|
||||
PreviewList.ClearValue(MaxWidthProperty);
|
||||
PreviewList.ClearValue(HorizontalAlignmentProperty);
|
||||
ClearValue(HeightProperty);
|
||||
ClearValue(MaxHeightProperty);
|
||||
|
||||
double availableHeight = ComputeAvailableHeight();
|
||||
|
||||
switch (dir)
|
||||
{
|
||||
case NavDirection.LeftBottom:
|
||||
case NavDirection.RightBottom:
|
||||
DockPanel.SetDock(PreviewList, Dock.Top);
|
||||
DockPanel.SetDock(ButtonRow, Dock.Bottom);
|
||||
ButtonRow.Orientation = Orientation.Horizontal;
|
||||
ButtonRow.Height = 50;
|
||||
if (expanded)
|
||||
{
|
||||
// 预览面板拉宽到 280,贴向同侧角落
|
||||
PreviewList.Width = 280;
|
||||
PreviewList.MaxHeight = Math.Max(200, availableHeight - 50);
|
||||
PreviewList.HorizontalAlignment = dir == NavDirection.LeftBottom
|
||||
? HorizontalAlignment.Left
|
||||
: HorizontalAlignment.Right;
|
||||
// 按钮组宽度限制为原始内容宽度,并贴向同侧,保持按钮位置不变
|
||||
ButtonRow.HorizontalAlignment = dir == NavDirection.LeftBottom
|
||||
? HorizontalAlignment.Left
|
||||
: HorizontalAlignment.Right;
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviewList.SetBinding(WidthProperty, new System.Windows.Data.Binding(nameof(ButtonRow.ActualWidth)) { Source = ButtonRow });
|
||||
PreviewList.MaxHeight = 380;
|
||||
}
|
||||
PreviousButtonGeometry.Geometry = HArrowLeft;
|
||||
NextButtonGeometry.Geometry = HArrowRight;
|
||||
break;
|
||||
|
||||
case NavDirection.LeftSide:
|
||||
DockPanel.SetDock(PreviewList, Dock.Right);
|
||||
DockPanel.SetDock(ButtonRow, Dock.Left);
|
||||
ButtonRow.Orientation = Orientation.Vertical;
|
||||
ButtonRow.Width = 50;
|
||||
PreviewList.Width = 240;
|
||||
PreviewList.MaxHeight = 480;
|
||||
PreviousButtonGeometry.Geometry = VArrowUp;
|
||||
NextButtonGeometry.Geometry = VArrowDown;
|
||||
break;
|
||||
case NavDirection.RightSide:
|
||||
DockPanel.SetDock(PreviewList, Dock.Left);
|
||||
DockPanel.SetDock(ButtonRow, Dock.Right);
|
||||
ButtonRow.Orientation = Orientation.Vertical;
|
||||
ButtonRow.Width = 50;
|
||||
PreviewList.Width = 240;
|
||||
PreviewList.MaxHeight = 480;
|
||||
PreviousButtonGeometry.Geometry = VArrowUp;
|
||||
NextButtonGeometry.Geometry = VArrowDown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private double ComputeAvailableHeight()
|
||||
{
|
||||
var window = Window.GetWindow(this);
|
||||
double h = window != null ? window.ActualHeight : SystemParameters.PrimaryScreenHeight;
|
||||
return Math.Max(240, h - 12);
|
||||
}
|
||||
|
||||
private void RefreshPageText()
|
||||
{
|
||||
if (CurrentSlide > 0 && TotalSlides > 0)
|
||||
{
|
||||
PageNowText.Text = CurrentSlide.ToString();
|
||||
PageTotalText.Text = $"/ {TotalSlides}";
|
||||
}
|
||||
else
|
||||
{
|
||||
PageNowText.Text = "?";
|
||||
PageTotalText.Text = "/ ?";
|
||||
}
|
||||
SyncPreviewSelection();
|
||||
}
|
||||
|
||||
private void SyncPreviewSelection()
|
||||
{
|
||||
if (PreviewItems == null || CurrentSlide <= 0) return;
|
||||
foreach (var item in PreviewItems)
|
||||
{
|
||||
if (item.SlideNumber == CurrentSlide)
|
||||
{
|
||||
PreviewList.SelectedItem = item;
|
||||
PreviewList.ScrollIntoView(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetFeedback(Border feedback, double opacity) => feedback.Opacity = opacity;
|
||||
|
||||
private object _lastDown;
|
||||
|
||||
private void PreviousButton_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_lastDown = sender;
|
||||
SetFeedback(PreviousButtonFeedbackBorder, 0.15);
|
||||
PreviousPressedDown?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void PreviousButton_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
SetFeedback(PreviousButtonFeedbackBorder, 0);
|
||||
PressEnded?.Invoke(this, EventArgs.Empty);
|
||||
if (_lastDown != sender) return;
|
||||
_lastDown = null;
|
||||
PreviousClick?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void PreviousButton_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
SetFeedback(PreviousButtonFeedbackBorder, 0);
|
||||
_lastDown = null;
|
||||
PressEnded?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void NextButton_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_lastDown = sender;
|
||||
SetFeedback(NextButtonFeedbackBorder, 0.15);
|
||||
NextPressedDown?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void NextButton_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
SetFeedback(NextButtonFeedbackBorder, 0);
|
||||
PressEnded?.Invoke(this, EventArgs.Empty);
|
||||
if (_lastDown != sender) return;
|
||||
_lastDown = null;
|
||||
NextClick?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void NextButton_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
SetFeedback(NextButtonFeedbackBorder, 0);
|
||||
_lastDown = null;
|
||||
PressEnded?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void PageButton_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_lastDown = sender;
|
||||
SetFeedback(PageButtonFeedbackBorder, 0.15);
|
||||
}
|
||||
|
||||
private void PageButton_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
SetFeedback(PageButtonFeedbackBorder, 0);
|
||||
if (_lastDown != sender) return;
|
||||
_lastDown = null;
|
||||
PageClick?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void PageButton_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
SetFeedback(PageButtonFeedbackBorder, 0);
|
||||
_lastDown = null;
|
||||
}
|
||||
|
||||
private void PreviewList_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (PreviewList.SelectedItem is PreviewItem item)
|
||||
{
|
||||
SlideSelected?.Invoke(this, item.SlideNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyTheme(bool isDark)
|
||||
{
|
||||
var fgBrush = isDark ? Brushes.White : new SolidColorBrush(Color.FromRgb(39, 39, 42));
|
||||
var feedbackBrush = isDark ? Brushes.White : new SolidColorBrush(Color.FromRgb(24, 24, 27));
|
||||
var bgBrush = isDark
|
||||
? new SolidColorBrush(Color.FromRgb(39, 39, 42))
|
||||
: new SolidColorBrush(Color.FromRgb(244, 244, 245));
|
||||
var borderBrush = isDark
|
||||
? new SolidColorBrush(Color.FromRgb(82, 82, 91))
|
||||
: new SolidColorBrush(Color.FromRgb(161, 161, 170));
|
||||
|
||||
PreviousButtonGeometry.Brush = fgBrush;
|
||||
NextButtonGeometry.Brush = fgBrush;
|
||||
PreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
NextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
PageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
PageNowText.Foreground = fgBrush;
|
||||
PageTotalText.Foreground = fgBrush;
|
||||
RootBorder.Background = bgBrush;
|
||||
RootBorder.BorderBrush = borderBrush;
|
||||
Resources["PptNavBarItemForeground"] = fgBrush;
|
||||
}
|
||||
|
||||
public void SetPageButtonVisibility(Visibility v) => PageButtonBorder.Visibility = v;
|
||||
public void SetBarOpacity(double opacity) => RootBorder.Opacity = opacity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// 工具栏按钮插件与宿主之间的桥梁。Phase 1 粗粒度暴露 MainWindow,后续收窄。
|
||||
/// </summary>
|
||||
public interface IToolbarHost
|
||||
{
|
||||
MainWindow Window { get; }
|
||||
|
||||
/// <summary>按 id 登记按钮的 view 实例(供 MainWindow 字段回填和互相查找)。</summary>
|
||||
void RegisterView(string id, FrameworkElement view);
|
||||
|
||||
/// <summary>按 id 获取之前注册的 view。不存在返回 null。</summary>
|
||||
FrameworkElement FindView(string id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// 一个工具栏按钮(或任意浮动栏/白板栏条目)的插件化契约。
|
||||
/// 实现类必须有无参构造函数,启动时会被 ToolbarRegistry 反射实例化。
|
||||
/// </summary>
|
||||
public interface IToolbarItem
|
||||
{
|
||||
/// <summary>稳定、唯一的 id,用于持久化用户配置。不要随便改。</summary>
|
||||
string Id { get; }
|
||||
|
||||
ToolbarSlot DefaultSlot { get; }
|
||||
|
||||
/// <summary>同一 slot 内的默认顺序,小的在前。</summary>
|
||||
int DefaultOrder { get; }
|
||||
|
||||
bool DefaultVisible { get; }
|
||||
|
||||
ToolbarInsertPosition DefaultPosition { get; }
|
||||
|
||||
/// <summary>仅当 Position 为 BeforeAnchor/AfterAnchor 时有意义,对应 XAML 里 x:Name。</summary>
|
||||
string DefaultAnchorName { get; }
|
||||
|
||||
/// <summary>构造 UI 元素并接线所有行为。</summary>
|
||||
FrameworkElement BuildView(IToolbarHost host);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
/// <summary>
|
||||
/// 清空按钮。位置:夹在颜色面板与 StackPanelCanvasControls 之间,
|
||||
/// 所以用 BeforeAnchor 锚到 StackPanelCanvasControls。
|
||||
/// </summary>
|
||||
internal sealed class ClearToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.clear";
|
||||
public override string LocalizationKey => "FloatingBar_Clear";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
|
||||
public override int DefaultOrder => 0;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.BeforeAnchor;
|
||||
public override string DefaultAnchorName => "StackPanelCanvasControls";
|
||||
|
||||
protected override string IconBrushResourceKey => "RedBrush";
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconDelete_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachSymbolIconDelete(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class CursorToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.cursor";
|
||||
public override string LocalizationKey => "FloatingBar_Mouse";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
|
||||
public override int DefaultOrder => 100;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.CursorIcon_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachCursorIconView(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class CursorWithDelToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.cursorWithDel";
|
||||
public override string LocalizationKey => "FloatingBar_ClearAndMouse";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 320;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.CursorWithDelIcon_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachCursorWithDelBtn(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class EraserByStrokesToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.eraserByStrokes";
|
||||
public override string LocalizationKey => "FloatingBar_StrokeEraser";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 110;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.EraserIconByStrokes_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachEraserByStrokesIcon(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class EraserToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.eraser";
|
||||
public override string LocalizationKey => "FloatingBar_AreaEraser";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 100;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.EraserIcon_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachEraserIcon(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class FoldToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.fold";
|
||||
public override string LocalizationKey => "FloatingBar_Hide";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
|
||||
public override int DefaultOrder => 120;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
|
||||
public override string DefaultAnchorName => "FloatingBarEndSeparator";
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.FoldFloatingBar_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachFoldIcon(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class PenToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.pen";
|
||||
public override string LocalizationKey => "FloatingBar_Annotate";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
|
||||
public override int DefaultOrder => 110;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.PenIcon_Click(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachPenIconView(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class RedoToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.redo";
|
||||
public override string LocalizationKey => "Board_Redo";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 310;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconRedo_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
{
|
||||
host.Window.AttachSymbolIconRedo(view);
|
||||
view.SetBinding(System.Windows.UIElement.IsEnabledProperty,
|
||||
new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnRedo" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class SelectToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.select";
|
||||
public override string LocalizationKey => "FloatingBar_LassoSelect";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 120;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconSelect_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachSymbolIconSelect(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class ShapeDrawToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.shapeDraw";
|
||||
public override string LocalizationKey => "FloatingBar_Geometry";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 130;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.ImageDrawShape_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachShapeDrawBtn(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Ink_Canvas.Properties;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用 ToolbarImageButton 工具栏条目基类——大幅减少每个按钮的样板代码。
|
||||
/// 派生类通常只需给 Id / 本地化键 / Slot / Order / 点击处理 / Attach 回填。
|
||||
/// </summary>
|
||||
internal abstract class ToolbarImageButtonItemBase : IToolbarItem
|
||||
{
|
||||
public abstract string Id { get; }
|
||||
public abstract string LocalizationKey { get; }
|
||||
public abstract ToolbarSlot DefaultSlot { get; }
|
||||
public abstract int DefaultOrder { get; }
|
||||
public virtual bool DefaultVisible => true;
|
||||
public virtual ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Prepend;
|
||||
public virtual string DefaultAnchorName => null;
|
||||
|
||||
/// <summary>DynamicResource 名称,用于 IconBrush。默认为 null(使用控件自带前景色)。</summary>
|
||||
protected virtual string IconBrushResourceKey => null;
|
||||
|
||||
protected abstract void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e);
|
||||
|
||||
/// <summary>构建后调用,用于回填 MainWindow 的原命名属性(partial 扩展里的 Attach*)。可选。</summary>
|
||||
protected virtual void AfterBuild(IToolbarHost host, ToolbarImageButton view) { }
|
||||
|
||||
public FrameworkElement BuildView(IToolbarHost host)
|
||||
{
|
||||
var btn = new ToolbarImageButton
|
||||
{
|
||||
Label = Strings.GetString(LocalizationKey) ?? LocalizationKey
|
||||
};
|
||||
if (!string.IsNullOrEmpty(IconBrushResourceKey))
|
||||
{
|
||||
if (btn.TryFindResource(IconBrushResourceKey) is Brush brush) btn.IconBrush = brush;
|
||||
else btn.SetResourceReference(ToolbarImageButton.IconBrushProperty, IconBrushResourceKey);
|
||||
}
|
||||
btn.ButtonMouseUp += (s, e) => OnClick(host, s, e);
|
||||
AfterBuild(host, btn);
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class ToolsToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.tools";
|
||||
public override string LocalizationKey => "Board_Tools";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
|
||||
public override int DefaultOrder => 110;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
|
||||
public override string DefaultAnchorName => "FloatingBarEndSeparator";
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconTools_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachToolsBtn(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class UndoToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.undo";
|
||||
public override string LocalizationKey => "Board_Undo";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
|
||||
public override int DefaultOrder => 300;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.SymbolIconUndo_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
{
|
||||
host.Window.AttachSymbolIconUndo(view);
|
||||
view.SetBinding(System.Windows.UIElement.IsEnabledProperty,
|
||||
new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnUndo" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar.Items
|
||||
{
|
||||
internal sealed class WhiteboardToolItem : ToolbarImageButtonItemBase
|
||||
{
|
||||
public override string Id => "builtin.whiteboard";
|
||||
public override string LocalizationKey => "FloatingBar_Whiteboard";
|
||||
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
|
||||
public override int DefaultOrder => 100;
|
||||
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
|
||||
public override string DefaultAnchorName => "FloatingBarEndSeparator";
|
||||
|
||||
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
|
||||
=> host.Window.ImageBlackboard_MouseUp(sender, e);
|
||||
|
||||
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
|
||||
=> host.Window.AttachWhiteboardBtn(view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// MainWindow 版的 IToolbarHost 实现。Phase 1 直接把 MainWindow 引用暴露给插件,
|
||||
/// 插件可通过 host.Window 访问私有/内部成员(partial class 扩展或 internal 字段)。
|
||||
/// 后续阶段逐步把具体行为抽成 Host 上的方法/事件,收窄这个接口。
|
||||
/// </summary>
|
||||
public sealed class ToolbarHost : IToolbarHost
|
||||
{
|
||||
private readonly Dictionary<string, FrameworkElement> _views = new Dictionary<string, FrameworkElement>();
|
||||
|
||||
public ToolbarHost(MainWindow window)
|
||||
{
|
||||
Window = window;
|
||||
}
|
||||
|
||||
public MainWindow Window { get; }
|
||||
|
||||
public void RegisterView(string id, FrameworkElement view)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id) || view == null) return;
|
||||
_views[id] = view;
|
||||
}
|
||||
|
||||
public FrameworkElement FindView(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return null;
|
||||
return _views.TryGetValue(id, out var v) ? v : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
public enum ToolbarInsertPosition
|
||||
{
|
||||
/// <summary>从容器头部依次插入;Order 小的在前。</summary>
|
||||
Prepend,
|
||||
/// <summary>追加到容器末尾。</summary>
|
||||
Append,
|
||||
/// <summary>插入到由 AnchorName 指定的已有元素之前。</summary>
|
||||
BeforeAnchor,
|
||||
/// <summary>插入到由 AnchorName 指定的已有元素之后(同一锚点多项按 Order 依次排列)。</summary>
|
||||
AfterAnchor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个工具栏按钮的用户配置(可见性、顺序、所属 slot、插入位置)。
|
||||
/// 由 Settings.Toolbar 持久化。
|
||||
/// </summary>
|
||||
public class ToolbarItemConfig
|
||||
{
|
||||
[JsonProperty("visible")]
|
||||
public bool Visible { get; set; } = true;
|
||||
|
||||
[JsonProperty("order")]
|
||||
public int Order { get; set; }
|
||||
|
||||
[JsonProperty("slot")]
|
||||
public ToolbarSlot Slot { get; set; } = ToolbarSlot.FloatingBarMain;
|
||||
|
||||
[JsonProperty("position")]
|
||||
public ToolbarInsertPosition Position { get; set; } = ToolbarInsertPosition.Prepend;
|
||||
|
||||
[JsonProperty("anchorName")]
|
||||
public string AnchorName { get; set; }
|
||||
}
|
||||
|
||||
public class ToolbarLayoutSettings
|
||||
{
|
||||
[JsonProperty("items")]
|
||||
public Dictionary<string, ToolbarItemConfig> Items { get; set; } = new Dictionary<string, ToolbarItemConfig>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Ink_Canvas.Helpers;
|
||||
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描当前程序集里的 IToolbarItem 实现,按用户配置(Settings.Toolbar)排序/过滤后注入到目标容器。
|
||||
/// </summary>
|
||||
public static class ToolbarRegistry
|
||||
{
|
||||
private static List<IToolbarItem> _items;
|
||||
|
||||
public static IReadOnlyList<IToolbarItem> Discover()
|
||||
{
|
||||
if (_items != null) return _items;
|
||||
|
||||
var itemType = typeof(IToolbarItem);
|
||||
_items = Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(t => !t.IsAbstract && !t.IsInterface && itemType.IsAssignableFrom(t))
|
||||
.Select(t =>
|
||||
{
|
||||
try { return (IToolbarItem)Activator.CreateInstance(t); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ToolbarRegistry: 实例化 {t.FullName} 失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(i => i != null)
|
||||
.ToList();
|
||||
return _items;
|
||||
}
|
||||
|
||||
/// <summary>按 slot 分配工具栏条目到对应容器。调用者负责清空目标容器里要被接管的旧内容。</summary>
|
||||
public static void Populate(IToolbarHost host, IDictionary<ToolbarSlot, Panel> slots, ToolbarLayoutSettings layout)
|
||||
{
|
||||
if (host == null || slots == null) return;
|
||||
layout = layout ?? new ToolbarLayoutSettings();
|
||||
|
||||
var grouped = new Dictionary<ToolbarSlot, List<(IToolbarItem item, ToolbarItemConfig cfg)>>();
|
||||
foreach (var item in Discover())
|
||||
{
|
||||
if (!layout.Items.TryGetValue(item.Id, out var cfg))
|
||||
{
|
||||
cfg = new ToolbarItemConfig
|
||||
{
|
||||
Visible = item.DefaultVisible,
|
||||
Order = item.DefaultOrder,
|
||||
Slot = item.DefaultSlot,
|
||||
Position = item.DefaultPosition,
|
||||
AnchorName = item.DefaultAnchorName
|
||||
};
|
||||
}
|
||||
if (!cfg.Visible) continue;
|
||||
if (!grouped.TryGetValue(cfg.Slot, out var list))
|
||||
{
|
||||
list = new List<(IToolbarItem, ToolbarItemConfig)>();
|
||||
grouped[cfg.Slot] = list;
|
||||
}
|
||||
list.Add((item, cfg));
|
||||
}
|
||||
|
||||
foreach (var kv in grouped)
|
||||
{
|
||||
if (!slots.TryGetValue(kv.Key, out var container) || container == null) continue;
|
||||
InjectIntoContainer(host, container, kv.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InjectIntoContainer(IToolbarHost host, Panel container,
|
||||
List<(IToolbarItem item, ToolbarItemConfig cfg)> entries)
|
||||
{
|
||||
// 按 Position 分桶,每桶内按 Order 升序。
|
||||
var prepend = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Prepend).OrderBy(e => e.cfg.Order).ToList();
|
||||
var append = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Append).OrderBy(e => e.cfg.Order).ToList();
|
||||
var before = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.BeforeAnchor).ToList();
|
||||
var after = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.AfterAnchor).ToList();
|
||||
|
||||
var prependIndex = 0;
|
||||
foreach (var entry in prepend)
|
||||
{
|
||||
var view = BuildAndRegister(host, entry.item);
|
||||
if (view == null) continue;
|
||||
container.Children.Insert(prependIndex++, view);
|
||||
}
|
||||
|
||||
foreach (var entry in append)
|
||||
{
|
||||
var view = BuildAndRegister(host, entry.item);
|
||||
if (view == null) continue;
|
||||
container.Children.Add(view);
|
||||
}
|
||||
|
||||
foreach (var group in before.GroupBy(e => e.cfg.AnchorName))
|
||||
{
|
||||
var anchor = FindNamedChild(container, group.Key);
|
||||
if (anchor == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (BeforeAnchor)", LogHelper.LogType.Warning);
|
||||
continue;
|
||||
}
|
||||
var idx = container.Children.IndexOf(anchor);
|
||||
foreach (var entry in group.OrderBy(e => e.cfg.Order))
|
||||
{
|
||||
var view = BuildAndRegister(host, entry.item);
|
||||
if (view == null) continue;
|
||||
container.Children.Insert(idx++, view);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var group in after.GroupBy(e => e.cfg.AnchorName))
|
||||
{
|
||||
var anchor = FindNamedChild(container, group.Key);
|
||||
if (anchor == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (AfterAnchor)", LogHelper.LogType.Warning);
|
||||
continue;
|
||||
}
|
||||
var idx = container.Children.IndexOf(anchor) + 1;
|
||||
foreach (var entry in group.OrderBy(e => e.cfg.Order))
|
||||
{
|
||||
var view = BuildAndRegister(host, entry.item);
|
||||
if (view == null) continue;
|
||||
container.Children.Insert(idx++, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static UIElement FindNamedChild(Panel container, string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return null;
|
||||
foreach (UIElement child in container.Children)
|
||||
{
|
||||
if (child is FrameworkElement fe && fe.Name == name) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static FrameworkElement BuildAndRegister(IToolbarHost host, IToolbarItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
var view = item.BuildView(host);
|
||||
if (view == null) return null;
|
||||
host.RegisterView(item.Id, view);
|
||||
return view;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"ToolbarRegistry: 构建 {item.Id} 失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Ink_Canvas.Controls.Toolbar
|
||||
{
|
||||
public enum ToolbarSlot
|
||||
{
|
||||
FloatingBarMain,
|
||||
FloatingBarCanvasControls,
|
||||
FloatingBarEnd,
|
||||
BlackboardLeft,
|
||||
BlackboardRight
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,22 @@ using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using Ink_Canvas.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
internal class AnimationsHelper
|
||||
{
|
||||
private static UIElement ResolveAnimationTarget(UIElement element)
|
||||
{
|
||||
if (element is BoardMenuFrame frame)
|
||||
{
|
||||
frame.ApplyTemplate();
|
||||
return frame.AnimationTarget ?? element;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
public static void ShowWithFadeIn(UIElement element, double duration = 0.15)
|
||||
{
|
||||
if (element.Visibility == Visibility.Visible) return;
|
||||
@@ -36,14 +47,17 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
if (element.Visibility == Visibility.Visible) return;
|
||||
|
||||
if (element == null)
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
|
||||
if (element.Visibility == Visibility.Visible) return;
|
||||
|
||||
element.Visibility = Visibility.Visible;
|
||||
|
||||
var target = ResolveAnimationTarget(element);
|
||||
|
||||
var sb = new Storyboard();
|
||||
|
||||
// 渐变动画
|
||||
var fadeInAnimation = new DoubleAnimation
|
||||
{
|
||||
From = 0.5,
|
||||
@@ -54,10 +68,9 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(UIElement.OpacityProperty));
|
||||
|
||||
// 滑动动画
|
||||
var slideAnimation = new DoubleAnimation
|
||||
{
|
||||
From = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
|
||||
From = 10,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromSeconds(duration)
|
||||
};
|
||||
@@ -68,10 +81,9 @@ namespace Ink_Canvas.Helpers
|
||||
sb.Children.Add(fadeInAnimation);
|
||||
sb.Children.Add(slideAnimation);
|
||||
|
||||
element.Visibility = Visibility.Visible;
|
||||
element.RenderTransform = new TranslateTransform();
|
||||
target.RenderTransform = new TranslateTransform();
|
||||
|
||||
sb.Begin((FrameworkElement)element);
|
||||
sb.Begin((FrameworkElement)target);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
@@ -207,14 +219,15 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
if (element.Visibility == Visibility.Collapsed) return;
|
||||
|
||||
if (element == null)
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
|
||||
if (element.Visibility == Visibility.Collapsed) return;
|
||||
|
||||
var target = ResolveAnimationTarget(element);
|
||||
|
||||
var sb = new Storyboard();
|
||||
|
||||
// 渐变动画
|
||||
var fadeOutAnimation = new DoubleAnimation
|
||||
{
|
||||
From = 1,
|
||||
@@ -224,11 +237,10 @@ namespace Ink_Canvas.Helpers
|
||||
fadeOutAnimation.EasingFunction = new CubicEase();
|
||||
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(UIElement.OpacityProperty));
|
||||
|
||||
// 滑动动画
|
||||
var slideAnimation = new DoubleAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
|
||||
To = 10,
|
||||
Duration = TimeSpan.FromSeconds(duration)
|
||||
};
|
||||
slideAnimation.EasingFunction = new CubicEase();
|
||||
@@ -243,8 +255,8 @@ namespace Ink_Canvas.Helpers
|
||||
element.Visibility = Visibility.Collapsed;
|
||||
};
|
||||
|
||||
element.RenderTransform = new TranslateTransform();
|
||||
sb.Begin((FrameworkElement)element);
|
||||
target.RenderTransform = new TranslateTransform();
|
||||
sb.Begin((FrameworkElement)target);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using Ink_Canvas.Windows.SettingsViews.Helpers;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Principal;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public static class AppRestartHelper
|
||||
{
|
||||
public static bool IsRunningAsAdmin()
|
||||
{
|
||||
try
|
||||
{
|
||||
var identity = WindowsIdentity.GetCurrent();
|
||||
var principal = new WindowsPrincipal(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void RestartApp(bool asAdmin)
|
||||
{
|
||||
try
|
||||
{
|
||||
App.IsAppExitByUser = true;
|
||||
|
||||
(Application.Current as App)?.ReleaseMutexForRestart();
|
||||
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
|
||||
if (asAdmin)
|
||||
{
|
||||
var psi = new ProcessStartInfo(exePath) { UseShellExecute = true, Verb = "runas" };
|
||||
Process.Start(psi);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 当前已是管理员时,直接通过用户令牌降权启动,避免经由 explorer 中转的延迟
|
||||
if (IsRunningAsAdmin() && UIAccessHelper.RestartAsNormalUser())
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start("explorer.exe", "\"" + exePath + "\"");
|
||||
}
|
||||
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"重启应用时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void RestartWithCurrentPrivileges()
|
||||
{
|
||||
RestartApp(IsRunningAsAdmin());
|
||||
}
|
||||
|
||||
public static void RestartAsAdmin()
|
||||
{
|
||||
RestartApp(true);
|
||||
}
|
||||
|
||||
public static void RestartAsNormal()
|
||||
{
|
||||
RestartApp(false);
|
||||
}
|
||||
|
||||
public static void SwitchToUIATopMostAndRestart()
|
||||
{
|
||||
try
|
||||
{
|
||||
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = true;
|
||||
|
||||
if (!SettingsManager.Settings.Advanced.IsAlwaysOnTop)
|
||||
{
|
||||
SettingsManager.Settings.Advanced.IsAlwaysOnTop = true;
|
||||
}
|
||||
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
|
||||
App.IsUIAccessTopMostEnabled = true;
|
||||
RestartApp(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"切换到UIA置顶模式时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void SwitchToNormalTopMostAndRestart()
|
||||
{
|
||||
try
|
||||
{
|
||||
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = false;
|
||||
SettingsManager.SaveSettingsToFile();
|
||||
|
||||
App.IsUIAccessTopMostEnabled = false;
|
||||
RestartApp(IsRunningAsAdmin());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"切换到普通置顶模式时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,118 @@ namespace Ink_Canvas.Helpers
|
||||
private static readonly string updatesFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "AutoUpdate");
|
||||
private static string statusFilePath;
|
||||
|
||||
// 全局下载取消令牌;UI 通过 RequestCancelDownload 取消当前下载
|
||||
private static CancellationTokenSource _activeDownloadCts;
|
||||
private static readonly object _activeDownloadLock = new object();
|
||||
|
||||
public static void RequestCancelDownload()
|
||||
{
|
||||
lock (_activeDownloadLock)
|
||||
{
|
||||
try { _activeDownloadCts?.Cancel(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static CancellationTokenSource BeginDownloadSession()
|
||||
{
|
||||
lock (_activeDownloadLock)
|
||||
{
|
||||
try { _activeDownloadCts?.Cancel(); } catch { }
|
||||
_activeDownloadCts = new CancellationTokenSource();
|
||||
return _activeDownloadCts;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EndDownloadSession(CancellationTokenSource cts)
|
||||
{
|
||||
lock (_activeDownloadLock)
|
||||
{
|
||||
if (ReferenceEquals(_activeDownloadCts, cts)) _activeDownloadCts = null;
|
||||
}
|
||||
try { cts?.Dispose(); } catch { }
|
||||
}
|
||||
|
||||
public static bool IsX64UpdatePackageSelected()
|
||||
{
|
||||
try
|
||||
{
|
||||
return MainWindow.Settings?.Startup?.UpdatePackageArchitecture == UpdatePackageArchitecture.X64;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeVersionForUpdate(string version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(version)) return version;
|
||||
return version.Trim().TrimStart('v', 'V');
|
||||
}
|
||||
|
||||
public static string AppendX64SuffixBeforeZipExtension(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url) || !IsX64UpdatePackageSelected()) return url;
|
||||
int query = url.IndexOf('?');
|
||||
string pathPart = query >= 0 ? url.Substring(0, query) : url;
|
||||
string qs = query >= 0 ? url.Substring(query) : "";
|
||||
const string ext = ".zip";
|
||||
int idx = pathPart.LastIndexOf(ext, StringComparison.OrdinalIgnoreCase);
|
||||
if (idx < 0) return url;
|
||||
var basePart = pathPart.Substring(0, idx);
|
||||
if (basePart.EndsWith("-x64", StringComparison.OrdinalIgnoreCase)) return url;
|
||||
return basePart + "-x64" + ext + qs;
|
||||
}
|
||||
|
||||
public static string GetUpdateZipFileName(string version)
|
||||
{
|
||||
var v = NormalizeVersionForUpdate(version);
|
||||
return IsX64UpdatePackageSelected()
|
||||
? $"InkCanvasForClass.CE.{v}-x64.zip"
|
||||
: $"InkCanvasForClass.CE.{v}.zip";
|
||||
}
|
||||
|
||||
public static string GetUpdateDownloadStatusFilePath(string version)
|
||||
{
|
||||
var v = NormalizeVersionForUpdate(version);
|
||||
string name = IsX64UpdatePackageSelected()
|
||||
? $"DownloadV{v}_x64Status.txt"
|
||||
: $"DownloadV{v}Status.txt";
|
||||
return Path.Combine(updatesFolderPath, name);
|
||||
}
|
||||
|
||||
public static string GetLocalUpdateZipFilePath(string version)
|
||||
{
|
||||
return Path.Combine(updatesFolderPath, GetUpdateZipFileName(version));
|
||||
}
|
||||
|
||||
private static string PickBrowserDownloadUrlFromAssets(JToken assets)
|
||||
{
|
||||
if (assets == null || !assets.Any()) return null;
|
||||
bool wantX64 = IsX64UpdatePackageSelected();
|
||||
string anyZip = null;
|
||||
string x64Zip = null;
|
||||
string nonX64Zip = null;
|
||||
foreach (JToken a in assets)
|
||||
{
|
||||
string name = a["name"]?.ToString();
|
||||
string url = a["browser_download_url"]?.ToString();
|
||||
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(url)) continue;
|
||||
if (!name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (anyZip == null) anyZip = url;
|
||||
if (name.EndsWith("-x64.zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (x64Zip == null) x64Zip = url;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (nonX64Zip == null) nonX64Zip = url;
|
||||
}
|
||||
}
|
||||
if (wantX64)
|
||||
return x64Zip ?? anyZip;
|
||||
return nonX64Zip ?? anyZip;
|
||||
}
|
||||
|
||||
// 线路组结构体(包含版本、下载、日志地址)
|
||||
public class UpdateLineGroup
|
||||
@@ -302,6 +414,8 @@ namespace Ink_Canvas.Helpers
|
||||
// 获取所有可用线路组,按延迟排序
|
||||
public static async Task<List<UpdateLineGroup>> GetAvailableLineGroupsOrdered(UpdateChannel channel)
|
||||
{
|
||||
var cached = TryGetCachedOrderedGroups(channel);
|
||||
if (cached != null) return cached;
|
||||
var groups = ChannelLineGroups[channel];
|
||||
var availableGroups = new List<(UpdateLineGroup group, long delay)>();
|
||||
|
||||
@@ -317,6 +431,7 @@ namespace Ink_Canvas.Helpers
|
||||
if (!string.IsNullOrEmpty(group.DownloadUrlFormat))
|
||||
{
|
||||
testUrl = group.DownloadUrlFormat.Replace("{0}", "test");
|
||||
testUrl = AppendX64SuffixBeforeZipExtension(testUrl);
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -386,9 +501,46 @@ namespace Ink_Canvas.Helpers
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
CacheOrderedGroups(channel, orderedGroups);
|
||||
return orderedGroups;
|
||||
}
|
||||
|
||||
// 缓存按延迟排序后的线路组,避免短时间内重复测速
|
||||
private static readonly Dictionary<UpdateChannel, (List<UpdateLineGroup> groups, DateTime cachedAt)> _orderedGroupsCache
|
||||
= new Dictionary<UpdateChannel, (List<UpdateLineGroup>, DateTime)>();
|
||||
private static readonly TimeSpan _orderedGroupsCacheTtl = TimeSpan.FromMinutes(15);
|
||||
|
||||
private static List<UpdateLineGroup> TryGetCachedOrderedGroups(UpdateChannel channel)
|
||||
{
|
||||
lock (_orderedGroupsCache)
|
||||
{
|
||||
if (_orderedGroupsCache.TryGetValue(channel, out var entry) &&
|
||||
entry.groups != null && entry.groups.Count > 0 &&
|
||||
DateTime.UtcNow - entry.cachedAt < _orderedGroupsCacheTtl)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 复用线路组延迟检测缓存({entry.groups.Count} 个)");
|
||||
return new List<UpdateLineGroup>(entry.groups);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CacheOrderedGroups(UpdateChannel channel, List<UpdateLineGroup> groups)
|
||||
{
|
||||
lock (_orderedGroupsCache)
|
||||
{
|
||||
_orderedGroupsCache[channel] = (new List<UpdateLineGroup>(groups), DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
public static void InvalidateOrderedGroupsCache()
|
||||
{
|
||||
lock (_orderedGroupsCache)
|
||||
{
|
||||
_orderedGroupsCache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<long> GetDownloadUrlDelay(string url)
|
||||
{
|
||||
try
|
||||
@@ -608,7 +760,7 @@ namespace Ink_Canvas.Helpers
|
||||
if (version == targetVersion || version == $"v{targetVersion}" || version == $"V{targetVersion}")
|
||||
{
|
||||
string releaseNotes = release["body"]?.ToString();
|
||||
string downloadUrl = release["assets"]?.First?["browser_download_url"]?.ToString();
|
||||
string downloadUrl = PickBrowserDownloadUrlFromAssets(release["assets"]);
|
||||
|
||||
// 解析发布时间
|
||||
DateTime? releaseTime = null;
|
||||
@@ -649,7 +801,7 @@ namespace Ink_Canvas.Helpers
|
||||
var latestRelease = releases[0];
|
||||
string version = latestRelease["tag_name"]?.ToString();
|
||||
string releaseNotes = latestRelease["body"]?.ToString();
|
||||
string downloadUrl = latestRelease["assets"]?.First?["browser_download_url"]?.ToString();
|
||||
string downloadUrl = PickBrowserDownloadUrlFromAssets(latestRelease["assets"]);
|
||||
|
||||
DateTime? releaseTime = null;
|
||||
if (latestRelease["published_at"] != null && DateTime.TryParse(latestRelease["published_at"].ToString(), out DateTime parsedTime))
|
||||
@@ -675,7 +827,7 @@ namespace Ink_Canvas.Helpers
|
||||
var json = JObject.Parse(response);
|
||||
string version = json["tag_name"]?.ToString();
|
||||
string releaseNotes = json["body"]?.ToString();
|
||||
string downloadUrl = json["assets"]?.First?["browser_download_url"]?.ToString();
|
||||
string downloadUrl = PickBrowserDownloadUrlFromAssets(json["assets"]);
|
||||
|
||||
DateTime? releaseTime = null;
|
||||
if (json["published_at"] != null && DateTime.TryParse(json["published_at"].ToString(), out DateTime parsedTime))
|
||||
@@ -863,9 +1015,11 @@ namespace Ink_Canvas.Helpers
|
||||
// 使用多线路组下载新版(支持自动切换)
|
||||
public static async Task<bool> DownloadSetupFileWithFallback(string version, List<UpdateLineGroup> groups, Action<double, string> progressCallback = null)
|
||||
{
|
||||
var session = BeginDownloadSession();
|
||||
try
|
||||
{
|
||||
statusFilePath = Path.Combine(updatesFolderPath, $"DownloadV{version}Status.txt");
|
||||
version = NormalizeVersionForUpdate(version);
|
||||
statusFilePath = GetUpdateDownloadStatusFilePath(version);
|
||||
|
||||
if (File.Exists(statusFilePath) && File.ReadAllText(statusFilePath).Trim().ToLower() == "true")
|
||||
{
|
||||
@@ -881,7 +1035,7 @@ namespace Ink_Canvas.Helpers
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 创建更新目录: {updatesFolderPath}");
|
||||
}
|
||||
|
||||
string zipFilePath = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip");
|
||||
string zipFilePath = GetLocalUpdateZipFilePath(version);
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 目标文件路径: {zipFilePath}");
|
||||
|
||||
SaveDownloadStatus(false);
|
||||
@@ -896,9 +1050,21 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
// 依次尝试每个线路组
|
||||
CancellationToken groupLoopToken;
|
||||
lock (_activeDownloadLock)
|
||||
{
|
||||
groupLoopToken = _activeDownloadCts?.Token ?? CancellationToken.None;
|
||||
}
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (groupLoopToken.IsCancellationRequested)
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 用户已取消,停止尝试后续线路组");
|
||||
break;
|
||||
}
|
||||
|
||||
string url = string.Format(group.DownloadUrlFormat, version);
|
||||
url = AppendX64SuffixBeforeZipExtension(url);
|
||||
// 智教联盟需要先获取真实下载地址
|
||||
if (group.GroupName == "智教联盟")
|
||||
{
|
||||
@@ -922,6 +1088,12 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback);
|
||||
|
||||
if (groupLoopToken.IsCancellationRequested)
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 用户已取消,停止尝试后续线路组");
|
||||
break;
|
||||
}
|
||||
|
||||
if (downloadSuccess)
|
||||
{
|
||||
SaveDownloadStatus(true);
|
||||
@@ -937,6 +1109,13 @@ namespace Ink_Canvas.Helpers
|
||||
progressCallback?.Invoke(0, "所有线路组下载均失败");
|
||||
return false;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 下载已被用户取消", LogHelper.LogType.Warning);
|
||||
SaveDownloadStatus(false);
|
||||
progressCallback?.Invoke(0, "下载已取消");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 下载更新时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
@@ -949,6 +1128,10 @@ namespace Ink_Canvas.Helpers
|
||||
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndDownloadSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件的具体实现
|
||||
@@ -959,6 +1142,12 @@ namespace Ink_Canvas.Helpers
|
||||
// 降低并发数,减少网络压力
|
||||
int[] threadOptions = { 32, 16, 8, 4, 1 };
|
||||
|
||||
CancellationToken externalToken;
|
||||
lock (_activeDownloadLock)
|
||||
{
|
||||
externalToken = _activeDownloadCts?.Token ?? CancellationToken.None;
|
||||
}
|
||||
|
||||
// 检查服务器是否支持Range分块下载
|
||||
bool supportRange = false;
|
||||
long totalSize = -1;
|
||||
@@ -1062,7 +1251,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 增加连接超时设置
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
|
||||
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, externalToken);
|
||||
var lastReadTime = DateTime.UtcNow;
|
||||
bool dataReceived = false;
|
||||
|
||||
@@ -1122,8 +1311,20 @@ namespace Ink_Canvas.Helpers
|
||||
success = true;
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载成功");
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException)
|
||||
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException || ex is OperationCanceledException)
|
||||
{
|
||||
// 用户主动取消:不再重试
|
||||
if (externalToken.IsCancellationRequested)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载已被用户取消", LogHelper.LogType.Warning);
|
||||
if (File.Exists(tempPath))
|
||||
{
|
||||
try { File.Delete(tempPath); } catch { }
|
||||
}
|
||||
cts.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}", LogHelper.LogType.Warning);
|
||||
progressCallback?.Invoke(0, $"分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}");
|
||||
|
||||
@@ -1134,7 +1335,8 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
// 增加重试间隔,避免频繁重试
|
||||
await Task.Delay(2000 * (retry + 1));
|
||||
try { await Task.Delay(2000 * (retry + 1), externalToken); }
|
||||
catch (OperationCanceledException) { cts.Cancel(); return; }
|
||||
}
|
||||
}
|
||||
if (success)
|
||||
@@ -1255,12 +1457,18 @@ namespace Ink_Canvas.Helpers
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 开始单线程下载: {fileUrl}");
|
||||
progressCallback?.Invoke(0, "开始单线程下载");
|
||||
|
||||
CancellationToken token;
|
||||
lock (_activeDownloadLock)
|
||||
{
|
||||
token = _activeDownloadCts?.Token ?? CancellationToken.None;
|
||||
}
|
||||
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||
client.Timeout = TimeSpan.FromMinutes(10); // 单线程下载设置更长的超时时间
|
||||
|
||||
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead))
|
||||
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token))
|
||||
{
|
||||
resp.EnsureSuccessStatusCode();
|
||||
using (var fs = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
@@ -1271,9 +1479,9 @@ namespace Ink_Canvas.Helpers
|
||||
long downloaded = 0;
|
||||
var lastProgressUpdate = DateTime.UtcNow;
|
||||
|
||||
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
|
||||
{
|
||||
await fs.WriteAsync(buffer, 0, read);
|
||||
await fs.WriteAsync(buffer, 0, read, token);
|
||||
downloaded += read;
|
||||
|
||||
// 限制进度更新频率,避免UI卡顿
|
||||
@@ -1295,6 +1503,13 @@ namespace Ink_Canvas.Helpers
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 单线程下载完成");
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 单线程下载已被取消", LogHelper.LogType.Warning);
|
||||
progressCallback?.Invoke(0, "下载已取消");
|
||||
try { if (File.Exists(destinationPath)) File.Delete(destinationPath); } catch { }
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 单线程下载失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
@@ -1352,7 +1567,7 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该方法会临时将 App.IsUpdateInstalling 置为 true、尝试关闭进程保护(并在结束时还原)、在必要时备份当前设置、解压更新 ZIP、启动解压后的新可执行文件(以更新模式传递旧进程 ID、解压路径和目标路径等参数),并在新进程启动后关闭当前进程。方法会记录日志并在遇到错误时安全退出相应步骤,但不会抛出异常给调用方以外的上下文。</remarks>
|
||||
/// <param name="version">要安装的版本号,用于定位更新包文件名(例如 InkCanvasForClass.CE.{version}.zip)。</param>
|
||||
/// <param name="version">要安装的版本号,用于定位更新包文件名(与 <see cref="GetLocalUpdateZipFilePath"/> 一致;选择 x64 包时为 InkCanvasForClass.CE.{version}-x64.zip)。</param>
|
||||
/// <param name="isInSilence">指示是否以静默模式启动新版本(影响传递给新进程的参数和可能的用户提示)。</param>
|
||||
public static void InstallNewVersionApp(string version, bool isInSilence)
|
||||
{
|
||||
@@ -1403,7 +1618,7 @@ namespace Ink_Canvas.Helpers
|
||||
LogHelper.WriteLogToFile($"更新前自动备份设置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
string zipFilePath = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip");
|
||||
string zipFilePath = GetLocalUpdateZipFilePath(version);
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 检查ZIP文件: {zipFilePath}");
|
||||
|
||||
if (!File.Exists(zipFilePath))
|
||||
@@ -2117,9 +2332,9 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 开始修复版本,通道: {channel}");
|
||||
|
||||
// 获取远程版本号(自动选择最快线路组,始终下载远程版本,版本修复模式)
|
||||
var (remoteVersion, group, _) = await CheckForUpdates(channel, true, true);
|
||||
if (string.IsNullOrEmpty(remoteVersion) || group == null)
|
||||
// 获取远程版本号(始终下载远程版本,版本修复模式)
|
||||
var (remoteVersion, preferredGroup, _) = await CheckForUpdates(channel, true, true);
|
||||
if (string.IsNullOrEmpty(remoteVersion))
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时获取远程版本失败", LogHelper.LogType.Error);
|
||||
return false;
|
||||
@@ -2127,8 +2342,22 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 修复版本远程版本: {remoteVersion}");
|
||||
|
||||
var availableGroups = await GetAvailableLineGroupsOrdered(channel);
|
||||
if (availableGroups.Count == 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时无可用线路组", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preferredGroup != null)
|
||||
{
|
||||
availableGroups.RemoveAll(g => g.GroupName == preferredGroup.GroupName);
|
||||
availableGroups.Insert(0, preferredGroup);
|
||||
LogHelper.WriteLogToFile($"AutoUpdate | 修复版本下载优先使用线路组: {preferredGroup.GroupName}");
|
||||
}
|
||||
|
||||
// 无论版本是否为最新,都下载远程版本
|
||||
bool downloadResult = await DownloadSetupFile(remoteVersion, group);
|
||||
bool downloadResult = await DownloadSetupFileWithFallback(remoteVersion, availableGroups);
|
||||
if (!downloadResult)
|
||||
{
|
||||
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时下载更新失败", LogHelper.LogType.Error);
|
||||
@@ -2170,7 +2399,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
string version = item["tag_name"]?.ToString();
|
||||
string releaseNotes = item["body"]?.ToString();
|
||||
string downloadUrl = item["assets"]?.First?["browser_download_url"]?.ToString();
|
||||
string downloadUrl = PickBrowserDownloadUrlFromAssets(item["assets"]);
|
||||
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
|
||||
result.Add((version, downloadUrl, releaseNotes));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Converter
|
||||
{
|
||||
@@ -152,4 +153,27 @@ namespace Ink_Canvas.Converter
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class StringToGeometryConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (value is string geometryString && !string.IsNullOrEmpty(geometryString))
|
||||
{
|
||||
return Geometry.Parse(geometryString);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public static class DebugConsoleManager
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool FreeConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetConsoleWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool SetConsoleTitle(string lpConsoleTitle);
|
||||
|
||||
private const int SW_HIDE = 0;
|
||||
private const int SW_SHOW = 5;
|
||||
private const uint SC_CLOSE = 0xF060;
|
||||
private const uint MF_BYCOMMAND = 0x00000000;
|
||||
|
||||
private static bool _allocated;
|
||||
|
||||
public static bool IsVisible { get; private set; }
|
||||
|
||||
public static void Show()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_allocated)
|
||||
{
|
||||
if (GetConsoleWindow() == IntPtr.Zero)
|
||||
{
|
||||
if (!AllocConsole()) return;
|
||||
}
|
||||
_allocated = true;
|
||||
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
SetConsoleTitle("InkCanvasForClass - Debug Console");
|
||||
|
||||
// 移除关闭菜单,避免用户点 X 时直接结束进程
|
||||
var hWnd = GetConsoleWindow();
|
||||
if (hWnd != IntPtr.Zero)
|
||||
{
|
||||
var hMenu = GetSystemMenu(hWnd, false);
|
||||
if (hMenu != IntPtr.Zero) DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var hWnd = GetConsoleWindow();
|
||||
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_SHOW);
|
||||
}
|
||||
IsVisible = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Show failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Hide()
|
||||
{
|
||||
try
|
||||
{
|
||||
var hWnd = GetConsoleWindow();
|
||||
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_HIDE);
|
||||
IsVisible = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Hide failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteLine(string line)
|
||||
{
|
||||
if (!IsVisible) return;
|
||||
try { Console.WriteLine(line); }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
@@ -22,6 +23,9 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
private static readonly string DeviceId;
|
||||
private static readonly object fileLock = new object();
|
||||
private static UsageStats usageStatsCache;
|
||||
private static DateTime usageStatsCacheTime;
|
||||
private static readonly TimeSpan UsageStatsCacheDuration = TimeSpan.FromMinutes(2);
|
||||
|
||||
static DeviceIdentifier()
|
||||
{
|
||||
@@ -116,114 +120,26 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
private static string GenerateHardwareFingerprint()
|
||||
{
|
||||
// 收集硬件信息
|
||||
var hardwareInfo = new StringBuilder();
|
||||
AppendFingerprintPart(hardwareInfo, "CPU",
|
||||
GetWmiProperty("SELECT ProcessorId FROM Win32_Processor", "ProcessorId"),
|
||||
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0", "ProcessorNameString"),
|
||||
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0", "Identifier"));
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.Load("System.Management");
|
||||
if (assembly != null)
|
||||
{
|
||||
// CPU信息
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT ProcessorId FROM Win32_Processor");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
AppendFingerprintPart(hardwareInfo, "BOARD",
|
||||
GetWmiProperty("SELECT SerialNumber FROM Win32_BaseBoard", "SerialNumber"),
|
||||
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BaseBoardSerialNumber"),
|
||||
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BaseBoardProduct"));
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
AppendFingerprintPart(hardwareInfo, "BIOS",
|
||||
GetWmiProperty("SELECT SerialNumber FROM Win32_BIOS", "SerialNumber"),
|
||||
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BIOSVersion"),
|
||||
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BIOSVendor"));
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var processorId = indexer.GetValue(obj, new object[] { "ProcessorId" });
|
||||
hardwareInfo.Append(processorId?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
// 主板序列号
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BaseBoard");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
|
||||
hardwareInfo.Append(serialNumber?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
// BIOS序列号
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BIOS");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
|
||||
hardwareInfo.Append(serialNumber?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
// 主硬盘序列号
|
||||
try
|
||||
{
|
||||
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
|
||||
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'");
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var enumerator = getMethod.Invoke(searcher, null);
|
||||
|
||||
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator.GetType().GetProperty("Current");
|
||||
|
||||
if ((bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
var obj = currentProperty.GetValue(enumerator);
|
||||
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
|
||||
hardwareInfo.Append(serialNumber?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var disposeMethod = searcher.GetType().GetMethod("Dispose");
|
||||
disposeMethod?.Invoke(searcher, null);
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
AppendFingerprintPart(hardwareInfo, "DISK",
|
||||
GetWmiProperty("SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'", "SerialNumber"),
|
||||
GetSystemDriveVolumeSerial(),
|
||||
GetRegistryValue(@"SOFTWARE\Microsoft\Cryptography", "MachineGuid"));
|
||||
|
||||
if (hardwareInfo.Length < 10)
|
||||
{
|
||||
@@ -235,6 +151,108 @@ namespace Ink_Canvas.Helpers
|
||||
return hardwareInfo.ToString();
|
||||
}
|
||||
|
||||
private static void AppendFingerprintPart(StringBuilder hardwareInfo, string key, params string[] candidates)
|
||||
{
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(candidate))
|
||||
{
|
||||
hardwareInfo.Append(key).Append(':').Append(candidate.Trim()).Append(';');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetWmiProperty(string query, string propertyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.Load("System.Management");
|
||||
var searcherType = assembly?.GetType("System.Management.ManagementObjectSearcher");
|
||||
if (searcherType == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var searcher = Activator.CreateInstance(searcherType, query);
|
||||
var getMethod = searcherType.GetMethod("Get");
|
||||
var resultCollection = getMethod?.Invoke(searcher, null);
|
||||
if (resultCollection == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var enumerator = resultCollection.GetType().GetMethod("GetEnumerator")?.Invoke(resultCollection, null);
|
||||
var moveNextMethod = enumerator?.GetType().GetMethod("MoveNext");
|
||||
var currentProperty = enumerator?.GetType().GetProperty("Current");
|
||||
if (enumerator == null || moveNextMethod == null || currentProperty == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(bool)moveNextMethod.Invoke(enumerator, null))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var currentObject = currentProperty.GetValue(enumerator);
|
||||
var indexer = currentObject?.GetType().GetProperty("Item", new[] { typeof(string) });
|
||||
var result = indexer?.GetValue(currentObject, new object[] { propertyName })?.ToString();
|
||||
|
||||
searcher?.GetType().GetMethod("Dispose")?.Invoke(searcher, null);
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRegistryValue(string subKey, string valueName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Microsoft.Win32.Registry.GetValue($@"HKEY_LOCAL_MACHINE\{subKey}", valueName, null)?.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetSystemDriveVolumeSerial()
|
||||
{
|
||||
try
|
||||
{
|
||||
var rootPath = Path.GetPathRoot(Environment.SystemDirectory);
|
||||
if (string.IsNullOrWhiteSpace(rootPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (GetVolumeInformation(rootPath, null, 0, out uint serialNumber, out _, out _, null, 0))
|
||||
{
|
||||
return serialNumber.ToString("X8");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern bool GetVolumeInformation(
|
||||
string rootPathName,
|
||||
StringBuilder volumeNameBuffer,
|
||||
uint volumeNameSize,
|
||||
out uint volumeSerialNumber,
|
||||
out uint maximumComponentLength,
|
||||
out uint fileSystemFlags,
|
||||
StringBuilder fileSystemNameBuffer,
|
||||
uint nFileSystemNameSize);
|
||||
|
||||
/// <summary>
|
||||
/// 基于硬件指纹生成25字符的设备ID
|
||||
/// </summary>
|
||||
@@ -654,7 +672,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
var stats = LoadUsageStats();
|
||||
var stats = GetUsageStatsCached();
|
||||
return stats.SystemVersion;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -773,7 +791,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
var stats = LoadUsageStats();
|
||||
var stats = GetUsageStatsCached();
|
||||
return stats.UpdatePriority;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -790,7 +808,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
var stats = LoadUsageStats();
|
||||
var stats = GetUsageStatsCached();
|
||||
return stats.UsageFrequency;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -892,6 +910,23 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private static UsageStats GetUsageStatsCached(bool forceRefresh = false)
|
||||
{
|
||||
lock (fileLock)
|
||||
{
|
||||
if (!forceRefresh
|
||||
&& usageStatsCache != null
|
||||
&& (DateTime.Now - usageStatsCacheTime) < UsageStatsCacheDuration)
|
||||
{
|
||||
return usageStatsCache;
|
||||
}
|
||||
|
||||
usageStatsCache = LoadUsageStats();
|
||||
usageStatsCacheTime = DateTime.Now;
|
||||
return usageStatsCache;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存使用统计
|
||||
/// </summary>
|
||||
@@ -902,6 +937,9 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
// 保存到备份文件
|
||||
SaveUsageStatsToFile(UsageStatsBackupPath, stats);
|
||||
|
||||
usageStatsCache = stats;
|
||||
usageStatsCacheTime = DateTime.Now;
|
||||
}
|
||||
|
||||
|
||||
@@ -1242,15 +1280,20 @@ namespace Ink_Canvas.Helpers
|
||||
int versionDiff = CalculateVersionGenerationDifference(localVersion, updateVersion);
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 无法获取版本发布时间,使用版本号差异判断 - 本地版本: {localVersion}, 远程版本: {updateVersion}, 代数差异: {versionDiff}");
|
||||
|
||||
if (versionDiff >= 1)
|
||||
if (versionDiff <= 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=1,允许更新");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})<1,可能是相同版本或降级,暂不更新");
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})<=0,可能是相同版本或降级,暂不更新");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 当代数差异较大(>=3)时直接放行,避免被分级策略卡住
|
||||
if (versionDiff >= 3)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=3,跳过分级策略直接推送");
|
||||
return true;
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=1,进入分级策略判断");
|
||||
}
|
||||
|
||||
// 计算最近活跃度(最后一次使用距今的天数)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public static class ExternalCallerLauncher
|
||||
{
|
||||
private static readonly string[] ClassIslandProtocols =
|
||||
{
|
||||
"classisland://plugins/IslandCaller/Simple/1",
|
||||
"classisland://plugins/IslandCaller/Simple",
|
||||
"classisland://plugins/IslandCaller/Run"
|
||||
};
|
||||
|
||||
public static string[] GetProtocolsByType(int externalCallerType)
|
||||
{
|
||||
switch (externalCallerType)
|
||||
{
|
||||
case 0:
|
||||
return ClassIslandProtocols;
|
||||
case 1:
|
||||
return new[]
|
||||
{
|
||||
"secrandom://roll_call/quick_draw",
|
||||
"secrandom://direct_extraction"
|
||||
};
|
||||
case 2:
|
||||
return new[] { "namepicker://" };
|
||||
default:
|
||||
return ClassIslandProtocols;
|
||||
}
|
||||
}
|
||||
|
||||
public static string[] GetProtocolsByName(string externalCallerName)
|
||||
{
|
||||
switch (externalCallerName)
|
||||
{
|
||||
case "ClassIsland":
|
||||
return ClassIslandProtocols;
|
||||
case "SecRandom":
|
||||
return new[]
|
||||
{
|
||||
"secrandom://roll_call/quick_draw",
|
||||
"secrandom://direct_extraction"
|
||||
};
|
||||
case "NamePicker":
|
||||
return new[] { "namepicker://" };
|
||||
default:
|
||||
return ClassIslandProtocols;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryLaunch(IEnumerable<string> protocols, out Exception lastException)
|
||||
{
|
||||
lastException = null;
|
||||
if (protocols == null) return false;
|
||||
|
||||
foreach (var protocol in protocols)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(protocol)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = protocol,
|
||||
UseShellExecute = true
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -54,6 +54,11 @@ namespace Ink_Canvas.Helpers
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromRect(ref RECT lprc, uint dwFlags);
|
||||
|
||||
public static IntPtr GetForegroundWindowHandle()
|
||||
{
|
||||
return GetForegroundWindow();
|
||||
}
|
||||
|
||||
public static string WindowTitle()
|
||||
{
|
||||
IntPtr foregroundWindowHandle = GetForegroundWindow();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
@@ -189,9 +188,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
/// <summary>
|
||||
/// 确保窗口全屏的Hook
|
||||
/// 使用HandleProcessCorruptedStateExceptions,防止访问内存过程中因为一些致命异常导致程序崩溃
|
||||
/// </summary>
|
||||
[HandleProcessCorruptedStateExceptions]
|
||||
private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
//处理WM_WINDOWPOSCHANGING消息
|
||||
|
||||
@@ -285,6 +285,7 @@ namespace Ink_Canvas.Helpers
|
||||
// 功能快捷键
|
||||
RegisterHotkey("DrawLine", Key.L, ModifierKeys.Alt, () => _mainWindow.BtnDrawLine_Click(null, null));
|
||||
RegisterHotkey("Screenshot", Key.C, ModifierKeys.Alt, () => _mainWindow.SaveScreenShotToDesktop());
|
||||
RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, () => _mainWindow.OpenQuickDrawFromHotkey());
|
||||
RegisterHotkey("Hide", Key.V, ModifierKeys.Alt, () => _mainWindow.SymbolIconEmoji_MouseUp(null, null));
|
||||
|
||||
// 退出快捷键
|
||||
@@ -566,6 +567,36 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新多屏相关设置(开关和跟随鼠标策略)。
|
||||
/// </summary>
|
||||
public void RefreshMultiScreenSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var advanced = MainWindow.Settings.Advanced;
|
||||
_isMultiScreenMode = advanced.EnableMultiScreenSupport && ScreenDetectionHelper.HasMultipleScreens();
|
||||
_enableScreenSpecificHotkeys = _isMultiScreenMode;
|
||||
|
||||
if (_isMultiScreenMode)
|
||||
{
|
||||
_currentScreen = advanced.FollowMouseForScreenSelection
|
||||
? Screen.FromPoint(Control.MousePosition)
|
||||
: ScreenDetectionHelper.GetWindowScreen(_mainWindow);
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentScreen = ScreenDetectionHelper.GetPrimaryScreen();
|
||||
}
|
||||
|
||||
RefreshHotkeysForCurrentScreen();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"刷新多屏设置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前屏幕信息
|
||||
/// </summary>
|
||||
@@ -623,13 +654,15 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检测是否有多个屏幕
|
||||
_isMultiScreenMode = ScreenDetectionHelper.HasMultipleScreens();
|
||||
var advanced = MainWindow.Settings.Advanced;
|
||||
_isMultiScreenMode = advanced.EnableMultiScreenSupport && ScreenDetectionHelper.HasMultipleScreens();
|
||||
_enableScreenSpecificHotkeys = _isMultiScreenMode;
|
||||
|
||||
if (_isMultiScreenMode)
|
||||
{
|
||||
// 获取当前窗口所在的屏幕
|
||||
_currentScreen = ScreenDetectionHelper.GetWindowScreen(_mainWindow);
|
||||
_currentScreen = advanced.FollowMouseForScreenSelection
|
||||
? Screen.FromPoint(Control.MousePosition)
|
||||
: ScreenDetectionHelper.GetWindowScreen(_mainWindow);
|
||||
|
||||
// 监听窗口位置变化事件
|
||||
_mainWindow.LocationChanged += OnWindowLocationChanged;
|
||||
@@ -687,6 +720,9 @@ namespace Ink_Canvas.Helpers
|
||||
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
|
||||
return;
|
||||
|
||||
if (MainWindow.Settings.Advanced.FollowMouseForScreenSelection)
|
||||
return;
|
||||
|
||||
var newScreen = ScreenDetectionHelper.GetWindowScreen(_mainWindow);
|
||||
if (newScreen != null && newScreen != _currentScreen)
|
||||
{
|
||||
@@ -799,9 +835,16 @@ namespace Ink_Canvas.Helpers
|
||||
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
|
||||
return;
|
||||
|
||||
// 检查鼠标是否在当前窗口所在的屏幕上
|
||||
var mousePosition = Control.MousePosition;
|
||||
var currentScreen = Screen.FromPoint(mousePosition);
|
||||
var mouseScreen = Screen.FromPoint(mousePosition);
|
||||
|
||||
if (MainWindow.Settings.Advanced.FollowMouseForScreenSelection &&
|
||||
mouseScreen != null &&
|
||||
mouseScreen != _currentScreen)
|
||||
{
|
||||
_currentScreen = mouseScreen;
|
||||
RefreshHotkeysForCurrentScreen();
|
||||
}
|
||||
|
||||
// 无论屏幕是否变化,都检查热键状态
|
||||
// 这样可以确保热键状态始终与当前上下文保持一致
|
||||
@@ -1033,6 +1076,7 @@ namespace Ink_Canvas.Helpers
|
||||
new HotkeyConfigItem { Name = "Pen5", Key = Key.D5, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "DrawLine", Key = Key.L, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Screenshot", Key = Key.C, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "QuickDraw", Key = Key.K, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Hide", Key = Key.V, Modifiers = ModifierKeys.Alt },
|
||||
new HotkeyConfigItem { Name = "Exit", Key = Key.Escape, Modifiers = ModifierKeys.None }
|
||||
});
|
||||
@@ -1111,6 +1155,14 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
// 旧版 HotkeyConfig.json 无「快抽」项时补注册默认组合,避免升级后无快捷键
|
||||
if (successCount > 0 && !IsHotkeyRegistered("QuickDraw"))
|
||||
{
|
||||
var quickDrawAction = GetActionByName("QuickDraw");
|
||||
if (quickDrawAction != null && RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, quickDrawAction))
|
||||
successCount++;
|
||||
}
|
||||
|
||||
if (successCount > 0)
|
||||
{
|
||||
_hotkeysShouldBeRegistered = true;
|
||||
@@ -1221,6 +1273,8 @@ namespace Ink_Canvas.Helpers
|
||||
return () => _mainWindow.BtnDrawLine_Click(null, null);
|
||||
case "Screenshot":
|
||||
return () => _mainWindow.SaveScreenShotToDesktop();
|
||||
case "QuickDraw":
|
||||
return () => _mainWindow.OpenQuickDrawFromHotkey();
|
||||
case "Hide":
|
||||
return () => _mainWindow.SymbolIconEmoji_MouseUp(null, null);
|
||||
case "Exit":
|
||||
|
||||
@@ -264,7 +264,6 @@ namespace Ink_Canvas.Helpers
|
||||
public void Enable()
|
||||
{
|
||||
IsEnabled = true;
|
||||
LogHelper.WriteLogToFile("墨迹渐隐功能已启用");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -273,7 +272,6 @@ namespace Ink_Canvas.Helpers
|
||||
public void Disable()
|
||||
{
|
||||
IsEnabled = false;
|
||||
LogHelper.WriteLogToFile("墨迹渐隐功能已禁用");
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -894,4 +892,4 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Ink;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public sealed class InkRecognitionManager
|
||||
{
|
||||
private static InkRecognitionManager _instance;
|
||||
private static readonly object _lock = new object();
|
||||
private readonly object _initSync = new object();
|
||||
|
||||
private ModernInkProcessor _modernProcessor;
|
||||
private bool _isModernSystemAvailable;
|
||||
private bool _isInitialized;
|
||||
|
||||
public static InkRecognitionManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_instance == null)
|
||||
_instance = new InkRecognitionManager();
|
||||
}
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private InkRecognitionManager() { }
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 启动阶段只做能力探测,不做 WinRT 组件实例化(避免冷启动延迟)
|
||||
_isModernSystemAvailable = WinRtInkShapeRecognizer.IsApiAvailable;
|
||||
_isInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("墨迹识别管理器初始化失败: " + ex.Message, LogHelper.LogType.Error);
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
lock (_initSync)
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureModernAnalyzerInitialized()
|
||||
{
|
||||
if (_modernProcessor != null || !_isModernSystemAvailable) return;
|
||||
|
||||
lock (_initSync)
|
||||
{
|
||||
if (_modernProcessor != null || !_isModernSystemAvailable) return;
|
||||
try
|
||||
{
|
||||
_modernProcessor ??= new ModernInkProcessor();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("WinRT 墨迹模块懒加载失败: " + ex.Message, LogHelper.LogType.Warning);
|
||||
_isModernSystemAvailable = false;
|
||||
_modernProcessor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<InkShapeRecognitionResult> RecognizeShapeAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (!_isInitialized || strokes == null || strokes.Count == 0)
|
||||
return Task.FromResult(InkShapeRecognitionResult.Empty);
|
||||
|
||||
try
|
||||
{
|
||||
if (ShapeRecognitionRouter.ResolveUseWinRt(mode)
|
||||
&& WinRtInkShapeRecognizer.IsApiAvailable)
|
||||
{
|
||||
return RecognizeShapeWinRtOnDispatcherContext(strokes);
|
||||
}
|
||||
|
||||
var legacy = InkRecognizeHelper.RecognizeShapeIACore(strokes);
|
||||
return Task.FromResult(InkRecognizeHelper.FromIACoreOrEmpty(legacy));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("墨迹形状识别失败: " + ex.Message, LogHelper.LogType.Error);
|
||||
return Task.FromResult(InkShapeRecognitionResult.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<InkShapeRecognitionResult> RecognizeShapeWinRtOnDispatcherContext(
|
||||
StrokeCollection strokes)
|
||||
{
|
||||
return await WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <param name="applyHandwritingBeautify">为 true 且走 WinRT 时,将识别成功的词替换为手写风格字体的轮廓墨迹(见设置中的字体列表)。</param>
|
||||
/// <param name="handwritingFontFamilyList">逗号分隔的字体回退列表(WPF FontFamily);null 时使用内置默认。</param>
|
||||
public Task<StrokeCollection> CorrectInkAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode,
|
||||
bool applyHandwritingBeautify = false,
|
||||
string handwritingFontFamilyList = null)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (!_isInitialized)
|
||||
{
|
||||
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
{
|
||||
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:无笔画。", LogHelper.LogType.Info);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var useWinRt = ShapeRecognitionRouter.ResolveUseWinRt(mode);
|
||||
if (!applyHandwritingBeautify)
|
||||
{
|
||||
LogHelper.WriteLogToFile(
|
||||
"[手写体] CorrectInkAsync 跳过:未开启「识别转手写体字形」(applyHandwritingBeautify=false)。笔画数=" +
|
||||
strokes.Count,
|
||||
LogHelper.LogType.Info);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
if (!useWinRt)
|
||||
{
|
||||
LogHelper.WriteLogToFile(
|
||||
"[手写体] CorrectInkAsync 跳过:当前引擎非 WinRT(模式=" + mode + ")。笔画数=" + strokes.Count,
|
||||
LogHelper.LogType.Info);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
EnsureModernAnalyzerInitialized();
|
||||
if (_modernProcessor == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile(
|
||||
"[手写体] CorrectInkAsync 跳过:ModernInkProcessor 未就绪(WinRT 初始化失败?)。笔画数=" +
|
||||
strokes.Count,
|
||||
LogHelper.LogType.Warning);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile(
|
||||
"[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count +
|
||||
",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()),
|
||||
LogHelper.LogType.Info);
|
||||
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(strokes, handwritingFontFamilyList);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("墨迹纠正失败: " + ex.Message, LogHelper.LogType.Error);
|
||||
return Task.FromResult(strokes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WinRT 手写体识别(需 Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。
|
||||
/// </summary>
|
||||
public Task<HandwritingRecognitionResult> RecognizeHandwritingAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (!_isInitialized || strokes == null || strokes.Count == 0)
|
||||
return Task.FromResult(HandwritingRecognitionResult.Empty);
|
||||
|
||||
try
|
||||
{
|
||||
if (!ShapeRecognitionRouter.ResolveUseWinRt(mode)
|
||||
|| !WinRtHandwritingRecognizer.IsApiAvailable)
|
||||
return Task.FromResult(HandwritingRecognitionResult.Empty);
|
||||
|
||||
return WinRtHandwritingRecognizer.RecognizeHandwritingAsync(strokes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("手写识别失败: " + ex.Message, LogHelper.LogType.Error);
|
||||
return Task.FromResult(HandwritingRecognitionResult.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValidShapeType(string shapeName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(shapeName)
|
||||
&& (shapeName.Contains("Triangle") || shapeName.Contains("Circle")
|
||||
|| shapeName.Contains("Rectangle") || shapeName.Contains("Diamond")
|
||||
|| shapeName.Contains("Parallelogram") || shapeName.Contains("Square")
|
||||
|| shapeName.Contains("Ellipse") || shapeName.Contains("Line")
|
||||
|| shapeName.Contains("Arrow"));
|
||||
}
|
||||
|
||||
public string GetSystemInfo()
|
||||
{
|
||||
return _isModernSystemAvailable
|
||||
? $"现代化墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}"
|
||||
: $"传统墨迹识别系统 (IACore) - 进程架构: {Environment.Is64BitProcess}";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_modernProcessor?.Dispose();
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ModernInkProcessor : IDisposable
|
||||
{
|
||||
public ModernInkProcessor()
|
||||
{
|
||||
if (!WinRtInkShapeRecognizer.IsApiAvailable)
|
||||
throw new InvalidOperationException("WinRT 墨迹分析需要 Windows 10 及以上。");
|
||||
}
|
||||
|
||||
public Task<InkShapeRecognitionResult> RecognizeShapeAsync(StrokeCollection strokes)
|
||||
{
|
||||
return WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Media;
|
||||
@@ -7,8 +8,8 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
public class InkRecognizeHelper
|
||||
{
|
||||
//识别形状
|
||||
public static ShapeRecognizeResult RecognizeShape(StrokeCollection strokes)
|
||||
/// <summary>IACore / IAWinFX 形状识别(典型用于 32 位进程)。</summary>
|
||||
public static ShapeRecognizeResult RecognizeShapeIACore(StrokeCollection strokes)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
return default;
|
||||
@@ -25,35 +26,158 @@ namespace Ink_Canvas.Helpers
|
||||
var alternates = analyzer.GetAlternates();
|
||||
if (alternates.Count > 0)
|
||||
{
|
||||
while ((!alternates[0].Strokes.Contains(strokes.Last()) ||
|
||||
!IsContainShapeType(((InkDrawingNode)alternates[0].AlternateNodes[0]).GetShapeName()))
|
||||
&& strokesCount >= 2)
|
||||
while (strokesCount >= 2)
|
||||
{
|
||||
var alt0 = alternates[0];
|
||||
if (alt0?.AlternateNodes == null || alt0.AlternateNodes.Count == 0)
|
||||
break;
|
||||
var drawNode = alt0.AlternateNodes[0] as InkDrawingNode;
|
||||
if (drawNode == null)
|
||||
break;
|
||||
var shapeOk = IsContainShapeType(drawNode.GetShapeName());
|
||||
if (alt0.Strokes.Contains(strokes.Last()) && shapeOk)
|
||||
break;
|
||||
analyzer.RemoveStroke(strokes[strokes.Count - strokesCount]);
|
||||
strokesCount--;
|
||||
sfsaf = analyzer.Analyze();
|
||||
if (sfsaf.Successful)
|
||||
{
|
||||
alternates = analyzer.GetAlternates();
|
||||
}
|
||||
else
|
||||
break;
|
||||
if (alternates.Count == 0)
|
||||
break;
|
||||
}
|
||||
if (alternates.Count > 0)
|
||||
{
|
||||
var altFinal = alternates[0];
|
||||
if (altFinal?.AlternateNodes != null && altFinal.AlternateNodes.Count > 0)
|
||||
analysisAlternate = altFinal;
|
||||
}
|
||||
analysisAlternate = alternates[0];
|
||||
}
|
||||
}
|
||||
|
||||
analyzer.Dispose();
|
||||
|
||||
if (analysisAlternate != null && analysisAlternate.AlternateNodes.Count > 0)
|
||||
if (analysisAlternate != null && analysisAlternate.AlternateNodes != null && analysisAlternate.AlternateNodes.Count > 0)
|
||||
{
|
||||
var node = analysisAlternate.AlternateNodes[0] as InkDrawingNode;
|
||||
if (node == null)
|
||||
return default;
|
||||
return new ShapeRecognizeResult(node.Centroid, node.HotPoints, analysisAlternate, node);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>兼容旧调用:等价于 <see cref="RecognizeShapeIACore"/>。</summary>
|
||||
public static ShapeRecognizeResult RecognizeShape(StrokeCollection strokes) =>
|
||||
RecognizeShapeIACore(strokes);
|
||||
|
||||
/// <summary>按设置选择 WinRT(<see cref="InkRecognitionManager"/>)或 IACore;WinRT 请用 <see cref="RecognizeShapeUnifiedAsync"/>。</summary>
|
||||
public static InkShapeRecognitionResult RecognizeShapeUnified(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var legacy = RecognizeShapeIACore(strokes);
|
||||
return FromIACoreOrEmpty(legacy);
|
||||
}
|
||||
|
||||
/// <summary>与 CE 反编译版 <c>InkRecognitionManager.RecognizeShapeAsync</c> 对齐的统一入口。</summary>
|
||||
public static Task<InkShapeRecognitionResult> RecognizeShapeUnifiedAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
return Task.FromResult(InkShapeRecognitionResult.Empty);
|
||||
|
||||
return InkRecognitionManager.Instance.RecognizeShapeAsync(strokes, mode);
|
||||
}
|
||||
|
||||
public static void WarmupShapeRecognition(ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
|
||||
{
|
||||
WinRtInkShapeRecognizer.Warmup();
|
||||
WinRtHandwritingRecognizer.Warmup();
|
||||
}
|
||||
else
|
||||
RecognizeShapeIACore(new StrokeCollection());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 预热失败不影响启动
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>WinRT 手写识别(Windows 10+)。</summary>
|
||||
public static Task<HandwritingRecognitionResult> RecognizeHandwritingUnifiedAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode) =>
|
||||
InkRecognitionManager.Instance.RecognizeHandwritingAsync(strokes, mode);
|
||||
|
||||
/// <summary>WinRT 下将识别成功的词替换为手写体字形墨迹;是否应用由设置「WinRT 识别转手写体字形」控制。</summary>
|
||||
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode) =>
|
||||
InkRecognitionManager.Instance.CorrectInkAsync(
|
||||
strokes,
|
||||
mode,
|
||||
MainWindow.Settings?.InkToShape?.EnableWinRtHandwritingStrokeBeautify ?? false,
|
||||
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
|
||||
|
||||
/// <summary>显式指定是否应用手写体字形替换(忽略开关);字体仍从设置读取。</summary>
|
||||
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
|
||||
StrokeCollection strokes,
|
||||
ShapeRecognitionEngineMode mode,
|
||||
bool applyHandwritingBeautify) =>
|
||||
InkRecognitionManager.Instance.CorrectInkAsync(
|
||||
strokes,
|
||||
mode,
|
||||
applyHandwritingBeautify,
|
||||
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
|
||||
|
||||
internal static InkShapeRecognitionResult FromIACoreOrEmpty(ShapeRecognizeResult legacy)
|
||||
{
|
||||
if (legacy?.InkDrawingNode == null)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var node = legacy.InkDrawingNode;
|
||||
var shape = node.GetShape();
|
||||
if (shape == null)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var hot = ClonePointCollection(node.HotPoints);
|
||||
return new InkShapeRecognitionResult(
|
||||
node.GetShapeName(),
|
||||
legacy.Centroid,
|
||||
hot,
|
||||
shape.Width,
|
||||
shape.Height,
|
||||
node.Strokes);
|
||||
}
|
||||
|
||||
private static PointCollection ClonePointCollection(PointCollection src)
|
||||
{
|
||||
var dst = new PointCollection();
|
||||
if (src == null) return dst;
|
||||
foreach (System.Windows.Point p in src)
|
||||
dst.Add(p);
|
||||
return dst;
|
||||
}
|
||||
|
||||
public static bool IsContainShapeType(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return false;
|
||||
|
||||
if (name.Contains("Triangle") || name.Contains("Circle") ||
|
||||
name.Contains("Rectangle") || name.Contains("Diamond") ||
|
||||
name.Contains("Parallelogram") || name.Contains("Square")
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using OSVersionExtension;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>墨迹形状识别后端:自动 / IACore / WinRT。</summary>
|
||||
public enum ShapeRecognitionEngineMode
|
||||
{
|
||||
Auto = 0,
|
||||
IACore = 1,
|
||||
WinRT = 2,
|
||||
}
|
||||
|
||||
public static class ShapeRecognitionRouter
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动模式:在 Windows 10 及以上系统默认使用 WinRT,否则使用 IACore。
|
||||
/// </summary>
|
||||
public static bool ResolveUseWinRt(ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
if (mode == ShapeRecognitionEngineMode.WinRT) return true;
|
||||
if (mode == ShapeRecognitionEngineMode.IACore) return false;
|
||||
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
|
||||
}
|
||||
|
||||
public static bool ShouldRunShapeRecognition(bool inkToShapeEnabled, ShapeRecognitionEngineMode mode)
|
||||
{
|
||||
if (!inkToShapeEnabled) return false;
|
||||
if (ResolveUseWinRt(mode))
|
||||
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ShapeRecognitionEngineMode FromSettingsInt(int value)
|
||||
{
|
||||
if (value == (int)ShapeRecognitionEngineMode.IACore) return ShapeRecognitionEngineMode.IACore;
|
||||
if (value == (int)ShapeRecognitionEngineMode.WinRT) return ShapeRecognitionEngineMode.WinRT;
|
||||
return ShapeRecognitionEngineMode.Auto;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>与具体识别后端无关的形状识别结果,供统一纠正模块消费。</summary>
|
||||
public sealed class InkShapeRecognitionResult
|
||||
{
|
||||
public static readonly InkShapeRecognitionResult Empty = new InkShapeRecognitionResult();
|
||||
|
||||
private InkShapeRecognitionResult()
|
||||
{
|
||||
IsSuccess = false;
|
||||
ShapeName = string.Empty;
|
||||
Centroid = new Point();
|
||||
HotPoints = new PointCollection();
|
||||
StrokesToRemove = new StrokeCollection();
|
||||
}
|
||||
|
||||
public InkShapeRecognitionResult(
|
||||
string shapeName,
|
||||
Point centroid,
|
||||
PointCollection hotPoints,
|
||||
double shapeWidth,
|
||||
double shapeHeight,
|
||||
StrokeCollection strokesToRemove)
|
||||
{
|
||||
ShapeName = shapeName ?? string.Empty;
|
||||
Centroid = centroid;
|
||||
HotPoints = hotPoints ?? new PointCollection();
|
||||
ShapeWidth = shapeWidth;
|
||||
ShapeHeight = shapeHeight;
|
||||
StrokesToRemove = strokesToRemove ?? new StrokeCollection();
|
||||
IsSuccess = StrokesToRemove.Count > 0
|
||||
&& !string.IsNullOrEmpty(ShapeName)
|
||||
&& ShapeName != "Drawing";
|
||||
}
|
||||
|
||||
public bool IsSuccess { get; }
|
||||
public string ShapeName { get; }
|
||||
public Point Centroid { get; set; }
|
||||
public PointCollection HotPoints { get; }
|
||||
public double ShapeWidth { get; }
|
||||
public double ShapeHeight { get; }
|
||||
public StrokeCollection StrokesToRemove { get; }
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,7 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
string logLine = string.Format("{0} [T{1}] [{2}] [{3}] {4}", DateTime.Now.ToString("O"), threadId, strLogType, callerInfo, str);
|
||||
DebugConsoleManager.WriteLine(logLine);
|
||||
ProcessProtectionManager.WithWriteAccess(file, () =>
|
||||
{
|
||||
using (StreamWriter sw = new StreamWriter(file, true))
|
||||
|
||||
@@ -111,6 +111,14 @@ namespace Ink_Canvas.Helpers
|
||||
/// <summary>
|
||||
/// 绘制点段到新的DrawingVisual
|
||||
/// </summary>
|
||||
private static double PressureToVisualScale(float pressureFactor, bool ignorePressure)
|
||||
{
|
||||
if (ignorePressure)
|
||||
return 1.0;
|
||||
// 与 WPF 墨迹观感接近:0.5 为标称,压低变细、抬高变粗(预览此前固定 Pen 宽,等同忽略压感)
|
||||
return Math.Max(0.22, Math.Min(2.1, 0.42 + 1.16 * pressureFactor));
|
||||
}
|
||||
|
||||
private void DrawSegmentToNewVisual(int startIndex, int endIndex)
|
||||
{
|
||||
if (Stroke == null || Stroke.StylusPoints.Count == 0 || _visualCanvas == null) return;
|
||||
@@ -118,6 +126,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
var points = Stroke.StylusPoints;
|
||||
var drawingAttributes = Stroke.DrawingAttributes;
|
||||
var ignorePressure = drawingAttributes.IgnorePressure;
|
||||
|
||||
// 创建新的DrawingVisual用于绘制这个点段
|
||||
var segmentVisual = new DrawingVisual();
|
||||
@@ -128,11 +137,6 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -141,6 +145,15 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
var startPoint = new Point(points[i].X, points[i].Y);
|
||||
var endPoint = new Point(points[i + 1].X, points[i + 1].Y);
|
||||
var s0 = PressureToVisualScale(points[i].PressureFactor, ignorePressure);
|
||||
var s1 = PressureToVisualScale(points[i + 1].PressureFactor, ignorePressure);
|
||||
var thickness = Math.Max(0.35, (drawingAttributes.Width * s0 + drawingAttributes.Width * s1) / 2.0);
|
||||
var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), thickness)
|
||||
{
|
||||
StartLineCap = PenLineCap.Round,
|
||||
EndLineCap = PenLineCap.Round,
|
||||
LineJoin = PenLineJoin.Round
|
||||
};
|
||||
dc.DrawLine(pen, startPoint, endPoint);
|
||||
}
|
||||
}
|
||||
@@ -149,8 +162,9 @@ namespace Ink_Canvas.Helpers
|
||||
// 只有一个点,绘制圆点
|
||||
var brush = new SolidColorBrush(drawingAttributes.Color);
|
||||
var point = points[startIndex];
|
||||
var s = PressureToVisualScale(point.PressureFactor, ignorePressure);
|
||||
dc.DrawEllipse(brush, null, new Point(point.X, point.Y),
|
||||
drawingAttributes.Width / 2, drawingAttributes.Height / 2);
|
||||
drawingAttributes.Width * s / 2, drawingAttributes.Height * s / 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// .NET Core / 5+ 未提供 <see cref="Marshal.GetActiveObject"/>,通过 OLE 实现等效行为。
|
||||
/// </summary>
|
||||
internal static class OleActiveObject
|
||||
{
|
||||
[DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
|
||||
private static extern int CLSIDFromProgID(string lpszProgId, out Guid lpclsid);
|
||||
|
||||
[DllImport("oleaut32.dll", PreserveSig = true)]
|
||||
private static extern int GetActiveObject(ref Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
|
||||
|
||||
public static object GetActiveObject(string progId)
|
||||
{
|
||||
int hr = CLSIDFromProgID(progId, out Guid clsid);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
hr = GetActiveObject(ref clsid, IntPtr.Zero, out object obj);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,7 +460,6 @@ namespace Ink_Canvas.Helpers
|
||||
_memoryStreams = new MemoryStream[_maxSlides + 2];
|
||||
}
|
||||
CurrentStrokes?.Clear();
|
||||
LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -67,6 +67,7 @@ namespace Ink_Canvas.Helpers
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _disposed;
|
||||
private static bool IsPptBusyHResult(uint hr) => hr == 0x80010001 || hr == 0x8001010A;
|
||||
private static bool IsNoActivePresentationHResult(uint hr) => hr == 0x80048240;
|
||||
private const int ConnectionCheckIntervalTicks = 3;
|
||||
private const int SlideShowCheckIntervalTicks = 2;
|
||||
private const int WpsCheckIntervalTicks = 6;
|
||||
@@ -150,13 +151,13 @@ namespace Ink_Canvas.Helpers
|
||||
private void CheckAndConnectToPPT()
|
||||
{
|
||||
if (_isModuleUnloading) return;
|
||||
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_isModuleUnloading) return;
|
||||
|
||||
|
||||
// 尝试连接到PowerPoint
|
||||
var pptApp = TryConnectToPowerPoint();
|
||||
if (pptApp == null && IsSupportWPS)
|
||||
@@ -270,7 +271,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("PowerPoint.Application");
|
||||
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("PowerPoint.Application");
|
||||
|
||||
if (pptApp != null && Marshal.IsComObject(pptApp))
|
||||
{
|
||||
@@ -297,7 +298,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("kwpp.Application");
|
||||
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("kwpp.Application");
|
||||
|
||||
if (wpsApp != null && Marshal.IsComObject(wpsApp))
|
||||
{
|
||||
@@ -328,7 +329,7 @@ namespace Ink_Canvas.Helpers
|
||||
_cachedIsConnected = true;
|
||||
|
||||
// 在主线程中注册事件,确保COM对象在正确的线程中
|
||||
Application.Current?.Dispatcher?.Invoke(() =>
|
||||
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -343,9 +344,8 @@ namespace Ink_Canvas.Helpers
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"PPT事件注册失败: {ex}", LogHelper.LogType.Error);
|
||||
throw; // 重新抛出异常,让外层处理
|
||||
}
|
||||
}, DispatcherPriority.Normal, CancellationToken.None, TimeSpan.FromSeconds(2));
|
||||
}), DispatcherPriority.Normal);
|
||||
|
||||
// 获取当前演示文稿信息
|
||||
UpdateCurrentPresentationInfo();
|
||||
@@ -386,15 +386,19 @@ namespace Ink_Canvas.Helpers
|
||||
if (Marshal.IsComObject(PPTApplication))
|
||||
{
|
||||
// 尝试在主线程中取消事件注册
|
||||
Application.Current?.Dispatcher?.Invoke(() =>
|
||||
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
PPTApplication.PresentationOpen -= OnPresentationOpen;
|
||||
PPTApplication.PresentationClose -= OnPresentationClose;
|
||||
PPTApplication.SlideShowBegin -= OnSlideShowBegin;
|
||||
PPTApplication.SlideShowNextSlide -= OnSlideShowNextSlide;
|
||||
PPTApplication.SlideShowEnd -= OnSlideShowEnd;
|
||||
// 再次检查PPTApplication是否为null,因为可能在异步操作期间被修改
|
||||
if (PPTApplication != null && Marshal.IsComObject(PPTApplication))
|
||||
{
|
||||
PPTApplication.PresentationOpen -= OnPresentationOpen;
|
||||
PPTApplication.PresentationClose -= OnPresentationClose;
|
||||
PPTApplication.SlideShowBegin -= OnSlideShowBegin;
|
||||
PPTApplication.SlideShowNextSlide -= OnSlideShowNextSlide;
|
||||
PPTApplication.SlideShowEnd -= OnSlideShowEnd;
|
||||
}
|
||||
}
|
||||
catch (COMException comEx)
|
||||
{
|
||||
@@ -406,11 +410,20 @@ namespace Ink_Canvas.Helpers
|
||||
// COM对象类型转换失败,通常是因为对象已经被释放
|
||||
LogHelper.WriteLogToFile("PPT COM对象已被释放,跳过事件注册取消", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (System.Reflection.TargetInvocationException tie) when (tie.InnerException is InvalidComObjectException)
|
||||
{
|
||||
// RCW 已分离:Office Interop 内部通过反射创建 EventProvider 时抛出,是正常情况
|
||||
LogHelper.WriteLogToFile("PPT COM对象RCW已分离,跳过事件注册取消", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (InvalidComObjectException)
|
||||
{
|
||||
LogHelper.WriteLogToFile("PPT COM对象RCW已分离,跳过事件注册取消", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"取消PPT事件注册时发生异常: {ex}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}, DispatcherPriority.Normal, CancellationToken.None, TimeSpan.FromSeconds(1));
|
||||
}), DispatcherPriority.Normal);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -421,7 +434,7 @@ namespace Ink_Canvas.Helpers
|
||||
SafeReleaseComObject(CurrentSlide, "CurrentSlide");
|
||||
SafeReleaseComObject(CurrentSlides, "CurrentSlides");
|
||||
SafeReleaseComObject(CurrentPresentation, "CurrentPresentation");
|
||||
|
||||
|
||||
if (PPTApplication != null && Marshal.IsComObject(PPTApplication))
|
||||
{
|
||||
try
|
||||
@@ -474,13 +487,13 @@ namespace Ink_Canvas.Helpers
|
||||
_isModuleUnloading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
|
||||
Thread.Sleep(1000);
|
||||
|
||||
|
||||
_isModuleUnloading = false;
|
||||
|
||||
try
|
||||
@@ -494,7 +507,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
LogHelper.WriteLogToFile("PPT联动模块重载时计时器已释放,跳过重启", LogHelper.LogType.Trace);
|
||||
}
|
||||
|
||||
|
||||
LogHelper.WriteLogToFile("PPT联动模块已重新加载", LogHelper.LogType.Trace);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -555,7 +568,7 @@ namespace Ink_Canvas.Helpers
|
||||
object view = null;
|
||||
object selection = null;
|
||||
object slideRange = null;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
if (PPTApplication != null && Marshal.IsComObject(PPTApplication))
|
||||
@@ -605,7 +618,7 @@ namespace Ink_Canvas.Helpers
|
||||
if (view != null)
|
||||
{
|
||||
dynamic viewObj = view;
|
||||
CurrentSlide = viewObj.Slide as Slide;
|
||||
CurrentSlide = viewObj.Slide as Slide;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -632,7 +645,7 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (CurrentSlide == null && SlidesCount > 0)
|
||||
{
|
||||
CurrentSlide = CurrentSlides[1];
|
||||
@@ -642,7 +655,7 @@ namespace Ink_Canvas.Helpers
|
||||
catch (COMException comEx)
|
||||
{
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr != 0x8001010E && hr != 0x80004005)
|
||||
if (hr != 0x8001010E && hr != 0x80004005 && !IsNoActivePresentationHResult(hr))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取当前幻灯片失败: {comEx.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
@@ -664,7 +677,7 @@ namespace Ink_Canvas.Helpers
|
||||
catch (COMException comEx)
|
||||
{
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706B5 || hr == 0x80048240)
|
||||
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706B5 || IsNoActivePresentationHResult(hr))
|
||||
{
|
||||
CurrentPresentation = null;
|
||||
CurrentSlides = null;
|
||||
@@ -1076,7 +1089,7 @@ namespace Ink_Canvas.Helpers
|
||||
catch (COMException comEx)
|
||||
{
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (hr == 0x80048240 || IsPptBusyHResult(hr))
|
||||
if (IsNoActivePresentationHResult(hr) || IsPptBusyHResult(hr))
|
||||
return null;
|
||||
throw;
|
||||
}
|
||||
@@ -1094,7 +1107,7 @@ namespace Ink_Canvas.Helpers
|
||||
var hr = (uint)comEx.HResult;
|
||||
if (!IsPptBusyHResult(hr) && (hr == 0x8001010E || hr == 0x80004005))
|
||||
DisconnectFromPPT();
|
||||
if (!IsPptBusyHResult(hr) && hr != 0x80048240)
|
||||
if (!IsPptBusyHResult(hr) && !IsNoActivePresentationHResult(hr))
|
||||
LogHelper.WriteLogToFile($"获取当前活跃演示文稿失败: {comEx.Message}", LogHelper.LogType.Warning);
|
||||
return CurrentPresentation;
|
||||
}
|
||||
@@ -1251,7 +1264,6 @@ namespace Ink_Canvas.Helpers
|
||||
object slideNavigation = null;
|
||||
try
|
||||
{
|
||||
LogHelper.WriteLogToFile($"尝试显示幻灯片导航 - 连接状态: {IsConnected}, 放映状态: {IsInSlideShow}", LogHelper.LogType.Trace);
|
||||
|
||||
if (!IsConnected || !IsInSlideShow || PPTApplication == null)
|
||||
{
|
||||
@@ -1284,7 +1296,6 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
dynamic sn = slideNavigation;
|
||||
sn.Visible = true;
|
||||
LogHelper.WriteLogToFile("成功显示幻灯片导航(PowerPoint模式)", LogHelper.LogType.Event);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
try
|
||||
{
|
||||
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("PowerPoint.Application");
|
||||
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("PowerPoint.Application");
|
||||
if (pptApp != null && Marshal.IsComObject(pptApp))
|
||||
{
|
||||
try
|
||||
@@ -124,7 +124,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("kwpp.Application");
|
||||
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("kwpp.Application");
|
||||
if (wpsApp != null && Marshal.IsComObject(wpsApp))
|
||||
{
|
||||
try
|
||||
|
||||
@@ -86,18 +86,8 @@ namespace Ink_Canvas.Helpers
|
||||
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Collapsed;
|
||||
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Visible;
|
||||
|
||||
// 只有在页数有效时才更新页码显示
|
||||
if (currentSlide > 0 && totalSlides > 0)
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
|
||||
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 页数无效时清空页码显示
|
||||
_mainWindow.PPTBtnPageNow.Text = "?";
|
||||
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
|
||||
}
|
||||
// 同步页码到所有翻页条 + 兼容旧绑定的隐藏 placeholder
|
||||
SetPageNumberOnAllBars(currentSlide, totalSlides);
|
||||
|
||||
UpdateNavigationPanelsVisibility();
|
||||
UpdateNavigationButtonStyles();
|
||||
@@ -112,6 +102,11 @@ namespace Ink_Canvas.Helpers
|
||||
MainWindow.MoveWindow(new WindowInteropHelper(_mainWindow).Handle, 0, 0,
|
||||
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
|
||||
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height, true);
|
||||
|
||||
// MoveWindow 触发的 WM_WINDOWPOSCHANGING + 重绘会打断面板的 ShowWithFadeIn 动画,
|
||||
// 在窗口尺寸最终确定后重新评估一次翻页面板的可见性。
|
||||
UpdateNavigationPanelsVisibility();
|
||||
UpdateNavigationButtonStyles();
|
||||
}), DispatcherPriority.ApplicationIdle);
|
||||
|
||||
_mainWindow.isFullScreenApplied = true; // 标记已应用全屏处理
|
||||
@@ -158,18 +153,7 @@ namespace Ink_Canvas.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
// 只有在页数有效时才更新页码显示
|
||||
if (currentSlide > 0 && totalSlides > 0)
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
|
||||
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 页数无效时清空页码显示
|
||||
_mainWindow.PPTBtnPageNow.Text = "?";
|
||||
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
|
||||
}
|
||||
SetPageNumberOnAllBars(currentSlide, totalSlides);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -178,6 +162,34 @@ namespace Ink_Canvas.Helpers
|
||||
});
|
||||
}
|
||||
|
||||
private void SetPageNumberOnAllBars(int currentSlide, int totalSlides)
|
||||
{
|
||||
var bars = new[]
|
||||
{
|
||||
_mainWindow.LeftBottomPanelForPPTNavigation,
|
||||
_mainWindow.RightBottomPanelForPPTNavigation,
|
||||
_mainWindow.LeftSidePanelForPPTNavigation,
|
||||
_mainWindow.RightSidePanelForPPTNavigation,
|
||||
};
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
if (bar == null) continue;
|
||||
bar.CurrentSlide = currentSlide;
|
||||
bar.TotalSlides = totalSlides;
|
||||
}
|
||||
// 兼容旧绑定(其它界面通过 ElementName 引用 PPTBtnPageNow / PPTBtnPageTotal)
|
||||
if (currentSlide > 0 && totalSlides > 0)
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
|
||||
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
|
||||
}
|
||||
else
|
||||
{
|
||||
_mainWindow.PPTBtnPageNow.Text = "?";
|
||||
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理PPT放映状态变化
|
||||
/// </summary>
|
||||
@@ -386,16 +398,17 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
// 页码按钮显示
|
||||
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
|
||||
_mainWindow.PPTLSPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.PPTRSPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.LeftSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
|
||||
_mainWindow.RightSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
|
||||
|
||||
// 透明度设置 - 直接使用用户设置的透明度值
|
||||
_mainWindow.PPTBtnLSBorder.Opacity = PPTLSButtonOpacity;
|
||||
_mainWindow.PPTBtnRSBorder.Opacity = PPTRSButtonOpacity;
|
||||
// 透明度
|
||||
_mainWindow.LeftSidePanelForPPTNavigation.SetBarOpacity(PPTLSButtonOpacity);
|
||||
_mainWindow.RightSidePanelForPPTNavigation.SetBarOpacity(PPTRSButtonOpacity);
|
||||
|
||||
// 颜色主题
|
||||
bool isDarkTheme = options[2] == '2';
|
||||
ApplyButtonTheme(_mainWindow.PPTBtnLSBorder, _mainWindow.PPTBtnRSBorder, isDarkTheme, true);
|
||||
_mainWindow.LeftSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
|
||||
_mainWindow.RightSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -414,113 +427,23 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
// 页码按钮显示
|
||||
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
|
||||
_mainWindow.PPTLBPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.PPTRBPageButton.Visibility = pageButtonVisibility;
|
||||
_mainWindow.LeftBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
|
||||
_mainWindow.RightBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
|
||||
|
||||
// 透明度设置 - 直接使用用户设置的透明度值
|
||||
_mainWindow.PPTBtnLBBorder.Opacity = PPTLBButtonOpacity;
|
||||
_mainWindow.PPTBtnRBBorder.Opacity = PPTRBButtonOpacity;
|
||||
// 透明度
|
||||
_mainWindow.LeftBottomPanelForPPTNavigation.SetBarOpacity(PPTLBButtonOpacity);
|
||||
_mainWindow.RightBottomPanelForPPTNavigation.SetBarOpacity(PPTRBButtonOpacity);
|
||||
|
||||
// 颜色主题
|
||||
bool isDarkTheme = options[2] == '2';
|
||||
ApplyButtonTheme(_mainWindow.PPTBtnLBBorder, _mainWindow.PPTBtnRBBorder, isDarkTheme, false);
|
||||
_mainWindow.LeftBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
|
||||
_mainWindow.RightBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新底部按钮样式失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyButtonTheme(Border leftBorder, Border rightBorder, bool isDarkTheme, bool isSideButton)
|
||||
{
|
||||
try
|
||||
{
|
||||
Color backgroundColor, borderColor, foregroundColor, feedbackColor;
|
||||
|
||||
if (isDarkTheme)
|
||||
{
|
||||
backgroundColor = Color.FromRgb(39, 39, 42);
|
||||
borderColor = Color.FromRgb(82, 82, 91);
|
||||
foregroundColor = Colors.White;
|
||||
feedbackColor = Colors.White;
|
||||
}
|
||||
else
|
||||
{
|
||||
backgroundColor = Color.FromRgb(244, 244, 245);
|
||||
borderColor = Color.FromRgb(161, 161, 170);
|
||||
foregroundColor = Color.FromRgb(39, 39, 42);
|
||||
feedbackColor = Color.FromRgb(24, 24, 27);
|
||||
}
|
||||
|
||||
// 应用背景和边框颜色
|
||||
var backgroundBrush = new SolidColorBrush(backgroundColor);
|
||||
var borderBrush = new SolidColorBrush(borderColor);
|
||||
|
||||
leftBorder.Background = backgroundBrush;
|
||||
leftBorder.BorderBrush = borderBrush;
|
||||
rightBorder.Background = backgroundBrush;
|
||||
rightBorder.BorderBrush = borderBrush;
|
||||
|
||||
// 应用图标和文字颜色
|
||||
var foregroundBrush = new SolidColorBrush(foregroundColor);
|
||||
var feedbackBrush = new SolidColorBrush(feedbackColor);
|
||||
|
||||
if (isSideButton)
|
||||
{
|
||||
ApplySideButtonColors(foregroundBrush, feedbackBrush);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyBottomButtonColors(foregroundBrush, feedbackBrush);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用按钮主题失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySideButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
|
||||
{
|
||||
// 图标颜色
|
||||
_mainWindow.PPTLSPreviousButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTRSPreviousButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTLSNextButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTRSNextButtonGeometry.Brush = foregroundBrush;
|
||||
|
||||
// 反馈背景颜色
|
||||
_mainWindow.PPTLSPreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRSPreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTLSPageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRSPageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTLSNextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRSNextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
|
||||
// 文字颜色
|
||||
TextBlock.SetForeground(_mainWindow.PPTLSPageButton, foregroundBrush);
|
||||
TextBlock.SetForeground(_mainWindow.PPTRSPageButton, foregroundBrush);
|
||||
}
|
||||
|
||||
private void ApplyBottomButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
|
||||
{
|
||||
// 图标颜色
|
||||
_mainWindow.PPTLBPreviousButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTRBPreviousButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTLBNextButtonGeometry.Brush = foregroundBrush;
|
||||
_mainWindow.PPTRBNextButtonGeometry.Brush = foregroundBrush;
|
||||
|
||||
// 反馈背景颜色
|
||||
_mainWindow.PPTLBPreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRBPreviousButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTLBPageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRBPageButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTLBNextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
_mainWindow.PPTRBNextButtonFeedbackBorder.Background = feedbackBrush;
|
||||
|
||||
// 文字颜色
|
||||
TextBlock.SetForeground(_mainWindow.PPTLBPageButton, foregroundBrush);
|
||||
TextBlock.SetForeground(_mainWindow.PPTRBPageButton, foregroundBrush);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Windows.Data.Pdf;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 Windows.Data.Pdf(WinRT)将 PDF 页渲染为 WPF 可用的位图。
|
||||
/// </summary>
|
||||
internal static class PdfWinRtHelper
|
||||
{
|
||||
public static async Task<uint> GetPageCountAsync(string pdfPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath))
|
||||
return 0;
|
||||
|
||||
var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask();
|
||||
var doc = await PdfDocument.LoadFromFileAsync(file).AsTask();
|
||||
if (doc.IsPasswordProtected)
|
||||
return 0;
|
||||
return doc.PageCount;
|
||||
}
|
||||
|
||||
public static async Task<BitmapSource> RenderPageToBitmapSourceAsync(string pdfPath, uint pageIndex)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath))
|
||||
return null;
|
||||
|
||||
var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask();
|
||||
var doc = await PdfDocument.LoadFromFileAsync(file).AsTask();
|
||||
if (doc.IsPasswordProtected)
|
||||
return null;
|
||||
if (pageIndex >= doc.PageCount)
|
||||
return null;
|
||||
|
||||
var page = doc.GetPage(pageIndex);
|
||||
try
|
||||
{
|
||||
using (var ras = new InMemoryRandomAccessStream())
|
||||
{
|
||||
await page.RenderToStreamAsync(ras).AsTask();
|
||||
ras.Seek(0);
|
||||
|
||||
var ms = new MemoryStream();
|
||||
using (var netStream = ras.AsStreamForRead())
|
||||
netStream.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
try
|
||||
{
|
||||
return await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var bi = new BitmapImage();
|
||||
bi.BeginInit();
|
||||
bi.StreamSource = ms;
|
||||
bi.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bi.EndInit();
|
||||
bi.Freeze();
|
||||
return (BitmapSource)bi;
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
ms.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
(page as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
using iNKORE.UI.WPF.Controls;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动台按钮控件
|
||||
/// </summary>
|
||||
public class LauncherButton
|
||||
{
|
||||
/// <summary>
|
||||
/// 父插件
|
||||
/// </summary>
|
||||
private readonly SuperLauncherPlugin _plugin;
|
||||
|
||||
/// <summary>
|
||||
/// 实际按钮控件
|
||||
/// </summary>
|
||||
private readonly SimpleStackPanel _panel;
|
||||
|
||||
/// <summary>
|
||||
/// 获取按钮UI元素
|
||||
/// </summary>
|
||||
public UIElement Element => _panel;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="plugin">父插件</param>
|
||||
public LauncherButton(SuperLauncherPlugin plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
_plugin = plugin;
|
||||
LogHelper.WriteLogToFile("开始创建启动台按钮");
|
||||
|
||||
// 创建SimpleStackPanel
|
||||
_panel = new SimpleStackPanel
|
||||
{
|
||||
Name = "Launcher_Icon",
|
||||
Orientation = Orientation.Vertical,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Width = 28,
|
||||
Margin = new Thickness(0, -2, 0, 0),
|
||||
Background = Brushes.Transparent
|
||||
};
|
||||
|
||||
LogHelper.WriteLogToFile("创建SimpleStackPanel完成");
|
||||
|
||||
// 添加图标
|
||||
var image = CreateIconImage();
|
||||
_panel.Children.Add(image);
|
||||
|
||||
// 添加文本
|
||||
TextBlock textBlock = new TextBlock
|
||||
{
|
||||
Text = "启动台",
|
||||
Foreground = Brushes.Black,
|
||||
FontSize = 8,
|
||||
Margin = new Thickness(0, 1, 0, 0),
|
||||
TextAlignment = TextAlignment.Center
|
||||
};
|
||||
_panel.Children.Add(textBlock);
|
||||
|
||||
// 设置鼠标事件
|
||||
_panel.MouseDown += Panel_MouseDown;
|
||||
_panel.MouseUp += Panel_MouseUp;
|
||||
_panel.MouseLeave += Panel_MouseLeave;
|
||||
|
||||
// 右键菜单支持
|
||||
_panel.ContextMenu = CreateContextMenu();
|
||||
|
||||
// 设置工具提示
|
||||
_panel.ToolTip = "启动台";
|
||||
|
||||
LogHelper.WriteLogToFile("启动台按钮创建完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建右键菜单
|
||||
/// </summary>
|
||||
private ContextMenu CreateContextMenu()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建菜单
|
||||
ContextMenu menu = new ContextMenu();
|
||||
|
||||
// 创建位置切换菜单项
|
||||
MenuItem positionMenuItem = new MenuItem();
|
||||
positionMenuItem.Header = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ?
|
||||
"移至右侧" : "移至左侧";
|
||||
positionMenuItem.Click += (s, e) =>
|
||||
{
|
||||
// 切换位置
|
||||
_plugin.Config.ButtonPosition = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ?
|
||||
LauncherButtonPosition.Right : LauncherButtonPosition.Left;
|
||||
|
||||
// 更新按钮位置
|
||||
_plugin.UpdateButtonPosition();
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
|
||||
LogHelper.WriteLogToFile($"通过右键菜单切换启动台按钮位置为: {_plugin.Config.ButtonPosition}");
|
||||
};
|
||||
menu.Items.Add(positionMenuItem);
|
||||
|
||||
// 添加设置菜单项
|
||||
MenuItem settingsMenuItem = new MenuItem();
|
||||
settingsMenuItem.Header = "打开设置";
|
||||
settingsMenuItem.Click += (s, e) =>
|
||||
{
|
||||
// 打开插件设置窗口
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用反射调用主窗口的ShowPluginSettings方法
|
||||
var method = mainWindow.GetType().GetMethod("ShowPluginSettings");
|
||||
if (method != null)
|
||||
{
|
||||
method.Invoke(mainWindow, null);
|
||||
LogHelper.WriteLogToFile("已打开插件设置窗口");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"打开插件设置窗口失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
};
|
||||
menu.Items.Add(settingsMenuItem);
|
||||
|
||||
return menu;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建右键菜单时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取实际的UI元素
|
||||
/// </summary>
|
||||
[Obsolete("使用Element属性代替")]
|
||||
public UIElement GetUIElement()
|
||||
{
|
||||
return _panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建图标图像
|
||||
/// </summary>
|
||||
private Image CreateIconImage()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建图像
|
||||
Image image = new Image
|
||||
{
|
||||
Height = 17,
|
||||
Margin = new Thickness(0, 3, 0, 0)
|
||||
};
|
||||
|
||||
// 设置位图缩放模式
|
||||
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
|
||||
|
||||
// 创建绘图图像
|
||||
DrawingImage drawingImage = new DrawingImage();
|
||||
DrawingGroup drawingGroup = new DrawingGroup();
|
||||
drawingGroup.ClipGeometry = Geometry.Parse("M0,0 V24 H24 V0 H0 Z");
|
||||
|
||||
// 使用提供的应用网格图标
|
||||
GeometryDrawing geometryDrawing = new GeometryDrawing
|
||||
{
|
||||
Brush = new SolidColorBrush(Color.FromRgb(0x1B, 0x1B, 0x1B)),
|
||||
Geometry = Geometry.Parse("F0 M24,24z M0,0z M4.41721,4.29873C4.35178,4.29873,4.29873,4.35178,4.29873,4.41721L4.29873,9.15646C4.29873,9.22189,4.35178,9.27494,4.41721,9.27494L9.15646,9.27494C9.22189,9.27494,9.27494,9.22189,9.27494,9.15646L9.27494,4.41721C9.27494,4.35178,9.22189,4.29873,9.15646,4.29873L4.41721,4.29873z M2.64,4.41721C2.64,3.43569,3.43569,2.64,4.41721,2.64L9.15646,2.64C10.138,2.64,10.9337,3.43569,10.9337,4.41721L10.9337,9.15646C10.9337,10.138,10.138,10.9337,9.15646,10.9337L4.41721,10.9337C3.43569,10.9337,2.64,10.138,2.64,9.15646L2.64,4.41721z M14.8435,4.29873C14.7781,4.29873,14.7251,4.35178,14.7251,4.41721L14.7251,9.15646C14.7251,9.22189,14.7781,9.27494,14.8435,9.27494L19.5828,9.27494C19.6482,9.27494,19.7013,9.22189,19.7013,9.15646L19.7013,4.41721C19.7013,4.35178,19.6482,4.29873,19.5828,4.29873L14.8435,4.29873z M13.0663,4.41721C13.0663,3.43569,13.862,2.64,14.8435,2.64L19.5828,2.64C20.5643,2.64,21.36,3.43569,21.36,4.41721L21.36,9.15646C21.36,10.138,20.5643,10.9337,19.5828,10.9337L14.8435,10.9337C13.862,10.9337,13.0663,10.138,13.0663,9.15646L13.0663,4.41721z M14.8435,14.7251C14.7781,14.7251,14.7251,14.7781,14.7251,14.8435L14.7251,19.5828C14.7251,19.6482,14.7781,19.7013,14.8435,19.7013L19.5828,19.7013C19.6482,19.7013,19.7013,19.6482,19.7013,19.5828L19.7013,14.8435C19.7013,14.7781,19.6482,14.7251,19.5828,14.7251L14.8435,14.7251z M13.0663,14.8435C13.0663,13.862,13.862,13.0663,14.8435,13.0663L19.5828,13.0663C20.5643,13.0663,21.36,13.862,21.36,14.8435L21.36,19.5828C21.36,20.5643,20.5643,21.36,19.5828,21.36L14.8435,21.36C13.862,21.36,13.0663,20.5643,13.0663,19.5828L13.0663,14.8435z M4.41721,14.7251C4.35178,14.7251,4.29873,14.7781,4.29873,14.8435L4.29873,19.5828C4.29873,19.6482,4.35178,19.7013,4.41721,19.7013L9.15646,19.7013C9.22189,19.7013,9.27494,19.6482,9.27494,19.5828L9.27494,14.8435C9.27494,14.7781,9.22189,14.7251,9.15646,14.7251L4.41721,14.7251z M2.64,14.8435C2.64,13.862,3.43569,13.0663,4.41721,13.0663L9.15646,13.0663C10.138,13.0663,10.9337,13.862,10.9337,14.8435L10.9337,19.5828C10.9337,20.5643,10.138,21.36,9.15646,21.36L4.41721,21.36C3.43569,21.36,2.64,20.5643,2.64,19.5828L2.64,14.8435z")
|
||||
};
|
||||
|
||||
drawingGroup.Children.Add(geometryDrawing);
|
||||
|
||||
// 设置图像源
|
||||
drawingImage.Drawing = drawingGroup;
|
||||
image.Source = drawingImage;
|
||||
|
||||
return image;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建图标图像时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
|
||||
// 返回一个空图像
|
||||
return new Image();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标按下事件
|
||||
/// </summary>
|
||||
private void Panel_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 提供反馈
|
||||
_panel.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
|
||||
LogHelper.WriteLogToFile("启动台按钮鼠标按下");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动台按钮鼠标按下事件出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标抬起事件
|
||||
/// </summary>
|
||||
private void Panel_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 只有左键点击才显示启动台窗口
|
||||
if (e.ChangedButton != MouseButton.Left)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 恢复背景
|
||||
_panel.Background = Brushes.Transparent;
|
||||
LogHelper.WriteLogToFile("启动台按钮鼠标抬起,准备显示启动台窗口");
|
||||
|
||||
// 获取按钮在屏幕上的位置
|
||||
Point buttonPosition = _panel.PointToScreen(new Point(_panel.ActualWidth / 2, 0));
|
||||
|
||||
// 显示启动台窗口
|
||||
_plugin.ShowLauncherWindow(buttonPosition);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动台按钮鼠标抬起事件出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标离开事件
|
||||
/// </summary>
|
||||
private void Panel_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 恢复背景
|
||||
_panel.Background = Brushes.Transparent;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动台按钮鼠标离开事件出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动台按钮位置
|
||||
/// </summary>
|
||||
public enum LauncherButtonPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// 左侧
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// 右侧
|
||||
/// </summary>
|
||||
Right
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动台配置
|
||||
/// </summary>
|
||||
public class LauncherConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动台按钮位置
|
||||
/// </summary>
|
||||
public LauncherButtonPosition ButtonPosition { get; set; } = LauncherButtonPosition.Right;
|
||||
|
||||
/// <summary>
|
||||
/// 启动台应用程序列表
|
||||
/// </summary>
|
||||
public List<LauncherItem> Items { get; set; } = new List<LauncherItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动台应用项
|
||||
/// </summary>
|
||||
public class LauncherItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用程序名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序路径
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可见
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 在启动台中的位置(0-39)
|
||||
/// </summary>
|
||||
public int Position { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已固定位置
|
||||
/// </summary>
|
||||
public bool IsPositionFixed { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 图标缓存
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
private ImageSource _iconCache;
|
||||
|
||||
/// <summary>
|
||||
/// 获取应用程序图标
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ImageSource Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_iconCache != null)
|
||||
{
|
||||
return _iconCache;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(Path))
|
||||
{
|
||||
// 从文件中获取图标
|
||||
Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(Path);
|
||||
if (icon != null)
|
||||
{
|
||||
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
|
||||
icon.Handle,
|
||||
Int32Rect.Empty,
|
||||
BitmapSizeOptions.FromEmptyOptions());
|
||||
|
||||
icon.Dispose();
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 从注册表中获取文件类型关联图标
|
||||
string extension = System.IO.Path.GetExtension(Path);
|
||||
if (!string.IsNullOrEmpty(extension))
|
||||
{
|
||||
string fileType = Registry.ClassesRoot.OpenSubKey(extension)?.GetValue(string.Empty) as string;
|
||||
if (!string.IsNullOrEmpty(fileType))
|
||||
{
|
||||
string iconPath = Registry.ClassesRoot.OpenSubKey(fileType + "\\DefaultIcon")?.GetValue(string.Empty) as string;
|
||||
if (!string.IsNullOrEmpty(iconPath))
|
||||
{
|
||||
string[] parts = iconPath.Split(',');
|
||||
string iconFile = parts[0].Trim('"');
|
||||
int iconIndex = parts.Length > 1 ? Convert.ToInt32(parts[1]) : 0;
|
||||
|
||||
if (File.Exists(iconFile))
|
||||
{
|
||||
Icon icon = IconExtractor.Extract(iconFile, iconIndex, true);
|
||||
if (icon != null)
|
||||
{
|
||||
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
|
||||
icon.Handle,
|
||||
Int32Rect.Empty,
|
||||
BitmapSizeOptions.FromEmptyOptions());
|
||||
|
||||
icon.Dispose();
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取应用图标时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
// 返回默认图标
|
||||
return GetDefaultIcon();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认图标
|
||||
/// </summary>
|
||||
private ImageSource GetDefaultIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 对于资源管理器,使用特定图标
|
||||
if (Path.EndsWith("explorer.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
// 直接从C:\Windows\explorer.exe获取图标
|
||||
string explorerPath = @"C:\Windows\explorer.exe";
|
||||
if (File.Exists(explorerPath))
|
||||
{
|
||||
Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(explorerPath);
|
||||
if (icon != null)
|
||||
{
|
||||
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
|
||||
icon.Handle,
|
||||
Int32Rect.Empty,
|
||||
BitmapSizeOptions.FromEmptyOptions());
|
||||
|
||||
icon.Dispose();
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取资源管理器图标时出错: {ex.Message}", LogHelper.LogType.Warning);
|
||||
// 如果获取Windows图标失败,回退到默认图标
|
||||
}
|
||||
|
||||
// 回退到备用图标
|
||||
string explorerIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_folder_24_regular.png");
|
||||
if (File.Exists(explorerIconPath))
|
||||
{
|
||||
Uri uri = new Uri(explorerIconPath);
|
||||
BitmapImage image = new BitmapImage(uri);
|
||||
_iconCache = image;
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回一个简单的默认图标
|
||||
string iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-png", "icc.png");
|
||||
if (File.Exists(iconPath))
|
||||
{
|
||||
Uri uri = new Uri(iconPath);
|
||||
BitmapImage image = new BitmapImage(uri);
|
||||
_iconCache = image;
|
||||
return _iconCache;
|
||||
}
|
||||
|
||||
// 如果还是没有找到,尝试使用应用程序图标
|
||||
string appIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_apps_24_regular.png");
|
||||
if (File.Exists(appIconPath))
|
||||
{
|
||||
Uri uri = new Uri(appIconPath);
|
||||
BitmapImage image = new BitmapImage(uri);
|
||||
_iconCache = image;
|
||||
return _iconCache;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取默认图标时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动应用程序
|
||||
/// </summary>
|
||||
public void Launch()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(Path))
|
||||
{
|
||||
LogHelper.WriteLogToFile("无法启动应用程序:路径为空", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!File.Exists(Path) && !Path.Contains(":\\"))
|
||||
{
|
||||
// 可能是系统命令,如explorer.exe
|
||||
ProcessStartInfo psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path,
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(psi);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 使用Process.Start启动应用程序
|
||||
ProcessStartInfo psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path,
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(psi);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已启动应用程序: {Path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动应用程序时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"启动应用程序时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图标提取工具类
|
||||
/// </summary>
|
||||
public static class IconExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// 从文件中提取图标
|
||||
/// </summary>
|
||||
/// <param name="file">文件路径</param>
|
||||
/// <param name="index">图标索引</param>
|
||||
/// <param name="largeIcon">是否提取大图标</param>
|
||||
/// <returns>提取的图标</returns>
|
||||
public static Icon Extract(string file, int index, bool largeIcon)
|
||||
{
|
||||
try
|
||||
{
|
||||
IntPtr large;
|
||||
IntPtr small;
|
||||
ExtractIconEx(file, index, out large, out small, 1);
|
||||
|
||||
try
|
||||
{
|
||||
return Icon.FromHandle(largeIcon ? large : small);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (large != IntPtr.Zero)
|
||||
DestroyIcon(large);
|
||||
|
||||
if (small != IntPtr.Zero)
|
||||
DestroyIcon(small);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("Shell32.dll", EntryPoint = "ExtractIconEx")]
|
||||
private static extern int ExtractIconEx(
|
||||
[MarshalAs(UnmanagedType.LPStr)] string lpszFile,
|
||||
int nIconIndex,
|
||||
out IntPtr phiconLarge,
|
||||
out IntPtr phiconSmall,
|
||||
int nIcons);
|
||||
|
||||
[DllImport("User32.dll")]
|
||||
private static extern int DestroyIcon(IntPtr hIcon);
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
<UserControl x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherSettingsControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="500" d:DesignWidth="600">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- 自定义按钮样式 -->
|
||||
<Style x:Key="DefaultButtonStyle" TargetType="Button">
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextElement.Foreground="{TemplateBinding Foreground}"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Opacity" Value="0.8"/>
|
||||
<Setter Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect Color="Black" Direction="270" ShadowDepth="2" Opacity="0.3" BlurRadius="4"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="border" Property="Opacity" Value="0.6"/>
|
||||
<Setter Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="border" Property="Opacity" Value="0.4"/>
|
||||
<Setter Property="Cursor" Value="Arrow"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题 -->
|
||||
<TextBlock Grid.Row="0" Text="超级启动台设置" FontSize="16" FontWeight="Bold" Margin="0,0,0,15" Foreground="Black"/>
|
||||
|
||||
<!-- 基本设置 -->
|
||||
<StackPanel Grid.Row="1" Margin="0,0,0,15">
|
||||
<TextBlock Text="基本设置" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
|
||||
|
||||
<Grid Margin="10,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 按钮位置 -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="按钮位置:" VerticalAlignment="Center" Foreground="Black"/>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="0,5">
|
||||
<RadioButton x:Name="RbtnLeft" Content="浮动栏左侧" Margin="0,0,20,0" Checked="RbtnPosition_Checked" Foreground="Black"/>
|
||||
<RadioButton x:Name="RbtnRight" Content="浮动栏右侧" IsChecked="True" Checked="RbtnPosition_Checked" Foreground="Black"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 应用管理 -->
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Text="应用管理" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
|
||||
|
||||
<Border Grid.Row="1" BorderThickness="1" BorderBrush="#CCCCCC" CornerRadius="5">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 应用列表 -->
|
||||
<DataGrid Grid.Row="0" x:Name="DgApps" AutoGenerateColumns="False" Margin="5"
|
||||
CanUserAddRows="False" CanUserDeleteRows="False"
|
||||
HeadersVisibility="Column" SelectionMode="Single"
|
||||
SelectionChanged="DgApps_SelectionChanged">
|
||||
<DataGrid.Columns>
|
||||
<DataGridCheckBoxColumn Header="显示" Binding="{Binding IsVisible}" Width="50"/>
|
||||
<DataGridTextColumn Header="名称" Binding="{Binding Name}" Width="150"/>
|
||||
<DataGridTextColumn Header="路径" Binding="{Binding Path}" Width="*"/>
|
||||
<DataGridTextColumn Header="位置" Binding="{Binding Position}" Width="50"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="5">
|
||||
<Button x:Name="BtnAdd" Content="添加" Padding="10,5" Margin="0,5,5,5" Click="BtnAdd_Click"
|
||||
Background="#FF007ACC" Foreground="White" BorderBrush="#FF005A9B" BorderThickness="1"
|
||||
Style="{StaticResource DefaultButtonStyle}"/>
|
||||
<Button x:Name="BtnEdit" Content="编辑" Padding="10,5" Margin="5" Click="BtnEdit_Click"
|
||||
Background="#FF6C757D" Foreground="White" BorderBrush="#FF5A6268" BorderThickness="1"
|
||||
Style="{StaticResource DefaultButtonStyle}"/>
|
||||
<Button x:Name="BtnDelete" Content="删除" Padding="10,5" Margin="5" Click="BtnDelete_Click"
|
||||
Background="#FFDC3545" Foreground="White" BorderBrush="#FFBD2130" BorderThickness="1"
|
||||
Style="{StaticResource DefaultButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0">
|
||||
<Button x:Name="BtnSave" Content="保存设置" Padding="15,5" Click="BtnSave_Click"
|
||||
Background="#FF28A745" Foreground="White" BorderBrush="#FF1E7E34" BorderThickness="1"
|
||||
Style="{StaticResource DefaultButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,396 +0,0 @@
|
||||
using Ink_Canvas.Windows;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// LauncherSettingsControl.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class LauncherSettingsControl : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 父插件
|
||||
/// </summary>
|
||||
private readonly SuperLauncherPlugin _plugin;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="plugin">父插件</param>
|
||||
public LauncherSettingsControl(SuperLauncherPlugin plugin)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_plugin = plugin;
|
||||
|
||||
// 设置按钮位置
|
||||
RbtnLeft.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left;
|
||||
RbtnRight.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Right;
|
||||
|
||||
// 绑定应用列表
|
||||
DgApps.ItemsSource = _plugin.LauncherItems;
|
||||
|
||||
// 初始化按钮状态
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新按钮状态
|
||||
/// </summary>
|
||||
private void UpdateButtonStates()
|
||||
{
|
||||
bool hasSelection = DgApps.SelectedItem != null;
|
||||
BtnEdit.IsEnabled = hasSelection;
|
||||
BtnDelete.IsEnabled = hasSelection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 位置单选按钮选择事件
|
||||
/// </summary>
|
||||
private void RbtnPosition_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
|
||||
LauncherButtonPosition oldPosition = _plugin.Config.ButtonPosition;
|
||||
|
||||
if (sender == RbtnLeft)
|
||||
{
|
||||
_plugin.Config.ButtonPosition = LauncherButtonPosition.Left;
|
||||
}
|
||||
else if (sender == RbtnRight)
|
||||
{
|
||||
_plugin.Config.ButtonPosition = LauncherButtonPosition.Right;
|
||||
}
|
||||
|
||||
// 如果位置发生变化,更新按钮位置
|
||||
if (oldPosition != _plugin.Config.ButtonPosition)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 更新按钮位置
|
||||
_plugin.UpdateButtonPosition();
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
|
||||
LogHelper.WriteLogToFile($"启动台按钮位置已更改为: {_plugin.Config.ButtonPosition}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"更新启动台按钮位置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnAdd_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建新的启动项
|
||||
LauncherItem item = new LauncherItem
|
||||
{
|
||||
Name = "",
|
||||
Path = "",
|
||||
IsVisible = true,
|
||||
Position = -1 // 让插件管理器分配位置
|
||||
};
|
||||
|
||||
// 直接显示编辑对话框
|
||||
EditLauncherItem(item, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"添加启动项时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"添加启动项时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑应用按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnEdit_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DgApps.SelectedItem is LauncherItem item)
|
||||
{
|
||||
EditLauncherItem(item, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除应用按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnDelete_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DgApps.SelectedItem is LauncherItem item)
|
||||
{
|
||||
// 确认删除
|
||||
MessageBoxResult result = MessageBox.Show(
|
||||
$"确定要删除 {item.Name} 吗?",
|
||||
"删除确认",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
// 从集合中移除
|
||||
_plugin.LauncherItems.Remove(item);
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存设置按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnSave_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
|
||||
// 如果插件已启用,重新加载启动台按钮
|
||||
if (_plugin.IsEnabled)
|
||||
{
|
||||
_plugin.Disable();
|
||||
_plugin.Enable();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果插件未启用,则启用它
|
||||
_plugin.Enable();
|
||||
|
||||
// 通知PluginSettingsWindow刷新插件列表
|
||||
var window = Window.GetWindow(this);
|
||||
if (window is PluginSettingsWindow pluginSettingsWindow)
|
||||
{
|
||||
// 触发刷新
|
||||
pluginSettingsWindow.RefreshPluginList();
|
||||
}
|
||||
}
|
||||
|
||||
MessageBox.Show("设置已保存并应用!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存设置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"保存设置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用项选择变更事件
|
||||
/// </summary>
|
||||
private void DgApps_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑启动项
|
||||
/// </summary>
|
||||
/// <param name="item">启动项</param>
|
||||
/// <param name="isNew">是否为新建</param>
|
||||
private void EditLauncherItem(LauncherItem item, bool isNew)
|
||||
{
|
||||
// 创建简单的编辑窗口
|
||||
Window editWindow = new Window
|
||||
{
|
||||
Title = isNew ? "添加" : "编辑应用",
|
||||
Width = 400,
|
||||
Height = 200,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
ResizeMode = ResizeMode.NoResize
|
||||
};
|
||||
|
||||
// 创建编辑表单
|
||||
Grid grid = new Grid
|
||||
{
|
||||
Margin = new Thickness(20)
|
||||
};
|
||||
|
||||
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
// 名称输入框
|
||||
TextBlock nameLabel = new TextBlock
|
||||
{
|
||||
Text = "名称:",
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
TextBox nameTextBox = new TextBox
|
||||
{
|
||||
Text = item.Name,
|
||||
Margin = new Thickness(0, 5, 0, 5)
|
||||
};
|
||||
|
||||
Grid.SetRow(nameLabel, 0);
|
||||
Grid.SetColumn(nameLabel, 0);
|
||||
Grid.SetRow(nameTextBox, 0);
|
||||
Grid.SetColumn(nameTextBox, 1);
|
||||
|
||||
grid.Children.Add(nameLabel);
|
||||
grid.Children.Add(nameTextBox);
|
||||
|
||||
// 路径输入框
|
||||
TextBlock pathLabel = new TextBlock
|
||||
{
|
||||
Text = "路径:",
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
Grid pathGrid = new Grid();
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength() });
|
||||
|
||||
TextBox pathTextBox = new TextBox
|
||||
{
|
||||
Text = item.Path,
|
||||
Margin = new Thickness(0, 5, 5, 5)
|
||||
};
|
||||
Button browseButton = new Button
|
||||
{
|
||||
Content = "浏览",
|
||||
Padding = new Thickness(5, 0, 5, 0),
|
||||
Margin = new Thickness(0, 5, 0, 5)
|
||||
};
|
||||
|
||||
browseButton.Click += (s, e) =>
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog
|
||||
{
|
||||
Title = "选择应用程序",
|
||||
Filter = "应用程序 (*.exe)|*.exe|所有文件 (*.*)|*.*",
|
||||
Multiselect = false,
|
||||
FileName = pathTextBox.Text
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
pathTextBox.Text = dialog.FileName;
|
||||
|
||||
// 如果选择的是.exe文件,自动获取文件名填入名称字段
|
||||
if (Path.GetExtension(dialog.FileName).ToLower() == ".exe")
|
||||
{
|
||||
string fileName = Path.GetFileNameWithoutExtension(dialog.FileName);
|
||||
// 只有在名称字段为空或者是新建项目时才自动填入
|
||||
if (string.IsNullOrWhiteSpace(nameTextBox.Text) || isNew)
|
||||
{
|
||||
nameTextBox.Text = fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Grid.SetColumn(pathTextBox, 0);
|
||||
Grid.SetColumn(browseButton, 1);
|
||||
pathGrid.Children.Add(pathTextBox);
|
||||
pathGrid.Children.Add(browseButton);
|
||||
|
||||
Grid.SetRow(pathLabel, 1);
|
||||
Grid.SetColumn(pathLabel, 0);
|
||||
Grid.SetRow(pathGrid, 1);
|
||||
Grid.SetColumn(pathGrid, 1);
|
||||
|
||||
grid.Children.Add(pathLabel);
|
||||
grid.Children.Add(pathGrid);
|
||||
|
||||
// 确认和取消按钮
|
||||
StackPanel buttonPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 10, 0, 0)
|
||||
};
|
||||
|
||||
Button okButton = new Button
|
||||
{
|
||||
Content = "确定",
|
||||
Padding = new Thickness(15, 5, 15, 5),
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
IsDefault = true
|
||||
};
|
||||
|
||||
Button cancelButton = new Button
|
||||
{
|
||||
Content = "取消",
|
||||
Padding = new Thickness(15, 5, 15, 5),
|
||||
IsCancel = true
|
||||
};
|
||||
|
||||
okButton.Click += (s, e) =>
|
||||
{
|
||||
// 验证输入
|
||||
if (string.IsNullOrWhiteSpace(nameTextBox.Text))
|
||||
{
|
||||
MessageBox.Show("请输入应用名称!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pathTextBox.Text))
|
||||
{
|
||||
MessageBox.Show("请输入应用路径!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新项目
|
||||
item.Name = nameTextBox.Text;
|
||||
item.Path = pathTextBox.Text;
|
||||
|
||||
// 如果是新建,添加到集合
|
||||
if (isNew)
|
||||
{
|
||||
_plugin.AddLauncherItem(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 触发属性变更通知,刷新DataGrid
|
||||
if (DgApps.ItemsSource is ICollectionView view)
|
||||
{
|
||||
view.Refresh();
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
|
||||
editWindow.DialogResult = true;
|
||||
editWindow.Close();
|
||||
};
|
||||
|
||||
cancelButton.Click += (s, e) =>
|
||||
{
|
||||
editWindow.DialogResult = false;
|
||||
editWindow.Close();
|
||||
};
|
||||
|
||||
buttonPanel.Children.Add(okButton);
|
||||
buttonPanel.Children.Add(cancelButton);
|
||||
|
||||
Grid.SetRow(buttonPanel, 2);
|
||||
Grid.SetColumnSpan(buttonPanel, 2);
|
||||
|
||||
grid.Children.Add(buttonPanel);
|
||||
|
||||
// 设置窗口内容
|
||||
editWindow.Content = grid;
|
||||
|
||||
// 显示窗口
|
||||
editWindow.ShowDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
<Window x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
|
||||
mc:Ignorable="d"
|
||||
Title="启动台"
|
||||
Width="400"
|
||||
Height="300"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="#80000000"
|
||||
ResizeMode="NoResize"
|
||||
Topmost="True"
|
||||
Deactivated="Window_Deactivated"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<Window.Resources>
|
||||
<!-- 应用项样式 -->
|
||||
<Style x:Key="LauncherItemStyle" TargetType="Button">
|
||||
<Setter Property="Width" Value="80"/>
|
||||
<Setter Property="Height" Value="80"/>
|
||||
<Setter Property="Margin" Value="5"/>
|
||||
<Setter Property="Background" Value="#40FFFFFF"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="border" Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="8">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Image Grid.Row="0" Source="{Binding Icon}" Width="32" Height="32" Margin="0,10,0,5"/>
|
||||
<TextBlock Grid.Row="1" Text="{Binding Name}" TextWrapping="Wrap" TextAlignment="Center"
|
||||
Margin="2,0,2,8" FontSize="11" Foreground="White"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#80FFFFFF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#C0FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Border CornerRadius="15" Background="#80000000" BorderThickness="1" BorderBrush="#40FFFFFF">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<Grid Grid.Row="0" Height="40">
|
||||
<TextBlock Text="启动台" Foreground="White" FontSize="18" FontWeight="Bold"
|
||||
VerticalAlignment="Center" Margin="15,0,0,0"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,10,0">
|
||||
<Button x:Name="BtnFixMode" Click="BtnFixMode_Click" Width="30" Height="30"
|
||||
Margin="5,0" Background="Transparent" BorderThickness="0"
|
||||
ToolTip="切换固定模式">
|
||||
<Path x:Name="FixModeIcon" Data="M7,2V13H10V22L17,10H13L17,2H7Z" Fill="White" Stretch="Uniform" Width="16" Height="16"/>
|
||||
</Button>
|
||||
<Button x:Name="BtnClose" Click="BtnClose_Click" Width="30" Height="30"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
ToolTip="关闭">
|
||||
<Path Data="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
|
||||
Fill="White" Stretch="Uniform" Width="16" Height="16"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 应用网格 -->
|
||||
<ScrollViewer Grid.Row="1" Margin="10" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<WrapPanel x:Name="AppPanel" Orientation="Horizontal" HorizontalAlignment="Center"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -1,466 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
|
||||
{
|
||||
/// <summary>
|
||||
/// LauncherWindow.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class LauncherWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 父插件
|
||||
/// </summary>
|
||||
private readonly SuperLauncherPlugin _plugin;
|
||||
|
||||
/// <summary>
|
||||
/// 是否处于固定模式
|
||||
/// </summary>
|
||||
private bool _isFixMode;
|
||||
|
||||
/// <summary>
|
||||
/// 应用项按钮列表
|
||||
/// </summary>
|
||||
private readonly Dictionary<Button, LauncherItem> _appButtons = new Dictionary<Button, LauncherItem>();
|
||||
|
||||
/// <summary>
|
||||
/// 拖拽中的按钮
|
||||
/// </summary>
|
||||
private Button _draggingButton;
|
||||
|
||||
/// <summary>
|
||||
/// 拖拽开始位置
|
||||
/// </summary>
|
||||
private Point _dragStartPoint;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public LauncherWindow(SuperLauncherPlugin plugin)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_plugin = plugin;
|
||||
|
||||
// 加载应用项
|
||||
LoadLauncherItems();
|
||||
|
||||
// 添加鼠标按下事件(用于拖动窗口)
|
||||
MouseDown += (s, e) =>
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed)
|
||||
{
|
||||
DragMove();
|
||||
}
|
||||
};
|
||||
|
||||
// 根据应用数量调整窗口大小
|
||||
AdjustWindowSize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载启动台应用项
|
||||
/// </summary>
|
||||
private void LoadLauncherItems()
|
||||
{
|
||||
// 清空现有应用项
|
||||
AppPanel.Children.Clear();
|
||||
_appButtons.Clear();
|
||||
|
||||
// 获取显示的应用项
|
||||
var visibleItems = _plugin.LauncherItems
|
||||
.Where(item => item.IsVisible)
|
||||
.OrderBy(item => item.Position)
|
||||
.ToList();
|
||||
|
||||
foreach (var item in visibleItems)
|
||||
{
|
||||
// 创建应用按钮
|
||||
Button appButton = new Button
|
||||
{
|
||||
Style = (Style)FindResource("LauncherItemStyle"),
|
||||
DataContext = item,
|
||||
Tag = item.Position
|
||||
};
|
||||
|
||||
// 添加点击事件
|
||||
appButton.Click += AppButton_Click;
|
||||
|
||||
// 在固定模式下,添加拖拽事件
|
||||
appButton.PreviewMouseDown += AppButton_PreviewMouseDown;
|
||||
appButton.PreviewMouseMove += AppButton_PreviewMouseMove;
|
||||
appButton.PreviewMouseUp += AppButton_PreviewMouseUp;
|
||||
|
||||
// 记录按钮和项目的对应关系
|
||||
_appButtons.Add(appButton, item);
|
||||
|
||||
// 添加到面板
|
||||
AppPanel.Children.Add(appButton);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据应用数量调整窗口大小
|
||||
/// </summary>
|
||||
private void AdjustWindowSize()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 每行最多显示4个应用
|
||||
const int appsPerRow = 4;
|
||||
|
||||
// 计算行数
|
||||
int visibleCount = _appButtons.Count;
|
||||
int rowCount = (int)Math.Ceiling(visibleCount / (double)appsPerRow);
|
||||
|
||||
// 设置窗口宽度(每个应用90像素宽 = 80 + 5*2)
|
||||
Width = Math.Min(appsPerRow * 90 + 40, 400); // 最大宽度400
|
||||
|
||||
// 设置窗口高度(每个应用90像素高 = 80 + 5*2)
|
||||
Height = Math.Min(rowCount * 90 + 60, 600); // 最大高度600,标题栏40 + 边距20
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"调整启动台窗口大小时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用按钮点击事件
|
||||
/// </summary>
|
||||
private void AppButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_isFixMode) return; // 在固定模式下,不响应点击事件
|
||||
|
||||
if (sender is Button button && _appButtons.TryGetValue(button, out LauncherItem item))
|
||||
{
|
||||
// 获取应用路径和名称,用于后续启动
|
||||
string appPath = item.Path;
|
||||
string appName = item.Name;
|
||||
|
||||
LogHelper.WriteLogToFile($"点击启动应用: {appName}, 路径: {appPath}");
|
||||
|
||||
// 首先标记窗口正在关闭
|
||||
IsClosing = true;
|
||||
|
||||
// 创建一个应用启动任务
|
||||
var launchTask = new Task(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 等待一段时间,确保窗口关闭流程已经开始
|
||||
Thread.Sleep(200);
|
||||
|
||||
// 使用UI线程启动应用
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查应用路径是否存在
|
||||
if (File.Exists(appPath) || !appPath.Contains(":\\"))
|
||||
{
|
||||
// 创建进程启动信息
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = appPath,
|
||||
UseShellExecute = true,
|
||||
};
|
||||
|
||||
// 启动应用程序
|
||||
var process = Process.Start(psi);
|
||||
LogHelper.WriteLogToFile($"应用程序 {appName} 已启动");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用路径不存在: {appPath}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"找不到应用程序: {appPath}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动应用程序失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
MessageBox.Show($"启动应用程序失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用启动任务出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
});
|
||||
|
||||
// 关闭窗口
|
||||
try
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try { Close(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
// 启动应用程序任务
|
||||
launchTask.Start();
|
||||
}), DispatcherPriority.Background);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"关闭窗口或启动任务时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
// 如果无法通过UI关闭窗口,直接启动任务
|
||||
launchTask.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"应用按钮点击事件出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
try { IsClosing = true; Close(); } catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
|
||||
}
|
||||
}
|
||||
|
||||
#region 固定模式拖拽事件
|
||||
|
||||
/// <summary>
|
||||
/// 应用按钮鼠标按下事件
|
||||
/// </summary>
|
||||
private void AppButton_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_isFixMode) return;
|
||||
|
||||
if (e.ChangedButton == MouseButton.Left && sender is Button button)
|
||||
{
|
||||
_draggingButton = button;
|
||||
_dragStartPoint = e.GetPosition(AppPanel);
|
||||
button.CaptureMouse();
|
||||
button.Opacity = 0.7;
|
||||
|
||||
// 阻止事件冒泡,以避免触发按钮点击
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用按钮鼠标移动事件
|
||||
/// </summary>
|
||||
private void AppButton_PreviewMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_isFixMode || _draggingButton == null) return;
|
||||
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
{
|
||||
Point currentPosition = e.GetPosition(AppPanel);
|
||||
|
||||
// 移动按钮
|
||||
System.Windows.Controls.Canvas.SetLeft(_draggingButton, currentPosition.X - _draggingButton.ActualWidth / 2);
|
||||
System.Windows.Controls.Canvas.SetTop(_draggingButton, currentPosition.Y - _draggingButton.ActualHeight / 2);
|
||||
|
||||
// 将按钮移到最上层
|
||||
Panel.SetZIndex(_draggingButton, 100);
|
||||
|
||||
// 阻止事件冒泡
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用按钮鼠标释放事件
|
||||
/// </summary>
|
||||
private void AppButton_PreviewMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_isFixMode || _draggingButton == null) return;
|
||||
|
||||
// 释放鼠标捕获
|
||||
_draggingButton.ReleaseMouseCapture();
|
||||
|
||||
// 计算新位置
|
||||
Point releasePoint = e.GetPosition(AppPanel);
|
||||
int newPosition = CalculateGridPosition(releasePoint);
|
||||
|
||||
// 获取当前项目
|
||||
LauncherItem currentItem = _appButtons[_draggingButton];
|
||||
|
||||
// 重新排序
|
||||
ReorderItems(currentItem, newPosition);
|
||||
|
||||
// 重新加载应用项
|
||||
LoadLauncherItems();
|
||||
|
||||
// 保存配置
|
||||
_plugin.SaveConfig();
|
||||
|
||||
// 清除拖拽状态
|
||||
_draggingButton.Opacity = 1;
|
||||
Panel.SetZIndex(_draggingButton, 0);
|
||||
_draggingButton = null;
|
||||
|
||||
// 阻止事件冒泡
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算网格位置
|
||||
/// </summary>
|
||||
private int CalculateGridPosition(Point point)
|
||||
{
|
||||
// 计算行和列
|
||||
int columnCount = 4; // 每行最多4个应用
|
||||
int columnWidth = 90; // 应用宽度(包括边距)
|
||||
int rowHeight = 90; // 应用高度(包括边距)
|
||||
|
||||
int column = (int)(point.X / columnWidth);
|
||||
int row = (int)(point.Y / rowHeight);
|
||||
|
||||
// 确保在有效范围内
|
||||
column = Math.Max(0, Math.Min(column, columnCount - 1));
|
||||
row = Math.Max(0, row);
|
||||
|
||||
// 计算位置索引
|
||||
return row * columnCount + column;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新排序应用项
|
||||
/// </summary>
|
||||
private void ReorderItems(LauncherItem item, int newPosition)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 设置项目为固定位置
|
||||
item.IsPositionFixed = true;
|
||||
|
||||
// 如果位置相同,无需调整
|
||||
if (item.Position == newPosition)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有可见项目
|
||||
var visibleItems = _plugin.LauncherItems
|
||||
.Where(i => i.IsVisible)
|
||||
.OrderBy(i => i.Position)
|
||||
.ToList();
|
||||
|
||||
// 移除当前项目
|
||||
visibleItems.Remove(item);
|
||||
|
||||
// 查找插入位置
|
||||
int insertIndex = 0;
|
||||
for (int i = 0; i < visibleItems.Count; i++)
|
||||
{
|
||||
if (visibleItems[i].Position >= newPosition)
|
||||
{
|
||||
insertIndex = i;
|
||||
break;
|
||||
}
|
||||
insertIndex = i + 1;
|
||||
}
|
||||
|
||||
// 插入项目
|
||||
visibleItems.Insert(insertIndex, item);
|
||||
|
||||
// 重新分配位置
|
||||
for (int i = 0; i < visibleItems.Count; i++)
|
||||
{
|
||||
visibleItems[i].Position = i;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"重新排序应用项时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 窗口事件处理
|
||||
|
||||
/// <summary>
|
||||
/// 窗口失去焦点事件
|
||||
/// </summary>
|
||||
private void Window_Deactivated(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 只有在非固定模式、窗口已加载、未处于关闭状态且IsLoaded=true时关闭窗口
|
||||
if (!_isFixMode && IsLoaded && !IsClosing)
|
||||
{
|
||||
// 标记为正在关闭
|
||||
IsClosing = true;
|
||||
|
||||
// 使用Dispatcher.BeginInvoke而不是直接调用Close,避免冲突
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再次检查窗口状态
|
||||
if (IsLoaded && !IsClosing)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"延迟关闭窗口时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}), DispatcherPriority.Background);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"窗口失去焦点关闭时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口是否正在关闭
|
||||
/// </summary>
|
||||
private bool IsClosing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 重写OnClosing方法,标记窗口正在关闭
|
||||
/// </summary>
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
IsClosing = true;
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 固定模式按钮点击事件
|
||||
/// </summary>
|
||||
private void BtnFixMode_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 切换固定模式
|
||||
_isFixMode = !_isFixMode;
|
||||
|
||||
// 更新固定模式按钮图标颜色
|
||||
FixModeIcon.Fill = _isFixMode ? Brushes.Yellow : Brushes.White;
|
||||
|
||||
// 显示提示
|
||||
if (_isFixMode)
|
||||
{
|
||||
MessageBox.Show("已进入固定模式,您可以拖动应用图标调整位置。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,589 +0,0 @@
|
||||
using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins.BuiltIn
|
||||
{
|
||||
/// <summary>
|
||||
/// 超级启动台插件
|
||||
/// </summary>
|
||||
public class SuperLauncherPlugin : PluginBase
|
||||
{
|
||||
#region 插件基本信息
|
||||
|
||||
public override string Name => "超级启动台";
|
||||
|
||||
public override string Description => "在浮动栏添加一个启动台按钮,可快速启动常用应用程序。";
|
||||
|
||||
public override Version Version => new Version(1, 0, 1);
|
||||
|
||||
public override string Author => "ICC CE 团队";
|
||||
|
||||
public override bool IsBuiltIn => true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件属性和字段
|
||||
|
||||
/// <summary>
|
||||
/// 启动台配置
|
||||
/// </summary>
|
||||
public LauncherConfig Config { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动台应用程序列表
|
||||
/// </summary>
|
||||
public ObservableCollection<LauncherItem> LauncherItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动台按钮
|
||||
/// </summary>
|
||||
private LauncherButton _launcherButton;
|
||||
|
||||
/// <summary>
|
||||
/// 启动台窗口
|
||||
/// </summary>
|
||||
private LauncherWindow _launcherWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件路径
|
||||
/// </summary>
|
||||
private readonly string _configPath = Path.Combine(App.RootPath, "PluginConfigs", "SuperLauncher.json");
|
||||
|
||||
/// <summary>
|
||||
/// 标记是否已添加到浮动栏
|
||||
/// </summary>
|
||||
private bool _isAddedToFloatingBar;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件生命周期
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// 创建配置目录
|
||||
string configDir = Path.Combine(App.RootPath, "PluginConfigs");
|
||||
if (!Directory.Exists(configDir))
|
||||
{
|
||||
Directory.CreateDirectory(configDir);
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
LoadConfig();
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台插件已初始化");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsEnabled) return; // 防止重复启用
|
||||
|
||||
// 创建启动台按钮
|
||||
if (_launcherButton == null)
|
||||
{
|
||||
_launcherButton = new LauncherButton(this);
|
||||
LogHelper.WriteLogToFile("超级启动台按钮已创建");
|
||||
}
|
||||
|
||||
// 添加启动台按钮到浮动栏
|
||||
AddLauncherButtonToFloatingBar();
|
||||
|
||||
// 设置启用状态
|
||||
base.Enable();
|
||||
|
||||
// 保存插件配置
|
||||
SavePluginSettings();
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台插件已启用");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsEnabled) return; // 防止重复禁用
|
||||
|
||||
// 从浮动栏移除启动台按钮
|
||||
RemoveLauncherButtonFromFloatingBar();
|
||||
|
||||
// 如果启动台窗口打开,则关闭
|
||||
if (_launcherWindow != null && _launcherWindow.IsVisible)
|
||||
{
|
||||
_launcherWindow.Close();
|
||||
_launcherWindow = null;
|
||||
}
|
||||
|
||||
// 设置禁用状态
|
||||
base.Disable();
|
||||
|
||||
// 保存插件配置
|
||||
SavePluginSettings();
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台插件已禁用");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"禁用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override UserControl GetSettingsView()
|
||||
{
|
||||
return new LauncherSettingsControl(this);
|
||||
}
|
||||
|
||||
public override void Cleanup()
|
||||
{
|
||||
// 保存配置
|
||||
SaveConfig();
|
||||
|
||||
// 从浮动栏移除启动台按钮
|
||||
RemoveLauncherButtonFromFloatingBar();
|
||||
|
||||
// 如果启动台窗口打开,则关闭
|
||||
if (_launcherWindow != null && _launcherWindow.IsVisible)
|
||||
{
|
||||
_launcherWindow.Close();
|
||||
_launcherWindow = null;
|
||||
}
|
||||
|
||||
base.Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存插件设置
|
||||
/// </summary>
|
||||
public override void SavePluginSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 确保配置已加载
|
||||
if (Config == null)
|
||||
{
|
||||
LoadConfig();
|
||||
}
|
||||
|
||||
// 更新其他设置,但不更改插件启用状态
|
||||
|
||||
// 保存配置
|
||||
SaveConfig();
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台插件设置已保存");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存超级启动台插件设置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 配置管理
|
||||
|
||||
/// <summary>
|
||||
/// 加载配置
|
||||
/// </summary>
|
||||
private void LoadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_configPath))
|
||||
{
|
||||
string json = File.ReadAllText(_configPath);
|
||||
Config = JsonConvert.DeserializeObject<LauncherConfig>(json) ?? CreateDefaultConfig();
|
||||
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items ?? new List<LauncherItem>());
|
||||
|
||||
// 注意:不再根据配置更改插件启用状态
|
||||
// 插件状态由PluginManager统一管理
|
||||
}
|
||||
else
|
||||
{
|
||||
Config = CreateDefaultConfig();
|
||||
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items);
|
||||
SaveConfig();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
Config = CreateDefaultConfig();
|
||||
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存配置
|
||||
/// </summary>
|
||||
public void SaveConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 同步LauncherItems到Config
|
||||
Config.Items = new List<LauncherItem>(LauncherItems);
|
||||
|
||||
// 序列化并保存配置
|
||||
string json = JsonConvert.SerializeObject(Config, Formatting.Indented);
|
||||
File.WriteAllText(_configPath, json);
|
||||
|
||||
LogHelper.WriteLogToFile("超级启动台配置已保存");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建默认配置
|
||||
/// </summary>
|
||||
private LauncherConfig CreateDefaultConfig()
|
||||
{
|
||||
var config = new LauncherConfig
|
||||
{
|
||||
ButtonPosition = LauncherButtonPosition.Right,
|
||||
// 不再使用IsEnabled,插件状态由PluginManager管理
|
||||
Items = new List<LauncherItem>
|
||||
{
|
||||
new LauncherItem
|
||||
{
|
||||
Name = "资源管理器",
|
||||
Path = @"C:\Windows\explorer.exe",
|
||||
IsVisible = true,
|
||||
Position = 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 启动台按钮管理
|
||||
|
||||
/// <summary>
|
||||
/// 将启动台按钮添加到浮动栏
|
||||
/// </summary>
|
||||
private void AddLauncherButtonToFloatingBar()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果已经添加,先移除
|
||||
if (_isAddedToFloatingBar)
|
||||
{
|
||||
RemoveLauncherButtonFromFloatingBar();
|
||||
_isAddedToFloatingBar = false;
|
||||
}
|
||||
|
||||
// 获取主窗口实例
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到主窗口实例,无法添加启动台按钮", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建启动台按钮
|
||||
_launcherButton = new LauncherButton(this);
|
||||
var buttonElement = _launcherButton.Element;
|
||||
|
||||
// 查找浮动栏
|
||||
var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel;
|
||||
if (floatingBar == null)
|
||||
{
|
||||
// 如果直接查找失败,则尝试遍历可视树查找
|
||||
Panel floatingBarPanelFromTree = null;
|
||||
FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree);
|
||||
floatingBar = floatingBarPanelFromTree;
|
||||
}
|
||||
|
||||
if (floatingBar == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到浮动栏,无法添加启动台按钮", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加启动台按钮到浮动栏
|
||||
if (Config.ButtonPosition == LauncherButtonPosition.Left)
|
||||
{
|
||||
floatingBar.Children.Insert(0, buttonElement);
|
||||
LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏左侧");
|
||||
}
|
||||
else
|
||||
{
|
||||
floatingBar.Children.Add(buttonElement);
|
||||
LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏右侧");
|
||||
}
|
||||
|
||||
_isAddedToFloatingBar = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"添加启动台按钮到浮动栏时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归查找StackPanelFloatingBar
|
||||
/// </summary>
|
||||
private void FindStackPanelFloatingBar(DependencyObject parent, ref Panel result)
|
||||
{
|
||||
if (parent == null || result != null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 检查当前对象是否为我们要找的面板
|
||||
if (parent is Panel panel && panel.Name == "StackPanelFloatingBar")
|
||||
{
|
||||
result = panel;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取子元素数量
|
||||
int childCount = VisualTreeHelper.GetChildrenCount(parent);
|
||||
|
||||
// 遍历所有子元素
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
|
||||
FindStackPanelFloatingBar(child, ref result);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"查找StackPanelFloatingBar时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从浮动栏移除启动台按钮
|
||||
/// </summary>
|
||||
private void RemoveLauncherButtonFromFloatingBar()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_isAddedToFloatingBar || _launcherButton == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取主窗口实例
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到主窗口实例,无法移除启动台按钮", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取按钮元素
|
||||
var buttonElement = _launcherButton.Element;
|
||||
|
||||
// 查找浮动栏
|
||||
var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel;
|
||||
if (floatingBar == null)
|
||||
{
|
||||
// 如果直接查找失败,则尝试遍历可视树查找
|
||||
Panel floatingBarPanelFromTree = null;
|
||||
FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree);
|
||||
floatingBar = floatingBarPanelFromTree;
|
||||
}
|
||||
|
||||
if (floatingBar == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile("未找到浮动栏,无法移除启动台按钮", LogHelper.LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 从浮动栏移除启动台按钮
|
||||
if (floatingBar.Children.Contains(buttonElement))
|
||||
{
|
||||
floatingBar.Children.Remove(buttonElement);
|
||||
LogHelper.WriteLogToFile("启动台按钮已从浮动栏移除");
|
||||
}
|
||||
|
||||
_isAddedToFloatingBar = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"移除启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新启动台按钮位置
|
||||
/// </summary>
|
||||
public void UpdateButtonPosition()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果按钮已添加到浮动栏,重新添加以更新位置
|
||||
if (_isAddedToFloatingBar)
|
||||
{
|
||||
RemoveLauncherButtonFromFloatingBar();
|
||||
AddLauncherButtonToFloatingBar();
|
||||
LogHelper.WriteLogToFile($"启动台按钮位置已更新为: {Config.ButtonPosition}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
LogHelper.NewLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 启动台功能
|
||||
|
||||
/// <summary>
|
||||
/// 显示启动台窗口
|
||||
/// </summary>
|
||||
/// <param name="buttonPosition">按钮在屏幕上的位置</param>
|
||||
public void ShowLauncherWindow(Point buttonPosition)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果窗口已存在,关闭它
|
||||
if (_launcherWindow != null && _launcherWindow.IsVisible)
|
||||
{
|
||||
_launcherWindow.Close();
|
||||
_launcherWindow = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新的启动台窗口
|
||||
_launcherWindow = new LauncherWindow(this);
|
||||
|
||||
// 计算窗口位置,使其位于按钮上方
|
||||
PositionLauncherWindow(_launcherWindow, buttonPosition);
|
||||
|
||||
// 显示窗口
|
||||
_launcherWindow.Show();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"显示启动台窗口时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置启动台窗口位置
|
||||
/// </summary>
|
||||
/// <param name="window">启动台窗口</param>
|
||||
/// <param name="buttonPosition">按钮在屏幕上的位置</param>
|
||||
private void PositionLauncherWindow(LauncherWindow window, Point buttonPosition)
|
||||
{
|
||||
// 确保窗口已加载
|
||||
if (window.ActualWidth == 0 || window.ActualHeight == 0)
|
||||
{
|
||||
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
|
||||
// 设置窗口加载完成后的位置
|
||||
window.Loaded += (s, e) =>
|
||||
{
|
||||
// 窗口位于按钮上方居中
|
||||
double left = buttonPosition.X - (window.ActualWidth / 2);
|
||||
double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距
|
||||
|
||||
// 确保窗口在屏幕内
|
||||
left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth));
|
||||
top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight));
|
||||
|
||||
window.Left = left;
|
||||
window.Top = top;
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// 窗口位于按钮上方居中
|
||||
double left = buttonPosition.X - (window.ActualWidth / 2);
|
||||
double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距
|
||||
|
||||
// 确保窗口在屏幕内
|
||||
left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth));
|
||||
top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight));
|
||||
|
||||
window.Left = left;
|
||||
window.Top = top;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加应用到启动台
|
||||
/// </summary>
|
||||
/// <param name="item">启动台项</param>
|
||||
public void AddLauncherItem(LauncherItem item)
|
||||
{
|
||||
// 如果项目数量已达上限,则不添加
|
||||
if (LauncherItems.Count >= 40)
|
||||
{
|
||||
MessageBox.Show("启动台项目数量已达上限(40个)!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
// 寻找合适的位置
|
||||
if (item.Position < 0)
|
||||
{
|
||||
item.Position = FindNextAvailablePosition();
|
||||
}
|
||||
|
||||
// 添加项目并保存配置
|
||||
LauncherItems.Add(item);
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找下一个可用位置
|
||||
/// </summary>
|
||||
private int FindNextAvailablePosition()
|
||||
{
|
||||
// 获取已使用的位置列表
|
||||
var usedPositions = new HashSet<int>();
|
||||
foreach (var item in LauncherItems)
|
||||
{
|
||||
usedPositions.Add(item.Position);
|
||||
}
|
||||
|
||||
// 查找第一个可用位置
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
if (!usedPositions.Contains(i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有位置都已使用,则返回0
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 增强的插件基类,提供对插件服务的访问和基本实现
|
||||
/// </summary>
|
||||
public abstract class EnhancedPluginBase : PluginBase, IEnhancedPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件服务实例
|
||||
/// </summary>
|
||||
public IPluginService PluginService { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
protected EnhancedPluginBase()
|
||||
{
|
||||
PluginService = PluginServiceManager.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件启动时调用,在Initialize之后
|
||||
/// </summary>
|
||||
public virtual void OnStartup()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已启动");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件关闭时调用,在Cleanup之前
|
||||
/// </summary>
|
||||
public virtual void OnShutdown()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 正在关闭");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的菜单项
|
||||
/// </summary>
|
||||
/// <returns>菜单项集合</returns>
|
||||
public virtual MenuItem[] GetMenuItems()
|
||||
{
|
||||
return new MenuItem[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的工具栏按钮
|
||||
/// </summary>
|
||||
/// <returns>工具栏按钮集合</returns>
|
||||
public virtual Button[] GetToolbarButtons()
|
||||
{
|
||||
return new Button[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的状态栏信息
|
||||
/// </summary>
|
||||
/// <returns>状态栏信息</returns>
|
||||
public virtual string GetStatusBarInfo()
|
||||
{
|
||||
return $"{Name} v{Version} - {(IsEnabled ? "已启用" : "已禁用")}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件配置变更时调用
|
||||
/// </summary>
|
||||
public virtual void OnConfigurationChanged()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 配置已变更");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写初始化方法,调用OnStartup
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
OnStartup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写清理方法,调用OnShutdown
|
||||
/// </summary>
|
||||
public override void Cleanup()
|
||||
{
|
||||
OnShutdown();
|
||||
base.Cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 增强的插件基类 V2,提供对三个专门服务接口的访问
|
||||
/// 插件开发者可以根据需要选择性地使用这些服务
|
||||
/// </summary>
|
||||
public abstract class EnhancedPluginBaseV2 : PluginBase, IEnhancedPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取服务实例
|
||||
/// </summary>
|
||||
public IGetService GetService { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 窗口服务实例
|
||||
/// </summary>
|
||||
public IWindowService WindowService { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作服务实例
|
||||
/// </summary>
|
||||
public IActionService ActionService { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件服务实例(兼容性)
|
||||
/// </summary>
|
||||
public IPluginService PluginService { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
protected EnhancedPluginBaseV2()
|
||||
{
|
||||
// 初始化所有服务实例
|
||||
PluginService = PluginServiceManager.Instance;
|
||||
GetService = PluginServiceManager.Instance;
|
||||
WindowService = PluginServiceManager.Instance;
|
||||
ActionService = PluginServiceManager.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件启动时调用,在Initialize之后
|
||||
/// </summary>
|
||||
public virtual void OnStartup()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已启动");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件关闭时调用,在Cleanup之前
|
||||
/// </summary>
|
||||
public virtual void OnShutdown()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 正在关闭");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的菜单项
|
||||
/// </summary>
|
||||
/// <returns>菜单项集合</returns>
|
||||
public virtual MenuItem[] GetMenuItems()
|
||||
{
|
||||
return new MenuItem[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的工具栏按钮
|
||||
/// </summary>
|
||||
/// <returns>工具栏按钮集合</returns>
|
||||
public virtual Button[] GetToolbarButtons()
|
||||
{
|
||||
return new Button[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的状态栏信息
|
||||
/// </summary>
|
||||
/// <returns>状态栏信息</returns>
|
||||
public virtual string GetStatusBarInfo()
|
||||
{
|
||||
return $"{Name} v{Version} - {(IsEnabled ? "已启用" : "已禁用")}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件配置变更时调用
|
||||
/// </summary>
|
||||
public virtual void OnConfigurationChanged()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 配置已变更");
|
||||
}
|
||||
|
||||
#region 便捷方法
|
||||
|
||||
/// <summary>
|
||||
/// 显示通知消息
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="type">消息类型</param>
|
||||
protected void ShowNotification(string message, NotificationType type = NotificationType.Info)
|
||||
{
|
||||
WindowService.ShowNotification(message, type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示确认对话框
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <returns>用户选择结果</returns>
|
||||
protected bool ShowConfirmDialog(string message, string title = "确认")
|
||||
{
|
||||
return WindowService.ShowConfirmDialog(message, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示输入对话框
|
||||
/// </summary>
|
||||
/// <param name="message">提示消息</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>用户输入内容</returns>
|
||||
protected string ShowInputDialog(string message, string title = "输入", string defaultValue = "")
|
||||
{
|
||||
return WindowService.ShowInputDialog(message, title, defaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>设置值</returns>
|
||||
protected T GetSetting<T>(string key, T defaultValue = default(T))
|
||||
{
|
||||
return GetService.GetSetting(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="value">设置值</param>
|
||||
protected void SetSetting<T>(string key, T value)
|
||||
{
|
||||
ActionService.SetSetting(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存设置
|
||||
/// </summary>
|
||||
protected void SaveSettings()
|
||||
{
|
||||
ActionService.SaveSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除当前画布
|
||||
/// </summary>
|
||||
protected void ClearCanvas()
|
||||
{
|
||||
ActionService.ClearCanvas();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 撤销操作
|
||||
/// </summary>
|
||||
protected void Undo()
|
||||
{
|
||||
ActionService.Undo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重做操作
|
||||
/// </summary>
|
||||
protected void Redo()
|
||||
{
|
||||
ActionService.Redo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以撤销
|
||||
/// </summary>
|
||||
protected bool CanUndo => GetService.CanUndo;
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以重做
|
||||
/// </summary>
|
||||
protected bool CanRedo => GetService.CanRedo;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前绘制模式
|
||||
/// </summary>
|
||||
protected int CurrentDrawingMode => GetService.CurrentDrawingMode;
|
||||
|
||||
/// <summary>
|
||||
/// 设置绘制模式
|
||||
/// </summary>
|
||||
/// <param name="mode">绘制模式</param>
|
||||
protected void SetDrawingMode(int mode)
|
||||
{
|
||||
ActionService.SetDrawingMode(mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
protected void RegisterEventHandler(string eventName, System.EventHandler handler)
|
||||
{
|
||||
ActionService.RegisterEventHandler(eventName, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注销事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
protected void UnregisterEventHandler(string eventName, System.EventHandler handler)
|
||||
{
|
||||
ActionService.UnregisterEventHandler(eventName, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发事件
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="args">事件参数</param>
|
||||
protected void TriggerEvent(string eventName, object sender, System.EventArgs args)
|
||||
{
|
||||
ActionService.TriggerEvent(eventName, sender, args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 操作服务接口,统一所有执行操作相关的方法
|
||||
/// </summary>
|
||||
public interface IActionService
|
||||
{
|
||||
#region 画布操作
|
||||
|
||||
/// <summary>
|
||||
/// 清除当前画布
|
||||
/// </summary>
|
||||
void ClearCanvas();
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有画布
|
||||
/// </summary>
|
||||
void ClearAllCanvases();
|
||||
|
||||
/// <summary>
|
||||
/// 添加新页面
|
||||
/// </summary>
|
||||
void AddNewPage();
|
||||
|
||||
/// <summary>
|
||||
/// 删除当前页面
|
||||
/// </summary>
|
||||
void DeleteCurrentPage();
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定页面
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">页面索引</param>
|
||||
void SwitchToPage(int pageIndex);
|
||||
|
||||
/// <summary>
|
||||
/// 切换到下一页
|
||||
/// </summary>
|
||||
void NextPage();
|
||||
|
||||
/// <summary>
|
||||
/// 切换到上一页
|
||||
/// </summary>
|
||||
void PreviousPage();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 绘制操作
|
||||
|
||||
/// <summary>
|
||||
/// 设置绘制模式
|
||||
/// </summary>
|
||||
/// <param name="mode">绘制模式</param>
|
||||
void SetDrawingMode(int mode);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触宽度
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
void SetInkWidth(double width);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触颜色
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
void SetInkColor(Color color);
|
||||
|
||||
/// <summary>
|
||||
/// 设置高亮笔宽度
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
void SetHighlighterWidth(double width);
|
||||
|
||||
/// <summary>
|
||||
/// 设置橡皮擦大小
|
||||
/// </summary>
|
||||
/// <param name="size">大小</param>
|
||||
void SetEraserSize(int size);
|
||||
|
||||
/// <summary>
|
||||
/// 设置橡皮擦类型
|
||||
/// </summary>
|
||||
/// <param name="type">类型</param>
|
||||
void SetEraserType(int type);
|
||||
|
||||
/// <summary>
|
||||
/// 设置橡皮擦形状
|
||||
/// </summary>
|
||||
/// <param name="shape">形状</param>
|
||||
void SetEraserShape(int shape);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触透明度
|
||||
/// </summary>
|
||||
/// <param name="alpha">透明度</param>
|
||||
void SetInkAlpha(double alpha);
|
||||
|
||||
/// <summary>
|
||||
/// 设置笔触样式
|
||||
/// </summary>
|
||||
/// <param name="style">样式</param>
|
||||
void SetInkStyle(int style);
|
||||
|
||||
/// <summary>
|
||||
/// 设置背景颜色
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
void SetBackgroundColor(string color);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文件操作
|
||||
|
||||
/// <summary>
|
||||
/// 保存画布内容
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
void SaveCanvas(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// 加载画布内容
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
void LoadCanvas(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// 导出为图片
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="format">图片格式</param>
|
||||
void ExportAsImage(string filePath, string format);
|
||||
|
||||
/// <summary>
|
||||
/// 导出为PDF
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
void ExportAsPDF(string filePath);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 撤销重做操作
|
||||
|
||||
/// <summary>
|
||||
/// 撤销操作
|
||||
/// </summary>
|
||||
void Undo();
|
||||
|
||||
/// <summary>
|
||||
/// 重做操作
|
||||
/// </summary>
|
||||
void Redo();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 选择操作
|
||||
|
||||
/// <summary>
|
||||
/// 全选
|
||||
/// </summary>
|
||||
void SelectAll();
|
||||
|
||||
/// <summary>
|
||||
/// 取消选择
|
||||
/// </summary>
|
||||
void DeselectAll();
|
||||
|
||||
/// <summary>
|
||||
/// 删除选中内容
|
||||
/// </summary>
|
||||
void DeleteSelected();
|
||||
|
||||
/// <summary>
|
||||
/// 复制选中内容
|
||||
/// </summary>
|
||||
void CopySelected();
|
||||
|
||||
/// <summary>
|
||||
/// 剪切选中内容
|
||||
/// </summary>
|
||||
void CutSelected();
|
||||
|
||||
/// <summary>
|
||||
/// 粘贴内容
|
||||
/// </summary>
|
||||
void Paste();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 系统设置操作
|
||||
|
||||
/// <summary>
|
||||
/// 设置系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="value">设置值</param>
|
||||
void SetSetting<T>(string key, T value);
|
||||
|
||||
/// <summary>
|
||||
/// 保存设置到文件
|
||||
/// </summary>
|
||||
void SaveSettings();
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载设置
|
||||
/// </summary>
|
||||
void LoadSettings();
|
||||
|
||||
/// <summary>
|
||||
/// 重置设置为默认值
|
||||
/// </summary>
|
||||
void ResetSettings();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件管理操作
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
void EnablePlugin(string pluginName);
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
void DisablePlugin(string pluginName);
|
||||
|
||||
/// <summary>
|
||||
/// 卸载插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
void UnloadPlugin(string pluginName);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件系统操作
|
||||
|
||||
/// <summary>
|
||||
/// 注册事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
void RegisterEventHandler(string eventName, EventHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// 注销事件处理器
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
void UnregisterEventHandler(string eventName, EventHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// 触发事件
|
||||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="args">事件参数</param>
|
||||
void TriggerEvent(string eventName, object sender, EventArgs args);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 应用程序操作
|
||||
|
||||
/// <summary>
|
||||
/// 重启应用程序
|
||||
/// </summary>
|
||||
void RestartApplication();
|
||||
|
||||
/// <summary>
|
||||
/// 退出应用程序
|
||||
/// </summary>
|
||||
void ExitApplication();
|
||||
|
||||
/// <summary>
|
||||
/// 检查更新
|
||||
/// </summary>
|
||||
void CheckForUpdates();
|
||||
|
||||
/// <summary>
|
||||
/// 打开帮助文档
|
||||
/// </summary>
|
||||
void OpenHelpDocument();
|
||||
|
||||
/// <summary>
|
||||
/// 打开关于页面
|
||||
/// </summary>
|
||||
void OpenAboutPage();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// ICCPP 插件适配器,用于加载和管理 .iccpp 格式的插件
|
||||
/// </summary>
|
||||
public class ICCPPPluginAdapter : PluginBase
|
||||
{
|
||||
private readonly byte[] _pluginData;
|
||||
private readonly string _pluginPath;
|
||||
private readonly string _pluginName;
|
||||
private readonly Version _pluginVersion;
|
||||
private bool _isInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// 创建 ICCPP 插件适配器
|
||||
/// </summary>
|
||||
/// <param name="pluginPath">插件文件路径</param>
|
||||
/// <param name="pluginData">插件文件数据</param>
|
||||
public ICCPPPluginAdapter(string pluginPath, byte[] pluginData)
|
||||
{
|
||||
_pluginPath = pluginPath;
|
||||
_pluginData = pluginData;
|
||||
PluginPath = pluginPath;
|
||||
|
||||
// 从文件名获取插件名称
|
||||
_pluginName = Path.GetFileNameWithoutExtension(pluginPath);
|
||||
_pluginVersion = new Version(1, 0, 0); // 默认版本
|
||||
|
||||
// 尝试从插件数据中读取更多信息
|
||||
TryReadPluginMetadata();
|
||||
}
|
||||
|
||||
public ICCPPPluginAdapter()
|
||||
{
|
||||
_pluginPath = string.Empty;
|
||||
_pluginData = new byte[0];
|
||||
PluginPath = string.Empty;
|
||||
_pluginName = "ICCPPPlugin";
|
||||
_pluginVersion = new Version(1, 0, 0);
|
||||
// 可选:初始化其他字段
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从插件数据中读取元数据
|
||||
/// </summary>
|
||||
private void TryReadPluginMetadata()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 这里可以根据 .iccpp 文件的实际格式解析元数据
|
||||
// 例如,如果文件有特定的头部结构,可以在这里解析
|
||||
|
||||
// 示例:如果前100字节包含元数据
|
||||
if (_pluginData.Length > 100)
|
||||
{
|
||||
// 解析元数据的代码...
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"解析插件 {_pluginName} 元数据时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#region IPlugin 接口实现
|
||||
|
||||
/// <summary>
|
||||
/// 插件名称
|
||||
/// </summary>
|
||||
public override string Name => _pluginName;
|
||||
|
||||
/// <summary>
|
||||
/// 插件描述
|
||||
/// </summary>
|
||||
public override string Description => $"{_pluginName} (ICCPP 格式插件)";
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
public override Version Version => _pluginVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 插件作者
|
||||
/// </summary>
|
||||
public override string Author => "未知";
|
||||
|
||||
/// <summary>
|
||||
/// 是否为内置插件
|
||||
/// </summary>
|
||||
public override bool IsBuiltIn => false;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化插件
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 这里可以添加 .iccpp 插件的初始化逻辑
|
||||
// 例如,根据文件格式加载特定资源
|
||||
|
||||
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已初始化");
|
||||
_isInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"初始化 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// </summary>
|
||||
public override void Enable()
|
||||
{
|
||||
if (IsEnabled) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 这里可以添加 .iccpp 插件的启用逻辑
|
||||
// 例如,加载动态库、注册事件等
|
||||
|
||||
base.Enable(); // 设置启用状态并触发事件
|
||||
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已启用");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// </summary>
|
||||
public override void Disable()
|
||||
{
|
||||
if (!IsEnabled) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 这里可以添加 .iccpp 插件的禁用逻辑
|
||||
// 例如,卸载动态库、注销事件等
|
||||
|
||||
base.Disable(); // 设置禁用状态并触发事件
|
||||
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已禁用");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"禁用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理插件资源
|
||||
/// </summary>
|
||||
public override void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 这里可以添加 .iccpp 插件的清理逻辑
|
||||
// 例如,释放资源等
|
||||
|
||||
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已清理资源");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理 ICCPP 插件 {Name} 资源时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 增强的插件接口,提供对插件服务的访问
|
||||
/// </summary>
|
||||
public interface IEnhancedPlugin : IPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取插件服务实例
|
||||
/// </summary>
|
||||
IPluginService PluginService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件启动时调用,在Initialize之后
|
||||
/// </summary>
|
||||
void OnStartup();
|
||||
|
||||
/// <summary>
|
||||
/// 插件关闭时调用,在Cleanup之前
|
||||
/// </summary>
|
||||
void OnShutdown();
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的菜单项
|
||||
/// </summary>
|
||||
/// <returns>菜单项集合</returns>
|
||||
MenuItem[] GetMenuItems();
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的工具栏按钮
|
||||
/// </summary>
|
||||
/// <returns>工具栏按钮集合</returns>
|
||||
Button[] GetToolbarButtons();
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的状态栏信息
|
||||
/// </summary>
|
||||
/// <returns>状态栏信息</returns>
|
||||
string GetStatusBarInfo();
|
||||
|
||||
/// <summary>
|
||||
/// 插件配置变更时调用
|
||||
/// </summary>
|
||||
void OnConfigurationChanged();
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取服务接口,统一所有获取类的方法
|
||||
/// </summary>
|
||||
public interface IGetService
|
||||
{
|
||||
#region 窗口和UI获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取主窗口引用
|
||||
/// </summary>
|
||||
Window MainWindow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前画布
|
||||
/// </summary>
|
||||
InkCanvas CurrentCanvas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有画布页面
|
||||
/// </summary>
|
||||
List<Canvas> AllCanvasPages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前页面索引
|
||||
/// </summary>
|
||||
int CurrentPageIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前页面数量
|
||||
/// </summary>
|
||||
int TotalPageCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取浮动工具栏
|
||||
/// </summary>
|
||||
FrameworkElement FloatingToolBar { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取左侧面板
|
||||
/// </summary>
|
||||
FrameworkElement LeftPanel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取右侧面板
|
||||
/// </summary>
|
||||
FrameworkElement RightPanel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取顶部面板
|
||||
/// </summary>
|
||||
FrameworkElement TopPanel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取底部面板
|
||||
/// </summary>
|
||||
FrameworkElement BottomPanel { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 绘制工具状态获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前绘制模式
|
||||
/// </summary>
|
||||
int CurrentDrawingMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触宽度
|
||||
/// </summary>
|
||||
double CurrentInkWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触颜色
|
||||
/// </summary>
|
||||
Color CurrentInkColor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前高亮笔宽度
|
||||
/// </summary>
|
||||
double CurrentHighlighterWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前橡皮擦大小
|
||||
/// </summary>
|
||||
int CurrentEraserSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前橡皮擦类型
|
||||
/// </summary>
|
||||
int CurrentEraserType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前橡皮擦形状
|
||||
/// </summary>
|
||||
int CurrentEraserShape { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触透明度
|
||||
/// </summary>
|
||||
double CurrentInkAlpha { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前笔触样式
|
||||
/// </summary>
|
||||
int CurrentInkStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前背景颜色
|
||||
/// </summary>
|
||||
string CurrentBackgroundColor { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 应用状态获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前主题模式
|
||||
/// </summary>
|
||||
bool IsDarkTheme { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为白板模式
|
||||
/// </summary>
|
||||
bool IsWhiteboardMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为PPT模式
|
||||
/// </summary>
|
||||
bool IsPPTMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为全屏模式
|
||||
/// </summary>
|
||||
bool IsFullScreenMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为画板模式
|
||||
/// </summary>
|
||||
bool IsCanvasMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为选择模式
|
||||
/// </summary>
|
||||
bool IsSelectionMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为擦除模式
|
||||
/// </summary>
|
||||
bool IsEraserMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为形状绘制模式
|
||||
/// </summary>
|
||||
bool IsShapeDrawingMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前是否为高亮模式
|
||||
/// </summary>
|
||||
bool IsHighlighterMode { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 撤销重做状态获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否可以撤销
|
||||
/// </summary>
|
||||
bool CanUndo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否可以重做
|
||||
/// </summary>
|
||||
bool CanRedo { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 系统设置获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统设置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置类型</typeparam>
|
||||
/// <param name="key">设置键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>设置值</returns>
|
||||
T GetSetting<T>(string key, T defaultValue = default(T));
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件信息获取
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有已加载的插件
|
||||
/// </summary>
|
||||
/// <returns>插件列表</returns>
|
||||
List<IPlugin> GetAllPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定插件
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
/// <returns>插件实例</returns>
|
||||
IPlugin GetPlugin(string pluginName);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 定义插件的基本接口
|
||||
/// </summary>
|
||||
public interface IPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件名称
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件描述
|
||||
/// </summary>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件作者
|
||||
/// </summary>
|
||||
string Author { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为内置插件
|
||||
/// </summary>
|
||||
bool IsBuiltIn { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化插件
|
||||
/// 此方法在插件加载时被调用,用于执行一些初始化工作
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// 此方法在插件被用户或系统启用时调用,激活插件功能
|
||||
/// </summary>
|
||||
void Enable();
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// 此方法在插件被用户或系统禁用时调用,停用插件功能
|
||||
/// </summary>
|
||||
void Disable();
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件设置界面
|
||||
/// 此方法返回插件的设置界面控件,用于展示在设置窗口
|
||||
/// </summary>
|
||||
/// <returns>插件设置界面</returns>
|
||||
UserControl GetSettingsView();
|
||||
|
||||
/// <summary>
|
||||
/// 插件卸载时的清理工作
|
||||
/// 此方法在插件被卸载前调用,用于释放资源和执行清理
|
||||
/// </summary>
|
||||
void Cleanup();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件服务接口,提供对软件内部功能的访问
|
||||
/// 继承自三个专门的服务接口:获取服务、窗口服务、操作服务
|
||||
/// </summary>
|
||||
public interface IPluginService : IGetService, IWindowService, IActionService
|
||||
{
|
||||
// 这个接口现在继承自三个专门的服务接口
|
||||
// 所有方法都在子接口中定义,这里不需要重复定义
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知类型枚举
|
||||
/// </summary>
|
||||
public enum NotificationType
|
||||
{
|
||||
/// <summary>
|
||||
/// 信息
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// 成功
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// 警告
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// 错误
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 窗口服务接口,统一所有窗口操作相关的方法
|
||||
/// </summary>
|
||||
public interface IWindowService
|
||||
{
|
||||
#region 窗口显示和隐藏
|
||||
|
||||
/// <summary>
|
||||
/// 显示设置窗口
|
||||
/// </summary>
|
||||
void ShowSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏设置窗口
|
||||
/// </summary>
|
||||
void HideSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 显示插件设置窗口
|
||||
/// </summary>
|
||||
void ShowPluginSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏插件设置窗口
|
||||
/// </summary>
|
||||
void HidePluginSettingsWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 显示帮助窗口
|
||||
/// </summary>
|
||||
void ShowHelpWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏帮助窗口
|
||||
/// </summary>
|
||||
void HideHelpWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 显示关于窗口
|
||||
/// </summary>
|
||||
void ShowAboutWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏关于窗口
|
||||
/// </summary>
|
||||
void HideAboutWindow();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 对话框和通知
|
||||
|
||||
/// <summary>
|
||||
/// 显示通知消息
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="type">消息类型</param>
|
||||
void ShowNotification(string message, NotificationType type = NotificationType.Info);
|
||||
|
||||
/// <summary>
|
||||
/// 显示确认对话框
|
||||
/// </summary>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <returns>用户选择结果</returns>
|
||||
bool ShowConfirmDialog(string message, string title = "确认");
|
||||
|
||||
/// <summary>
|
||||
/// 显示输入对话框
|
||||
/// </summary>
|
||||
/// <param name="message">提示消息</param>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>用户输入内容</returns>
|
||||
string ShowInputDialog(string message, string title = "输入", string defaultValue = "");
|
||||
|
||||
#endregion
|
||||
|
||||
#region 窗口状态控制
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口全屏状态
|
||||
/// </summary>
|
||||
/// <param name="isFullScreen">是否全屏</param>
|
||||
void SetFullScreen(bool isFullScreen);
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口置顶状态
|
||||
/// </summary>
|
||||
/// <param name="isTopMost">是否置顶</param>
|
||||
void SetTopMost(bool isTopMost);
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口可见性
|
||||
/// </summary>
|
||||
/// <param name="isVisible">是否可见</param>
|
||||
void SetWindowVisibility(bool isVisible);
|
||||
|
||||
/// <summary>
|
||||
/// 最小化窗口
|
||||
/// </summary>
|
||||
void MinimizeWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 最大化窗口
|
||||
/// </summary>
|
||||
void MaximizeWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 恢复窗口
|
||||
/// </summary>
|
||||
void RestoreWindow();
|
||||
|
||||
/// <summary>
|
||||
/// 关闭窗口
|
||||
/// </summary>
|
||||
void CloseWindow();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 窗口位置和大小
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口位置
|
||||
/// </summary>
|
||||
/// <param name="x">X坐标</param>
|
||||
/// <param name="y">Y坐标</param>
|
||||
void SetWindowPosition(double x, double y);
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口大小
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
/// <param name="height">高度</param>
|
||||
void SetWindowSize(double width, double height);
|
||||
|
||||
/// <summary>
|
||||
/// 获取窗口位置
|
||||
/// </summary>
|
||||
/// <returns>窗口位置</returns>
|
||||
(double x, double y) GetWindowPosition();
|
||||
|
||||
/// <summary>
|
||||
/// 获取窗口大小
|
||||
/// </summary>
|
||||
/// <returns>窗口大小</returns>
|
||||
(double width, double height) GetWindowSize();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件基类,提供基本实现
|
||||
/// </summary>
|
||||
public abstract class PluginBase : IPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件状态(私有字段)
|
||||
/// </summary>
|
||||
private bool _isEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// 插件状态(公共属性)
|
||||
/// </summary>
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _isEnabled;
|
||||
protected set
|
||||
{
|
||||
if (_isEnabled != value)
|
||||
{
|
||||
_isEnabled = value;
|
||||
OnEnabledStateChanged(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件ID
|
||||
/// </summary>
|
||||
public string Id { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件路径
|
||||
/// </summary>
|
||||
public string PluginPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件名称
|
||||
/// </summary>
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件描述
|
||||
/// </summary>
|
||||
public abstract string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
public abstract Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 插件作者
|
||||
/// </summary>
|
||||
public abstract string Author { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为内置插件
|
||||
/// </summary>
|
||||
public virtual bool IsBuiltIn => false;
|
||||
|
||||
/// <summary>
|
||||
/// 状态变更事件
|
||||
/// </summary>
|
||||
public event EventHandler<bool> EnabledStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化插件
|
||||
/// </summary>
|
||||
public virtual void Initialize()
|
||||
{
|
||||
Id = GetType().FullName;
|
||||
|
||||
// 添加日志,记录插件名称
|
||||
try
|
||||
{
|
||||
string name = Name;
|
||||
LogHelper.WriteLogToFile($"初始化插件: ID={Id}, 名称={name ?? "未命名"}");
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"警告: 插件 {Id} 的名称为空", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取插件名称时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已初始化");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// </summary>
|
||||
public virtual void Enable()
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
IsEnabled = true;
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已启用");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// </summary>
|
||||
public virtual void Disable()
|
||||
{
|
||||
if (IsEnabled)
|
||||
{
|
||||
IsEnabled = false;
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已禁用");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件设置界面
|
||||
/// </summary>
|
||||
/// <returns>插件设置界面</returns>
|
||||
public virtual UserControl GetSettingsView()
|
||||
{
|
||||
// 默认返回空设置页面
|
||||
return new UserControl();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件卸载时的清理工作
|
||||
/// </summary>
|
||||
public virtual void Cleanup()
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已卸载");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存插件自身的设置
|
||||
/// 注意:此方法仅用于保存插件的特定设置,不应影响插件启用/禁用状态
|
||||
/// 插件启用状态由PluginManager统一管理
|
||||
/// </summary>
|
||||
public virtual void SavePluginSettings()
|
||||
{
|
||||
// 默认实现不做任何事情
|
||||
// 子类可以重写此方法,将自身设置保存到配置文件中
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 设置已保存", LogHelper.LogType.Event);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发状态变更事件
|
||||
/// </summary>
|
||||
/// <param name="isEnabled">是否启用</param>
|
||||
protected virtual void OnEnabledStateChanged(bool isEnabled)
|
||||
{
|
||||
EnabledStateChanged?.Invoke(this, isEnabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件配置管理器,允许插件管理自己的配置
|
||||
/// </summary>
|
||||
public class PluginConfigurationManager
|
||||
{
|
||||
private static readonly string PluginConfigDirectory = Path.Combine(App.RootPath, "PluginConfigs");
|
||||
private static readonly Dictionary<string, Dictionary<string, object>> _pluginConfigs = new Dictionary<string, Dictionary<string, object>>();
|
||||
private static readonly object _lockObject = new object();
|
||||
|
||||
static PluginConfigurationManager()
|
||||
{
|
||||
// 确保配置目录存在
|
||||
if (!Directory.Exists(PluginConfigDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(PluginConfigDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件配置值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">配置值类型</typeparam>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>配置值</returns>
|
||||
public static T GetConfiguration<T>(string pluginName, string key, T defaultValue = default(T))
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig))
|
||||
{
|
||||
if (pluginConfig.TryGetValue(key, out var value))
|
||||
{
|
||||
if (value is T typedValue)
|
||||
{
|
||||
return typedValue;
|
||||
}
|
||||
|
||||
// 尝试类型转换
|
||||
try
|
||||
{
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"获取插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置插件配置值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">配置值类型</typeparam>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <param name="value">配置值</param>
|
||||
public static void SetConfiguration<T>(string pluginName, string key, T value)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_pluginConfigs.ContainsKey(pluginName))
|
||||
{
|
||||
_pluginConfigs[pluginName] = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
_pluginConfigs[pluginName][key] = value;
|
||||
|
||||
// 异步保存配置
|
||||
Task.Run(() => SavePluginConfiguration(pluginName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"设置插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除插件配置
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
/// <param name="key">配置键</param>
|
||||
public static void RemoveConfiguration(string pluginName, string key)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig))
|
||||
{
|
||||
if (pluginConfig.Remove(key))
|
||||
{
|
||||
// 异步保存配置
|
||||
Task.Run(() => SavePluginConfiguration(pluginName));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"删除插件 {pluginName} 配置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件的所有配置
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
/// <returns>配置字典</returns>
|
||||
public static Dictionary<string, object> GetAllConfigurations(string pluginName)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_pluginConfigs.TryGetValue(pluginName, out var pluginConfig))
|
||||
{
|
||||
return new Dictionary<string, object>(pluginConfig);
|
||||
}
|
||||
return new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除插件的所有配置
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
public static void ClearAllConfigurations(string pluginName)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_pluginConfigs.Remove(pluginName))
|
||||
{
|
||||
// 删除配置文件
|
||||
string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json");
|
||||
if (File.Exists(configFile))
|
||||
{
|
||||
File.Delete(configFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清除插件 {pluginName} 所有配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载插件配置
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
public static void LoadPluginConfiguration(string pluginName)
|
||||
{
|
||||
try
|
||||
{
|
||||
string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json");
|
||||
if (File.Exists(configFile))
|
||||
{
|
||||
string json = File.ReadAllText(configFile);
|
||||
var config = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
_pluginConfigs[pluginName] = config ?? new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile($"已加载插件 {pluginName} 的配置");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载插件 {pluginName} 配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存插件配置
|
||||
/// </summary>
|
||||
/// <param name="pluginName">插件名称</param>
|
||||
private static void SavePluginConfiguration(string pluginName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Dictionary<string, object> pluginConfig;
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_pluginConfigs.TryGetValue(pluginName, out pluginConfig))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
string configFile = Path.Combine(PluginConfigDirectory, $"{pluginName}.json");
|
||||
string json = JsonConvert.SerializeObject(pluginConfig, Formatting.Indented);
|
||||
File.WriteAllText(configFile, json);
|
||||
|
||||
LogHelper.WriteLogToFile($"已保存插件 {pluginName} 的配置");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存插件 {pluginName} 配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载所有插件的配置
|
||||
/// </summary>
|
||||
public static void LoadAllPluginConfigurations()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(PluginConfigDirectory))
|
||||
{
|
||||
string[] configFiles = Directory.GetFiles(PluginConfigDirectory, "*.json");
|
||||
foreach (string configFile in configFiles)
|
||||
{
|
||||
string pluginName = Path.GetFileNameWithoutExtension(configFile);
|
||||
LoadPluginConfiguration(pluginName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载所有插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存所有插件的配置
|
||||
/// </summary>
|
||||
public static void SaveAllPluginConfigurations()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
foreach (string pluginName in _pluginConfigs.Keys)
|
||||
{
|
||||
SavePluginConfiguration(pluginName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存所有插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,509 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件服务管理器,实现IPluginService接口,提供对软件内部功能的访问
|
||||
/// </summary>
|
||||
public class PluginServiceManager : IPluginService
|
||||
{
|
||||
private static PluginServiceManager _instance;
|
||||
private MainWindow _mainWindow;
|
||||
private Dictionary<string, EventHandler> _eventHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 单例实例
|
||||
/// </summary>
|
||||
public static PluginServiceManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = new PluginServiceManager();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private PluginServiceManager()
|
||||
{
|
||||
_eventHandlers = new Dictionary<string, EventHandler>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置主窗口引用
|
||||
/// </summary>
|
||||
/// <param name="mainWindow">主窗口实例</param>
|
||||
public void SetMainWindow(MainWindow mainWindow)
|
||||
{
|
||||
_mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
#region 窗口和UI访问
|
||||
|
||||
public Window MainWindow => _mainWindow;
|
||||
|
||||
public InkCanvas CurrentCanvas => null; // 暂时返回null,避免访问权限问题
|
||||
|
||||
public List<Canvas> AllCanvasPages => new List<Canvas>(); // 暂时返回空列表
|
||||
|
||||
public int CurrentPageIndex => 0; // 暂时返回0
|
||||
|
||||
public int TotalPageCount => 0; // 暂时返回0
|
||||
|
||||
public FrameworkElement FloatingToolBar => _mainWindow?.ViewboxFloatingBar;
|
||||
|
||||
public FrameworkElement LeftPanel => _mainWindow?.BlackboardLeftSide;
|
||||
|
||||
public FrameworkElement RightPanel => _mainWindow?.BlackboardRightSide;
|
||||
|
||||
public FrameworkElement TopPanel => _mainWindow?.BorderTools;
|
||||
|
||||
public FrameworkElement BottomPanel => _mainWindow?.BorderSettings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 绘制工具状态
|
||||
|
||||
public int CurrentDrawingMode => 0; // 暂时返回0
|
||||
|
||||
public double CurrentInkWidth => 2.5; // 暂时返回默认值
|
||||
|
||||
public Color CurrentInkColor => Colors.Black; // 暂时返回默认值
|
||||
|
||||
public double CurrentHighlighterWidth => 20.0; // 暂时返回默认值
|
||||
|
||||
public int CurrentEraserSize => 2; // 暂时返回默认值
|
||||
|
||||
public int CurrentEraserType => 0; // 暂时返回默认值
|
||||
|
||||
public int CurrentEraserShape => 0; // 暂时返回默认值
|
||||
|
||||
public double CurrentInkAlpha => 255.0; // 暂时返回默认值
|
||||
|
||||
public int CurrentInkStyle => 0; // 暂时返回默认值
|
||||
|
||||
public string CurrentBackgroundColor => "#162924"; // 暂时返回默认值
|
||||
|
||||
#endregion
|
||||
|
||||
#region 应用状态
|
||||
|
||||
public bool IsDarkTheme => false; // 暂时返回默认值
|
||||
|
||||
public bool IsWhiteboardMode => false; // 暂时返回默认值
|
||||
|
||||
public bool IsPPTMode => false; // 暂时返回默认值
|
||||
|
||||
public bool IsFullScreenMode => false; // 暂时返回默认值
|
||||
|
||||
public bool IsCanvasMode => true; // 暂时返回默认值
|
||||
|
||||
public bool IsSelectionMode => false; // 暂时返回默认值
|
||||
|
||||
public bool IsEraserMode => false; // 暂时返回默认值
|
||||
|
||||
public bool IsShapeDrawingMode => false; // 暂时返回默认值
|
||||
|
||||
public bool IsHighlighterMode => false; // 暂时返回默认值
|
||||
|
||||
#endregion
|
||||
|
||||
#region IGetService 实现
|
||||
|
||||
public bool CanUndo => false; // 暂时返回默认值
|
||||
|
||||
public bool CanRedo => false; // 暂时返回默认值
|
||||
|
||||
public T GetSetting<T>(string key, T defaultValue = default(T))
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public List<IPlugin> GetAllPlugins()
|
||||
{
|
||||
return new List<IPlugin>(PluginManager.Instance.Plugins);
|
||||
}
|
||||
|
||||
public IPlugin GetPlugin(string pluginName)
|
||||
{
|
||||
return PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IWindowService 实现
|
||||
|
||||
public void ShowSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HideSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowPluginSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HidePluginSettingsWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowHelpWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HideHelpWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowAboutWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void HideAboutWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ShowNotification(string message, NotificationType type = NotificationType.Info)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public bool ShowConfirmDialog(string message, string title = "确认")
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return false;
|
||||
}
|
||||
|
||||
public string ShowInputDialog(string message, string title = "输入", string defaultValue = "")
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void SetFullScreen(bool isFullScreen)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetTopMost(bool isTopMost)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetWindowVisibility(bool isVisible)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void MinimizeWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void MaximizeWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void RestoreWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void CloseWindow()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetWindowPosition(double x, double y)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetWindowSize(double width, double height)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public (double x, double y) GetWindowPosition()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
public (double width, double height) GetWindowSize()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
return (800, 600);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IActionService 实现
|
||||
|
||||
public void ClearCanvas()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ClearAllCanvases()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void AddNewPage()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void DeleteCurrentPage()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SwitchToPage(int pageIndex)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void NextPage()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void PreviousPage()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetDrawingMode(int mode)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetInkWidth(double width)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetInkColor(Color color)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetHighlighterWidth(double width)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetEraserSize(int size)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetEraserType(int type)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetEraserShape(int shape)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetInkAlpha(double alpha)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetInkStyle(int style)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetBackgroundColor(string color)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SaveCanvas(string filePath)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void LoadCanvas(string filePath)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ExportAsImage(string filePath, string format)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ExportAsPDF(string filePath)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void Redo()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SelectAll()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void DeselectAll()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void DeleteSelected()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void CopySelected()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void CutSelected()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void Paste()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SetSetting<T>(string key, T value)
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void LoadSettings()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ResetSettings()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void EnablePlugin(string pluginName)
|
||||
{
|
||||
var plugin = GetPlugin(pluginName);
|
||||
if (plugin != null)
|
||||
{
|
||||
PluginManager.Instance.TogglePlugin(plugin, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void DisablePlugin(string pluginName)
|
||||
{
|
||||
var plugin = GetPlugin(pluginName);
|
||||
if (plugin != null)
|
||||
{
|
||||
PluginManager.Instance.TogglePlugin(plugin, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnloadPlugin(string pluginName)
|
||||
{
|
||||
var plugin = GetPlugin(pluginName);
|
||||
if (plugin != null)
|
||||
{
|
||||
PluginManager.Instance.UnloadPlugin(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterEventHandler(string eventName, EventHandler handler)
|
||||
{
|
||||
if (!_eventHandlers.ContainsKey(eventName))
|
||||
{
|
||||
_eventHandlers[eventName] = handler;
|
||||
}
|
||||
else
|
||||
{
|
||||
_eventHandlers[eventName] += handler;
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterEventHandler(string eventName, EventHandler handler)
|
||||
{
|
||||
if (_eventHandlers.ContainsKey(eventName))
|
||||
{
|
||||
_eventHandlers[eventName] -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerEvent(string eventName, object sender, EventArgs args)
|
||||
{
|
||||
if (_eventHandlers.ContainsKey(eventName))
|
||||
{
|
||||
_eventHandlers[eventName]?.Invoke(sender, args);
|
||||
}
|
||||
}
|
||||
|
||||
public void RestartApplication()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void ExitApplication()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void CheckForUpdates()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void OpenHelpDocument()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
public void OpenAboutPage()
|
||||
{
|
||||
// 暂时不实现,避免访问权限问题
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Ink_Canvas.Helpers.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件模板,用于开发者参考
|
||||
/// 注意:实际开发时,请将此类移到单独的程序集中
|
||||
/// </summary>
|
||||
public class PluginTemplate : PluginBase
|
||||
{
|
||||
#region 插件基本信息
|
||||
|
||||
/// <summary>
|
||||
/// 插件名称
|
||||
/// </summary>
|
||||
public override string Name => "插件模板";
|
||||
|
||||
/// <summary>
|
||||
/// 插件描述
|
||||
/// </summary>
|
||||
public override string Description => "这是一个插件开发模板,用于开发者参考。";
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
public override Version Version => new Version(1, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 插件作者
|
||||
/// </summary>
|
||||
public override string Author => "Your Name";
|
||||
|
||||
/// <summary>
|
||||
/// 是否为内置插件(外部插件请返回false)
|
||||
/// </summary>
|
||||
public override bool IsBuiltIn => false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件生命周期
|
||||
|
||||
/// <summary>
|
||||
/// 插件初始化
|
||||
/// 在这里进行插件的初始化工作,如加载配置、注册事件等
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
// 先调用基类方法,这样会设置插件ID和记录日志
|
||||
base.Initialize();
|
||||
|
||||
// TODO: 在这里进行插件初始化工作
|
||||
|
||||
// 示例:记录初始化信息
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 开始初始化");
|
||||
|
||||
// 示例:加载配置
|
||||
LoadConfig();
|
||||
|
||||
// 示例:注册自定义事件
|
||||
// MainWindow.Instance.SomeEvent += OnSomeEvent;
|
||||
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 初始化完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用插件
|
||||
/// 在这里激活插件功能
|
||||
/// </summary>
|
||||
public override void Enable()
|
||||
{
|
||||
// 先调用基类方法,这样会设置插件状态和记录日志
|
||||
base.Enable();
|
||||
|
||||
// TODO: 在这里启用插件功能
|
||||
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已启用");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用插件
|
||||
/// 在这里停用插件功能
|
||||
/// </summary>
|
||||
public override void Disable()
|
||||
{
|
||||
// 先调用基类方法,这样会设置插件状态和记录日志
|
||||
base.Disable();
|
||||
|
||||
// TODO: 在这里禁用插件功能
|
||||
|
||||
LogHelper.WriteLogToFile($"插件 {Name} 已禁用");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理资源
|
||||
/// 在插件卸载时调用,清理资源
|
||||
/// </summary>
|
||||
public override void Cleanup()
|
||||
{
|
||||
// TODO: 在这里清理插件资源
|
||||
|
||||
// 示例:取消注册事件
|
||||
// MainWindow.Instance.SomeEvent -= OnSomeEvent;
|
||||
|
||||
// 示例:保存配置
|
||||
SaveConfig();
|
||||
|
||||
// 最后调用基类方法
|
||||
base.Cleanup();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件配置
|
||||
|
||||
/// <summary>
|
||||
/// 加载插件配置
|
||||
/// </summary>
|
||||
private void LoadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: 从文件或其他位置加载配置
|
||||
// 示例:
|
||||
// string configPath = Path.Combine(App.RootPath, "PluginConfigs", "YourPluginName.json");
|
||||
// if (File.Exists(configPath))
|
||||
// {
|
||||
// string json = File.ReadAllText(configPath);
|
||||
// YourConfig = Newtonsoft.Json.JsonConvert.DeserializeObject<YourConfigClass>(json);
|
||||
// }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存插件配置
|
||||
/// </summary>
|
||||
private void SaveConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: 保存配置到文件或其他位置
|
||||
// 示例:
|
||||
// string configDir = Path.Combine(App.RootPath, "PluginConfigs");
|
||||
// if (!Directory.Exists(configDir))
|
||||
// {
|
||||
// Directory.CreateDirectory(configDir);
|
||||
// }
|
||||
// string configPath = Path.Combine(configDir, "YourPluginName.json");
|
||||
// string json = Newtonsoft.Json.JsonConvert.SerializeObject(YourConfig, Newtonsoft.Json.Formatting.Indented);
|
||||
// File.WriteAllText(configPath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"保存插件配置时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件设置界面
|
||||
|
||||
/// <summary>
|
||||
/// 获取插件设置界面
|
||||
/// </summary>
|
||||
/// <returns>插件设置界面</returns>
|
||||
public override UserControl GetSettingsView()
|
||||
{
|
||||
// 创建插件设置界面
|
||||
return new PluginTemplateSettingsControl();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 插件功能方法
|
||||
|
||||
// TODO: 在这里添加插件的具体功能方法
|
||||
|
||||
/// <summary>
|
||||
/// 示例方法:执行一些功能
|
||||
/// </summary>
|
||||
public void DoSomething()
|
||||
{
|
||||
if (!IsEnabled) return;
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: 实现你的功能
|
||||
MessageBox.Show("插件功能执行示例", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"执行插件功能时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插件设置控件
|
||||
/// </summary>
|
||||
public class PluginTemplateSettingsControl : UserControl
|
||||
{
|
||||
public PluginTemplateSettingsControl()
|
||||
{
|
||||
// 创建设置界面布局
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Margin = new Thickness(10)
|
||||
};
|
||||
|
||||
// 添加标题
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "插件模板设置",
|
||||
FontSize = 16,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Margin = new Thickness(0, 0, 0, 10)
|
||||
});
|
||||
|
||||
// 添加说明文字
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "这是一个示例设置界面,你可以在这里添加自己的设置控件。",
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 0, 0, 15)
|
||||
});
|
||||
|
||||
// 添加示例设置选项
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
Content = "启用某项功能",
|
||||
Margin = new Thickness(0, 0, 0, 10)
|
||||
};
|
||||
panel.Children.Add(checkBox);
|
||||
|
||||
// 添加文本输入框
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "设置项:",
|
||||
Margin = new Thickness(0, 5, 0, 5)
|
||||
});
|
||||
|
||||
panel.Children.Add(new TextBox
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
Width = 200,
|
||||
HorizontalAlignment = HorizontalAlignment.Left
|
||||
});
|
||||
|
||||
// 添加按钮
|
||||
var button = new Button
|
||||
{
|
||||
Content = "保存设置",
|
||||
Padding = new Thickness(10, 5, 10, 5),
|
||||
Margin = new Thickness(0, 10, 0, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Left
|
||||
};
|
||||
|
||||
button.Click += (sender, e) =>
|
||||
{
|
||||
MessageBox.Show("设置已保存!", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
};
|
||||
|
||||
panel.Children.Add(button);
|
||||
|
||||
// 设置控件内容
|
||||
Content = panel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,17 +366,21 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
/// <param name="root">要开始扫描的根目录路径。</param>
|
||||
/// <remarks>
|
||||
/// 仅处理扩展名为 `.exe`, `.dll`, `.config`, `.manifest`, `.dat`, `.enc` 的文件;会跳过被 IsExcludedPath 判定为排除的路径。遇到任何 I/O 或访问错误时会静默忽略,不会抛出异常。</remarks>
|
||||
/// 仅处理扩展名为 `.exe`, `.dll`, `.config`, `.manifest`, `.dat`, `.enc` 的文件,以及应用根目录下的点名名单 `Names.txt`;
|
||||
/// 会跳过被 IsExcludedPath 判定为排除的路径。遇到任何 I/O 或访问错误时会静默忽略,不会抛出异常。</remarks>
|
||||
private static void LockFilesRecursive(string root)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rollCallNamesPath = NormalizePath(Path.Combine(root, "Names.txt"));
|
||||
foreach (var file in Directory.GetFiles(root, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (!IsExcludedPath(file))
|
||||
{
|
||||
var ext = Path.GetExtension(file);
|
||||
if (string.Equals(ext, ".exe", StringComparison.OrdinalIgnoreCase) ||
|
||||
var normFile = NormalizePath(file);
|
||||
if (string.Equals(normFile, rollCallNamesPath, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".exe", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".config", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ext, ".manifest", StringComparison.OrdinalIgnoreCase) ||
|
||||
|
||||
+223
-747
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 渲染保存文件名模板。支持占位符: {date} {time} {datetime} {mode} {page} {count} {type}。
|
||||
/// 当模板为空、渲染结果非法或仅含分隔符时,回退到默认时间戳命名。
|
||||
/// </summary>
|
||||
public static class SaveFileNameHelper
|
||||
{
|
||||
private const string DefaultDateTime = "yyyy-MM-dd HH-mm-ss-fff";
|
||||
|
||||
public static string Render(string template, SaveFileNameContext ctx)
|
||||
{
|
||||
if (ctx == null) ctx = new SaveFileNameContext();
|
||||
var now = ctx.Time ?? DateTime.Now;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(template))
|
||||
return now.ToString(DefaultDateTime);
|
||||
|
||||
try
|
||||
{
|
||||
string result = template
|
||||
.Replace("{date}", now.ToString("yyyy-MM-dd"))
|
||||
.Replace("{time}", now.ToString("HH-mm-ss"))
|
||||
.Replace("{datetime}", now.ToString(DefaultDateTime))
|
||||
.Replace("{mode}", ctx.Mode ?? "")
|
||||
.Replace("{page}", ctx.Page?.ToString() ?? "")
|
||||
.Replace("{count}", ctx.Count?.ToString() ?? "")
|
||||
.Replace("{type}", ctx.Type ?? "");
|
||||
|
||||
result = SanitizeFileName(result);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result) || Regex.IsMatch(result, @"^[\s\-_]+$"))
|
||||
return now.ToString(DefaultDateTime);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return now.ToString(DefaultDateTime);
|
||||
}
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return name;
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
name = name.Replace(c, '_');
|
||||
}
|
||||
return name.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
public class SaveFileNameContext
|
||||
{
|
||||
public DateTime? Time { get; set; }
|
||||
/// <summary>"Annotation" or "BlackBoard" or "Screenshot" etc.</summary>
|
||||
public string Mode { get; set; }
|
||||
/// <summary>"User" or "Auto"</summary>
|
||||
public string Type { get; set; }
|
||||
public int? Page { get; set; }
|
||||
public int? Count { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,16 @@ namespace Ink_Canvas.Helpers
|
||||
public static bool IsPasswordRequiredForResetConfig(Settings settings)
|
||||
=> IsPasswordFeatureEnabled(settings) && HasPasswordConfigured(settings) && settings.Security.RequirePasswordOnResetConfig;
|
||||
|
||||
/// <summary>
|
||||
/// 指示在修改或清空点名名单前是否需要输入安全密码。
|
||||
/// </summary>
|
||||
/// <param name="settings">应用设置对象。</param>
|
||||
/// <returns>当已启用密码功能、已配置密码且开启了对应开关时返回 true;否则返回 false。</returns>
|
||||
public static bool IsPasswordRequiredForModifyOrClearNameList(Settings settings)
|
||||
=> IsPasswordFeatureEnabled(settings)
|
||||
&& HasPasswordConfigured(settings)
|
||||
&& settings.Security.RequirePasswordOnModifyOrClearNameList;
|
||||
|
||||
/// <summary>
|
||||
/// 将提供的明文密码与 Settings 中存储的密码散列进行比对以验证密码是否正确。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,72 +1,148 @@
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
internal class SoftwareLauncher
|
||||
internal static class SoftwareLauncher
|
||||
{
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
/// <summary>与 ICA 一致:在「程序和功能」卸载列表中按 DisplayName 匹配后启动 sweclauncher.exe。</summary>
|
||||
public static void LaunchEasiCamera(string softwareName)
|
||||
{
|
||||
string executablePath = FindEasiCameraExecutablePath(softwareName);
|
||||
|
||||
if (!string.IsNullOrEmpty(executablePath))
|
||||
if (string.IsNullOrEmpty(executablePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(executablePath);
|
||||
//Console.WriteLine(softwareName + " 启动成功!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("启动失败: " + ex.Message);
|
||||
//MessageBox.Show("启动失败: " + ex.Message);
|
||||
}
|
||||
MessageBox.Show(
|
||||
"未找到希沃视频展台安装信息(已扫描 64 位与 32 位卸载注册表)。请确认已通过官方安装包安装「希沃视频展台」。",
|
||||
"Ink Canvas",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(executablePath);
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = executablePath,
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = string.IsNullOrEmpty(directory) ? Environment.SystemDirectory : directory
|
||||
};
|
||||
Process.Start(psi);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"无法启动希沃视频展台:" + ex.Message,
|
||||
"Ink Canvas",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning);
|
||||
}
|
||||
//Console.WriteLine(softwareName + " 未找到可执行文件路径。");
|
||||
}
|
||||
|
||||
private static string FindEasiCameraExecutablePath(string softwareName)
|
||||
{
|
||||
string executablePath = null;
|
||||
if (string.IsNullOrWhiteSpace(softwareName))
|
||||
return null;
|
||||
|
||||
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall"))
|
||||
// 须用 OpenBaseKey + RegistryView 显式指定视图:Registry.LocalMachine.OpenSubKey 跟随进程位数,
|
||||
// 32 位进程下无法靠拼接 WOW6432Node 路径进入 64 位视图,会找不到 64 位安装的展台。
|
||||
const string uninstallSubKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
|
||||
foreach (RegistryView view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
|
||||
{
|
||||
foreach (string subkeyName in key.GetSubKeyNames())
|
||||
using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
|
||||
using (RegistryKey key = baseKey.OpenSubKey(uninstallSubKey))
|
||||
{
|
||||
using (RegistryKey subkey = key.OpenSubKey(subkeyName))
|
||||
{
|
||||
string displayName = subkey.GetValue("DisplayName") as string;
|
||||
string installLocation = subkey.GetValue("InstallLocation") as string;
|
||||
string uninstallString = subkey.GetValue("UninstallString") as string;
|
||||
|
||||
if (!string.IsNullOrEmpty(displayName) && displayName.Contains(softwareName))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(installLocation))
|
||||
{
|
||||
executablePath = Path.Combine(installLocation, "sweclauncher.exe");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(uninstallString))
|
||||
{
|
||||
int lastSlashIndex = uninstallString.LastIndexOf("\\");
|
||||
if (lastSlashIndex >= 0)
|
||||
{
|
||||
string folderPath = uninstallString.Substring(0, lastSlashIndex);
|
||||
executablePath = Path.Combine(folderPath, "sweclauncher", "sweclauncher.exe");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (key == null) continue;
|
||||
string found = FindInUninstallKey(key, softwareName);
|
||||
if (!string.IsNullOrEmpty(found))
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return executablePath;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string FindInUninstallKey(RegistryKey uninstallKey, string softwareName)
|
||||
{
|
||||
foreach (string subkeyName in uninstallKey.GetSubKeyNames())
|
||||
{
|
||||
using (RegistryKey subkey = uninstallKey.OpenSubKey(subkeyName))
|
||||
{
|
||||
if (subkey == null) continue;
|
||||
|
||||
string displayName = subkey.GetValue("DisplayName") as string;
|
||||
if (string.IsNullOrEmpty(displayName) || !displayName.Contains(softwareName))
|
||||
continue;
|
||||
|
||||
string installLocation = subkey.GetValue("InstallLocation") as string;
|
||||
string uninstallString = subkey.GetValue("UninstallString") as string;
|
||||
|
||||
string resolved = TryResolveSweclauncher(installLocation, uninstallString);
|
||||
if (!string.IsNullOrEmpty(resolved) && File.Exists(resolved))
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string TryResolveSweclauncher(string installLocation, string uninstallString)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(installLocation))
|
||||
{
|
||||
string fromLoc = ResolveSweclauncherUnderInstallRoot(installLocation.Trim().TrimEnd('\\'));
|
||||
if (!string.IsNullOrEmpty(fromLoc))
|
||||
return fromLoc;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(uninstallString))
|
||||
{
|
||||
// 常见:"...\uninstall.exe" 或带引号路径
|
||||
string trimmed = uninstallString.Trim();
|
||||
if (trimmed.Length >= 2 && trimmed[0] == '"')
|
||||
{
|
||||
int end = trimmed.IndexOf('"', 1);
|
||||
if (end > 1)
|
||||
trimmed = trimmed.Substring(1, end - 1);
|
||||
}
|
||||
|
||||
int lastSlash = trimmed.LastIndexOf('\\');
|
||||
if (lastSlash < 0)
|
||||
return null;
|
||||
|
||||
string folderPath = trimmed.Substring(0, lastSlash);
|
||||
string candidate = Path.Combine(folderPath, "sweclauncher", "sweclauncher.exe");
|
||||
if (File.Exists(candidate))
|
||||
return candidate;
|
||||
|
||||
candidate = Path.Combine(folderPath, "sweclauncher.exe");
|
||||
if (File.Exists(candidate))
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ResolveSweclauncherUnderInstallRoot(string installRoot)
|
||||
{
|
||||
string[] candidates =
|
||||
{
|
||||
Path.Combine(installRoot, "sweclauncher.exe"),
|
||||
Path.Combine(installRoot, "sweclauncher", "sweclauncher.exe"),
|
||||
};
|
||||
|
||||
foreach (string p in candidates)
|
||||
{
|
||||
if (File.Exists(p))
|
||||
return p;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
@@ -100,8 +100,8 @@ namespace Ink_Canvas.Helpers
|
||||
var item = _currentStrokeHistory[_currentIndex];
|
||||
item.StrokeHasBeenCleared = !item.StrokeHasBeenCleared;
|
||||
_currentIndex--;
|
||||
OnUndoStateChanged?.Invoke(_currentIndex > -1);
|
||||
OnRedoStateChanged?.Invoke(_currentStrokeHistory.Count - _currentIndex - 1 > 0);
|
||||
OnUndoStateChanged?.Invoke(CanUndo);
|
||||
OnRedoStateChanged?.Invoke(CanRedo);
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -137,9 +137,15 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
private void NotifyUndoRedoState()
|
||||
{
|
||||
OnUndoStateChanged?.Invoke(_currentIndex > -1);
|
||||
OnRedoStateChanged?.Invoke(_currentStrokeHistory.Count - _currentIndex - 1 > 0);
|
||||
OnUndoStateChanged?.Invoke(CanUndo);
|
||||
OnRedoStateChanged?.Invoke(CanRedo);
|
||||
}
|
||||
|
||||
/// <summary>当前历史是否允许撤销。</summary>
|
||||
public bool CanUndo => _currentIndex > -1;
|
||||
|
||||
/// <summary>当前历史是否允许重做。</summary>
|
||||
public bool CanRedo => _currentStrokeHistory.Count > 0 && _currentStrokeHistory.Count - _currentIndex - 1 > 0;
|
||||
}
|
||||
|
||||
public class TimeMachineHistory
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// UIAccess DLL释放器
|
||||
/// </summary>
|
||||
public static class UIAccessDllExtractor
|
||||
{
|
||||
private static readonly string[] RequiredDlls = {
|
||||
"UIAccessDLL_x64.dll",
|
||||
"UIAccessDLL_x86.dll"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 在应用启动时释放UIAccess相关DLL
|
||||
/// </summary>
|
||||
public static void ExtractUIAccessDlls()
|
||||
{
|
||||
try
|
||||
{
|
||||
string appDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
LogHelper.WriteLogToFile("开始检查并释放UIAccess相关DLL文件");
|
||||
|
||||
foreach (string dllName in RequiredDlls)
|
||||
{
|
||||
string targetPath = Path.Combine(appDirectory, dllName);
|
||||
|
||||
// 检查文件是否已存在且有效
|
||||
if (File.Exists(targetPath) && IsValidDll(targetPath))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"{dllName} 已存在且有效,跳过释放");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 从嵌入资源中释放DLL
|
||||
if (ExtractDllFromResource(dllName, targetPath))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"成功释放 {dllName} 到 {targetPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.WriteLogToFile($"警告:无法释放 {dllName},可能影响UIA置顶功能", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.WriteLogToFile("UIAccess DLL释放检查完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"释放UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从嵌入资源中提取DLL文件
|
||||
/// </summary>
|
||||
private static bool ExtractDllFromResource(string dllName, string targetPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
string resourceName = $"Ink_Canvas.{dllName}";
|
||||
|
||||
using (Stream resourceStream = assembly.GetManifestResourceStream(resourceName))
|
||||
{
|
||||
if (resourceStream == null)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"未找到嵌入资源: {resourceName}", LogHelper.LogType.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
string targetDirectory = Path.GetDirectoryName(targetPath);
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
using (FileStream fileStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
resourceStream.CopyTo(fileStream);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从资源提取 {dllName} 失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查DLL文件是否有效
|
||||
/// </summary>
|
||||
private static bool IsValidDll(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
return false;
|
||||
|
||||
FileInfo fileInfo = new FileInfo(filePath);
|
||||
|
||||
// 检查文件大小(空文件或过小的文件可能无效)
|
||||
if (fileInfo.Length < 1024) // 小于1KB可能无效
|
||||
return false;
|
||||
|
||||
// 简单检查PE头(DLL文件应该以MZ开头)
|
||||
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
byte[] buffer = new byte[2];
|
||||
if (fs.Read(buffer, 0, 2) == 2)
|
||||
{
|
||||
return buffer[0] == 0x4D && buffer[1] == 0x5A; // "MZ"
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理释放的DLL文件(可选,在应用退出时调用)
|
||||
/// </summary>
|
||||
public static void CleanupExtractedDlls()
|
||||
{
|
||||
try
|
||||
{
|
||||
string appDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
foreach (string dllName in RequiredDlls)
|
||||
{
|
||||
string filePath = Path.Combine(appDirectory, dllName);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
LogHelper.WriteLogToFile($"已清理 {dllName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理 {dllName} 失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"清理UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 通过 Winlogon 令牌模拟实现 UIAccess 提权重启。
|
||||
/// 1. 找到当前会话中 winlogon.exe 的令牌,复制为模拟令牌;
|
||||
/// 2. SetThreadToken 暂时模拟 winlogon(拥有 TCB 权限);
|
||||
/// 3. 在自身令牌副本上 SetTokenInformation(TokenUIAccess, TRUE);
|
||||
/// 4. RevertToSelf 后用 CreateProcessWithTokenW 启动新进程;
|
||||
/// 5. 新进程具有 UIAccess 权限,可置顶于 UAC 提示之上。
|
||||
/// </summary>
|
||||
public static class UIAccessHelper
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const uint TOKEN_QUERY = 0x0008;
|
||||
private const uint TOKEN_DUPLICATE = 0x0002;
|
||||
private const uint TOKEN_IMPERSONATE = 0x0004;
|
||||
private const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
|
||||
private const uint TOKEN_ADJUST_DEFAULT = 0x0080;
|
||||
private const uint TOKEN_ADJUST_SESSIONID = 0x0100;
|
||||
private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
|
||||
|
||||
private const int SecurityAnonymous = 0;
|
||||
private const int SecurityImpersonation = 2;
|
||||
private const int TokenPrimary = 1;
|
||||
private const int TokenImpersonation = 2;
|
||||
|
||||
// TOKEN_INFORMATION_CLASS
|
||||
private const int TokenSessionId = 12;
|
||||
private const int TokenElevationType = 18;
|
||||
private const int TokenUIAccess = 26;
|
||||
|
||||
// TOKEN_ELEVATION_TYPE
|
||||
private const int TokenElevationTypeDefault = 1;
|
||||
private const int TokenElevationTypeFull = 2;
|
||||
private const int TokenElevationTypeLimited = 3;
|
||||
|
||||
private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
|
||||
private const uint TH32CS_SNAPPROCESS = 0x00000002;
|
||||
|
||||
private const uint LOGON_WITH_PROFILE = 0x00000001;
|
||||
private const uint CREATE_NEW_CONSOLE = 0x00000010;
|
||||
private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
|
||||
|
||||
private const uint SE_PRIVILEGE_ENABLED = 0x00000002;
|
||||
private const string SE_ASSIGNPRIMARYTOKEN_NAME = "SeAssignPrimaryTokenPrivilege";
|
||||
|
||||
private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Structs
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LUID
|
||||
{
|
||||
public uint LowPart;
|
||||
public int HighPart;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LUID_AND_ATTRIBUTES
|
||||
{
|
||||
public LUID Luid;
|
||||
public uint Attributes;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct TOKEN_PRIVILEGES
|
||||
{
|
||||
public uint PrivilegeCount;
|
||||
public LUID_AND_ATTRIBUTES Privilege;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct PROCESSENTRY32W
|
||||
{
|
||||
public uint dwSize;
|
||||
public uint cntUsage;
|
||||
public uint th32ProcessID;
|
||||
public IntPtr th32DefaultHeapID;
|
||||
public uint th32ModuleID;
|
||||
public uint cntThreads;
|
||||
public uint th32ParentProcessID;
|
||||
public int pcPriClassBase;
|
||||
public uint dwFlags;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
||||
public string szExeFile;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct STARTUPINFOW
|
||||
{
|
||||
public uint cb;
|
||||
public IntPtr lpReserved;
|
||||
public IntPtr lpDesktop;
|
||||
public IntPtr lpTitle;
|
||||
public uint dwX, dwY, dwXSize, dwYSize;
|
||||
public uint dwXCountChars, dwYCountChars;
|
||||
public uint dwFillAttribute;
|
||||
public uint dwFlags;
|
||||
public ushort wShowWindow;
|
||||
public ushort cbReserved2;
|
||||
public IntPtr lpReserved2;
|
||||
public IntPtr hStdInput, hStdOutput, hStdError;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct PROCESS_INFORMATION
|
||||
{
|
||||
public IntPtr hProcess;
|
||||
public IntPtr hThread;
|
||||
public uint dwProcessId;
|
||||
public uint dwThreadId;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region P/Invoke
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern IntPtr GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool DuplicateTokenEx(
|
||||
IntPtr hExistingToken,
|
||||
uint dwDesiredAccess,
|
||||
IntPtr lpTokenAttributes,
|
||||
int ImpersonationLevel,
|
||||
int TokenType,
|
||||
out IntPtr phNewToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetTokenInformation(
|
||||
IntPtr TokenHandle,
|
||||
int TokenInformationClass,
|
||||
IntPtr TokenInformation,
|
||||
uint TokenInformationLength,
|
||||
out uint ReturnLength);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetTokenInformation(
|
||||
IntPtr TokenHandle,
|
||||
int TokenInformationClass,
|
||||
IntPtr TokenInformation,
|
||||
uint TokenInformationLength);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetThreadToken(IntPtr Thread, IntPtr Token);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool RevertToSelf();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool Process32FirstW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool Process32NextW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool LookupPrivilegeValueW(string lpSystemName, string lpName, out LUID lpLuid);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AdjustTokenPrivileges(
|
||||
IntPtr TokenHandle,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges,
|
||||
ref TOKEN_PRIVILEGES NewState,
|
||||
uint BufferLength,
|
||||
IntPtr PreviousState,
|
||||
IntPtr ReturnLength);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CreateProcessWithTokenW(
|
||||
IntPtr hToken,
|
||||
uint dwLogonFlags,
|
||||
string lpApplicationName,
|
||||
StringBuilder lpCommandLine,
|
||||
uint dwCreationFlags,
|
||||
IntPtr lpEnvironment,
|
||||
string lpCurrentDirectory,
|
||||
ref STARTUPINFOW lpStartupInfo,
|
||||
out PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern void GetStartupInfoW(ref STARTUPINFOW lpStartupInfo);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// 检查当前进程是否已具有 UIAccess 标志。
|
||||
/// </summary>
|
||||
public static bool HasUIAccess()
|
||||
{
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hToken))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
IntPtr buf = Marshal.AllocHGlobal(sizeof(uint));
|
||||
try
|
||||
{
|
||||
Marshal.WriteInt32(buf, 0);
|
||||
if (!GetTokenInformation(hToken, TokenUIAccess, buf, sizeof(uint), out _))
|
||||
return false;
|
||||
return Marshal.ReadInt32(buf) != 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buf);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(hToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 UIAccess 令牌重启自身。当前进程必须已经以管理员身份运行。
|
||||
/// 成功时新进程已启动,调用方应立即退出当前进程。
|
||||
/// </summary>
|
||||
/// <param name="extraArgs">追加到新进程的额外命令行参数(例如 --skip-mutex-check)。</param>
|
||||
public static bool RestartWithUIAccess(string extraArgs = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (HasUIAccess())
|
||||
{
|
||||
LogHelper.WriteLogToFile("UIAccess | 当前进程已具有 UIAccess,跳过重启");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!CreateUIAccessToken(out IntPtr uiaToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | 创建 UIAccess 令牌失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return LaunchWithToken(uiaToken, extraArgs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(uiaToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | RestartWithUIAccess 异常: {ex}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以普通用户权限(非提升)重启自身。
|
||||
/// 通过获取 explorer.exe / ctfmon.exe 的非特权令牌,再用 CreateProcessWithTokenW 启动新进程,
|
||||
/// 避免经由 explorer.exe 中转可能产生的 UAC 提示或丢失参数问题。
|
||||
/// 成功时调用方应立即退出当前进程。
|
||||
/// </summary>
|
||||
/// <param name="extraArgs">追加到新进程的额外命令行参数。</param>
|
||||
public static bool RestartAsNormalUser(string extraArgs = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!GetUserPrimaryToken(out IntPtr userToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | 获取用户令牌失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return LaunchWithToken(userToken, extraArgs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(userToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | RestartAsNormalUser 异常: {ex}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Token Manipulation
|
||||
|
||||
private static bool CreateUIAccessToken(out IntPtr uiaToken)
|
||||
{
|
||||
uiaToken = IntPtr.Zero;
|
||||
|
||||
// 1. 获取当前进程的 session id
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hSelfQuery))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | OpenProcessToken(query) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint sessionId;
|
||||
try
|
||||
{
|
||||
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
|
||||
try
|
||||
{
|
||||
if (!GetTokenInformation(hSelfQuery, TokenSessionId, sesBuf, sizeof(uint), out _))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | GetTokenInformation(SessionId) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
sessionId = (uint)Marshal.ReadInt32(sesBuf);
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(sesBuf); }
|
||||
}
|
||||
finally { CloseHandle(hSelfQuery); }
|
||||
|
||||
// 2. 找到同一会话的 winlogon 模拟令牌
|
||||
if (!GetWinlogonImpersonationToken(sessionId, out IntPtr winlogonToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile("UIAccess | 未能获取 winlogon 模拟令牌(需要管理员权限)", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 3. 模拟 winlogon
|
||||
if (!SetThreadToken(IntPtr.Zero, winlogonToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | SetThreadToken(winlogon) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 4. 复制自身令牌为主令牌
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, out IntPtr hSelfDup))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | OpenProcessToken(dup) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
IntPtr dupToken;
|
||||
try
|
||||
{
|
||||
bool ok = DuplicateTokenEx(
|
||||
hSelfDup,
|
||||
TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
|
||||
IntPtr.Zero,
|
||||
SecurityAnonymous,
|
||||
TokenPrimary,
|
||||
out dupToken);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | DuplicateTokenEx 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally { CloseHandle(hSelfDup); }
|
||||
|
||||
// 5. 在副本上设置 UIAccess = TRUE
|
||||
IntPtr uiBuf = Marshal.AllocHGlobal(sizeof(uint));
|
||||
try
|
||||
{
|
||||
Marshal.WriteInt32(uiBuf, 1);
|
||||
if (!SetTokenInformation(dupToken, TokenUIAccess, uiBuf, sizeof(uint)))
|
||||
{
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
LogHelper.WriteLogToFile($"UIAccess | SetTokenInformation(UIAccess) 失败: {err}", LogHelper.LogType.Error);
|
||||
CloseHandle(dupToken);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(uiBuf); }
|
||||
|
||||
uiaToken = dupToken;
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RevertToSelf();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(winlogonToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool GetWinlogonImpersonationToken(uint sessionId, out IntPtr winlogonToken)
|
||||
{
|
||||
winlogonToken = IntPtr.Zero;
|
||||
|
||||
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE || snapshot == IntPtr.Zero)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | CreateToolhelp32Snapshot 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var pe = new PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32W)) };
|
||||
bool more = Process32FirstW(snapshot, ref pe);
|
||||
|
||||
while (more)
|
||||
{
|
||||
if (string.Equals(pe.szExeFile, "winlogon.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (TryDuplicateWinlogonToken(pe.th32ProcessID, sessionId, out winlogonToken))
|
||||
return true;
|
||||
}
|
||||
more = Process32NextW(snapshot, ref pe);
|
||||
}
|
||||
}
|
||||
finally { CloseHandle(snapshot); }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryDuplicateWinlogonToken(uint pid, uint sessionId, out IntPtr dupToken)
|
||||
{
|
||||
dupToken = IntPtr.Zero;
|
||||
|
||||
IntPtr hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
||||
if (hProc == IntPtr.Zero) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE, out IntPtr hToken))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// 检查 session id 匹配
|
||||
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
|
||||
try
|
||||
{
|
||||
if (!GetTokenInformation(hToken, TokenSessionId, sesBuf, sizeof(uint), out _))
|
||||
return false;
|
||||
if ((uint)Marshal.ReadInt32(sesBuf) != sessionId)
|
||||
return false;
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(sesBuf); }
|
||||
|
||||
if (!DuplicateTokenEx(
|
||||
hToken,
|
||||
TOKEN_IMPERSONATE | TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | TOKEN_DUPLICATE,
|
||||
IntPtr.Zero,
|
||||
SecurityImpersonation,
|
||||
TokenImpersonation,
|
||||
out dupToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 启用 SeAssignPrimaryTokenPrivilege(Inkeys 行为)
|
||||
var tkp = new TOKEN_PRIVILEGES
|
||||
{
|
||||
PrivilegeCount = 1,
|
||||
Privilege = new LUID_AND_ATTRIBUTES { Attributes = SE_PRIVILEGE_ENABLED }
|
||||
};
|
||||
if (LookupPrivilegeValueW(null, SE_ASSIGNPRIMARYTOKEN_NAME, out tkp.Privilege.Luid))
|
||||
{
|
||||
AdjustTokenPrivileges(dupToken, false, ref tkp, (uint)Marshal.SizeOf(tkp), IntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally { CloseHandle(hToken); }
|
||||
}
|
||||
finally { CloseHandle(hProc); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 explorer.exe / ctfmon.exe 取得普通用户(非提升)令牌的主令牌副本,用于降权启动。
|
||||
/// 仅当当前进程为管理员时才能成功。
|
||||
/// </summary>
|
||||
private static bool GetUserPrimaryToken(out IntPtr userToken)
|
||||
{
|
||||
userToken = IntPtr.Zero;
|
||||
|
||||
string[] candidates = { "explorer.exe", "ctfmon.exe" };
|
||||
foreach (var name in candidates)
|
||||
{
|
||||
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE || snapshot == IntPtr.Zero) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var pe = new PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32W)) };
|
||||
bool more = Process32FirstW(snapshot, ref pe);
|
||||
while (more)
|
||||
{
|
||||
if (string.Equals(pe.szExeFile, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (TryDuplicateUserPrimaryToken(pe.th32ProcessID, out userToken))
|
||||
{
|
||||
LogHelper.WriteLogToFile($"UIAccess | 已从 {name} (PID={pe.th32ProcessID}) 取得用户令牌");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
more = Process32NextW(snapshot, ref pe);
|
||||
}
|
||||
}
|
||||
finally { CloseHandle(snapshot); }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryDuplicateUserPrimaryToken(uint pid, out IntPtr dupToken)
|
||||
{
|
||||
dupToken = IntPtr.Zero;
|
||||
|
||||
IntPtr hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
||||
if (hProc == IntPtr.Zero) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE, out IntPtr hToken))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// 仅接受非提升令牌(否则降权失败)
|
||||
IntPtr elevBuf = Marshal.AllocHGlobal(sizeof(int));
|
||||
try
|
||||
{
|
||||
if (!GetTokenInformation(hToken, TokenElevationType, elevBuf, sizeof(int), out _))
|
||||
return false;
|
||||
int elev = Marshal.ReadInt32(elevBuf);
|
||||
if (elev == TokenElevationTypeFull)
|
||||
return false; // 该进程是提升令牌,跳过
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(elevBuf); }
|
||||
|
||||
return DuplicateTokenEx(
|
||||
hToken,
|
||||
TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
|
||||
IntPtr.Zero,
|
||||
SecurityAnonymous,
|
||||
TokenPrimary,
|
||||
out dupToken);
|
||||
}
|
||||
finally { CloseHandle(hToken); }
|
||||
}
|
||||
finally { CloseHandle(hProc); }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Process Launch
|
||||
|
||||
private static bool LaunchWithToken(IntPtr token, string extraArgs)
|
||||
{
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
string workDir = System.IO.Path.GetDirectoryName(exePath);
|
||||
|
||||
// 重建命令行:保留原始参数,追加 --skip-mutex-check 防止单实例阻塞
|
||||
var cmdBuilder = new StringBuilder(32768);
|
||||
cmdBuilder.Append('"').Append(exePath).Append('"');
|
||||
|
||||
string[] args = Environment.GetCommandLineArgs();
|
||||
for (int i = 1; i < args.Length; i++)
|
||||
{
|
||||
cmdBuilder.Append(' ');
|
||||
AppendQuoted(cmdBuilder, args[i]);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(extraArgs))
|
||||
cmdBuilder.Append(' ').Append(extraArgs);
|
||||
|
||||
// 防止单实例 Mutex 阻塞新进程
|
||||
if (Array.IndexOf(args, "--skip-mutex-check") < 0
|
||||
&& (extraArgs == null || extraArgs.IndexOf("--skip-mutex-check", StringComparison.Ordinal) < 0))
|
||||
{
|
||||
cmdBuilder.Append(" --skip-mutex-check");
|
||||
}
|
||||
|
||||
var si = new STARTUPINFOW { cb = (uint)Marshal.SizeOf(typeof(STARTUPINFOW)) };
|
||||
GetStartupInfoW(ref si);
|
||||
|
||||
bool ok = CreateProcessWithTokenW(
|
||||
token,
|
||||
LOGON_WITH_PROFILE,
|
||||
null,
|
||||
cmdBuilder,
|
||||
CREATE_UNICODE_ENVIRONMENT,
|
||||
IntPtr.Zero,
|
||||
workDir,
|
||||
ref si,
|
||||
out PROCESS_INFORMATION pi);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
LogHelper.WriteLogToFile($"UIAccess | CreateProcessWithTokenW 失败: {err}", LogHelper.LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
LogHelper.WriteLogToFile($"UIAccess | 已使用 UIAccess 令牌启动新进程 (PID={pi.dwProcessId})");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void AppendQuoted(StringBuilder sb, string arg)
|
||||
{
|
||||
if (arg == null) { sb.Append("\"\""); return; }
|
||||
|
||||
bool needQuote = arg.Length == 0 || arg.IndexOfAny(new[] { ' ', '\t', '"' }) >= 0;
|
||||
if (!needQuote) { sb.Append(arg); return; }
|
||||
|
||||
sb.Append('"');
|
||||
int backslashes = 0;
|
||||
foreach (char c in arg)
|
||||
{
|
||||
if (c == '\\') { backslashes++; continue; }
|
||||
if (c == '"')
|
||||
{
|
||||
sb.Append('\\', backslashes * 2 + 1);
|
||||
sb.Append('"');
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append('\\', backslashes);
|
||||
sb.Append(c);
|
||||
}
|
||||
backslashes = 0;
|
||||
}
|
||||
sb.Append('\\', backslashes * 2);
|
||||
sb.Append('"');
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
using OSVersionExtension;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using WinAnalysis = global::Windows.UI.Input.Inking.Analysis;
|
||||
using WinRtInk = global::Windows.UI.Input.Inking;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// WinRT 手写体识别,以及将识别结果用手写风格字体轮廓转为墨迹笔画(「识别转手写体字形」)。
|
||||
/// </summary>
|
||||
internal static class WinRtHandwritingRecognizer
|
||||
{
|
||||
private static WinRtInk.InkRecognizer _preferredHandwritingRecognizer;
|
||||
private static bool _preferredHandwritingRecognizerResolved;
|
||||
|
||||
private static void LogHandwriting(string message, LogHelper.LogType logType = LogHelper.LogType.Info)
|
||||
{
|
||||
LogHelper.WriteLogToFile("[手写体] " + message, logType);
|
||||
}
|
||||
|
||||
public static bool IsApiAvailable =>
|
||||
OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
|
||||
|
||||
/// <summary>
|
||||
/// 启动阶段不再预热线程内 WinRT 手写管线。历史上曾用 <see cref="WinRtInkShapeRecognizer.CreateMinimalWarmupStrokeCollection"/> 跑全链路,
|
||||
/// 会显著拖慢启动;与更早的「空 <see cref="StrokeCollection"/>」一样,此处不再在 Idle 上做任何工作。
|
||||
/// 首次真正需要手写识别时由 <see cref="RecognizeHandwritingAsync"/> 承担冷启动成本。
|
||||
/// </summary>
|
||||
public static void Warmup()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前笔画集合识别为文字片段(含候选):先用墨迹分析得到分词与 <see cref="WinAnalysis.InkAnalysisInkWord.RecognizedText"/>,
|
||||
/// 再对每一分词用 <see cref="WinRtInk.InkRecognizerContainer"/> 取 <c>GetTextCandidates</c>(与当前 SDK 中部分版本的
|
||||
/// <see cref="WinRtInk.InkRecognitionResult"/> 未暴露笔画映射的局限兼容)。
|
||||
/// </summary>
|
||||
/// <param name="verboseTrace">为 false 时跳过详细识别日志(用于 <see cref="Warmup"/> 等)。</param>
|
||||
public static async Task<HandwritingRecognitionResult> RecognizeHandwritingAsync(
|
||||
StrokeCollection strokes,
|
||||
bool verboseTrace = true)
|
||||
{
|
||||
if (!IsApiAvailable || strokes == null || strokes.Count == 0)
|
||||
return HandwritingRecognitionResult.Empty;
|
||||
|
||||
var traceRecognition = verboseTrace;
|
||||
|
||||
try
|
||||
{
|
||||
var recognizer = new WinRtInk.InkRecognizerContainer();
|
||||
TryApplyPreferredHandwritingRecognizer(recognizer, traceRecognition);
|
||||
|
||||
var analyzer = new WinAnalysis.InkAnalyzer();
|
||||
var idToWpf = new Dictionary<uint, Stroke>();
|
||||
|
||||
foreach (Stroke s in strokes)
|
||||
{
|
||||
var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s);
|
||||
if (ink == null) continue;
|
||||
analyzer.AddDataForStroke(ink);
|
||||
analyzer.SetStrokeDataKind(ink.Id, WinAnalysis.InkAnalysisStrokeKind.Writing);
|
||||
idToWpf[ink.Id] = s;
|
||||
}
|
||||
|
||||
if (idToWpf.Count == 0)
|
||||
{
|
||||
if (traceRecognition)
|
||||
LogHandwriting("识别:无有效 WinRT 笔画(全部转换失败),输入笔画数=" + strokes.Count);
|
||||
return HandwritingRecognitionResult.Empty;
|
||||
}
|
||||
|
||||
var analysisResult = await analyzer.AnalyzeAsync().AsTask().ConfigureAwait(true);
|
||||
if (analysisResult == null || analysisResult.Status != WinAnalysis.InkAnalysisStatus.Updated)
|
||||
{
|
||||
if (traceRecognition)
|
||||
LogHandwriting(
|
||||
"识别:AnalyzeAsync 未得到 Updated,Status=" +
|
||||
(analysisResult == null ? "null" : analysisResult.Status.ToString()) +
|
||||
",有效笔画数=" + idToWpf.Count +
|
||||
",尝试整批 RecognizeAsync 回退。");
|
||||
return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
var wordNodes = analyzer.AnalysisRoot?.FindNodes(WinAnalysis.InkAnalysisNodeKind.InkWord);
|
||||
if (wordNodes == null || wordNodes.Count == 0)
|
||||
{
|
||||
if (traceRecognition)
|
||||
LogHandwriting(
|
||||
"识别:未找到 InkWord 节点(墨迹分析常将非横平笔划判为绘图),有效笔画数=" + idToWpf.Count +
|
||||
",改用整批 RecognizeAsync 回退。");
|
||||
return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
var segments = new List<HandwritingWordSegment>();
|
||||
|
||||
foreach (var node in wordNodes)
|
||||
{
|
||||
if (!(node is WinAnalysis.InkAnalysisInkWord word))
|
||||
continue;
|
||||
|
||||
var ids = word.GetStrokeIds();
|
||||
if (ids == null || ids.Count == 0)
|
||||
continue;
|
||||
|
||||
var group = new List<Stroke>();
|
||||
foreach (var sid in ids)
|
||||
{
|
||||
if (idToWpf.TryGetValue(sid, out var st))
|
||||
group.Add(st);
|
||||
}
|
||||
|
||||
if (group.Count == 0)
|
||||
continue;
|
||||
|
||||
var wbr = word.BoundingRect;
|
||||
var wpfRect = new Rect(wbr.X, wbr.Y, wbr.Width, wbr.Height);
|
||||
var analysisText = word.RecognizedText ?? string.Empty;
|
||||
|
||||
IReadOnlyList<string> candList = Array.Empty<string>();
|
||||
try
|
||||
{
|
||||
if (recognizer != null)
|
||||
{
|
||||
var mini = new WinRtInk.InkStrokeContainer();
|
||||
foreach (var st in group)
|
||||
{
|
||||
var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(st);
|
||||
if (ink != null)
|
||||
mini.AddStroke(ink);
|
||||
}
|
||||
|
||||
var miniStrokes = mini.GetStrokes();
|
||||
if (miniStrokes != null && miniStrokes.Count > 0)
|
||||
{
|
||||
var rr = await recognizer
|
||||
.RecognizeAsync(mini, WinRtInk.InkRecognitionTarget.All)
|
||||
.AsTask()
|
||||
.ConfigureAwait(true);
|
||||
if (rr != null && rr.Count > 0 && rr[0] != null)
|
||||
{
|
||||
var cands = rr[0].GetTextCandidates();
|
||||
if (cands != null && cands.Count > 0)
|
||||
candList = cands.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
candList = Array.Empty<string>();
|
||||
}
|
||||
|
||||
var primary = candList.Count > 0 ? candList[0] : analysisText;
|
||||
var mergedCandidates = new List<string>();
|
||||
if (candList.Count > 0)
|
||||
{
|
||||
foreach (var c in candList)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(c) && !mergedCandidates.Contains(c))
|
||||
mergedCandidates.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(analysisText) && !mergedCandidates.Contains(analysisText))
|
||||
mergedCandidates.Insert(0, analysisText);
|
||||
|
||||
if (mergedCandidates.Count == 0 && !string.IsNullOrEmpty(primary))
|
||||
mergedCandidates.Add(primary);
|
||||
|
||||
segments.Add(new HandwritingWordSegment(
|
||||
primary,
|
||||
mergedCandidates,
|
||||
wpfRect,
|
||||
group));
|
||||
}
|
||||
|
||||
if (segments.Count == 0)
|
||||
{
|
||||
if (traceRecognition)
|
||||
LogHandwriting("识别:分词列表为空(InkWord 无有效笔画映射)。");
|
||||
return HandwritingRecognitionResult.Empty;
|
||||
}
|
||||
|
||||
var hr = new HandwritingRecognitionResult(segments);
|
||||
if (traceRecognition)
|
||||
{
|
||||
var preview = hr.CombinedText;
|
||||
if (preview.Length > 120)
|
||||
preview = preview.Substring(0, 117) + "...";
|
||||
LogHandwriting(
|
||||
"识别成功:词数=" + segments.Count +
|
||||
",合并文本=\"" + preview + "\"" +
|
||||
",进程位数=" + (Environment.Is64BitProcess ? "x64" : "x86"));
|
||||
for (var i = 0; i < segments.Count; i++)
|
||||
{
|
||||
var seg = segments[i];
|
||||
var t = seg.Text ?? "";
|
||||
if (t.Length > 40)
|
||||
t = t.Substring(0, 37) + "...";
|
||||
LogHandwriting(
|
||||
" 词[" + i + "] 文本=\"" + t + "\",笔画数=" + seg.Strokes.Count +
|
||||
",候选数=" + (seg.TextCandidates?.Count ?? 0) +
|
||||
",框=(" + Math.Round(seg.BoundingRectangle.X, 1) + "," +
|
||||
Math.Round(seg.BoundingRectangle.Y, 1) + "," +
|
||||
Math.Round(seg.BoundingRectangle.Width, 1) + "×" +
|
||||
Math.Round(seg.BoundingRectangle.Height, 1) + ")");
|
||||
}
|
||||
}
|
||||
|
||||
return hr;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("WinRT 手写识别失败: " + ex.Message, LogHelper.LogType.Warning);
|
||||
if (strokes != null && strokes.Count > 0)
|
||||
LogHandwriting("识别异常:" + ex.Message, LogHelper.LogType.Warning);
|
||||
return HandwritingRecognitionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryApplyPreferredHandwritingRecognizer(
|
||||
WinRtInk.InkRecognizerContainer container,
|
||||
bool logDetail)
|
||||
{
|
||||
if (container == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (!_preferredHandwritingRecognizerResolved)
|
||||
{
|
||||
_preferredHandwritingRecognizerResolved = true;
|
||||
var all = container.GetRecognizers();
|
||||
_preferredHandwritingRecognizer = SelectBestInkRecognizer(all);
|
||||
if (logDetail)
|
||||
{
|
||||
if (_preferredHandwritingRecognizer != null)
|
||||
LogHandwriting("识别器:已选用 \"" + _preferredHandwritingRecognizer.Name + "\"。");
|
||||
else if (all != null && all.Count > 0)
|
||||
LogHandwriting("识别器:未匹配到与 UI/区域语言对应的引擎,使用系统默认(共 " + all.Count + " 个)。");
|
||||
}
|
||||
}
|
||||
|
||||
if (_preferredHandwritingRecognizer != null)
|
||||
container.SetDefaultRecognizer(_preferredHandwritingRecognizer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("[手写体] 设置默认手写识别器失败: " + ex.Message, LogHelper.LogType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private static WinRtInk.InkRecognizer SelectBestInkRecognizer(
|
||||
IReadOnlyList<WinRtInk.InkRecognizer> list)
|
||||
{
|
||||
if (list == null || list.Count == 0)
|
||||
return null;
|
||||
|
||||
var culture = PrimaryHandwritingCulture();
|
||||
var lang = (culture?.TwoLetterISOLanguageName ?? string.Empty).ToLowerInvariant();
|
||||
var name = culture?.Name ?? string.Empty;
|
||||
|
||||
bool wantZhHans = lang == "zh" &&
|
||||
(name.IndexOf("hans", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
name.Equals("zh-cn", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Equals("zh-sg", StringComparison.OrdinalIgnoreCase) ||
|
||||
(name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) < 0 &&
|
||||
!name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) &&
|
||||
!name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) &&
|
||||
!name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
bool wantZhHant = lang == "zh" &&
|
||||
(name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
WinRtInk.InkRecognizer Pick(Func<string, bool> match)
|
||||
{
|
||||
foreach (var r in list)
|
||||
{
|
||||
var n = r?.Name;
|
||||
if (string.IsNullOrEmpty(n))
|
||||
continue;
|
||||
if (match(n))
|
||||
return r;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wantZhHans)
|
||||
{
|
||||
var r = Pick(n =>
|
||||
n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
(n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
(n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0)) ||
|
||||
(n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
(n.IndexOf("Simplified", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("Hans", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("PRC", StringComparison.OrdinalIgnoreCase) >= 0)));
|
||||
if (r != null)
|
||||
return r;
|
||||
r = Pick(n =>
|
||||
n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
if (r != null)
|
||||
return r;
|
||||
}
|
||||
else if (wantZhHant)
|
||||
{
|
||||
var r = Pick(n =>
|
||||
n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
(n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
(n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0)) ||
|
||||
(n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
(n.IndexOf("Traditional", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("Hant", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("Taiwan", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("Hong Kong", StringComparison.OrdinalIgnoreCase) >= 0)));
|
||||
if (r != null)
|
||||
return r;
|
||||
r = Pick(n =>
|
||||
n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
if (r != null)
|
||||
return r;
|
||||
}
|
||||
else if (lang == "ja")
|
||||
{
|
||||
var r = Pick(n =>
|
||||
n.IndexOf("Japanese", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("日本語", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("日语", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
if (r != null)
|
||||
return r;
|
||||
}
|
||||
else if (lang == "en")
|
||||
{
|
||||
var r = Pick(n => n.IndexOf("English", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
if (r != null)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (lang == "zh")
|
||||
{
|
||||
var r = Pick(n =>
|
||||
n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
if (r != null)
|
||||
return r;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CultureInfo PrimaryHandwritingCulture()
|
||||
{
|
||||
var ui = CultureInfo.CurrentUICulture;
|
||||
var ct = CultureInfo.CurrentCulture;
|
||||
if (string.Equals(ui.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase))
|
||||
return ui;
|
||||
if (string.Equals(ct.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase))
|
||||
return ct;
|
||||
return ui;
|
||||
}
|
||||
|
||||
private static async Task<HandwritingRecognitionResult> RecognizeHandwritingWholeInkAsync(
|
||||
StrokeCollection strokes,
|
||||
bool traceRecognition)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
return HandwritingRecognitionResult.Empty;
|
||||
|
||||
var container = new WinRtInk.InkStrokeContainer();
|
||||
foreach (Stroke s in strokes)
|
||||
{
|
||||
var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s);
|
||||
if (ink != null)
|
||||
container.AddStroke(ink);
|
||||
}
|
||||
|
||||
var winStrokes = container.GetStrokes();
|
||||
if (winStrokes == null || winStrokes.Count == 0)
|
||||
{
|
||||
if (traceRecognition)
|
||||
LogHandwriting("整批回退:无有效 WinRT 笔画。");
|
||||
return HandwritingRecognitionResult.Empty;
|
||||
}
|
||||
|
||||
var reco = new WinRtInk.InkRecognizerContainer();
|
||||
TryApplyPreferredHandwritingRecognizer(reco, false);
|
||||
|
||||
IReadOnlyList<WinRtInk.InkRecognitionResult> rr;
|
||||
try
|
||||
{
|
||||
rr = await reco
|
||||
.RecognizeAsync(container, WinRtInk.InkRecognitionTarget.All)
|
||||
.AsTask()
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (traceRecognition)
|
||||
LogHandwriting("整批回退:RecognizeAsync 异常:" + ex.Message);
|
||||
return HandwritingRecognitionResult.Empty;
|
||||
}
|
||||
|
||||
if (rr == null || rr.Count == 0 || rr[0] == null)
|
||||
{
|
||||
if (traceRecognition)
|
||||
LogHandwriting("整批回退:RecognizeAsync 无结果。");
|
||||
return HandwritingRecognitionResult.Empty;
|
||||
}
|
||||
|
||||
var cands = rr[0].GetTextCandidates();
|
||||
var primary = (cands != null && cands.Count > 0) ? cands[0] : string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(primary))
|
||||
{
|
||||
if (traceRecognition)
|
||||
LogHandwriting("整批回退:候选文本为空。");
|
||||
return HandwritingRecognitionResult.Empty;
|
||||
}
|
||||
|
||||
var merged = new List<string>();
|
||||
if (cands != null)
|
||||
{
|
||||
foreach (var c in cands)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(c) && !merged.Contains(c))
|
||||
merged.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
var bounds = UnionStrokeBounds(strokes);
|
||||
var group = new List<Stroke>();
|
||||
foreach (Stroke s in strokes)
|
||||
group.Add(s);
|
||||
|
||||
var seg = new HandwritingWordSegment(primary, merged, bounds, group);
|
||||
return new HandwritingRecognitionResult(new List<HandwritingWordSegment> { seg });
|
||||
}
|
||||
|
||||
private static Rect UnionStrokeBounds(StrokeCollection strokes)
|
||||
{
|
||||
if (strokes == null || strokes.Count == 0)
|
||||
return Rect.Empty;
|
||||
|
||||
var r = strokes[0].GetBounds();
|
||||
for (var i = 1; i < strokes.Count; i++)
|
||||
r = Rect.Union(r, strokes[i].GetBounds());
|
||||
return r;
|
||||
}
|
||||
|
||||
private const string DefaultHandwritingFontFamilyList = "Ink Free,KaiTi,Segoe Script";
|
||||
|
||||
/// <summary>
|
||||
/// 识别手写词后,将「有识别文本」的分词替换为指定手写风格字体的字形轮廓墨迹;未识别或空文本的词保留原笔画。
|
||||
/// </summary>
|
||||
public static async Task<StrokeCollection> ConvertRecognizedTextToHandwritingInkAsync(
|
||||
StrokeCollection strokes,
|
||||
string handwritingFontFamilyList)
|
||||
{
|
||||
if (!IsApiAvailable || strokes == null || strokes.Count == 0)
|
||||
{
|
||||
if (strokes != null && strokes.Count > 0 && !IsApiAvailable)
|
||||
LogHandwriting("字形替换:跳过,IsApiAvailable=false。");
|
||||
return strokes;
|
||||
}
|
||||
|
||||
var fontList = string.IsNullOrWhiteSpace(handwritingFontFamilyList)
|
||||
? DefaultHandwritingFontFamilyList
|
||||
: handwritingFontFamilyList.Trim();
|
||||
LogHandwriting(
|
||||
"字形替换开始:输入笔画数=" + strokes.Count +
|
||||
",字体链=\"" + fontList + "\"" +
|
||||
",PixelsPerDip=" + Math.Round(GetPixelsPerDipSafe(), 3));
|
||||
|
||||
try
|
||||
{
|
||||
var reco = await RecognizeHandwritingAsync(strokes).ConfigureAwait(true);
|
||||
if (!reco.IsSuccess || reco.Words == null || reco.Words.Count == 0)
|
||||
{
|
||||
LogHandwriting(
|
||||
"字形替换中止:识别未成功(IsSuccess=" + reco.IsSuccess +
|
||||
",词数=" + (reco.Words?.Count ?? 0) + "),原样返回笔画。");
|
||||
return strokes;
|
||||
}
|
||||
|
||||
var firstStrokeToSegment = new Dictionary<Stroke, HandwritingWordSegment>();
|
||||
foreach (var w in reco.Words)
|
||||
{
|
||||
if (w?.Strokes == null || w.Strokes.Count == 0)
|
||||
continue;
|
||||
var ordered = w.Strokes.OrderBy(st => IndexOfStrokeInCollection(strokes, st)).ToList();
|
||||
var first = ordered[0];
|
||||
if (!firstStrokeToSegment.ContainsKey(first))
|
||||
firstStrokeToSegment[first] = w;
|
||||
}
|
||||
|
||||
if (firstStrokeToSegment.Count == 0)
|
||||
{
|
||||
LogHandwriting("字形替换中止:无法建立「首笔画→分词」映射,原样返回。");
|
||||
return strokes;
|
||||
}
|
||||
|
||||
var consumed = new HashSet<Stroke>();
|
||||
var result = new StrokeCollection();
|
||||
var pixelsPerDip = GetPixelsPerDipSafe();
|
||||
var replacedWordCount = 0;
|
||||
var keptOriginalWordCount = 0;
|
||||
var glyphStrokeTotal = 0;
|
||||
|
||||
foreach (Stroke s in strokes)
|
||||
{
|
||||
if (consumed.Contains(s))
|
||||
continue;
|
||||
|
||||
if (!firstStrokeToSegment.TryGetValue(s, out var seg))
|
||||
{
|
||||
result.Add(s);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(seg.Text))
|
||||
{
|
||||
LogHandwriting(
|
||||
" 分词:文本为空,保留原笔画,笔画数=" + seg.Strokes.Count);
|
||||
keptOriginalWordCount++;
|
||||
foreach (var z in seg.Strokes)
|
||||
{
|
||||
if (!consumed.Contains(z))
|
||||
{
|
||||
result.Add(z);
|
||||
consumed.Add(z);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var templateDa = seg.Strokes[0]?.DrawingAttributes?.Clone() ?? new DrawingAttributes();
|
||||
OutlineAttributesForGlyphInk(templateDa);
|
||||
|
||||
var glyphStrokes = CreateHandwritingGlyphStrokes(
|
||||
seg.Text.Trim(),
|
||||
seg.BoundingRectangle,
|
||||
templateDa,
|
||||
fontList,
|
||||
pixelsPerDip);
|
||||
|
||||
if (glyphStrokes == null || glyphStrokes.Count == 0)
|
||||
{
|
||||
LogHandwriting(
|
||||
" 分词:字形轮廓生成失败,保留原笔画。文本=\"" +
|
||||
(seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) + "\"");
|
||||
keptOriginalWordCount++;
|
||||
foreach (var z in seg.Strokes)
|
||||
{
|
||||
if (!consumed.Contains(z))
|
||||
{
|
||||
result.Add(z);
|
||||
consumed.Add(z);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var nk in glyphStrokes)
|
||||
result.Add(nk);
|
||||
glyphStrokeTotal += glyphStrokes.Count;
|
||||
replacedWordCount++;
|
||||
LogHandwriting(
|
||||
" 分词:已替换为手写体字形墨迹,文本=\"" +
|
||||
(seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) +
|
||||
"\",生成笔画数=" + glyphStrokes.Count + ",移除原笔画数=" + seg.Strokes.Count);
|
||||
|
||||
foreach (var z in seg.Strokes)
|
||||
consumed.Add(z);
|
||||
}
|
||||
|
||||
LogHandwriting(
|
||||
"字形替换结束:输出笔画数=" + result.Count +
|
||||
"(输入=" + strokes.Count + "),替换词数=" + replacedWordCount +
|
||||
",保留原迹词数=" + keptOriginalWordCount +
|
||||
",字形子笔画合计=" + glyphStrokeTotal);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile("WinRT 手写体字形替换失败: " + ex.Message, LogHelper.LogType.Warning);
|
||||
LogHandwriting("字形替换异常:" + ex, LogHelper.LogType.Warning);
|
||||
return strokes;
|
||||
}
|
||||
}
|
||||
|
||||
private static int IndexOfStrokeInCollection(StrokeCollection collection, Stroke stroke)
|
||||
{
|
||||
if (collection == null || stroke == null)
|
||||
return int.MaxValue;
|
||||
for (var i = 0; i < collection.Count; i++)
|
||||
{
|
||||
if (ReferenceEquals(collection[i], stroke))
|
||||
return i;
|
||||
}
|
||||
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
private static void OutlineAttributesForGlyphInk(DrawingAttributes da)
|
||||
{
|
||||
if (da == null) return;
|
||||
var w = Math.Max(0.8, Math.Min(da.Width, da.Height) * 0.2);
|
||||
da.Width = w;
|
||||
da.Height = w;
|
||||
da.StylusTip = StylusTip.Ellipse;
|
||||
da.IsHighlighter = false;
|
||||
}
|
||||
|
||||
private static double GetPixelsPerDipSafe()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Application.Current?.MainWindow is Visual v)
|
||||
return VisualTreeHelper.GetDpi(v).PixelsPerDip;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
private static Typeface ResolveHandwritingTypeface(string fontFamilyList)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ff = new FontFamily(fontFamilyList ?? DefaultHandwritingFontFamilyList);
|
||||
return new Typeface(ff, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new Typeface(
|
||||
SystemFonts.MessageFontFamily,
|
||||
SystemFonts.MessageFontStyle,
|
||||
SystemFonts.MessageFontWeight,
|
||||
FontStretches.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Stroke> CreateHandwritingGlyphStrokes(
|
||||
string text,
|
||||
Rect placeRect,
|
||||
DrawingAttributes templateDa,
|
||||
string fontFamilyList,
|
||||
double pixelsPerDip)
|
||||
{
|
||||
var list = new List<Stroke>();
|
||||
if (string.IsNullOrEmpty(text) || placeRect.Width < 1 || placeRect.Height < 1)
|
||||
return list;
|
||||
|
||||
var typeface = ResolveHandwritingTypeface(fontFamilyList);
|
||||
var culture = CultureInfo.CurrentCulture;
|
||||
var em = Math.Max(6.0, placeRect.Height * 0.72);
|
||||
FormattedText ft = null;
|
||||
|
||||
for (var i = 0; i < 14; i++)
|
||||
{
|
||||
ft = new FormattedText(
|
||||
text,
|
||||
culture,
|
||||
FlowDirection.LeftToRight,
|
||||
typeface,
|
||||
em,
|
||||
Brushes.Black,
|
||||
new NumberSubstitution(NumberCultureSource.Text, culture, NumberSubstitutionMethod.Context),
|
||||
TextFormattingMode.Display,
|
||||
pixelsPerDip);
|
||||
|
||||
if (ft.Width <= placeRect.Width * 0.96 && ft.Height <= placeRect.Height * 0.96)
|
||||
break;
|
||||
|
||||
em *= 0.9;
|
||||
if (em < 4.5)
|
||||
break;
|
||||
}
|
||||
|
||||
if (ft == null || ft.Width < 0.5 || ft.Height < 0.5)
|
||||
return list;
|
||||
|
||||
var scale = Math.Min(
|
||||
placeRect.Width * 0.94 / Math.Max(1e-6, ft.Width),
|
||||
placeRect.Height * 0.94 / Math.Max(1e-6, ft.Height));
|
||||
var tx = placeRect.Left + (placeRect.Width - ft.Width * scale) / 2.0;
|
||||
var ty = placeRect.Top + (placeRect.Height - ft.Height * scale) / 2.0;
|
||||
|
||||
Geometry geom;
|
||||
try
|
||||
{
|
||||
geom = ft.BuildGeometry(new Point(0, 0));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
if (geom == null || geom.IsEmpty())
|
||||
return list;
|
||||
|
||||
var m = new Matrix(scale, 0, 0, scale, tx, ty);
|
||||
geom.Transform = new MatrixTransform(m);
|
||||
return StrokesFromOutlinedGeometry(geom, templateDa, 0.35);
|
||||
}
|
||||
|
||||
private static List<Stroke> StrokesFromOutlinedGeometry(Geometry geometry, DrawingAttributes da, double tolerance)
|
||||
{
|
||||
var list = new List<Stroke>();
|
||||
if (geometry == null || geometry.IsEmpty() || da == null)
|
||||
return list;
|
||||
|
||||
Geometry outlined;
|
||||
try
|
||||
{
|
||||
outlined = geometry.GetOutlinedPathGeometry(tolerance, ToleranceType.Absolute);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
if (outlined == null || outlined.IsEmpty())
|
||||
return list;
|
||||
|
||||
Geometry flat;
|
||||
try
|
||||
{
|
||||
flat = outlined.GetFlattenedPathGeometry(tolerance, ToleranceType.Absolute);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
if (!(flat is PathGeometry pg))
|
||||
return list;
|
||||
|
||||
foreach (var fig in pg.Figures)
|
||||
{
|
||||
var pts = new StylusPointCollection();
|
||||
pts.Add(new StylusPoint(fig.StartPoint.X, fig.StartPoint.Y, 0.5f));
|
||||
foreach (var seg in fig.Segments)
|
||||
{
|
||||
switch (seg)
|
||||
{
|
||||
case LineSegment ls:
|
||||
pts.Add(new StylusPoint(ls.Point.X, ls.Point.Y, 0.5f));
|
||||
break;
|
||||
case PolyLineSegment pls:
|
||||
foreach (var p in pls.Points)
|
||||
pts.Add(new StylusPoint(p.X, p.Y, 0.5f));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pts.Count >= 2)
|
||||
list.Add(new Stroke(pts) { DrawingAttributes = da.Clone() });
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>单个手写词片段的识别结果。</summary>
|
||||
public sealed class HandwritingWordSegment
|
||||
{
|
||||
public HandwritingWordSegment(
|
||||
string text,
|
||||
IReadOnlyList<string> textCandidates,
|
||||
Rect boundingRectangle,
|
||||
IReadOnlyList<Stroke> strokes)
|
||||
{
|
||||
Text = text ?? string.Empty;
|
||||
TextCandidates = textCandidates ?? Array.Empty<string>();
|
||||
BoundingRectangle = boundingRectangle;
|
||||
Strokes = strokes ?? Array.Empty<Stroke>();
|
||||
}
|
||||
|
||||
public string Text { get; }
|
||||
public IReadOnlyList<string> TextCandidates { get; }
|
||||
public Rect BoundingRectangle { get; }
|
||||
public IReadOnlyList<Stroke> Strokes { get; }
|
||||
}
|
||||
|
||||
/// <summary>一次手写识别批次的汇总结果。</summary>
|
||||
public sealed class HandwritingRecognitionResult
|
||||
{
|
||||
public static readonly HandwritingRecognitionResult Empty = new HandwritingRecognitionResult();
|
||||
|
||||
private HandwritingRecognitionResult()
|
||||
{
|
||||
Words = Array.Empty<HandwritingWordSegment>();
|
||||
IsSuccess = false;
|
||||
CombinedText = string.Empty;
|
||||
}
|
||||
|
||||
public HandwritingRecognitionResult(IReadOnlyList<HandwritingWordSegment> words)
|
||||
{
|
||||
Words = words ?? Array.Empty<HandwritingWordSegment>();
|
||||
IsSuccess = Words.Count > 0;
|
||||
CombinedText = string.Join("", Words.Select(w => w.Text ?? string.Empty));
|
||||
}
|
||||
|
||||
public bool IsSuccess { get; }
|
||||
public IReadOnlyList<HandwritingWordSegment> Words { get; }
|
||||
public string CombinedText { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
using OSVersionExtension;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using SysPoint = System.Windows.Point;
|
||||
using WinRtInkAnalyzer = global::Windows.UI.Input.Inking.Analysis.InkAnalyzer;
|
||||
|
||||
namespace Ink_Canvas.Helpers
|
||||
{
|
||||
internal class ModernInkAnalyzer : IDisposable
|
||||
{
|
||||
public static readonly Guid ShapeStrokePropertyGuid = new Guid("11111111-2222-3333-4444-555555555555");
|
||||
|
||||
private global::Windows.UI.Input.Inking.Analysis.InkAnalyzer _internalAnalyzer;
|
||||
private readonly Dictionary<Stroke, uint> _strokeIdMap = new Dictionary<Stroke, uint>();
|
||||
private readonly Dictionary<uint, Stroke> _reverseIdMap = new Dictionary<uint, Stroke>();
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
public ModernInkAnalyzer()
|
||||
{
|
||||
if (!WinRtInkShapeRecognizer.IsApiAvailable)
|
||||
return;
|
||||
|
||||
_internalAnalyzer = new global::Windows.UI.Input.Inking.Analysis.InkAnalyzer();
|
||||
}
|
||||
|
||||
private void AddStrokeInternal(Stroke stroke)
|
||||
{
|
||||
if (stroke.ContainsPropertyData(ShapeStrokePropertyGuid))
|
||||
return;
|
||||
|
||||
var inkStroke = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(stroke);
|
||||
if (inkStroke == null) return;
|
||||
|
||||
_internalAnalyzer.AddDataForStroke(inkStroke);
|
||||
_internalAnalyzer.SetStrokeDataKind(
|
||||
inkStroke.Id,
|
||||
global::Windows.UI.Input.Inking.Analysis.InkAnalysisStrokeKind.Drawing);
|
||||
|
||||
_strokeIdMap[stroke] = inkStroke.Id;
|
||||
_reverseIdMap[inkStroke.Id] = stroke;
|
||||
}
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
public async Task<InkShapeRecognitionResult> AnalyzeAsync(StrokeCollection strokes)
|
||||
{
|
||||
if (_internalAnalyzer == null || strokes == null || strokes.Count == 0)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
_cts?.Cancel();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
|
||||
try
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_internalAnalyzer.ClearDataForAllStrokes();
|
||||
_strokeIdMap.Clear();
|
||||
_reverseIdMap.Clear();
|
||||
|
||||
foreach (var stroke in strokes)
|
||||
{
|
||||
AddStrokeInternal(stroke);
|
||||
}
|
||||
}
|
||||
|
||||
if (_strokeIdMap.Count == 0)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var result = await _internalAnalyzer.AnalyzeAsync().AsTask(token).ConfigureAwait(true);
|
||||
|
||||
if (token.IsCancellationRequested) return InkShapeRecognitionResult.Empty;
|
||||
|
||||
// Use the internal method from WinRtInkShapeRecognizer to find the primary drawing
|
||||
var drawing = WinRtInkShapeRecognizer.FindPrimaryDrawing(_internalAnalyzer);
|
||||
if (drawing == null)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
if (drawing.DrawingKind == global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Drawing)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var name = WinRtInkShapeRecognizer.MapDrawingKindToShapeName(drawing.DrawingKind);
|
||||
if (string.IsNullOrEmpty(name) || name == "Drawing")
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var winPts = WinRtInkShapeRecognizer.CopyWinRtPoints(drawing);
|
||||
var hot = WinRtInkShapeRecognizer.ToWpfPointCollection(winPts);
|
||||
var c = drawing.Center;
|
||||
var centroid = new SysPoint(c.X, c.Y);
|
||||
WinRtInkShapeRecognizer.BoundsFromPoints(winPts, out double w, out double h);
|
||||
|
||||
var toRemove = new StrokeCollection();
|
||||
lock (_syncLock)
|
||||
{
|
||||
foreach (var id in drawing.GetStrokeIds())
|
||||
{
|
||||
if (_reverseIdMap.TryGetValue(id, out var stroke))
|
||||
{
|
||||
toRemove.Add(stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove.Count == 0)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
return new InkShapeRecognitionResult(name, centroid, hot, w, h, toRemove);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<StrokeCollection> AnalyzeAndCorrectAsync(
|
||||
StrokeCollection strokes,
|
||||
string handwritingFontFamilyList)
|
||||
{
|
||||
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(
|
||||
strokes,
|
||||
handwritingFontFamilyList);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_internalAnalyzer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>基于 Windows.UI.Input.Inking.Analysis 的形状识别(适用于 64 位进程等场景)。</summary>
|
||||
internal static class WinRtInkShapeRecognizer
|
||||
{
|
||||
public static bool IsApiAvailable =>
|
||||
OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
|
||||
|
||||
public static void Warmup()
|
||||
{
|
||||
if (!IsApiAvailable) return;
|
||||
try
|
||||
{
|
||||
var d = Application.Current?.Dispatcher;
|
||||
if (d == null) return;
|
||||
d.BeginInvoke(new Action(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 空 StrokeCollection 在 RecognizeShapeAsync 入口会直接返回,无法预热 WinRT InkAnalyzer。
|
||||
await RecognizeShapeAsync(CreateMinimalWarmupStrokeCollection()).ConfigureAwait(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>由 <see cref="ModernInkProcessor"/> / <see cref="InkRecognitionManager"/> 在 UI 上 await(勿在收笔回调中同步阻塞)。</summary>
|
||||
internal static async Task<InkShapeRecognitionResult> RecognizeShapeAsync(StrokeCollection strokes)
|
||||
{
|
||||
if (!IsApiAvailable || strokes == null || strokes.Count == 0)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var analyzer = new WinRtInkAnalyzer();
|
||||
var added = 0;
|
||||
foreach (Stroke s in strokes)
|
||||
{
|
||||
var inkStroke = CreateInkStrokeFromWpf(s);
|
||||
if (inkStroke == null)
|
||||
continue;
|
||||
|
||||
analyzer.AddDataForStroke(inkStroke);
|
||||
analyzer.SetStrokeDataKind(
|
||||
inkStroke.Id,
|
||||
global::Windows.UI.Input.Inking.Analysis.InkAnalysisStrokeKind.Drawing);
|
||||
added++;
|
||||
}
|
||||
|
||||
if (added == 0)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
await analyzer.AnalyzeAsync().AsTask().ConfigureAwait(true);
|
||||
|
||||
var drawing = FindPrimaryDrawing(analyzer);
|
||||
if (drawing == null)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
if (drawing.DrawingKind == global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Drawing)
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var name = MapDrawingKindToShapeName(drawing.DrawingKind);
|
||||
if (string.IsNullOrEmpty(name) || name == "Drawing")
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
|
||||
var winPts = CopyWinRtPoints(drawing);
|
||||
var hot = ToWpfPointCollection(winPts);
|
||||
var c = drawing.Center;
|
||||
var centroid = new SysPoint(c.X, c.Y);
|
||||
BoundsFromPoints(winPts, out double w, out double h);
|
||||
|
||||
var toRemove = new StrokeCollection();
|
||||
foreach (Stroke s in strokes)
|
||||
toRemove.Add(s);
|
||||
|
||||
return new InkShapeRecognitionResult(name, centroid, hot, w, h, toRemove);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return InkShapeRecognitionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 极短合成笔画,供 <see cref="Warmup"/> 等场景走完整 WinRT 转换与分析管线(空集合在入口处会被直接返回)。
|
||||
/// </summary>
|
||||
internal static StrokeCollection CreateMinimalWarmupStrokeCollection()
|
||||
{
|
||||
var da = new DrawingAttributes { Color = Colors.Black, Width = 2, Height = 2 };
|
||||
var pts = new StylusPointCollection
|
||||
{
|
||||
new StylusPoint(8, 8),
|
||||
new StylusPoint(14, 10),
|
||||
new StylusPoint(20, 8),
|
||||
};
|
||||
var col = new StrokeCollection();
|
||||
col.Add(new Stroke(pts, da));
|
||||
return col;
|
||||
}
|
||||
|
||||
/// <summary>供 WinRT 手写等模块复用:将 WPF <see cref="Stroke"/> 转为 WinRT <see cref="global::Windows.UI.Input.Inking.InkStroke"/>。</summary>
|
||||
internal static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke)
|
||||
{
|
||||
if (stroke?.StylusPoints == null || stroke.StylusPoints.Count == 0)
|
||||
return null;
|
||||
|
||||
var da = stroke.DrawingAttributes;
|
||||
if (da == null)
|
||||
return null;
|
||||
|
||||
var wda = new global::Windows.UI.Input.Inking.InkDrawingAttributes
|
||||
{
|
||||
PenTip = global::Windows.UI.Input.Inking.PenTipShape.Circle,
|
||||
Color = global::Windows.UI.Color.FromArgb(da.Color.A, da.Color.R, da.Color.G, da.Color.B),
|
||||
Size = new global::Windows.Foundation.Size((float)da.Width, (float)da.Height)
|
||||
};
|
||||
|
||||
var builder = new global::Windows.UI.Input.Inking.InkStrokeBuilder();
|
||||
builder.SetDefaultDrawingAttributes(wda);
|
||||
|
||||
var points = new List<global::Windows.Foundation.Point>(stroke.StylusPoints.Count);
|
||||
foreach (StylusPoint sp in stroke.StylusPoints)
|
||||
{
|
||||
var pi = sp.ToPoint();
|
||||
points.Add(new global::Windows.Foundation.Point((float)pi.X, (float)pi.Y));
|
||||
}
|
||||
|
||||
if (points.Count == 0)
|
||||
return null;
|
||||
|
||||
return builder.CreateStroke(points);
|
||||
}
|
||||
|
||||
internal static global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing FindPrimaryDrawing(
|
||||
global::Windows.UI.Input.Inking.Analysis.InkAnalyzer analyzer)
|
||||
{
|
||||
global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing best = null;
|
||||
double bestArea = -1;
|
||||
if (analyzer?.AnalysisRoot != null)
|
||||
Visit(analyzer.AnalysisRoot);
|
||||
return best;
|
||||
|
||||
void Visit(global::Windows.UI.Input.Inking.Analysis.IInkAnalysisNode node)
|
||||
{
|
||||
if (node == null) return;
|
||||
|
||||
if (node is global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing d &&
|
||||
d.DrawingKind != global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Drawing)
|
||||
{
|
||||
double area = EstimateDrawingArea(d);
|
||||
if (area > bestArea)
|
||||
{
|
||||
bestArea = area;
|
||||
best = d;
|
||||
}
|
||||
}
|
||||
|
||||
// WinRT IInkAnalysisNode.Children 可能为 null,不可直接 foreach。
|
||||
var children = node.Children;
|
||||
if (children == null) return;
|
||||
|
||||
foreach (var child in children)
|
||||
Visit(child);
|
||||
}
|
||||
}
|
||||
|
||||
private static double EstimateDrawingArea(global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing drawing)
|
||||
{
|
||||
var pts = CopyWinRtPoints(drawing);
|
||||
BoundsFromPoints(pts, out double w, out double h);
|
||||
return w * h;
|
||||
}
|
||||
|
||||
internal static global::Windows.Foundation.Point[] CopyWinRtPoints(
|
||||
global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing drawing)
|
||||
{
|
||||
var src = drawing?.Points;
|
||||
if (src == null)
|
||||
return Array.Empty<global::Windows.Foundation.Point>();
|
||||
|
||||
var n = src.Count;
|
||||
if (n == 0)
|
||||
return Array.Empty<global::Windows.Foundation.Point>();
|
||||
|
||||
var arr = new global::Windows.Foundation.Point[n];
|
||||
for (var i = 0; i < n; i++)
|
||||
arr[i] = src[i];
|
||||
return arr;
|
||||
}
|
||||
|
||||
internal static void BoundsFromPoints(
|
||||
System.Collections.Generic.IReadOnlyList<global::Windows.Foundation.Point> points,
|
||||
out double w,
|
||||
out double h)
|
||||
{
|
||||
if (points == null || points.Count == 0)
|
||||
{
|
||||
w = h = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
double minX = double.MaxValue, maxX = double.MinValue, minY = double.MaxValue, maxY = double.MinValue;
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
{
|
||||
var pt = points[i];
|
||||
minX = Math.Min(minX, pt.X);
|
||||
maxX = Math.Max(maxX, pt.X);
|
||||
minY = Math.Min(minY, pt.Y);
|
||||
maxY = Math.Max(maxY, pt.Y);
|
||||
}
|
||||
|
||||
w = Math.Max(0, maxX - minX);
|
||||
h = Math.Max(0, maxY - minY);
|
||||
}
|
||||
|
||||
internal static PointCollection ToWpfPointCollection(
|
||||
System.Collections.Generic.IReadOnlyList<global::Windows.Foundation.Point> points)
|
||||
{
|
||||
var hot = new PointCollection();
|
||||
if (points == null) return hot;
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
{
|
||||
var pt = points[i];
|
||||
hot.Add(new SysPoint(pt.X, pt.Y));
|
||||
}
|
||||
|
||||
return hot;
|
||||
}
|
||||
|
||||
internal static string MapDrawingKindToShapeName(
|
||||
global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Circle:
|
||||
return "Circle";
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Ellipse:
|
||||
return "Ellipse";
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Triangle:
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.IsoscelesTriangle:
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.EquilateralTriangle:
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.RightTriangle:
|
||||
return "Triangle";
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Rectangle:
|
||||
return "Rectangle";
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Square:
|
||||
return "Square";
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Diamond:
|
||||
return "Diamond";
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Trapezoid:
|
||||
return "Trapezoid";
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Parallelogram:
|
||||
return "Parallelogram";
|
||||
case global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Quadrilateral:
|
||||
return "Quadrilateral";
|
||||
default:
|
||||
return kind == global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Drawing
|
||||
? "Drawing"
|
||||
: kind.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using H.NotifyIcon;
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using System;
|
||||
using System.Windows;
|
||||
@@ -40,10 +40,9 @@ namespace Ink_Canvas.Helpers
|
||||
|
||||
taskbar.Visibility = Visibility.Visible;
|
||||
|
||||
taskbar.ShowBalloonTip(
|
||||
taskbar.ShowNotification(
|
||||
"InkCanvasForClass CE",
|
||||
$"发现新版本!:{version}",
|
||||
BalloonIcon.Info);
|
||||
$"发现新版本!:{version}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RuntimeIdentifiers>win;win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>Ink_Canvas</RootNamespace>
|
||||
<AssemblyName>InkCanvasForClass</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
@@ -25,91 +27,79 @@
|
||||
<BootstrapperEnabled>false</BootstrapperEnabled>
|
||||
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Configurations>Debug;Release;x86 Debug</Configurations>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<Platforms>AnyCPU;x86;x64</Platforms>
|
||||
<LangVersion>10</LangVersion>
|
||||
<NoWarn>$(NoWarn);CA1416;NU1701;MSB3270;CS8012;NETSDK1138</NoWarn>
|
||||
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
|
||||
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
|
||||
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugType>embedded</DebugType>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<Prefer32Bit>True</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='x86 Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<Prefer32Bit>True</Prefer32Bit>
|
||||
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>embedded</DebugType>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<Prefer32Bit>True</Prefer32Bit>
|
||||
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>Resources\icc.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<DebugType>full</DebugType>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='x86 Debug|x86'">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<DebugType>full</DebugType>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
|
||||
<DebugType>embedded</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
|
||||
<DebugType>embedded</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Title>InkCanvasForClass</Title>
|
||||
<Version>5.0.4</Version>
|
||||
<Authors>Dubi906w</Authors>
|
||||
<Version>1.7</Version>
|
||||
<Authors>CJK_mkp</Authors>
|
||||
<Product>InkCanvasForClass</Product>
|
||||
<Copyright>© Copyright HARKOTEK Studio 2024-now</Copyright>
|
||||
<PackageProjectUrl>https://icc.bliemhax.com</PackageProjectUrl>
|
||||
<Copyright>© Copyright CJK_mkp 2025-now</Copyright>
|
||||
<PackageProjectUrl>https://inkcanvasforclass.github.io</PackageProjectUrl>
|
||||
<FileVersion>bundled</FileVersion>
|
||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
|
||||
<DebugType>full</DebugType>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='x86 Debug|ARM64'">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<DebugType>full</DebugType>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<PlatformTarget>ARM64</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<PlatformTarget>ARM64</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<DebugType>full</DebugType>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='x86 Debug|x64'">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<DebugType>full</DebugType>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<OutputPath>bin\$(Configuration)\$(Platform)\</OutputPath>
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="IACore">
|
||||
@@ -124,22 +114,6 @@
|
||||
<HintPath>.\IAWinFX.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualBasic" />
|
||||
<Reference Include="netstandard" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="UIAutomationClient" />
|
||||
<Reference Include="UIAutomationTypes" />
|
||||
<Reference Include="WindowsBase" />
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Reference Include="WindowsFormsIntegration" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.manifest" />
|
||||
@@ -147,9 +121,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Costura.Fody" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Hardcodet.NotifyIcon.Wpf" Version="2.0.1" />
|
||||
<PackageReference Include="gong-wpf-dragdrop" Version="4.0.0" />
|
||||
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.0.131" />
|
||||
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.10.2.1" />
|
||||
<PackageReference Include="iNKORE.UI.WPF" Version="1.2.8" />
|
||||
<PackageReference Include="MdXaml" Version="1.27.0" />
|
||||
@@ -157,10 +131,14 @@
|
||||
<PackageReference Include="MicrosoftOfficeCore" Version="15.0.0" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<PackageReference Include="Microsoft.International.Converters.PinYinConverter" Version="1.0.0" />
|
||||
<PackageReference Include="Sentry" Version="6.1.0" />
|
||||
<PackageReference Include="Sentry" Version="6.3.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.VisualBasic" Version="10.3.0" />
|
||||
<PackageReference Include="System.ComponentModel.Composition" Version="10.0.5" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" Version="10.0.5" />
|
||||
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
|
||||
<PackageReference Include="System.Management" Version="10.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="NHotkey.Wpf" Version="4.0.0" />
|
||||
<PackageReference Include="OSVersionExt" Version="4.1.0" />
|
||||
@@ -168,7 +146,13 @@
|
||||
<PackageReference Include="AForge.Video.DirectShow" Version="2.2.5" />
|
||||
<PackageReference Include="AForge.Imaging" Version="2.2.5" />
|
||||
<PackageReference Include="AForge.Math" Version="2.2.5" />
|
||||
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageReference Include="WebDav.Client" Version="2.9.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\InkCanvas.PluginSdk\InkCanvas.PluginSdk.csproj" />
|
||||
<ProjectReference Include="..\InkCanvas.Controls\InkCanvas.Controls.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(MSBuildRuntimeType)' == 'Full'">
|
||||
<COMReference Include="IWshRuntimeLibrary">
|
||||
@@ -189,15 +173,6 @@
|
||||
<Isolated>False</Isolated>
|
||||
<EmbedInteropTypes>True</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
<COMReference Include="VBIDE">
|
||||
<Guid>{0002E157-0000-0000-C000-000000000046}</Guid>
|
||||
<VersionMajor>5</VersionMajor>
|
||||
<VersionMinor>3</VersionMinor>
|
||||
<Lcid>0</Lcid>
|
||||
<WrapperTool>primary</WrapperTool>
|
||||
<Isolated>False</Isolated>
|
||||
<EmbedInteropTypes>True</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(MSBuildRuntimeType)' != 'Full'">
|
||||
<Reference Include="Interop.IWshRuntimeLibrary">
|
||||
@@ -219,8 +194,6 @@
|
||||
<EmbeddedResource Include="Resources\IACore\IACore.dll" />
|
||||
<EmbeddedResource Include="Resources\IACore\IALoader.dll" />
|
||||
<EmbeddedResource Include="Resources\IACore\IAWinFX.dll" />
|
||||
<EmbeddedResource Include="UIAccessDLL_x64.dll" />
|
||||
<EmbeddedResource Include="UIAccessDLL_x86.dll" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Cursors\Cursor.cur" />
|
||||
@@ -585,14 +558,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Remove="AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="Properties\Strings.en-US.resx" />
|
||||
<EmbeddedResource Remove="**\Strings.en-US.resx" />
|
||||
<None Include="Properties\Strings.en-US.resx" />
|
||||
<EmbeddedResource Include="Properties\Strings.enUS.xml">
|
||||
<LogicalName>Ink_Canvas.Properties.Strings.enUS.xml</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="MainWindow.xaml~RF6c3144.TMP" />
|
||||
<None Remove="Resources\Cursors\Cursor.cur" />
|
||||
|
||||
+1177
-8393
File diff suppressed because it is too large
Load Diff
+328
-1767
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
@@ -64,7 +63,7 @@ namespace Ink_Canvas
|
||||
/// 处理折叠浮动栏的鼠标点击事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="e">鼠标按钮事件参数。</param>
|
||||
/// <param name="e">路由事件参数。</param>
|
||||
public async void FoldFloatingBar_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
await FoldFloatingBar(sender);
|
||||
@@ -91,19 +90,6 @@ namespace Ink_Canvas
|
||||
/// </remarks>
|
||||
public async Task FoldFloatingBar(object sender, bool isAutoFoldCommand = false)
|
||||
{
|
||||
var isShouldRejectAction = false;
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (lastBorderMouseDownObject != null && lastBorderMouseDownObject is Panel)
|
||||
((Panel)lastBorderMouseDownObject).Background = new SolidColorBrush(Colors.Transparent);
|
||||
if (sender == Fold_Icon && lastBorderMouseDownObject != Fold_Icon) isShouldRejectAction = true;
|
||||
});
|
||||
|
||||
if (isShouldRejectAction)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// FloatingBarIcons_MouseUp_New(sender);
|
||||
if (sender == null)
|
||||
@@ -338,7 +324,7 @@ namespace Ink_Canvas
|
||||
/// 处理展开浮动栏的鼠标点击事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="e">鼠标按钮事件参数。</param>
|
||||
/// <param name="e">路由事件参数。</param>
|
||||
public async void UnFoldFloatingBar_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
await UnFoldFloatingBar(sender);
|
||||
@@ -525,5 +511,85 @@ namespace Ink_Canvas
|
||||
});
|
||||
isFloatingBarChangingHideMode = false;
|
||||
}
|
||||
|
||||
private bool IsFloatingBarUiAbsentFromScreens()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ViewboxFloatingBar == null) return true;
|
||||
if (Settings?.Automation?.ThoroughlyHideWhenFolded == true &&
|
||||
(!IsVisible || Visibility != Visibility.Visible))
|
||||
return true;
|
||||
if (ViewboxFloatingBar.Visibility != Visibility.Visible)
|
||||
return true;
|
||||
return IsOutsideOfScreenHelper.IsOutsideOfScreen(ViewboxFloatingBar);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动收纳校验:检测浮动栏可见性时出错: {ex.Message}", LogHelper.LogType.Warning);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitUntilFloatingBarHideModeIdleAsync(TimeSpan timeout)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
while (DateTime.UtcNow - start < timeout)
|
||||
{
|
||||
bool busy = await Dispatcher.InvokeAsync(() => isFloatingBarChangingHideMode);
|
||||
if (!busy) return;
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task WaitForStartupFoldSettledAsync(TimeSpan timeout)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
while (DateTime.UtcNow - start < timeout)
|
||||
{
|
||||
var settled = await Dispatcher.InvokeAsync(() => isFloatingBarFolded && !isFloatingBarChangingHideMode);
|
||||
if (settled) return;
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task VerifyStartupFoldAbsenceAfterDelayAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Settings.Startup.IsFoldAtStartup || App.StartWithBoardMode || App.StartWithShowMode)
|
||||
return;
|
||||
|
||||
await WaitForStartupFoldSettledAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false);
|
||||
await Task.Delay(3000).ConfigureAwait(false);
|
||||
|
||||
bool needRetry = await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (!isFloatingBarFolded) return false;
|
||||
if (currentMode != 0) return false;
|
||||
return !IsFloatingBarUiAbsentFromScreens();
|
||||
});
|
||||
|
||||
if (!needRetry) return;
|
||||
|
||||
LogHelper.WriteLogToFile("启动收纳校验:检测到浮动栏仍在屏幕上,将展开后等待 0.2s 再次收纳", LogHelper.LogType.Event);
|
||||
|
||||
await UnFoldFloatingBar(null);
|
||||
await WaitUntilFloatingBarHideModeIdleAsync(TimeSpan.FromSeconds(15)).ConfigureAwait(false);
|
||||
await Task.Delay(200).ConfigureAwait(false);
|
||||
await FoldFloatingBar(new object()).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"启动收纳校验失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleStartupFoldAbsenceVerification()
|
||||
{
|
||||
_ = VerifyStartupFoldAbsenceAfterDelayAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
using IWshRuntimeLibrary;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using Application = System.Windows.Forms.Application;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建开机自启动快捷方式。
|
||||
/// </summary>
|
||||
/// <param name="exeName">可执行文件名,用于命名快捷方式。</param>
|
||||
/// <returns>创建成功返回true,失败返回false。</returns>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 创建Windows Shell对象
|
||||
/// 2. 在启动文件夹中创建快捷方式
|
||||
/// 3. 设置快捷方式的目标路径为当前可执行文件路径
|
||||
/// 4. 设置工作目录为当前目录
|
||||
/// 5. 设置窗口样式为普通窗口
|
||||
/// 6. 设置快捷方式描述
|
||||
/// 7. 保存快捷方式
|
||||
/// 8. 捕获可能的异常,确保方法不会因异常而崩溃
|
||||
/// </remarks>
|
||||
public static bool StartAutomaticallyCreate(string exeName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var shell = new WshShell();
|
||||
var shortcut = (IWshShortcut)shell.CreateShortcut(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Startup) + "\\" + exeName + ".lnk");
|
||||
//设置快捷方式的目标所在的位置(源程序完整路径)
|
||||
shortcut.TargetPath = Application.ExecutablePath;
|
||||
//应用程序的工作目录
|
||||
//当用户没有指定一个具体的目录时,快捷方式的目标应用程序将使用该属性所指定的目录来装载或保存文件。
|
||||
shortcut.WorkingDirectory = Environment.CurrentDirectory;
|
||||
//目标应用程序窗口类型(1.Normal window普通窗口,3.Maximized最大化窗口,7.Minimized最小化)
|
||||
shortcut.WindowStyle = 1;
|
||||
//快捷方式的描述
|
||||
shortcut.Description = exeName + "_Ink";
|
||||
//设置快捷键(如果有必要的话.)
|
||||
//shortcut.Hotkey = "CTRL+ALT+D";
|
||||
shortcut.Save();
|
||||
return true;
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除开机自启动快捷方式。
|
||||
/// </summary>
|
||||
/// <param name="exeName">可执行文件名,用于定位要删除的快捷方式。</param>
|
||||
/// <returns>删除成功返回true,失败返回false。</returns>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 在启动文件夹中删除指定名称的快捷方式
|
||||
/// 2. 捕获可能的异常,确保方法不会因异常而崩溃
|
||||
/// </remarks>
|
||||
public static bool StartAutomaticallyDel(string exeName)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(Environment.GetFolderPath(Environment.SpecialFolder.Startup) + "\\" + exeName +
|
||||
".lnk");
|
||||
return true;
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,19 +14,18 @@ namespace Ink_Canvas
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 浮动栏前景色,根据当前主题动态更新。
|
||||
/// </summary>
|
||||
private const string ThemeLight = "Light";
|
||||
private const string ThemeDark = "Dark";
|
||||
private const string LightThemePath = "Resources/Styles/Light.xaml";
|
||||
private const string DarkThemePath = "Resources/Styles/Dark.xaml";
|
||||
private const string DrawShapeImagePath = "Resources/DrawShapeImageDictionary.xaml";
|
||||
private const string SeewoImagePath = "Resources/SeewoImageDictionary.xaml";
|
||||
private const string IconImagePath = "Resources/IconImageDictionary.xaml";
|
||||
|
||||
private Color FloatBarForegroundColor;
|
||||
|
||||
/// <summary>
|
||||
/// 应用并切换到指定的主题("Light" 或 "Dark"),更新主题资源并刷新相关 UI 元素以反映主题变化。
|
||||
/// </summary>
|
||||
/// <param name="theme">主题标识,支持 "Light" 或 "Dark"(区分大小写)。</param>
|
||||
/// <param name="autoSwitchIcon">若为 true,则根据主题自动切换并保存浮动工具栏的图标设置。</param>
|
||||
private void SetTheme(string theme, bool autoSwitchIcon = false)
|
||||
{
|
||||
// 清理现有的主题资源
|
||||
var resourcesToRemove = new List<ResourceDictionary>();
|
||||
foreach (var dict in Application.Current.Resources.MergedDictionaries)
|
||||
{
|
||||
@@ -43,195 +42,85 @@ namespace Ink_Canvas
|
||||
Application.Current.Resources.MergedDictionaries.Remove(dict);
|
||||
}
|
||||
|
||||
if (theme == "Light")
|
||||
var isLightTheme = theme == ThemeLight;
|
||||
var themePath = isLightTheme ? LightThemePath : DarkThemePath;
|
||||
var elementTheme = isLightTheme ? ElementTheme.Light : ElementTheme.Dark;
|
||||
|
||||
var rd1 = new ResourceDictionary { Source = new Uri(themePath, UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd1);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// 先加载主题
|
||||
var rd1 = new ResourceDictionary
|
||||
await Task.Delay(100);
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
Source = new Uri("Resources/Styles/Light.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd1);
|
||||
|
||||
// 异步加载图形资源,避免阻塞启动
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
var rd2 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd2);
|
||||
|
||||
var rd3 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/SeewoImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd3);
|
||||
|
||||
var rd4 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/IconImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd4);
|
||||
});
|
||||
LoadImageResourceDictionary(DrawShapeImagePath);
|
||||
LoadImageResourceDictionary(SeewoImagePath);
|
||||
LoadImageResourceDictionary(IconImagePath);
|
||||
});
|
||||
});
|
||||
|
||||
ThemeManager.SetRequestedTheme(window, ElementTheme.Light);
|
||||
ThemeManager.SetRequestedTheme(window, elementTheme);
|
||||
|
||||
InitializeFloatBarForegroundColor();
|
||||
InitializeFloatBarForegroundColor();
|
||||
RefreshQuickPanelIcons();
|
||||
RefreshStrokeSelectionIcons();
|
||||
RefreshImageSelectionIcons();
|
||||
RefreshGestureButtonIcon();
|
||||
RefreshFloatingBarHighlightColors();
|
||||
|
||||
// 刷新快速面板图标
|
||||
RefreshQuickPanelIcons();
|
||||
|
||||
// 刷新墨迹选中栏图标
|
||||
RefreshStrokeSelectionIcons();
|
||||
|
||||
// 刷新图片选中栏图标
|
||||
RefreshImageSelectionIcons();
|
||||
|
||||
// 刷新手势按钮图标
|
||||
RefreshGestureButtonIcon();
|
||||
|
||||
RefreshFloatingBarHighlightColors();
|
||||
|
||||
if (autoSwitchIcon)
|
||||
{
|
||||
AutoSwitchFloatingBarIconForTheme("Light");
|
||||
}
|
||||
|
||||
// 强制刷新UI
|
||||
window.InvalidateVisual();
|
||||
|
||||
// 通知其他窗口刷新主题
|
||||
RefreshOtherWindowsTheme();
|
||||
}
|
||||
else if (theme == "Dark")
|
||||
if (autoSwitchIcon)
|
||||
{
|
||||
// 先加载主题
|
||||
var rd1 = new ResourceDictionary { Source = new Uri("Resources/Styles/Dark.xaml", UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd1);
|
||||
|
||||
// 异步加载图形资源,避免阻塞启动
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
var rd2 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/DrawShapeImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd2);
|
||||
|
||||
var rd3 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/SeewoImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd3);
|
||||
|
||||
var rd4 = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri("Resources/IconImageDictionary.xaml", UriKind.Relative)
|
||||
};
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd4);
|
||||
});
|
||||
});
|
||||
|
||||
ThemeManager.SetRequestedTheme(window, ElementTheme.Dark);
|
||||
|
||||
InitializeFloatBarForegroundColor();
|
||||
|
||||
// 刷新快速面板图标
|
||||
RefreshQuickPanelIcons();
|
||||
|
||||
// 刷新墨迹选中栏图标
|
||||
RefreshStrokeSelectionIcons();
|
||||
|
||||
// 刷新图片选中栏图标
|
||||
RefreshImageSelectionIcons();
|
||||
|
||||
// 刷新手势按钮图标
|
||||
RefreshGestureButtonIcon();
|
||||
|
||||
RefreshFloatingBarHighlightColors();
|
||||
|
||||
if (autoSwitchIcon)
|
||||
{
|
||||
AutoSwitchFloatingBarIconForTheme("Dark");
|
||||
}
|
||||
|
||||
// 强制刷新UI
|
||||
window.InvalidateVisual();
|
||||
|
||||
// 通知其他窗口刷新主题
|
||||
RefreshOtherWindowsTheme();
|
||||
AutoSwitchFloatingBarIconForTheme(theme);
|
||||
}
|
||||
|
||||
window.InvalidateVisual();
|
||||
RefreshOtherWindowsTheme();
|
||||
}
|
||||
|
||||
void LoadImageResourceDictionary(string path)
|
||||
{
|
||||
var rd = new ResourceDictionary { Source = new Uri(path, UriKind.Relative) };
|
||||
Application.Current.Resources.MergedDictionaries.Add(rd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化FloatBarForegroundColor,从当前主题资源中加载颜色
|
||||
/// </summary>
|
||||
private void InitializeFloatBarForegroundColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
FloatBarForegroundColor = (Color)Application.Current.FindResource("FloatBarForegroundColor");
|
||||
|
||||
// 强制刷新浮动工具栏按钮颜色
|
||||
RefreshFloatingBarButtonColors();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 如果无法从资源中加载,使用默认颜色
|
||||
FloatBarForegroundColor = Color.FromRgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新快速面板图标
|
||||
/// </summary>
|
||||
private void RefreshQuickPanelIcons()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (LeftUnFoldButtonQuickPanel != null)
|
||||
{
|
||||
LeftUnFoldButtonQuickPanel.InvalidateVisual();
|
||||
}
|
||||
if (RightUnFoldButtonQuickPanel != null)
|
||||
{
|
||||
RightUnFoldButtonQuickPanel.InvalidateVisual();
|
||||
}
|
||||
if (LeftSidePanel != null)
|
||||
{
|
||||
LeftSidePanel.InvalidateVisual();
|
||||
}
|
||||
if (RightSidePanel != null)
|
||||
{
|
||||
RightSidePanel.InvalidateVisual();
|
||||
}
|
||||
LeftUnFoldButtonQuickPanel?.InvalidateVisual();
|
||||
RightUnFoldButtonQuickPanel?.InvalidateVisual();
|
||||
LeftSidePanel?.InvalidateVisual();
|
||||
RightSidePanel?.InvalidateVisual();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新浮动栏高光条颜色
|
||||
/// </summary>
|
||||
private void RefreshFloatingBarHighlightColors()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (FloatingbarSelectionBG != null && FloatingbarSelectionBG.Visibility == Visibility.Visible)
|
||||
{
|
||||
// 根据主题设置高光颜色
|
||||
bool isDarkTheme = IsCurrentThemeDark();
|
||||
|
||||
Color highlightBackgroundColor;
|
||||
Color highlightBarColor;
|
||||
bool isDarkTheme = Settings.Appearance.Theme == 1 ||
|
||||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
|
||||
|
||||
if (isDarkTheme)
|
||||
{
|
||||
@@ -244,7 +133,6 @@ namespace Ink_Canvas
|
||||
highlightBarColor = Color.FromRgb(37, 99, 235);
|
||||
}
|
||||
|
||||
// 设置高光背景颜色
|
||||
FloatingbarSelectionBG.Background = new SolidColorBrush(highlightBackgroundColor);
|
||||
if (FloatingbarSelectionBG.Child is System.Windows.Controls.Canvas canvas && canvas.Children.Count > 0)
|
||||
{
|
||||
@@ -261,73 +149,67 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新浮动工具栏按钮颜色
|
||||
/// </summary>
|
||||
private bool IsCurrentThemeDark()
|
||||
{
|
||||
return Settings.Appearance.Theme == 1 ||
|
||||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
|
||||
}
|
||||
|
||||
private void RefreshFloatingBarButtonColors()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 根据主题选择高光颜色
|
||||
Color selectedColor;
|
||||
bool isDarkTheme = Settings.Appearance.Theme == 1 ||
|
||||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
|
||||
SymbolIconDelete.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.DeleteIcon);
|
||||
ShapeDrawFloatingBarBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ShapesIcon);
|
||||
SymbolIconUndo.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.UndoIcon);
|
||||
SymbolIconRedo.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.RedoIcon);
|
||||
CursorWithDelFloatingBarBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.CursorWithDelFloatingBarBtnIcon);
|
||||
WhiteboardFloatingBarBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.WhiteboardFloatingBarBtnIcon);
|
||||
ToolsFloatingBarBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ToolsFloatingBarBtnIcon);
|
||||
Fold_Icon.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.FoldIcon);
|
||||
|
||||
if (isDarkTheme)
|
||||
{
|
||||
selectedColor = Color.FromRgb(102, 204, 255);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedColor = Color.FromRgb(30, 58, 138);
|
||||
}
|
||||
TimerToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.TimerIconGeometry);
|
||||
RandomDrawToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.RandomDrawIconGeometry);
|
||||
SingleDrawToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SingleDrawIconGeometry);
|
||||
SaveToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SaveIconGeometry);
|
||||
OpenToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.OpenIconGeometry);
|
||||
ReplayToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ReplayIconGeometry);
|
||||
ScreenshotToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ScreenshotIconGeometry);
|
||||
ManualToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ManualIconGeometry);
|
||||
SettingsToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SettingsIconGeometry);
|
||||
|
||||
BoardTimerToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.TimerIconGeometry);
|
||||
BoardRandomDrawToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.RandomDrawIconGeometry);
|
||||
BoardSingleDrawToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SingleDrawIconGeometry);
|
||||
BoardSaveToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SaveIconGeometry);
|
||||
BoardOpenToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.OpenIconGeometry);
|
||||
BoardReplayToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ReplayIconGeometry);
|
||||
BoardScreenshotToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ScreenshotIconGeometry);
|
||||
BoardManualToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.ManualIconGeometry);
|
||||
BoardSettingsToolBtn.Icon.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.SettingsIconGeometry);
|
||||
|
||||
bool isDarkTheme = IsCurrentThemeDark();
|
||||
Color selectedColor = isDarkTheme ? Color.FromRgb(102, 204, 255) : Color.FromRgb(30, 58, 138);
|
||||
|
||||
SetAllFloatingBarButtonsToColor(FloatBarForegroundColor);
|
||||
|
||||
// 根据当前模式设置按钮颜色
|
||||
switch (_currentToolMode)
|
||||
{
|
||||
case "cursor":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
Cursor_Icon.Icon.Brush = new SolidColorBrush(selectedColor);
|
||||
break;
|
||||
case "pen":
|
||||
case "color":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
Pen_Icon.Icon.Brush = new SolidColorBrush(selectedColor);
|
||||
break;
|
||||
case "eraser":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
Eraser_Icon.Icon.Brush = new SolidColorBrush(selectedColor);
|
||||
break;
|
||||
case "eraserByStrokes":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
EraserByStrokes_Icon.Icon.Brush = new SolidColorBrush(selectedColor);
|
||||
break;
|
||||
case "select":
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(selectedColor);
|
||||
break;
|
||||
default:
|
||||
// 默认情况,所有按钮都使用主题颜色
|
||||
CursorIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
PenIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
StrokeEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
CircleEraserIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
LassoSelectIconGeometry.Brush = new SolidColorBrush(FloatBarForegroundColor);
|
||||
SymbolIconSelect.Icon.Brush = new SolidColorBrush(selectedColor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -336,79 +218,60 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理系统主题偏好变化事件,根据当前设置更新应用主题。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者。</param>
|
||||
/// <param name="e">用户偏好变化事件参数。</param>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 根据当前主题设置(Settings.Appearance.Theme)决定使用哪种主题
|
||||
/// 2. 如果设置为0(浅色主题),则设置为Light主题
|
||||
/// 3. 如果设置为1(深色主题),则设置为Dark主题
|
||||
/// 4. 如果设置为2(跟随系统主题),则根据系统主题设置应用相应的主题
|
||||
/// </remarks>
|
||||
void SetAllFloatingBarButtonsToColor(Color color)
|
||||
{
|
||||
var brush = new SolidColorBrush(color);
|
||||
Cursor_Icon.Icon.Brush = brush;
|
||||
Pen_Icon.Icon.Brush = brush;
|
||||
EraserByStrokes_Icon.Icon.Brush = brush;
|
||||
Eraser_Icon.Icon.Brush = brush;
|
||||
SymbolIconSelect.Icon.Brush = brush;
|
||||
ShapeDrawFloatingBarBtn.Icon.Brush = brush;
|
||||
SymbolIconUndo.Icon.Brush = brush;
|
||||
SymbolIconRedo.Icon.Brush = brush;
|
||||
CursorWithDelFloatingBarBtn.Icon.Brush = brush;
|
||||
WhiteboardFloatingBarBtn.Icon.Brush = brush;
|
||||
ToolsFloatingBarBtn.Icon.Brush = brush;
|
||||
Fold_Icon.Icon.Brush = brush;
|
||||
}
|
||||
|
||||
private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
|
||||
{
|
||||
switch (Settings.Appearance.Theme)
|
||||
{
|
||||
case 0:
|
||||
SetTheme("Light");
|
||||
SetTheme(ThemeLight);
|
||||
break;
|
||||
case 1:
|
||||
SetTheme("Dark");
|
||||
SetTheme(ThemeDark);
|
||||
break;
|
||||
case 2:
|
||||
if (IsSystemThemeLight()) SetTheme("Light");
|
||||
else SetTheme("Dark");
|
||||
SetTheme(IsSystemThemeLight() ? ThemeLight : ThemeDark);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查系统主题是否为浅色主题。
|
||||
/// </summary>
|
||||
/// <returns>系统主题为浅色返回true,深色返回false。</returns>
|
||||
/// <remarks>
|
||||
/// 操作包括:
|
||||
/// 1. 从注册表中读取系统主题设置
|
||||
/// 2. 检查"SystemUsesLightTheme"键的值
|
||||
/// 3. 如果值为1,则表示系统使用浅色主题
|
||||
/// 4. 捕获可能的异常,确保方法不会因异常而崩溃
|
||||
/// </remarks>
|
||||
private bool IsSystemThemeLight()
|
||||
{
|
||||
var light = false;
|
||||
try
|
||||
{
|
||||
var registryKey = Registry.CurrentUser;
|
||||
var themeKey =
|
||||
registryKey.OpenSubKey("software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
|
||||
var keyValue = 0;
|
||||
if (themeKey != null) keyValue = (int)themeKey.GetValue("SystemUsesLightTheme");
|
||||
if (keyValue == 1) light = true;
|
||||
var themeKey = registryKey.OpenSubKey("software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
|
||||
if (themeKey != null)
|
||||
{
|
||||
int keyValue = (int)themeKey.GetValue("SystemUsesLightTheme");
|
||||
return keyValue == 1;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
|
||||
|
||||
return light;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据主题自动切换浮动栏图标
|
||||
/// </summary>
|
||||
private void AutoSwitchFloatingBarIconForTheme(string theme)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (theme == "Light")
|
||||
{
|
||||
Settings.Appearance.FloatingBarImg = 0;
|
||||
}
|
||||
else if (theme == "Dark")
|
||||
{
|
||||
Settings.Appearance.FloatingBarImg = 3;
|
||||
}
|
||||
|
||||
Settings.Appearance.FloatingBarImg = theme == ThemeLight ? 0 : 3;
|
||||
UpdateFloatingBarIcon();
|
||||
UpdateFloatingBarIconComboBox();
|
||||
}
|
||||
@@ -417,111 +280,49 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新设置界面中的浮动栏图标选择下拉框显示
|
||||
/// </summary>
|
||||
private void UpdateFloatingBarIconComboBox()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ComboBoxFloatingBarImg != null)
|
||||
{
|
||||
ComboBoxFloatingBarImg.SelectedIndex = Settings.Appearance.FloatingBarImg;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新墨迹选中栏图标
|
||||
/// </summary>
|
||||
private void RefreshStrokeSelectionIcons()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (BorderStrokeSelectionControl != null)
|
||||
{
|
||||
// 强制刷新墨迹选中栏的视觉状态
|
||||
BorderStrokeSelectionControl.InvalidateVisual();
|
||||
|
||||
// 刷新墨迹选中栏内的所有图标
|
||||
var viewbox = BorderStrokeSelectionControl.Child as Viewbox;
|
||||
if (viewbox?.Child is ui.SimpleStackPanel stackPanel)
|
||||
{
|
||||
RefreshStrokeSelectionIconsRecursive(stackPanel);
|
||||
RefreshIconsRecursive(stackPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 忽略异常,确保主题切换不会因为图标刷新失败而中断
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归刷新墨迹选中栏内的图标
|
||||
/// </summary>
|
||||
private void RefreshStrokeSelectionIconsRecursive(System.Windows.Controls.Panel panel)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var child in panel.Children)
|
||||
{
|
||||
if (child is Image image)
|
||||
{
|
||||
// 强制刷新图像
|
||||
image.InvalidateVisual();
|
||||
}
|
||||
else if (child is System.Windows.Controls.Panel childPanel)
|
||||
{
|
||||
// 递归处理子面板
|
||||
RefreshStrokeSelectionIconsRecursive(childPanel);
|
||||
}
|
||||
else if (child is Border border && border.Child is System.Windows.Controls.Panel borderPanel)
|
||||
{
|
||||
// 处理Border内的面板
|
||||
RefreshStrokeSelectionIconsRecursive(borderPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 忽略异常
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新图片选中栏图标
|
||||
/// </summary>
|
||||
private void RefreshImageSelectionIcons()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (BorderImageSelectionControl != null)
|
||||
{
|
||||
// 强制刷新图片选中栏的视觉状态
|
||||
BorderImageSelectionControl.InvalidateVisual();
|
||||
|
||||
// 刷新图片选中栏内的所有图标
|
||||
var viewbox = BorderImageSelectionControl.Child as Viewbox;
|
||||
if (viewbox?.Child is ui.SimpleStackPanel stackPanel)
|
||||
{
|
||||
RefreshImageSelectionIconsRecursive(stackPanel);
|
||||
RefreshIconsRecursive(stackPanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 忽略异常,确保主题切换不会因为图标刷新失败而中断
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归刷新图片选中栏内的图标
|
||||
/// </summary>
|
||||
private void RefreshImageSelectionIconsRecursive(System.Windows.Controls.Panel panel)
|
||||
private void RefreshIconsRecursive(System.Windows.Controls.Panel panel)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -529,22 +330,18 @@ namespace Ink_Canvas
|
||||
{
|
||||
if (child is Image image)
|
||||
{
|
||||
// 强制刷新图像
|
||||
image.InvalidateVisual();
|
||||
}
|
||||
else if (child is System.Windows.Controls.Panel childPanel)
|
||||
{
|
||||
// 递归处理子面板
|
||||
RefreshImageSelectionIconsRecursive(childPanel);
|
||||
RefreshIconsRecursive(childPanel);
|
||||
}
|
||||
else if (child is Border border && border.Child is System.Windows.Controls.Panel borderPanel)
|
||||
{
|
||||
// 处理Border内的面板
|
||||
RefreshImageSelectionIconsRecursive(borderPanel);
|
||||
RefreshIconsRecursive(borderPanel);
|
||||
}
|
||||
else if (child is Grid grid)
|
||||
{
|
||||
// 处理Grid内的子元素
|
||||
foreach (var gridChild in grid.Children)
|
||||
{
|
||||
if (gridChild is Image gridImage)
|
||||
@@ -557,18 +354,13 @@ namespace Ink_Canvas
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 忽略异常
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新手势按钮图标
|
||||
/// </summary>
|
||||
private void RefreshGestureButtonIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 调用手势按钮颜色和图标更新方法,该方法会根据当前主题和手势状态设置正确的图标
|
||||
CheckEnableTwoFingerGestureBtnColorPrompt();
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -576,14 +368,10 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新其他窗口的主题
|
||||
/// </summary>
|
||||
private void RefreshOtherWindowsTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 刷新所有打开的窗口
|
||||
foreach (Window window in Application.Current.Windows)
|
||||
{
|
||||
if (window is CountdownTimerWindow timerWindow)
|
||||
@@ -598,22 +386,18 @@ namespace Ink_Canvas
|
||||
{
|
||||
operatingGuideWindow.RefreshTheme();
|
||||
}
|
||||
else if (window is Windows.SettingsViews.SettingsWindow settingsWindow)
|
||||
{
|
||||
settingsWindow.RefreshTheme();
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新计时器控件
|
||||
if (TimerControl != null)
|
||||
{
|
||||
TimerControl.RefreshTheme();
|
||||
}
|
||||
|
||||
if (MinimizedTimerControl != null)
|
||||
{
|
||||
MinimizedTimerControl.RefreshTheme();
|
||||
}
|
||||
TimerControl?.RefreshTheme();
|
||||
MinimizedTimerControl?.RefreshTheme();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Ink_Canvas.Controls;
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -5,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
@@ -81,7 +83,7 @@ namespace Ink_Canvas
|
||||
var missingElements = 0;
|
||||
foreach (UIElement child in inkCanvas.Children)
|
||||
{
|
||||
if (child is Image || child is MediaElement)
|
||||
if (child is Image || child is MediaElement || child is PdfEmbeddedView)
|
||||
{
|
||||
if (child is Image img && img.Tag is string tag && tag == VideoPresenterLiveFrameTag)
|
||||
{
|
||||
@@ -225,6 +227,7 @@ namespace Ink_Canvas
|
||||
if (TimeMachineHistories[targetIndex] == null)
|
||||
{
|
||||
timeMachine.ClearStrokeHistory();
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -281,7 +284,11 @@ namespace Ink_Canvas
|
||||
/// </summary>
|
||||
private void ProcessElementsAfterRestore(List<UIElement> elements)
|
||||
{
|
||||
if (elements == null || elements.Count == 0) return;
|
||||
if (elements == null || elements.Count == 0)
|
||||
{
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用低优先级异步处理,让 UI 先响应,图片位置和事件绑定稍后完成
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
|
||||
@@ -310,7 +317,19 @@ namespace Ink_Canvas
|
||||
}
|
||||
BindElementEvents(media);
|
||||
}
|
||||
else if (element is PdfEmbeddedView pdf)
|
||||
{
|
||||
double left = InkCanvas.GetLeft(pdf);
|
||||
double top = InkCanvas.GetTop(pdf);
|
||||
if (double.IsNaN(left) || double.IsNaN(top))
|
||||
{
|
||||
CenterAndScaleElement(pdf);
|
||||
}
|
||||
BindElementEvents(pdf);
|
||||
}
|
||||
}
|
||||
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -408,7 +427,10 @@ namespace Ink_Canvas
|
||||
/// <remarks>
|
||||
/// 该方法在切换前会取消当前选中元素(同时保留并恢复编辑模式)、调用视频呈现器的离开页前钩子、保存当前页的笔迹与元素、清空画布;切换到前一页后恢复该页内容、调用视频呈现器的页已更改钩子并刷新页面索引显示。
|
||||
/// </remarks>
|
||||
private void BtnWhiteBoardSwitchPrevious_Click(object sender, EventArgs e)
|
||||
private void BoardBtnWhiteBoardSwitchPrevious_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
=> BtnWhiteBoardSwitchPrevious_Click(sender, e);
|
||||
|
||||
private void BtnWhiteBoardSwitchPrevious_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (CurrentWhiteboardIndex <= 1) return;
|
||||
|
||||
@@ -440,7 +462,10 @@ namespace Ink_Canvas
|
||||
/// </summary>
|
||||
/// <param name="sender">触发事件的源对象(通常为按钮)。</param>
|
||||
/// <param name="e">事件参数。</param>
|
||||
private void BtnWhiteBoardSwitchNext_Click(object sender, EventArgs e)
|
||||
private void BoardBtnWhiteBoardSwitchNext_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
=> BtnWhiteBoardSwitchNext_Click(sender, e);
|
||||
|
||||
private void BtnWhiteBoardSwitchNext_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (CurrentWhiteboardIndex < WhiteboardTotalCount &&
|
||||
Settings.Automation.IsAutoSaveStrokesAtClear &&
|
||||
@@ -487,7 +512,10 @@ namespace Ink_Canvas
|
||||
/// - 将当前页面的历史保存到时间轴并清空画布,然后在白板集合中插入一个空白页面(其历史为 null),随后恢复该页面并触发页面变更回调。
|
||||
/// - 更新页码显示并在达到上限时禁用添加按钮;若侧边页列表可见,则刷新该列表。
|
||||
/// </remarks>
|
||||
private void BtnWhiteBoardAdd_Click(object sender, EventArgs e)
|
||||
private void BoardBtnWhiteBoardAdd_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
=> BtnWhiteBoardAdd_Click(sender, e);
|
||||
|
||||
private void BtnWhiteBoardAdd_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WhiteboardTotalCount >= 99) return;
|
||||
if (Settings.Automation.IsAutoSaveStrokesAtClear &&
|
||||
@@ -634,8 +662,8 @@ namespace Ink_Canvas
|
||||
bool isMaxPage = WhiteboardTotalCount >= 99;
|
||||
|
||||
// 设置按钮文本
|
||||
BtnLeftWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
|
||||
BtnRightWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
|
||||
BtnLeftWhiteBoardSwitchNext.LabelTextBlockControl.Text = isLastPage ? "新页面" : "下一页";
|
||||
BtnRightWhiteBoardSwitchNext.LabelTextBlockControl.Text = isLastPage ? "新页面" : "下一页";
|
||||
|
||||
if (isLastPage)
|
||||
{
|
||||
@@ -652,11 +680,11 @@ namespace Ink_Canvas
|
||||
// 设置下一页按钮颜色
|
||||
if (iconForegroundBrush != null)
|
||||
{
|
||||
BtnLeftWhiteBoardSwitchNextGeometry.Brush = iconForegroundBrush;
|
||||
BtnRightWhiteBoardSwitchNextGeometry.Brush = iconForegroundBrush;
|
||||
BtnLeftWhiteBoardSwitchNext.IconGeometryDrawing.Brush = iconForegroundBrush;
|
||||
BtnRightWhiteBoardSwitchNext.IconGeometryDrawing.Brush = iconForegroundBrush;
|
||||
}
|
||||
BtnLeftWhiteBoardSwitchNextLabel.Opacity = 1;
|
||||
BtnRightWhiteBoardSwitchNextLabel.Opacity = 1;
|
||||
BtnLeftWhiteBoardSwitchNext.LabelTextBlockControl.Opacity = 1;
|
||||
BtnRightWhiteBoardSwitchNext.LabelTextBlockControl.Opacity = 1;
|
||||
|
||||
BtnWhiteBoardSwitchPrevious.IsEnabled = true;
|
||||
|
||||
@@ -666,21 +694,21 @@ namespace Ink_Canvas
|
||||
if (iconForegroundBrush != null)
|
||||
{
|
||||
var disabledBrush = new SolidColorBrush(Color.FromArgb(127, iconForegroundBrush.Color.R, iconForegroundBrush.Color.G, iconForegroundBrush.Color.B));
|
||||
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = disabledBrush;
|
||||
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = disabledBrush;
|
||||
BtnLeftWhiteBoardSwitchPrevious.IconGeometryDrawing.Brush = disabledBrush;
|
||||
BtnRightWhiteBoardSwitchPrevious.IconGeometryDrawing.Brush = disabledBrush;
|
||||
}
|
||||
BtnLeftWhiteBoardSwitchPreviousLabel.Opacity = 0.5;
|
||||
BtnRightWhiteBoardSwitchPreviousLabel.Opacity = 0.5;
|
||||
BtnLeftWhiteBoardSwitchPrevious.LabelTextBlockControl.Opacity = 0.5;
|
||||
BtnRightWhiteBoardSwitchPrevious.LabelTextBlockControl.Opacity = 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (iconForegroundBrush != null)
|
||||
{
|
||||
BtnLeftWhiteBoardSwitchPreviousGeometry.Brush = iconForegroundBrush;
|
||||
BtnRightWhiteBoardSwitchPreviousGeometry.Brush = iconForegroundBrush;
|
||||
BtnLeftWhiteBoardSwitchPrevious.IconGeometryDrawing.Brush = iconForegroundBrush;
|
||||
BtnRightWhiteBoardSwitchPrevious.IconGeometryDrawing.Brush = iconForegroundBrush;
|
||||
}
|
||||
BtnLeftWhiteBoardSwitchPreviousLabel.Opacity = 1;
|
||||
BtnRightWhiteBoardSwitchPreviousLabel.Opacity = 1;
|
||||
BtnLeftWhiteBoardSwitchPrevious.LabelTextBlockControl.Opacity = 1;
|
||||
BtnRightWhiteBoardSwitchPrevious.LabelTextBlockControl.Opacity = 1;
|
||||
}
|
||||
|
||||
BtnWhiteBoardDelete.IsEnabled = WhiteboardTotalCount != 1;
|
||||
|
||||
@@ -3,10 +3,8 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Ink_Canvas
|
||||
{
|
||||
@@ -29,594 +27,159 @@ namespace Ink_Canvas
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
|
||||
// 创建背景选项面板(如果不存在)
|
||||
if (BackgroundPalette == null)
|
||||
if (BackgroundPalette.Visibility == Visibility.Visible)
|
||||
{
|
||||
CreateBackgroundPalette();
|
||||
}
|
||||
|
||||
// 显示或隐藏背景选项面板
|
||||
if (BackgroundPalette != null)
|
||||
{
|
||||
if (BackgroundPalette.Visibility == Visibility.Visible)
|
||||
{
|
||||
// 如果面板已经显示,则隐藏它
|
||||
AnimationsHelper.HideWithSlideAndFade(BackgroundPalette);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 隐藏其他可能显示的面板
|
||||
AnimationsHelper.HideWithSlideAndFade(EraserSizePanel);
|
||||
AnimationsHelper.HideWithSlideAndFade(BorderTools);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
|
||||
AnimationsHelper.HideWithSlideAndFade(PenPalette);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardPenPalette);
|
||||
AnimationsHelper.HideWithSlideAndFade(BorderDrawShape);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardBorderDrawShape);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardEraserSizePanel);
|
||||
AnimationsHelper.HideWithSlideAndFade(TwoFingerGestureBorder);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardTwoFingerGestureBorder);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
|
||||
|
||||
// 显示背景选项面板
|
||||
AnimationsHelper.ShowWithSlideFromBottomAndFade(BackgroundPalette);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 原有的背景切换代码
|
||||
Settings.Canvas.UsingWhiteboard = !Settings.Canvas.UsingWhiteboard;
|
||||
SaveSettingsToFile();
|
||||
if (Settings.Canvas.UsingWhiteboard)
|
||||
{
|
||||
if (inkColor == 5) lastBoardInkColor = 0;
|
||||
ICCWaterMarkDark.Visibility = Visibility.Visible;
|
||||
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
|
||||
|
||||
// 设置为白板默认背景色
|
||||
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
|
||||
|
||||
if (currentMode == 1) // 白板模式
|
||||
{
|
||||
// 设置背景为默认白板背景色
|
||||
GridBackgroundCover.Background = new SolidColorBrush(defaultWhiteboardColor);
|
||||
|
||||
// 更新RGB滑块的值为默认白板背景色
|
||||
if (BackgroundPalette != null && BackgroundPalette.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateRGBSliders(defaultWhiteboardColor);
|
||||
}
|
||||
|
||||
// 更新自定义背景色为默认白板背景色
|
||||
CustomBackgroundColor = defaultWhiteboardColor;
|
||||
|
||||
// 保存到设置
|
||||
string colorHex = $"#{defaultWhiteboardColor.R:X2}{defaultWhiteboardColor.G:X2}{defaultWhiteboardColor.B:X2}";
|
||||
Settings.Canvas.CustomBackgroundColor = colorHex;
|
||||
SaveSettingsToFile();
|
||||
}
|
||||
|
||||
// 设置墨迹颜色为黑色
|
||||
CheckLastColor(0);
|
||||
forceEraser = false;
|
||||
AnimationsHelper.HideWithSlideAndFade(BackgroundPalette);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (inkColor == 0) lastBoardInkColor = 5;
|
||||
ICCWaterMarkWhite.Visibility = Visibility.Visible;
|
||||
ICCWaterMarkDark.Visibility = Visibility.Collapsed;
|
||||
AnimationsHelper.HideWithSlideAndFade(EraserSizePanel);
|
||||
AnimationsHelper.HideWithSlideAndFade(BorderTools);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
|
||||
AnimationsHelper.HideWithSlideAndFade(PenPalette);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardPenPalette);
|
||||
AnimationsHelper.HideWithSlideAndFade(BorderDrawShape);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardBorderDrawShape);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardEraserSizePanel);
|
||||
AnimationsHelper.HideWithSlideAndFade(TwoFingerGestureBorder);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardTwoFingerGestureBorder);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
|
||||
|
||||
// 设置为黑板默认背景色
|
||||
Color defaultBlackboardColor = Color.FromRgb(22, 41, 36);
|
||||
|
||||
if (currentMode == 1) // 黑板模式
|
||||
{
|
||||
// 设置背景为默认黑板背景色
|
||||
GridBackgroundCover.Background = new SolidColorBrush(defaultBlackboardColor);
|
||||
|
||||
// 更新RGB滑块的值为默认黑板背景色
|
||||
if (BackgroundPalette != null && BackgroundPalette.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateRGBSliders(defaultBlackboardColor);
|
||||
}
|
||||
|
||||
// 更新自定义背景色为默认黑板背景色
|
||||
CustomBackgroundColor = defaultBlackboardColor;
|
||||
|
||||
// 保存到设置
|
||||
string colorHex = $"#{defaultBlackboardColor.R:X2}{defaultBlackboardColor.G:X2}{defaultBlackboardColor.B:X2}";
|
||||
Settings.Canvas.CustomBackgroundColor = colorHex;
|
||||
SaveSettingsToFile();
|
||||
}
|
||||
|
||||
// 设置墨迹颜色为白色
|
||||
CheckLastColor(5);
|
||||
forceEraser = false;
|
||||
LoadCustomBackgroundColor();
|
||||
UpdateBackgroundButtonsState();
|
||||
AnimationsHelper.ShowWithSlideFromBottomAndFade(BackgroundPalette);
|
||||
}
|
||||
|
||||
CheckColorTheme(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建背景颜色选项面板
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 加载自定义背景色
|
||||
/// - 创建背景选项面板UI
|
||||
/// - 添加标题栏和关闭按钮
|
||||
/// - 添加白板/黑板模式选择按钮
|
||||
/// - 添加RGB颜色选择器
|
||||
/// - 添加颜色预览和应用按钮
|
||||
/// - 将面板添加到主网格
|
||||
/// </remarks>
|
||||
private void CreateBackgroundPalette()
|
||||
private void WhiteboardModeBtn_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 确保加载自定义背景色
|
||||
LoadCustomBackgroundColor();
|
||||
Settings.Canvas.UsingWhiteboard = true;
|
||||
SaveSettingsToFile();
|
||||
ICCWaterMarkDark.Visibility = Visibility.Visible;
|
||||
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
|
||||
|
||||
// 创建一个类似于PenPalette的面板
|
||||
BackgroundPalette = new Border
|
||||
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
|
||||
|
||||
if (currentMode == 1)
|
||||
{
|
||||
Name = "BackgroundPalette",
|
||||
Visibility = Visibility.Collapsed,
|
||||
Background = (SolidColorBrush)Application.Current.FindResource("SettingsPageBackground"),
|
||||
Opacity = 1,
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Width = 300,
|
||||
MaxHeight = 400
|
||||
};
|
||||
|
||||
// 确保面板显示在顶层
|
||||
Panel.SetZIndex(BackgroundPalette, 1000);
|
||||
|
||||
// 创建面板内容
|
||||
var stackPanel = new StackPanel();
|
||||
|
||||
// 创建标题栏
|
||||
var titleBorder = new Border
|
||||
{
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0x1e, 0x3a, 0x8a)),
|
||||
Height = 32,
|
||||
BorderThickness = new Thickness(0, 0, 0, 1),
|
||||
CornerRadius = new CornerRadius(8, 8, 0, 0),
|
||||
Background = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)),
|
||||
Margin = new Thickness(-1, -1, -1, 0),
|
||||
Padding = new Thickness(1, 1, 1, 0)
|
||||
};
|
||||
|
||||
var titleCanvas = new System.Windows.Controls.Canvas { Height = 24, ClipToBounds = true };
|
||||
var titleText = new TextBlock
|
||||
{
|
||||
Text = "背景设置",
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("FloatBarForeground"),
|
||||
Padding = new Thickness(0, 5, 0, 0),
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.Bold,
|
||||
TextAlignment = TextAlignment.Center
|
||||
};
|
||||
System.Windows.Controls.Canvas.SetLeft(titleText, 8);
|
||||
titleCanvas.Children.Add(titleText);
|
||||
|
||||
// 关闭按钮
|
||||
var closeImage = new Image
|
||||
{
|
||||
Source = new BitmapImage(new Uri("/Resources/new-icons/close-white.png", UriKind.Relative)),
|
||||
Height = 16,
|
||||
Width = 16
|
||||
};
|
||||
RenderOptions.SetBitmapScalingMode(closeImage, BitmapScalingMode.HighQuality);
|
||||
closeImage.MouseUp += CloseBordertools_MouseUp;
|
||||
System.Windows.Controls.Canvas.SetRight(closeImage, 8);
|
||||
System.Windows.Controls.Canvas.SetTop(closeImage, 4);
|
||||
titleCanvas.Children.Add(closeImage);
|
||||
|
||||
titleBorder.Child = titleCanvas;
|
||||
stackPanel.Children.Add(titleBorder);
|
||||
|
||||
// 创建背景选项内容区域
|
||||
var contentPanel = new StackPanel { Margin = new Thickness(8) };
|
||||
|
||||
// 黑板/白板选择
|
||||
var modeTitle = new TextBlock
|
||||
{
|
||||
Text = "白板模式",
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground"),
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(0, 4, 0, 8)
|
||||
};
|
||||
contentPanel.Children.Add(modeTitle);
|
||||
|
||||
var modePanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
|
||||
// 白板按钮
|
||||
var whiteboardButton = new Border
|
||||
{
|
||||
Width = 60,
|
||||
Height = 30,
|
||||
Background = Settings.Canvas.UsingWhiteboard ? new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) : new SolidColorBrush(Colors.LightGray),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Margin = new Thickness(0, 0, 8, 0)
|
||||
};
|
||||
var whiteboardText = new TextBlock
|
||||
{
|
||||
Text = "白板",
|
||||
Foreground = Settings.Canvas.UsingWhiteboard ? new SolidColorBrush(Colors.White) : new SolidColorBrush(Colors.Black),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
whiteboardButton.Child = whiteboardText;
|
||||
whiteboardButton.MouseUp += (s, args) =>
|
||||
{
|
||||
Settings.Canvas.UsingWhiteboard = true;
|
||||
GridBackgroundCover.Background = new SolidColorBrush(defaultWhiteboardColor);
|
||||
UpdateRGBSliders(defaultWhiteboardColor);
|
||||
CustomBackgroundColor = defaultWhiteboardColor;
|
||||
string colorHex = $"#{defaultWhiteboardColor.R:X2}{defaultWhiteboardColor.G:X2}{defaultWhiteboardColor.B:X2}";
|
||||
Settings.Canvas.CustomBackgroundColor = colorHex;
|
||||
SaveSettingsToFile();
|
||||
ICCWaterMarkDark.Visibility = Visibility.Visible;
|
||||
ICCWaterMarkWhite.Visibility = Visibility.Collapsed;
|
||||
|
||||
// 设置为白板默认背景色
|
||||
Color defaultWhiteboardColor = Color.FromRgb(255, 255, 255);
|
||||
|
||||
if (currentMode == 1) // 白板模式
|
||||
{
|
||||
// 设置背景为默认白板背景色
|
||||
GridBackgroundCover.Background = new SolidColorBrush(defaultWhiteboardColor);
|
||||
|
||||
// 更新RGB滑块的值为默认白板背景色
|
||||
UpdateRGBSliders(defaultWhiteboardColor);
|
||||
|
||||
// 更新自定义背景色为默认白板背景色
|
||||
CustomBackgroundColor = defaultWhiteboardColor;
|
||||
|
||||
// 保存到设置
|
||||
string colorHex = $"#{defaultWhiteboardColor.R:X2}{defaultWhiteboardColor.G:X2}{defaultWhiteboardColor.B:X2}";
|
||||
Settings.Canvas.CustomBackgroundColor = colorHex;
|
||||
SaveSettingsToFile();
|
||||
}
|
||||
|
||||
// 设置墨迹颜色为黑色
|
||||
CheckLastColor(0);
|
||||
forceEraser = false;
|
||||
|
||||
CheckColorTheme(true);
|
||||
UpdateBackgroundButtonsState();
|
||||
};
|
||||
modePanel.Children.Add(whiteboardButton);
|
||||
|
||||
// 黑板按钮
|
||||
var blackboardButton = new Border
|
||||
{
|
||||
Width = 60,
|
||||
Height = 30,
|
||||
Background = !Settings.Canvas.UsingWhiteboard ? new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) : new SolidColorBrush(Colors.LightGray),
|
||||
CornerRadius = new CornerRadius(4)
|
||||
};
|
||||
var blackboardText = new TextBlock
|
||||
{
|
||||
Text = "黑板",
|
||||
Foreground = !Settings.Canvas.UsingWhiteboard ? new SolidColorBrush(Colors.White) : new SolidColorBrush(Colors.Black),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
blackboardButton.Child = blackboardText;
|
||||
blackboardButton.MouseUp += (s, args) =>
|
||||
{
|
||||
Settings.Canvas.UsingWhiteboard = false;
|
||||
SaveSettingsToFile();
|
||||
ICCWaterMarkWhite.Visibility = Visibility.Visible;
|
||||
ICCWaterMarkDark.Visibility = Visibility.Collapsed;
|
||||
|
||||
// 设置为黑板默认背景色
|
||||
Color defaultBlackboardColor = Color.FromRgb(22, 41, 36);
|
||||
|
||||
if (currentMode == 1) // 黑板模式
|
||||
{
|
||||
// 设置背景为默认黑板背景色
|
||||
GridBackgroundCover.Background = new SolidColorBrush(defaultBlackboardColor);
|
||||
|
||||
// 更新RGB滑块的值为默认黑板背景色
|
||||
UpdateRGBSliders(defaultBlackboardColor);
|
||||
|
||||
// 更新自定义背景色为默认黑板背景色
|
||||
CustomBackgroundColor = defaultBlackboardColor;
|
||||
|
||||
// 保存到设置
|
||||
string colorHex = $"#{defaultBlackboardColor.R:X2}{defaultBlackboardColor.G:X2}{defaultBlackboardColor.B:X2}";
|
||||
Settings.Canvas.CustomBackgroundColor = colorHex;
|
||||
SaveSettingsToFile();
|
||||
}
|
||||
|
||||
// 设置墨迹颜色为白色
|
||||
CheckLastColor(5);
|
||||
forceEraser = false;
|
||||
|
||||
CheckColorTheme(true);
|
||||
UpdateBackgroundButtonsState();
|
||||
};
|
||||
modePanel.Children.Add(blackboardButton);
|
||||
|
||||
contentPanel.Children.Add(modePanel);
|
||||
|
||||
// 添加一条分隔线
|
||||
var separator = new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = (SolidColorBrush)Application.Current.FindResource("SettingsPageBorderBrush"),
|
||||
Margin = new Thickness(0, 12, 0, 12)
|
||||
};
|
||||
contentPanel.Children.Add(separator);
|
||||
|
||||
// 添加RGB颜色选择器部分
|
||||
var colorTitle = new TextBlock
|
||||
{
|
||||
Text = "背景颜色",
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground"),
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(0, 4, 0, 8)
|
||||
};
|
||||
contentPanel.Children.Add(colorTitle);
|
||||
|
||||
// 创建颜色预览
|
||||
Border colorPreview = new Border
|
||||
{
|
||||
Width = 100,
|
||||
Height = 40,
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = (SolidColorBrush)Application.Current.FindResource("SettingsPageBorderBrush"),
|
||||
Background = new SolidColorBrush(Colors.White),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
contentPanel.Children.Add(colorPreview);
|
||||
|
||||
// 获取当前背景颜色
|
||||
Color currentBackgroundColor;
|
||||
if (currentMode == 1) // 白板或黑板模式
|
||||
{
|
||||
if (GridBackgroundCover.Background is SolidColorBrush brush)
|
||||
{
|
||||
currentBackgroundColor = brush.Color;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认颜色
|
||||
currentBackgroundColor = Settings.Canvas.UsingWhiteboard ?
|
||||
Color.FromRgb(234, 235, 237) : // 白板默认颜色
|
||||
Color.FromRgb(22, 41, 36); // 黑板默认颜色
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认白色
|
||||
currentBackgroundColor = Colors.White;
|
||||
}
|
||||
|
||||
// 更新颜色预览
|
||||
colorPreview.Background = new SolidColorBrush(currentBackgroundColor);
|
||||
CheckLastColor(0);
|
||||
forceEraser = false;
|
||||
CheckColorTheme(true);
|
||||
UpdateBackgroundButtonsState();
|
||||
}
|
||||
|
||||
// 先创建所有滑块控件
|
||||
// R滑块和文本框
|
||||
var rPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 0, 10, 5) };
|
||||
var rLabel = new TextBlock { Text = "R:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
|
||||
var rSlider = new Slider
|
||||
private void BlackboardModeBtn_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
Settings.Canvas.UsingWhiteboard = false;
|
||||
SaveSettingsToFile();
|
||||
ICCWaterMarkWhite.Visibility = Visibility.Visible;
|
||||
ICCWaterMarkDark.Visibility = Visibility.Collapsed;
|
||||
|
||||
Color defaultBlackboardColor = Color.FromRgb(22, 41, 36);
|
||||
|
||||
if (currentMode == 1)
|
||||
{
|
||||
Minimum = 0,
|
||||
Maximum = 255,
|
||||
Value = currentBackgroundColor.R,
|
||||
Width = 150,
|
||||
Margin = new Thickness(5, 0, 5, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
var rValueText = new TextBlock
|
||||
GridBackgroundCover.Background = new SolidColorBrush(defaultBlackboardColor);
|
||||
UpdateRGBSliders(defaultBlackboardColor);
|
||||
CustomBackgroundColor = defaultBlackboardColor;
|
||||
string colorHex = $"#{defaultBlackboardColor.R:X2}{defaultBlackboardColor.G:X2}{defaultBlackboardColor.B:X2}";
|
||||
Settings.Canvas.CustomBackgroundColor = colorHex;
|
||||
SaveSettingsToFile();
|
||||
}
|
||||
|
||||
CheckLastColor(5);
|
||||
forceEraser = false;
|
||||
CheckColorTheme(true);
|
||||
UpdateBackgroundButtonsState();
|
||||
}
|
||||
|
||||
private void BackgroundRSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (BackgroundRValue != null)
|
||||
{
|
||||
Text = currentBackgroundColor.R.ToString(),
|
||||
Width = 30,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
|
||||
};
|
||||
|
||||
// G滑块和文本框
|
||||
var gPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 0, 10, 5) };
|
||||
var gLabel = new TextBlock { Text = "G:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
|
||||
var gSlider = new Slider
|
||||
{
|
||||
Minimum = 0,
|
||||
Maximum = 255,
|
||||
Value = currentBackgroundColor.G,
|
||||
Width = 150,
|
||||
Margin = new Thickness(5, 0, 5, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
var gValueText = new TextBlock
|
||||
{
|
||||
Text = currentBackgroundColor.G.ToString(),
|
||||
Width = 30,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
|
||||
};
|
||||
|
||||
// B滑块和文本框
|
||||
var bPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 0, 10, 5) };
|
||||
var bLabel = new TextBlock { Text = "B:", Width = 20, VerticalAlignment = VerticalAlignment.Center, Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground") };
|
||||
var bSlider = new Slider
|
||||
{
|
||||
Minimum = 0,
|
||||
Maximum = 255,
|
||||
Value = currentBackgroundColor.B,
|
||||
Width = 150,
|
||||
Margin = new Thickness(5, 0, 5, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
var bValueText = new TextBlock
|
||||
{
|
||||
Text = currentBackgroundColor.B.ToString(),
|
||||
Width = 30,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
Foreground = (SolidColorBrush)Application.Current.FindResource("TextForeground")
|
||||
};
|
||||
|
||||
// 现在添加事件处理程序
|
||||
rSlider.ValueChanged += (s, e) =>
|
||||
{
|
||||
int value = (int)e.NewValue;
|
||||
rValueText.Text = value.ToString();
|
||||
UpdateColorPreview(colorPreview, rSlider, gSlider, bSlider);
|
||||
};
|
||||
|
||||
gSlider.ValueChanged += (s, e) =>
|
||||
{
|
||||
int value = (int)e.NewValue;
|
||||
gValueText.Text = value.ToString();
|
||||
UpdateColorPreview(colorPreview, rSlider, gSlider, bSlider);
|
||||
};
|
||||
|
||||
bSlider.ValueChanged += (s, e) =>
|
||||
{
|
||||
int value = (int)e.NewValue;
|
||||
bValueText.Text = value.ToString();
|
||||
UpdateColorPreview(colorPreview, rSlider, gSlider, bSlider);
|
||||
};
|
||||
|
||||
// 添加控件到面板
|
||||
rPanel.Children.Add(rLabel);
|
||||
rPanel.Children.Add(rSlider);
|
||||
rPanel.Children.Add(rValueText);
|
||||
contentPanel.Children.Add(rPanel);
|
||||
|
||||
gPanel.Children.Add(gLabel);
|
||||
gPanel.Children.Add(gSlider);
|
||||
gPanel.Children.Add(gValueText);
|
||||
contentPanel.Children.Add(gPanel);
|
||||
|
||||
bPanel.Children.Add(bLabel);
|
||||
bPanel.Children.Add(bSlider);
|
||||
bPanel.Children.Add(bValueText);
|
||||
contentPanel.Children.Add(bPanel);
|
||||
|
||||
// 应用按钮
|
||||
var applyButton = new Button
|
||||
{
|
||||
Content = "应用颜色",
|
||||
Margin = new Thickness(0, 10, 0, 0),
|
||||
Padding = new Thickness(10, 5, 10, 5),
|
||||
Background = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)),
|
||||
Foreground = new SolidColorBrush(Colors.White),
|
||||
BorderThickness = new Thickness(0),
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
applyButton.Click += (s, e) =>
|
||||
{
|
||||
Color selectedColor = Color.FromRgb(
|
||||
(byte)rSlider.Value,
|
||||
(byte)gSlider.Value,
|
||||
(byte)bSlider.Value
|
||||
);
|
||||
ApplyCustomBackgroundColor(selectedColor);
|
||||
};
|
||||
|
||||
contentPanel.Children.Add(applyButton);
|
||||
|
||||
stackPanel.Children.Add(contentPanel);
|
||||
|
||||
// 将面板添加到父容器
|
||||
BackgroundPalette.Child = stackPanel;
|
||||
|
||||
// 获取主窗口中的根网格,确保面板添加到顶层
|
||||
Grid mainGrid = FindName("Main_Grid") as Grid;
|
||||
if (mainGrid != null)
|
||||
{
|
||||
// 删除可能已存在的BackgroundPalette
|
||||
foreach (UIElement element in mainGrid.Children)
|
||||
{
|
||||
if (element is Border border && border.Name == "BackgroundPalette")
|
||||
{
|
||||
mainGrid.Children.Remove(border);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 重新定位面板
|
||||
BackgroundPalette.HorizontalAlignment = HorizontalAlignment.Center;
|
||||
BackgroundPalette.VerticalAlignment = VerticalAlignment.Center;
|
||||
BackgroundPalette.Margin = new Thickness(0, 0, 0, 0);
|
||||
|
||||
// 添加到主网格
|
||||
mainGrid.Children.Add(BackgroundPalette);
|
||||
|
||||
// 设置面板位置
|
||||
var clickElement = FindName("BoardChangeBackgroundColorBtn") as FrameworkElement;
|
||||
if (clickElement != null)
|
||||
{
|
||||
Point position = clickElement.TranslatePoint(new Point(0, 0), mainGrid);
|
||||
BackgroundPalette.Margin = new Thickness(
|
||||
position.X - 150,
|
||||
position.Y + clickElement.ActualHeight + 5,
|
||||
0, 0);
|
||||
BackgroundPalette.HorizontalAlignment = HorizontalAlignment.Left;
|
||||
BackgroundPalette.VerticalAlignment = VerticalAlignment.Top;
|
||||
}
|
||||
BackgroundRValue.Text = ((int)e.NewValue).ToString();
|
||||
UpdateColorPreviewFromSliders();
|
||||
}
|
||||
}
|
||||
|
||||
private void BackgroundGSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (BackgroundGValue != null)
|
||||
{
|
||||
BackgroundGValue.Text = ((int)e.NewValue).ToString();
|
||||
UpdateColorPreviewFromSliders();
|
||||
}
|
||||
}
|
||||
|
||||
private void BackgroundBSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (BackgroundBValue != null)
|
||||
{
|
||||
BackgroundBValue.Text = ((int)e.NewValue).ToString();
|
||||
UpdateColorPreviewFromSliders();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyBackgroundColorBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Color selectedColor = Color.FromRgb(
|
||||
(byte)BackgroundRSlider.Value,
|
||||
(byte)BackgroundGSlider.Value,
|
||||
(byte)BackgroundBSlider.Value
|
||||
);
|
||||
ApplyCustomBackgroundColor(selectedColor);
|
||||
}
|
||||
|
||||
private void UpdateColorPreviewFromSliders()
|
||||
{
|
||||
if (BackgroundColorPreview != null)
|
||||
{
|
||||
Color previewColor = Color.FromRgb(
|
||||
(byte)BackgroundRSlider.Value,
|
||||
(byte)BackgroundGSlider.Value,
|
||||
(byte)BackgroundBSlider.Value
|
||||
);
|
||||
BackgroundColorPreview.Background = new SolidColorBrush(previewColor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新背景颜色选项面板中的按钮状态
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// - 更新白板和黑板按钮的背景和前景色
|
||||
/// - 根据当前使用的模式设置按钮状态
|
||||
/// </remarks>
|
||||
private void UpdateBackgroundButtonsState()
|
||||
{
|
||||
if (BackgroundPalette != null && BackgroundPalette.Child is StackPanel stackPanel)
|
||||
if (WhiteboardModeBtn != null)
|
||||
{
|
||||
if (stackPanel.Children.Count > 1 && stackPanel.Children[1] is StackPanel contentPanel)
|
||||
WhiteboardModeBtn.Background = Settings.Canvas.UsingWhiteboard ?
|
||||
new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) :
|
||||
new SolidColorBrush(Colors.LightGray);
|
||||
if (WhiteboardModeBtn.Child is TextBlock whiteboardText)
|
||||
{
|
||||
if (contentPanel.Children.Count > 1 && contentPanel.Children[1] is StackPanel modePanel)
|
||||
{
|
||||
if (modePanel.Children.Count > 1)
|
||||
{
|
||||
var whiteboardButton = modePanel.Children[0] as Border;
|
||||
var blackboardButton = modePanel.Children[1] as Border;
|
||||
whiteboardText.Foreground = Settings.Canvas.UsingWhiteboard ?
|
||||
new SolidColorBrush(Colors.White) :
|
||||
new SolidColorBrush(Colors.Black);
|
||||
}
|
||||
}
|
||||
|
||||
if (whiteboardButton != null && whiteboardButton.Child is TextBlock whiteboardText)
|
||||
{
|
||||
whiteboardButton.Background = Settings.Canvas.UsingWhiteboard ?
|
||||
new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) :
|
||||
new SolidColorBrush(Colors.LightGray);
|
||||
whiteboardText.Foreground = Settings.Canvas.UsingWhiteboard ?
|
||||
new SolidColorBrush(Colors.White) :
|
||||
new SolidColorBrush(Colors.Black);
|
||||
}
|
||||
|
||||
if (blackboardButton != null && blackboardButton.Child is TextBlock blackboardText)
|
||||
{
|
||||
blackboardButton.Background = !Settings.Canvas.UsingWhiteboard ?
|
||||
new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) :
|
||||
new SolidColorBrush(Colors.LightGray);
|
||||
blackboardText.Foreground = !Settings.Canvas.UsingWhiteboard ?
|
||||
new SolidColorBrush(Colors.White) :
|
||||
new SolidColorBrush(Colors.Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (BlackboardModeBtn != null)
|
||||
{
|
||||
BlackboardModeBtn.Background = !Settings.Canvas.UsingWhiteboard ?
|
||||
new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xeb)) :
|
||||
new SolidColorBrush(Colors.LightGray);
|
||||
if (BlackboardModeBtn.Child is TextBlock blackboardText)
|
||||
{
|
||||
blackboardText.Foreground = !Settings.Canvas.UsingWhiteboard ?
|
||||
new SolidColorBrush(Colors.White) :
|
||||
new SolidColorBrush(Colors.Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色选项面板
|
||||
/// </summary>
|
||||
private Border BackgroundPalette { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前自定义背景色
|
||||
/// </summary>
|
||||
@@ -767,41 +330,7 @@ namespace Ink_Canvas
|
||||
/// - 启用橡皮擦模式
|
||||
/// - 设置橡皮擦形状为圆形
|
||||
/// - 设置当前工具模式为按笔画擦除
|
||||
/// - 禁用形状绘制模式
|
||||
/// - 重置钢笔类型和属性
|
||||
/// - 触发编辑模式变更事件
|
||||
/// - 取消单指拖动模式
|
||||
/// - 隐藏子面板
|
||||
/// </remarks>
|
||||
private void BoardEraserIconByStrokes_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
//if (BoardEraserByStrokes.Background.ToString() == "#FF679CF4") {
|
||||
// AnimationsHelper.ShowWithSlideFromBottomAndFade(BoardDeleteIcon);
|
||||
//}
|
||||
//else {
|
||||
// 禁用高级橡皮擦系统
|
||||
DisableEraserOverlay();
|
||||
|
||||
forceEraser = true;
|
||||
forcePointEraser = false;
|
||||
|
||||
inkCanvas.EraserShape = new EllipseStylusShape(5, 5);
|
||||
// 使用集中化的工具模式切换方法
|
||||
SetCurrentToolMode(InkCanvasEditingMode.EraseByStroke);
|
||||
drawingShapeMode = 0;
|
||||
|
||||
penType = 0;
|
||||
drawingAttributes.IsHighlighter = false;
|
||||
drawingAttributes.StylusTip = StylusTip.Ellipse;
|
||||
|
||||
inkCanvas_EditingModeChanged(inkCanvas, null);
|
||||
CancelSingleFingerDragMode();
|
||||
|
||||
HideSubPanels("eraserByStrokes");
|
||||
//}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理删除图标点击事件,清空画布内容
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
@@ -913,53 +442,9 @@ namespace Ink_Canvas
|
||||
/// </summary>
|
||||
private void UpdateRGBSliders(Color color)
|
||||
{
|
||||
if (BackgroundPalette != null && BackgroundPalette.Child is StackPanel stackPanel)
|
||||
{
|
||||
if (stackPanel.Children.Count > 1 && stackPanel.Children[1] is StackPanel contentPanel)
|
||||
{
|
||||
// 查找RGB滑块
|
||||
Slider rSlider = null;
|
||||
Slider gSlider = null;
|
||||
Slider bSlider = null;
|
||||
|
||||
// 遍历面板查找RGB滑块
|
||||
foreach (var child in contentPanel.Children)
|
||||
{
|
||||
if (child is StackPanel panel && panel.Orientation == Orientation.Horizontal)
|
||||
{
|
||||
foreach (var panelChild in panel.Children)
|
||||
{
|
||||
if (panelChild is Slider slider)
|
||||
{
|
||||
if (panel.Children.Count > 0 && panel.Children[0] is TextBlock label)
|
||||
{
|
||||
if (label.Text == "R:")
|
||||
{
|
||||
rSlider = slider;
|
||||
}
|
||||
else if (label.Text == "G:")
|
||||
{
|
||||
gSlider = slider;
|
||||
}
|
||||
else if (label.Text == "B:")
|
||||
{
|
||||
bSlider = slider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新滑块值
|
||||
if (rSlider != null && gSlider != null && bSlider != null)
|
||||
{
|
||||
rSlider.Value = color.R;
|
||||
gSlider.Value = color.G;
|
||||
bSlider.Value = color.B;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (BackgroundRSlider != null) BackgroundRSlider.Value = color.R;
|
||||
if (BackgroundGSlider != null) BackgroundGSlider.Value = color.G;
|
||||
if (BackgroundBSlider != null) BackgroundBSlider.Value = color.B;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Ink_Canvas
|
||||
@@ -52,6 +50,7 @@ namespace Ink_Canvas
|
||||
AnimationsHelper.HideWithSlideAndFade(TwoFingerGestureBorder);
|
||||
AnimationsHelper.HideWithSlideAndFade(BoardTwoFingerGestureBorder);
|
||||
EnableTwoFingerGestureBorder.Visibility = Visibility.Collapsed;
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
}
|
||||
|
||||
BtnHideInkCanvas_Click(BtnHideInkCanvas, null);
|
||||
@@ -282,26 +281,26 @@ namespace Ink_Canvas
|
||||
{
|
||||
// 亮系
|
||||
// 亮色的红色
|
||||
BorderPenColorRed.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68));
|
||||
BoardBorderPenColorRed.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68));
|
||||
BorderPenColorRed.Color = Color.FromRgb(239, 68, 68);
|
||||
BoardBorderPenColorRed.Color = Color.FromRgb(239, 68, 68);
|
||||
// 亮色的绿色
|
||||
BorderPenColorGreen.Background = new SolidColorBrush(Color.FromRgb(34, 197, 94));
|
||||
BoardBorderPenColorGreen.Background = new SolidColorBrush(Color.FromRgb(34, 197, 94));
|
||||
BorderPenColorGreen.Color = Color.FromRgb(34, 197, 94);
|
||||
BoardBorderPenColorGreen.Color = Color.FromRgb(34, 197, 94);
|
||||
// 亮色的蓝色
|
||||
BorderPenColorBlue.Background = new SolidColorBrush(Color.FromRgb(59, 130, 246));
|
||||
BoardBorderPenColorBlue.Background = new SolidColorBrush(Color.FromRgb(59, 130, 246));
|
||||
BorderPenColorBlue.Color = Color.FromRgb(59, 130, 246);
|
||||
BoardBorderPenColorBlue.Color = Color.FromRgb(59, 130, 246);
|
||||
// 亮色的黄色
|
||||
BorderPenColorYellow.Background = new SolidColorBrush(Color.FromRgb(250, 204, 21));
|
||||
BoardBorderPenColorYellow.Background = new SolidColorBrush(Color.FromRgb(250, 204, 21));
|
||||
BorderPenColorYellow.Color = Color.FromRgb(250, 204, 21);
|
||||
BoardBorderPenColorYellow.Color = Color.FromRgb(250, 204, 21);
|
||||
// 亮色的粉色
|
||||
BorderPenColorPink.Background = new SolidColorBrush(Color.FromRgb(236, 72, 153));
|
||||
BoardBorderPenColorPink.Background = new SolidColorBrush(Color.FromRgb(236, 72, 153));
|
||||
BorderPenColorPink.Color = Color.FromRgb(236, 72, 153);
|
||||
BoardBorderPenColorPink.Color = Color.FromRgb(236, 72, 153);
|
||||
// 亮色的Teal
|
||||
BorderPenColorTeal.Background = new SolidColorBrush(Color.FromRgb(20, 184, 166));
|
||||
BoardBorderPenColorTeal.Background = new SolidColorBrush(Color.FromRgb(20, 184, 166));
|
||||
BorderPenColorTeal.Color = Color.FromRgb(20, 184, 166);
|
||||
BoardBorderPenColorTeal.Color = Color.FromRgb(20, 184, 166);
|
||||
// 亮色的Orange
|
||||
BorderPenColorOrange.Background = new SolidColorBrush(Color.FromRgb(249, 115, 22));
|
||||
BoardBorderPenColorOrange.Background = new SolidColorBrush(Color.FromRgb(249, 115, 22));
|
||||
BorderPenColorOrange.Color = Color.FromRgb(249, 115, 22);
|
||||
BoardBorderPenColorOrange.Color = Color.FromRgb(249, 115, 22);
|
||||
|
||||
var newImageSource = new BitmapImage();
|
||||
newImageSource.BeginInit();
|
||||
@@ -318,26 +317,26 @@ namespace Ink_Canvas
|
||||
{
|
||||
// 暗系
|
||||
// 暗色的红色
|
||||
BorderPenColorRed.Background = new SolidColorBrush(Color.FromRgb(220, 38, 38));
|
||||
BoardBorderPenColorRed.Background = new SolidColorBrush(Color.FromRgb(220, 38, 38));
|
||||
BorderPenColorRed.Color = Color.FromRgb(220, 38, 38);
|
||||
BoardBorderPenColorRed.Color = Color.FromRgb(220, 38, 38);
|
||||
// 暗色的绿色
|
||||
BorderPenColorGreen.Background = new SolidColorBrush(Color.FromRgb(22, 163, 74));
|
||||
BoardBorderPenColorGreen.Background = new SolidColorBrush(Color.FromRgb(22, 163, 74));
|
||||
BorderPenColorGreen.Color = Color.FromRgb(22, 163, 74);
|
||||
BoardBorderPenColorGreen.Color = Color.FromRgb(22, 163, 74);
|
||||
// 暗色的蓝色
|
||||
BorderPenColorBlue.Background = new SolidColorBrush(Color.FromRgb(37, 99, 235));
|
||||
BoardBorderPenColorBlue.Background = new SolidColorBrush(Color.FromRgb(37, 99, 235));
|
||||
BorderPenColorBlue.Color = Color.FromRgb(37, 99, 235);
|
||||
BoardBorderPenColorBlue.Color = Color.FromRgb(37, 99, 235);
|
||||
// 暗色的黄色
|
||||
BorderPenColorYellow.Background = new SolidColorBrush(Color.FromRgb(234, 179, 8));
|
||||
BoardBorderPenColorYellow.Background = new SolidColorBrush(Color.FromRgb(234, 179, 8));
|
||||
BorderPenColorYellow.Color = Color.FromRgb(234, 179, 8);
|
||||
BoardBorderPenColorYellow.Color = Color.FromRgb(234, 179, 8);
|
||||
// 暗色的紫色对应亮色的粉色
|
||||
BorderPenColorPink.Background = new SolidColorBrush(Color.FromRgb(147, 51, 234));
|
||||
BoardBorderPenColorPink.Background = new SolidColorBrush(Color.FromRgb(147, 51, 234));
|
||||
BorderPenColorPink.Color = Color.FromRgb(147, 51, 234);
|
||||
BoardBorderPenColorPink.Color = Color.FromRgb(147, 51, 234);
|
||||
// 暗色的Teal
|
||||
BorderPenColorTeal.Background = new SolidColorBrush(Color.FromRgb(13, 148, 136));
|
||||
BoardBorderPenColorTeal.Background = new SolidColorBrush(Color.FromRgb(13, 148, 136));
|
||||
BorderPenColorTeal.Color = Color.FromRgb(13, 148, 136);
|
||||
BoardBorderPenColorTeal.Color = Color.FromRgb(13, 148, 136);
|
||||
// 暗色的Orange
|
||||
BorderPenColorOrange.Background = new SolidColorBrush(Color.FromRgb(234, 88, 12));
|
||||
BoardBorderPenColorOrange.Background = new SolidColorBrush(Color.FromRgb(234, 88, 12));
|
||||
BorderPenColorOrange.Color = Color.FromRgb(234, 88, 12);
|
||||
BoardBorderPenColorOrange.Color = Color.FromRgb(234, 88, 12);
|
||||
|
||||
var newImageSource = new BitmapImage();
|
||||
newImageSource.BeginInit();
|
||||
@@ -352,127 +351,129 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
// 改变选中提示
|
||||
ViewboxBtnColorBlackContent.Visibility = Visibility.Collapsed;
|
||||
ViewboxBtnColorBlueContent.Visibility = Visibility.Collapsed;
|
||||
ViewboxBtnColorGreenContent.Visibility = Visibility.Collapsed;
|
||||
ViewboxBtnColorRedContent.Visibility = Visibility.Collapsed;
|
||||
ViewboxBtnColorYellowContent.Visibility = Visibility.Collapsed;
|
||||
ViewboxBtnColorWhiteContent.Visibility = Visibility.Collapsed;
|
||||
ViewboxBtnColorPinkContent.Visibility = Visibility.Collapsed;
|
||||
ViewboxBtnColorTealContent.Visibility = Visibility.Collapsed;
|
||||
ViewboxBtnColorOrangeContent.Visibility = Visibility.Collapsed;
|
||||
BorderPenColorBlack.IsChecked = false;
|
||||
BorderPenColorBlue.IsChecked = false;
|
||||
BorderPenColorGreen.IsChecked = false;
|
||||
BorderPenColorRed.IsChecked = false;
|
||||
BorderPenColorYellow.IsChecked = false;
|
||||
BorderPenColorWhite.IsChecked = false;
|
||||
BorderPenColorPink.IsChecked = false;
|
||||
BorderPenColorTeal.IsChecked = false;
|
||||
BorderPenColorOrange.IsChecked = false;
|
||||
|
||||
BoardViewboxBtnColorBlackContent.Visibility = Visibility.Collapsed;
|
||||
BoardViewboxBtnColorBlueContent.Visibility = Visibility.Collapsed;
|
||||
BoardViewboxBtnColorGreenContent.Visibility = Visibility.Collapsed;
|
||||
BoardViewboxBtnColorRedContent.Visibility = Visibility.Collapsed;
|
||||
BoardViewboxBtnColorYellowContent.Visibility = Visibility.Collapsed;
|
||||
BoardViewboxBtnColorWhiteContent.Visibility = Visibility.Collapsed;
|
||||
BoardViewboxBtnColorPinkContent.Visibility = Visibility.Collapsed;
|
||||
BoardViewboxBtnColorTealContent.Visibility = Visibility.Collapsed;
|
||||
BoardViewboxBtnColorOrangeContent.Visibility = Visibility.Collapsed;
|
||||
BoardBorderPenColorBlack.IsChecked = false;
|
||||
BoardBorderPenColorBlue.IsChecked = false;
|
||||
BoardBorderPenColorGreen.IsChecked = false;
|
||||
BoardBorderPenColorRed.IsChecked = false;
|
||||
BoardBorderPenColorYellow.IsChecked = false;
|
||||
BoardBorderPenColorWhite.IsChecked = false;
|
||||
BoardBorderPenColorPink.IsChecked = false;
|
||||
BoardBorderPenColorTeal.IsChecked = false;
|
||||
BoardBorderPenColorOrange.IsChecked = false;
|
||||
|
||||
HighlighterPenViewboxBtnColorBlackContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenViewboxBtnColorBlueContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenViewboxBtnColorGreenContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenViewboxBtnColorOrangeContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenViewboxBtnColorPurpleContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenViewboxBtnColorRedContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenViewboxBtnColorTealContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenViewboxBtnColorWhiteContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenViewboxBtnColorYellowContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenViewboxBtnColorZincContent.Visibility = Visibility.Collapsed;
|
||||
HighlighterPenColorBlack.IsChecked = false;
|
||||
HighlighterPenColorBlue.IsChecked = false;
|
||||
HighlighterPenColorGreen.IsChecked = false;
|
||||
HighlighterPenColorOrange.IsChecked = false;
|
||||
HighlighterPenPenColorPurple.IsChecked = false;
|
||||
HighlighterPenColorRed.IsChecked = false;
|
||||
HighlighterPenColorTeal.IsChecked = false;
|
||||
HighlighterPenColorWhite.IsChecked = false;
|
||||
HighlighterPenColorYellow.IsChecked = false;
|
||||
HighlighterPenColorZinc.IsChecked = false;
|
||||
|
||||
BoardHighlighterPenViewboxBtnColorBlackContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenViewboxBtnColorBlueContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenViewboxBtnColorGreenContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenViewboxBtnColorOrangeContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenViewboxBtnColorPurpleContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenViewboxBtnColorRedContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenViewboxBtnColorTealContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenViewboxBtnColorWhiteContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenViewboxBtnColorYellowContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenViewboxBtnColorZincContent.Visibility = Visibility.Collapsed;
|
||||
BoardHighlighterPenColorBlack.IsChecked = false;
|
||||
BoardHighlighterPenColorBlue.IsChecked = false;
|
||||
BoardHighlighterPenColorGreen.IsChecked = false;
|
||||
BoardHighlighterPenColorOrange.IsChecked = false;
|
||||
BoardHighlighterPenPenColorPurple.IsChecked = false;
|
||||
BoardHighlighterPenColorRed.IsChecked = false;
|
||||
BoardHighlighterPenColorTeal.IsChecked = false;
|
||||
BoardHighlighterPenColorWhite.IsChecked = false;
|
||||
BoardHighlighterPenColorYellow.IsChecked = false;
|
||||
BoardHighlighterPenColorZinc.IsChecked = false;
|
||||
|
||||
switch (inkColor)
|
||||
{
|
||||
case 0:
|
||||
ViewboxBtnColorBlackContent.Visibility = Visibility.Visible;
|
||||
BoardViewboxBtnColorBlackContent.Visibility = Visibility.Visible;
|
||||
BorderPenColorBlack.IsChecked = true;
|
||||
BoardBorderPenColorBlack.IsChecked = true;
|
||||
break;
|
||||
case 1:
|
||||
ViewboxBtnColorRedContent.Visibility = Visibility.Visible;
|
||||
BoardViewboxBtnColorRedContent.Visibility = Visibility.Visible;
|
||||
BorderPenColorRed.IsChecked = true;
|
||||
BoardBorderPenColorRed.IsChecked = true;
|
||||
break;
|
||||
case 2:
|
||||
ViewboxBtnColorGreenContent.Visibility = Visibility.Visible;
|
||||
BoardViewboxBtnColorGreenContent.Visibility = Visibility.Visible;
|
||||
BorderPenColorGreen.IsChecked = true;
|
||||
BoardBorderPenColorGreen.IsChecked = true;
|
||||
break;
|
||||
case 3:
|
||||
ViewboxBtnColorBlueContent.Visibility = Visibility.Visible;
|
||||
BoardViewboxBtnColorBlueContent.Visibility = Visibility.Visible;
|
||||
BorderPenColorBlue.IsChecked = true;
|
||||
BoardBorderPenColorBlue.IsChecked = true;
|
||||
break;
|
||||
case 4:
|
||||
ViewboxBtnColorYellowContent.Visibility = Visibility.Visible;
|
||||
BoardViewboxBtnColorYellowContent.Visibility = Visibility.Visible;
|
||||
BorderPenColorYellow.IsChecked = true;
|
||||
BoardBorderPenColorYellow.IsChecked = true;
|
||||
break;
|
||||
case 5:
|
||||
ViewboxBtnColorWhiteContent.Visibility = Visibility.Visible;
|
||||
BoardViewboxBtnColorWhiteContent.Visibility = Visibility.Visible;
|
||||
BorderPenColorWhite.IsChecked = true;
|
||||
BoardBorderPenColorWhite.IsChecked = true;
|
||||
break;
|
||||
case 6:
|
||||
ViewboxBtnColorPinkContent.Visibility = Visibility.Visible;
|
||||
BoardViewboxBtnColorPinkContent.Visibility = Visibility.Visible;
|
||||
BorderPenColorPink.IsChecked = true;
|
||||
BoardBorderPenColorPink.IsChecked = true;
|
||||
break;
|
||||
case 7:
|
||||
ViewboxBtnColorTealContent.Visibility = Visibility.Visible;
|
||||
BorderPenColorTeal.IsChecked = true;
|
||||
BoardBorderPenColorTeal.IsChecked = true;
|
||||
break;
|
||||
case 8:
|
||||
ViewboxBtnColorOrangeContent.Visibility = Visibility.Visible;
|
||||
BorderPenColorOrange.IsChecked = true;
|
||||
BoardBorderPenColorOrange.IsChecked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (highlighterColor)
|
||||
{
|
||||
case 100:
|
||||
HighlighterPenViewboxBtnColorBlackContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorBlackContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenColorBlack.IsChecked = true;
|
||||
BoardHighlighterPenColorBlack.IsChecked = true;
|
||||
break;
|
||||
case 101:
|
||||
HighlighterPenViewboxBtnColorWhiteContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorWhiteContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenColorWhite.IsChecked = true;
|
||||
BoardHighlighterPenColorWhite.IsChecked = true;
|
||||
break;
|
||||
case 102:
|
||||
HighlighterPenViewboxBtnColorRedContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorRedContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenColorRed.IsChecked = true;
|
||||
BoardHighlighterPenColorRed.IsChecked = true;
|
||||
break;
|
||||
case 103:
|
||||
HighlighterPenViewboxBtnColorYellowContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorYellowContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenColorYellow.IsChecked = true;
|
||||
BoardHighlighterPenColorYellow.IsChecked = true;
|
||||
break;
|
||||
case 104:
|
||||
HighlighterPenViewboxBtnColorGreenContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorGreenContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenColorGreen.IsChecked = true;
|
||||
BoardHighlighterPenColorGreen.IsChecked = true;
|
||||
break;
|
||||
case 105:
|
||||
HighlighterPenViewboxBtnColorZincContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorZincContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenColorZinc.IsChecked = true;
|
||||
BoardHighlighterPenColorZinc.IsChecked = true;
|
||||
break;
|
||||
case 106:
|
||||
HighlighterPenViewboxBtnColorBlueContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorBlueContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenColorBlue.IsChecked = true;
|
||||
BoardHighlighterPenColorBlue.IsChecked = true;
|
||||
break;
|
||||
case 107:
|
||||
HighlighterPenViewboxBtnColorPurpleContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorPurpleContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenPenColorPurple.IsChecked = true;
|
||||
BoardHighlighterPenPenColorPurple.IsChecked = true;
|
||||
break;
|
||||
case 108:
|
||||
HighlighterPenViewboxBtnColorTealContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorTealContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenColorTeal.IsChecked = true;
|
||||
BoardHighlighterPenColorTeal.IsChecked = true;
|
||||
break;
|
||||
case 109:
|
||||
HighlighterPenViewboxBtnColorOrangeContent.Visibility = Visibility.Visible;
|
||||
BoardHighlighterPenViewboxBtnColorOrangeContent.Visibility = Visibility.Visible;
|
||||
HighlighterPenColorOrange.IsChecked = true;
|
||||
BoardHighlighterPenColorOrange.IsChecked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -553,37 +554,15 @@ namespace Ink_Canvas
|
||||
BoardHighlightPenTabButton.Background = new SolidColorBrush(Colors.Transparent);
|
||||
BoardHighlightPenTabButtonIndicator.Visibility = Visibility.Collapsed;
|
||||
|
||||
// PenPalette.Margin = new Thickness(-160, -200, -33, 32);
|
||||
// 动态计算面板位置,使其对齐笔按钮(考虑快捷调色盘等动态宽度)
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var marginAnimation = new ThicknessAnimation
|
||||
{
|
||||
Duration = TimeSpan.FromSeconds(0.1),
|
||||
From = PenPalette.Margin,
|
||||
To = new Thickness(-160, -200, -33, 32),
|
||||
EasingFunction = new CubicEase()
|
||||
};
|
||||
PenPalette.BeginAnimation(MarginProperty, marginAnimation);
|
||||
PenPalette.BeginAnimation(MarginProperty, null);
|
||||
var currentMargin = PenPalette.Margin;
|
||||
// 先设置正确的Top/Bottom,保持当前Left/Right
|
||||
PenPalette.Margin = new Thickness(currentMargin.Left, -200, currentMargin.Right, 32);
|
||||
UpdatePenPalettePosition();
|
||||
});
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var marginAnimation = new ThicknessAnimation
|
||||
{
|
||||
Duration = TimeSpan.FromSeconds(0.1),
|
||||
From = PenPalette.Margin,
|
||||
To = new Thickness(-160, -200, -33, 50),
|
||||
EasingFunction = new CubicEase()
|
||||
};
|
||||
BoardPenPaletteGrid.BeginAnimation(MarginProperty, marginAnimation);
|
||||
});
|
||||
|
||||
|
||||
await Task.Delay(100);
|
||||
|
||||
await Dispatcher.InvokeAsync(() => { PenPalette.Margin = new Thickness(-160, -200, -33, 32); });
|
||||
|
||||
await Dispatcher.InvokeAsync(() => { BoardPenPaletteGrid.Margin = new Thickness(-160, -200, -33, 50); });
|
||||
}
|
||||
else if (penType == 1)
|
||||
{
|
||||
@@ -621,36 +600,15 @@ namespace Ink_Canvas
|
||||
BoardHighlightPenTabButton.Background = new SolidColorBrush(Color.FromArgb(72, 219, 234, 254));
|
||||
BoardHighlightPenTabButtonIndicator.Visibility = Visibility.Visible;
|
||||
|
||||
// PenPalette.Margin = new Thickness(-160, -157, -33, 32);
|
||||
// 动态计算面板位置,使其对齐笔按钮(考虑快捷调色盘等动态宽度)
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var marginAnimation = new ThicknessAnimation
|
||||
{
|
||||
Duration = TimeSpan.FromSeconds(0.1),
|
||||
From = PenPalette.Margin,
|
||||
To = new Thickness(-160, -157, -33, 32),
|
||||
EasingFunction = new CubicEase()
|
||||
};
|
||||
PenPalette.BeginAnimation(MarginProperty, marginAnimation);
|
||||
PenPalette.BeginAnimation(MarginProperty, null);
|
||||
var currentMargin = PenPalette.Margin;
|
||||
// 荧光笔模式面板稍小,使用不同的Top/Bottom
|
||||
PenPalette.Margin = new Thickness(currentMargin.Left, -157, currentMargin.Right, 32);
|
||||
UpdatePenPalettePosition();
|
||||
});
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var marginAnimation = new ThicknessAnimation
|
||||
{
|
||||
Duration = TimeSpan.FromSeconds(0.1),
|
||||
From = PenPalette.Margin,
|
||||
To = new Thickness(-160, -154, -33, 50),
|
||||
EasingFunction = new CubicEase()
|
||||
};
|
||||
BoardPenPaletteGrid.BeginAnimation(MarginProperty, marginAnimation);
|
||||
});
|
||||
|
||||
await Task.Delay(100);
|
||||
|
||||
await Dispatcher.InvokeAsync(() => { PenPalette.Margin = new Thickness(-160, -157, -33, 32); });
|
||||
|
||||
await Dispatcher.InvokeAsync(() => { BoardPenPaletteGrid.Margin = new Thickness(-160, -154, -33, 50); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Ink_Canvas.Controls;
|
||||
using Ink_Canvas.Helpers;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
@@ -34,6 +35,14 @@ namespace Ink_Canvas
|
||||
/// </summary>
|
||||
private Point dragStartPoint;
|
||||
|
||||
/// <summary>页码侧栏当前订阅 <see cref="PdfEmbeddedView.PageNavigationStateChanged"/> 的 PDF 视图。</summary>
|
||||
private PdfEmbeddedView _pdfPageSidebarEventSource;
|
||||
|
||||
private bool _pdfSidebarPositionRefreshPending;
|
||||
|
||||
/// <summary>为 true 时,下一次成功算出 PDF 边界后的侧栏定位使用宿主 Visual 变换(仅用于刚插入/恢复 PDF 的首帧对齐)。</summary>
|
||||
private bool _pdfSidebarNextPositionUseHostTransform;
|
||||
|
||||
#region Image
|
||||
/// <summary>
|
||||
/// 处理图片插入按钮点击事件
|
||||
@@ -56,48 +65,52 @@ namespace Ink_Canvas
|
||||
private async void BtnImageInsert_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
OpenFileDialog openFileDialog = new OpenFileDialog();
|
||||
openFileDialog.Filter = "Image files (*.jpg; *.jpeg; *.png; *.bmp)|*.jpg;*.jpeg;*.png;*.bmp";
|
||||
openFileDialog.Filter = "图片与 PDF|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.pdf|图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif|PDF|*.pdf";
|
||||
|
||||
if (openFileDialog.ShowDialog() == true)
|
||||
{
|
||||
string filePath = openFileDialog.FileName;
|
||||
|
||||
Image image = await CreateAndCompressImageAsync(filePath);
|
||||
FrameworkElement element = await CreateAndCompressImageAsync(filePath);
|
||||
|
||||
if (image != null)
|
||||
if (element != null)
|
||||
{
|
||||
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
|
||||
image.Name = timestamp;
|
||||
element.Name = timestamp;
|
||||
|
||||
// 设置图片属性,避免被InkCanvas选择系统处理
|
||||
image.IsHitTestVisible = true;
|
||||
image.Focusable = false;
|
||||
element.IsHitTestVisible = true;
|
||||
element.Focusable = false;
|
||||
|
||||
// 初始化InkCanvas选择设置
|
||||
InitializeInkCanvasSelectionSettings();
|
||||
|
||||
// 先添加到画布
|
||||
inkCanvas.Children.Add(image);
|
||||
inkCanvas.Children.Add(element);
|
||||
|
||||
// 等待图片加载完成后再进行后续处理
|
||||
image.Loaded += (s, args) =>
|
||||
element.Loaded += (s, args) =>
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
// 初始化TransformGroup
|
||||
InitializeElementTransform(image);
|
||||
InitializeElementTransform(element);
|
||||
|
||||
// 居中缩放
|
||||
CenterAndScaleElement(image);
|
||||
CenterAndScaleElement(element);
|
||||
|
||||
// 最后绑定事件处理器
|
||||
BindElementEvents(image);
|
||||
BindElementEvents(element);
|
||||
|
||||
LogHelper.WriteLogToFile($"图片插入完成: {image.Name}");
|
||||
if (element is PdfEmbeddedView)
|
||||
_pdfSidebarNextPositionUseHostTransform = true;
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
|
||||
LogHelper.WriteLogToFile($"图片插入完成: {element.Name}");
|
||||
}), DispatcherPriority.Loaded);
|
||||
};
|
||||
|
||||
timeMachine.CommitElementInsertHistory(image);
|
||||
timeMachine.CommitElementInsertHistory(element);
|
||||
|
||||
// 插入图片后切换到选择模式并刷新浮动栏高光显示
|
||||
SetCurrentToolMode(InkCanvasEditingMode.Select);
|
||||
@@ -270,13 +283,13 @@ namespace Ink_Canvas
|
||||
ApplyMouseDragTransform(element, currentPoint, dragStartPoint);
|
||||
|
||||
// 如果是图片元素,更新工具栏位置
|
||||
if (element is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
}
|
||||
|
||||
// 如果是图片元素,更新选择点位置
|
||||
if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
}
|
||||
@@ -306,13 +319,13 @@ namespace Ink_Canvas
|
||||
ApplyWheelScaleTransform(element, e);
|
||||
|
||||
// 如果是图片元素,更新工具栏位置
|
||||
if (element is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
}
|
||||
|
||||
// 如果是图片元素,更新选择点位置
|
||||
if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
}
|
||||
@@ -396,13 +409,13 @@ namespace Ink_Canvas
|
||||
ApplyTouchManipulationTransform(element, e);
|
||||
|
||||
// 如果是图片元素,更新工具栏位置
|
||||
if (element is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(element) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
}
|
||||
|
||||
// 如果是图片元素,更新选择点位置
|
||||
if (element is Image && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
}
|
||||
@@ -523,12 +536,12 @@ namespace Ink_Canvas
|
||||
currentSelectedElement = element;
|
||||
|
||||
// 根据元素类型显示不同的选择工具栏
|
||||
if (element is Image)
|
||||
if (IsBitmapLikeCanvasElement(element))
|
||||
{
|
||||
// 显示图片选择工具栏并设置位置
|
||||
if (BorderImageSelectionControl != null)
|
||||
{
|
||||
// 计算工具栏位置
|
||||
// 计算工具栏位置(内部会同步 PDF 右侧栏位置)
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
BorderImageSelectionControl.Visibility = Visibility.Visible;
|
||||
}
|
||||
@@ -568,6 +581,8 @@ namespace Ink_Canvas
|
||||
// 保持选择模式,这样用户可以直接点击墨迹来选择
|
||||
inkCanvas.EditingMode = InkCanvasEditingMode.Select;
|
||||
}
|
||||
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -607,6 +622,8 @@ namespace Ink_Canvas
|
||||
{
|
||||
inkCanvas.EditingMode = InkCanvasEditingMode.Select;
|
||||
}
|
||||
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -914,15 +931,24 @@ namespace Ink_Canvas
|
||||
/// - 否则使用原始尺寸
|
||||
/// - 返回创建的Image对象
|
||||
/// </remarks>
|
||||
private async Task<Image> CreateAndCompressImageAsync(string filePath)
|
||||
/// <summary>与图片选择工具栏、缩放控制点联动的画布位图类元素(普通图片或多页 PDF 嵌入)。</summary>
|
||||
private static bool IsBitmapLikeCanvasElement(FrameworkElement fe)
|
||||
{
|
||||
return fe is Image || fe is PdfEmbeddedView;
|
||||
}
|
||||
|
||||
private async Task<FrameworkElement> CreateAndCompressImageAsync(string filePath)
|
||||
{
|
||||
string fileExtension = Path.GetExtension(filePath);
|
||||
if (string.Equals(fileExtension, ".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
return await CreateAndCompressImageFromPdfAsync(filePath);
|
||||
|
||||
string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency");
|
||||
if (!Directory.Exists(savePath))
|
||||
{
|
||||
Directory.CreateDirectory(savePath);
|
||||
}
|
||||
|
||||
string fileExtension = Path.GetExtension(filePath);
|
||||
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
|
||||
string newFilePath = Path.Combine(savePath, timestamp + fileExtension);
|
||||
|
||||
@@ -965,6 +991,84 @@ namespace Ink_Canvas
|
||||
return image;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插入完整 PDF:嵌入控件内可翻页,右下角显示页码(类似希沃白板交互)。
|
||||
/// </summary>
|
||||
private async Task<PdfEmbeddedView> CreateAndCompressImageFromPdfAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
string savePath = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "File Dependency");
|
||||
if (!Directory.Exists(savePath))
|
||||
Directory.CreateDirectory(savePath);
|
||||
|
||||
string timestamp = "img_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff");
|
||||
string newFilePath = Path.Combine(savePath, timestamp + ".pdf");
|
||||
await Task.Run(() => File.Copy(filePath, newFilePath, true));
|
||||
|
||||
uint pageCount = await PdfWinRtHelper.GetPageCountAsync(newFilePath);
|
||||
if (pageCount == 0)
|
||||
{
|
||||
ShowNotification("无法打开 PDF(可能已加密、损坏或不支持)。");
|
||||
return null;
|
||||
}
|
||||
|
||||
bool compress = isLoaded && Settings.Canvas.IsCompressPicturesUploaded;
|
||||
var view = new PdfEmbeddedView();
|
||||
await view.InitializeAsync(newFilePath, pageCount, compress);
|
||||
view.Tag = filePath;
|
||||
return view;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"插入 PDF 失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
ShowNotification($"插入 PDF 失败: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从保存的 <see cref="CanvasElementInfo"/> 恢复 PDF(与打开墨迹时的图片恢复流程一致,不单独写入时间轴)。</summary>
|
||||
private async Task RestorePdfFromElementInfoAsync(CanvasElementInfo info)
|
||||
{
|
||||
if (info == null || inkCanvas == null) return;
|
||||
if (!string.Equals(info.Type, "Pdf", StringComparison.OrdinalIgnoreCase)) return;
|
||||
if (string.IsNullOrEmpty(info.SourcePath) || !File.Exists(info.SourcePath)) return;
|
||||
|
||||
try
|
||||
{
|
||||
uint pageCount = await PdfWinRtHelper.GetPageCountAsync(info.SourcePath);
|
||||
if (pageCount == 0) return;
|
||||
|
||||
bool compress = isLoaded && Settings.Canvas.IsCompressPicturesUploaded;
|
||||
uint initial = 0;
|
||||
if (info.PdfCurrentPage.HasValue)
|
||||
initial = (uint)Math.Max(0, Math.Min(info.PdfCurrentPage.Value, (int)pageCount - 1));
|
||||
|
||||
var view = new PdfEmbeddedView
|
||||
{
|
||||
Name = "pdf_" + DateTime.Now.ToString("yyyyMMdd_HH_mm_ss_fff")
|
||||
};
|
||||
await view.InitializeAsync(info.SourcePath, pageCount, compress, initial);
|
||||
|
||||
if (info.Width > 0) view.Width = info.Width;
|
||||
if (info.Height > 0) view.Height = info.Height;
|
||||
|
||||
InkCanvas.SetLeft(view, info.Left);
|
||||
InkCanvas.SetTop(view, info.Top);
|
||||
|
||||
InitializeElementTransform(view);
|
||||
BindElementEvents(view);
|
||||
inkCanvas.Children.Add(view);
|
||||
_pdfSidebarNextPositionUseHostTransform = true;
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"从 .elements.json 恢复 PDF 失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Media
|
||||
@@ -1453,6 +1557,10 @@ namespace Ink_Canvas
|
||||
|
||||
// 设置工具栏位置
|
||||
BorderImageSelectionControl.Margin = new Thickness(toolbarLeft, toolbarTop, 0, 0);
|
||||
|
||||
var pdfTarget = GetPdfSidebarTargetElement();
|
||||
if (pdfTarget != null && BorderPdfPageSidebar != null && BorderPdfPageSidebar.Visibility == Visibility.Visible)
|
||||
UpdatePdfPageSidebarPosition(pdfTarget);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1460,6 +1568,245 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
private const double PdfPageSidebarGap = 10;
|
||||
|
||||
/// <summary>
|
||||
/// 侧栏绑定的 PDF:若当前选中的是 PDF 则用该项;否则用画布上最后一个 PdfEmbeddedView。
|
||||
/// </summary>
|
||||
private PdfEmbeddedView GetPdfSidebarTargetElement()
|
||||
{
|
||||
if (inkCanvas == null) return null;
|
||||
var pdfs = inkCanvas.Children.OfType<PdfEmbeddedView>().ToList();
|
||||
if (pdfs.Count == 0) return null;
|
||||
if (currentSelectedElement is PdfEmbeddedView sel && pdfs.Contains(sel))
|
||||
return sel;
|
||||
return pdfs[pdfs.Count - 1];
|
||||
}
|
||||
|
||||
private void AttachPdfPageSidebarEvents(PdfEmbeddedView pdf)
|
||||
{
|
||||
if (pdf == null || _pdfPageSidebarEventSource == pdf) return;
|
||||
DetachPdfPageSidebarEvents();
|
||||
_pdfPageSidebarEventSource = pdf;
|
||||
_pdfPageSidebarEventSource.PageNavigationStateChanged += SelectedPdf_PageNavigationStateChanged;
|
||||
_pdfPageSidebarEventSource.LayoutUpdated += OnPdfSidebarTargetLayoutUpdated;
|
||||
}
|
||||
|
||||
private void DetachPdfPageSidebarEvents()
|
||||
{
|
||||
if (_pdfPageSidebarEventSource != null)
|
||||
{
|
||||
_pdfPageSidebarEventSource.PageNavigationStateChanged -= SelectedPdf_PageNavigationStateChanged;
|
||||
_pdfPageSidebarEventSource.LayoutUpdated -= OnPdfSidebarTargetLayoutUpdated;
|
||||
_pdfPageSidebarEventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPdfSidebarTargetLayoutUpdated(object sender, EventArgs e)
|
||||
{
|
||||
if (BorderPdfPageSidebar?.Visibility != Visibility.Visible || inkCanvas == null) return;
|
||||
if (!(sender is PdfEmbeddedView p) || !ReferenceEquals(_pdfPageSidebarEventSource, p)) return;
|
||||
if (!inkCanvas.Children.Contains(p))
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
else
|
||||
RequestPdfSidebarPositionRefresh();
|
||||
}
|
||||
|
||||
/// <summary>在下一帧合并更新侧栏位置(初始布局、翻页改尺寸后 ActualWidth 仍为 0 时尤其需要)。</summary>
|
||||
private void RequestPdfSidebarPositionRefresh()
|
||||
{
|
||||
if (_pdfSidebarPositionRefreshPending) return;
|
||||
_pdfSidebarPositionRefreshPending = true;
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
_pdfSidebarPositionRefreshPending = false;
|
||||
try
|
||||
{
|
||||
var t = GetPdfSidebarTargetElement();
|
||||
if (t == null || BorderPdfPageSidebar?.Visibility != Visibility.Visible)
|
||||
{
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inkCanvas.Children.Contains(t))
|
||||
{
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
t.UpdateLayout();
|
||||
inkCanvas.UpdateLayout();
|
||||
UpdatePdfPageSidebarPosition(t);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"PDF 侧栏延迟定位失败: {ex.Message}", LogHelper.LogType.Warning);
|
||||
}
|
||||
}), DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 画布上存在 PDF 时始终显示右侧页码栏并跟随目标 PDF;无任何 PDF 时隐藏。
|
||||
/// </summary>
|
||||
private void SyncPdfPageSidebarWithCanvas()
|
||||
{
|
||||
if (BorderPdfPageSidebar == null || inkCanvas == null) return;
|
||||
|
||||
// 屏幕模式(已退出白板/黑板)下不显示侧栏,避免画布仍含 PDF 时栏残留在桌面上
|
||||
if (currentMode == 0)
|
||||
{
|
||||
DetachPdfPageSidebarEvents();
|
||||
BorderPdfPageSidebar.Visibility = Visibility.Collapsed;
|
||||
ResetPdfSidebarToIdle();
|
||||
return;
|
||||
}
|
||||
|
||||
var pdf = GetPdfSidebarTargetElement();
|
||||
if (pdf == null)
|
||||
{
|
||||
DetachPdfPageSidebarEvents();
|
||||
BorderPdfPageSidebar.Visibility = Visibility.Collapsed;
|
||||
ResetPdfSidebarToIdle();
|
||||
return;
|
||||
}
|
||||
|
||||
AttachPdfPageSidebarEvents(pdf);
|
||||
BorderPdfPageSidebar.Visibility = Visibility.Visible;
|
||||
UpdatePdfSidebarFromPdf(pdf);
|
||||
pdf.UpdateLayout();
|
||||
inkCanvas.UpdateLayout();
|
||||
UpdatePdfPageSidebarPosition(pdf);
|
||||
RequestPdfSidebarPositionRefresh();
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var t = GetPdfSidebarTargetElement();
|
||||
if (t != null && BorderPdfPageSidebar?.Visibility == Visibility.Visible && inkCanvas.Children.Contains(t))
|
||||
UpdatePdfPageSidebarPosition(t);
|
||||
}), DispatcherPriority.ContextIdle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 PDF 专用页码栏贴在当前 PDF 右侧。常态与早期实现一致:画布坐标 + <c>Measure(Width, ∞)</c>;仅在 <see cref="_pdfSidebarNextPositionUseHostTransform"/> 为 true 时用宿主 Visual 变换对齐首帧。
|
||||
/// </summary>
|
||||
private void UpdatePdfPageSidebarPosition(FrameworkElement element)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (BorderPdfPageSidebar == null || inkCanvas == null || !(element is PdfEmbeddedView pdfEl))
|
||||
return;
|
||||
|
||||
if (!inkCanvas.Children.Contains(pdfEl))
|
||||
{
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
bool wantHostOnce = _pdfSidebarNextPositionUseHostTransform;
|
||||
|
||||
// 插入首帧:先布局再取界,便于 Transform 与墨迹一致;常态与最初 PDF 侧栏实现一致,不在此强制 UpdateLayout。
|
||||
if (wantHostOnce)
|
||||
pdfEl.UpdateLayout();
|
||||
|
||||
Rect b = GetElementActualBounds(pdfEl);
|
||||
if (b.Width <= 0 || b.Height <= 0 || double.IsNaN(b.Width) || double.IsNaN(b.Height))
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
var t = GetPdfSidebarTargetElement();
|
||||
if (t is PdfEmbeddedView pe && inkCanvas.Children.Contains(pe) && BorderPdfPageSidebar?.Visibility == Visibility.Visible)
|
||||
UpdatePdfPageSidebarPosition(pe);
|
||||
}), DispatcherPriority.Loaded);
|
||||
return;
|
||||
}
|
||||
|
||||
Visual sidebarHost = VisualTreeHelper.GetParent(BorderPdfPageSidebar) as Visual;
|
||||
double left = 0, top = 0, maxLeft = 0, maxTop = 0;
|
||||
bool hostOk = false;
|
||||
|
||||
if (wantHostOnce && sidebarHost != null)
|
||||
{
|
||||
BorderPdfPageSidebar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
double sidebarW = BorderPdfPageSidebar.DesiredSize.Width;
|
||||
double sidebarH = BorderPdfPageSidebar.DesiredSize.Height;
|
||||
if (sidebarW <= 0)
|
||||
sidebarW = BorderPdfPageSidebar.Width;
|
||||
if (sidebarH <= 0)
|
||||
sidebarH = BorderPdfPageSidebar.ActualHeight;
|
||||
if (sidebarH <= 0)
|
||||
sidebarH = 220;
|
||||
|
||||
try
|
||||
{
|
||||
GeneralTransform inkToHost = inkCanvas.TransformToVisual(sidebarHost);
|
||||
Point tl = inkToHost.Transform(new Point(b.Left, b.Top));
|
||||
Point br = inkToHost.Transform(new Point(b.Right, b.Bottom));
|
||||
double rightX = Math.Max(tl.X, br.X);
|
||||
double midY = (tl.Y + br.Y) * 0.5;
|
||||
left = rightX + PdfPageSidebarGap;
|
||||
top = midY - sidebarH * 0.5;
|
||||
|
||||
var feHost = sidebarHost as FrameworkElement;
|
||||
double hostW = feHost != null && feHost.ActualWidth > 0 ? feHost.ActualWidth : inkCanvas.ActualWidth;
|
||||
double hostH = feHost != null && feHost.ActualHeight > 0 ? feHost.ActualHeight : inkCanvas.ActualHeight;
|
||||
maxLeft = Math.Max(0, hostW - sidebarW);
|
||||
maxTop = Math.Max(0, hostH - sidebarH);
|
||||
|
||||
if (left > maxLeft)
|
||||
{
|
||||
double leftEdge = Math.Min(tl.X, br.X);
|
||||
double leftAlt = leftEdge - PdfPageSidebarGap - sidebarW;
|
||||
if (leftAlt >= 0)
|
||||
left = leftAlt;
|
||||
}
|
||||
|
||||
hostOk = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
hostOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hostOk)
|
||||
{
|
||||
// 与 ea74592「PDF 侧栏」初版一致:固定宽度测量竖向所需高度,再按墨迹边界与 inkCanvas 尺寸夹紧。
|
||||
BorderPdfPageSidebar.Measure(new Size(BorderPdfPageSidebar.Width, double.PositiveInfinity));
|
||||
double sidebarW = BorderPdfPageSidebar.DesiredSize.Width;
|
||||
double sidebarH = BorderPdfPageSidebar.DesiredSize.Height;
|
||||
if (sidebarW <= 0)
|
||||
sidebarW = BorderPdfPageSidebar.Width;
|
||||
if (sidebarH <= 0)
|
||||
sidebarH = BorderPdfPageSidebar.ActualHeight;
|
||||
if (sidebarH <= 0)
|
||||
sidebarH = 220;
|
||||
|
||||
left = b.Right + PdfPageSidebarGap;
|
||||
top = b.Top + (b.Height * 0.5) - (sidebarH * 0.5);
|
||||
maxLeft = Math.Max(0, inkCanvas.ActualWidth - sidebarW);
|
||||
maxTop = Math.Max(0, inkCanvas.ActualHeight - sidebarH);
|
||||
if (left > maxLeft)
|
||||
{
|
||||
double leftAlt = b.Left - PdfPageSidebarGap - sidebarW;
|
||||
if (leftAlt >= 0)
|
||||
left = leftAlt;
|
||||
}
|
||||
}
|
||||
|
||||
if (wantHostOnce)
|
||||
_pdfSidebarNextPositionUseHostTransform = false;
|
||||
|
||||
left = Math.Max(0, Math.Min(left, maxLeft));
|
||||
top = Math.Max(0, Math.Min(top, maxTop));
|
||||
|
||||
BorderPdfPageSidebar.Margin = new Thickness(left, top, 0, 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新 PDF 右侧页码栏位置失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取元素的实际边界(考虑变换)
|
||||
/// </summary>
|
||||
@@ -1644,7 +1991,7 @@ namespace Ink_Canvas
|
||||
ApplyRotateTransform(currentSelectedElement, -45);
|
||||
|
||||
// 更新工具栏位置
|
||||
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
}
|
||||
@@ -1678,7 +2025,7 @@ namespace Ink_Canvas
|
||||
ApplyRotateTransform(currentSelectedElement, 45);
|
||||
|
||||
// 更新工具栏位置
|
||||
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
}
|
||||
@@ -1714,7 +2061,7 @@ namespace Ink_Canvas
|
||||
ApplyScaleTransform(currentSelectedElement, 0.9, elementCenter);
|
||||
|
||||
// 更新工具栏位置
|
||||
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
}
|
||||
@@ -1750,7 +2097,7 @@ namespace Ink_Canvas
|
||||
ApplyScaleTransform(currentSelectedElement, 1.1, elementCenter);
|
||||
|
||||
// 更新工具栏位置
|
||||
if (currentSelectedElement is Image && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
if (IsBitmapLikeCanvasElement(currentSelectedElement) && BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
}
|
||||
@@ -1764,19 +2111,110 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetPdfSidebarToIdle()
|
||||
{
|
||||
if (TextBlockPdfSidebarPageLabel != null)
|
||||
{
|
||||
TextBlockPdfSidebarPageLabel.Text = "— / —";
|
||||
TextBlockPdfSidebarPageLabel.Opacity = 0.55;
|
||||
}
|
||||
|
||||
if (BorderPdfSidebarPagePrev != null)
|
||||
{
|
||||
BorderPdfSidebarPagePrev.Opacity = 0.35;
|
||||
BorderPdfSidebarPagePrev.IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
if (BorderPdfSidebarPageNext != null)
|
||||
{
|
||||
BorderPdfSidebarPageNext.Opacity = 0.35;
|
||||
BorderPdfSidebarPageNext.IsHitTestVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePdfSidebarFromPdf(PdfEmbeddedView pdf)
|
||||
{
|
||||
if (pdf == null) return;
|
||||
|
||||
if (TextBlockPdfSidebarPageLabel != null)
|
||||
{
|
||||
TextBlockPdfSidebarPageLabel.Text = pdf.PageLabelText;
|
||||
TextBlockPdfSidebarPageLabel.Opacity = 1.0;
|
||||
}
|
||||
|
||||
bool prevOk = pdf.CanGoPrevious;
|
||||
bool nextOk = pdf.CanGoNext;
|
||||
if (BorderPdfSidebarPagePrev != null)
|
||||
{
|
||||
BorderPdfSidebarPagePrev.Opacity = prevOk ? 1.0 : 0.35;
|
||||
BorderPdfSidebarPagePrev.IsHitTestVisible = prevOk;
|
||||
}
|
||||
|
||||
if (BorderPdfSidebarPageNext != null)
|
||||
{
|
||||
BorderPdfSidebarPageNext.Opacity = nextOk ? 1.0 : 0.35;
|
||||
BorderPdfSidebarPageNext.IsHitTestVisible = nextOk;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectedPdf_PageNavigationStateChanged(object sender, EventArgs e)
|
||||
{
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (sender is PdfEmbeddedView pdf)
|
||||
{
|
||||
if (!inkCanvas.Children.Contains(pdf))
|
||||
{
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdatePdfSidebarFromPdf(pdf);
|
||||
UpdatePdfPageSidebarPosition(pdf);
|
||||
RequestPdfSidebarPositionRefresh();
|
||||
}
|
||||
if (currentSelectedElement != null && IsBitmapLikeCanvasElement(currentSelectedElement))
|
||||
{
|
||||
UpdateImageSelectionToolbarPosition(currentSelectedElement);
|
||||
if (ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(currentSelectedElement));
|
||||
}
|
||||
}), DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private async void BorderPdfSidebarPagePrev_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pdf = GetPdfSidebarTargetElement();
|
||||
if (pdf != null && pdf.CanGoPrevious)
|
||||
await pdf.GoToPreviousPageAsync();
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"PDF 上一页失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void BorderPdfSidebarPageNext_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pdf = GetPdfSidebarTargetElement();
|
||||
if (pdf != null && pdf.CanGoNext)
|
||||
await pdf.GoToNextPageAsync();
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"PDF 下一页失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理图片删除功能
|
||||
/// </summary>
|
||||
/// <param name="sender">事件发送者</param>
|
||||
/// <param name="e">事件参数</param>
|
||||
/// <remarks>
|
||||
/// - 检查当前是否有选中元素
|
||||
/// - 保存删除前的编辑模式
|
||||
/// - 记录删除历史
|
||||
/// - 从画布中移除
|
||||
/// - 清除选中状态
|
||||
/// - 包含异常处理
|
||||
/// </remarks>
|
||||
private void BorderImageDelete_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
try
|
||||
@@ -1789,16 +2227,19 @@ namespace Ink_Canvas
|
||||
// 记录删除历史
|
||||
timeMachine.CommitElementRemoveHistory(currentSelectedElement);
|
||||
|
||||
var toRemove = currentSelectedElement;
|
||||
// 从画布中移除
|
||||
inkCanvas.Children.Remove(currentSelectedElement);
|
||||
inkCanvas.Children.Remove(toRemove);
|
||||
|
||||
// 清除选中状态
|
||||
UnselectElement(currentSelectedElement);
|
||||
UnselectElement(toRemove);
|
||||
currentSelectedElement = null;
|
||||
|
||||
// 恢复到删除前的编辑模式
|
||||
inkCanvas.EditingMode = previousEditingMode;
|
||||
|
||||
SyncPdfPageSidebarWithCanvas();
|
||||
|
||||
LogHelper.WriteLogToFile($"图片删除完成,已恢复到编辑模式: {previousEditingMode}");
|
||||
}
|
||||
}
|
||||
@@ -2029,7 +2470,7 @@ namespace Ink_Canvas
|
||||
{
|
||||
try
|
||||
{
|
||||
if (currentSelectedElement is Image image && sender is Ellipse ellipse)
|
||||
if (IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse)
|
||||
{
|
||||
isResizingImage = true;
|
||||
imageResizeStartPoint = e.GetPosition(inkCanvas);
|
||||
@@ -2072,10 +2513,10 @@ namespace Ink_Canvas
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isResizingImage && currentSelectedElement is Image image && sender is Ellipse ellipse)
|
||||
if (isResizingImage && IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse)
|
||||
{
|
||||
var currentPoint = e.GetPosition(inkCanvas);
|
||||
ResizeImageByHandle(image, imageResizeStartPoint, currentPoint, activeResizeHandle);
|
||||
ResizeImageByHandle(currentSelectedElement, imageResizeStartPoint, currentPoint, activeResizeHandle);
|
||||
imageResizeStartPoint = currentPoint;
|
||||
e.Handled = true;
|
||||
}
|
||||
@@ -2087,11 +2528,11 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
// 根据控制点缩放图片
|
||||
private void ResizeImageByHandle(Image image, Point startPoint, Point currentPoint, string handleName)
|
||||
private void ResizeImageByHandle(FrameworkElement element, Point startPoint, Point currentPoint, string handleName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (image.RenderTransform is TransformGroup transformGroup)
|
||||
if (element.RenderTransform is TransformGroup transformGroup)
|
||||
{
|
||||
var scaleTransform = transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault();
|
||||
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
|
||||
@@ -2099,7 +2540,7 @@ namespace Ink_Canvas
|
||||
if (scaleTransform == null || translateTransform == null) return;
|
||||
|
||||
// 获取图片的当前边界
|
||||
Rect currentBounds = GetElementActualBounds(image);
|
||||
Rect currentBounds = GetElementActualBounds(element);
|
||||
double deltaX = currentPoint.X - startPoint.X;
|
||||
double deltaY = currentPoint.Y - startPoint.Y;
|
||||
|
||||
@@ -2160,7 +2601,10 @@ namespace Ink_Canvas
|
||||
translateTransform.Y += translateY;
|
||||
|
||||
// 更新选择点位置
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(image));
|
||||
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
|
||||
|
||||
if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
|
||||
UpdateImageSelectionToolbarPosition(element);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
using Ink_Canvas.Helpers;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace Ink_Canvas
|
||||
@@ -10,25 +8,14 @@ namespace Ink_Canvas
|
||||
{
|
||||
#region 悬浮窗拦截功能
|
||||
|
||||
/// <summary>
|
||||
/// 初始化悬浮窗拦截管理器
|
||||
/// </summary>
|
||||
private void InitializeFloatingWindowInterceptor()
|
||||
{
|
||||
try
|
||||
{
|
||||
_floatingWindowInterceptorManager = new FloatingWindowInterceptorManager();
|
||||
|
||||
// 订阅事件
|
||||
_floatingWindowInterceptorManager.WindowIntercepted += OnFloatingWindowIntercepted;
|
||||
_floatingWindowInterceptorManager.WindowRestored += OnFloatingWindowRestored;
|
||||
|
||||
// 初始化拦截器
|
||||
_floatingWindowInterceptorManager.Initialize(Settings.Automation.FloatingWindowInterceptor);
|
||||
|
||||
// 加载UI状态
|
||||
LoadFloatingWindowInterceptorUI();
|
||||
|
||||
LogHelper.WriteLogToFile("悬浮窗拦截管理器初始化完成", LogHelper.LogType.Event);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -37,83 +24,11 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载悬浮窗拦截UI状态
|
||||
/// </summary>
|
||||
private void LoadFloatingWindowInterceptorUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
|
||||
// 设置主开关状态
|
||||
ToggleSwitchFloatingWindowInterceptorEnabled.IsOn = Settings.Automation.FloatingWindowInterceptor.IsEnabled;
|
||||
|
||||
// 设置各个拦截规则的状态
|
||||
foreach (var kvp in Settings.Automation.FloatingWindowInterceptor.InterceptRules)
|
||||
{
|
||||
var toggleName = $"ToggleSwitch{kvp.Key}";
|
||||
var toggle = FindName(toggleName) as ToggleSwitch;
|
||||
if (toggle != null)
|
||||
{
|
||||
toggle.IsOn = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新UI可见性
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"加载悬浮窗拦截UI状态失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新悬浮窗拦截UI
|
||||
/// </summary>
|
||||
private void UpdateFloatingWindowInterceptorUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
var isEnabled = Settings.Automation.FloatingWindowInterceptor.IsEnabled;
|
||||
FloatingWindowInterceptorGrid.Visibility = isEnabled ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// 计算启用的规则数量
|
||||
var enabledRulesCount = Settings.Automation.FloatingWindowInterceptor.InterceptRules.Where(kvp => kvp.Value).Count();
|
||||
var totalRulesCount = Settings.Automation.FloatingWindowInterceptor.InterceptRules.Count;
|
||||
|
||||
// 更新状态文本
|
||||
if (_floatingWindowInterceptorManager != null)
|
||||
{
|
||||
var stats = _floatingWindowInterceptorManager.GetStatistics();
|
||||
TextBlockFloatingWindowInterceptorStatus.Text = stats.IsRunning
|
||||
? $"拦截器运行中 - 已启用 {enabledRulesCount}/{totalRulesCount} 个规则"
|
||||
: $"拦截器未启动 - 已启用 {enabledRulesCount}/{totalRulesCount} 个规则";
|
||||
}
|
||||
else
|
||||
{
|
||||
TextBlockFloatingWindowInterceptorStatus.Text = $"拦截器未初始化 - 已启用 {enabledRulesCount}/{totalRulesCount} 个规则";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"更新悬浮窗拦截UI失败: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口被拦截事件处理
|
||||
/// </summary>
|
||||
private void OnFloatingWindowIntercepted(object sender, FloatingWindowInterceptor.WindowInterceptedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 在UI线程中更新状态
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
}));
|
||||
Dispatcher.BeginInvoke(new Action(() => { }));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -121,18 +36,11 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口被恢复事件处理
|
||||
/// </summary>
|
||||
private void OnFloatingWindowRestored(object sender, FloatingWindowInterceptor.WindowRestoredEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 在UI线程中更新状态
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
}));
|
||||
Dispatcher.BeginInvoke(new Action(() => { }));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -144,30 +52,24 @@ namespace Ink_Canvas
|
||||
|
||||
#region 悬浮窗拦截事件处理
|
||||
|
||||
/// <summary>
|
||||
/// 主开关切换事件
|
||||
/// </summary>
|
||||
private void ToggleSwitchFloatingWindowInterceptorEnabled_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
|
||||
try
|
||||
{
|
||||
Settings.Automation.FloatingWindowInterceptor.IsEnabled = ToggleSwitchFloatingWindowInterceptorEnabled.IsOn;
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null)
|
||||
Settings.Automation.FloatingWindowInterceptor.IsEnabled = toggle.IsOn;
|
||||
|
||||
if (_floatingWindowInterceptorManager != null)
|
||||
{
|
||||
if (Settings.Automation.FloatingWindowInterceptor.IsEnabled)
|
||||
{
|
||||
_floatingWindowInterceptorManager.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
_floatingWindowInterceptorManager.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
SaveSettingsToFile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -176,129 +78,98 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃白板3拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoWhiteboard3Floating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard3Floating, ToggleSwitchSeewoWhiteboard3Floating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard3Floating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃白板5拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoWhiteboard5Floating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5Floating, ToggleSwitchSeewoWhiteboard5Floating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5Floating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃白板5C拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoWhiteboard5CFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5CFloating, ToggleSwitchSeewoWhiteboard5CFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoWhiteboard5CFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃品课侧栏拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoPincoSideBarFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoSideBarFloating, ToggleSwitchSeewoPincoSideBarFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoSideBarFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃品课画笔拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoPincoDrawingFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoDrawingFloating, ToggleSwitchSeewoPincoDrawingFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPincoDrawingFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃PPT小工具拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoPPTFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPPTFloating, ToggleSwitchSeewoPPTFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoPPTFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AiClass拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchAiClassFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.AiClassFloating, ToggleSwitchAiClassFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.AiClassFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鸿合屏幕书写拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchHiteAnnotationFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.HiteAnnotationFloating, ToggleSwitchHiteAnnotationFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.HiteAnnotationFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 畅言智慧课堂拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchChangYanFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanFloating, ToggleSwitchChangYanFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 畅言PPT拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchChangYanPptFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanPptFloating, ToggleSwitchChangYanPptFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.ChangYanPptFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 天喻教育云拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchIntelligentClassFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.IntelligentClassFloating, ToggleSwitchIntelligentClassFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.IntelligentClassFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃桌面画笔拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoDesktopAnnotationFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopAnnotationFloating, ToggleSwitchSeewoDesktopAnnotationFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopAnnotationFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 希沃桌面侧栏拦截开关
|
||||
/// </summary>
|
||||
private void ToggleSwitchSeewoDesktopSideBarFloating_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopSideBarFloating, ToggleSwitchSeewoDesktopSideBarFloating.IsOn);
|
||||
var toggle = sender as iNKORE.UI.WPF.Modern.Controls.ToggleSwitch;
|
||||
if (toggle != null) SetInterceptRule(FloatingWindowInterceptor.InterceptType.SeewoDesktopSideBarFloating, toggle.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置拦截规则
|
||||
/// </summary>
|
||||
/// <param name="type">拦截类型</param>
|
||||
/// <param name="enabled">是否启用拦截</param>
|
||||
private void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
|
||||
public void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -307,18 +178,15 @@ namespace Ink_Canvas
|
||||
_floatingWindowInterceptorManager.SetInterceptRule(type, enabled);
|
||||
}
|
||||
|
||||
// 更新设置
|
||||
var ruleName = type.ToString();
|
||||
if (Settings.Automation.FloatingWindowInterceptor.InterceptRules.ContainsKey(ruleName))
|
||||
{
|
||||
Settings.Automation.FloatingWindowInterceptor.InterceptRules[ruleName] = enabled;
|
||||
}
|
||||
|
||||
// 获取规则信息以处理父子关系
|
||||
var rule = _floatingWindowInterceptorManager?.GetInterceptRule(type);
|
||||
if (rule != null)
|
||||
{
|
||||
// 如果是父规则,更新所有子规则的设置
|
||||
if (rule.ChildTypes.Count > 0)
|
||||
{
|
||||
foreach (var childType in rule.ChildTypes)
|
||||
@@ -330,7 +198,6 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果是子规则,更新父规则的设置
|
||||
else if (rule.ParentType.HasValue)
|
||||
{
|
||||
var parentRule = _floatingWindowInterceptorManager?.GetInterceptRule(rule.ParentType.Value);
|
||||
@@ -345,9 +212,6 @@ namespace Ink_Canvas
|
||||
}
|
||||
}
|
||||
|
||||
// 更新UI显示
|
||||
UpdateFloatingWindowInterceptorUI();
|
||||
|
||||
SaveSettingsToFile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -357,4 +221,4 @@ namespace Ink_Canvas
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user