diff --git a/.all-contributorsrc b/.all-contributorsrc index 8b3af54b..a47d5eb7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -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" } diff --git a/.github/workflows/prcheck.yml b/.github/workflows/prcheck.yml deleted file mode 100644 index ca6031a1..00000000 --- a/.github/workflows/prcheck.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: PR Check - -on: - pull_request: - types: [opened, synchronize] - branches: [ main, beta ] - 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 }}\net472\InkCanvasForClass.exe" - - if (Test-Path $exePath) { - echo "build_success=true" >> $env:GITHUB_OUTPUT - } else { - echo "build_success=false" >> $env:GITHUB_OUTPUT - - if ("${{ github.event_name }}" -eq "workflow_dispatch") { - exit 1 - } - } - - - name: 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 }}/net472/*" - - - - name: Create Summary - if: always() - shell: bash - run: | - echo "# Build Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then - echo "## ✅ Build Successful" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY - echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY - echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY - echo "**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 diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml deleted file mode 100644 index 80b36cca..00000000 --- a/.github/workflows/prerelease.yml +++ /dev/null @@ -1,754 +0,0 @@ -name: Pre-release and Changelog - -on: - push: - tags: - - '*' - workflow_dispatch: - inputs: - version_type: - description: 'Version bump type' - required: true - default: 'patch' - type: choice - options: - - patch - - minor - - major - - build - prerelease: - description: 'Create as pre-release' - required: true - default: true - type: boolean - draft: - description: 'Create as draft release' - required: true - default: false - type: boolean - -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.sha }} - cancel-in-progress: true - -jobs: - prepare: - runs-on: windows-latest - outputs: - tag_name: ${{ steps.get_tag.outputs.tag_name }} - version: ${{ steps.get_tag.outputs.version }} - is_prerelease: ${{ steps.release_type.outputs.is_prerelease }} - changelog: ${{ steps.read_changelog.outputs.changelog }} - - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - fetch-tags: true - - # ========== 获取当前版本 ========== - - name: Get current version from Git tag - id: get_version - run: | - # 获取最新的tag - $latestTag = git describe --tags --abbrev=0 2>$null - if ($latestTag) { - $version = $latestTag - echo "Found latest tag: $latestTag" - } else { - # 如果没有tag,使用默认值 - $version = "1.0.0.0" - echo "No tag found, using default version: $version" - } - echo "current_version=$version" >> $env:GITHUB_OUTPUT - echo "Current version: $version" - - # ========== 处理版本号和标签名 ========== - - name: Get tag name and version - id: get_tag - run: | - if ("${{ github.event_name }}" -eq "push") { - # 从 push tag 事件获取原始标签名 - $tagName = "${{ github.ref }}".Replace("refs/tags/", "") - $cleanVersion = $tagName - - echo "tag_name=$tagName" >> $env:GITHUB_OUTPUT - echo "version=$cleanVersion" >> $env:GITHUB_OUTPUT - echo "Using pushed tag: $tagName, version: $cleanVersion" - } else { - # 从 workflow_dispatch 计算新版本(4位格式) - $currentVersion = "${{ steps.get_version.outputs.current_version }}" - $versionParts = $currentVersion.Split('.') - - # 确保版本号格式正确(至少4部分) - if ($versionParts.Length -ge 4) { - $major = [int]$versionParts[0] - $minor = [int]$versionParts[1] - $patch = [int]$versionParts[2] - $build = [int]$versionParts[3] - } else { - # 如果版本号格式不正确,补充为4位 - if ($versionParts.Length -ge 3) { - $major = [int]$versionParts[0] - $minor = [int]$versionParts[1] - $patch = [int]$versionParts[2] - $build = 0 - } else { - # 如果版本号格式不正确,抛出错误 - echo "Error: Invalid version format. Expected format: x.y.z.w (e.g., 1.7.18.0)" - exit 1 - } - } - - $versionType = "${{ github.event.inputs.version_type }}" - $isPrerelease = "${{ github.event.inputs.prerelease }}" -eq "true" - - switch ($versionType) { - "major" { - $major++ - $minor = 0 - $patch = 0 - $build = 0 - } - "minor" { - $minor++ - $patch = 0 - $build = 0 - } - "patch" { - $patch++ - $build = 0 - } - "build" { - $build++ - } - } - - # 生成新版本号(4位格式,如1.7.18.0) - $newVersion = "$major.$minor.$patch.$build" - - # 根据是否为预发布决定版本号最后一位 - # 如果是预发布,确保最后一位不为0(使用1) - if ($isPrerelease -and $build -eq 0) { - $build = 1 - $newVersion = "$major.$minor.$patch.$build" - } - $tagName = $newVersion - - echo "tag_name=$tagName" >> $env:GITHUB_OUTPUT - echo "version=$newVersion" >> $env:GITHUB_OUTPUT - echo "New tag: $tagName, version: $newVersion" - } - - - name: Determine release type - id: release_type - run: | - if ("${{ github.event_name }}" -eq "push") { - # 根据版本号最后一位确定是否为预发布版本 - # 最后一位为0表示正式版本,非0表示预发布版本 - $version = "${{ steps.get_tag.outputs.version }}" - $versionParts = $version.Split('.') - if ($versionParts.Length -ge 4) { - $build = [int]$versionParts[3] - if ($build -eq 0) { - echo "is_prerelease=false" >> $env:GITHUB_OUTPUT - echo "This is a release" - } else { - echo "is_prerelease=true" >> $env:GITHUB_OUTPUT - echo "This is a pre-release (beta)" - } - } else { - echo "is_prerelease=false" >> $env:GITHUB_OUTPUT - echo "This is a release (invalid version format)" - } - } else { - # workflow_dispatch 方式 - echo "is_prerelease=${{ github.event.inputs.prerelease }}" >> $env:GITHUB_OUTPUT - } - - # ========== 使用 git-cliff 生成变更日志 ========== - - name: Generate changelog with git-cliff (for pushed tag) - if: github.event_name == 'push' - id: git_cliff_tag - uses: orhun/git-cliff-action@v4 - with: - config: build/cliff.toml # 使用项目build目录的 cliff.toml 配置 - args: --latest --tag ${{ steps.get_tag.outputs.tag_name }} --output CHANGELOG.md - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate changelog with git-cliff (for workflow_dispatch) - if: github.event_name == 'workflow_dispatch' - id: git_cliff_unreleased - uses: orhun/git-cliff-action@v4 - with: - config: build/cliff.toml - args: --unreleased --tag ${{ steps.get_tag.outputs.tag_name }} --output CHANGELOG.md - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Read changelog content - id: read_changelog - run: | - $changelogContent = Get-Content -Path CHANGELOG.md -Raw - echo "changelog<> $env:GITHUB_OUTPUT - echo $changelogContent >> $env:GITHUB_OUTPUT - echo "EOF" >> $env:GITHUB_OUTPUT - - build: - needs: prepare - if: success() - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - architecture: [AnyCPU, x86] - outputs: - archive_name: ${{ steps.create_archive.outputs.archive_name }} - zip_size: ${{ steps.calculate_size.outputs.zip_size }} - installer_size: ${{ steps.calculate_installer_size.outputs.installer_size }} - - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 1 - - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v3 - - - name: Setup dotnet - uses: actions/setup-dotnet@v5 - with: - cache: true - cache-dependency-path: '**/packages.lock.json' - - - name: Restore Package - run: dotnet restore "Ink Canvas.sln" --locked-mode - - - name: Build the Solution (Release) - env: - DLASS_SENTRY_DSN: ${{ secrets.DLASS_SENTRY_DSN }} - run: | - msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false - - - name: Check if exe file is generated - id: check-exe - run: | - $exePath = "Ink Canvas/bin/Release/${{ matrix.architecture }}/net462/InkCanvasForClass.exe" - - if (Test-Path $exePath) { - echo "build_success=true" >> $env:GITHUB_OUTPUT - } else { - echo "build_success=false" >> $env:GITHUB_OUTPUT - exit 1 - } - - - name: Install Inno Setup Unofficial Language Files - if: steps.check-exe.outputs.build_success == 'true' - run: | - # 创建临时目录用于下载文件 - New-Item -ItemType Directory -Path "temp_lang" -Force - - # 下载英语英国版语言文件 - Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/refs/heads/main/Files/Languages/Unofficial/EnglishBritish.isl" -OutFile "temp_lang/EnglishBritish.isl" - - # 下载简体中文版语言文件 - Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/refs/heads/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "temp_lang/ChineseSimplified.isl" - - # 将文件移动到 Inno Setup 的语言目录 - Move-Item -Path "temp_lang/EnglishBritish.isl" -Destination "C:/Program Files (x86)/Inno Setup 6/Languages/EnglishBritish.isl" -Force - Move-Item -Path "temp_lang/ChineseSimplified.isl" -Destination "C:/Program Files (x86)/Inno Setup 6/Languages/ChineseSimplified.isl" -Force - - # 清理临时目录 - Remove-Item -Path "temp_lang" -Recurse -Force - - - name: Create Release Archive - id: create_archive - if: steps.check-exe.outputs.build_success == 'true' - run: | - $version = "${{ needs.prepare.outputs.version }}" - $architecture = "${{ matrix.architecture }}" - - # 根据架构生成文件名后缀 - if ($architecture -eq "AnyCPU") { - $suffix = "-x64" - } else { - $suffix = "" - } - - $archiveName = "InkCanvasForClass.CE.$version$suffix.zip" - - # 创建发布目录 - New-Item -ItemType Directory -Path "release" -Force - - # 复制发布文件 - Copy-Item "Ink Canvas/bin/Release/$architecture/net462/*" "release/" -Recurse -Force - - # 创建压缩包 - Compress-Archive -Path "release/*" -DestinationPath $archiveName -Force - - echo "archive_name=$archiveName" >> $env:GITHUB_OUTPUT - - - name: Prepare Inno Setup script - if: steps.check-exe.outputs.build_success == 'true' - run: | - $version = "${{ needs.prepare.outputs.version }}" - $architecture = "${{ matrix.architecture }}" - - # 更新 ISS 文件中的版本信息 - $issPath = "build/InkCanvasForClass CE.iss" - $issContent = Get-Content -Path $issPath -Raw - - # 替换版本信息 - $issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`"" - - # 替换源文件路径为相对路径(考虑到ISS文件在build目录下,需要返回上级目录) - $issContent = $issContent -replace 'Source: ".*\\{#MyAppExeName}";', 'Source: "../release/{#MyAppExeName}";' - $issContent = $issContent -replace 'Source: ".*\\InkCanvasForClass.exe.config";', 'Source: "../release/InkCanvasForClass.exe.config";' - - # 更新输出目录为当前目录 - $issContent = $issContent -replace 'OutputDir=.*', 'OutputDir=.' - - # 更新默认安装目录 - $issContent = $issContent -replace 'DefaultDirName=.*', 'DefaultDirName={autopf}/{#MyAppName}' - - # 更新许可证文件路径为相对路径(考虑到ISS文件在build目录下,需要返回上级目录) - $issContent = $issContent -replace 'LicenseFile=.*', 'LicenseFile=../LICENSE' - - # 保存修改后的 ISS 文件 - $issContent | Set-Content -Path $issPath -Encoding UTF8 - - - name: Build MSI installer with Inno Setup - if: steps.check-exe.outputs.build_success == 'true' - uses: Minionguyjpro/Inno-Setup-Action@v1.2.7 - with: - path: build/InkCanvasForClass CE.iss - options: /O. - - - name: Rename installer file - if: steps.check-exe.outputs.build_success == 'true' - run: | - $version = "${{ needs.prepare.outputs.version }}" - $architecture = "${{ matrix.architecture }}" - - # 根据架构生成文件名后缀 - if ($architecture -eq "AnyCPU") { - $suffix = "-x64" - } else { - $suffix = "" - } - - $setupFile = "InkCanvasForClass CE Setup.exe" - $newSetupName = "InkCanvasForClass.CE.$version$suffix.Setup.exe" - - if (Test-Path $setupFile) { - Rename-Item -Path $setupFile -NewName $newSetupName - } else { - Write-Error "Setup file not found: $setupFile" - } - - - name: Calculate archive size - id: calculate_size - if: steps.check-exe.outputs.build_success == 'true' - run: | - $version = "${{ needs.prepare.outputs.version }}" - $architecture = "${{ matrix.architecture }}" - - # 根据架构生成文件名后缀 - if ($architecture -eq "AnyCPU") { - $suffix = "-x64" - } else { - $suffix = "" - } - - $archiveName = "InkCanvasForClass.CE.$version$suffix.zip" - - # 获取文件大小(字节) - $fileSize = (Get-Item $archiveName).Length - - echo "zip_size=$fileSize" >> $env:GITHUB_OUTPUT - - - name: Calculate installer size - id: calculate_installer_size - if: steps.check-exe.outputs.build_success == 'true' - run: | - $version = "${{ needs.prepare.outputs.version }}" - $architecture = "${{ matrix.architecture }}" - - # 根据架构生成文件名后缀 - if ($architecture -eq "AnyCPU") { - $suffix = "-x64" - } else { - $suffix = "" - } - - $installerName = "InkCanvasForClass.CE.$version$suffix.Setup.exe" - - if (Test-Path $installerName) { - # 获取文件大小(字节) - $fileSize = (Get-Item $installerName).Length - - echo "installer_size=$fileSize" >> $env:GITHUB_OUTPUT - } else { - Write-Error "Installer file not found: $installerName" - } - - - name: Upload Build Artifacts - if: steps.check-exe.outputs.build_success == 'true' - run: | - $version = "${{ needs.prepare.outputs.version }}" - $architecture = "${{ matrix.architecture }}" - - # 根据架构生成文件名后缀 - if ($architecture -eq "AnyCPU") { - $suffix = "-x64" - } else { - $suffix = "" - } - - $zipFile = "InkCanvasForClass.CE.$version$suffix.zip" - $setupFile = "InkCanvasForClass.CE.$version$suffix.Setup.exe" - - echo "zip_file=$zipFile" >> $env:GITHUB_OUTPUT - echo "setup_file=$setupFile" >> $env:GITHUB_OUTPUT - id: get_file_names - - - name: Upload Build Artifacts - if: steps.check-exe.outputs.build_success == 'true' - uses: actions/upload-artifact@v7 - with: - name: build-files-${{ needs.prepare.outputs.version }}-${{ matrix.architecture }} - path: | - ${{ steps.get_file_names.outputs.zip_file }} - ${{ steps.get_file_names.outputs.setup_file }} - - - name: Create Build Summary - if: always() - shell: bash - run: | - echo "# Release Build Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then - echo "## ✅ Release Build Successful" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY - echo "**Release Type:** ${{ needs.prepare.outputs.is_prerelease == 'true' && 'Pre-release' || 'Release' }}" >> $GITHUB_STEP_SUMMARY - echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY - echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY - echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ -n "${{ steps.calculate_size.outputs.zip_size }}" ]; then - echo "**Archive Size:** ${{ steps.calculate_size.outputs.zip_size }} bytes" >> $GITHUB_STEP_SUMMARY - fi - - if [ -n "${{ steps.calculate_installer_size.outputs.installer_size }}" ]; then - echo "**Installer Size:** ${{ steps.calculate_installer_size.outputs.installer_size }} bytes" >> $GITHUB_STEP_SUMMARY - fi - - else - echo "## ❌ Release Build Failed" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY - echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY - echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY - echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY - echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY - fi - - sign: - needs: [prepare, build] - if: success() - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - name: Download Build Artifacts - uses: actions/download-artifact@v8 - with: - pattern: build-files-${{ needs.prepare.outputs.version }}-* - merge-multiple: false - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: '3.14' - - - name: Sign release artifacts with sigstore-python - uses: sigstore/gh-action-sigstore-python@v3.3.0 - with: - inputs: | - build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.zip - build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.exe - build-files-${{ needs.prepare.outputs.version }}-x86/*.zip - build-files-${{ needs.prepare.outputs.version }}-x86/*.exe - release-signing-artifacts: true - upload-signing-artifacts: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload Signed Artifacts - uses: actions/upload-artifact@v7 - with: - name: signed-files-${{ needs.prepare.outputs.version }} - path: | - build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.sigstore.json - build-files-${{ needs.prepare.outputs.version }}-x86/*.sigstore.json - - release: - needs: [prepare, build, sign] - if: success() - runs-on: ubuntu-slim - permissions: - contents: write - outputs: - enhanced_changelog: ${{steps.enhanced_changelog.outputs.enhanced_changelog}} - - steps: - - name: Download Build Artifacts - uses: actions/download-artifact@v8 - with: - pattern: build-files-${{ needs.prepare.outputs.version }}-* - merge-multiple: true - - - name: Download Signed Artifacts (if exists) - uses: actions/download-artifact@v8 - with: - name: signed-files-${{ needs.prepare.outputs.version }} - continue-on-error: true - - - name: Create enhanced changelog with file table - id: enhanced_changelog - run: | - version='${{ needs.prepare.outputs.version }}' - - # 读取git-cliff生成的changelog内容 - originalChangelog='${{ needs.prepare.outputs.changelog }}' - - # 检查是否为预发布版本,如果是则添加警告提示 - if [ '${{ needs.prepare.outputs.is_prerelease }}' = "true" ]; then - warningText=$'\n> [!CAUTION]\n' - warningText+=$'> **注意:此版本为预览或测试版**\n' - warningText+=$'> \n' - warningText+=$'> 请注意,这是一个预览/测试版本,使用时可能出现BUG,常规用户建议使用预览版或正式版\n\n' - originalChangelog="${warningText}${originalChangelog}" - fi - - # 构建文件信息表格 - fileTable=$'\n## 文件信息 (File Information)\n' - fileTable+=$'| 文件名 | 大小 |\n' - fileTable+=$'|--------|------|\n' - - # AnyCPU (x64) 架构文件 - if [ -f "InkCanvasForClass.CE.$version-x64.zip" ]; then - zipSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip") - fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip | '"$zipSize"' bytes |\n' - fi - - if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe" ]; then - installerSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe") - fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe | '"$installerSize"' bytes |\n' - fi - - if [ -f "InkCanvasForClass.CE.$version-x64.zip.sigstore.json" ]; then - sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip.sigstore.json") - fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip.sigstore.json | '"$sigstoreSize"' bytes |\n' - fi - - if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json" ]; then - sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json") - fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n' - fi - - # x86 架构文件 - if [ -f "InkCanvasForClass.CE.$version.zip" ]; then - zipSize=$(wc -c < "InkCanvasForClass.CE.$version.zip") - fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip | '"$zipSize"' bytes |\n' - fi - - if [ -f "InkCanvasForClass.CE.$version.Setup.exe" ]; then - installerSize=$(wc -c < "InkCanvasForClass.CE.$version.Setup.exe") - fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe | '"$installerSize"' bytes |\n' - fi - - if [ -f "InkCanvasForClass.CE.$version.zip.sigstore.json" ]; then - sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version.zip.sigstore.json") - fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip.sigstore.json | '"$sigstoreSize"' bytes |\n' - fi - - if [ -f "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json" ]; then - sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json") - fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n' - fi - - fileTable+=$'\n*文件大小信息由GitHub Actions自动生成*\n' - - # 将表格附加到原始changelog - enhancedChangelog="${originalChangelog}${fileTable}" - - # 输出增强版changelog内容 - echo "enhanced_changelog<> $GITHUB_OUTPUT - echo "$enhancedChangelog" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - echo "Enhanced changelog created with file information table" - - - name: Display Release Info - run: | - echo "=== Creating Release ===" - echo "Version: ${{ needs.prepare.outputs.version }}" - echo "Tag: ${{ needs.prepare.outputs.tag_name }}" - echo "Pre-release: ${{ needs.prepare.outputs.is_prerelease }}" - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ needs.prepare.outputs.tag_name }} - name: ICC CE ${{ needs.prepare.outputs.version }} - body: | - ${{ steps.enhanced_changelog.outputs.enhanced_changelog }} - draft: ${{ github.event.inputs.draft || false }} - prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }} - files: | - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip.sigstore.json - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe.sigstore.json - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe.sigstore.json - fail_on_unmatched_files: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - post_release: - needs: [prepare, release] - if: success() && github.event.inputs.draft != 'true' - runs-on: ubuntu-slim - permissions: - id-token: write - contents: write - steps: - - name: Download Build Artifacts - uses: actions/download-artifact@v8 - with: - pattern: build-files-${{ needs.prepare.outputs.version }}-* - merge-multiple: true - - - name: Get beta token - uses: octo-sts/action@main - id: octo-sts-beta - with: - scope: InkCanvasForClass/community-beta - identity: repo-sync - - - name: Get download token - uses: octo-sts/action@main - id: octo-sts-downloads - with: - scope: InkCanvasForClass/downloads - identity: repo-sync - - - name: Sync downloads repos(Universal) - env: - GITHUB_TOKEN: ${{ steps.octo-sts-downloads.outputs.token }} - run: | - set -e - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - REPO_DIR=$(mktemp -d) - git clone --depth 1 --filter=blob:none --branch main https://x-access-token:${{ steps.octo-sts-downloads.outputs.token }}@github.com/InkCanvasForClass/downloads.git $REPO_DIR - - cd $REPO_DIR - IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}" - VERSION="${{ needs.prepare.outputs.version }}" - X64_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION-x64.zip" - X86_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION.zip" - - if [ "$IS_PRERELEASE" == "true" ]; then - mkdir -p Beta - if [ -f "$X64_ZIP_FILE" ]; then - cp "$X64_ZIP_FILE" Beta/ - git add Beta/InkCanvasForClass.CE.$VERSION-x64.zip - fi - if [ -f "$X86_ZIP_FILE" ]; then - cp "$X86_ZIP_FILE" Beta/ - git add Beta/InkCanvasForClass.CE.$VERSION.zip - fi - git commit -m "Add $VERSION PreRelease" - else - mkdir -p Release Beta - if [ -f "$X64_ZIP_FILE" ]; then - cp "$X64_ZIP_FILE" Release/ - cp "$X64_ZIP_FILE" Beta/ - git add Release/InkCanvasForClass.CE.$VERSION-x64.zip Beta/InkCanvasForClass.CE.$VERSION-x64.zip - fi - if [ -f "$X86_ZIP_FILE" ]; then - cp "$X86_ZIP_FILE" Release/ - cp "$X86_ZIP_FILE" Beta/ - git add Release/InkCanvasForClass.CE.$VERSION.zip Beta/InkCanvasForClass.CE.$VERSION.zip - fi - git commit -m "Add $VERSION Release" - fi - git push origin main - - - name: Update AutomaticUpdateVersionControl in beta repo - env: - GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }} - run: | - CONTENT=$(echo -n "${{ needs.prepare.outputs.version }}" | base64 -w0) - - SHA=$(gh api /repos/InkCanvasForClass/community-beta/contents/AutomaticUpdateVersionControl.txt --jq '.sha' 2>/dev/null || echo "") - - gh api \ - --method PUT \ - /repos/InkCanvasForClass/community-beta/contents/AutomaticUpdateVersionControl.txt \ - -f message="Update AutomaticUpdateVersionControl.txt" \ - -f content="$CONTENT" \ - -f branch="main" \ - ${SHA:+-f sha="$SHA"} - - - name: Create GitHub Release on beta repo - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ needs.prepare.outputs.tag_name }} - name: ICC CE ${{ needs.prepare.outputs.version }} - body: | - ${{ needs.release.outputs.enhanced_changelog }} - draft: false - prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }} - files: | - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip - InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip - fail_on_unmatched_files: false - repository: "InkCanvasForClass/community-beta" - token: ${{ steps.octo-sts-beta.outputs.token }} - env: - GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }} - - - name: Update community repo AutomaticUpdateVersionControl - if: ${{needs.prepare.outputs.is_prerelease == 'false'}} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - CONTENT=$(echo -n "${{ needs.prepare.outputs.version }}" | base64 -w0) - - SHA=$(gh api /repos/InkCanvasForClass/community/contents/AutomaticUpdateVersionControl.txt --jq '.sha' 2>/dev/null || echo "") - - gh api \ - --method PUT \ - /repos/InkCanvasForClass/community/contents/AutomaticUpdateVersionControl.txt \ - -f message="Update AutomaticUpdateVersionControl.txt" \ - -f content="$CONTENT" \ - -f branch="beta" \ - ${SHA:+-f sha="$SHA"} diff --git a/.gitignore b/.gitignore index 5cec64f6..d0885ead 100644 --- a/.gitignore +++ b/.gitignore @@ -429,4 +429,5 @@ FodyWeavers.xsd # Telemetry DSN configuration file (contains sensitive information) telemetry_dsn.txt -**/telemetry_dsn.txt \ No newline at end of file +**/telemetry_dsn.txt +.trae/skills/migrate-toggle-switch/SKILL.md diff --git a/Ink Canvas.sln b/Ink Canvas.sln index 9e46f5cd..0bf57c17 100644 --- a/Ink Canvas.sln +++ b/Ink Canvas.sln @@ -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 @@ -39,6 +43,46 @@ Global {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 diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index 2238c408..c2d05853 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -1,5 +1,6 @@ using H.NotifyIcon; using Ink_Canvas.Helpers; +using Ink_Canvas.Plugins; using Ink_Canvas.Properties; using iNKORE.UI.WPF.Modern.Controls; using Microsoft.Win32; @@ -7,6 +8,7 @@ using Newtonsoft.Json; using Sentry; using System; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -17,6 +19,7 @@ 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; @@ -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,24 @@ 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)) { ShowSplashScreen(); SetSplashMessage(Strings.GetString("Splash_Starting")); - SetSplashProgress(20); - await Task.Delay(500); + SetSplashProgress(25); // 强制刷新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)); @@ -739,64 +814,13 @@ namespace Ink_Canvas LogHelper.WriteLogToFile("App | 检测到最终应用启动(更新后的应用)"); } - // 释放IACore相关DLL - if (_isSplashScreenShown) - { - SetSplashMessage("正在初始化组件..."); - SetSplashProgress(40); - await Task.Delay(500); - } - try - { - IACoreDllExtractor.ExtractIACoreDlls(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"释放IACore DLL时出错: {ex.Message}", LogHelper.LogType.Error); - } - // 释放UIAccess DLL - if (_isSplashScreenShown) - { - 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); + SetSplashProgress(50); + await Task.Delay(100); } - 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); @@ -831,7 +855,7 @@ namespace Ink_Canvas LogHelper.WriteLogToFile($"App | 清理更新标记文件失败: {ex.Message}", LogHelper.LogType.Warning); } - Task.Run(async () => + _ = Task.Run(async () => { try { @@ -1061,7 +1085,6 @@ namespace Ink_Canvas } _taskbar = (TaskbarIcon)FindResource("TaskbarTrayIcon"); - _taskbar.ForceCreate(); StartArgs = e.Args; @@ -1069,39 +1092,59 @@ namespace Ink_Canvas if (_isSplashScreenShown) { SetSplashMessage("正在初始化主界面..."); - SetSplashProgress(80); - await Task.Delay(500); + SetSplashProgress(75); } var mainWindow = new MainWindow(); MainWindow = mainWindow; + // 注册 InkCanvas 服务供插件使用 + try + { + var inkCanvasService = new Plugins.InkCanvasService(mainWindow); + Plugins.PluginManager.Instance.RegisterService(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(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("完成初始化..."); - SetSplashProgress(80); - Task.Delay(300).ContinueWith(_ => + SetSplashMessage("启动完成!"); + SetSplashProgress(100); + Task.Delay(100).ContinueWith(_ => { Dispatcher.Invoke(() => { - SetSplashMessage("启动完成!"); - SetSplashProgress(100); // 延迟关闭启动画面,让用户看到完成消息 - Task.Delay(500).ContinueWith(__ => + Task.Delay(100).ContinueWith(__ => { Dispatcher.Invoke(() => CloseSplashScreen()); }); @@ -1111,6 +1154,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)); @@ -1118,7 +1174,7 @@ namespace Ink_Canvas { LogHelper.WriteLogToFile($"App | 处理启动URI参数: {startupUriArg}", LogHelper.LogType.Event); // 延迟一点执行,确保窗口初始化完成 - Task.Delay(1000).ContinueWith(_ => + _ = Task.Delay(1000).ContinueWith(_ => { mainWindow.Dispatcher.Invoke(() => { @@ -1127,40 +1183,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) @@ -1437,6 +1576,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 { diff --git a/Ink Canvas/AssemblyInfo.cs b/Ink Canvas/AssemblyInfo.cs index affa789e..67fa9e01 100644 --- a/Ink Canvas/AssemblyInfo.cs +++ b/Ink Canvas/AssemblyInfo.cs @@ -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.9")] -[assembly: AssemblyFileVersion("1.7.18.9")] +[assembly: AssemblyVersion("1.7.18.10")] +[assembly: AssemblyFileVersion("1.7.18.10")] diff --git a/Ink Canvas/Controls/BoardMenuFrame.cs b/Ink Canvas/Controls/BoardMenuFrame.cs new file mode 100644 index 00000000..cadefa70 --- /dev/null +++ b/Ink Canvas/Controls/BoardMenuFrame.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/PptNavBar.xaml b/Ink Canvas/Controls/PptNavBar.xaml new file mode 100644 index 00000000..045195ca --- /dev/null +++ b/Ink Canvas/Controls/PptNavBar.xaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ink Canvas/Controls/PptNavBar.xaml.cs b/Ink Canvas/Controls/PptNavBar.xaml.cs new file mode 100644 index 00000000..53ae5e61 --- /dev/null +++ b/Ink Canvas/Controls/PptNavBar.xaml.cs @@ -0,0 +1,392 @@ +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; + +namespace Ink_Canvas.Controls +{ + /// + /// PPT 翻页 + 增强预览一体化控件。 + /// 通过 切换底部条 (LB/RB) 与侧边条 (LS/RS) 布局, + /// 预览列表内嵌于同一个 Border,展开时占据按钮组之外的剩余空间。 + /// + 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), 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 PreviewItems + { + get => (IList)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 SlideSelected; + public event EventHandler PreviousPressedDown; + public event EventHandler NextPressedDown; + public event EventHandler PressEnded; + public event EventHandler 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; + 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; + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/IToolbarHost.cs b/Ink Canvas/Controls/Toolbar/IToolbarHost.cs new file mode 100644 index 00000000..8d4106f4 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/IToolbarHost.cs @@ -0,0 +1,18 @@ +using System.Windows; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// 工具栏按钮插件与宿主之间的桥梁。Phase 1 粗粒度暴露 MainWindow,后续收窄。 + /// + public interface IToolbarHost + { + MainWindow Window { get; } + + /// 按 id 登记按钮的 view 实例(供 MainWindow 字段回填和互相查找)。 + void RegisterView(string id, FrameworkElement view); + + /// 按 id 获取之前注册的 view。不存在返回 null。 + FrameworkElement FindView(string id); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/IToolbarItem.cs b/Ink Canvas/Controls/Toolbar/IToolbarItem.cs new file mode 100644 index 00000000..0c7498c7 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/IToolbarItem.cs @@ -0,0 +1,29 @@ +using System.Windows; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// 一个工具栏按钮(或任意浮动栏/白板栏条目)的插件化契约。 + /// 实现类必须有无参构造函数,启动时会被 ToolbarRegistry 反射实例化。 + /// + public interface IToolbarItem + { + /// 稳定、唯一的 id,用于持久化用户配置。不要随便改。 + string Id { get; } + + ToolbarSlot DefaultSlot { get; } + + /// 同一 slot 内的默认顺序,小的在前。 + int DefaultOrder { get; } + + bool DefaultVisible { get; } + + ToolbarInsertPosition DefaultPosition { get; } + + /// 仅当 Position 为 BeforeAnchor/AfterAnchor 时有意义,对应 XAML 里 x:Name。 + string DefaultAnchorName { get; } + + /// 构造 UI 元素并接线所有行为。 + FrameworkElement BuildView(IToolbarHost host); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/ClearToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/ClearToolItem.cs new file mode 100644 index 00000000..39bd1585 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/ClearToolItem.cs @@ -0,0 +1,27 @@ +using System.Windows.Input; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + /// + /// 清空按钮。位置:夹在颜色面板与 StackPanelCanvasControls 之间, + /// 所以用 BeforeAnchor 锚到 StackPanelCanvasControls。 + /// + 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 string LabelBrushResourceKey => "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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/CursorToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/CursorToolItem.cs new file mode 100644 index 00000000..e86ba866 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/CursorToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/CursorWithDelToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/CursorWithDelToolItem.cs new file mode 100644 index 00000000..0234b7a5 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/CursorWithDelToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/EraserByStrokesToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/EraserByStrokesToolItem.cs new file mode 100644 index 00000000..752328c8 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/EraserByStrokesToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs new file mode 100644 index 00000000..fb44594a --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/EraserToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/FoldToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/FoldToolItem.cs new file mode 100644 index 00000000..dcb7942b --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/FoldToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs new file mode 100644 index 00000000..8f339eb0 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/PenToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/RedoToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/RedoToolItem.cs new file mode 100644 index 00000000..76da523b --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/RedoToolItem.cs @@ -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" }); + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/SelectToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/SelectToolItem.cs new file mode 100644 index 00000000..671a6c75 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/SelectToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs new file mode 100644 index 00000000..675cc43a --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/ShapeDrawToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs b/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs new file mode 100644 index 00000000..330322e5 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/ToolbarImageButtonItemBase.cs @@ -0,0 +1,54 @@ +using Ink_Canvas.Properties; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; + +namespace Ink_Canvas.Controls.Toolbar.Items +{ + /// + /// 通用 ToolbarImageButton 工具栏条目基类——大幅减少每个按钮的样板代码。 + /// 派生类通常只需给 Id / 本地化键 / Slot / Order / 点击处理 / Attach 回填。 + /// + 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; + + /// DynamicResource 名称,用于 IconBrush。默认为 null(使用控件自带前景色)。 + protected virtual string IconBrushResourceKey => null; + + /// DynamicResource 名称,用于 LabelBrush(文字颜色)。默认为 null(使用控件自带前景色)。 + protected virtual string LabelBrushResourceKey => null; + + protected abstract void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e); + + /// 构建后调用,用于回填 MainWindow 的原命名属性(partial 扩展里的 Attach*)。可选。 + 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); + } + if (!string.IsNullOrEmpty(LabelBrushResourceKey)) + { + if (btn.TryFindResource(LabelBrushResourceKey) is Brush brush) btn.LabelBrush = brush; + else btn.SetResourceReference(ToolbarImageButton.LabelBrushProperty, LabelBrushResourceKey); + } + btn.ButtonMouseUp += (s, e) => OnClick(host, s, e); + AfterBuild(host, btn); + return btn; + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs new file mode 100644 index 00000000..cb80b8f8 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/ToolsToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/UndoToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/UndoToolItem.cs new file mode 100644 index 00000000..7d006d83 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/UndoToolItem.cs @@ -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" }); + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/Items/WhiteboardToolItem.cs b/Ink Canvas/Controls/Toolbar/Items/WhiteboardToolItem.cs new file mode 100644 index 00000000..ffe50e68 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/Items/WhiteboardToolItem.cs @@ -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); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarHost.cs b/Ink Canvas/Controls/Toolbar/ToolbarHost.cs new file mode 100644 index 00000000..0418a941 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarHost.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Windows; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// MainWindow 版的 IToolbarHost 实现。Phase 1 直接把 MainWindow 引用暴露给插件, + /// 插件可通过 host.Window 访问私有/内部成员(partial class 扩展或 internal 字段)。 + /// 后续阶段逐步把具体行为抽成 Host 上的方法/事件,收窄这个接口。 + /// + public sealed class ToolbarHost : IToolbarHost + { + private readonly Dictionary _views = new Dictionary(); + + 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; + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarInsertPosition.cs b/Ink Canvas/Controls/Toolbar/ToolbarInsertPosition.cs new file mode 100644 index 00000000..4b1159ea --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarInsertPosition.cs @@ -0,0 +1,14 @@ +namespace Ink_Canvas.Controls.Toolbar +{ + public enum ToolbarInsertPosition + { + /// 从容器头部依次插入;Order 小的在前。 + Prepend, + /// 追加到容器末尾。 + Append, + /// 插入到由 AnchorName 指定的已有元素之前。 + BeforeAnchor, + /// 插入到由 AnchorName 指定的已有元素之后(同一锚点多项按 Order 依次排列)。 + AfterAnchor + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarItemConfig.cs b/Ink Canvas/Controls/Toolbar/ToolbarItemConfig.cs new file mode 100644 index 00000000..7a364e42 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarItemConfig.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// 单个工具栏按钮的用户配置(可见性、顺序、所属 slot、插入位置)。 + /// 由 Settings.Toolbar 持久化。 + /// + 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 Items { get; set; } = new Dictionary(); + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs b/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs new file mode 100644 index 00000000..3701c296 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarRegistry.cs @@ -0,0 +1,161 @@ +using Ink_Canvas.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Controls; + +namespace Ink_Canvas.Controls.Toolbar +{ + /// + /// 扫描当前程序集里的 IToolbarItem 实现,按用户配置(Settings.Toolbar)排序/过滤后注入到目标容器。 + /// + public static class ToolbarRegistry + { + private static List _items; + + public static IReadOnlyList 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; + } + + /// 按 slot 分配工具栏条目到对应容器。调用者负责清空目标容器里要被接管的旧内容。 + public static void Populate(IToolbarHost host, IDictionary slots, ToolbarLayoutSettings layout) + { + if (host == null || slots == null) return; + layout = layout ?? new ToolbarLayoutSettings(); + + var grouped = new Dictionary>(); + 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; + } + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Controls/Toolbar/ToolbarSlot.cs b/Ink Canvas/Controls/Toolbar/ToolbarSlot.cs new file mode 100644 index 00000000..79ca1e60 --- /dev/null +++ b/Ink Canvas/Controls/Toolbar/ToolbarSlot.cs @@ -0,0 +1,11 @@ +namespace Ink_Canvas.Controls.Toolbar +{ + public enum ToolbarSlot + { + FloatingBarMain, + FloatingBarCanvasControls, + FloatingBarEnd, + BlackboardLeft, + BlackboardRight + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/AnimationsHelper.cs b/Ink Canvas/Helpers/AnimationsHelper.cs index dbc1c030..076bd4fd 100644 --- a/Ink Canvas/Helpers/AnimationsHelper.cs +++ b/Ink Canvas/Helpers/AnimationsHelper.cs @@ -1,3 +1,4 @@ +using Ink_Canvas.Controls; using System; using System.Windows; using System.Windows.Media; @@ -7,6 +8,16 @@ 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); } } diff --git a/Ink Canvas/Helpers/AppRestartHelper.cs b/Ink Canvas/Helpers/AppRestartHelper.cs new file mode 100644 index 00000000..f7c4157f --- /dev/null +++ b/Ink Canvas/Helpers/AppRestartHelper.cs @@ -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}"); + } + } + } +} diff --git a/Ink Canvas/Helpers/AutoUpdateHelper.cs b/Ink Canvas/Helpers/AutoUpdateHelper.cs index 0b96e22d..544b5d76 100644 --- a/Ink Canvas/Helpers/AutoUpdateHelper.cs +++ b/Ink Canvas/Helpers/AutoUpdateHelper.cs @@ -27,6 +27,37 @@ 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 @@ -383,6 +414,8 @@ namespace Ink_Canvas.Helpers // 获取所有可用线路组,按延迟排序 public static async Task> GetAvailableLineGroupsOrdered(UpdateChannel channel) { + var cached = TryGetCachedOrderedGroups(channel); + if (cached != null) return cached; var groups = ChannelLineGroups[channel]; var availableGroups = new List<(UpdateLineGroup group, long delay)>(); @@ -468,9 +501,46 @@ namespace Ink_Canvas.Helpers LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error); } + CacheOrderedGroups(channel, orderedGroups); return orderedGroups; } + // 缓存按延迟排序后的线路组,避免短时间内重复测速 + private static readonly Dictionary groups, DateTime cachedAt)> _orderedGroupsCache + = new Dictionary, DateTime)>(); + private static readonly TimeSpan _orderedGroupsCacheTtl = TimeSpan.FromMinutes(15); + + private static List 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(entry.groups); + } + return null; + } + } + + private static void CacheOrderedGroups(UpdateChannel channel, List groups) + { + lock (_orderedGroupsCache) + { + _orderedGroupsCache[channel] = (new List(groups), DateTime.UtcNow); + } + } + + public static void InvalidateOrderedGroupsCache() + { + lock (_orderedGroupsCache) + { + _orderedGroupsCache.Clear(); + } + } + private static async Task GetDownloadUrlDelay(string url) { try @@ -945,6 +1015,7 @@ namespace Ink_Canvas.Helpers // 使用多线路组下载新版(支持自动切换) public static async Task DownloadSetupFileWithFallback(string version, List groups, Action progressCallback = null) { + var session = BeginDownloadSession(); try { version = NormalizeVersionForUpdate(version); @@ -979,8 +1050,19 @@ 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); // 智教联盟需要先获取真实下载地址 @@ -1006,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); @@ -1021,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); @@ -1033,6 +1128,10 @@ namespace Ink_Canvas.Helpers progressCallback?.Invoke(0, $"下载异常: {ex.Message}"); return false; } + finally + { + EndDownloadSession(session); + } } // 下载文件的具体实现 @@ -1043,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; @@ -1146,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; @@ -1206,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}"); @@ -1218,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) @@ -1339,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)) @@ -1355,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卡顿 @@ -1379,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); @@ -2201,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; @@ -2211,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); diff --git a/Ink Canvas/Helpers/BaseUploadQueue.cs b/Ink Canvas/Helpers/BaseUploadQueue.cs index 452eb729..c0714f35 100644 --- a/Ink Canvas/Helpers/BaseUploadQueue.cs +++ b/Ink Canvas/Helpers/BaseUploadQueue.cs @@ -519,7 +519,7 @@ namespace Ink_Canvas.Helpers /// /// 异步上传文件 /// - public async Task UploadFileAsync(string filePath, CancellationToken cancellationToken = default) + public Task UploadFileAsync(string filePath, CancellationToken cancellationToken = default) { try { @@ -528,19 +528,19 @@ namespace Ink_Canvas.Helpers // 检查是否启用 if (!IsUploadEnabled()) { - return false; + return Task.FromResult(false); } // 基本验证 if (!File.Exists(filePath)) { LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error); - return false; + return Task.FromResult(false); } if (!IsValidFile(filePath)) { - return false; + return Task.FromResult(false); } // 确保队列已初始化 @@ -552,7 +552,7 @@ namespace Ink_Canvas.Helpers // 加入队列 EnqueueFile(filePath, 0, cancellationToken); - return true; + return Task.FromResult(true); } catch (OperationCanceledException) { @@ -562,7 +562,7 @@ namespace Ink_Canvas.Helpers catch (Exception ex) { LogHelper.WriteLogToFile($"[{GetType().Name}] 加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error); - return false; + return Task.FromResult(false); } } diff --git a/Ink Canvas/Helpers/Converters.cs b/Ink Canvas/Helpers/Converters.cs index 4736f6ce..a6908ccc 100644 --- a/Ink Canvas/Helpers/Converters.cs +++ b/Ink Canvas/Helpers/Converters.cs @@ -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(); + } + } } diff --git a/Ink Canvas/Helpers/DebugConsoleManager.cs b/Ink Canvas/Helpers/DebugConsoleManager.cs new file mode 100644 index 00000000..ab48118b --- /dev/null +++ b/Ink Canvas/Helpers/DebugConsoleManager.cs @@ -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 { } + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/DeviceIdentifier.cs b/Ink Canvas/Helpers/DeviceIdentifier.cs index a78e1a4d..85b839b0 100644 --- a/Ink Canvas/Helpers/DeviceIdentifier.cs +++ b/Ink Canvas/Helpers/DeviceIdentifier.cs @@ -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 /// 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); + /// /// 基于硬件指纹生成25字符的设备ID /// @@ -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; + } + } + /// /// 保存使用统计 /// @@ -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,进入分级策略判断"); } // 计算最近活跃度(最后一次使用距今的天数) diff --git a/Ink Canvas/Helpers/ForegroundWindowInfo.cs b/Ink Canvas/Helpers/ForegroundWindowInfo.cs index 8759468a..b4c066ee 100644 --- a/Ink Canvas/Helpers/ForegroundWindowInfo.cs +++ b/Ink Canvas/Helpers/ForegroundWindowInfo.cs @@ -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(); diff --git a/Ink Canvas/Helpers/FullScreenHelper.cs b/Ink Canvas/Helpers/FullScreenHelper.cs index a1dced48..35368d88 100644 --- a/Ink Canvas/Helpers/FullScreenHelper.cs +++ b/Ink Canvas/Helpers/FullScreenHelper.cs @@ -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 /// /// 确保窗口全屏的Hook - /// 使用HandleProcessCorruptedStateExceptions,防止访问内存过程中因为一些致命异常导致程序崩溃 /// - [HandleProcessCorruptedStateExceptions] private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { //处理WM_WINDOWPOSCHANGING消息 diff --git a/Ink Canvas/Helpers/GlobalHotkeyManager.cs b/Ink Canvas/Helpers/GlobalHotkeyManager.cs index e0516650..7dea1ec1 100644 --- a/Ink Canvas/Helpers/GlobalHotkeyManager.cs +++ b/Ink Canvas/Helpers/GlobalHotkeyManager.cs @@ -567,6 +567,36 @@ namespace Ink_Canvas.Helpers } } + /// + /// 刷新多屏相关设置(开关和跟随鼠标策略)。 + /// + 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); + } + } + /// /// 获取当前屏幕信息 /// @@ -624,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; @@ -688,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) { @@ -800,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(); + } // 无论屏幕是否变化,都检查热键状态 // 这样可以确保热键状态始终与当前上下文保持一致 diff --git a/Ink Canvas/Helpers/InkRecognitionManager.cs b/Ink Canvas/Helpers/InkRecognitionManager.cs index 69c86b32..8c30d975 100644 --- a/Ink Canvas/Helpers/InkRecognitionManager.cs +++ b/Ink Canvas/Helpers/InkRecognitionManager.cs @@ -8,9 +8,9 @@ namespace Ink_Canvas.Helpers { private static InkRecognitionManager _instance; private static readonly object _lock = new object(); + private readonly object _initSync = new object(); private ModernInkProcessor _modernProcessor; - private ModernInkAnalyzer _modernAnalyzer; private bool _isModernSystemAvailable; private bool _isInitialized; @@ -31,35 +31,16 @@ namespace Ink_Canvas.Helpers } } - private InkRecognitionManager() - { - Initialize(); - } + private InkRecognitionManager() { } private void Initialize() { + if (_isInitialized) return; + try { - var tryModern = WinRtInkShapeRecognizer.IsApiAvailable && Environment.Is64BitProcess; - - _isModernSystemAvailable = false; - if (tryModern) - { - try - { - _modernProcessor = new ModernInkProcessor(); - _modernAnalyzer = new ModernInkAnalyzer(); - _isModernSystemAvailable = true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile("WinRT 墨迹初始化失败: " + ex.Message, LogHelper.LogType.Warning); - _isModernSystemAvailable = false; - _modernProcessor = null; - _modernAnalyzer = null; - } - } - + // 启动阶段只做能力探测,不做 WinRT 组件实例化(避免冷启动延迟) + _isModernSystemAvailable = WinRtInkShapeRecognizer.IsApiAvailable; _isInitialized = true; } catch (Exception ex) @@ -69,10 +50,41 @@ namespace Ink_Canvas.Helpers } } + 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 RecognizeShapeAsync( StrokeCollection strokes, ShapeRecognitionEngineMode mode) { + EnsureInitialized(); if (!_isInitialized || strokes == null || strokes.Count == 0) return Task.FromResult(InkShapeRecognitionResult.Empty); @@ -108,6 +120,7 @@ namespace Ink_Canvas.Helpers bool applyHandwritingBeautify = false, string handwritingFontFamilyList = null) { + EnsureInitialized(); if (!_isInitialized) { LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info); @@ -140,18 +153,11 @@ namespace Ink_Canvas.Helpers return Task.FromResult(strokes); } - if (!Environment.Is64BitProcess) + EnsureModernAnalyzerInitialized(); + if (_modernProcessor == null) { LogHelper.WriteLogToFile( - "[手写体] CorrectInkAsync 跳过:非 64 位进程,WinRT 手写体替换不可用。笔画数=" + strokes.Count, - LogHelper.LogType.Info); - return Task.FromResult(strokes); - } - - if (_modernAnalyzer == null) - { - LogHelper.WriteLogToFile( - "[手写体] CorrectInkAsync 跳过:ModernInkAnalyzer 未就绪(WinRT 初始化失败?)。笔画数=" + + "[手写体] CorrectInkAsync 跳过:ModernInkProcessor 未就绪(WinRT 初始化失败?)。笔画数=" + strokes.Count, LogHelper.LogType.Warning); return Task.FromResult(strokes); @@ -161,7 +167,7 @@ namespace Ink_Canvas.Helpers "[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count + ",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()), LogHelper.LogType.Info); - return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes, handwritingFontFamilyList); + return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(strokes, handwritingFontFamilyList); } catch (Exception ex) { @@ -171,19 +177,19 @@ namespace Ink_Canvas.Helpers } /// - /// WinRT 手写体识别(需 64 位进程、Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。 + /// WinRT 手写体识别(需 Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。 /// public Task RecognizeHandwritingAsync( StrokeCollection strokes, ShapeRecognitionEngineMode mode) { + EnsureInitialized(); if (!_isInitialized || strokes == null || strokes.Count == 0) return Task.FromResult(HandwritingRecognitionResult.Empty); try { - if (!Environment.Is64BitProcess - || !ShapeRecognitionRouter.ResolveUseWinRt(mode) + if (!ShapeRecognitionRouter.ResolveUseWinRt(mode) || !WinRtHandwritingRecognizer.IsApiAvailable) return Task.FromResult(HandwritingRecognitionResult.Empty); @@ -209,14 +215,13 @@ namespace Ink_Canvas.Helpers public string GetSystemInfo() { return _isModernSystemAvailable - ? $"现代化64位墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}" + ? $"现代化墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}" : $"传统墨迹识别系统 (IACore) - 进程架构: {Environment.Is64BitProcess}"; } public void Dispose() { _modernProcessor?.Dispose(); - _modernAnalyzer?.Dispose(); _isInitialized = false; } } @@ -238,20 +243,4 @@ namespace Ink_Canvas.Helpers { } } - - internal sealed class ModernInkAnalyzer : IDisposable - { - public Task AnalyzeAndCorrectAsync( - StrokeCollection strokes, - string handwritingFontFamilyList) - { - return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync( - strokes, - handwritingFontFamilyList); - } - - public void Dispose() - { - } - } } diff --git a/Ink Canvas/Helpers/InkRecognizeHelper.cs b/Ink Canvas/Helpers/InkRecognizeHelper.cs index d283aaa5..ed56e693 100644 --- a/Ink Canvas/Helpers/InkRecognizeHelper.cs +++ b/Ink Canvas/Helpers/InkRecognizeHelper.cs @@ -103,7 +103,6 @@ namespace Ink_Canvas.Helpers { try { - _ = InkRecognitionManager.Instance; if (ShapeRecognitionRouter.ResolveUseWinRt(mode)) { WinRtInkShapeRecognizer.Warmup(); @@ -118,7 +117,7 @@ namespace Ink_Canvas.Helpers } } - /// WinRT 手写识别(64 位 + Windows 10+)。 + /// WinRT 手写识别(Windows 10+)。 public static Task RecognizeHandwritingUnifiedAsync( StrokeCollection strokes, ShapeRecognitionEngineMode mode) => @@ -152,6 +151,9 @@ namespace Ink_Canvas.Helpers var node = legacy.InkDrawingNode; var shape = node.GetShape(); + if (shape == null) + return InkShapeRecognitionResult.Empty; + var hot = ClonePointCollection(node.HotPoints); return new InkShapeRecognitionResult( node.GetShapeName(), @@ -173,6 +175,9 @@ namespace Ink_Canvas.Helpers 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") diff --git a/Ink Canvas/Helpers/InkShapeRecognition.cs b/Ink Canvas/Helpers/InkShapeRecognition.cs index 0d04c0cf..ff87b14f 100644 --- a/Ink Canvas/Helpers/InkShapeRecognition.cs +++ b/Ink Canvas/Helpers/InkShapeRecognition.cs @@ -1,5 +1,4 @@ using OSVersionExtension; -using System; using System.Windows; using System.Windows.Ink; using System.Windows.Media; @@ -17,13 +16,13 @@ namespace Ink_Canvas.Helpers public static class ShapeRecognitionRouter { /// - /// 自动模式:按当前进程位数选择——64 位进程用 WinRT,32 位进程(含 x86 目标在 WOW64 下运行)用 IACore。 + /// 自动模式:在 Windows 10 及以上系统默认使用 WinRT,否则使用 IACore。 /// public static bool ResolveUseWinRt(ShapeRecognitionEngineMode mode) { if (mode == ShapeRecognitionEngineMode.WinRT) return true; if (mode == ShapeRecognitionEngineMode.IACore) return false; - return Environment.Is64BitProcess; + return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10; } public static bool ShouldRunShapeRecognition(bool inkToShapeEnabled, ShapeRecognitionEngineMode mode) @@ -31,7 +30,7 @@ namespace Ink_Canvas.Helpers if (!inkToShapeEnabled) return false; if (ResolveUseWinRt(mode)) return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10; - return !Environment.Is64BitProcess; + return true; } public static ShapeRecognitionEngineMode FromSettingsInt(int value) diff --git a/Ink Canvas/Helpers/LogHelper.cs b/Ink Canvas/Helpers/LogHelper.cs index 0b65f5a9..fe0d63a3 100644 --- a/Ink Canvas/Helpers/LogHelper.cs +++ b/Ink Canvas/Helpers/LogHelper.cs @@ -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)) diff --git a/Ink Canvas/Helpers/OleActiveObject.cs b/Ink Canvas/Helpers/OleActiveObject.cs new file mode 100644 index 00000000..fb7235e2 --- /dev/null +++ b/Ink Canvas/Helpers/OleActiveObject.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ink_Canvas.Helpers +{ + /// + /// .NET Core / 5+ 未提供 ,通过 OLE 实现等效行为。 + /// + 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; + } + } +} diff --git a/Ink Canvas/Helpers/PPTInkManager.cs b/Ink Canvas/Helpers/PPTInkManager.cs index 1dbde920..1a5c3263 100644 --- a/Ink Canvas/Helpers/PPTInkManager.cs +++ b/Ink Canvas/Helpers/PPTInkManager.cs @@ -460,7 +460,6 @@ namespace Ink_Canvas.Helpers _memoryStreams = new MemoryStream[_maxSlides + 2]; } CurrentStrokes?.Clear(); - LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace); } /// diff --git a/Ink Canvas/Helpers/PPTManager.cs b/Ink Canvas/Helpers/PPTManager.cs index a02c3371..4b1c8e23 100644 --- a/Ink Canvas/Helpers/PPTManager.cs +++ b/Ink Canvas/Helpers/PPTManager.cs @@ -271,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)) { @@ -298,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)) { @@ -410,6 +410,15 @@ 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); @@ -1255,7 +1264,6 @@ namespace Ink_Canvas.Helpers object slideNavigation = null; try { - LogHelper.WriteLogToFile($"尝试显示幻灯片导航 - 连接状态: {IsConnected}, 放映状态: {IsInSlideShow}", LogHelper.LogType.Trace); if (!IsConnected || !IsInSlideShow || PPTApplication == null) { @@ -1288,7 +1296,6 @@ namespace Ink_Canvas.Helpers { dynamic sn = slideNavigation; sn.Visible = true; - LogHelper.WriteLogToFile("成功显示幻灯片导航(PowerPoint模式)", LogHelper.LogType.Event); return true; } diff --git a/Ink Canvas/Helpers/PPTROTConnectionHelper.cs b/Ink Canvas/Helpers/PPTROTConnectionHelper.cs index a042ea0c..2e430ec8 100644 --- a/Ink Canvas/Helpers/PPTROTConnectionHelper.cs +++ b/Ink Canvas/Helpers/PPTROTConnectionHelper.cs @@ -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 diff --git a/Ink Canvas/Helpers/PPTUIManager.cs b/Ink Canvas/Helpers/PPTUIManager.cs index d500fd32..6ceed021 100644 --- a/Ink Canvas/Helpers/PPTUIManager.cs +++ b/Ink Canvas/Helpers/PPTUIManager.cs @@ -1,8 +1,6 @@ using System; using System.Windows; -using System.Windows.Controls; using System.Windows.Interop; -using System.Windows.Media; using System.Windows.Threading; namespace Ink_Canvas.Helpers @@ -86,18 +84,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 +100,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 +151,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 +160,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 = "/ ?"; + } + } + /// /// 处理PPT放映状态变化 /// @@ -386,16 +396,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 +425,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 } } diff --git a/Ink Canvas/Helpers/ROTPPTManager.cs b/Ink Canvas/Helpers/ROTPPTManager.cs index 4044058a..034c0d5b 100644 --- a/Ink Canvas/Helpers/ROTPPTManager.cs +++ b/Ink Canvas/Helpers/ROTPPTManager.cs @@ -29,96 +29,13 @@ namespace Ink_Canvas.Helpers public dynamic CurrentSlides { get; private set; } public dynamic CurrentSlide { get; private set; } public int SlidesCount { get; private set; } - public bool IsConnected - { - get - { - try - { - if (PPTApplication == null) return false; - if (!Marshal.IsComObject(PPTApplication)) return false; - // 尝试访问一个简单的属性来验证连接是否有效 - var _ = PPTApplication.Name; - return true; - } - catch (COMException comEx) - { - var hr = (uint)comEx.HResult; - // 如果COM对象已失效,返回false - if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706B5) - { - return false; - } - return false; - } - catch - { - return false; - } - } - } - public bool IsInSlideShow - { - get - { - object slideShowWindows = null; - object slideShowWindow = null; - object view = null; - try - { - if (PPTApplication == null || !Marshal.IsComObject(PPTApplication)) return false; + private volatile bool _cachedIsConnected; + private volatile bool _cachedIsInSlideShow; - slideShowWindows = PPTApplication.SlideShowWindows; - if (slideShowWindows == null) return false; + public bool IsConnected => _cachedIsConnected && PPTApplication != null; - dynamic ssw = slideShowWindows; - if (ssw.Count == 0) return false; - - try - { - slideShowWindow = ssw[1]; - if (slideShowWindow == null) return false; - - dynamic sswObj = slideShowWindow; - view = sswObj.View; - if (view == null) return false; - - return true; - } - catch (COMException comEx) - { - var hr = (uint)comEx.HResult; - if (hr == 0x8001010E || hr == 0x80004005) - { - DisconnectFromPPT(); - } - return false; - } - } - catch (COMException comEx) - { - var hr = (uint)comEx.HResult; - if (hr == 0x8001010E || hr == 0x80004005) - { - DisconnectFromPPT(); - } - LogHelper.WriteLogToFile($"检查PPT放映状态失败: {comEx.Message} (HR: 0x{hr:X8})", LogHelper.LogType.Warning); - return false; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"检查PPT放映状态时发生意外错误: {ex}", LogHelper.LogType.Warning); - return false; - } - finally - { - SafeReleaseComObject(view); - SafeReleaseComObject(slideShowWindow); - SafeReleaseComObject(slideShowWindows); - } - } - } + public bool IsInSlideShow => _cachedIsInSlideShow && _pptSlideShowWindow != null; public bool IsSupportWPS { get; set; } = false; public bool SkipAnimationsWhenNavigating { get; set; } = false; #endregion @@ -146,6 +63,8 @@ namespace Ink_Canvas.Helpers private bool _bindingEvents = false; private DateTime _updateTime; private int _lastPolledSlideNumber = -1; + private int _reconnectFailureCount = 0; + private DateTime _nextReconnectAttemptUtc = DateTime.MinValue; #endregion #region Constructor & Initialization @@ -171,7 +90,6 @@ namespace Ink_Canvas.Helpers Name = "PPTMonitoringThread" }; _monitoringThread.Start(); - LogHelper.WriteLogToFile("PPT监控已启动", LogHelper.LogType.Trace); } } @@ -191,43 +109,63 @@ namespace Ink_Canvas.Helpers } DisconnectFromPPT(); - LogHelper.WriteLogToFile("PPT监控已停止", LogHelper.LogType.Trace); } } + private int _isReloading; // 0 = idle, 1 = in reload + private volatile bool _suppressDisconnectEvent; + public void ReloadConnection() { if (_disposed) return; - LogHelper.WriteLogToFile("[ROT] 执行热重载:强制断开并重新连接", LogHelper.LogType.Event); - - lock (_monitoringLock) + if (Interlocked.CompareExchange(ref _isReloading, 1, 0) != 0) { - _shouldStop = true; - - if (_monitoringThread != null && _monitoringThread.IsAlive) - { - if (!_monitoringThread.Join(2000)) - { - LogHelper.WriteLogToFile("等待监控线程退出超时(热重载)", LogHelper.LogType.Warning); - } - } - - DisconnectFromPPT(); - _monitoringThread = null; - _shouldStop = false; - _isModuleUnloading = false; + return; } - StartMonitoring(); + try + { + LogHelper.WriteLogToFile("[ROT] 执行热重载:强制断开并重新连接", LogHelper.LogType.Event); + + lock (_monitoringLock) + { + _shouldStop = true; + + if (_monitoringThread != null && _monitoringThread.IsAlive) + { + if (!_monitoringThread.Join(2000)) + { + LogHelper.WriteLogToFile("等待监控线程退出超时(热重载)", LogHelper.LogType.Warning); + } + } + + _suppressDisconnectEvent = true; + try + { + DisconnectFromPPT(); + } + finally + { + _suppressDisconnectEvent = false; + } + _monitoringThread = null; + _shouldStop = false; + _isModuleUnloading = false; + } + + StartMonitoring(); + } + finally + { + Interlocked.Exchange(ref _isReloading, 0); + } } #endregion #region Connection Management private void PptComService() { - LogHelper.WriteLogToFile("PPT Monitor ReStarted", LogHelper.LogType.Trace); - _bindingEvents = false; _lastPolledSlideNumber = -1; _polling = 0; @@ -272,55 +210,9 @@ namespace Ink_Canvas.Helpers { if (wait) Thread.Sleep(1000); - PPTApplication = bestApp; - try { - _pptActivePresentation = PPTApplication.ActivePresentation; - _updateTime = DateTime.Now; - - try - { - _pptSlideShowWindow = _pptActivePresentation.SlideShowWindow; - tempTotalPage = GetTotalSlideIndex(_pptActivePresentation); - } - catch - { - tempTotalPage = -1; - } - - if (tempTotalPage == -1) - { - _lastPolledSlideNumber = -1; - _polling = 0; - } - else - { - try - { - _lastPolledSlideNumber = GetCurrentSlideIndex(_pptSlideShowWindow); - - if (GetCurrentSlideIndex(_pptSlideShowWindow) >= GetTotalSlideIndex(_pptActivePresentation)) _polling = 1; - else _polling = 0; - } - catch - { - _lastPolledSlideNumber = -1; - _polling = 1; - } - } - - ConnectToPPT(null); - - try - { - dynamic pptAppDynamic = PPTApplication; - LogHelper.WriteLogToFile($"成功绑定! {pptAppDynamic.Name}", LogHelper.LogType.Trace); - } - catch - { - LogHelper.WriteLogToFile("成功绑定!", LogHelper.LogType.Trace); - } + ConnectToPPT(bestApp); } catch (Exception ex) { @@ -338,7 +230,6 @@ namespace Ink_Canvas.Helpers } else if (bestApp == null && PPTApplication != null) { - LogHelper.WriteLogToFile("检测到PPT已关闭,断开连接", LogHelper.LogType.Trace); DisconnectFromPPT(); } } @@ -373,7 +264,6 @@ namespace Ink_Canvas.Helpers if (!PPTROTConnectionHelper.AreComObjectsEqual(_pptActivePresentation, activePresentation)) { - LogHelper.WriteLogToFile("检测到演示文稿切换,断开连接", LogHelper.LogType.Trace); DisconnectFromPPT(); continue; } @@ -381,13 +271,11 @@ namespace Ink_Canvas.Helpers catch (System.Runtime.InteropServices.InvalidComObjectException) { // COM对象已失效 - LogHelper.WriteLogToFile("检测到COM对象失效,断开连接", LogHelper.LogType.Trace); DisconnectFromPPT(); continue; } catch (COMException ex) when ((uint)ex.ErrorCode == 0x8001010A) { - LogHelper.WriteLogToFile("PowerPoint 忙,稍后重试", LogHelper.LogType.Trace); } catch (COMException comEx) { @@ -426,34 +314,12 @@ namespace Ink_Canvas.Helpers isSlideShowActive = true; dynamic activeSlideShowWindow = null; - try { - for (int i = 1; i <= count; i++) - { - try - { - dynamic ssw = slideShowWindows[i]; - if (PPTROTConnectionHelper.IsSlideShowWindowActive(ssw)) - { - activeSlideShowWindow = ssw; - break; - } - } - catch { } - } + activeSlideShowWindow = activePresentation.SlideShowWindow; } catch { } - if (activeSlideShowWindow == null) - { - try - { - activeSlideShowWindow = activePresentation.SlideShowWindow; - } - catch { } - } - if (activeSlideShowWindow != null) { slideShowWindow = activeSlideShowWindow; @@ -479,17 +345,12 @@ namespace Ink_Canvas.Helpers } } } + } - PPTROTConnectionHelper.SafeReleaseComObject(slideShowWindows); - } - else - { - PPTROTConnectionHelper.SafeReleaseComObject(slideShowWindows); - } + PPTROTConnectionHelper.SafeReleaseComObject(slideShowWindows); } catch (COMException ex) when ((uint)ex.ErrorCode == 0x8001010A) { - LogHelper.WriteLogToFile("PowerPoint 忙,稍后重试", LogHelper.LogType.Trace); } catch (Exception ex) { @@ -608,8 +469,6 @@ namespace Ink_Canvas.Helpers { if (_lastSlideShowState) { - LogHelper.WriteLogToFile("轮询检测到放映已结束", LogHelper.LogType.Trace); - PPTROTConnectionHelper.SafeReleaseComObject(_pptSlideShowWindow); _pptSlideShowWindow = null; _lastPolledSlideNumber = -1; @@ -617,6 +476,7 @@ namespace Ink_Canvas.Helpers SlidesCount = 0; _lastSlideShowState = false; + _cachedIsInSlideShow = false; SlideShowStateChanged?.Invoke(false); if (_pptActivePresentation != null) @@ -646,7 +506,6 @@ namespace Ink_Canvas.Helpers if (_shouldStop || _isModuleUnloading) { - LogHelper.WriteLogToFile("收到停止信号,退出循环", LogHelper.LogType.Trace); break; } @@ -671,422 +530,6 @@ namespace Ink_Canvas.Helpers } } - private void CheckAndConnectToPPT() - { - if (_isModuleUnloading) return; - - lock (_lockObject) - { - try - { - if (_isModuleUnloading) return; - - object bestApp = PPTROTConnectionHelper.GetAnyActivePowerPoint(PPTApplication, out int bestPriority, out int targetPriority); - bool needRebind = false; - - LogHelper.WriteLogToFile($"ROT扫描结果: now={targetPriority}, best={bestPriority}, bestApp={(bestApp != null ? "found" : "null")}", LogHelper.LogType.Trace); - - if (PPTApplication == null && bestApp != null) - { - needRebind = true; - } - else if (PPTApplication != null && bestApp != null && bestPriority > targetPriority) - { - if (!PPTROTConnectionHelper.AreComObjectsEqual(PPTApplication, bestApp)) - { - needRebind = true; - } - } - - if (needRebind) - { - LogHelper.WriteLogToFile($"需要重新绑定: bestPriority={bestPriority}, targetPriority={targetPriority}", LogHelper.LogType.Trace); - - bool wait = (PPTApplication != null); - DisconnectFromPPT(); - - if (bestApp != null) - { - if (wait) Thread.Sleep(1000); - - try - { - LogHelper.WriteLogToFile("使用dynamic类型连接", LogHelper.LogType.Trace); - PPTApplication = bestApp; - ConnectToPPT(null); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"连接失败: {ex.Message}", LogHelper.LogType.Warning); - PPTROTConnectionHelper.SafeReleaseComObject(bestApp); - } - } - } - else - { - if (bestApp != null && (PPTApplication == null || !PPTROTConnectionHelper.AreComObjectsEqual(PPTApplication, bestApp))) - { - PPTROTConnectionHelper.SafeReleaseComObject(bestApp); - bestApp = null; - } - } - - if (PPTApplication != null) - { - // 即使 _pptActivePresentation 为 null,也要检查(可能在轮询模式下需要初始化) - if (_pptActivePresentation == null) - { - try - { - _pptActivePresentation = PPTApplication.ActivePresentation; - if (_pptActivePresentation != null) - { - LogHelper.WriteLogToFile("轮询模式:初始化_pptActivePresentation", LogHelper.LogType.Trace); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"轮询模式:无法获取ActivePresentation: {ex.Message}", LogHelper.LogType.Trace); - } - } - - if (_pptActivePresentation != null) - { - CheckPresentationAndSlideShowState(); - } - } - } - catch (InvalidComObjectException) - { - // COM对象已失效,断开连接 - LogHelper.WriteLogToFile("检测到COM对象失效,断开连接", LogHelper.LogType.Trace); - DisconnectFromPPT(); - } - catch (COMException comEx) - { - // COM异常,记录并断开连接 - var hr = (uint)comEx.HResult; - LogHelper.WriteLogToFile($"PPT连接检查COM异常: {comEx.Message} (HR: 0x{hr:X8})", LogHelper.LogType.Warning); - DisconnectFromPPT(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"PptComService异常: {ex.Message}", LogHelper.LogType.Error); - if (PPTApplication != null) - { - DisconnectFromPPT(); - } - } - } - } - - private void CheckPresentationAndSlideShowState() - { - try - { - // 检查COM对象是否仍然有效 - if (PPTApplication == null || !System.Runtime.InteropServices.Marshal.IsComObject(PPTApplication)) - { - return; - } - - try - { - var _ = System.Runtime.InteropServices.Marshal.GetIUnknownForObject(PPTApplication); - System.Runtime.InteropServices.Marshal.Release(_); - } - catch (System.Runtime.InteropServices.InvalidComObjectException) - { - // COM对象已失效 - DisconnectFromPPT(); - return; - } - - dynamic activePresentation = null; - dynamic slideShowWindow = null; - - try - { - activePresentation = PPTApplication.ActivePresentation; - - if (activePresentation != null && _pptActivePresentation != null && !PPTROTConnectionHelper.AreComObjectsEqual(_pptActivePresentation, activePresentation)) - { - LogHelper.WriteLogToFile("检测到演示文稿切换,断开连接", LogHelper.LogType.Trace); - DisconnectFromPPT(); - return; - } - } - catch (System.Runtime.InteropServices.InvalidComObjectException) - { - // COM对象已失效 - LogHelper.WriteLogToFile("检测到COM对象失效,断开连接", LogHelper.LogType.Trace); - DisconnectFromPPT(); - return; - } - catch (COMException ex) when ((uint)ex.HResult == 0x8001010A) - { - LogHelper.WriteLogToFile("PowerPoint 忙,稍后重试", LogHelper.LogType.Trace); - return; - } - catch (COMException comEx) - { - // COM异常,对象可能已失效 - var hr = (uint)comEx.HResult; - LogHelper.WriteLogToFile($"检查演示文稿状态COM异常: {comEx.Message} (HR: 0x{hr:X8})", LogHelper.LogType.Warning); - DisconnectFromPPT(); - return; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"检查演示文稿状态失败: {ex.Message},继续使用轮询模式", LogHelper.LogType.Warning); - activePresentation = null; - } - finally - { - if (activePresentation != null && (_pptActivePresentation == null || !PPTROTConnectionHelper.AreComObjectsEqual(_pptActivePresentation, activePresentation))) - { - SafeReleaseComObject(activePresentation); - } - } - - bool isSlideShowActive = false; - try - { - if (activePresentation == null) - { - try - { - activePresentation = PPTApplication.ActivePresentation; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"访问ActivePresentation失败: {ex.Message},继续使用轮询模式", LogHelper.LogType.Warning); - } - } - - if (activePresentation != null) - { - try - { - dynamic slideShowWindows = PPTApplication.SlideShowWindows; - if (slideShowWindows != null && slideShowWindows.Count > 0) - { - isSlideShowActive = true; - LogHelper.WriteLogToFile($"检测到放映模式,轮询模式={_forcePolling}", LogHelper.LogType.Trace); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"检查SlideShowWindows失败: {ex.Message}", LogHelper.LogType.Trace); - } - } - - if (isSlideShowActive && activePresentation != null) - { - dynamic pres = activePresentation; - slideShowWindow = pres.SlideShowWindow; - if (_pptSlideShowWindow == null || !PPTROTConnectionHelper.IsValidSlideShowWindow(_pptSlideShowWindow)) - { - if (!PPTROTConnectionHelper.AreComObjectsEqual(_pptSlideShowWindow, slideShowWindow)) - { - SafeReleaseComObject(_pptSlideShowWindow); - _pptSlideShowWindow = slideShowWindow; - LogHelper.WriteLogToFile("发现窗口,成功设置 slideshowwindow", LogHelper.LogType.Trace); - } - } - } - } - catch (COMException ex) when ((uint)ex.HResult == 0x8001010A) - { - LogHelper.WriteLogToFile("PowerPoint 忙,稍后重试", LogHelper.LogType.Trace); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"发现窗口失败: {ex}", LogHelper.LogType.Warning); - } - finally - { - if (activePresentation != null && (_pptActivePresentation == null || !PPTROTConnectionHelper.AreComObjectsEqual(_pptActivePresentation, activePresentation))) - { - SafeReleaseComObject(activePresentation); - } - if (slideShowWindow != null && !PPTROTConnectionHelper.AreComObjectsEqual(_pptSlideShowWindow, slideShowWindow)) - { - SafeReleaseComObject(slideShowWindow); - } - } - - if (isSlideShowActive) - { - if ((DateTime.Now - _updateTime).TotalMilliseconds > 3000 || _forcePolling) - { - LogHelper.WriteLogToFile($"轮询", LogHelper.LogType.Trace); - - try - { - dynamic pres = _pptActivePresentation; - if (pres == null) - { - LogHelper.WriteLogToFile("_pptActivePresentation为null,无法轮询", LogHelper.LogType.Warning); - } - else - { - slideShowWindow = pres.SlideShowWindow; - - int tempTotalPage = -1; - if (slideShowWindow != null) - { - tempTotalPage = GetTotalSlideIndex(_pptActivePresentation); - } - - if (tempTotalPage == -1) - { - SlidesCount = 0; - _polling = 0; - } - else - { - try - { - if (_pptSlideShowWindow == null) - { - LogHelper.WriteLogToFile("轮询: _pptSlideShowWindow为null", LogHelper.LogType.Warning); - } - else - { - int currentPage = GetCurrentSlideIndex(_pptSlideShowWindow); - SlidesCount = tempTotalPage; - if (currentPage >= tempTotalPage) _polling = 1; - else _polling = 0; - - if (_lastPolledSlideNumber != -1 && currentPage != _lastPolledSlideNumber) - { - try - { - LogHelper.WriteLogToFile($"轮询模式检测到页码变化: {_lastPolledSlideNumber} -> {currentPage},触发事件", LogHelper.LogType.Trace); - SlideShowNextSlide?.Invoke(_pptSlideShowWindow); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"触发轮询模式幻灯片切换事件失败: {ex.Message}", LogHelper.LogType.Warning); - } - } - - _lastPolledSlideNumber = currentPage; - } - } - catch (Exception ex) - { - SlidesCount = 0; - _polling = 1; - LogHelper.WriteLogToFile($"获取当前页数失败: {ex}", LogHelper.LogType.Warning); - } - } - } - } - catch (Exception ex) - { - SlidesCount = 0; - LogHelper.WriteLogToFile($"获取总页数失败: {ex.Message}", LogHelper.LogType.Warning); - } - finally - { - SafeReleaseComObject(slideShowWindow); - } - - _updateTime = DateTime.Now; - } - - if (_polling != 0) - { - try - { - int currentPage = GetCurrentSlideIndex(_pptSlideShowWindow); - - if (_lastPolledSlideNumber != -1 && currentPage != _lastPolledSlideNumber) - { - try - { - LogHelper.WriteLogToFile($"轮询模式检测到页码变化: {_lastPolledSlideNumber} -> {currentPage},触发事件", LogHelper.LogType.Trace); - SlideShowNextSlide?.Invoke(_pptSlideShowWindow); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"触发轮询模式幻灯片切换事件失败: {ex.Message}", LogHelper.LogType.Warning); - } - } - - _lastPolledSlideNumber = currentPage; - UpdateCurrentPresentationInfo(); - _polling = 2; - } - catch - { - } - } - } - else - { - if (_lastSlideShowState) - { - LogHelper.WriteLogToFile("轮询检测到放映已结束", LogHelper.LogType.Trace); - - PPTROTConnectionHelper.SafeReleaseComObject(_pptSlideShowWindow); - _pptSlideShowWindow = null; - _lastPolledSlideNumber = -1; - _polling = 1; - SlidesCount = 0; - - _lastSlideShowState = false; - SlideShowStateChanged?.Invoke(false); - - if (_pptActivePresentation != null) - { - try - { - OnSlideShowEnd(_pptActivePresentation); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"触发SlideShowEnd事件失败: {ex.Message}", LogHelper.LogType.Warning); - } - } - } - else - { - SlidesCount = 0; - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"检查演示文稿和放映状态失败: {ex}", LogHelper.LogType.Warning); - } - } - - private void CheckSlideShowState() - { - try - { - if (!IsConnected) return; - - var currentSlideShowState = IsInSlideShow; - if (currentSlideShowState != _lastSlideShowState) - { - _lastSlideShowState = currentSlideShowState; - SlideShowStateChanged?.Invoke(currentSlideShowState); - - if (!currentSlideShowState) - { - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"检查PPT放映状态异常: {ex}", LogHelper.LogType.Error); - } - } - private Microsoft.Office.Interop.PowerPoint.Application TryConnectToPowerPoint() { try @@ -1101,20 +544,6 @@ namespace Ink_Canvas.Helpers } } - private Microsoft.Office.Interop.PowerPoint.Application TryConnectToWPS() - { - try - { - var wpsApp = PPTROTConnectionHelper.TryConnectViaROT(true); - return wpsApp; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"ROT连接WPS异常: {ex}", LogHelper.LogType.Error); - return null; - } - } - private void ConnectToPPT(object pptApp) { try @@ -1214,21 +643,19 @@ namespace Ink_Canvas.Helpers } _bindingEvents = true; + _forcePolling = false; - LogHelper.WriteLogToFile("PPT事件注册成功", LogHelper.LogType.Trace); } else { _bindingEvents = false; _forcePolling = true; - LogHelper.WriteLogToFile("无法转换为强类型Application,使用轮询模式", LogHelper.LogType.Trace); } } - catch (Exception ex) + catch (Exception) { _bindingEvents = false; _forcePolling = true; - LogHelper.WriteLogToFile($"事件注册失败: {ex.Message},使用轮询模式", LogHelper.LogType.Trace); } } else @@ -1244,15 +671,17 @@ namespace Ink_Canvas.Helpers LogHelper.WriteLogToFile($"无法注册事件: {ex.Message}", LogHelper.LogType.Warning); } - _bindingEvents = false; - _forcePolling = true; - if (_pptActivePresentation != null) { UpdateCurrentPresentationInfo(); } - PPTConnectionChanged?.Invoke(true); + bool wasConnected = _cachedIsConnected; + _cachedIsConnected = true; + if (!wasConnected) + { + PPTConnectionChanged?.Invoke(true); + } try { @@ -1284,6 +713,9 @@ namespace Ink_Canvas.Helpers private void TryRaiseSlideShowBeginOnConnect() { + dynamic slideShowWindows = null; + dynamic slideShowWindow = null; + try { if (!IsConnected || !IsInSlideShow || PPTApplication == null) @@ -1293,26 +725,43 @@ namespace Ink_Canvas.Helpers return; dynamic app = PPTApplication; - dynamic slideShowWindows = app.SlideShowWindows; + slideShowWindows = app.SlideShowWindows; if (slideShowWindows == null || slideShowWindows.Count == 0) return; - dynamic slideShowWindow = slideShowWindows[1]; + slideShowWindow = slideShowWindows[1]; if (slideShowWindow == null) return; SlideShowBegin?.Invoke(slideShowWindow); - LogHelper.WriteLogToFile("检测到放映已在进行中,热重载", LogHelper.LogType.Trace); } catch (COMException comEx) { var hr = (uint)comEx.HResult; - LogHelper.WriteLogToFile($"TryRaiseSlideShowBeginOnConnect COM 异常: {comEx.Message} (HR: 0x{hr:X8})", LogHelper.LogType.Trace); } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"TryRaiseSlideShowBeginOnConnect 异常: {ex}", LogHelper.LogType.Trace); } + finally + { + if (slideShowWindow != null && !PPTROTConnectionHelper.AreComObjectsEqual(_pptSlideShowWindow, slideShowWindow)) + { + SafeReleaseComObject(slideShowWindow); + } + + if (slideShowWindows != null) + { + SafeReleaseComObject(slideShowWindows); + } + } + } + + private void ApplyReconnectBackoff() + { + _reconnectFailureCount = Math.Min(_reconnectFailureCount + 1, 4); + int delayMs = 250 * (1 << (_reconnectFailureCount - 1)); + _nextReconnectAttemptUtc = DateTime.UtcNow.AddMilliseconds(delayMs); + } private void UnbindEvents() @@ -1333,9 +782,8 @@ namespace Ink_Canvas.Helpers app.PresentationBeforeClose -= new EApplication_PresentationBeforeCloseEventHandler(OnPresentationBeforeClose); } } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"取消PPT事件注册失败: {ex.Message}", LogHelper.LogType.Trace); } _bindingEvents = false; @@ -1370,11 +818,11 @@ namespace Ink_Canvas.Helpers { UnbindEvents(); - if (_pptActivePresentation != null) + if (activePresentation != null) { try { - PresentationClose?.Invoke(_pptActivePresentation); + PresentationClose?.Invoke(activePresentation); } catch (Exception ex) { @@ -1382,69 +830,12 @@ namespace Ink_Canvas.Helpers } } - SafeReleaseComObject(_pptSlideShowWindow, "_pptSlideShowWindow"); - SafeReleaseComObject(_pptActivePresentation, "_pptActivePresentation"); - SafeReleaseComObject(CurrentSlide, "CurrentSlide"); - SafeReleaseComObject(CurrentSlides, "CurrentSlides"); - SafeReleaseComObject(CurrentPresentation, "CurrentPresentation"); - - if (PPTApplication != null) - { - try - { - if (Marshal.IsComObject(PPTApplication)) - { - try - { - var unk = Marshal.GetIUnknownForObject(PPTApplication); - Marshal.Release(unk); - - try - { - Marshal.FinalReleaseComObject(PPTApplication); - } - catch - { - try - { - int refCount = Marshal.ReleaseComObject(PPTApplication); - while (refCount > 0) - { - refCount = Marshal.ReleaseComObject(PPTApplication); - } - } - catch - { - } - } - } - catch (InvalidComObjectException) - { - LogHelper.WriteLogToFile("PPTApplication COM对象已失效,跳过释放", LogHelper.LogType.Trace); - } - catch (COMException comEx) when (IsIgnorableDisconnectComException(comEx)) - { - LogHelper.WriteLogToFile( - $"PPTApplication COM对象在断开连接时已不可用,跳过释放 (HR: 0x{(uint)comEx.HResult:X8})", - LogHelper.LogType.Trace); - } - } - } - catch (InvalidComObjectException) - { - LogHelper.WriteLogToFile("PPTApplication COM对象已失效,跳过释放", LogHelper.LogType.Trace); - } - catch (COMException comEx) when (IsIgnorableDisconnectComException(comEx)) - { - LogHelper.WriteLogToFile( - $"释放PPTApplication时检测到COM对象已不可用,跳过释放 (HR: 0x{(uint)comEx.HResult:X8})", - LogHelper.LogType.Trace); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"释放PPTApplication时发生异常: {ex.Message}", LogHelper.LogType.Warning); - } - } + SafeReleaseComObject(slideShowWindow, "_pptSlideShowWindow"); + SafeReleaseComObject(activePresentation, "_pptActivePresentation"); + SafeReleaseComObject(currentSlide, "CurrentSlide"); + SafeReleaseComObject(currentSlides, "CurrentSlides"); + SafeReleaseComObject(currentPresentation, "CurrentPresentation"); + SafeFinalReleaseComObject(pptApplication, "PPTApplication"); PPTApplication = null; _pptActivePresentation = null; @@ -1457,7 +848,13 @@ namespace Ink_Canvas.Helpers _forcePolling = true; _bindingEvents = false; - PPTConnectionChanged?.Invoke(false); + bool wasConnected = _cachedIsConnected; + _cachedIsConnected = false; + _cachedIsInSlideShow = false; + if (wasConnected && !_suppressDisconnectEvent) + { + PPTConnectionChanged?.Invoke(false); + } LogHelper.WriteLogToFile("已断开PPT连接,并显式释放所有COM对象", LogHelper.LogType.Event); @@ -1468,7 +865,6 @@ namespace Ink_Canvas.Helpers Thread.Sleep(2000); _isModuleUnloading = false; - LogHelper.WriteLogToFile("PPT联动模块已允许重新连接", LogHelper.LogType.Trace); } catch (Exception ex) { @@ -1521,25 +917,19 @@ namespace Ink_Canvas.Helpers catch (InvalidComObjectException) { // COM对象已失效,直接返回 - LogHelper.WriteLogToFile($"COM对象 {objectName} 已失效,跳过释放", LogHelper.LogType.Trace); return; } catch (COMException comEx) when (IsIgnorableDisconnectComException(comEx)) { - LogHelper.WriteLogToFile( - $"COM对象 {objectName} 在释放前已不可用,跳过释放 (HR: 0x{(uint)comEx.HResult:X8})", - LogHelper.LogType.Trace); return; } int refCount = Marshal.ReleaseComObject(comObject); - LogHelper.WriteLogToFile($"已释放COM对象 {objectName},引用计数: {refCount}", LogHelper.LogType.Trace); } } catch (InvalidComObjectException) { // COM对象已失效,这是正常的,无需记录错误 - LogHelper.WriteLogToFile($"COM对象 {objectName} 已失效,跳过释放", LogHelper.LogType.Trace); } catch (COMException comEx) { @@ -1553,6 +943,45 @@ namespace Ink_Canvas.Helpers } } + /// + /// 终态释放 COM 对象:先尝试 FinalReleaseComObject 一次性归零引用计数,失败时回退为循环 ReleaseComObject。 + /// + private void SafeFinalReleaseComObject(object comObject, string objectName) + { + if (comObject == null) return; + try + { + if (!Marshal.IsComObject(comObject)) return; + + try + { + Marshal.FinalReleaseComObject(comObject); + } + catch + { + try + { + int refCount = Marshal.ReleaseComObject(comObject); + while (refCount > 0) + { + refCount = Marshal.ReleaseComObject(comObject); + } + } + catch { } + } + } + catch (InvalidComObjectException) + { + } + catch (COMException comEx) when (IsIgnorableDisconnectComException(comEx)) + { + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"最终释放COM对象 {objectName} 时发生异常: {ex.Message}", LogHelper.LogType.Warning); + } + } + private static bool IsIgnorableDisconnectComException(COMException comEx) { var hr = (uint)comEx.HResult; @@ -1653,9 +1082,8 @@ namespace Ink_Canvas.Helpers CurrentSlide = viewObj.Slide; } } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"获取SlideShowWindow的Slide失败: {ex.Message}", LogHelper.LogType.Trace); } } else @@ -1690,7 +1118,7 @@ namespace Ink_Canvas.Helpers catch (COMException comEx) { var hr = (uint)comEx.HResult; - if (hr != 0x8001010E && hr != 0x80004005) + if (hr != 0x8001010E && hr != 0x80004005 && hr != 0x80048240) { LogHelper.WriteLogToFile($"获取当前幻灯片失败: {comEx.Message}", LogHelper.LogType.Warning); } @@ -1789,8 +1217,6 @@ namespace Ink_Canvas.Helpers { try { - LogHelper.WriteLogToFile("PresentationBeforeClose事件触发", LogHelper.LogType.Trace); - if (_bindingEvents && PPTApplication != null) { try @@ -1805,9 +1231,8 @@ namespace Ink_Canvas.Helpers app.PresentationBeforeClose -= new EApplication_PresentationBeforeCloseEventHandler(OnPresentationBeforeClose); } } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"取消PPT事件注册失败: {ex.Message}", LogHelper.LogType.Trace); } _bindingEvents = false; @@ -1829,6 +1254,42 @@ namespace Ink_Canvas.Helpers _pptSlideShowWindow = wn; _lastPolledSlideNumber = -1; // 重置页码跟踪 + // 兜底:ROT 路径下事件触发时 _pptActivePresentation 可能尚未绑定, + // 先从 SlideShowWindow.Presentation 取,再退回 PPTApplication.ActivePresentation, + // 保证后续 SlidesCount 在 UI 读取前一定有效。 + if (_pptActivePresentation == null) + { + try + { + if (wn != null) + { + dynamic ssw = wn; + dynamic presFromWindow = ssw.Presentation; + if (presFromWindow != null) + { + _pptActivePresentation = presFromWindow; + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"OnSlideShowBegin 从 SlideShowWindow 兜底获取演示文稿失败: {ex.Message}", LogHelper.LogType.Warning); + } + + if (_pptActivePresentation == null && PPTApplication != null) + { + try + { + dynamic app = PPTApplication; + _pptActivePresentation = app.ActivePresentation; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"OnSlideShowBegin 从 PPTApplication 兜底获取演示文稿失败: {ex.Message}", LogHelper.LogType.Warning); + } + } + } + try { if (_pptActivePresentation != null) @@ -1854,6 +1315,7 @@ namespace Ink_Canvas.Helpers if (!_lastSlideShowState) { _lastSlideShowState = true; + _cachedIsInSlideShow = true; SlideShowStateChanged?.Invoke(true); } @@ -1920,6 +1382,7 @@ namespace Ink_Canvas.Helpers if (_lastSlideShowState) { _lastSlideShowState = false; + _cachedIsInSlideShow = false; SlideShowStateChanged?.Invoke(false); } @@ -2542,8 +2005,6 @@ namespace Ink_Canvas.Helpers object slideNavigation = null; try { - LogHelper.WriteLogToFile($"尝试显示幻灯片导航 - 连接状态: {IsConnected}, 放映状态: {IsInSlideShow}", LogHelper.LogType.Trace); - if (!IsConnected || !IsInSlideShow || PPTApplication == null) { LogHelper.WriteLogToFile("PPT未连接或未在放映状态", LogHelper.LogType.Warning); @@ -2575,7 +2036,6 @@ namespace Ink_Canvas.Helpers { dynamic sn = slideNavigation; sn.Visible = true; - LogHelper.WriteLogToFile("成功显示幻灯片导航(PowerPoint模式)", LogHelper.LogType.Event); return true; } @@ -2663,8 +2123,6 @@ namespace Ink_Canvas.Helpers _hasWpsProcessId = true; _wpsProcessRecordTime = DateTime.Now; _wpsProcessCheckCount = 0; - LogHelper.WriteLogToFile($"成功记录 WPS 进程 ID: {wpsProcess.Id}", LogHelper.LogType.Trace); - StartWpsProcessCheckTimer(); } else @@ -2692,7 +2150,6 @@ namespace Ink_Canvas.Helpers _wpsProcessCheckTimer = new Timer(2000); _wpsProcessCheckTimer.Elapsed += OnWpsProcessCheckTimerElapsed; _wpsProcessCheckTimer.Start(); - LogHelper.WriteLogToFile("启动 WPS 进程检测定时器", LogHelper.LogType.Trace); } private void OnWpsProcessCheckTimerElapsed(object sender, ElapsedEventArgs e) @@ -2716,7 +2173,6 @@ namespace Ink_Canvas.Helpers if (_wpsProcess.HasExited) { - LogHelper.WriteLogToFile("WPS 进程已正常关闭", LogHelper.LogType.Trace); StopWpsProcessCheckTimer(); return; } @@ -2729,7 +2185,6 @@ namespace Ink_Canvas.Helpers { if (_wpsProcessCheckCount % 5 == 0) // 每10秒记录一次日志 { - LogHelper.WriteLogToFile($"WPS窗口仍然活跃,继续监控(已检查{_wpsProcessCheckCount}次)", LogHelper.LogType.Trace); } return; } @@ -2737,7 +2192,6 @@ namespace Ink_Canvas.Helpers // 多重验证确保准确性 if (!PerformMultipleWpsWindowChecks()) { - LogHelper.WriteLogToFile("多重验证显示WPS窗口仍然存在,跳过查杀", LogHelper.LogType.Trace); return; } @@ -2797,7 +2251,6 @@ namespace Ink_Canvas.Helpers Thread.Sleep(1000); if (IsForegroundWpsWindowStillActiveOptimized()) { - LogHelper.WriteLogToFile("第一重验证:WPS窗口仍然存在", LogHelper.LogType.Trace); return false; } @@ -2810,7 +2263,6 @@ namespace Ink_Canvas.Helpers var windows = GetWpsWindowsByProcess(process.Id); if (windows.Any(w => w.IsVisible && !w.IsMinimized)) { - LogHelper.WriteLogToFile($"第二重验证:发现其他WPS进程{process.Id}有活跃窗口", LogHelper.LogType.Trace); return false; } } @@ -2818,7 +2270,6 @@ namespace Ink_Canvas.Helpers // 第三重验证:检查任务栏中的WPS窗口 if (HasWpsWindowInTaskbar()) { - LogHelper.WriteLogToFile("第三重验证:任务栏中仍有WPS窗口", LogHelper.LogType.Trace); return false; } @@ -2876,7 +2327,6 @@ namespace Ink_Canvas.Helpers { if (_wpsProcess == null || _wpsProcess.HasExited) { - LogHelper.WriteLogToFile("WPS进程已经结束,无需查杀", LogHelper.LogType.Trace); StopWpsProcessCheckTimer(); return; } @@ -2904,7 +2354,6 @@ namespace Ink_Canvas.Helpers { Marshal.ReleaseComObject(pptActWindow); pptActWindow = null; - LogHelper.WriteLogToFile("已释放pptActWindow对象", LogHelper.LogType.Trace); } // 第二步:释放 pptActDoc 对象(CurrentPresentation) @@ -2914,7 +2363,6 @@ namespace Ink_Canvas.Helpers Marshal.ReleaseComObject(pptActDoc); pptActDoc = null; CurrentPresentation = null; - LogHelper.WriteLogToFile("已释放pptActDoc对象", LogHelper.LogType.Trace); } // 第三步:释放 pptApp 对象(PPTApplication) @@ -2924,17 +2372,14 @@ namespace Ink_Canvas.Helpers { Marshal.ReleaseComObject(PPTApplication); PPTApplication = null; - LogHelper.WriteLogToFile("已释放pptApp对象", LogHelper.LogType.Trace); } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"释放pptApp对象失败: {ex.Message}", LogHelper.LogType.Trace); PPTApplication = null; } } // 第四步:强制垃圾回收及等待终结器执行 - LogHelper.WriteLogToFile("执行强制垃圾回收", LogHelper.LogType.Trace); GC.Collect(); GC.WaitForPendingFinalizers(); @@ -3020,7 +2465,13 @@ namespace Ink_Canvas.Helpers SlidesCount = 0; StopWpsProcessCheckTimer(); - PPTConnectionChanged?.Invoke(false); + bool wasConnected = _cachedIsConnected; + _cachedIsConnected = false; + _cachedIsInSlideShow = false; + if (wasConnected) + { + PPTConnectionChanged?.Invoke(false); + } LogHelper.WriteLogToFile("WPS进程结束后已清理所有COM对象并重启连接检查", LogHelper.LogType.Event); } @@ -3043,7 +2494,6 @@ namespace Ink_Canvas.Helpers _wpsProcessCheckCount = 0; _lastForegroundWpsWindow = null; _lastWindowCheckTime = DateTime.MinValue; - LogHelper.WriteLogToFile("停止 WPS 进程检测定时器", LogHelper.LogType.Trace); } #endregion @@ -3236,7 +2686,6 @@ namespace Ink_Canvas.Helpers if (processPath.Contains("kingsoft") || processPath.Contains("wps office")) { wpsProcesses.Add(process); - LogHelper.WriteLogToFile($"检测到WPS进程: {process.ProcessName} (PID: {process.Id})", LogHelper.LogType.Trace); } } catch @@ -3245,7 +2694,6 @@ namespace Ink_Canvas.Helpers if (exactWpsNames.Contains(pname)) { wpsProcesses.Add(process); - LogHelper.WriteLogToFile($"基于进程名检测到WPS进程: {process.ProcessName} (PID: {process.Id})", LogHelper.LogType.Trace); } } } @@ -3261,7 +2709,6 @@ namespace Ink_Canvas.Helpers LogHelper.WriteLogToFile($"获取WPS进程失败: {ex}", LogHelper.LogType.Error); } - LogHelper.WriteLogToFile($"共检测到{wpsProcesses.Count}个WPS进程", LogHelper.LogType.Trace); return wpsProcesses; } @@ -3278,16 +2725,13 @@ namespace Ink_Canvas.Helpers if (_lastForegroundWpsWindow.Handle != currentForegroundWindow.Handle || _lastForegroundWpsWindow.Title != currentForegroundWindow.Title) { - LogHelper.WriteLogToFile($"前台WPS窗口发生变化: {_lastForegroundWpsWindow.Title} -> {currentForegroundWindow.Title}", LogHelper.LogType.Trace); } } else if (_lastForegroundWpsWindow == null && currentForegroundWindow != null) { - LogHelper.WriteLogToFile($"检测到新的前台WPS窗口: {currentForegroundWindow.Title}", LogHelper.LogType.Trace); } else if (_lastForegroundWpsWindow != null && currentForegroundWindow == null) { - LogHelper.WriteLogToFile($"前台WPS窗口已消失: {_lastForegroundWpsWindow.Title}", LogHelper.LogType.Trace); } // 更新记录 @@ -3387,9 +2831,8 @@ namespace Ink_Canvas.Helpers ret = GetPptHwndWin32(fullName, appName); } } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"获取PPT窗口句柄失败: {ex.Message}", LogHelper.LogType.Trace); } } @@ -3414,7 +2857,6 @@ namespace Ink_Canvas.Helpers { int hwndVal = slideWindow.HWND; hwnd = new IntPtr(hwndVal); - LogHelper.WriteLogToFile($"从SlideShowWindow获取窗口句柄成功: {hwnd}", LogHelper.LogType.Trace); } else { @@ -3424,17 +2866,14 @@ namespace Ink_Canvas.Helpers dynamic ssw = pptSlideShowWindowObj; int hwndVal = ssw.HWND; hwnd = new IntPtr(hwndVal); - LogHelper.WriteLogToFile($"从SlideShowWindow获取窗口句柄成功(dynamic): {hwnd}", LogHelper.LogType.Trace); } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"从SlideShowWindow获取窗口句柄失败: {ex.Message}", LogHelper.LogType.Trace); } } } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"从SlideShowWindow获取窗口句柄异常: {ex.Message}", LogHelper.LogType.Trace); } return hwnd; @@ -3522,20 +2961,17 @@ namespace Ink_Canvas.Helpers // 0 个表示没找到,>1 个表示有歧义(无法确定是哪一个),均视为失败 if (candidates.Count == 1) { - LogHelper.WriteLogToFile($"通过窗口标题匹配获取窗口句柄成功: {candidates[0]}", LogHelper.LogType.Trace); return candidates[0]; } else if (candidates.Count > 1) { - LogHelper.WriteLogToFile($"通过窗口标题匹配找到多个候选窗口({candidates.Count}个),无法确定唯一窗口", LogHelper.LogType.Trace); } return IntPtr.Zero; } - catch (Exception ex) + catch (Exception) { // 发生任何不可预知的异常(如Path解析错误等),返回安全值 - LogHelper.WriteLogToFile($"GetPptHwndWin32异常: {ex.Message}", LogHelper.LogType.Trace); return IntPtr.Zero; } } diff --git a/Ink Canvas/Helpers/SaveFileNameHelper.cs b/Ink Canvas/Helpers/SaveFileNameHelper.cs new file mode 100644 index 00000000..f8ef2a2f --- /dev/null +++ b/Ink Canvas/Helpers/SaveFileNameHelper.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Text.RegularExpressions; + +namespace Ink_Canvas.Helpers +{ + /// + /// 渲染保存文件名模板。支持占位符: {date} {time} {datetime} {mode} {page} {count} {type}。 + /// 当模板为空、渲染结果非法或仅含分隔符时,回退到默认时间戳命名。 + /// + 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; } + /// "Annotation" or "BlackBoard" or "Screenshot" etc. + public string Mode { get; set; } + /// "User" or "Auto" + public string Type { get; set; } + public int? Page { get; set; } + public int? Count { get; set; } + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/ThemeHelper.cs b/Ink Canvas/Helpers/ThemeHelper.cs new file mode 100644 index 00000000..c6dbf0de --- /dev/null +++ b/Ink Canvas/Helpers/ThemeHelper.cs @@ -0,0 +1,92 @@ +using iNKORE.UI.WPF.Modern; +using Microsoft.Win32; +using System; +using System.Windows; + +namespace Ink_Canvas.Helpers +{ + public static class ThemeHelper + { + public static bool IsSystemThemeLight() + { + try + { + var registryKey = Registry.CurrentUser; + var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + if (themeKey != null) + { + var value = themeKey.GetValue("AppsUseLightTheme"); + if (value != null) + { + bool result = (int)value == 1; + themeKey.Close(); + return result; + } + themeKey.Close(); + } + } + catch + { + } + return true; + } + + public static bool IsSystemThemeLightLegacy() + { + try + { + var registryKey = Registry.CurrentUser; + var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + if (themeKey != null) + { + int keyValue = (int)themeKey.GetValue("SystemUsesLightTheme"); + themeKey.Close(); + return keyValue == 1; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); + } + return false; + } + + public static ElementTheme GetEffectiveTheme(Settings settings) + { + if (settings.Appearance.Theme == 0) + return ElementTheme.Light; + if (settings.Appearance.Theme == 1) + return ElementTheme.Dark; + + return IsSystemThemeLight() ? ElementTheme.Light : ElementTheme.Dark; + } + + public static void ApplyTheme(FrameworkElement element, Settings settings) + { + if (element == null || settings == null) return; + try + { + ThemeManager.SetRequestedTheme(element, GetEffectiveTheme(settings)); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"应用主题失败: {ex.Message}", LogHelper.LogType.Error); + } + } + + public static void ApplyTheme(FrameworkElement element, Settings settings, Action onThemeApplied) + { + if (element == null || settings == null) return; + try + { + var theme = GetEffectiveTheme(settings); + ThemeManager.SetRequestedTheme(element, theme); + onThemeApplied?.Invoke(theme == ElementTheme.Dark ? "Dark" : "Light"); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"应用主题失败: {ex.Message}", LogHelper.LogType.Error); + } + } + } +} diff --git a/Ink Canvas/Helpers/UIAccessDllExtractor.cs b/Ink Canvas/Helpers/UIAccessDllExtractor.cs deleted file mode 100644 index f9448167..00000000 --- a/Ink Canvas/Helpers/UIAccessDllExtractor.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.IO; -using System.Reflection; - -namespace Ink_Canvas.Helpers -{ - /// - /// UIAccess DLL释放器 - /// - public static class UIAccessDllExtractor - { - private static readonly string[] RequiredDlls = { - "UIAccessDLL_x64.dll", - "UIAccessDLL_x86.dll" - }; - - /// - /// 在应用启动时释放UIAccess相关DLL - /// - 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); - } - } - - /// - /// 从嵌入资源中提取DLL文件 - /// - 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; - } - } - - /// - /// 检查DLL文件是否有效 - /// - 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; - } - } - - /// - /// 清理释放的DLL文件(可选,在应用退出时调用) - /// - 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); - } - } - } -} diff --git a/Ink Canvas/Helpers/UIAccessHelper.cs b/Ink Canvas/Helpers/UIAccessHelper.cs new file mode 100644 index 00000000..dfae8688 --- /dev/null +++ b/Ink Canvas/Helpers/UIAccessHelper.cs @@ -0,0 +1,672 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ink_Canvas.Helpers +{ + /// + /// 通过 Winlogon 令牌模拟实现 UIAccess 提权重启。 + /// 1. 找到当前会话中 winlogon.exe 的令牌,复制为模拟令牌; + /// 2. SetThreadToken 暂时模拟 winlogon(拥有 TCB 权限); + /// 3. 在自身令牌副本上 SetTokenInformation(TokenUIAccess, TRUE); + /// 4. RevertToSelf 后用 CreateProcessWithTokenW 启动新进程; + /// 5. 新进程具有 UIAccess 权限,可置顶于 UAC 提示之上。 + /// + 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 + + /// + /// 检查当前进程是否已具有 UIAccess 标志。 + /// + 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); + } + } + + /// + /// 以 UIAccess 令牌重启自身。当前进程必须已经以管理员身份运行。 + /// 成功时新进程已启动,调用方应立即退出当前进程。 + /// + /// 追加到新进程的额外命令行参数(例如 --skip-mutex-check)。 + 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; + } + } + + /// + /// 以普通用户权限(非提升)重启自身。 + /// 通过获取 explorer.exe / ctfmon.exe 的非特权令牌,再用 CreateProcessWithTokenW 启动新进程, + /// 避免经由 explorer.exe 中转可能产生的 UAC 提示或丢失参数问题。 + /// 成功时调用方应立即退出当前进程。 + /// + /// 追加到新进程的额外命令行参数。 + 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); } + } + + /// + /// 从 explorer.exe / ctfmon.exe 取得普通用户(非提升)令牌的主令牌副本,用于降权启动。 + /// 仅当当前进程为管理员时才能成功。 + /// + 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 + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs index ff6c06eb..6b90f765 100644 --- a/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs +++ b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs @@ -29,31 +29,13 @@ namespace Ink_Canvas.Helpers public static bool IsApiAvailable => OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10; + /// + /// 启动阶段不再预热线程内 WinRT 手写管线。历史上曾用 跑全链路, + /// 会显著拖慢启动;与更早的「空 」一样,此处不再在 Idle 上做任何工作。 + /// 首次真正需要手写识别时由 承担冷启动成本。 + /// public static void Warmup() { - if (!IsApiAvailable || !Environment.Is64BitProcess) return; - try - { - var d = Application.Current?.Dispatcher; - if (d == null) return; - d.BeginInvoke(new Action(async () => - { - try - { - await RecognizeHandwritingAsync( - WinRtInkShapeRecognizer.CreateMinimalWarmupStrokeCollection(), - verboseTrace: false).ConfigureAwait(true); - } - catch - { - // ignore - } - })); - } - catch - { - // ignore - } } /// diff --git a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs index c39a3621..2b4f7892 100644 --- a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs +++ b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs @@ -1,6 +1,7 @@ using OSVersionExtension; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Ink; @@ -11,6 +12,128 @@ 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 _strokeIdMap = new Dictionary(); + private readonly Dictionary _reverseIdMap = new Dictionary(); + 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 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 AnalyzeAndCorrectAsync( + StrokeCollection strokes, + string handwritingFontFamilyList) + { + return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync( + strokes, + handwritingFontFamilyList); + } + + public void Dispose() + { + _internalAnalyzer = null; + } + } + /// 基于 Windows.UI.Input.Inking.Analysis 的形状识别(适用于 64 位进程等场景)。 internal static class WinRtInkShapeRecognizer { @@ -124,6 +247,9 @@ namespace Ink_Canvas.Helpers 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, @@ -147,8 +273,8 @@ namespace Ink_Canvas.Helpers return builder.CreateStroke(points); } - private static global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing FindPrimaryDrawing( - WinRtInkAnalyzer analyzer) + 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; @@ -187,7 +313,7 @@ namespace Ink_Canvas.Helpers return w * h; } - private static global::Windows.Foundation.Point[] CopyWinRtPoints( + internal static global::Windows.Foundation.Point[] CopyWinRtPoints( global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing drawing) { var src = drawing?.Points; @@ -204,7 +330,7 @@ namespace Ink_Canvas.Helpers return arr; } - private static void BoundsFromPoints( + internal static void BoundsFromPoints( System.Collections.Generic.IReadOnlyList points, out double w, out double h) @@ -229,7 +355,7 @@ namespace Ink_Canvas.Helpers h = Math.Max(0, maxY - minY); } - private static PointCollection ToWpfPointCollection( + internal static PointCollection ToWpfPointCollection( System.Collections.Generic.IReadOnlyList points) { var hot = new PointCollection(); @@ -243,7 +369,7 @@ namespace Ink_Canvas.Helpers return hot; } - private static string MapDrawingKindToShapeName( + internal static string MapDrawingKindToShapeName( global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind kind) { switch (kind) diff --git a/Ink Canvas/InkCanvasForClass.csproj b/Ink Canvas/InkCanvasForClass.csproj index abe1f3f3..92c42f06 100644 --- a/Ink Canvas/InkCanvasForClass.csproj +++ b/Ink Canvas/InkCanvasForClass.csproj @@ -1,10 +1,12 @@ - + - win;win-x86;win-x64;win-arm64 + win-x86;win-x64;win-arm64 WinExe Ink_Canvas InkCanvasForClass net462 + disable + disable true {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} true @@ -25,8 +27,15 @@ false False true + true + true Debug;Release - AnyCPU;x86;x64;ARM64 + AnyCPU;x86;x64 + 10 + $(NoWarn);CA1416;NU1701;MSB3270;CS8012;NETSDK1138 + true + None + PerMonitorV2 embedded @@ -46,14 +55,12 @@ bin\$(Configuration)\$(Platform)\ embedded - 7.3 x86 true bin\$(Configuration)\$(Platform)\ embedded - 7.3 x86 true @@ -71,14 +78,12 @@ bin\$(Configuration)\$(Platform)\ full - 7.3 ARM64 false bin\$(Configuration)\$(Platform)\ pdbonly - 7.3 ARM64 false @@ -86,7 +91,6 @@ bin\$(Configuration)\$(Platform)\ none false - 7.3 x64 false @@ -94,7 +98,6 @@ bin\$(Configuration)\$(Platform)\ none false - 7.3 x64 false @@ -111,23 +114,6 @@ .\IAWinFX.dll False - - - - - - - - - - - - - - - - - @@ -136,20 +122,23 @@ all + - - - + - + + + + + @@ -157,7 +146,13 @@ + + + + + + @@ -199,8 +194,6 @@ - - diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index cf129f5f..a91db569 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -7,11 +7,13 @@ xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf" xmlns:c="clr-namespace:Ink_Canvas.Converter" xmlns:Controls="http://schemas.microsoft.com/netfx/2009/xaml/presentation" - xmlns:controls="clr-namespace:Ink_Canvas.Controls" + xmlns:controls="clr-namespace:Ink_Canvas.Controls;assembly=InkCanvas.Controls" + xmlns:localControls="clr-namespace:Ink_Canvas.Controls" xmlns:Windows="clr-namespace:Ink_Canvas.Windows" xmlns:props="clr-namespace:Ink_Canvas.Properties" xmlns:i18n="clr-namespace:Ink_Canvas.MarkupExtensions" xmlns:helpers="clr-namespace:Ink_Canvas.Helpers" + xmlns:icons="clr-namespace:Ink_Canvas" mc:Ignorable="d" AllowsTransparency="True" WindowStyle="None" @@ -24,7 +26,7 @@ Closing="Window_Closing" Closed="Window_Closed" PreviewKeyDown="Main_Grid_PreviewKeyDown" - Height="16000" Width="1440" + Height="1080" Width="1920" FontFamily="Microsoft YaHei UI" MouseWheel="Window_MouseWheel" Foreground="{DynamicResource FloatBarForeground}" @@ -43,11 +45,14 @@ + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -4656,7 +273,7 @@ - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -9912,7 +2924,7 @@ CornerRadius="6,6,0,0" Background="#2563eb" Margin="-1,-1,-1,1"> - - - - - - + + + + + @@ -10021,7 +3033,7 @@ + Geometry="{Binding Source={x:Static icons:XamlGraphicsIconGeometries.ClearInkIconGeometry}, Converter={StaticResource StringToGeometryConverter}}"/> @@ -10036,83 +3048,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + \ No newline at end of file diff --git a/Ink Canvas/UIAccessDLL_x64.dll b/Ink Canvas/UIAccessDLL_x64.dll deleted file mode 100644 index 9edc84b8..00000000 Binary files a/Ink Canvas/UIAccessDLL_x64.dll and /dev/null differ diff --git a/Ink Canvas/UIAccessDLL_x86.dll b/Ink Canvas/UIAccessDLL_x86.dll deleted file mode 100644 index 48fe7e3e..00000000 Binary files a/Ink Canvas/UIAccessDLL_x86.dll and /dev/null differ diff --git a/Ink Canvas/Windows/CloudStorageManagementWindow.xaml.cs b/Ink Canvas/Windows/CloudStorageManagementWindow.xaml.cs index 3bc966fe..4b2efb3e 100644 --- a/Ink Canvas/Windows/CloudStorageManagementWindow.xaml.cs +++ b/Ink Canvas/Windows/CloudStorageManagementWindow.xaml.cs @@ -657,7 +657,7 @@ namespace Ink_Canvas.Windows /// /// 保存按钮点击事件 /// - private async void BtnSave_Click(object sender, RoutedEventArgs e) + private void BtnSave_Click(object sender, RoutedEventArgs e) { try { diff --git a/Ink Canvas/Windows/CountdownTimerWindow.xaml b/Ink Canvas/Windows/CountdownTimerWindow.xaml index ec8c4700..cce4eafb 100644 --- a/Ink Canvas/Windows/CountdownTimerWindow.xaml +++ b/Ink Canvas/Windows/CountdownTimerWindow.xaml @@ -172,7 +172,7 @@ - + diff --git a/Ink Canvas/Windows/CustomIconWindow.xaml.cs b/Ink Canvas/Windows/CustomIconWindow.xaml.cs index 5fff7008..e685c61d 100644 --- a/Ink Canvas/Windows/CustomIconWindow.xaml.cs +++ b/Ink Canvas/Windows/CustomIconWindow.xaml.cs @@ -41,7 +41,6 @@ namespace Ink_Canvas MainWindow.Settings.Appearance.FloatingBarImg - 12 >= MainWindow.Settings.Appearance.CustomFloatingBarImgs.Count) { MainWindow.Settings.Appearance.FloatingBarImg = 0; - mainWindow.ComboBoxFloatingBarImg.SelectedIndex = 0; mainWindow.UpdateFloatingBarIcon(); } diff --git a/Ink Canvas/Windows/HasNewUpdateWindow.xaml b/Ink Canvas/Windows/HasNewUpdateWindow.xaml deleted file mode 100644 index b09c3939..00000000 --- a/Ink Canvas/Windows/HasNewUpdateWindow.xaml +++ /dev/null @@ -1,346 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # InkCanvasForClass v5.0.2更新 - - 你好,旅行者们,本次InkCanvasForClass Community Edition更新带来了如下新功能供您探索: - - 1. 全新设计的UI界面,包括浮动工具栏和白板页面均经过重新设计,更加现代化的UI让您在使用的过程中更加舒适。 - 2. 带来了实时修改橡皮大小和橡皮形状的菜单。您可以选择使用圆形橡皮,方形橡皮,和类似希沃白板的真实黑板擦(矩形)橡皮。 - 3. 白板页面支持显示当前时间和日期 - 4. 自动收纳新增对希沃轻白板、智绘教、鸿合屏幕书写等软件的支持,自动查杀新增对鸿合屏幕书写、希沃轻白板等软件的支持。 - 5. 为设置界面重写了全新的UI。 - 6. 重写了随机抽选模块,现在支持更丰富的抽选机制和自定义选项。 - 7. 修复了部分小Bug,提升了整体的用户体验。 - 8. 带来了基于FitToCurve的笔迹平滑,基于贝塞尔曲线平滑,让墨迹线条更加优美好看。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + \ No newline at end of file diff --git a/Ink Canvas/Windows/HotkeyItem.xaml.cs b/Ink Canvas/Windows/HotkeyItem.xaml.cs index 7cbd2856..63cda413 100644 --- a/Ink Canvas/Windows/HotkeyItem.xaml.cs +++ b/Ink Canvas/Windows/HotkeyItem.xaml.cs @@ -2,7 +2,6 @@ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Media; namespace Ink_Canvas.Windows { @@ -11,34 +10,24 @@ namespace Ink_Canvas.Windows /// public partial class HotkeyItem : UserControl { - private static readonly SolidColorBrush HotkeyValueForeground = CreateFrozenBrush(0xFA, 0xFA, 0xFA); - private static readonly SolidColorBrush HotkeyPlaceholderForeground = CreateFrozenBrush(0xA1, 0xA1, 0xAA); + public static readonly DependencyProperty TitleProperty = + DependencyProperty.Register(nameof(Title), typeof(string), typeof(HotkeyItem), + new PropertyMetadata(string.Empty)); - private static SolidColorBrush CreateFrozenBrush(byte r, byte g, byte b) - { - var brush = new SolidColorBrush(Color.FromRgb(r, g, b)); - brush.Freeze(); - return brush; - } + public static readonly DependencyProperty DescriptionProperty = + DependencyProperty.Register(nameof(Description), typeof(string), typeof(HotkeyItem), + new PropertyMetadata(string.Empty)); - #region Events - /// - /// 快捷键变更事件 - /// - public event EventHandler HotkeyChanged; - #endregion - - #region Properties public string Title { - get => TitleTextBlock.Text; - set => TitleTextBlock.Text = value; + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); } public string Description { - get => DescriptionTextBlock.Text; - set => DescriptionTextBlock.Text = value; + get => (string)GetValue(DescriptionProperty); + set => SetValue(DescriptionProperty, value); } public string DefaultKey { get; set; } @@ -49,24 +38,20 @@ namespace Ink_Canvas.Windows /// public string HotkeyName { get; set; } + /// + /// 快捷键变更事件 + /// + public event EventHandler HotkeyChanged; + private Key _currentKey = Key.None; private ModifierKeys _currentModifiers = ModifierKeys.None; - #endregion - #region Constructor public HotkeyItem() { InitializeComponent(); UpdateHotkeyDisplay(); } - #endregion - #region Public Methods - /// - /// 设置当前快捷键 - /// - /// 按键 - /// 修饰键 public void SetCurrentHotkey(Key key, ModifierKeys modifiers) { _currentKey = key; @@ -74,60 +59,62 @@ namespace Ink_Canvas.Windows UpdateHotkeyDisplay(); } - /// - /// 获取当前快捷键 - /// - /// 快捷键信息 public (Key key, ModifierKeys modifiers) GetCurrentHotkey() { return (_currentKey, _currentModifiers); } - #endregion - #region Private Methods private void UpdateHotkeyDisplay() { if (_currentKey == Key.None) { CurrentHotkeyTextBlock.Text = "未设置"; - CurrentHotkeyTextBlock.Foreground = HotkeyPlaceholderForeground; } else { var modifiersText = _currentModifiers == ModifierKeys.None ? "" : $"{_currentModifiers}+"; CurrentHotkeyTextBlock.Text = $"{modifiersText}{_currentKey}"; - CurrentHotkeyTextBlock.Foreground = HotkeyValueForeground; } } + private static HotkeyItem _activeCaptureItem; + private void StartHotkeyCapture() { - BtnSetHotkey.Content = "请按键..."; - BtnSetHotkey.Background = Brushes.Orange; + if (_activeCaptureItem != null && _activeCaptureItem != this) + { + _activeCaptureItem.StopHotkeyCapture(); + } + _activeCaptureItem = this; - // 设置焦点以捕获键盘事件 + CurrentHotkeyTextBlock.Text = "请按键..."; Focus(); - - // 添加键盘事件处理器 KeyDown += HotkeyItem_KeyDown; KeyUp += HotkeyItem_KeyUp; + LostFocus += HotkeyItem_LostFocus; } private void StopHotkeyCapture() { - BtnSetHotkey.Content = "设置"; - BtnSetHotkey.ClearValue(Button.BackgroundProperty); - - // 移除键盘事件处理器 + UpdateHotkeyDisplay(); KeyDown -= HotkeyItem_KeyDown; KeyUp -= HotkeyItem_KeyUp; + LostFocus -= HotkeyItem_LostFocus; + if (_activeCaptureItem == this) + { + _activeCaptureItem = null; + } + } + + private void HotkeyItem_LostFocus(object sender, RoutedEventArgs e) + { + StopHotkeyCapture(); } private void HotkeyItem_KeyDown(object sender, KeyEventArgs e) { e.Handled = true; - // 忽略某些特殊键 if (e.Key == Key.LeftShift || e.Key == Key.RightShift || e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl || e.Key == Key.LeftAlt || e.Key == Key.RightAlt || @@ -136,7 +123,6 @@ namespace Ink_Canvas.Windows return; } - // 获取修饰键 var modifiers = ModifierKeys.None; if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) modifiers |= ModifierKeys.Control; @@ -147,20 +133,15 @@ namespace Ink_Canvas.Windows if (Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin)) modifiers |= ModifierKeys.Windows; - // 设置新的快捷键 - var oldKey = _currentKey; - var oldModifiers = _currentModifiers; - _currentKey = e.Key; _currentModifiers = modifiers; UpdateHotkeyDisplay(); StopHotkeyCapture(); - // 触发快捷键变更事件 HotkeyChanged?.Invoke(this, new HotkeyChangedEventArgs { - HotkeyName = HotkeyName ?? Title, // 优先使用HotkeyName,如果没有则使用Title + HotkeyName = HotkeyName ?? Title, Key = _currentKey, Modifiers = _currentModifiers }); @@ -170,13 +151,20 @@ namespace Ink_Canvas.Windows { e.Handled = true; } - #endregion - #region Event Handlers private void BtnSetHotkey_Click(object sender, RoutedEventArgs e) { StartHotkeyCapture(); } - #endregion + } + + /// + /// 快捷键变更事件参数 + /// + public class HotkeyChangedEventArgs : EventArgs + { + public string HotkeyName { get; set; } + public Key Key { get; set; } + public ModifierKeys Modifiers { get; set; } } } \ No newline at end of file diff --git a/Ink Canvas/Windows/HotkeySettingsWindow.xaml b/Ink Canvas/Windows/HotkeySettingsWindow.xaml deleted file mode 100644 index 18d6518e..00000000 --- a/Ink Canvas/Windows/HotkeySettingsWindow.xaml +++ /dev/null @@ -1,375 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - 主题、启动动画、托盘图标等均可在 设置 → 外观 中修改。 - + + + + + + + - - - - - - - - - + + + + + + - + + + - - + + + + + + + + + + + + + + + + + + + + - - + - - - - - - 具体快捷键可在 设置 → 快捷键设置 中配置。 - - - + - - - - - - 发生未处理异常时的行为可在 设置 → 崩溃处理 中修改。 - - - - - - + - - - - - - 以下选项均可在 设置 → PPT 中修改,用于与 PowerPoint/WPS 放映联动。 - + + + + + + + + + + - + + + - + - + - - + - - - - - - 进入/退出特定软件时自动收纳、墨迹自动保存、悬浮窗拦截等可在 设置 → 自动化行为 中修改。 - - - - - + - - - - - - 点名窗口样式与行为可在 设置 → 随机点名 中修改。 - - - + + - - - - - - 日志、特殊屏幕、窗口模式等可在 设置 → 高级选项 中修改。 - - - + + + - - - - - - 清屏截图、按日期保存等可在 设置 → 截图和屏幕捕捉 中修改。 - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - 此向导仅在首次启动时出现;之后可在 设置 中按侧边栏分区修改对应选项。 - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ink Canvas/Windows/PrivacyAgreementWindow.xaml.cs b/Ink Canvas/Windows/PrivacyAgreementWindow.xaml.cs index 32bd359f..30c20fb2 100644 --- a/Ink Canvas/Windows/PrivacyAgreementWindow.xaml.cs +++ b/Ink Canvas/Windows/PrivacyAgreementWindow.xaml.cs @@ -5,9 +5,7 @@ using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Windows; -using System.Windows.Controls; using System.Windows.Interop; -using System.Windows.Media; using System.Windows.Threading; namespace Ink_Canvas @@ -15,61 +13,26 @@ namespace Ink_Canvas public partial class PrivacyAgreementWindow : Window { public bool UserAccepted { get; private set; } = false; - private bool wasSettingsPanelVisible = false; public PrivacyAgreementWindow() { InitializeComponent(); - this.Topmost = true; + Topmost = true; AnimationsHelper.ShowWithSlideFromBottomAndFade(this, 0.25); ApplyTheme(); - HideSettingsPanel(); - } - - private void HideSettingsPanel() - { - try - { - if (Application.Current.MainWindow is MainWindow mainWindow) - { - var borderSettings = mainWindow.FindName("BorderSettings") as Border; - var borderSettingsMask = mainWindow.FindName("BorderSettingsMask") as Border; - - if (borderSettings != null) - { - wasSettingsPanelVisible = borderSettings.Visibility == Visibility.Visible; - if (wasSettingsPanelVisible) - { - borderSettings.Visibility = Visibility.Hidden; - } - } - - if (borderSettingsMask != null) - { - if (wasSettingsPanelVisible) - { - borderSettingsMask.Visibility = Visibility.Hidden; - } - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"隐藏设置面板失败: {ex.Message}", LogHelper.LogType.Warning); - } } private void Window_Loaded(object sender, RoutedEventArgs e) { try { - this.Topmost = true; + Topmost = true; Dispatcher.BeginInvoke(new Action(() => { - this.Activate(); - this.Focus(); - this.Topmost = true; + Activate(); + Focus(); + Topmost = true; SetForegroundWindow(new WindowInteropHelper(this).Handle); }), DispatcherPriority.Loaded); @@ -112,34 +75,7 @@ namespace Ink_Canvas [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); - private void Window_Closing(object sender, CancelEventArgs e) - { - if (wasSettingsPanelVisible) - { - try - { - if (Application.Current.MainWindow is MainWindow mainWindow) - { - var borderSettings = mainWindow.FindName("BorderSettings") as Border; - var borderSettingsMask = mainWindow.FindName("BorderSettingsMask") as Border; - - if (borderSettings != null) - { - borderSettings.Visibility = Visibility.Visible; - } - - if (borderSettingsMask != null) - { - borderSettingsMask.Visibility = Visibility.Visible; - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"恢复设置面板失败: {ex.Message}", LogHelper.LogType.Warning); - } - } - } + private void Window_Closing(object sender, CancelEventArgs e) { } private void ButtonCancel_Click(object sender, RoutedEventArgs e) { @@ -159,10 +95,20 @@ namespace Ink_Canvas { try { - if (MainWindow.Settings != null) + var settings = MainWindow.Settings; + if (settings == null) return; + + iNKORE.UI.WPF.Modern.ElementTheme target; + switch (settings.Appearance.Theme) { - ApplyTheme(MainWindow.Settings); + case 0: target = iNKORE.UI.WPF.Modern.ElementTheme.Light; break; + case 1: target = iNKORE.UI.WPF.Modern.ElementTheme.Dark; break; + default: + target = IsSystemThemeLight() + ? iNKORE.UI.WPF.Modern.ElementTheme.Light + : iNKORE.UI.WPF.Modern.ElementTheme.Dark; break; } + iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, target); } catch (Exception ex) { @@ -170,97 +116,19 @@ namespace Ink_Canvas } } - private void ApplyTheme(Settings settings) + private static bool IsSystemThemeLight() { - try - { - if (settings.Appearance.Theme == 0) - { - iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Light); - ApplyThemeResources("Light"); - } - else if (settings.Appearance.Theme == 1) - { - iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Dark); - ApplyThemeResources("Dark"); - } - else - { - bool isSystemLight = IsSystemThemeLight(); - if (isSystemLight) - { - iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Light); - ApplyThemeResources("Light"); - } - else - { - iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Dark); - ApplyThemeResources("Dark"); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用隐私说明窗口主题出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - private void ApplyThemeResources(string theme) - { - try - { - var resources = this.Resources; - - if (theme == "Light") - { - resources["PrivacyAgreementWindowBackground"] = new SolidColorBrush(Color.FromRgb(255, 255, 255)); - resources["PrivacyAgreementWindowForeground"] = new SolidColorBrush(Color.FromRgb(24, 24, 27)); - resources["PrivacyAgreementWindowButtonBackground"] = new SolidColorBrush(Color.FromRgb(244, 244, 245)); - resources["PrivacyAgreementWindowButtonForeground"] = new SolidColorBrush(Color.FromRgb(24, 24, 27)); - resources["PrivacyAgreementWindowBorderBrush"] = new SolidColorBrush(Color.FromRgb(228, 228, 231)); - resources["PrivacyAgreementWindowButtonAcceptBackground"] = new SolidColorBrush(Color.FromRgb(53, 132, 228)); - resources["PrivacyAgreementWindowButtonAcceptForeground"] = new SolidColorBrush(Colors.White); - } - else - { - resources["PrivacyAgreementWindowBackground"] = new SolidColorBrush(Color.FromRgb(31, 31, 31)); - resources["PrivacyAgreementWindowForeground"] = new SolidColorBrush(Colors.White); - resources["PrivacyAgreementWindowButtonBackground"] = new SolidColorBrush(Color.FromRgb(42, 42, 42)); - resources["PrivacyAgreementWindowButtonForeground"] = new SolidColorBrush(Colors.White); - resources["PrivacyAgreementWindowBorderBrush"] = new SolidColorBrush(Color.FromRgb(224, 224, 224)); - resources["PrivacyAgreementWindowButtonAcceptBackground"] = new SolidColorBrush(Color.FromRgb(53, 132, 228)); - resources["PrivacyAgreementWindowButtonAcceptForeground"] = new SolidColorBrush(Colors.White); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用隐私说明窗口主题资源出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - private bool IsSystemThemeLight() - { - var light = false; try { var registryKey = Microsoft.Win32.Registry.CurrentUser; - var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); - if (themeKey != null) + using (var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")) { - var value = themeKey.GetValue("AppsUseLightTheme"); - if (value != null) - { - light = (int)value == 1; - } - themeKey.Close(); + var value = themeKey?.GetValue("AppsUseLightTheme"); + if (value is int i) return i == 1; } } - catch - { - light = true; - } - return light; + catch { } + return true; } } -} - +} \ No newline at end of file diff --git a/Ink Canvas/Windows/RandWindow.xaml b/Ink Canvas/Windows/RandWindow.xaml index ff7bf4b6..31bc0c3d 100644 --- a/Ink Canvas/Windows/RandWindow.xaml +++ b/Ink Canvas/Windows/RandWindow.xaml @@ -106,7 +106,7 @@ - ClassIsland点名 diff --git a/Ink Canvas/Windows/RandWindow.xaml.cs b/Ink Canvas/Windows/RandWindow.xaml.cs index b49540fc..0fbaab29 100644 --- a/Ink Canvas/Windows/RandWindow.xaml.cs +++ b/Ink Canvas/Windows/RandWindow.xaml.cs @@ -79,60 +79,16 @@ namespace Ink_Canvas private void ApplyTheme(Settings settings) { - try + ThemeHelper.ApplyTheme(this, settings, theme => { - if (settings.Appearance.Theme == 0) // 浅色主题 - { - iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Light); - } - else if (settings.Appearance.Theme == 1) // 深色主题 - { - iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Dark); - } - else // 跟随系统主题 - { - bool isSystemLight = IsSystemThemeLight(); - if (isSystemLight) - { - iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Light); - } - else - { - iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Dark); - } - } - - // 根据主题设置窗口背景 if (settings.RandSettings.SelectedBackgroundIndex <= 0) { - // 没有自定义背景时,使用主题背景色 if (Application.Current.FindResource("RandWindowBackground") is SolidColorBrush backgroundBrush) { MainBorder.Background = backgroundBrush; } } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用点名窗口主题出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - private bool IsSystemThemeLight() - { - var light = false; - try - { - var registryKey = Microsoft.Win32.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; - } - catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - - return light; + }); } public RandWindow(Settings settings, bool IsAutoClose) @@ -401,7 +357,9 @@ namespace Ink_Canvas if (!ok) return; } - new NamesInputWindow().ShowDialog(); + var namesInputWindow = new NamesInputWindow(); + namesInputWindow.Owner = this; + namesInputWindow.ShowDialog(); Window_Loaded(this, null); } diff --git a/Ink Canvas/Windows/RollCallHistoryWindow.xaml b/Ink Canvas/Windows/RollCallHistoryWindow.xaml index 8beb593c..07def63f 100644 --- a/Ink Canvas/Windows/RollCallHistoryWindow.xaml +++ b/Ink Canvas/Windows/RollCallHistoryWindow.xaml @@ -59,9 +59,9 @@ + + + + + + + + 16 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ink Canvas/Windows/SettingsViews/Pages/UpdatePage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/UpdatePage.xaml.cs new file mode 100644 index 00000000..49f5edf7 --- /dev/null +++ b/Ink Canvas/Windows/SettingsViews/Pages/UpdatePage.xaml.cs @@ -0,0 +1,940 @@ +using Ink_Canvas.Helpers; +using Ink_Canvas.Windows.SettingsViews.Helpers; +using iNKORE.UI.WPF.Modern.Common.IconKeys; +using iNKORE.UI.WPF.Modern.Controls; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; +using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox; + +namespace Ink_Canvas.Windows.SettingsViews.Pages +{ + public partial class UpdatePage : iNKORE.UI.WPF.Modern.Controls.Page + { + private enum UpdateUiState + { + Idle, + Checking, + UpdateAvailable, + Downloading, + Downloaded, + NetworkError + } + + private class VersionItem + { + public string Version { get; set; } + public string DownloadUrl { get; set; } + public string ReleaseNotes { get; set; } + } + + private bool _isLoaded; + private bool _isChangingUpdateChannelInternally; + private bool _isChangingUpdatePackageArchInternally; + + private UpdateUiState _state = UpdateUiState.Idle; + private string _remoteVersion; + private AutoUpdateHelper.UpdateLineGroup _remoteLineGroup; + private string _remoteReleaseNotes; + + private List _versionList = new List(); + private VersionItem _selectedHistoricalItem; + private bool _isHistoryLoaded; + + public UpdatePage() + { + InitializeComponent(); + Loaded += UpdatePage_Loaded; + } + + private async void UpdatePage_Loaded(object sender, RoutedEventArgs e) + { + LoadSettings(); + _isLoaded = true; + + // 复用启动时自动检查的结果,避免二次检查 + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null && !string.IsNullOrEmpty(mainWindow.AvailableLatestVersion)) + { + _remoteVersion = mainWindow.AvailableLatestVersion; + _remoteLineGroup = mainWindow.AvailableLatestLineGroup; + _remoteReleaseNotes = mainWindow.AvailableLatestReleaseNotes; + + try + { + var statusFile = AutoUpdateHelper.GetUpdateDownloadStatusFilePath(_remoteVersion); + if (System.IO.File.Exists(statusFile) && + System.IO.File.ReadAllText(statusFile).Trim().ToLower() == "true") + { + ApplyState(UpdateUiState.Downloaded); + } + else + { + ApplyState(UpdateUiState.UpdateAvailable); + } + } + catch + { + ApplyState(UpdateUiState.UpdateAvailable); + } + + if (!string.IsNullOrEmpty(_remoteReleaseNotes)) + ChangelogViewer.Markdown = _remoteReleaseNotes; + else + ChangelogViewer.Markdown = "切换到 *历史版本* 或点击 *检查更新* 查看具体更新日志。"; + return; + } + + // 没有缓存的检查结果时,仅展示空白;不主动联网,避免打开页面就触发请求 + ApplyState(UpdateUiState.Idle); + ChangelogViewer.Markdown = "点击 *检查更新* 来获取最新版本及更新日志。"; + await System.Threading.Tasks.Task.CompletedTask; + } + + #region 更新设置加载 + + private void LoadSettings() + { + _isLoaded = false; + + try + { + var settings = SettingsManager.Settings; + if (settings.Startup != null) + { + CardAutoUpdate.IsOn = settings.Startup.IsAutoUpdate; + CardSilentUpdate.IsOn = settings.Startup.IsAutoUpdateWithSilence; + + AutoUpdateWithSilenceTimeComboBox.InitializeAutoUpdateWithSilenceTimeComboBoxOptions( + AutoUpdateWithSilenceStartTimeComboBox, AutoUpdateWithSilenceEndTimeComboBox); + AutoUpdateWithSilenceStartTimeComboBox.SelectedItem = settings.Startup.AutoUpdateWithSilenceStartTime; + AutoUpdateWithSilenceEndTimeComboBox.SelectedItem = settings.Startup.AutoUpdateWithSilenceEndTime; + + foreach (var item in UpdateChannelSelector.Items) + { + if (item is ComboBoxItem cbi && cbi.Tag != null && + string.Equals(cbi.Tag.ToString(), settings.Startup.UpdateChannel.ToString(), StringComparison.OrdinalIgnoreCase)) + { + UpdateChannelSelector.SelectedItem = cbi; + break; + } + } + + _isChangingUpdatePackageArchInternally = true; + try + { + string wantTag = settings.Startup.UpdatePackageArchitecture == UpdatePackageArchitecture.X64 ? "X64" : "X86"; + foreach (var item in UpdatePackageArchitectureSelector.Items) + { + if (item is ComboBoxItem cbi && cbi.Tag != null && + string.Equals(cbi.Tag.ToString(), wantTag, StringComparison.OrdinalIgnoreCase)) + { + UpdatePackageArchitectureSelector.SelectedItem = cbi; + break; + } + } + } + finally + { + _isChangingUpdatePackageArchInternally = false; + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"加载更新设置时出错: {ex.Message}"); + } + + _isLoaded = true; + } + + #endregion + + #region 自动更新事件处理 + + private void ToggleSwitchIsAutoUpdate_Toggled(object sender, RoutedEventArgs e) + { + if (!_isLoaded) return; + + try + { + bool newState = CardAutoUpdate.IsOn; + SettingsManager.Settings.Startup.IsAutoUpdate = newState; + + if (!newState) + { + SettingsManager.Settings.Startup.IsAutoUpdateWithSilence = false; + CardSilentUpdate.IsOn = false; + } + + SettingsManager.SaveSettingsToFile(); + } + catch (Exception ex) + { + Debug.WriteLine($"设置自动更新时出错: {ex.Message}"); + } + } + + private void ToggleSwitchIsAutoUpdateWithSilence_Toggled(object sender, RoutedEventArgs e) + { + if (!_isLoaded) return; + + try + { + SettingsManager.Settings.Startup.IsAutoUpdateWithSilence = CardSilentUpdate.IsOn; + SettingsManager.SaveSettingsToFile(); + } + catch (Exception ex) + { + Debug.WriteLine($"设置静默更新时出错: {ex.Message}"); + } + } + + private void AutoUpdateWithSilenceStartTimeComboBox_SelectionChanged(object sender, RoutedEventArgs e) + { + if (!_isLoaded) return; + + try + { + SettingsManager.Settings.Startup.AutoUpdateWithSilenceStartTime = + (string)AutoUpdateWithSilenceStartTimeComboBox.SelectedItem; + SettingsManager.SaveSettingsToFile(); + } + catch (Exception ex) + { + Debug.WriteLine($"设置静默更新开始时间时出错: {ex.Message}"); + } + } + + private void AutoUpdateWithSilenceEndTimeComboBox_SelectionChanged(object sender, RoutedEventArgs e) + { + if (!_isLoaded) return; + + try + { + SettingsManager.Settings.Startup.AutoUpdateWithSilenceEndTime = + (string)AutoUpdateWithSilenceEndTimeComboBox.SelectedItem; + SettingsManager.SaveSettingsToFile(); + } + catch (Exception ex) + { + Debug.WriteLine($"设置静默更新结束时间时出错: {ex.Message}"); + } + } + + #endregion + + #region 更新通道和架构事件处理 + + private void UpdatePackageArchitectureSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) return; + if (_isChangingUpdatePackageArchInternally) return; + if (!(UpdatePackageArchitectureSelector.SelectedItem is ComboBoxItem cbi) || cbi.Tag == null) return; + + var newArch = string.Equals(cbi.Tag.ToString(), "X64", StringComparison.OrdinalIgnoreCase) + ? UpdatePackageArchitecture.X64 + : UpdatePackageArchitecture.X86; + + if (SettingsManager.Settings.Startup.UpdatePackageArchitecture == newArch) + return; + + SettingsManager.Settings.Startup.UpdatePackageArchitecture = newArch; + SettingsManager.SaveSettingsToFile(); + LogHelper.WriteLogToFile($"Settings | Update package architecture: {newArch}"); + } + + private async void UpdateChannelSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (!_isLoaded) return; + if (_isChangingUpdateChannelInternally) return; + if (!(UpdateChannelSelector.SelectedItem is ComboBoxItem cbi) || cbi.Tag == null) return; + + var oldChannel = SettingsManager.Settings.Startup.UpdateChannel; + string channel = cbi.Tag.ToString(); + UpdateChannel newChannel = channel == "Beta" ? UpdateChannel.Beta + : channel == "Preview" ? UpdateChannel.Preview + : UpdateChannel.Release; + + if (SettingsManager.Settings.Startup.UpdateChannel == newChannel) + return; + + bool isTestChannel = newChannel == UpdateChannel.Preview || newChannel == UpdateChannel.Beta; + + if (isTestChannel && !SettingsManager.Settings.Startup.HasAcceptedTelemetryPrivacy) + { + MessageBox.Show( + "加入预览 / 测试通道前,请先在关于页面勾选“我已阅读并同意 privacy 中的隐私说明”。", + "需要同意隐私说明", + MessageBoxButton.OK, + MessageBoxImage.Warning); + + SettingsManager.Settings.Startup.UpdateChannel = oldChannel; + RevertChannelSelection(oldChannel); + SettingsManager.SaveSettingsToFile(); + LogHelper.WriteLogToFile("Settings | User not accepted privacy, reverted update channel"); + return; + } + + if (isTestChannel && SettingsManager.Settings.Startup.TelemetryUploadLevel == TelemetryUploadLevel.None) + { + var result = MessageBox.Show( + "加入预览 / 测试通道需要开启匿名基础数据上传。\n\n是否立即开启匿名基础数据上传?", + "需要开启匿名使用数据上传", + MessageBoxButton.YesNo, + MessageBoxImage.Warning); + + if (result == MessageBoxResult.Yes) + { + SettingsManager.Settings.Startup.TelemetryUploadLevel = TelemetryUploadLevel.Basic; + SettingsManager.SaveSettingsToFile(); + LogHelper.WriteLogToFile("Settings | Telemetry enabled (Basic) for preview/beta update channel"); + } + else + { + SettingsManager.Settings.Startup.UpdateChannel = oldChannel; + RevertChannelSelection(oldChannel); + SettingsManager.SaveSettingsToFile(); + LogHelper.WriteLogToFile("Settings | User declined telemetry, reverted update channel"); + return; + } + } + + SettingsManager.Settings.Startup.UpdateChannel = newChannel; + DeviceIdentifier.UpdateUsageChannel(newChannel); + LogHelper.WriteLogToFile($"Settings | Update channel changed to {SettingsManager.Settings.Startup.UpdateChannel}"); + SettingsManager.SaveSettingsToFile(); + + // 通道切换后强制刷新更新日志和历史版本缓存 + _isHistoryLoaded = false; + _versionList.Clear(); + VersionComboBox.ItemsSource = null; + ReleaseNotesViewer.Markdown = ""; + await LoadChangelogAsync(); + + if (SettingsManager.Settings.Startup.IsAutoUpdate) + { + LogHelper.WriteLogToFile($"AutoUpdate | Channel changed to {newChannel}, performing immediate update check"); + + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + mainWindow.ResetUpdateCheckRetry(); + await System.Threading.Tasks.Task.Run(() => + { + try + { + Dispatcher.Invoke(() => + { + mainWindow.AutoUpdate(); + }); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"AutoUpdate | Error during channel switch update check: {ex.Message}", LogHelper.LogType.Error); + } + }); + } + } + } + + private void RevertChannelSelection(UpdateChannel targetChannel) + { + Dispatcher.BeginInvoke(new Action(() => + { + _isChangingUpdateChannelInternally = true; + try + { + string targetTag = targetChannel.ToString(); + foreach (var item in UpdateChannelSelector.Items) + { + if (item is ComboBoxItem cbi && cbi.Tag != null && + string.Equals(cbi.Tag.ToString(), targetTag, StringComparison.OrdinalIgnoreCase)) + { + UpdateChannelSelector.SelectedItem = cbi; + break; + } + } + } + finally + { + _isChangingUpdateChannelInternally = false; + } + }), DispatcherPriority.Normal); + } + + #endregion + + #region 更新状态机 + + private void ApplyState(UpdateUiState state, string customSubtitle = null) + { + _state = state; + + CheckUpdateButton.Visibility = Visibility.Collapsed; + UpdateNowButton.Visibility = Visibility.Collapsed; + UpdateLaterButton.Visibility = Visibility.Collapsed; + SkipVersionButton.Visibility = Visibility.Collapsed; + CancelDownloadButton.Visibility = Visibility.Collapsed; + ProgressPanel.Visibility = Visibility.Collapsed; + + CheckUpdateButton.IsEnabled = true; + + switch (state) + { + case UpdateUiState.Idle: + StatusIcon.Icon = SegoeFluentIcons.Completed; + StatusTitle.Text = "已是最新版本"; + StatusSubtitle.Text = customSubtitle ?? BuildLastCheckSubtitle(); + CheckUpdateButton.Visibility = Visibility.Visible; + break; + + case UpdateUiState.Checking: + StatusIcon.Icon = SegoeFluentIcons.Sync; + StatusTitle.Text = "正在检查更新..."; + StatusSubtitle.Text = ""; + CheckUpdateButton.Visibility = Visibility.Visible; + CheckUpdateButton.IsEnabled = false; + ProgressPanel.Visibility = Visibility.Visible; + ProgressText.Text = "正在连接更新服务器..."; + ProgressBar.IsIndeterminate = true; + break; + + case UpdateUiState.UpdateAvailable: + StatusIcon.Icon = SegoeFluentIcons.Upload; + StatusTitle.Text = $"检测到新版本 {_remoteVersion}"; + StatusSubtitle.Text = customSubtitle ?? $"当前版本 {GetCurrentVersion()} → {_remoteVersion}"; + UpdateNowButton.Visibility = Visibility.Visible; + UpdateLaterButton.Visibility = Visibility.Visible; + SkipVersionButton.Visibility = Visibility.Visible; + break; + + case UpdateUiState.Downloading: + StatusIcon.Icon = SegoeFluentIcons.Download; + StatusTitle.Text = "正在下载更新..."; + StatusSubtitle.Text = customSubtitle ?? $"目标版本 {_remoteVersion}"; + ProgressPanel.Visibility = Visibility.Visible; + ProgressBar.IsIndeterminate = false; + break; + + case UpdateUiState.Downloaded: + StatusIcon.Icon = SegoeFluentIcons.Download; + StatusTitle.Text = "更新已下载完成"; + StatusSubtitle.Text = customSubtitle ?? $"将在软件关闭时自动安装 {_remoteVersion}"; + CheckUpdateButton.Visibility = Visibility.Visible; + break; + + case UpdateUiState.NetworkError: + StatusIcon.Icon = SegoeFluentIcons.Error; + StatusTitle.Text = "网络错误"; + StatusSubtitle.Text = customSubtitle ?? "请检查网络连接后重试。"; + CheckUpdateButton.Visibility = Visibility.Visible; + break; + } + } + + private string BuildLastCheckSubtitle() + { + string current = GetCurrentVersion(); + return $"当前版本 {current}"; + } + + private static string GetCurrentVersion() + { + try + { + return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); + } + catch + { + return "未知"; + } + } + + #endregion + + #region 更新日志 + + private async System.Threading.Tasks.Task LoadChangelogAsync() + { + try + { + ChangelogViewer.Markdown = "正在加载更新日志..."; + + // 优先尝试从 GitHub API 获取最新 Release 的 body(带超时,失败则回退到镜像) + try + { + var apiTask = AutoUpdateHelper.GetAllGithubReleases(SettingsManager.Settings.Startup.UpdateChannel); + var completed = await System.Threading.Tasks.Task.WhenAny(apiTask, System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(8))); + if (completed == apiTask) + { + var releases = await apiTask; + var latest = releases? + .OrderByDescending(r => ParseVersionForSort(r.version)) + .Select(r => (Tuple)Tuple.Create(r.version, r.downloadUrl, r.releaseNotes)) + .FirstOrDefault(); + if (latest != null && !string.IsNullOrWhiteSpace(latest.Item3)) + { + ChangelogViewer.Markdown = latest.Item3; + return; + } + LogHelper.WriteLogToFile("UpdatePage | GitHub API 未返回可用的更新日志,回退到镜像", LogHelper.LogType.Warning); + } + else + { + LogHelper.WriteLogToFile("UpdatePage | GitHub API 获取更新日志超时,回退到镜像", LogHelper.LogType.Warning); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"UpdatePage | GitHub API 获取更新日志失败,回退到镜像: {ex.Message}", LogHelper.LogType.Warning); + } + + // 回退到镜像源 UpdateLog.md + string md = await AutoUpdateHelper.GetUpdateLog(SettingsManager.Settings.Startup.UpdateChannel); + ChangelogViewer.Markdown = string.IsNullOrEmpty(md) ? "暂无更新日志。" : md; + } + catch (Exception ex) + { + ChangelogViewer.Markdown = $"加载更新日志失败:{ex.Message}"; + } + } + + #endregion + + #region 检查 / 下载 / 安装 + + private async void CheckUpdateButton_Click(object sender, RoutedEventArgs e) + { + ApplyState(UpdateUiState.Checking); + try + { + LogHelper.WriteLogToFile("ManualUpdate | Manual update button clicked"); + + string remoteVersion = null; + string apiReleaseNotes = null; + AutoUpdateHelper.UpdateLineGroup lineGroup = null; + + // 优先通过 GitHub Releases API 获取最新版本 + try + { + var releases = await AutoUpdateHelper.GetAllGithubReleases(SettingsManager.Settings.Startup.UpdateChannel); + var latest = releases? + .OrderByDescending(r => ParseVersionForSort(r.version)) + .Select(r => Tuple.Create(r.version, r.downloadUrl, r.releaseNotes)) + .FirstOrDefault(); + if (latest != null && !string.IsNullOrEmpty(latest.Item1)) + { + var localVersion = new Version(GetCurrentVersion()); + var remote = ParseVersionForSort(latest.Item1); + if (remote > localVersion) + { + remoteVersion = latest.Item1.TrimStart('v', 'V'); + apiReleaseNotes = latest.Item3; + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"UpdatePage | GitHub API 检查更新失败,回退到 CheckForUpdates: {ex.Message}", LogHelper.LogType.Warning); + } + + // 回退:调用统一的 CheckForUpdates(包含镜像源 txt 方案) + if (string.IsNullOrEmpty(remoteVersion)) + { + var (rv, lg, notes) = await AutoUpdateHelper.CheckForUpdates( + SettingsManager.Settings.Startup.UpdateChannel, true, false); + remoteVersion = rv; + lineGroup = lg; + if (string.IsNullOrEmpty(apiReleaseNotes)) apiReleaseNotes = notes; + } + + if (!string.IsNullOrEmpty(remoteVersion)) + { + _remoteVersion = remoteVersion; + _remoteLineGroup = lineGroup; + _remoteReleaseNotes = apiReleaseNotes; + + if (!string.IsNullOrEmpty(apiReleaseNotes)) + ChangelogViewer.Markdown = apiReleaseNotes; + + ApplyState(UpdateUiState.UpdateAvailable); + } + else + { + ApplyState(UpdateUiState.Idle); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"Error in CheckUpdateButton_Click: {ex.Message}", LogHelper.LogType.Error); + ApplyState(UpdateUiState.NetworkError, ex.Message); + } + } + + private bool _downloadCancelled; + private static List _cachedOrderedGroups; + private static UpdateChannel _cachedGroupsChannel; + + private static async System.Threading.Tasks.Task> GetOrderedGroupsCachedAsync(UpdateChannel channel) + { + if (_cachedOrderedGroups != null && _cachedOrderedGroups.Count > 0 && _cachedGroupsChannel == channel) + return _cachedOrderedGroups; + var groups = await AutoUpdateHelper.GetAvailableLineGroupsOrdered(channel); + _cachedOrderedGroups = groups; + _cachedGroupsChannel = channel; + return groups; + } + + private async System.Threading.Tasks.Task DownloadWithProgressAsync() + { + _downloadCancelled = false; + var groups = await GetOrderedGroupsCachedAsync(SettingsManager.Settings.Startup.UpdateChannel); + if (groups == null || groups.Count == 0) + { + LogHelper.WriteLogToFile("UpdatePage | 没有可用的下载线路组", LogHelper.LogType.Error); + return false; + } + return await AutoUpdateHelper.DownloadSetupFileWithFallback(_remoteVersion, groups, (percent, text) => + { + if (_downloadCancelled) return; + Dispatcher.Invoke(() => + { + if (_state != UpdateUiState.Downloading) return; + ProgressBar.IsIndeterminate = false; + ProgressBar.Value = percent; + ProgressText.Text = text; + }); + }); + } + + private async void UpdateNowButton_Click(object sender, RoutedEventArgs e) + { + if (string.IsNullOrEmpty(_remoteVersion)) return; + + ApplyState(UpdateUiState.Downloading); + CancelDownloadButton.Visibility = Visibility.Visible; + ProgressBar.Value = 0; + ProgressText.Text = "正在准备下载..."; + + try + { + bool ok = await DownloadWithProgressAsync(); + + if (!ok) + { + ApplyState(UpdateUiState.NetworkError, "更新下载失败,请检查网络连接后重试。"); + return; + } + + MessageBoxResult result = MessageBox.Show( + "更新已下载完成,点击确定后将关闭软件并安装新版本!", + "安装更新", + MessageBoxButton.OKCancel, + MessageBoxImage.Information); + + if (result == MessageBoxResult.OK) + { + App.IsAppExitByUser = true; + AutoUpdateHelper.InstallNewVersionApp(_remoteVersion, true); + Application.Current.Shutdown(); + } + else + { + ApplyState(UpdateUiState.Downloaded); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"Error in UpdateNowButton_Click: {ex.Message}", LogHelper.LogType.Error); + ApplyState(UpdateUiState.NetworkError, ex.Message); + } + } + + private async void UpdateLaterButton_Click(object sender, RoutedEventArgs e) + { + if (string.IsNullOrEmpty(_remoteVersion)) return; + + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow == null) return; + + ApplyState(UpdateUiState.Downloading); + CancelDownloadButton.Visibility = Visibility.Visible; + ProgressBar.Value = 0; + ProgressText.Text = "正在后台下载..."; + + try + { + bool ok = await DownloadWithProgressAsync(); + + if (!ok) + { + ApplyState(UpdateUiState.NetworkError, "更新下载失败,请检查网络连接后重试。"); + return; + } + + SettingsManager.Settings.Startup.IsAutoUpdate = true; + SettingsManager.Settings.Startup.IsAutoUpdateWithSilence = true; + SettingsManager.SaveSettingsToFile(); + CardAutoUpdate.IsOn = true; + CardSilentUpdate.IsOn = true; + + mainWindow.StartSilentUpdateTimer(); + ApplyState(UpdateUiState.Downloaded); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"Error in UpdateLaterButton_Click: {ex.Message}", LogHelper.LogType.Error); + ApplyState(UpdateUiState.NetworkError, ex.Message); + } + } + + private void SkipVersionButton_Click(object sender, RoutedEventArgs e) + { + if (string.IsNullOrEmpty(_remoteVersion)) return; + + SettingsManager.Settings.Startup.SkippedVersion = _remoteVersion; + SettingsManager.SaveSettingsToFile(); + LogHelper.WriteLogToFile($"ManualUpdate | User chose to skip version {_remoteVersion}"); + + ApplyState(UpdateUiState.Idle, $"已跳过版本 {_remoteVersion}"); + } + + private void CancelDownloadButton_Click(object sender, RoutedEventArgs e) + { + _downloadCancelled = true; + AutoUpdateHelper.RequestCancelDownload(); + ApplyState(UpdateUiState.UpdateAvailable); + } + + #endregion + + #region 历史版本回滚 + + private async void UpdateTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (e.OriginalSource != UpdateTabControl) return; + if (UpdateTabControl.SelectedItem == HistoryTabItem && !_isHistoryLoaded) + { + await LoadHistoryAsync(); + } + } + + private async void HistoryTabItem_GotFocus(object sender, RoutedEventArgs e) + { + // 兼容用户首次切到历史版本 Tab 时再加载 + if (_isHistoryLoaded) return; + await LoadHistoryAsync(); + } + + private async System.Threading.Tasks.Task LoadHistoryAsync() + { + try + { + _isHistoryLoaded = true; + ReleaseNotesViewer.Markdown = "正在获取历史版本..."; + RollbackButton.IsEnabled = false; + + var releases = await AutoUpdateHelper.GetAllGithubReleases(SettingsManager.Settings.Startup.UpdateChannel); + _versionList = releases + .Select(r => new VersionItem { Version = r.version, DownloadUrl = r.downloadUrl, ReleaseNotes = r.releaseNotes }) + .OrderByDescending(v => ParseVersionForSort(v.Version)) + .ToList(); + VersionComboBox.ItemsSource = _versionList; + + if (_versionList.Count > 0) + { + VersionComboBox.SelectedIndex = 0; + RollbackButton.IsEnabled = true; + } + else + { + ReleaseNotesViewer.Markdown = "未获取到历史版本信息。"; + } + } + catch (Exception ex) + { + ReleaseNotesViewer.Markdown = $"加载历史版本失败:{ex.Message}"; + } + } + + private static Version ParseVersionForSort(string version) + { + var v = (version ?? "").TrimStart('v', 'V'); + return Version.TryParse(v, out var result) ? result : new Version(0, 0, 0, 0); + } + + private void VersionComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + _selectedHistoricalItem = VersionComboBox.SelectedItem as VersionItem; + if (_selectedHistoricalItem != null) + { + ReleaseNotesViewer.Markdown = _selectedHistoricalItem.ReleaseNotes ?? "无更新日志"; + LogHelper.WriteLogToFile($"HistoryRollback | 用户选择版本: {_selectedHistoricalItem.Version}"); + } + Keyboard.ClearFocus(); + } + + private async void RollbackButton_Click(object sender, RoutedEventArgs e) + { + if (_selectedHistoricalItem == null) return; + + int days = await AskPauseDaysAsync(); + if (days < 0) + { + LogHelper.WriteLogToFile("HistoryRollback | 用户取消了回滚操作"); + return; + } + + if (days == 0) + { + MainWindow.Settings.Startup.AutoUpdatePauseUntilDate = ""; + } + else + { + DateTime pauseUntilDate = DateTime.Now.AddDays(days); + MainWindow.Settings.Startup.AutoUpdatePauseUntilDate = pauseUntilDate.ToString("yyyy-MM-dd"); + LogHelper.WriteLogToFile($"HistoryRollback | 用户选择暂停自动更新 {days} 天,截止日期: {pauseUntilDate:yyyy-MM-dd}"); + } + MainWindow.SaveSettingsToFile(); + + LogHelper.WriteLogToFile($"HistoryRollback | 用户确认回滚,目标版本: {_selectedHistoricalItem.Version}"); + RollbackButton.IsEnabled = false; + VersionComboBox.IsEnabled = false; + RollbackProgressPanel.Visibility = Visibility.Visible; + RollbackProgressBar.Value = 0; + RollbackProgressText.Text = "正在准备下载..."; + + bool downloadSuccess = false; + try + { + downloadSuccess = await AutoUpdateHelper.StartManualDownloadAndInstall( + _selectedHistoricalItem.Version, + SettingsManager.Settings.Startup.UpdateChannel, + (percent, text) => + { + Dispatcher.Invoke(() => + { + RollbackProgressBar.Value = percent; + RollbackProgressText.Text = text; + }); + }); + } + catch (Exception ex) + { + RollbackProgressText.Text = $"下载失败: {ex.Message}"; + LogHelper.WriteLogToFile($"HistoryRollback | 下载异常: {ex.Message}", LogHelper.LogType.Error); + } + + if (downloadSuccess) + { + RollbackProgressBar.Value = 100; + RollbackProgressText.Text = "下载完成,准备安装..."; + } + else + { + RollbackProgressText.Text = "下载失败,请检查网络后重试。"; + RollbackButton.IsEnabled = true; + VersionComboBox.IsEnabled = true; + } + } + + private async System.Threading.Tasks.Task AskPauseDaysAsync() + { + var dialog = new ContentDialog + { + Title = "暂停自动更新", + PrimaryButtonText = "确定", + SecondaryButtonText = "取消" + }; + + var panel = new iNKORE.UI.WPF.Controls.SimpleStackPanel + { + Spacing = 16, + Margin = new Thickness(0, 10, 0, 0) + }; + + var textBlock = new TextBlock + { + Text = "请选择在回滚后多久不再接收自动更新:", + FontSize = 14 + }; + + var daysComboBox = new ComboBox + { + Width = 200, + Height = 36, + HorizontalAlignment = HorizontalAlignment.Left + }; + for (int i = 0; i <= 7; i++) + { + daysComboBox.Items.Add(new ComboBoxItem { Content = $"{i} 天", Tag = i }); + } + daysComboBox.SelectedIndex = 0; + + panel.Children.Add(textBlock); + panel.Children.Add(daysComboBox); + dialog.Content = panel; + + var result = await dialog.ShowAsync(); + if (result != ContentDialogResult.Primary) return -1; + + if (daysComboBox.SelectedItem is ComboBoxItem cbi && cbi.Tag is int days) + return days; + + return 1; + } + + #endregion + + #region 维护 + + private async void FixVersionButton_Click(object sender, RoutedEventArgs e) + { + var confirm = MessageBox.Show( + "此操作将下载当前选择通道的最新版本并安装,软件将自动关闭并更新。\n\n确定要执行版本修复吗?", + "版本修复确认", + MessageBoxButton.YesNo, + MessageBoxImage.Question); + + if (confirm != MessageBoxResult.Yes) return; + + FixVersionButton.IsEnabled = false; + FixVersionButton.Content = "正在修复..."; + + try + { + bool result = await AutoUpdateHelper.FixVersion(SettingsManager.Settings.Startup.UpdateChannel); + if (!result) + { + MessageBox.Show( + "版本修复失败,可能是网络问题或当前已是最新版本。", + "修复失败", + MessageBoxButton.OK, + MessageBoxImage.Error); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"Error in FixVersionButton_Click: {ex.Message}", LogHelper.LogType.Error); + MessageBox.Show( + $"版本修复过程中发生错误: {ex.Message}", + "修复错误", + MessageBoxButton.OK, + MessageBoxImage.Error); + } + finally + { + FixVersionButton.IsEnabled = true; + FixVersionButton.Content = "版本修复"; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Ink Canvas/Windows/SettingsViews/Pages/WindowPage.xaml b/Ink Canvas/Windows/SettingsViews/Pages/WindowPage.xaml new file mode 100644 index 00000000..0b3b0d34 --- /dev/null +++ b/Ink Canvas/Windows/SettingsViews/Pages/WindowPage.xaml @@ -0,0 +1,134 @@ + + + + + + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +