Compare commits

..

118 Commits

Author SHA1 Message Date
CJKmkp 8ed4f25499 improve:GitHub工作流 2025-12-13 21:12:18 +08:00
CJKmkp 3afd4641cd 更新版本号 2025-12-13 21:06:31 +08:00
CJKmkp e9fde97453 improve:GitHub工作流 2025-12-13 21:02:11 +08:00
CJKmkp 441f8b6e26 优化按钮名称 2025-12-13 20:47:30 +08:00
CJKmkp 8042b917a0 improve:浮动栏按钮UI
优化按钮显示
2025-12-13 20:45:31 +08:00
CJKmkp 343e7281fe improve:端点吸附
避免点线和虚线吸附
2025-12-13 20:32:43 +08:00
CJKmkp c64e6a4554 improve:issue #252
改进双指拖动
2025-12-13 20:22:23 +08:00
CJKmkp 8190bf275c Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-12-13 20:13:14 +08:00
CJKmkp e792f2637d improve:PPT墨迹加载 2025-12-13 20:13:08 +08:00
CJKmkp 3cd26323dc improve:翻页墨迹加载及浮动栏定位 2025-12-13 20:07:25 +08:00
PrefacedCorg 40e1c4d467 Update MW_FloatingBarIcons.cs 2025-12-13 19:54:10 +08:00
PrefacedCorg 86c22d373a Revert "feat: 添加白板模式自动全屏功能" 2025-12-13 19:48:21 +08:00
PrefacedCorg cb7a76efc5 Delete .vscode directory 2025-12-13 19:39:08 +08:00
PrefacedCorg 545425c4d3 Delete .kiro/steering directory 2025-12-13 19:38:34 +08:00
PrefacedCorg eb1aaa10e4 feat: 添加白板模式自动全屏功能
- 新增"白板模式自动全屏"设置选项
- 进入白板模式时自动全屏,退出时恢复工作区域大小
- 需配合"避免全屏助手"功能使用
- 用户可独立控制白板模式的全屏行为
2025-12-13 19:27:47 +08:00
CJKmkp daf0db312b improve:仅PPT模式
优化浮动栏显示
2025-12-13 18:02:05 +08:00
CJKmkp 8b2bc2f064 improve:退出放映时收纳及PPT记忆上次播放页数 2025-12-13 17:38:35 +08:00
CJKmkp 2e343cbbf9 improve:计时器
优化计时器行为
2025-12-13 17:20:18 +08:00
CJKmkp a75f0470bc improve:点名算法
缩小极差
2025-12-13 17:12:00 +08:00
CJKmkp 287d31a3a9 improve:窗口模式 2025-12-13 17:03:58 +08:00
CJKmkp 430fff0515 优化注释 2025-12-06 23:04:27 +08:00
CJKmkp fbbb7b8ad7 improve:issue #252
改进几何绘制
2025-12-06 23:01:41 +08:00
CJKmkp 54b74d7411 improve:issue #252
改进手掌擦及手势
2025-12-06 22:55:55 +08:00
CJKmkp 82dba31b2a improve:直线拉直
改进高精度拉直
2025-12-06 22:06:28 +08:00
CJKmkp 38d7e782e0 improve:issue #252
修复了几何绘制和多指书写状态问题
2025-11-30 11:51:19 +08:00
CJKmkp 05e5ceeb43 improve:计时器
改进主题加载
2025-11-29 23:11:01 +08:00
CJKmkp fcbbad71d2 更新版本号 2025-11-29 22:23:14 +08:00
CJKmkp 6f9161439f improve:直线拉直
改进直线拉直算法
2025-11-29 22:20:13 +08:00
CJKmkp aa0c4fb841 improve:Dlass服务
优化上传体验
2025-11-29 17:26:47 +08:00
CJKmkp cff50d1f81 fix:快捷键管理 2025-11-29 17:26:41 +08:00
CJKmkp b8581b6368 improve:点名算法
优化概率模型
2025-11-29 16:58:40 +08:00
CJKmkp 094f1223d1 improve:崩溃日志位置 2025-11-29 16:46:33 +08:00
CJKmkp 6802476afa improve:计时器UI
将计时器窗口整合至主窗口,优化全屏计时逻辑
2025-11-29 16:27:35 +08:00
CJKmkp a0539dce9b 更新版本号 2025-11-15 21:36:49 +08:00
CJKmkp bf2b8fec35 improve:计时器UI与点名UI
改进视觉反馈和按钮逻辑及窗口置顶
2025-11-15 21:14:08 +08:00
CJKmkp 082c9a03ec fix:issue #287 2025-11-15 20:48:04 +08:00
CJKmkp 4ccdd862ba improve:PPT模块
修复无焦点状态下键盘翻页无效
2025-11-15 20:24:28 +08:00
CJKmkp e5a20ed0fc improve:点名历史查看 2025-11-15 20:20:56 +08:00
CJKmkp a72022704e improve:点名算法 2025-11-15 19:34:52 +08:00
CJKmkp 2acc7ada30 improve:错误检测 2025-11-15 19:31:57 +08:00
CJKmkp e61882c331 improve:点名
点名算法优化
2025-11-15 19:20:35 +08:00
CJKmkp 61c145689a fix:issue #281 2025-11-15 18:41:42 +08:00
CJKmkp 40ea9664a7 improve:快抽置顶 2025-11-15 18:37:23 +08:00
CJKmkp bccd2d0f3e improve:PPT墨迹保存 2025-11-15 18:19:53 +08:00
CJKmkp b918809dca improve:线擦擦除 2025-11-15 18:06:36 +08:00
CJKmkp e7c2e92879 improve:PPT联动及墨迹管理
改回原处理模式,优化翻页操作
2025-11-15 18:04:22 +08:00
CJK_mkp cf03c921a7 更新版本号 2025-11-08 23:16:36 +08:00
CJK_mkp 2c45c839b1 Revert "improve:无焦点模式"
This reverts commit 83f5fc58d1.
2025-11-08 23:09:04 +08:00
CJK_mkp 83f5fc58d1 improve:无焦点模式 2025-11-08 22:55:18 +08:00
CJK_mkp 1a267f1e5a improve:点名 2025-11-08 22:23:44 +08:00
CJK_mkp c3fd5551d8 improve:快抽按钮 2025-11-08 22:17:52 +08:00
CJK_mkp 1baa74bb69 improve:快抽按钮 2025-11-08 22:07:38 +08:00
CJK_mkp 261ecefb17 improve:注释 2025-11-08 21:42:33 +08:00
CJK_mkp d81d8f7c5d improve:Dlass联动 2025-11-08 21:01:50 +08:00
CJK_mkp de1af12157 更新版本号 2025-11-08 20:41:43 +08:00
CJK_mkp 803cbbdee9 improve:默认设置 2025-11-08 20:29:18 +08:00
CJK_mkp 008477d5fa improve:默认设置 2025-11-08 20:17:51 +08:00
CJK_mkp 7f0d29ebd2 improve:快抽窗口 2025-11-08 20:07:00 +08:00
CJK_mkp 11bf8cffb2 improve:PPT墨迹加载 2025-11-08 19:55:10 +08:00
CJK_mkp 87b9ebc7e1 improve:PPT墨迹显示 2025-11-08 19:36:05 +08:00
CJK_mkp b89d27411b improve:计时器逻辑 2025-11-08 19:20:26 +08:00
CJK_mkp 24c37f1d3e fix:计时器时间不一致 2025-11-08 19:17:58 +08:00
CJK_mkp 58b0a0a3be fix:负数导致的计时器崩溃 2025-11-08 19:15:16 +08:00
CJK_mkp ed58873a82 fix:issue #272 2025-11-08 19:02:37 +08:00
CJK_mkp a8dcbd4af0 add:点名历史查看 2025-11-08 18:01:56 +08:00
CJK_mkp 4b2f29442a improve:点名 2025-11-07 10:36:24 +08:00
CJK_mkp dfab0d7ddf improvve:点名快抽 2025-11-07 10:35:27 +08:00
CJK_mkp ce1998b701 improve:Dlass 界面UI 2025-11-07 09:46:21 +08:00
CJK_mkp 92c631d6ce improve:操作逻辑 2025-11-02 12:34:50 +08:00
CJK_mkp 01009f9e35 更新版本号 2025-11-02 11:47:52 +08:00
CJK_mkp 74eca093da improve:悬浮快抽按钮 2025-11-02 11:40:27 +08:00
CJK_mkp e7d89e65b2 improve:AutoUpdate 2025-11-02 11:27:46 +08:00
CJK_mkp 24b2bffe8e improve:计时器窗口 2025-11-02 11:12:13 +08:00
CJK_mkp d2906476c8 add:Dlass联动 2025-11-02 10:46:16 +08:00
CJK_mkp 2b31a355ae add:Dlass联动 2025-11-02 10:30:36 +08:00
CJK_mkp b602048186 add:Dlass联动 2025-11-02 10:16:31 +08:00
CJK_mkp 4fb7031060 add:Dlass联动 2025-11-02 10:11:15 +08:00
CJK_mkp 4ef77c2e72 add:Dlass联动 2025-11-02 09:41:53 +08:00
CJK_mkp 72ba1a9f58 add:Dlass联动 2025-11-02 09:29:06 +08:00
CJK_mkp 0c3938b652 Merge pull request #274 from MKStoler1024/patch-1
Readme
2025-11-01 22:18:46 +08:00
CJK_mkp 16f80adb0d fix:issue #210 2025-11-01 22:12:47 +08:00
CJK_mkp 637b6bb4f9 fix:issue #210 2025-11-01 22:05:19 +08:00
CJK_mkp 12e91927a5 fix:issue #210 2025-11-01 21:56:31 +08:00
CJK_mkp 8f6f22ba7f 更新版本号 2025-11-01 21:37:55 +08:00
CJK_mkp b520d6a334 fix:issue #210 2025-11-01 21:00:23 +08:00
CJK_mkp b7bff30445 improve:UI 2025-11-01 20:49:37 +08:00
CJK_mkp 0d790bbd80 add:墨迹自动保存 2025-11-01 20:42:18 +08:00
CJK_mkp 8394e99127 improve:主题切换 2025-11-01 20:26:52 +08:00
CJK_mkp 16ae32bfd7 improve:点名UI 2025-11-01 19:17:52 +08:00
CJK_mkp cc6423e384 improve:点名UI 2025-11-01 18:56:38 +08:00
CJK_mkp 06cc587599 improve:启动动画 2025-11-01 18:46:04 +08:00
CJK_mkp 9d36088f1d fix:issue #210 2025-11-01 18:39:52 +08:00
CJK_mkp f9f73b015c improve:启动动画 2025-11-01 18:31:27 +08:00
CJK_mkp 77ffd696bb fix:手势面板不显示 2025-11-01 18:29:58 +08:00
CJK_mkp bdb8bed053 fix:issue #210 2025-11-01 18:25:35 +08:00
MKStoler1024 4b17c8e96e chore: remove sth in readme 2025-10-31 17:56:02 +08:00
MKStoler1024 8109711f4e chore: readme 2025-10-31 16:07:03 +08:00
CJK_mkp 327eba3fa7 Update MW_FloatingBarIcons.cs 2025-10-31 15:35:27 +08:00
CJK_mkp f3dccb2e99 Update MW_FloatingBarIcons.cs 2025-10-31 14:46:20 +08:00
CJK_mkp 7112d58e7c Update MW_ShapeDrawing.cs 2025-10-31 14:44:59 +08:00
CJK_mkp 6e0aad853c Update MW_TouchEvents.cs 2025-10-31 14:44:05 +08:00
CJK_mkp c64b1d0846 Update MW_BoardIcons.cs 2025-10-31 14:43:15 +08:00
CJK_mkp 6eba16ce99 Update MW_BoardIcons.cs 2025-10-31 12:19:30 +08:00
CJK_mkp 2f6f719843 Update MW_ShapeDrawing.cs 2025-10-31 12:18:45 +08:00
CJK_mkp f34bac49e4 Update MW_TouchEvents.cs 2025-10-31 12:16:52 +08:00
CJK_mkp 39cdc6231f Update MW_FloatingBarIcons.cs 2025-10-31 12:16:05 +08:00
CJK_mkp 745e24da70 Update MW_FloatingBarIcons.cs 2025-10-31 12:15:26 +08:00
CJK_mkp a4f4f4fb15 Update MW_TouchEvents.cs 2025-10-31 12:12:51 +08:00
CJK_mkp 3833c229c6 Update MW_FloatingBarIcons.cs 2025-10-31 12:11:02 +08:00
CJK_mkp 3dc3e9b5a8 Update MW_FloatingBarIcons.cs 2025-10-31 10:49:10 +08:00
CJK_mkp 12eeb79e9f 回滚一下 2025-10-30 12:02:29 +08:00
CJK_mkp e08c84f70d improve:点名快抽 2025-10-27 17:54:58 +08:00
CJK_mkp 10f55d5b65 fix:抽选结果截断 2025-10-27 17:45:41 +08:00
CJKmkp 761992d089 Delete PositionConverters.cs 2025-10-26 00:31:32 +08:00
CJKmkp 991a823700 更新版本号 2025-10-26 00:24:28 +08:00
CJKmkp 61dbcf762c add:点名快抽 2025-10-26 00:21:10 +08:00
CJKmkp 60b0149a9c add:新点名UI 2025-10-26 00:00:13 +08:00
CJKmkp 501c034cfa fix:点名UI按钮主题显示 2025-10-25 21:14:59 +08:00
57 changed files with 9976 additions and 3842 deletions
+21 -12
View File
@@ -12,6 +12,7 @@ on:
- patch
- minor
- major
- build
prerelease:
description: 'Create as pre-release'
required: true
@@ -20,7 +21,7 @@ on:
jobs:
prerelease:
if: github.ref == 'refs/heads/main'
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta'
runs-on: windows-latest
permissions:
contents: write
@@ -51,7 +52,7 @@ jobs:
echo "Found latest tag: $latestTag"
} else {
# 如果没有tag,使用默认版本
$version = "1.0.0"
$version = "1.0.0.0"
echo "No tags found, using default version"
}
echo "current_version=$version" >> $env:GITHUB_OUTPUT
@@ -63,16 +64,18 @@ jobs:
$currentVersion = "${{ steps.get_version.outputs.current_version }}"
$versionParts = $currentVersion.Split('.')
# 确保版本号格式正确(至少3部分)
# 确保版本号格式正确(支持4部分)
if ($versionParts.Length -ge 3) {
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
$build = [int]$versionParts[3]
} else {
# 如果版本号格式不正确,使用默认值
$major = 1
$minor = 0
$patch = 0
$build = 0
}
$versionType = "${{ github.event.inputs.version_type }}"
@@ -82,18 +85,24 @@ jobs:
$major++
$minor = 0
$patch = 0
$build = 0
}
"minor" {
$minor++
$patch = 0
$build = 0
}
"patch" {
$patch++
$build = 0
}
"build" {
$build++
}
}
# 生成新版本号(保持3位格式,如1.7.13
$newVersion = "$major.$minor.$patch"
# 生成新版本号(4位格式,如1.7.18.0
$newVersion = "$major.$minor.$patch.$build"
echo "new_version=$newVersion" >> $env:GITHUB_OUTPUT
echo "New version: $newVersion"
@@ -150,7 +159,7 @@ jobs:
# 生成changelog内容
$version = "${{ steps.calc_version.outputs.new_version }}"
$changelog = "# ICC CE $version.0 更新日志`n`n## 修复 (Fixes)"
$changelog = "# ICC CE $version 更新日志`n`n## 修复 (Fixes)"
if ($fixes.Count -gt 0) {
foreach ($fix in $fixes) {
@@ -236,7 +245,7 @@ jobs:
- name: Create Release Archive
run: |
$version = "${{ steps.calc_version.outputs.new_version }}"
$archiveName = "InkCanvasForClass.CE.$version.0.zip"
$archiveName = "InkCanvasForClass.CE.$version.zip"
# 创建发布目录
New-Item -ItemType Directory -Path "release" -Force
@@ -254,7 +263,7 @@ jobs:
with:
name: release-files-${{ steps.calc_version.outputs.new_version }}
path: |
InkCanvasForClass.CE.${{ steps.calc_version.outputs.new_version }}.0.zip
InkCanvasForClass.CE.${{ steps.calc_version.outputs.new_version }}.zip
CHANGELOG_${{ steps.calc_version.outputs.new_version }}.md
- name: Prepare Release Info
@@ -265,14 +274,14 @@ jobs:
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.calc_version.outputs.new_version }}.0
name: ICC CE ${{ steps.calc_version.outputs.new_version }}.0
tag_name: ${{ steps.calc_version.outputs.new_version }}
name: ICC CE ${{ steps.calc_version.outputs.new_version }}
body: |
${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: ${{ github.event.inputs.prerelease }}
files: |
InkCanvasForClass.CE.${{ steps.calc_version.outputs.new_version }}.0.zip
InkCanvasForClass.CE.${{ steps.calc_version.outputs.new_version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -285,7 +294,7 @@ jobs:
$changelogContent = Get-Content $changelogFile -Raw
# 生成预览内容
$previewContent = "ICC CE $version.0 更新日志`n" + $changelogContent
$previewContent = "ICC CE $version 更新日志`n" + $changelogContent
echo "UpdateLog preview generated (not written to file):"
echo $previewContent
+1 -1
View File
@@ -1 +1 @@
1.7.15.0
1.7.18.0
+69 -9
View File
@@ -31,7 +31,7 @@ namespace Ink_Canvas
Mutex mutex;
public static string[] StartArgs;
public static string RootPath = Environment.GetEnvironmentVariable("APPDATA") + "\\Ink Canvas\\";
public static string RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
// 新增:标记是否通过--board参数启动
public static bool StartWithBoardMode = false;
@@ -46,7 +46,7 @@ namespace Ink_Canvas
// 新增:退出信号文件路径
private static string watchdogExitSignalFile = Path.Combine(Path.GetTempPath(), "icc_watchdog_exit_" + Process.GetCurrentProcess().Id + ".flag");
// 新增:崩溃日志文件路径
private static string crashLogFile = Path.Combine(Environment.GetEnvironmentVariable("APPDATA"), "Ink Canvas", "crash_logs");
private static string crashLogFile = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "Crashes");
// 新增:进程ID
private static int currentProcessId = Process.GetCurrentProcess().Id;
// 新增:应用启动时间
@@ -107,7 +107,6 @@ namespace Ink_Canvas
if (isWindows7)
{
LogHelper.WriteLogToFile("检测到Windows 7系统,配置TLS协议支持");
// 启用所有TLS版本以支持Windows 7
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
@@ -117,17 +116,14 @@ namespace Ink_Canvas
ServicePointManager.Expect100Continue = false;
ServicePointManager.UseNagleAlgorithm = false;
LogHelper.WriteLogToFile("TLS协议配置完成,已启用TLS 1.2/1.1/1.0支持");
}
else
{
// 对于更新的Windows版本,不进行任何TLS配置,使用系统默认设置
LogHelper.WriteLogToFile($"检测到Windows版本: {osVersion.VersionString},使用系统默认TLS配置");
}
}
catch (Exception ex)
catch (Exception)
{
LogHelper.WriteLogToFile($"配置TLS协议时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
@@ -346,6 +342,25 @@ namespace Ink_Canvas
try
{
var exception = e.ExceptionObject as Exception;
if (exception is InvalidOperationException invalidOpEx)
{
string exceptionMessage = invalidOpEx.Message ?? "";
string exceptionStackTrace = invalidOpEx.StackTrace ?? "";
if (exceptionMessage.Contains("调用线程无法访问此对象") ||
exceptionMessage.Contains("because another thread owns it") ||
exceptionStackTrace.Contains("DynamicRenderer") ||
exceptionStackTrace.Contains("CompositionTarget.get_RootVisual"))
{
LogHelper.WriteLogToFile(
$"检测到DynamicRenderer线程访问异常: {invalidOpEx.Message}",
LogHelper.LogType.Warning
);
return;
}
}
string errorMessage = exception?.ToString() ?? "未知异常";
lastErrorMessage = errorMessage;
@@ -361,8 +376,11 @@ namespace Ink_Canvas
// 尝试在最后时刻记录错误
try
{
string timeStr = (appStartTime != default(DateTime) && appStartTime != DateTime.MinValue)
? appStartTime.ToString("yyyy-MM-dd-HH-mm-ss")
: DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss");
File.AppendAllText(
Path.Combine(crashLogFile, $"critical_error_{DateTime.Now:yyyyMMdd_HHmmss}.log"),
Path.Combine(crashLogFile, $"Crash_{timeStr}.txt"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 记录未处理异常时发生错误: {ex.Message}\r\n"
);
}
@@ -500,7 +518,10 @@ namespace Ink_Canvas
Directory.CreateDirectory(crashLogFile);
}
string logFileName = Path.Combine(crashLogFile, $"crash_{DateTime.Now:yyyyMMdd}.log");
string appStartTimeStr = (appStartTime != default(DateTime) && appStartTime != DateTime.MinValue)
? appStartTime.ToString("yyyy-MM-dd-HH-mm-ss")
: DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss");
string logFileName = Path.Combine(crashLogFile, $"Crash_{appStartTimeStr}.txt");
// 收集系统状态信息
string memoryUsage = (Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024)) + " MB";
@@ -549,6 +570,31 @@ namespace Ink_Canvas
private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
// 检查是否是DynamicRenderer线程访问UI对象的已知问题
if (e.Exception is InvalidOperationException invalidOpEx)
{
string exceptionMessage = invalidOpEx.Message ?? "";
string exceptionStackTrace = invalidOpEx.StackTrace ?? "";
// 检查是否是DynamicRenderer相关的线程访问问题
if (exceptionMessage.Contains("调用线程无法访问此对象") ||
exceptionMessage.Contains("because another thread owns it") ||
exceptionStackTrace.Contains("DynamicRenderer") ||
exceptionStackTrace.Contains("CompositionTarget.get_RootVisual"))
{
// 这是WPF InkCanvas的已知问题,DynamicRenderer的后台线程尝试访问UI对象
// 这个异常不会影响应用程序功能,可以安全地忽略
LogHelper.WriteLogToFile(
$"检测到DynamicRenderer线程访问异常(已安全处理): {invalidOpEx.Message}",
LogHelper.LogType.Warning
);
// 标记为已处理,不显示错误消息,不触发重启
e.Handled = true;
return;
}
}
Ink_Canvas.MainWindow.ShowNewMessage("抱歉,出现未预期的异常,可能导致 InkCanvasForClass 运行不稳定。\n建议保存墨迹后重启应用。");
LogHelper.NewLog(e.Exception.ToString());
@@ -706,6 +752,20 @@ namespace Ink_Canvas
{
LogHelper.WriteLogToFile($"App | 清理更新标记文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
Task.Run(async () =>
{
try
{
await Task.Delay(3000);
LogHelper.WriteLogToFile("App | 最终应用启动,删除AutoUpdate文件夹");
AutoUpdateHelper.DeleteUpdatesFolder();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"App | 删除AutoUpdate文件夹失败: {ex.Message}", LogHelper.LogType.Warning);
}
});
}
// 如果不是最终应用启动,才检查更新标记文件
+2 -2
View File
@@ -49,5 +49,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.15.0")]
[assembly: AssemblyFileVersion("1.7.15.0")]
[assembly: AssemblyVersion("1.7.18.1")]
[assembly: AssemblyFileVersion("1.7.18.1")]
@@ -0,0 +1,86 @@
<UserControl x:Class="Ink_Canvas.Controls.QuickDrawFloatingButtonControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Width="65" Height="45">
<UserControl.Resources>
<ResourceDictionary>
<!-- 悬浮按钮资源 -->
<SolidColorBrush x:Key="QuickDrawFloatingButtonBackground" Color="#80000000"/>
<SolidColorBrush x:Key="QuickDrawFloatingButtonBorderBrush" Color="#40000000"/>
<SolidColorBrush x:Key="QuickDrawFloatingButtonIconForeground" Color="White"/>
</ResourceDictionary>
</UserControl.Resources>
<Border Background="{DynamicResource QuickDrawFloatingButtonBackground}"
CornerRadius="8"
BorderThickness="1"
BorderBrush="{DynamicResource QuickDrawFloatingButtonBorderBrush}">
<Border.Effect>
<DropShadowEffect Color="Black" Direction="315" ShadowDepth="3" Opacity="0.3" BlurRadius="5"/>
</Border.Effect>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="22"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 拖动区域 -->
<Border Grid.Column="0"
MouseLeftButtonDown="DragArea_MouseLeftButtonDown"
MouseMove="DragArea_MouseMove"
MouseLeftButtonUp="DragArea_MouseLeftButtonUp"
Cursor="SizeAll"
Background="Transparent">
<Grid VerticalAlignment="Center" Height="14" IsHitTestVisible="False">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 三个白色横线 -->
<Border Grid.Row="0" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
<Border Grid.Row="2" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
<Border Grid.Row="4" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
</Grid>
</Border>
<!-- 半透明分割线 -->
<Rectangle Grid.Column="1" Width="1" Fill="#20FFFFFF" Margin="0,8,0,8"/>
<!-- 按钮区域 -->
<Border Grid.Column="2"
MouseLeftButtonDown="FloatingButton_Click"
Cursor="Hand"
Background="Transparent">
<Grid IsHitTestVisible="False">
<Path Data="M5 7C5 8.06087 5.42143 9.07828 6.17157 9.82843C6.92172 10.5786 7.93913 11 9 11C10.0609 11 11.0783 10.5786 11.8284 9.82843C12.5786 9.07828 13 8.06087 13 7C13 5.93913 12.5786 4.92172 11.8284 4.17157C11.0783 3.42143 10.0609 3 9 3C7.93913 3 6.92172 3.42143 6.17157 4.17157C5.42143 4.92172 5 5.93913 5 7Z M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21 M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88 M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15"
Stroke="{DynamicResource QuickDrawFloatingButtonIconForeground}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False"/>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,149 @@
using Ink_Canvas.Windows;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
using HorizontalAlignment = System.Windows.HorizontalAlignment;
using VerticalAlignment = System.Windows.VerticalAlignment;
namespace Ink_Canvas.Controls
{
/// <summary>
/// 快抽悬浮按钮控件
/// </summary>
public partial class QuickDrawFloatingButtonControl : UserControl
{
private bool _isDragging = false;
private Point _dragStartPoint;
private Point _controlStartPoint;
public QuickDrawFloatingButtonControl()
{
InitializeComponent();
}
/// <summary>
/// 快抽按钮点击事件
/// </summary>
private void FloatingButton_Click(object sender, MouseButtonEventArgs e)
{
try
{
// 如果正在拖动,不触发点击事件
if (_isDragging) return;
// 打开快抽窗口
var quickDrawWindow = new QuickDrawWindow();
quickDrawWindow.ShowDialog();
}
catch (Exception ex)
{
Helpers.LogHelper.WriteLogToFile($"打开快抽窗口失败: {ex.Message}", Helpers.LogHelper.LogType.Error);
}
}
/// <summary>
/// 拖动区域鼠标按下事件
/// </summary>
private void DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isDragging = false;
// 记录鼠标在屏幕上的初始位置
_dragStartPoint = this.PointToScreen(e.GetPosition(this));
// 记录控件的初始位置
var parent = this.Parent as FrameworkElement;
if (parent != null)
{
var transform = this.TransformToVisual(parent);
var currentPos = transform.Transform(new Point(0, 0));
_controlStartPoint = currentPos;
}
else
{
var currentMargin = this.Margin;
_controlStartPoint = new Point(
double.IsNaN(currentMargin.Left) ? 0 : currentMargin.Left,
double.IsNaN(currentMargin.Top) ? 0 : currentMargin.Top);
}
((UIElement)sender).CaptureMouse();
e.Handled = true;
}
/// <summary>
/// 拖动区域鼠标移动事件
/// </summary>
private void DragArea_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && ((UIElement)sender).IsMouseCaptured)
{
// 获取鼠标在屏幕上的当前位置
Point currentScreenPoint = this.PointToScreen(e.GetPosition(this));
Vector diff = currentScreenPoint - _dragStartPoint;
if (!_isDragging && (Math.Abs(diff.X) > 3 || Math.Abs(diff.Y) > 3))
{
_isDragging = true;
// 切换到绝对定位模式
this.HorizontalAlignment = HorizontalAlignment.Left;
this.VerticalAlignment = VerticalAlignment.Top;
}
if (_isDragging)
{
// 计算新位置
var parent = this.Parent as FrameworkElement;
if (parent != null)
{
// 计算屏幕坐标相对于父容器的位置
var parentPoint = parent.PointFromScreen(currentScreenPoint);
var startParentPoint = parent.PointFromScreen(_dragStartPoint);
// 计算相对于初始位置的偏移
double offsetX = parentPoint.X - startParentPoint.X;
double offsetY = parentPoint.Y - startParentPoint.Y;
// 新位置 = 初始位置 + 偏移
double newLeft = _controlStartPoint.X + offsetX;
double newTop = _controlStartPoint.Y + offsetY;
// 限制在父容器范围内
newLeft = Math.Max(0, Math.Min(newLeft, parent.ActualWidth - this.ActualWidth));
newTop = Math.Max(0, Math.Min(newTop, parent.ActualHeight - this.ActualHeight));
// 更新Margin
this.Margin = new Thickness(newLeft, newTop, 0, 0);
}
}
}
}
/// <summary>
/// 拖动区域鼠标释放事件
/// </summary>
private void DragArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (((UIElement)sender).IsMouseCaptured)
{
((UIElement)sender).ReleaseMouseCapture();
}
if (_isDragging)
{
Dispatcher.BeginInvoke(new Action(() => { _isDragging = false; }),
DispatcherPriority.Background);
}
else
{
_isDragging = false;
}
e.Handled = true;
}
}
}
+37 -2
View File
@@ -281,8 +281,16 @@ namespace Ink_Canvas.Helpers
// 应用旋转
Bitmap rotatedFrame = ApplyRotation(sourceFrame);
// 应用分辨率调整
_currentFrame = ResizeImage(rotatedFrame, _resolutionWidth, _resolutionHeight);
int targetWidth = _resolutionWidth;
int targetHeight = _resolutionHeight;
if (_rotationAngle == 1 || _rotationAngle == 3)
{
targetWidth = _resolutionHeight;
targetHeight = _resolutionWidth;
}
_currentFrame = ResizeImageWithAspectRatio(rotatedFrame, targetWidth, targetHeight);
rotatedFrame?.Dispose();
}
@@ -357,6 +365,33 @@ namespace Ink_Canvas.Helpers
return rotated;
}
/// <summary>
/// 调整图像大小
/// </summary>
private Bitmap ResizeImageWithAspectRatio(Bitmap source, int targetWidth, int targetHeight)
{
if (source.Width == targetWidth && source.Height == targetHeight)
return new Bitmap(source);
double scaleX = (double)targetWidth / source.Width;
double scaleY = (double)targetHeight / source.Height;
double scale = Math.Min(scaleX, scaleY);
// 计算实际尺寸
int actualWidth = (int)(source.Width * scale);
int actualHeight = (int)(source.Height * scale);
var resized = new Bitmap(actualWidth, actualHeight, PixelFormat.Format24bppRgb);
using (var graphics = Graphics.FromImage(resized))
{
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
graphics.DrawImage(source, 0, 0, actualWidth, actualHeight);
}
return resized;
}
/// <summary>
/// 调整图像大小
/// </summary>
+456
View File
@@ -0,0 +1,456 @@
using Ink_Canvas.Helpers;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass API 客户端,用于与服务端通信
/// </summary>
public class DlassApiClient : IDisposable
{
private const string DEFAULT_BASE_URL = "https://dlass.tech";
private readonly string _appId;
private readonly string _appSecret;
private readonly string _baseUrl;
private HttpClient _httpClient;
private string _accessToken;
private DateTime _tokenExpiresAt;
private string _userToken;
/// <summary>
/// 初始化 Dlass API 客户端
/// </summary>
/// <param name="appId">应用ID</param>
/// <param name="appSecret">应用密钥</param>
/// <param name="baseUrl">API基础URL,如果为空则使用默认URL</param>
/// <param name="userToken">用户Token,如果提供则优先使用用户token而不是App Secret</param>
public DlassApiClient(string appId, string appSecret, string baseUrl = null, string userToken = null)
{
_appId = appId ?? throw new ArgumentNullException(nameof(appId));
_appSecret = appSecret ?? throw new ArgumentNullException(nameof(appSecret));
_userToken = userToken;
_baseUrl = baseUrl ?? DEFAULT_BASE_URL;
_baseUrl = _baseUrl.TrimEnd('/');
if (!_baseUrl.StartsWith("http://") && !_baseUrl.StartsWith("https://"))
{
_baseUrl = "https://" + _baseUrl;
}
_httpClient = new HttpClient
{
BaseAddress = new Uri(_baseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
_httpClient.DefaultRequestHeaders.Add("User-Agent", "InkCanvas/1.0");
}
/// <summary>
/// 获取访问令牌(Access Token
/// </summary>
public async Task<string> GetAccessTokenAsync()
{
if (!string.IsNullOrEmpty(_userToken))
{
return _userToken;
}
if (!string.IsNullOrEmpty(_accessToken) && DateTime.Now < _tokenExpiresAt.AddMinutes(-5))
{
return _accessToken;
}
try
{
var requestData = new
{
app_id = _appId,
app_secret = _appSecret,
grant_type = "client_credentials"
};
var json = JsonConvert.SerializeObject(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/oauth/token", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
_accessToken = tokenResponse.AccessToken;
_tokenExpiresAt = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn ?? 3600);
return _accessToken;
}
else
{
throw new Exception($"获取Access Token失败: {response.StatusCode}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"获取Access Token时网络错误: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception("获取Access Token时请求超时", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"获取Access Token时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送GET请求
/// </summary>
public async Task<T> GetAsync<T>(string endpoint, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"请求超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送POST请求
/// </summary>
public async Task<T> PostAsync<T>(string endpoint, object data = null, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"请求超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送PUT请求
/// </summary>
public async Task<T> PutAsync<T>(string endpoint, object data = null, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"请求超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送DELETE请求
/// </summary>
public async Task<bool> DeleteAsync(string endpoint, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return true;
}
else
{
return false;
}
}
catch (HttpRequestException)
{
return false;
}
catch (TaskCanceledException)
{
return false;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 上传笔记文件
/// </summary>
/// <param name="endpoint">上传端点</param>
/// <param name="filePath">文件路径</param>
/// <param name="boardId">白板ID</param>
/// <param name="secretKey">白板密钥</param>
/// <param name="title">笔记标题(可选)</param>
/// <param name="description">笔记描述(可选)</param>
/// <param name="tags">笔记标签(可选)</param>
public async Task<T> UploadNoteAsync<T>(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null)
{
try
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"文件不存在: {filePath}");
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
// 设置白板认证头
request.Headers.Add("X-Board-ID", boardId);
request.Headers.Add("X-Secret-Key", secretKey);
// 创建multipart/form-data内容
var content = new MultipartFormDataContent();
// 添加文件
var fileContent = new ByteArrayContent(File.ReadAllBytes(filePath));
var fileName = Path.GetFileName(filePath);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(fileContent, "file", fileName);
// 添加可选参数
if (!string.IsNullOrEmpty(title))
{
content.Add(new StringContent(title), "title");
}
if (!string.IsNullOrEmpty(description))
{
content.Add(new StringContent(description), "description");
}
if (!string.IsNullOrEmpty(tags))
{
content.Add(new StringContent(tags), "tags");
}
request.Content = content;
var response = await _httpClient.SendAsync(request);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(responseContent))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(responseContent);
}
else
{
throw new Exception($"上传文件失败: {response.StatusCode} - {responseContent}");
}
}
catch (HttpRequestException httpEx)
{
throw new Exception($"上传文件时网络错误: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"上传文件超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"上传文件时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_httpClient?.Dispose();
}
#region
/// <summary>
/// Token响应模型
/// </summary>
private class TokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int? ExpiresIn { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
}
#endregion
}
}
+758
View File
@@ -0,0 +1,758 @@
using Ink_Canvas.Helpers;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass笔记自动上传辅助类
/// </summary>
public class DlassNoteUploader
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
private const int BATCH_SIZE = 10; // 批量上传大小
private const int MAX_RETRY_COUNT = 3; // 最大重试次数
private const string QUEUE_FILE_NAME = "DlassUploadQueue.json";
/// <summary>
/// 上传队列项
/// </summary>
private class UploadQueueItemData
{
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("retry_count")]
public int RetryCount { get; set; }
[JsonProperty("added_time")]
public DateTime AddedTime { get; set; }
}
/// <summary>
/// 上传队列项
/// </summary>
private class UploadQueueItem
{
public string FilePath { get; set; }
public int RetryCount { get; set; }
}
/// <summary>
/// 上传队列
/// </summary>
private static readonly ConcurrentQueue<UploadQueueItem> _uploadQueue = new ConcurrentQueue<UploadQueueItem>();
/// <summary>
/// 队列处理锁,防止并发处理
/// </summary>
private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 队列保存锁,防止并发保存
/// </summary>
private static readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 是否已初始化队列
/// </summary>
private static bool _isQueueInitialized = false;
/// <summary>
/// 获取队列文件路径
/// </summary>
private static string GetQueueFilePath()
{
var configsDir = Path.Combine(App.RootPath, "Configs");
if (!Directory.Exists(configsDir))
{
Directory.CreateDirectory(configsDir);
}
return Path.Combine(configsDir, QUEUE_FILE_NAME);
}
/// <summary>
/// 初始化上传队列
/// </summary>
public static void InitializeQueue()
{
if (_isQueueInitialized)
{
return;
}
try
{
var queueFilePath = GetQueueFilePath();
if (!File.Exists(queueFilePath))
{
_isQueueInitialized = true;
return;
}
var jsonContent = File.ReadAllText(queueFilePath);
if (string.IsNullOrWhiteSpace(jsonContent))
{
_isQueueInitialized = true;
return;
}
var queueData = JsonConvert.DeserializeObject<List<UploadQueueItemData>>(jsonContent);
if (queueData == null || queueData.Count == 0)
{
_isQueueInitialized = true;
return;
}
int restoredCount = 0;
int skippedCount = 0;
foreach (var item in queueData)
{
// 验证文件是否存在
if (!File.Exists(item.FilePath))
{
skippedCount++;
continue;
}
// 验证文件格式和大小
var fileExtension = Path.GetExtension(item.FilePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip")
{
skippedCount++;
continue;
}
try
{
var fileInfo = new FileInfo(item.FilePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
skippedCount++;
continue;
}
}
catch
{
skippedCount++;
continue;
}
// 恢复队列项
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = item.FilePath,
RetryCount = item.RetryCount
});
restoredCount++;
}
_isQueueInitialized = true;
if (restoredCount > 0)
{
LogHelper.WriteLogToFile($"已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
// 如果恢复了队列,触发处理
_ = ProcessUploadQueueAsync();
}
else if (skippedCount > 0)
{
LogHelper.WriteLogToFile($"队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
_isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试
}
}
/// <summary>
/// 保存队列到文件
/// </summary>
private static async Task SaveQueueToFileAsync()
{
if (!await _queueSaveLock.WaitAsync(1000)) // 最多等待1秒
{
return; // 如果无法获取锁,跳过保存(避免阻塞)
}
try
{
var queueData = new List<UploadQueueItemData>();
// 将队列转换为可序列化的格式
foreach (var item in _uploadQueue)
{
queueData.Add(new UploadQueueItemData
{
FilePath = item.FilePath,
RetryCount = item.RetryCount,
AddedTime = DateTime.Now
});
}
var queueFilePath = GetQueueFilePath();
// 如果队列为空,清空文件
if (queueData.Count == 0)
{
ClearQueueFile();
return;
}
var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented);
// 使用临时文件写入,然后替换,确保原子性
var tempFilePath = queueFilePath + ".tmp";
File.WriteAllText(tempFilePath, jsonContent);
// 如果原文件存在,先删除
if (File.Exists(queueFilePath))
{
File.Delete(queueFilePath);
}
// 重命名临时文件
File.Move(tempFilePath, queueFilePath);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
}
finally
{
_queueSaveLock.Release();
}
}
/// <summary>
/// 清空队列文件
/// </summary>
private static void ClearQueueFile()
{
try
{
var queueFilePath = GetQueueFilePath();
if (File.Exists(queueFilePath))
{
File.WriteAllText(queueFilePath, "[]");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 上传笔记响应模型
/// </summary>
public class UploadNoteResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("note_id")]
public int? NoteId { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("file_url")]
public string FileUrl { get; set; }
}
/// <summary>
/// 白板信息模型(用于查找白板)
/// </summary>
private class WhiteboardInfo
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("board_id")]
public string BoardId { get; set; }
[JsonProperty("secret_key")]
public string SecretKey { get; set; }
[JsonProperty("class_name")]
public string ClassName { get; set; }
[JsonProperty("class_id")]
public int ClassId { get; set; }
}
/// <summary>
/// 认证响应模型
/// </summary>
private class AuthWithTokenResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("whiteboards")]
public List<WhiteboardInfo> Whiteboards { get; set; }
}
/// <summary>
/// 异步上传笔记文件到Dlass(支持PNG、ICSTK和ZIP格式)
/// </summary>
/// <param name="filePath">文件路径(支持PNG、ICSTK和ZIP</param>
/// <returns>是否成功加入队列(不等待实际上传完成)</returns>
public static async Task<bool> UploadNoteFileAsync(string filePath)
{
try
{
// 检查是否启用自动上传
if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true)
{
return false;
}
// 基本验证
if (!File.Exists(filePath))
{
LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
return false;
}
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip")
{
return false;
}
var fileInfo = new FileInfo(filePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
return false;
}
// 获取上传延迟时间(分钟)
var delayMinutes = MainWindow.Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
// 如果设置了延迟时间,在后台任务中等待后再加入队列
if (delayMinutes > 0)
{
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
EnqueueFile(filePath);
});
}
else
{
EnqueueFile(filePath);
}
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 将文件加入上传队列
/// </summary>
private static void EnqueueFile(string filePath, int retryCount = 0)
{
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = filePath,
RetryCount = retryCount
});
// 异步保存队列到文件
_ = Task.Run(async () => await SaveQueueToFileAsync());
// 如果队列达到批量大小,触发批量上传
if (_uploadQueue.Count >= BATCH_SIZE)
{
_ = ProcessUploadQueueAsync();
}
}
/// <summary>
/// 处理上传队列,批量上传文件
/// </summary>
private static async Task ProcessUploadQueueAsync()
{
// 使用信号量防止并发处理
if (!await _queueProcessingLock.WaitAsync(0))
{
return; // 已有处理任务在运行
}
try
{
var filesToUpload = new List<UploadQueueItem>();
// 从队列中取出最多BATCH_SIZE个文件
while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
{
// 再次检查文件是否存在
if (File.Exists(item.FilePath))
{
filesToUpload.Add(item);
}
}
if (filesToUpload.Count == 0)
{
return;
}
// 获取共享的白板信息(同一批次的所有文件共享认证信息)
WhiteboardInfo sharedWhiteboard = null;
string apiBaseUrl = null;
string userToken = null;
try
{
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
userToken = MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
{
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
// 获取白板信息(只获取一次,所有文件共享)
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
sharedWhiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (sharedWhiteboard == null || string.IsNullOrEmpty(sharedWhiteboard.BoardId) || string.IsNullOrEmpty(sharedWhiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
// 并发上传所有文件(共享白板信息),并处理失败重试
var uploadTasks = filesToUpload.Select(async item =>
{
try
{
var success = await UploadFileInternalAsync(item.FilePath, sharedWhiteboard, apiBaseUrl, userToken);
if (!success)
{
// 检查是否是可重试的错误
if (IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1);
}
else
{
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
}
return success;
}
catch (Exception ex)
{
// 检查是否是可重试的错误(超时、网络错误等)
var errorMessage = ex.Message.ToLower();
bool isRetryable = errorMessage.Contains("超时") ||
errorMessage.Contains("timeout") ||
errorMessage.Contains("网络错误") ||
errorMessage.Contains("network");
if (isRetryable && IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1);
}
else
{
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
return false;
}
});
await Task.WhenAll(uploadTasks);
// 上传完成后保存队列状态
await SaveQueueToFileAsync();
// 如果队列达到批量大小,继续处理
if (_uploadQueue.Count >= BATCH_SIZE)
{
_ = ProcessUploadQueueAsync();
}
}
finally
{
_queueProcessingLock.Release();
}
}
/// <summary>
/// 内部上传方法,执行实际上传操作
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="whiteboard">白板信息(如果为null则重新获取)</param>
/// <param name="apiBaseUrl">API基础URL(如果为null则从设置获取)</param>
/// <param name="userToken">用户Token(如果为null则从设置获取)</param>
private static async Task<bool> UploadFileInternalAsync(string filePath, WhiteboardInfo whiteboard = null, string apiBaseUrl = null, string userToken = null)
{
try
{
// 再次检查文件是否存在(可能在队列等待时被删除)
if (!File.Exists(filePath))
{
return false;
}
// 检查文件扩展名
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip")
{
return false;
}
// 检查文件大小(最大10MB,ZIP文件可能更大,允许50MB)
var fileInfo = new FileInfo(filePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
return false;
}
// 如果白板信息未提供,则重新获取
if (whiteboard == null)
{
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
return false;
}
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
{
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
return false;
}
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
// 创建API客户端并获取白板信息
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
return false;
}
// 查找匹配班级的白板
whiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
return false;
}
}
}
// 获取API基础URL和用户Token(如果未提供)
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
// 准备上传参数
var fileName = Path.GetFileNameWithoutExtension(filePath);
var title = fileName;
string fileType;
string tags;
if (fileExtension == ".zip")
{
fileType = "多页面墨迹压缩包";
tags = "自动上传,多页面,zip,压缩包";
}
else if (fileExtension == ".icstk")
{
fileType = "墨迹文件";
tags = "自动上传,墨迹,icstk";
}
else
{
fileType = "笔记";
tags = "自动上传,笔记,png";
}
var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
// 创建API客户端并上传文件
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var uploadResult = await apiClient.UploadNoteAsync<UploadNoteResponse>(
"/api/whiteboard/upload_note",
filePath,
whiteboard.BoardId,
whiteboard.SecretKey,
title,
description,
tags);
if (uploadResult != null && uploadResult.Success)
{
LogHelper.WriteLogToFile($"笔记上传成功:{fileName} -> {uploadResult.FileUrl}", LogHelper.LogType.Event);
return true;
}
else
{
LogHelper.WriteLogToFile($"上传失败:服务器响应失败 - {uploadResult?.Message ?? ""}", LogHelper.LogType.Error);
return false;
}
}
}
catch (Exception ex)
{
// 记录错误信息,抛出异常以便调用方判断是否可重试
LogHelper.WriteLogToFile($"上传笔记时出错: {ex.Message}", LogHelper.LogType.Error);
throw;
}
}
/// <summary>
/// 判断错误是否可重试(超时、网络错误等)
/// </summary>
private static bool IsRetryableError(string filePath)
{
// 检查文件是否存在
if (!File.Exists(filePath))
{
return false; // 文件不存在,不可重试
}
// 检查文件扩展名
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip")
{
return false; // 文件格式错误,不可重试
}
// 检查文件大小
try
{
var fileInfo = new FileInfo(filePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
return false; // 文件过大,不可重试
}
}
catch
{
return false; // 无法读取文件信息,不可重试
}
// 其他错误(超时、网络错误等)可以重试
return true;
}
}
}
+11 -2
View File
@@ -78,6 +78,16 @@ namespace Ink_Canvas.Helpers
{
UnregisterHotkey(hotkeyName);
}
else
{
try
{
HotkeyManager.Current.Remove(hotkeyName);
}
catch
{
}
}
// 创建快捷键信息
var hotkeyInfo = new HotkeyInfo
@@ -112,9 +122,8 @@ namespace Ink_Canvas.Helpers
return true;
}
catch (Exception ex)
catch (Exception)
{
LogHelper.WriteLogToFile($"注册全局快捷键 {hotkeyName} 失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
-813
View File
@@ -1,813 +0,0 @@
using Microsoft.Office.Interop.PowerPoint;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Ink;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 多PPT墨迹管理器 - 支持多个PPT窗口分别管理墨迹
/// </summary>
public class MultiPPTInkManager : IDisposable
{
#region Properties
public bool IsAutoSaveEnabled { get; set; } = true;
public string AutoSaveLocation { get; set; } = "";
public PPTManager PPTManager { get; set; }
#endregion
#region Private Fields
private readonly Dictionary<string, PPTInkManager> _presentationManagers;
private readonly Dictionary<string, PresentationInfo> _presentationInfos;
private readonly object _lockObject = new object();
private bool _disposed;
private string _currentActivePresentationId = "";
// 墨迹备份机制
private readonly Dictionary<string, Dictionary<int, StrokeCollection>> _strokeBackups;
private DateTime _lastBackupTime = DateTime.MinValue;
private const int BackupIntervalMinutes = 2; // 每2分钟备份一次
#endregion
#region Constructor
public MultiPPTInkManager()
{
_presentationManagers = new Dictionary<string, PPTInkManager>();
_presentationInfos = new Dictionary<string, PresentationInfo>();
_strokeBackups = new Dictionary<string, Dictionary<int, StrokeCollection>>();
}
#endregion
#region Public Methods
/// <summary>
/// 初始化新的演示文稿
/// </summary>
public void InitializePresentation(Presentation presentation)
{
if (presentation == null) return;
lock (_lockObject)
{
try
{
var presentationId = GeneratePresentationId(presentation);
// 如果已存在该演示文稿的管理器,先清理
if (_presentationManagers.ContainsKey(presentationId))
{
_presentationManagers[presentationId].Dispose();
_presentationManagers.Remove(presentationId);
}
// 创建新的墨迹管理器
var inkManager = new PPTInkManager();
inkManager.IsAutoSaveEnabled = IsAutoSaveEnabled;
inkManager.AutoSaveLocation = AutoSaveLocation;
inkManager.InitializePresentation(presentation);
// 保存管理器和演示文稿信息
_presentationManagers[presentationId] = inkManager;
_presentationInfos[presentationId] = new PresentationInfo
{
Id = presentationId,
Name = presentation.Name,
FullName = presentation.FullName,
SlideCount = presentation.Slides.Count,
CreatedTime = DateTime.Now,
LastAccessTime = DateTime.Now
};
// 设置为当前活跃的演示文稿
_currentActivePresentationId = presentationId;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化多PPT墨迹管理失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 切换到指定的演示文稿
/// </summary>
public bool SwitchToPresentation(Presentation presentation)
{
if (presentation == null) return false;
lock (_lockObject)
{
try
{
var presentationId = GeneratePresentationId(presentation);
if (_presentationManagers.ContainsKey(presentationId))
{
// 如果切换的是不同的演示文稿,先保存当前活跃演示文稿的墨迹
if (!string.IsNullOrEmpty(_currentActivePresentationId) &&
_currentActivePresentationId != presentationId)
{
var currentManager = GetCurrentManager();
if (currentManager != null)
{
// 获取当前活跃的演示文稿并保存墨迹
var currentPresentation = GetCurrentActivePresentation();
if (currentPresentation != null)
{
try
{
currentManager.SaveAllStrokesToFile(currentPresentation);
LogHelper.WriteLogToFile($"已保存当前演示文稿墨迹: {currentPresentation.Name}", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存当前演示文稿墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
}
}
_currentActivePresentationId = presentationId;
// 更新最后访问时间
if (_presentationInfos.ContainsKey(presentationId))
{
_presentationInfos[presentationId].LastAccessTime = DateTime.Now;
}
if (_currentActivePresentationId != presentationId)
{
LogHelper.WriteLogToFile($"已切换到演示文稿: {presentation.Name}", LogHelper.LogType.Trace);
}
return true;
}
else
{
// 如果不存在,尝试初始化
InitializePresentation(presentation);
return true;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"切换到演示文稿失败: {ex}", LogHelper.LogType.Error);
return false;
}
}
}
/// <summary>
/// 保存当前页面的墨迹
/// </summary>
public void SaveCurrentSlideStrokes(int slideIndex, StrokeCollection strokes)
{
if (slideIndex <= 0 || strokes == null) return;
lock (_lockObject)
{
try
{
var manager = GetCurrentManager();
if (manager != null)
{
// 保存到管理器
manager.SaveCurrentSlideStrokes(slideIndex, strokes);
// 只有在保存成功后才创建备份
if (!string.IsNullOrEmpty(_currentActivePresentationId))
{
CreateStrokeBackup(_currentActivePresentationId, slideIndex, strokes);
}
// 检查是否需要执行定期备份
CheckAndPerformBackup();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存当前页面墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 强制保存指定页面的墨迹(忽略锁定状态)
/// </summary>
public void ForceSaveSlideStrokes(int slideIndex, StrokeCollection strokes)
{
if (slideIndex <= 0 || strokes == null) return;
lock (_lockObject)
{
try
{
var manager = GetCurrentManager();
if (manager != null)
{
manager.ForceSaveSlideStrokes(slideIndex, strokes);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"强制保存页面墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 加载指定页面的墨迹
/// </summary>
public StrokeCollection LoadSlideStrokes(int slideIndex)
{
if (slideIndex <= 0) return new StrokeCollection();
lock (_lockObject)
{
try
{
var manager = GetCurrentManager();
if (manager != null)
{
var strokes = manager.LoadSlideStrokes(slideIndex);
// 如果从管理器加载失败,尝试从备份恢复
if (strokes == null || strokes.Count == 0)
{
if (!string.IsNullOrEmpty(_currentActivePresentationId))
{
strokes = RestoreStrokeFromBackup(_currentActivePresentationId, slideIndex);
}
}
return strokes ?? new StrokeCollection();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载页面墨迹失败: {ex}", LogHelper.LogType.Error);
// 尝试从备份恢复
if (!string.IsNullOrEmpty(_currentActivePresentationId))
{
return RestoreStrokeFromBackup(_currentActivePresentationId, slideIndex);
}
}
}
return new StrokeCollection();
}
/// <summary>
/// 切换到指定页面并加载墨迹
/// </summary>
public StrokeCollection SwitchToSlide(int slideIndex, StrokeCollection currentStrokes = null)
{
lock (_lockObject)
{
try
{
var manager = GetCurrentManager();
if (manager != null)
{
return manager.SwitchToSlide(slideIndex, currentStrokes);
}
else
{
LogHelper.WriteLogToFile($"无法获取当前墨迹管理器,页面切换失败: {slideIndex}", LogHelper.LogType.Warning);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"切换页面墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
return new StrokeCollection();
}
/// <summary>
/// 保存所有墨迹到文件
/// </summary>
public void SaveAllStrokesToFile(Presentation presentation)
{
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return;
lock (_lockObject)
{
try
{
var presentationId = GeneratePresentationId(presentation);
if (_presentationManagers.ContainsKey(presentationId))
{
_presentationManagers[presentationId].SaveAllStrokesToFile(presentation);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存所有墨迹到文件失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 从文件加载已保存的墨迹
/// </summary>
public void LoadSavedStrokes(Presentation presentation)
{
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return;
lock (_lockObject)
{
try
{
var presentationId = GeneratePresentationId(presentation);
if (_presentationManagers.ContainsKey(presentationId))
{
_presentationManagers[presentationId].LoadSavedStrokes();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从文件加载墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 清除指定演示文稿的所有墨迹
/// </summary>
public void ClearPresentationStrokes(Presentation presentation)
{
if (presentation == null) return;
lock (_lockObject)
{
try
{
var presentationId = GeneratePresentationId(presentation);
if (_presentationManagers.ContainsKey(presentationId))
{
_presentationManagers[presentationId].ClearAllStrokes();
LogHelper.WriteLogToFile($"已清除演示文稿墨迹: {presentation.Name}", LogHelper.LogType.Trace);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清除演示文稿墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 清除所有演示文稿的墨迹
/// </summary>
public void ClearAllStrokes()
{
lock (_lockObject)
{
try
{
foreach (var manager in _presentationManagers.Values)
{
manager?.ClearAllStrokes();
}
LogHelper.WriteLogToFile("已清除所有演示文稿墨迹", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清除所有墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 翻页后锁定墨迹写入
/// </summary>
public void LockInkForSlide(int slideIndex)
{
lock (_lockObject)
{
try
{
var manager = GetCurrentManager();
if (manager != null)
{
manager.LockInkForSlide(slideIndex);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"锁定墨迹写入失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 检查是否可以写入墨迹
/// </summary>
public bool CanWriteInk(int currentSlideIndex)
{
lock (_lockObject)
{
try
{
var manager = GetCurrentManager();
if (manager != null)
{
return manager.CanWriteInk(currentSlideIndex);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"检查墨迹写入权限失败: {ex}", LogHelper.LogType.Error);
}
}
return false;
}
/// <summary>
/// 重置当前演示文稿的墨迹锁定状态
/// </summary>
public void ResetCurrentPresentationLockState()
{
lock (_lockObject)
{
try
{
var manager = GetCurrentManager();
if (manager != null)
{
manager.ResetLockState();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"重置墨迹锁定状态失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 移除演示文稿管理器
/// </summary>
public void RemovePresentation(Presentation presentation)
{
if (presentation == null) return;
lock (_lockObject)
{
try
{
var presentationId = GeneratePresentationId(presentation);
if (_presentationManagers.ContainsKey(presentationId))
{
// 保存墨迹到文件
_presentationManagers[presentationId].SaveAllStrokesToFile(presentation);
// 释放资源
_presentationManagers[presentationId].Dispose();
_presentationManagers.Remove(presentationId);
}
if (_presentationInfos.ContainsKey(presentationId))
{
_presentationInfos.Remove(presentationId);
}
// 如果移除的是当前活跃的演示文稿,重置活跃ID
if (_currentActivePresentationId == presentationId)
{
_currentActivePresentationId = "";
}
}
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706BA || hr == 0x800706BE || hr == 0x80048010)
{
}
}
catch (Exception)
{
}
}
}
/// <summary>
/// 获取当前管理的演示文稿数量
/// </summary>
public int GetPresentationCount()
{
lock (_lockObject)
{
return _presentationManagers.Count;
}
}
/// <summary>
/// 获取所有演示文稿信息
/// </summary>
public List<PresentationInfo> GetAllPresentationInfos()
{
lock (_lockObject)
{
return _presentationInfos.Values.ToList();
}
}
/// <summary>
/// 清理长时间未访问的演示文稿管理器
/// </summary>
public void CleanupInactivePresentations(TimeSpan inactiveThreshold)
{
lock (_lockObject)
{
try
{
var inactiveIds = new List<string>();
var cutoffTime = DateTime.Now - inactiveThreshold;
foreach (var info in _presentationInfos.Values)
{
if (info.LastAccessTime < cutoffTime && info.Id != _currentActivePresentationId)
{
inactiveIds.Add(info.Id);
}
}
foreach (var id in inactiveIds)
{
if (_presentationManagers.ContainsKey(id))
{
_presentationManagers[id].Dispose();
_presentationManagers.Remove(id);
}
_presentationInfos.Remove(id);
// 清理备份数据
if (_strokeBackups.ContainsKey(id))
{
_strokeBackups.Remove(id);
}
LogHelper.WriteLogToFile($"已清理非活跃演示文稿: {id}", LogHelper.LogType.Trace);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理非活跃演示文稿失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 创建墨迹备份
/// </summary>
private void CreateStrokeBackup(string presentationId, int slideIndex, StrokeCollection strokes)
{
try
{
if (strokes == null || strokes.Count == 0) return;
if (!_strokeBackups.ContainsKey(presentationId))
{
_strokeBackups[presentationId] = new Dictionary<int, StrokeCollection>();
}
// 释放旧的备份
if (_strokeBackups[presentationId].ContainsKey(slideIndex))
{
_strokeBackups[presentationId][slideIndex] = null;
}
// 创建新的备份
_strokeBackups[presentationId][slideIndex] = strokes.Clone();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建墨迹备份失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 从备份恢复墨迹
/// </summary>
private StrokeCollection RestoreStrokeFromBackup(string presentationId, int slideIndex)
{
try
{
if (_strokeBackups.ContainsKey(presentationId) &&
_strokeBackups[presentationId].ContainsKey(slideIndex))
{
var backup = _strokeBackups[presentationId][slideIndex];
if (backup != null)
{
LogHelper.WriteLogToFile($"从备份恢复第{slideIndex}页墨迹", LogHelper.LogType.Trace);
return backup.Clone();
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从备份恢复墨迹失败: {ex}", LogHelper.LogType.Error);
}
return new StrokeCollection();
}
/// <summary>
/// 检查并执行定期备份
/// </summary>
private void CheckAndPerformBackup()
{
try
{
var now = DateTime.Now;
// 检查是否需要执行备份
if (now - _lastBackupTime < TimeSpan.FromMinutes(BackupIntervalMinutes))
{
return;
}
// 备份当前活跃演示文稿的所有墨迹
if (!string.IsNullOrEmpty(_currentActivePresentationId) &&
_presentationManagers.ContainsKey(_currentActivePresentationId))
{
var manager = _presentationManagers[_currentActivePresentationId];
if (manager != null)
{
// 这里可以添加更详细的备份逻辑
}
}
_lastBackupTime = now;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"定期备份检查失败: {ex}", LogHelper.LogType.Error);
}
}
#endregion
#region Private Methods
private PPTInkManager GetCurrentManager()
{
if (string.IsNullOrEmpty(_currentActivePresentationId) ||
!_presentationManagers.ContainsKey(_currentActivePresentationId))
{
return null;
}
return _presentationManagers[_currentActivePresentationId];
}
private Presentation GetCurrentActivePresentation()
{
try
{
// 通过PPTManager获取当前活跃的演示文稿
return PPTManager?.GetCurrentActivePresentation();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取当前活跃演示文稿失败: {ex}", LogHelper.LogType.Error);
return null;
}
}
private string GeneratePresentationId(Presentation presentation)
{
try
{
// 检查COM对象是否仍然有效
if (presentation == null)
{
return $"invalid_{DateTime.Now.Ticks}";
}
var presentationPath = presentation.FullName;
var fileHash = GetFileHash(presentationPath);
var processId = GetProcessId(presentation);
return $"{presentation.Name}_{presentation.Slides.Count}_{fileHash}_{processId}";
}
catch (COMException comEx)
{
var hr = (uint)comEx.HResult;
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706BA || hr == 0x800706BE || hr == 0x80048010)
{
return $"disconnected_{DateTime.Now.Ticks}";
}
return $"error_{DateTime.Now.Ticks}";
}
catch (Exception)
{
return $"unknown_{DateTime.Now.Ticks}";
}
}
private string GetFileHash(string filePath)
{
try
{
if (string.IsNullOrEmpty(filePath)) return "unknown";
using (var md5 = MD5.Create())
{
byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(filePath));
return BitConverter.ToString(hashBytes).Replace("-", "").Substring(0, 8);
}
}
catch (Exception)
{
// 所有异常都静默处理,避免日志噪音
return "error";
}
}
private string GetProcessId(Presentation presentation)
{
try
{
// 尝试获取PowerPoint应用程序的进程ID
if (presentation.Application != null)
{
// 通过COM对象获取进程信息
var hwnd = presentation.Application.HWND;
if (hwnd != 0)
{
return hwnd.ToString();
}
}
return "unknown";
}
catch (COMException comEx)
{
// COM对象已失效,这是正常情况,完全静默处理
var hr = (uint)comEx.HResult;
if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706BA || hr == 0x800706BE || hr == 0x80048010)
{
return "disconnected";
}
return "error";
}
catch (Exception)
{
return "error";
}
}
#endregion
#region Dispose
public void Dispose()
{
if (!_disposed)
{
lock (_lockObject)
{
// 释放所有管理器
foreach (var manager in _presentationManagers.Values)
{
manager?.Dispose();
}
_presentationManagers.Clear();
_presentationInfos.Clear();
// 清理备份数据
foreach (var backupDict in _strokeBackups.Values)
{
foreach (var backup in backupDict.Values)
{
backup?.Clear();
}
backupDict.Clear();
}
_strokeBackups.Clear();
}
_disposed = true;
}
}
#endregion
}
/// <summary>
/// 演示文稿信息
/// </summary>
public class PresentationInfo
{
public string Id { get; set; }
public string Name { get; set; }
public string FullName { get; set; }
public int SlideCount { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime LastAccessTime { get; set; }
}
}
+15 -2
View File
@@ -273,7 +273,9 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 保存所有墨迹到文件
/// </summary>
public void SaveAllStrokesToFile(Presentation presentation)
/// <param name="presentation">演示文稿对象</param>
/// <param name="currentSlideIndex">当前播放的页码,如果提供则使用此值保存位置,否则使用_lockedSlideIndex</param>
public void SaveAllStrokesToFile(Presentation presentation, int currentSlideIndex = -1)
{
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return;
@@ -290,7 +292,18 @@ namespace Ink_Canvas.Helpers
// 保存位置信息
try
{
File.WriteAllText(Path.Combine(folderPath, "Position"), _lockedSlideIndex.ToString());
// 优先使用传入的当前页码,否则使用锁定的页码
int positionToSave = currentSlideIndex > 0 ? currentSlideIndex : _lockedSlideIndex;
// 如果都没有有效值,尝试使用最后切换的页码
if (positionToSave <= 0 && _lastSwitchSlideIndex > 0)
{
positionToSave = _lastSwitchSlideIndex;
}
if (positionToSave > 0)
{
File.WriteAllText(Path.Combine(folderPath, "Position"), positionToSave.ToString());
}
}
catch (Exception ex)
{
+1
View File
@@ -421,6 +421,7 @@
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\new-icons\gesture.png" />
<Resource Include="Resources\new-icons\gesture_white.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\new-icons\gesture-enabled.png" />
+144 -13
View File
@@ -6,11 +6,12 @@
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
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:Windows="clr-namespace:Ink_Canvas.Windows"
mc:Ignorable="d"
AllowsTransparency="True"
WindowStyle="None"
ResizeMode="NoResize"
WindowState="Maximized"
Loaded="Window_Loaded"
Background="Transparent"
ShowInTaskbar="False"
@@ -603,6 +604,13 @@
IsOn="False" FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchNoFocusMode_Toggled" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="窗口无边框模式" VerticalAlignment="Center"
FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent="" Name="ToggleSwitchWindowMode"
IsOn="True" FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchWindowMode_Toggled" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="窗口置顶" VerticalAlignment="Center"
FontSize="14" Margin="0,0,16,0" />
@@ -2824,17 +2832,11 @@
TextWrapping="Wrap" Foreground="#a1a1aa" Margin="0,0,0,8" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,0,0,8">
<Button Name="BtnUnregisterFileAssociation" Content="取消文件关联"
Padding="12,6" Margin="0,0,8,0" Click="BtnUnregisterFileAssociation_Click"
Background="#FFDC3545" Foreground="White" BorderBrush="#FFBD2130"
FontFamily="Microsoft YaHei UI" FontSize="12" />
Padding="15,5" Margin="0,0,8,0" Click="BtnUnregisterFileAssociation_Click" />
<Button Name="BtnCheckFileAssociation" Content="检查关联状态"
Padding="12,6" Margin="0,0,8,0" Click="BtnCheckFileAssociation_Click"
Background="#FF17A2B8" Foreground="White" BorderBrush="#FF138496"
FontFamily="Microsoft YaHei UI" FontSize="12" />
Padding="15,5" Margin="0,0,8,0" Click="BtnCheckFileAssociation_Click" />
<Button Name="BtnRegisterFileAssociation" Content="重新注册关联"
Padding="12,6" Click="BtnRegisterFileAssociation_Click"
Background="#FF28A745" Foreground="White" BorderBrush="#FF1E7E34"
FontFamily="Microsoft YaHei UI" FontSize="12" />
Padding="15,5" Click="BtnRegisterFileAssociation_Click" />
</ui:SimpleStackPanel>
<TextBlock Name="TextBlockFileAssociationStatus" Text=""
Foreground="#a1a1aa" FontSize="12" Margin="0,4,0,0" />
@@ -3083,6 +3085,30 @@
Toggled="ToggleSwitchAutoSaveStrokesAtScreenshot_Toggled" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="定时自动保存墨迹" VerticalAlignment="Center"
FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent=""
Name="ToggleSwitchEnableAutoSaveStrokes" IsOn="False"
FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchEnableAutoSaveStrokes_Toggled" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#a1a1aa" Text="保存间隔" VerticalAlignment="Center"
FontSize="12" Margin="0,0,16,0" />
<ComboBox Name="ComboBoxAutoSaveStrokesInterval" Width="120"
SelectionChanged="ComboBoxAutoSaveStrokesInterval_SelectionChanged">
<ComboBoxItem Content="1分钟" Tag="1" />
<ComboBoxItem Content="3分钟" Tag="3" />
<ComboBoxItem Content="5分钟" Tag="5" />
<ComboBoxItem Content="10分钟" Tag="10" />
<ComboBoxItem Content="15分钟" Tag="15" />
<ComboBoxItem Content="30分钟" Tag="30" />
<ComboBoxItem Content="60分钟" Tag="60" />
</ComboBox>
</ui:SimpleStackPanel>
<TextBlock Text="# 开启后将在设定时间间隔自动保存墨迹,仅在画布可见且有墨迹时才会保存" TextWrapping="Wrap" Foreground="#a1a1aa" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="墨迹全页面保存" VerticalAlignment="Center"
FontSize="14" Margin="0,0,16,0" />
@@ -3167,6 +3193,17 @@
FontSize="14" Margin="8,0,0,0" />
</ui:SimpleStackPanel>
<Line HorizontalAlignment="Center" X1="0" Y1="0" X2="400" Y2="0" Stroke="#3f3f46"
StrokeThickness="1" Margin="0,8,0,8" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0">
<Button Name="BtnDlassSettingsManage" Content="Dlass设置管理"
HorizontalAlignment="Left"
Click="BtnDlassSettingsManage_Click"
Padding="15,5"
Margin="0,0,0,0"/>
</ui:SimpleStackPanel>
<Line HorizontalAlignment="Center" X1="0" Y1="0" X2="400" Y2="0" Stroke="#3f3f46"
StrokeThickness="1" Margin="0,8,0,8" />
@@ -3183,14 +3220,14 @@
</ui:SimpleStackPanel>
<TextBlock Text="# 开启后,退出收纳模式时将自动切换至批注模式,便于快速批注" TextWrapping="Wrap" Foreground="#a1a1aa" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="退出PPT放映后自动恢复浮动栏状态"
<TextBlock Foreground="#fafafa" Text="退出PPT放映后自动收纳浮动栏"
VerticalAlignment="Center" FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent=""
Name="ToggleSwitchAutoFoldAfterPPTSlideShow"
IsOn="False" FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchAutoFoldAfterPPTSlideShow_Toggled" />
</ui:SimpleStackPanel>
<TextBlock Text="# 开启后,如果进入PPT放映前为收纳模式则退出后也为收纳模式,如果进入前不是收纳模式则退出后也不是收纳模式" TextWrapping="Wrap" Foreground="#a1a1aa" />
<TextBlock Text="# 开启后,退出PPT放映后会自动收纳浮动栏" TextWrapping="Wrap" Foreground="#a1a1aa" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="退出白板时自动收纳"
VerticalAlignment="Center" FontSize="14" Margin="0,0,16,0" />
@@ -3249,6 +3286,15 @@
FontWeight="Bold"
Toggled="ToggleSwitchShowRandomAndSingleDraw_Toggled" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="启用快抽悬浮按钮"
VerticalAlignment="Center" FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent=""
Name="ToggleSwitchEnableQuickDraw"
IsOn="True" FontFamily="Microsoft YaHei UI"
FontWeight="Bold"
Toggled="ToggleSwitchEnableQuickDraw_Toggled" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="直接调用外部点名"
VerticalAlignment="Center" FontSize="14" Margin="0,0,16,0" />
@@ -3299,6 +3345,62 @@
FontFamily="Consolas"
Text="{Binding ElementName=RandWindowOnceMaxStudentsSlider, Path=Value, Converter={StaticResource IntNumberToString}}" />
</ui:SimpleStackPanel>
<!-- 新点名UI设置 -->
<Line HorizontalAlignment="Center" X1="0" Y1="0" X2="400" Y2="0"
Stroke="#3f3f46" StrokeThickness="1" Margin="0,4,0,4" />
<TextBlock Foreground="#fafafa" Text="新点名UI设置"
FontSize="16" FontWeight="Bold" Margin="0,10,0,5" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="启用新点名UI"
VerticalAlignment="Center" FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent=""
Name="ToggleSwitchUseNewRollCallUI"
IsOn="True" FontFamily="Microsoft YaHei UI"
FontWeight="Bold"
Toggled="ToggleSwitchUseNewRollCallUI_Toggled" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="启用机器学习避免重复"
VerticalAlignment="Center" FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent=""
Name="ToggleSwitchEnableMLAvoidance"
IsOn="True" FontFamily="Microsoft YaHei UI"
FontWeight="Bold"
Toggled="ToggleSwitchEnableMLAvoidance_Toggled" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="避免重复历史记录数量" VerticalAlignment="Center"
FontSize="14" Margin="0,0,16,0" />
<Slider x:Name="MLAvoidanceHistorySlider" Minimum="5"
Maximum="50" Width="168" FontFamily="Microsoft YaHei UI"
ValueChanged="MLAvoidanceHistorySlider_ValueChanged"
FontSize="20" IsSnapToTickEnabled="True" Value="50" TickFrequency="5"
TickPlacement="None" AutoToolTipPlacement="None" />
<TextBlock VerticalAlignment="Center" Margin="12,0,16,0" FontSize="14"
FontFamily="Consolas"
Text="{Binding ElementName=MLAvoidanceHistorySlider, Path=Value, Converter={StaticResource IntNumberToString}}" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="避免重复权重" VerticalAlignment="Center"
FontSize="14" Margin="0,0,16,0" />
<Slider x:Name="MLAvoidanceWeightSlider" Minimum="0.1"
Maximum="1.0" Width="168" FontFamily="Microsoft YaHei UI"
ValueChanged="MLAvoidanceWeightSlider_ValueChanged"
FontSize="20" IsSnapToTickEnabled="True" Value="1.0" TickFrequency="0.1"
TickPlacement="None" AutoToolTipPlacement="None" />
<TextBlock VerticalAlignment="Center" Margin="12,0,16,0" FontSize="14"
FontFamily="Consolas"
Text="{Binding ElementName=MLAvoidanceWeightSlider, Path=Value, StringFormat={}{0:P0}}" />
</ui:SimpleStackPanel>
<TextBlock Text="# 机器学习算法会分析最近的点名历史,智能避免重复选择相同人员"
TextWrapping="Wrap" Foreground="#a1a1aa" Margin="0,5,0,0" />
<Line HorizontalAlignment="Center" X1="0" Y1="0" X2="400" Y2="0"
Stroke="#3f3f46" StrokeThickness="1" Margin="0,4,0,4" />
<TextBlock Foreground="#fafafa" Text="计时器设置"
@@ -3862,6 +3964,14 @@
</Image.RenderTransform>
</Image>
</Canvas>
<!-- 快抽悬浮按钮 -->
<controls:QuickDrawFloatingButtonControl x:Name="QuickDrawFloatingButton"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Margin="0,0,0,200"
Visibility="Collapsed"
Panel.ZIndex="1001"/>
</Grid>
<Canvas IsHitTestVisible="False">
@@ -9369,7 +9479,7 @@
MouseUp="TwoFingerGestureBorder_MouseUp" Background="Transparent"
Orientation="Vertical" HorizontalAlignment="Center" Width="36" Margin="0">
<Image x:Name="EnableTwoFingerGestureBtn" Source="/Resources/new-icons/gesture.png"
<Image x:Name="EnableTwoFingerGestureBtn" Source="{DynamicResource GestureIcon}"
RenderOptions.BitmapScalingMode="HighQuality" Width="28" Height="18"
Margin="0,3,0,0" />
<TextBlock x:Name="gestureiconText" Text="手势" Foreground="{DynamicResource FloatBarForeground}" FontSize="8"
@@ -9847,6 +9957,27 @@
</ui:SimpleStackPanel>
</Border>
</Viewbox>
<Border x:Name="TimerContainer"
Visibility="Collapsed"
Panel.ZIndex="998"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="900"
Height="500">
<Windows:TimerControl x:Name="TimerControl"/>
</Border>
<Border x:Name="MinimizedTimerContainer"
Visibility="Collapsed"
Panel.ZIndex="997"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="20,20,0,0"
Width="600"
Height="200">
<Windows:MinimizedTimerControl x:Name="MinimizedTimerControl"/>
</Border>
</Grid>
</Window>
+287 -47
View File
@@ -28,6 +28,9 @@ using Application = System.Windows.Application;
using Brushes = System.Windows.Media.Brushes;
using Button = System.Windows.Controls.Button;
using Cursor = System.Windows.Input.Cursor;
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
using HorizontalAlignment = System.Windows.HorizontalAlignment;
using VerticalAlignment = System.Windows.VerticalAlignment;
using Cursors = System.Windows.Input.Cursors;
using DpiChangedEventArgs = System.Windows.DpiChangedEventArgs;
using File = System.IO.File;
@@ -60,6 +63,7 @@ namespace Ink_Canvas
// 悬浮窗拦截管理器
private FloatingWindowInterceptorManager _floatingWindowInterceptorManager;
// 设置面板相关状态
private bool userChangedNoFocusModeInSettings;
private bool isTemporarilyDisablingNoFocusMode = false;
@@ -264,6 +268,74 @@ namespace Ink_Canvas
// 为滑块控件添加触摸事件支持
AddTouchSupportToSliders();
// 初始化计时器控件事件
Dispatcher.BeginInvoke(new Action(() =>
{
if (TimerControl != null)
{
TimerControl.ShowMinimizedRequested += TimerControl_ShowMinimizedRequested;
TimerControl.HideMinimizedRequested += TimerControl_HideMinimizedRequested;
}
if (MinimizedTimerControl != null && TimerControl != null)
{
MinimizedTimerControl.SetParentControl(TimerControl);
}
}), DispatcherPriority.Loaded);
}
private void TimerControl_ShowMinimizedRequested(object sender, EventArgs e)
{
var timerContainer = FindName("TimerContainer") as FrameworkElement;
var minimizedContainer = FindName("MinimizedTimerContainer") as FrameworkElement;
if (timerContainer != null && minimizedContainer != null)
{
double x = 0, y = 0;
if (timerContainer.HorizontalAlignment == HorizontalAlignment.Center &&
timerContainer.VerticalAlignment == VerticalAlignment.Center)
{
var timerPoint = timerContainer.TransformToAncestor(this).Transform(new Point(0, 0));
x = timerPoint.X;
y = timerPoint.Y;
}
else
{
var timerMargin = timerContainer.Margin;
x = double.IsNaN(timerMargin.Left) ? 0 : timerMargin.Left;
y = double.IsNaN(timerMargin.Top) ? 0 : timerMargin.Top;
}
minimizedContainer.Margin = new Thickness(x, y, 0, 0);
minimizedContainer.HorizontalAlignment = HorizontalAlignment.Left;
minimizedContainer.VerticalAlignment = VerticalAlignment.Top;
timerContainer.Margin = new Thickness(x, y, 0, 0);
timerContainer.HorizontalAlignment = HorizontalAlignment.Left;
timerContainer.VerticalAlignment = VerticalAlignment.Top;
timerContainer.Visibility = Visibility.Collapsed;
minimizedContainer.Visibility = Visibility.Visible;
}
}
private void TimerControl_HideMinimizedRequested(object sender, EventArgs e)
{
var timerContainer = FindName("TimerContainer") as FrameworkElement;
var minimizedContainer = FindName("MinimizedTimerContainer") as FrameworkElement;
if (timerContainer != null && minimizedContainer != null)
{
minimizedContainer.Visibility = Visibility.Collapsed;
timerContainer.Visibility = Visibility.Visible;
if (TimerControl != null)
{
TimerControl.UpdateActivityTime();
}
}
}
@@ -321,13 +393,11 @@ namespace Ink_Canvas
{
if (gest.ApplicationGesture == ApplicationGesture.Left)
{
// 直接发送翻页请求到PPT放映软件
SendKeyToPPTSlideShow(false); // 下一页
BtnPPTSlidesDown_Click(null, null); // 下一页
}
if (gest.ApplicationGesture == ApplicationGesture.Right)
{
// 直接发送翻页请求到PPT放映软件
SendKeyToPPTSlideShow(true); // 上一页
BtnPPTSlidesUp_Click(null, null); // 上一页
}
}
}
@@ -400,6 +470,9 @@ namespace Ink_Canvas
LoadSettings(true);
AutoBackupManager.Initialize(Settings);
// 初始化Dlass上传队列(恢复上次的上传队列)
DlassNoteUploader.InitializeQueue();
// 检查保存路径是否可用,不可用则修正
try
{
@@ -441,6 +514,9 @@ namespace Ink_Canvas
// 加载自定义背景颜色
LoadCustomBackgroundColor();
// 设置窗口模式
SetWindowMode();
// 注册设置面板滚动事件
if (SettingsPanelScrollViewer != null)
{
@@ -530,6 +606,9 @@ namespace Ink_Canvas
else
RadioCrashNoAction.IsChecked = true;
// 显示快抽悬浮按钮
ShowQuickDrawFloatingButton();
// 如果当前不是黑板模式,则切换到黑板模式
if (currentMode == 0)
{
@@ -610,6 +689,34 @@ namespace Ink_Canvas
}
}), DispatcherPriority.Loaded);
}
// 初始化计时器控件关联
Dispatcher.BeginInvoke(new Action(() =>
{
if (TimerControl != null && MinimizedTimerControl != null)
{
MinimizedTimerControl.SetParentControl(TimerControl);
TimerControl.ShowMinimizedRequested += (s, args) =>
{
if (TimerContainer != null && MinimizedTimerContainer != null && MinimizedTimerControl != null)
{
TimerContainer.Visibility = Visibility.Collapsed;
MinimizedTimerContainer.Visibility = Visibility.Visible;
MinimizedTimerControl.Visibility = Visibility.Visible;
}
};
TimerControl.HideMinimizedRequested += (s, args) =>
{
if (MinimizedTimerContainer != null && MinimizedTimerControl != null)
{
MinimizedTimerContainer.Visibility = Visibility.Collapsed;
MinimizedTimerControl.Visibility = Visibility.Collapsed;
}
};
}
}), DispatcherPriority.Loaded);
}
private void SystemEventsOnDisplaySettingsChanged(object sender, EventArgs e)
@@ -665,9 +772,34 @@ namespace Ink_Canvas
}
}
private void SetWindowMode()
{
if (Settings.Advanced.WindowMode)
{
WindowState = WindowState.Normal;
Left = 0.0;
Top = 0.0;
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
}
else // 全屏
{
WindowState = WindowState.Maximized;
}
}
private void Window_Closing(object sender, CancelEventArgs e)
{
LogHelper.WriteLogToFile("Ink Canvas closing", LogHelper.LogType.Event);
try
{
// 快抽按钮现在集成在主窗口中,不需要单独关闭
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"关闭快抽悬浮按钮时出错: {ex.Message}", LogHelper.LogType.Error);
}
if (!CloseIsFromButton && Settings.Advanced.IsSecondConfirmWhenShutdownApp)
{
// 第一个确认对话框
@@ -763,6 +895,8 @@ namespace Ink_Canvas
// 停止置顶维护定时器
StopTopmostMaintenance();
UninstallKeyboardHook();
// 从Z-Order管理器中移除主窗口
WindowZOrderManager.UnregisterWindow(this);
@@ -1819,7 +1953,40 @@ namespace Ink_Canvas
private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentProcessId();
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private const int WM_SYSKEYDOWN = 0x0104;
private const int WM_SYSKEYUP = 0x0105;
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
private struct KBDLLHOOKSTRUCT
{
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
private LowLevelKeyboardProc _keyboardProc;
private IntPtr _keyboardHookId = IntPtr.Zero;
private const int GWL_EXSTYLE = -20;
private const int WS_EX_NOACTIVATE = 0x08000000;
@@ -1834,8 +2001,70 @@ namespace Ink_Canvas
// 添加定时器来维护置顶状态
private DispatcherTimer topmostMaintenanceTimer;
private DispatcherTimer autoSaveStrokesTimer;
private bool isTopmostMaintenanceEnabled;
private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
if (Settings.Advanced.IsNoFocusMode &&
BtnPPTSlideShowEnd.Visibility == Visibility.Visible &&
currentMode == 0)
{
KBDLLHOOKSTRUCT hookStruct = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
uint vkCode = hookStruct.vkCode;
if (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN)
{
if (vkCode == 0x22 || vkCode == 0x28 || vkCode == 0x27 ||
vkCode == 0x4E || vkCode == 0x20)
{
Dispatcher.BeginInvoke(new Action(() =>
{
BtnPPTSlidesDown_Click(null, null);
}), DispatcherPriority.Normal);
return (IntPtr)1;
}
else if (vkCode == 0x21 || vkCode == 0x26 || vkCode == 0x25 ||
vkCode == 0x50)
{
Dispatcher.BeginInvoke(new Action(() =>
{
BtnPPTSlidesUp_Click(null, null);
}), DispatcherPriority.Normal);
return (IntPtr)1;
}
}
}
}
return CallNextHookEx(_keyboardHookId, nCode, wParam, lParam);
}
private void InstallKeyboardHook()
{
if (_keyboardHookId == IntPtr.Zero)
{
_keyboardProc = KeyboardHookProc;
_keyboardHookId = SetWindowsHookEx(WH_KEYBOARD_LL, _keyboardProc,
GetModuleHandle(null), 0);
if (_keyboardHookId == IntPtr.Zero)
{
LogHelper.WriteLogToFile("安装低级键盘钩子失败", LogHelper.LogType.Error);
}
}
}
private void UninstallKeyboardHook()
{
if (_keyboardHookId != IntPtr.Zero)
{
UnhookWindowsHookEx(_keyboardHookId);
_keyboardHookId = IntPtr.Zero;
_keyboardProc = null;
}
}
private void ApplyNoFocusMode()
{
var hwnd = new WindowInteropHelper(this).Handle;
@@ -1847,10 +2076,12 @@ namespace Ink_Canvas
if (shouldBeNoFocus)
{
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE);
InstallKeyboardHook();
}
else
{
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_NOACTIVATE);
UninstallKeyboardHook();
}
}
@@ -1941,6 +2172,28 @@ namespace Ink_Canvas
}
}
public void PauseTopmostMaintenance()
{
if (topmostMaintenanceTimer != null && isTopmostMaintenanceEnabled)
{
topmostMaintenanceTimer.Stop();
}
}
public void ResumeTopmostMaintenance()
{
if (Settings.Advanced.IsAlwaysOnTop &&
Settings.Advanced.IsNoFocusMode &&
!Settings.Advanced.EnableUIAccessTopMost)
{
if (topmostMaintenanceTimer != null && !isTopmostMaintenanceEnabled)
{
topmostMaintenanceTimer.Start();
isTopmostMaintenanceEnabled = true;
}
}
}
/// <summary>
/// 置顶维护定时器事件
/// </summary>
@@ -2329,47 +2582,6 @@ namespace Ink_Canvas
}
#endregion
#region PPT翻页直接传递
/// <summary>
/// 直接发送翻页请求到PPT放映软件,让PPT软件处理翻页
/// </summary>
/// <param name="isPrevious">是否为上一页</param>
private void SendKeyToPPTSlideShow(bool isPrevious)
{
try
{
// 查找PPT放映窗口并发送按键
var pptWindows = Process.GetProcessesByName("POWERPNT");
var wpsWindows = Process.GetProcessesByName("wpp");
foreach (var process in pptWindows.Concat(wpsWindows))
{
if (process.MainWindowHandle != IntPtr.Zero)
{
// 激活PPT窗口
SetForegroundWindow(process.MainWindowHandle);
// 发送翻页按键消息
int keyCode = isPrevious ? 0x21 : 0x22; // VK_PRIOR : VK_NEXT
// 发送按键按下和释放消息
PostMessage(process.MainWindowHandle, 0x0100, (IntPtr)keyCode, IntPtr.Zero); // WM_KEYDOWN
PostMessage(process.MainWindowHandle, 0x0101, (IntPtr)keyCode, IntPtr.Zero); // WM_KEYUP
break;
}
}
}
catch (Exception)
{
// 如果直接发送失败,回退到原来的方法
if (isPrevious)
BtnPPTSlidesUp_Click(BtnPPTSlidesUp, null);
else
BtnPPTSlidesDown_Click(BtnPPTSlidesDown, null);
}
}
#endregion
/// <summary>
/// 初始化文件关联状态显示
@@ -2509,7 +2721,9 @@ namespace Ink_Canvas
BoardHighlighterWidthSlider,
InkWidthSlider,
InkAlphaSlider,
HighlighterWidthSlider
HighlighterWidthSlider,
MLAvoidanceHistorySlider,
MLAvoidanceWeightSlider
};
foreach (var slider in sliders)
@@ -2808,7 +3022,6 @@ namespace Ink_Canvas
else if (!isInSlideShow && IsVisible)
{
Hide();
LogHelper.WriteLogToFile("PPT放映结束,隐藏主窗口(仅PPT模式)", LogHelper.LogType.Trace);
}
}
else
@@ -3053,6 +3266,33 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 显示快抽悬浮按钮
/// </summary>
private void ShowQuickDrawFloatingButton()
{
try
{
var quickDrawButton = FindName("QuickDrawFloatingButton") as Controls.QuickDrawFloatingButtonControl;
if (quickDrawButton == null) return;
// 检查设置是否启用快抽功能
if (Settings?.RandSettings?.EnableQuickDraw == true)
{
quickDrawButton.Visibility = Visibility.Visible;
}
else
{
quickDrawButton.Visibility = Visibility.Collapsed;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"显示快抽悬浮按钮失败: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
}
}
+32
View File
@@ -65,6 +65,9 @@ namespace Ink_Canvas
// 刷新图片选中栏图标
RefreshImageSelectionIcons();
// 刷新手势按钮图标
RefreshGestureButtonIcon();
RefreshFloatingBarHighlightColors();
if (autoSwitchIcon)
@@ -109,6 +112,9 @@ namespace Ink_Canvas
// 刷新图片选中栏图标
RefreshImageSelectionIcons();
// 刷新手势按钮图标
RefreshGestureButtonIcon();
RefreshFloatingBarHighlightColors();
if (autoSwitchIcon)
@@ -492,6 +498,21 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 刷新手势按钮图标
/// </summary>
private void RefreshGestureButtonIcon()
{
try
{
// 调用手势按钮颜色和图标更新方法,该方法会根据当前主题和手势状态设置正确的图标
CheckEnableTwoFingerGestureBtnColorPrompt();
}
catch (Exception)
{
}
}
/// <summary>
/// 刷新其他窗口的主题
/// </summary>
@@ -515,6 +536,17 @@ namespace Ink_Canvas
operatingGuideWindow.RefreshTheme();
}
}
// 刷新计时器控件
if (TimerControl != null)
{
TimerControl.RefreshTheme();
}
if (MinimizedTimerControl != null)
{
MinimizedTimerControl.RefreshTheme();
}
}
catch (Exception)
{
+8 -8
View File
@@ -206,9 +206,6 @@ namespace Ink_Canvas
// 检查是否保存了多指书写模式状态
if (savedMultiTouchModeStates[pageIndex])
{
// 恢复多指书写模式
EnterMultiTouchModeIfNeeded();
// 更新UI状态
if (ToggleSwitchEnableMultiTouchMode != null)
{
@@ -219,9 +216,6 @@ namespace Ink_Canvas
}
else
{
// 确保多指书写模式关闭
ExitMultiTouchModeIfNeeded();
// 更新UI状态
if (ToggleSwitchEnableMultiTouchMode != null)
{
@@ -424,8 +418,14 @@ namespace Ink_Canvas
BtnLeftWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
BtnRightWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页";
// 始终允许点击"下一页/新页面"按钮(除非已达最大页数)
BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage;
if (isLastPage)
{
BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage;
}
else
{
BtnWhiteBoardSwitchNext.IsEnabled = true;
}
// 获取主题颜色资源
var iconForegroundBrush = Application.Current.FindResource("IconForeground") as SolidColorBrush;
+2 -2
View File
@@ -1,4 +1,4 @@
using Ink_Canvas.Helpers;
using Ink_Canvas.Helpers;
using System;
using System.Diagnostics;
using System.Windows;
@@ -857,4 +857,4 @@ namespace Ink_Canvas
}
}
}
}
}
+234 -157
View File
@@ -73,83 +73,67 @@ namespace Ink_Canvas
/// <summary>
/// 用於更新浮動工具欄的"手勢"按鈕和白板工具欄的"手勢"按鈕的樣式(開啟和關閉狀態)
/// </summary>
private void CheckEnableTwoFingerGestureBtnColorPrompt()
{
if (ToggleSwitchEnableMultiTouchMode.IsOn)
{
// 多指书写模式启用时,手势功能被禁用
private void CheckEnableTwoFingerGestureBtnColorPrompt() {
// 根据主题选择手势图标和颜色
bool isDarkTheme = Settings.Appearance.Theme == 1 ||
(Settings.Appearance.Theme == 2 && !IsSystemThemeLight());
bool isLightTheme = !isDarkTheme;
string gestureIconPath = isLightTheme ? "/Resources/new-icons/gesture.png" : "/Resources/new-icons/gesture_white.png";
// 根据主题设置白板模式下的颜色
Color boardBgColor, boardIconColor, boardTextColor, boardBorderColor;
if (isLightTheme) {
// 浅色主题
boardBgColor = Color.FromRgb(244, 244, 245);
boardIconColor = Color.FromRgb(24, 24, 27);
boardTextColor = Color.FromRgb(24, 24, 27);
boardBorderColor = Color.FromRgb(161, 161, 170);
} else {
// 深色主题
boardBgColor = Color.FromRgb(39, 39, 42);
boardIconColor = Color.FromRgb(244, 244, 245);
boardTextColor = Color.FromRgb(244, 244, 245);
boardBorderColor = Color.FromRgb(113, 113, 122);
}
if (ToggleSwitchEnableMultiTouchMode.IsOn) {
TwoFingerGestureSimpleStackPanel.Opacity = 0.5;
TwoFingerGestureSimpleStackPanel.IsHitTestVisible = false;
EnableTwoFingerGestureBtn.Source =
new BitmapImage(new Uri("/Resources/new-icons/gesture.png", UriKind.Relative));
// 根据主题设置颜色
if (Settings.Appearance.Theme == 1) // 深色主题
{
BoardGesture.Background = new SolidColorBrush(Color.FromRgb(42, 42, 42));
BoardGestureGeometry.Brush = new SolidColorBrush(Color.FromRgb(255, 255, 255));
BoardGestureGeometry2.Brush = new SolidColorBrush(Color.FromRgb(255, 255, 255));
BoardGestureLabel.Foreground = new SolidColorBrush(Color.FromRgb(255, 255, 255));
BoardGesture.BorderBrush = new SolidColorBrush(Color.FromRgb(85, 85, 85));
}
else // 浅色主题或跟随系统
{
BoardGesture.Background = new SolidColorBrush(Color.FromRgb(244, 244, 245));
BoardGestureGeometry.Brush = new SolidColorBrush(Color.FromRgb(24, 24, 27));
BoardGestureGeometry2.Brush = new SolidColorBrush(Color.FromRgb(24, 24, 27));
BoardGestureLabel.Foreground = new SolidColorBrush(Color.FromRgb(24, 24, 27));
BoardGesture.BorderBrush = new SolidColorBrush(Color.FromRgb(161, 161, 170));
}
new BitmapImage(new Uri(gestureIconPath, UriKind.Relative));
BoardGesture.Background = new SolidColorBrush(boardBgColor);
BoardGestureGeometry.Brush = new SolidColorBrush(boardIconColor);
BoardGestureGeometry2.Brush = new SolidColorBrush(boardIconColor);
BoardGestureLabel.Foreground = new SolidColorBrush(boardTextColor);
BoardGesture.BorderBrush = new SolidColorBrush(boardBorderColor);
BoardGestureGeometry.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.DisabledGestureIcon);
BoardGestureGeometry2.Geometry = Geometry.Parse("F0 M24,24z M0,0z");
// 强制禁用所有双指手势功能
ForceDisableTwoFingerGestures();
}
else
{
// 多指书写模式禁用时,根据实际手势功能状态显示
else {
TwoFingerGestureSimpleStackPanel.Opacity = 1;
TwoFingerGestureSimpleStackPanel.IsHitTestVisible = true;
// 检查是否有任何手势功能启用
bool hasGestureEnabled = Settings.Gesture.IsEnableTwoFingerGesture;
if (hasGestureEnabled)
{
if (Settings.Gesture.IsEnableTwoFingerGesture) {
EnableTwoFingerGestureBtn.Source =
new BitmapImage(new Uri("/Resources/new-icons/gesture-enabled.png", UriKind.Relative));
BoardGesture.Background = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardGestureGeometry.Brush = new SolidColorBrush(Colors.GhostWhite);
BoardGestureGeometry2.Brush = new SolidColorBrush(Colors.GhostWhite);
BoardGestureLabel.Foreground = new SolidColorBrush(Colors.GhostWhite);
BoardGesture.BorderBrush = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardGestureGeometry.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.EnabledGestureIcon);
BoardGestureGeometry2.Geometry = Geometry.Parse("F0 M24,24z M0,0z " + XamlGraphicsIconGeometries.EnabledGestureIconBadgeCheck);
BoardGestureGeometry2.Geometry = Geometry.Parse("F0 M24,24z M0,0z "+XamlGraphicsIconGeometries.EnabledGestureIconBadgeCheck);
}
else
{
else {
EnableTwoFingerGestureBtn.Source =
new BitmapImage(new Uri("/Resources/new-icons/gesture.png", UriKind.Relative));
// 根据主题设置颜色
if (Settings.Appearance.Theme == 1) // 深色主题
{
BoardGesture.Background = new SolidColorBrush(Color.FromRgb(42, 42, 42));
BoardGestureGeometry.Brush = new SolidColorBrush(Color.FromRgb(255, 255, 255));
BoardGestureGeometry2.Brush = new SolidColorBrush(Color.FromRgb(255, 255, 255));
BoardGestureLabel.Foreground = new SolidColorBrush(Color.FromRgb(255, 255, 255));
BoardGesture.BorderBrush = new SolidColorBrush(Color.FromRgb(85, 85, 85));
}
else // 浅色主题或跟随系统
{
BoardGesture.Background = new SolidColorBrush(Color.FromRgb(244, 244, 245));
BoardGestureGeometry.Brush = new SolidColorBrush(Color.FromRgb(24, 24, 27));
BoardGestureGeometry2.Brush = new SolidColorBrush(Color.FromRgb(24, 24, 27));
BoardGestureLabel.Foreground = new SolidColorBrush(Color.FromRgb(24, 24, 27));
BoardGesture.BorderBrush = new SolidColorBrush(Color.FromRgb(161, 161, 170));
}
new BitmapImage(new Uri(gestureIconPath, UriKind.Relative));
BoardGesture.Background = new SolidColorBrush(boardBgColor);
BoardGestureGeometry.Brush = new SolidColorBrush(boardIconColor);
BoardGestureGeometry2.Brush = new SolidColorBrush(boardIconColor);
BoardGestureLabel.Foreground = new SolidColorBrush(boardTextColor);
BoardGesture.BorderBrush = new SolidColorBrush(boardBorderColor);
BoardGestureGeometry.Geometry = Geometry.Parse(XamlGraphicsIconGeometries.DisabledGestureIcon);
BoardGestureGeometry2.Geometry = Geometry.Parse("F0 M24,24z M0,0z");
}
@@ -176,11 +160,16 @@ namespace Ink_Canvas
return;
}
// 在屏幕模式(非放映模式)下,不显示手势按钮
if (currentMode == 0)
{
EnableTwoFingerGestureBorder.Visibility = Visibility.Collapsed;
return;
if (GridTransparencyFakeBackground.Background != Brushes.Transparent && isVisible)
{
}
else
{
EnableTwoFingerGestureBorder.Visibility = Visibility.Collapsed;
return;
}
}
if (StackPanelCanvasControls.Visibility != Visibility.Visible
@@ -484,13 +473,23 @@ namespace Ink_Canvas
highlightColor = Color.FromRgb(30, 58, 138); // Keep current color for light theme
}
bool useLegacyUI = Settings.Appearance.UseLegacyFloatingBarUI;
switch (mode)
{
case "pen":
case "color":
{
PenIconGeometry.Brush = new SolidColorBrush(highlightColor);
PenIconGeometry.Geometry = Geometry.Parse(GetCorrectIcon("pen", true));
if (useLegacyUI)
{
PenIconGeometry.Brush = new SolidColorBrush(highlightColor);
PenIconGeometry.Geometry = Geometry.Parse(GetCorrectIcon("pen", false));
}
else
{
PenIconGeometry.Brush = new SolidColorBrush(highlightColor);
PenIconGeometry.Geometry = Geometry.Parse(GetCorrectIcon("pen", true));
}
BoardPen.Background = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardPen.BorderBrush = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardPenGeometry.Brush = new SolidColorBrush(Colors.GhostWhite);
@@ -501,9 +500,18 @@ namespace Ink_Canvas
}
case "eraser":
{
CircleEraserIconGeometry.Brush = new SolidColorBrush(highlightColor);
CircleEraserIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("eraserCircle", true));
if (useLegacyUI)
{
CircleEraserIconGeometry.Brush = new SolidColorBrush(highlightColor);
CircleEraserIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("eraserCircle", false));
}
else
{
CircleEraserIconGeometry.Brush = new SolidColorBrush(highlightColor);
CircleEraserIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("eraserCircle", true));
}
BoardEraser.Background = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardEraser.BorderBrush = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardEraserGeometry.Brush = new SolidColorBrush(Colors.GhostWhite);
@@ -514,9 +522,18 @@ namespace Ink_Canvas
}
case "eraserByStrokes":
{
StrokeEraserIconGeometry.Brush = new SolidColorBrush(highlightColor);
StrokeEraserIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("eraserStroke", true));
if (useLegacyUI)
{
StrokeEraserIconGeometry.Brush = new SolidColorBrush(highlightColor);
StrokeEraserIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("eraserStroke", false));
}
else
{
StrokeEraserIconGeometry.Brush = new SolidColorBrush(highlightColor);
StrokeEraserIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("eraserStroke", true));
}
BoardEraser.Background = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardEraser.BorderBrush = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardEraserGeometry.Brush = new SolidColorBrush(Colors.GhostWhite);
@@ -527,9 +544,18 @@ namespace Ink_Canvas
}
case "select":
{
LassoSelectIconGeometry.Brush = new SolidColorBrush(highlightColor);
LassoSelectIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("lassoSelect", true));
if (useLegacyUI)
{
LassoSelectIconGeometry.Brush = new SolidColorBrush(highlightColor);
LassoSelectIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("lassoSelect", false));
}
else
{
LassoSelectIconGeometry.Brush = new SolidColorBrush(highlightColor);
LassoSelectIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("lassoSelect", true));
}
BoardSelect.Background = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardSelect.BorderBrush = new SolidColorBrush(Color.FromRgb(37, 99, 235));
BoardSelectGeometry.Brush = new SolidColorBrush(Colors.GhostWhite);
@@ -540,9 +566,18 @@ namespace Ink_Canvas
}
case "cursor":
{
CursorIconGeometry.Brush = new SolidColorBrush(highlightColor);
CursorIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("cursor", true));
if (useLegacyUI)
{
CursorIconGeometry.Brush = new SolidColorBrush(highlightColor);
CursorIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("cursor", false));
}
else
{
CursorIconGeometry.Brush = new SolidColorBrush(highlightColor);
CursorIconGeometry.Geometry =
Geometry.Parse(GetCorrectIcon("cursor", true));
}
// 根据主题设置颜色
if (Settings.Appearance.Theme == 1) // 深色主题
{
@@ -740,7 +775,6 @@ namespace Ink_Canvas
ICCWaterMarkDark.Visibility = Visibility.Collapsed;
}
// 新增:确保在白板模式下基础浮动栏被隐藏
ViewboxFloatingBar.Visibility = Visibility.Collapsed;
}
else
@@ -1050,17 +1084,40 @@ namespace Ink_Canvas
AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
// 参考老计时器的窗口置顶功能:在白板模式下停止窗口置顶
if (currentMode == 1) // 白板模式
if (Settings.RandSettings?.UseNewStyleUI == true)
{
Topmost = false;
if (TimerContainer != null && TimerControl != null)
{
// 每次打开计时器窗口时重置计时器
TimerControl.ResetTimerState();
TimerContainer.Visibility = Visibility.Visible;
if (MinimizedTimerContainer != null)
{
MinimizedTimerContainer.Visibility = Visibility.Collapsed;
}
TimerControl.CloseRequested += (s, args) =>
{
TimerContainer.Visibility = Visibility.Collapsed;
if (MinimizedTimerContainer != null)
{
MinimizedTimerContainer.Visibility = Visibility.Collapsed;
}
};
}
}
var timerWindow = CountdownTimerWindow.CreateTimerWindow();
timerWindow.Show();
if (currentMode == 1) // 白板模式
else
{
timerWindow.Topmost = true;
if (currentMode == 1)
{
Topmost = false;
}
var timerWindow = CountdownTimerWindow.CreateTimerWindow();
timerWindow.Show();
if (currentMode == 1)
{
timerWindow.Topmost = true;
}
}
}
@@ -1085,11 +1142,20 @@ namespace Ink_Canvas
AnimationsHelper.HideWithSlideAndFade(BoardBorderTools);
AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel);
var randWindow = new RandWindow(Settings);
randWindow.Show();
// 根据设置决定使用哪个点名窗口
if (Settings.RandSettings.UseNewRollCallUI)
{
// 使用新点名UI - 随机抽模式
new NewStyleRollCallWindow(Settings, false).ShowDialog();
}
else
{
// 使用默认的随机点名窗口
var randWindow = new RandWindow(Settings);
randWindow.Show();
// 使用延迟确保窗口完全显示后再强制置顶
randWindow.Dispatcher.BeginInvoke(new Action(() =>
// 使用延迟确保窗口完全显示后再强制置顶
randWindow.Dispatcher.BeginInvoke(new Action(() =>
{
try
{
@@ -1126,6 +1192,7 @@ namespace Ink_Canvas
LogHelper.WriteLogToFile($"强制置顶RandWindow失败: {ex.Message}", LogHelper.LogType.Error);
}
}), DispatcherPriority.Loaded);
}
}
public void CheckEraserTypeTab()
@@ -1232,14 +1299,30 @@ namespace Ink_Canvas
{
MessageBox.Show("无法调用外部点名:" + ex.Message);
// 调用失败时回退到默认的随机点名窗口
new RandWindow(Settings, true).ShowDialog();
// 调用失败时回退到相应的点名窗口
if (Settings.RandSettings.UseNewRollCallUI)
{
new NewStyleRollCallWindow(Settings, true).ShowDialog(); // 单次抽模式
}
else
{
new RandWindow(Settings, true).ShowDialog();
}
}
}
else
{
// 使用默认的随机点名窗口
new RandWindow(Settings, true).ShowDialog();
// 根据设置决定使用哪个点名窗口
if (Settings.RandSettings.UseNewRollCallUI)
{
// 使用新点名UI - 单次抽模式
new NewStyleRollCallWindow(Settings, true).ShowDialog();
}
else
{
// 使用默认的随机点名窗口
new RandWindow(Settings, true).ShowDialog();
}
}
}
@@ -1775,7 +1858,7 @@ namespace Ink_Canvas
});
}
public async void PureViewboxFloatingBarMarginAnimationInPPTMode()
public async void PureViewboxFloatingBarMarginAnimationInPPTMode(bool isRetry = false)
{
// 新增:在白板模式下不执行浮动栏动画
if (currentMode == 1)
@@ -1871,6 +1954,25 @@ namespace Ink_Canvas
{
ViewboxFloatingBar.Margin = new Thickness(pos.X, pos.Y, -2000, -200);
});
if (Settings.ModeSettings.IsPPTOnlyMode && !isRetry)
{
await Task.Delay(2000); // 等待动画完成后再检查
bool isFloatingBarVisible = false;
await Dispatcher.InvokeAsync(() =>
{
// 检查浮动栏是否真的显示了
isFloatingBarVisible = ViewboxFloatingBar.Visibility == Visibility.Visible &&
ViewboxFloatingBar.Margin.Left >= 0 &&
ViewboxFloatingBar.Margin.Top >= 0;
});
if (!isFloatingBarVisible)
{
PureViewboxFloatingBarMarginAnimationInPPTMode(true);
}
}
}
internal async void CursorIcon_Click(object sender, RoutedEventArgs e)
@@ -1997,8 +2099,6 @@ namespace Ink_Canvas
// 禁用高级橡皮擦系统
DisableEraserOverlay();
ExitMultiTouchModeIfNeeded();
SetFloatingBarHighlightPosition("pen");
// 记录当前是否已经是批注模式且是否为高光显示模式
@@ -2016,7 +2116,6 @@ namespace Ink_Canvas
{
drawingShapeMode = 0;
isLongPressSelected = false;
ResetAllShapeButtonsOpacity();
}
// 使用集中化的工具模式切换方法
@@ -2224,7 +2323,6 @@ namespace Ink_Canvas
internal void EraserIcon_Click(object sender, RoutedEventArgs e)
{
EnterMultiTouchModeIfNeeded();
bool isAlreadyEraser = inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint;
forceEraser = false;
forcePointEraser = true;
@@ -2276,7 +2374,6 @@ namespace Ink_Canvas
private void BoardEraserIcon_Click(object sender, RoutedEventArgs e)
{
EnterMultiTouchModeIfNeeded();
bool isAlreadyEraser = inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint;
forceEraser = false;
forcePointEraser = true;
@@ -2315,7 +2412,6 @@ namespace Ink_Canvas
private void EraserIconByStrokes_Click(object sender, RoutedEventArgs e)
{
EnterMultiTouchModeIfNeeded();
if (lastBorderMouseDownObject != null && lastBorderMouseDownObject is Panel)
((Panel)lastBorderMouseDownObject).Background = new SolidColorBrush(Colors.Transparent);
@@ -2840,39 +2936,14 @@ namespace Ink_Canvas
// 恢复非笔画元素
RestoreNonStrokeElements(preservedElements);
CancelSingleFingerDragMode();
if (Settings.Canvas.ClearCanvasAndClearTimeMachine) timeMachine.ClearStrokeHistory();
if (Settings.Gesture.IsEnableMultiTouchMode && ToggleSwitchEnableMultiTouchMode != null && ToggleSwitchEnableMultiTouchMode.IsOn)
{
ReinitializeMultiTouchMode();
}
CancelSingleFingerDragMode();
}
private bool lastIsInMultiTouchMode;
private void ReinitializeMultiTouchMode()
{
try
{
if (!isInMultiTouchMode)
{
isInMultiTouchMode = true;
}
inkCanvas.TouchDown -= Main_Grid_TouchDown;
inkCanvas.TouchDown += MainWindow_TouchDown;
inkCanvas.StylusDown += MainWindow_StylusDown;
inkCanvas.StylusMove += MainWindow_StylusMove;
inkCanvas.StylusUp += MainWindow_StylusUp;
}
catch (Exception)
{
}
}
private void CancelSingleFingerDragMode()
{
if (ToggleSwitchDrawShapeBorderAutoHide.IsOn) CollapseBorderDrawShape();
@@ -2881,8 +2952,6 @@ namespace Ink_Canvas
if (isSingleFingerDragMode) BtnFingerDragMode_Click(BtnFingerDragMode, null);
isLongPressSelected = false;
ResetTouchStates();
}
/// <summary>
@@ -2895,23 +2964,6 @@ namespace Ink_Canvas
// 清空触摸点计数器
dec.Clear();
// 重置单指拖动模式状态
if (isSingleFingerDragMode)
{
isSingleFingerDragMode = false;
if (BtnFingerDragMode != null)
{
BtnFingerDragMode.Content = "单指\n拖动";
}
}
// 重置手掌擦状态
if (isPalmEraserActive)
{
isPalmEraserActive = false;
}
// 确保触摸事件能正常响应
inkCanvas.IsHitTestVisible = true;
inkCanvas.IsManipulationEnabled = true;
@@ -3009,10 +3061,26 @@ namespace Ink_Canvas
ClearStrokes(true);
RestoreStrokes(true);
// 新增:在屏幕模式下恢复基础浮动栏的显示
// 退出白板模式时取消全屏
if (Settings.Advanced.IsEnableAvoidFullScreenHelper)
{
// 恢复为非画板模式,重新启用全屏限制
AvoidFullScreenHelper.SetBoardMode(false);
Dispatcher.BeginInvoke(new Action(() =>
{
// 退出白板模式,恢复到工作区域大小
var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
MainWindow.MoveWindow(new WindowInteropHelper(this).Handle,
workingArea.Left, workingArea.Top,
workingArea.Width, workingArea.Height, true);
}), DispatcherPriority.ApplicationIdle);
}
// 在屏幕模式下恢复基础浮动栏的显示
ViewboxFloatingBar.Visibility = Visibility.Visible;
// 新增:退出白板时自动收纳功能 - 等待浮动栏完全展开后再收纳
// 退出白板时自动收纳功能 - 等待浮动栏完全展开后再收纳
if (Settings.Automation.IsAutoFoldWhenExitWhiteboard && !isFloatingBarFolded)
{
// 使用异步延迟,等待浮动栏展开动画完成后再收纳
@@ -3021,7 +3089,10 @@ namespace Ink_Canvas
await Task.Delay(700);
await Dispatcher.InvokeAsync(() =>
{
FoldFloatingBar_MouseUp(new object(), null);
if (_pptManager?.IsInSlideShow != true)
{
FoldFloatingBar_MouseUp(new object(), null);
}
});
});
}
@@ -3060,11 +3131,21 @@ namespace Ink_Canvas
SaveStrokes(true);
ClearStrokes(true);
// 总是恢复备份墨迹,不管是否在PPT模式
// PPT墨迹和白板墨迹应该分别管理,不应该互相影响
RestoreStrokes();
// 新增:在白板模式下隐藏基础浮动栏
// 进入白板模式时全屏
if (Settings.Advanced.IsEnableAvoidFullScreenHelper)
{
// 设置为画板模式,允许全屏操作
AvoidFullScreenHelper.SetBoardMode(true);
Dispatcher.BeginInvoke(new Action(() =>
{
MainWindow.MoveWindow(new WindowInteropHelper(this).Handle, 0, 0,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height, true);
}), DispatcherPriority.ApplicationIdle);
}
ViewboxFloatingBar.Visibility = Visibility.Collapsed;
BtnSwitch.Content = "屏幕";
@@ -3215,7 +3296,6 @@ namespace Ink_Canvas
CheckEnableTwoFingerGestureBtnVisibility(false);
HideSubPanels("cursor");
// 新增:在屏幕模式下显示基础浮动栏
if (currentMode == 0)
{
ViewboxFloatingBar.Visibility = Visibility.Visible;
@@ -3226,7 +3306,6 @@ namespace Ink_Canvas
AnimationsHelper.ShowWithSlideFromLeftAndFade(StackPanelCanvasControls);
CheckEnableTwoFingerGestureBtnVisibility(true);
// 新增:在批注模式下显示基础浮动栏
if (currentMode == 0)
{
ViewboxFloatingBar.Visibility = Visibility.Visible;
@@ -3370,7 +3449,7 @@ namespace Ink_Canvas
}
}
// 新增:插入图片方法
// 插入图片方法
private async void InsertImage_MouseUp_New(object sender, MouseButtonEventArgs e)
{
var dialog = new OpenFileDialog
@@ -3602,8 +3681,6 @@ namespace Ink_Canvas
// 检查浮动栏是否处于收起状态
if (isFloatingBarFolded || (BorderFloatingBarMainControls != null && BorderFloatingBarMainControls.Visibility == Visibility.Collapsed))
{
// 在收起状态下,仍然需要设置高光位置,但可能需要调整计算方式
// 这里先隐藏高光,等浮动栏展开时再显示
FloatingbarSelectionBG.Visibility = Visibility.Hidden;
return;
}
+8 -26
View File
@@ -7,49 +7,31 @@ namespace Ink_Canvas
{
private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
{
// 只有在PPT放映模式下才响应鼠标滚轮翻页
if (StackPanelPPTControls.Visibility != Visibility.Visible ||
currentMode != 0 ||
BtnPPTSlideShowEnd.Visibility != Visibility.Visible ||
PPTManager?.IsInSlideShow != true) return;
// 直接发送翻页请求到PPT放映软件,不通过软件处理
if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return;
if (e.Delta >= 120)
{
// 上一页 - 发送PageUp键到PPT放映窗口
SendKeyToPPTSlideShow(true);
BtnPPTSlidesUp_Click(null, null);
}
else if (e.Delta <= -120)
{
// 下一页 - 发送PageDown键到PPT放映窗口
SendKeyToPPTSlideShow(false);
BtnPPTSlidesDown_Click(null, null);
}
}
private void Main_Grid_PreviewKeyDown(object sender, KeyEventArgs e)
{
// 只有在PPT放映模式下才响应键盘翻页快捷键
if (StackPanelPPTControls.Visibility != Visibility.Visible ||
currentMode != 0 ||
BtnPPTSlideShowEnd.Visibility != Visibility.Visible ||
PPTManager?.IsInSlideShow != true) return;
if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return;
// 直接发送翻页请求到PPT放映软件,不通过软件处理
if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N ||
e.Key == Key.Space)
if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N || e.Key == Key.Space)
{
e.Handled = true; // 阻止事件继续传播
SendKeyToPPTSlideShow(false); // 下一页
BtnPPTSlidesDown_Click(null, null);
}
else if (e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Left || e.Key == Key.P)
if (e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Left || e.Key == Key.P)
{
e.Handled = true; // 阻止事件继续传播
SendKeyToPPTSlideShow(true); // 上一页
BtnPPTSlidesUp_Click(null, null);
}
}
// 保留PPT翻页快捷键处理
// 以下方法保留供全局快捷键调用
private void HotKey_Undo(object sender, ExecutedRoutedEventArgs e)
{
+346 -359
View File
@@ -77,7 +77,6 @@ namespace Ink_Canvas
#endregion
#region PPT State Management
private bool wasFloatingBarFoldedWhenEnterSlideShow;
private bool isEnteredSlideShowEndEvent;
private bool isPresentationHaveBlackSpace;
@@ -94,17 +93,20 @@ namespace Ink_Canvas
// 上次播放位置相关字段
private int _lastPlaybackPage = 0;
private bool _shouldNavigateToLastPage = false;
// 当前播放页码跟踪
private int _currentSlideShowPosition = 0;
// 页面切换防抖机制
private DateTime _lastSlideSwitchTime = DateTime.MinValue;
private int _pendingSlideIndex = -1;
private System.Timers.Timer _slideSwitchDebounceTimer;
private const int SlideSwitchDebounceMs = 150; // 防抖延迟150毫秒
private const int SlideSwitchDebounceMs = 150;
private bool _isInkClearedByButton = false;
#endregion
#region PPT Managers
private PPTManager _pptManager;
private MultiPPTInkManager _multiPPTInkManager;
private PPTInkManager _singlePPTInkManager;
private PPTUIManager _pptUIManager;
@@ -135,19 +137,9 @@ namespace Ink_Canvas
_pptManager.PresentationClose += OnPPTPresentationClose;
_pptManager.SlideShowStateChanged += OnPPTSlideShowStateChanged;
if (Settings.PowerPointSettings.IsSupportWPS)
{
_singlePPTInkManager = new PPTInkManager();
_singlePPTInkManager.IsAutoSaveEnabled = Settings.PowerPointSettings.IsAutoSaveStrokesInPowerPoint;
_singlePPTInkManager.AutoSaveLocation = Settings.Automation.AutoSavedStrokesLocation;
}
else
{
_multiPPTInkManager = new MultiPPTInkManager();
_multiPPTInkManager.IsAutoSaveEnabled = Settings.PowerPointSettings.IsAutoSaveStrokesInPowerPoint;
_multiPPTInkManager.AutoSaveLocation = Settings.Automation.AutoSavedStrokesLocation;
_multiPPTInkManager.PPTManager = _pptManager;
}
_singlePPTInkManager = new PPTInkManager();
_singlePPTInkManager.IsAutoSaveEnabled = Settings.PowerPointSettings.IsAutoSaveStrokesInPowerPoint;
_singlePPTInkManager.AutoSaveLocation = Settings.Automation.AutoSavedStrokesLocation;
// 初始化UI管理器
_pptUIManager = new PPTUIManager(this);
@@ -430,12 +422,10 @@ namespace Ink_Canvas
try
{
_pptManager?.Dispose();
_multiPPTInkManager?.Dispose();
_singlePPTInkManager?.Dispose();
_longPressTimer?.Stop();
_longPressTimer = null;
_pptManager = null;
_multiPPTInkManager = null;
_singlePPTInkManager = null;
_pptUIManager = null;
@@ -521,14 +511,7 @@ namespace Ink_Canvas
else
{
LogHelper.WriteLogToFile("PPT连接已断开", LogHelper.LogType.Event);
if (Settings.PowerPointSettings.IsSupportWPS)
{
_singlePPTInkManager?.ClearAllStrokes();
}
else
{
_multiPPTInkManager?.ClearAllStrokes();
}
_singlePPTInkManager?.ClearAllStrokes();
}
});
}
@@ -553,14 +536,7 @@ namespace Ink_Canvas
TimeMachineHistories[0] = null;
}
if (Settings.PowerPointSettings.IsSupportWPS)
{
_singlePPTInkManager?.InitializePresentation(pres);
}
else
{
_multiPPTInkManager?.InitializePresentation(pres);
}
_singlePPTInkManager?.InitializePresentation(pres);
// 处理跳转到首页或上次播放页的逻辑
HandlePresentationOpenNavigation(pres);
@@ -594,15 +570,7 @@ namespace Ink_Canvas
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
if (Settings.PowerPointSettings.IsSupportWPS)
{
_singlePPTInkManager?.SaveAllStrokesToFile(pres);
}
else
{
_multiPPTInkManager?.SaveAllStrokesToFile(pres);
_multiPPTInkManager?.RemovePresentation(pres);
}
_singlePPTInkManager?.SaveAllStrokesToFile(pres);
_pptUIManager?.UpdateConnectionStatus(false);
});
@@ -647,9 +615,6 @@ namespace Ink_Canvas
{
try
{
// 始终记录进入放映时浮动栏收纳状态,用于退出时恢复
wasFloatingBarFoldedWhenEnterSlideShow = isFloatingBarFolded;
if (Settings.Automation.IsAutoFoldInPPTSlideShow)
{
if (!isFloatingBarFolded)
@@ -665,17 +630,40 @@ namespace Ink_Canvas
isStopInkReplay = true;
await Application.Current.Dispatcher.InvokeAsync(() =>
await Application.Current.Dispatcher.InvokeAsync(async () =>
{
var activePresentation = _pptManager?.GetCurrentActivePresentation();
Presentation activePresentation = null;
int currentSlide = 0;
int totalSlides = 0;
if (wn?.View != null && wn.Presentation != null)
{
activePresentation = wn.Presentation;
currentSlide = wn.View.CurrentShowPosition;
totalSlides = activePresentation.Slides.Count;
// 初始化当前播放页码跟踪
_currentSlideShowPosition = currentSlide;
}
else
{
activePresentation = _pptManager?.GetCurrentActivePresentation();
currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0;
totalSlides = _pptManager?.SlidesCount ?? 0;
// 初始化当前播放页码跟踪
_currentSlideShowPosition = currentSlide;
}
if (activePresentation != null)
{
if (Settings.PowerPointSettings.IsSupportWPS)
if (_singlePPTInkManager != null)
{
}
else
{
_multiPPTInkManager?.SwitchToPresentation(activePresentation);
try
{
_singlePPTInkManager.InitializePresentation(activePresentation);
}
catch (Exception)
{
}
}
}
@@ -691,8 +679,6 @@ namespace Ink_Canvas
}
// 更新UI状态
var currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0;
var totalSlides = _pptManager?.SlidesCount ?? 0;
_pptUIManager?.UpdateSlideShowStatus(true, currentSlide, totalSlides);
// 设置浮动栏透明度和边距
@@ -740,41 +726,39 @@ namespace Ink_Canvas
if (Settings.PowerPointSettings.IsShowCanvasAtNewSlideShow &&
!Settings.Automation.IsAutoFoldInPPTSlideShow)
{
await Task.Delay(300);
// 先进入批注模式,这会显示调色盘
PenIcon_Click(null, null);
// 然后设置颜色
BtnColorRed_Click(null, null);
Dispatcher.BeginInvoke(new Action(() =>
try
{
try
if (inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
{
if (inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
UpdateCurrentToolMode("pen");
SetFloatingBarHighlightPosition("pen");
if (Settings.Appearance.IsShowQuickColorPalette && QuickColorPalettePanel != null && QuickColorPaletteSingleRowPanel != null)
{
UpdateCurrentToolMode("pen");
SetFloatingBarHighlightPosition("pen");
if (Settings.Appearance.IsShowQuickColorPalette && QuickColorPalettePanel != null && QuickColorPaletteSingleRowPanel != null)
// 根据显示模式选择显示哪个面板
if (Settings.Appearance.QuickColorPaletteDisplayMode == 0)
{
// 根据显示模式选择显示哪个面板
if (Settings.Appearance.QuickColorPaletteDisplayMode == 0)
{
// 单行显示模式
QuickColorPalettePanel.Visibility = Visibility.Collapsed;
QuickColorPaletteSingleRowPanel.Visibility = Visibility.Visible;
}
else
{
// 双行显示模式
QuickColorPalettePanel.Visibility = Visibility.Visible;
QuickColorPaletteSingleRowPanel.Visibility = Visibility.Collapsed;
}
// 单行显示模式
QuickColorPalettePanel.Visibility = Visibility.Collapsed;
QuickColorPaletteSingleRowPanel.Visibility = Visibility.Visible;
}
else
{
// 双行显示模式
QuickColorPalettePanel.Visibility = Visibility.Visible;
QuickColorPaletteSingleRowPanel.Visibility = Visibility.Collapsed;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PPT进入批注模式后同步浮动栏高光状态失败: {ex.Message}", LogHelper.LogType.Error);
}
}), DispatcherPriority.Loaded);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PPT进入批注模式后同步浮动栏高光状态失败: {ex.Message}", LogHelper.LogType.Error);
}
}
isEnteredSlideShowEndEvent = false;
@@ -785,19 +769,50 @@ namespace Ink_Canvas
if (!isFloatingBarFolded)
{
new Thread(() =>
_ = Task.Run(async () =>
{
Thread.Sleep(100);
Application.Current.Dispatcher.Invoke(() =>
try
{
ViewboxFloatingBarMarginAnimation(60);
});
}).Start();
await Task.Delay(100);
await Application.Current.Dispatcher.InvokeAsync(() =>
{
ViewboxFloatingBar.UpdateLayout();
// 如果浮动栏宽度仍未计算好,再等待一段时间
if (ViewboxFloatingBar.ActualWidth <= 0)
{
LogHelper.WriteLogToFile("浮动栏宽度未准备好,等待布局完成", LogHelper.LogType.Trace);
}
});
await Task.Delay(100);
await Application.Current.Dispatcher.InvokeAsync(() =>
{
PureViewboxFloatingBarMarginAnimationInPPTMode(false);
});
}
catch (Exception)
{
try
{
await Task.Delay(100);
await Application.Current.Dispatcher.InvokeAsync(() =>
{
ViewboxFloatingBarMarginAnimation(60);
});
}
catch (Exception)
{
}
}
});
}
}
catch (Exception ex)
catch (Exception)
{
LogHelper.WriteLogToFile($"处理幻灯片放映开始事件失败: {ex}", LogHelper.LogType.Error);
}
}
@@ -805,25 +820,69 @@ namespace Ink_Canvas
{
try
{
Application.Current.Dispatcher.InvokeAsync(() =>
Application.Current.Dispatcher.Invoke(() =>
{
var activePresentation = _pptManager?.GetCurrentActivePresentation();
if (activePresentation != null)
if (wn?.View == null || wn.Presentation == null)
{
if (Settings.PowerPointSettings.IsSupportWPS)
return;
}
var currentSlide = wn.View.CurrentShowPosition;
var activePresentation = wn.Presentation;
var totalSlides = activePresentation.Slides.Count;
// 获取之前的页码(用于保存墨迹)
var previousSlide = _currentSlideShowPosition > 0 ? _currentSlideShowPosition :
(_pptManager?.GetCurrentSlideNumber() ?? 0);
if (_isInkClearedByButton)
{
_isInkClearedByButton = false;
}
else
{
StrokeCollection strokesToSave = null;
if (previousSlide > 0 && previousSlide != currentSlide && inkCanvas.Strokes.Count > 0)
{
strokesToSave = inkCanvas.Strokes.Clone();
}
else
// 清除墨迹
if (inkCanvas.Strokes.Count > 0)
{
_multiPPTInkManager?.SwitchToPresentation(activePresentation);
ClearStrokes(true);
timeMachine.ClearStrokeHistory();
}
// 异步保存之前页面的墨迹
if (strokesToSave != null && previousSlide > 0 && previousSlide != currentSlide)
{
Task.Run(() =>
{
try
{
Application.Current.Dispatcher.Invoke(() =>
{
bool canWrite = _singlePPTInkManager?.CanWriteInk(previousSlide) == true;
if (canWrite)
{
_singlePPTInkManager?.SaveCurrentSlideStrokes(previousSlide, strokesToSave);
}
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"异步保存PPT页面墨迹失败: {ex}", LogHelper.LogType.Error);
}
});
}
}
var currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0;
var totalSlides = _pptManager?.SlidesCount ?? 0;
// 更新当前播放页码
_currentSlideShowPosition = currentSlide;
// 使用防抖机制处理页面切换
HandleSlideSwitchWithDebounce(currentSlide, totalSlides);
LoadCurrentSlideInk(currentSlide, skipClear: true);
_pptUIManager?.UpdateCurrentSlideNumber(currentSlide, totalSlides);
});
}
@@ -837,47 +896,40 @@ namespace Ink_Canvas
{
try
{
if (Settings.Automation.IsAutoFoldAfterPPTSlideShow)
// PPT退出时自动收纳浮动栏
if (!isFloatingBarFolded)
{
if (wasFloatingBarFoldedWhenEnterSlideShow)
{
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(new object(), null);
}
else
{
if (isFloatingBarFolded) await UnFoldFloatingBar(new object());
}
}
else
{
if (Settings.Automation.IsAutoFoldInPPTSlideShow)
{
if (isFloatingBarFolded)
{
await UnFoldFloatingBar(new object());
}
}
else
{
if (isFloatingBarFolded)
{
await UnFoldFloatingBar(new object());
}
}
FoldFloatingBar_MouseUp(new object(), null);
}
if (isEnteredSlideShowEndEvent) return;
isEnteredSlideShowEndEvent = true;
if (Settings.PowerPointSettings.IsSupportWPS)
// 获取当前播放页码,优先使用跟踪的页码,否则尝试从PPT管理器获取
int currentPage = _currentSlideShowPosition;
if (currentPage <= 0)
{
_singlePPTInkManager?.SaveAllStrokesToFile(pres);
}
else
{
_multiPPTInkManager?.SaveAllStrokesToFile(pres);
try
{
currentPage = _pptManager?.GetCurrentSlideNumber() ?? 0;
}
catch
{
// 如果无法获取,尝试从演示文稿的SlideShowWindow获取
try
{
if (pres.SlideShowWindow != null && pres.SlideShowWindow.View != null)
{
currentPage = pres.SlideShowWindow.View.CurrentShowPosition;
}
}
catch { }
}
}
// 保存墨迹和位置信息
_singlePPTInkManager?.SaveAllStrokesToFile(pres, currentPage);
await Application.Current.Dispatcher.InvokeAsync(() =>
{
try
@@ -950,7 +1002,7 @@ namespace Ink_Canvas
await Application.Current.Dispatcher.InvokeAsync(() =>
{
PureViewboxFloatingBarMarginAnimationInDesktopMode();
ViewboxFloatingBarMarginAnimation(100, true);
ViewboxFloatingBarMarginAnimation(-60);
});
}
catch (Exception ex)
@@ -1131,23 +1183,20 @@ namespace Ink_Canvas
}
}
private void LoadCurrentSlideInk(int slideIndex)
private void LoadCurrentSlideInk(int slideIndex, bool skipClear = false)
{
try
{
StrokeCollection strokes = null;
if (Settings.PowerPointSettings.IsSupportWPS)
// 如果未跳过清除,则清除当前墨迹
if (!skipClear)
{
strokes = _singlePPTInkManager?.LoadSlideStrokes(slideIndex);
}
else
{
strokes = _multiPPTInkManager?.LoadSlideStrokes(slideIndex);
ClearStrokes(true);
timeMachine.ClearStrokeHistory();
}
StrokeCollection strokes = _singlePPTInkManager?.LoadSlideStrokes(slideIndex);
if (strokes != null)
if (strokes != null && strokes.Count > 0)
{
inkCanvas.Strokes.Clear();
inkCanvas.Strokes.Add(strokes);
}
}
@@ -1164,19 +1213,7 @@ namespace Ink_Canvas
{
try
{
if (Settings.PowerPointSettings.IsSupportWPS)
{
_singlePPTInkManager?.ResetLockState();
}
else
{
var activePresentation = _pptManager?.GetCurrentActivePresentation();
if (activePresentation != null)
{
_multiPPTInkManager?.SwitchToPresentation(activePresentation);
_multiPPTInkManager?.ResetCurrentPresentationLockState();
}
}
_singlePPTInkManager?.ResetLockState();
}
catch (Exception ex)
{
@@ -1191,9 +1228,6 @@ namespace Ink_Canvas
{
try
{
// 重置进入PPT时的浮动栏收纳状态记录
wasFloatingBarFoldedWhenEnterSlideShow = false;
// 重置PPT放映结束事件标志
isEnteredSlideShowEndEvent = false;
@@ -1203,6 +1237,9 @@ namespace Ink_Canvas
// 重置上次播放位置相关字段
_lastPlaybackPage = 0;
_shouldNavigateToLastPage = false;
// 重置当前播放页码跟踪
_currentSlideShowPosition = 0;
// 重置页面切换防抖机制
_lastSlideSwitchTime = DateTime.MinValue;
@@ -1221,51 +1258,14 @@ namespace Ink_Canvas
/// </summary>
private void HandleSlideSwitchWithDebounce(int currentSlide, int totalSlides)
{
try
{
var now = DateTime.Now;
// 如果距离上次切换时间太短,使用防抖机制
if (now - _lastSlideSwitchTime < TimeSpan.FromMilliseconds(SlideSwitchDebounceMs))
{
_pendingSlideIndex = currentSlide;
// 停止之前的定时器
_slideSwitchDebounceTimer?.Stop();
// 创建新的定时器
_slideSwitchDebounceTimer = new System.Timers.Timer(SlideSwitchDebounceMs);
_slideSwitchDebounceTimer.Elapsed += (sender, e) =>
{
Application.Current.Dispatcher.Invoke(() =>
{
if (_pendingSlideIndex > 0)
{
SwitchSlideInk(_pendingSlideIndex);
_pptUIManager?.UpdateCurrentSlideNumber(_pendingSlideIndex, totalSlides);
_pendingSlideIndex = -1;
}
});
_slideSwitchDebounceTimer?.Stop();
};
_slideSwitchDebounceTimer.Start();
}
else
{
// 直接处理页面切换
SwitchSlideInk(currentSlide);
_pptUIManager?.UpdateCurrentSlideNumber(currentSlide, totalSlides);
}
_lastSlideSwitchTime = now;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理页面切换防抖失败: {ex}", LogHelper.LogType.Error);
}
}
private void SwitchSlideInk(int newSlideIndex)
/// <summary>
/// 切换页面墨迹
/// </summary>
/// <param name="newSlideIndex">新页面索引</param>
/// <param name="skipClear">是否跳过清除操作(如果已在翻页时立即清除,则设为true</param>
private void SwitchSlideInk(int newSlideIndex, bool skipClear = false)
{
try
{
@@ -1278,7 +1278,6 @@ namespace Ink_Canvas
// 获取当前页面索引
var currentSlideIndex = _pptManager?.GetCurrentSlideNumber() ?? 0;
// 验证页面索引的有效性
if (newSlideIndex <= 0)
{
@@ -1287,50 +1286,30 @@ namespace Ink_Canvas
}
// 如果有当前墨迹且不是第一次切换,先保存到当前页面
if (inkCanvas.Strokes.Count > 0 && currentSlideIndex > 0 && currentSlideIndex != newSlideIndex)
if (currentSlideIndex > 0 && currentSlideIndex != newSlideIndex)
{
bool canWrite = false;
if (Settings.PowerPointSettings.IsSupportWPS)
bool canWrite = _singlePPTInkManager?.CanWriteInk(currentSlideIndex) == true;
if (canWrite && inkCanvas.Strokes.Count > 0)
{
canWrite = _singlePPTInkManager?.CanWriteInk(currentSlideIndex) == true;
_singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlideIndex, inkCanvas.Strokes);
}
else
{
canWrite = _multiPPTInkManager?.CanWriteInk(currentSlideIndex) == true;
}
if (canWrite)
{
if (Settings.PowerPointSettings.IsSupportWPS)
{
_singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlideIndex, inkCanvas.Strokes);
}
else
{
_multiPPTInkManager?.SaveCurrentSlideStrokes(currentSlideIndex, inkCanvas.Strokes);
}
}
}
else if (inkCanvas.Strokes.Count > 0 && currentSlideIndex <= 0)
{
}
StrokeCollection newStrokes = null;
if (Settings.PowerPointSettings.IsSupportWPS)
if (!skipClear)
{
newStrokes = _singlePPTInkManager?.SwitchToSlide(newSlideIndex, null);
ClearStrokes(true);
timeMachine.ClearStrokeHistory();
}
else
// 加载新页面的墨迹
StrokeCollection newStrokes = _singlePPTInkManager?.SwitchToSlide(newSlideIndex, null);
if (newStrokes != null && newStrokes.Count > 0)
{
newStrokes = _multiPPTInkManager?.SwitchToSlide(newSlideIndex, null);
}
if (newStrokes != null)
{
inkCanvas.Strokes.Clear();
inkCanvas.Strokes.Add(newStrokes);
}
// 注意:LockInkForSlide已经在SwitchToSlide中调用,这里不需要重复调用
}
catch (Exception ex)
{
@@ -1460,32 +1439,72 @@ namespace Ink_Canvas
{
try
{
// 保存当前页墨迹
var currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0;
if (currentSlide > 0)
var previousSlideBeforeNavigate = _pptManager?.GetCurrentSlideNumber() ?? 0;
StrokeCollection strokesToSave = null;
if (previousSlideBeforeNavigate > 0 && inkCanvas.Strokes.Count > 0)
{
if (Settings.PowerPointSettings.IsSupportWPS)
{
_singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes);
}
else
{
_multiPPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes);
}
strokesToSave = inkCanvas.Strokes.Clone();
}
// 保存截图(如果启用)
if (inkCanvas.Strokes.Count > Settings.Automation.MinimumAutomationStrokeNumber &&
Settings.PowerPointSettings.IsAutoSaveScreenShotInPowerPoint)
{
var presentationName = _pptManager?.GetPresentationName() ?? "";
SaveScreenShot(true, $"{presentationName}/{currentSlide}");
}
// 执行翻页
if (_pptManager?.TryNavigatePrevious() == true)
{
// 翻页成功,等待事件处理墨迹切换
var currentSlideAfterNavigate = _pptManager?.GetCurrentSlideNumber() ?? 0;
if (previousSlideBeforeNavigate == currentSlideAfterNavigate && previousSlideBeforeNavigate > 0)
{
Thread.Sleep(50);
currentSlideAfterNavigate = _pptManager?.GetCurrentSlideNumber() ?? 0;
}
if (previousSlideBeforeNavigate != currentSlideAfterNavigate && previousSlideBeforeNavigate > 0)
{
if (inkCanvas.Strokes.Count > 0)
{
ClearStrokes(true);
timeMachine.ClearStrokeHistory();
_isInkClearedByButton = true;
}
if (strokesToSave != null && previousSlideBeforeNavigate > 0)
{
Task.Run(() =>
{
try
{
Application.Current.Dispatcher.Invoke(() =>
{
_singlePPTInkManager?.SaveCurrentSlideStrokes(previousSlideBeforeNavigate, strokesToSave);
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"异步保存PPT上一页墨迹失败: {ex}", LogHelper.LogType.Error);
}
});
// 异步保存截图(如果启用)
if (strokesToSave.Count > Settings.Automation.MinimumAutomationStrokeNumber &&
Settings.PowerPointSettings.IsAutoSaveScreenShotInPowerPoint)
{
Task.Run(() =>
{
try
{
Application.Current.Dispatcher.Invoke(() =>
{
var presentationName = _pptManager?.GetPresentationName() ?? "";
SaveScreenShot(true, $"{presentationName}/{previousSlideBeforeNavigate}");
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"异步保存PPT上一页截图失败: {ex}", LogHelper.LogType.Error);
}
});
}
}
}
}
else
{
@@ -1507,32 +1526,72 @@ namespace Ink_Canvas
{
try
{
// 保存当前页墨迹
var currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0;
if (currentSlide > 0)
var previousSlideBeforeNavigate = _pptManager?.GetCurrentSlideNumber() ?? 0;
StrokeCollection strokesToSave = null;
if (previousSlideBeforeNavigate > 0 && inkCanvas.Strokes.Count > 0)
{
if (Settings.PowerPointSettings.IsSupportWPS)
{
_singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes);
}
else
{
_multiPPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes);
}
strokesToSave = inkCanvas.Strokes.Clone();
}
// 保存截图(如果启用)
if (inkCanvas.Strokes.Count > Settings.Automation.MinimumAutomationStrokeNumber &&
Settings.PowerPointSettings.IsAutoSaveScreenShotInPowerPoint)
{
var presentationName = _pptManager?.GetPresentationName() ?? "";
SaveScreenShot(true, $"{presentationName}/{currentSlide}");
}
// 执行翻页
if (_pptManager?.TryNavigateNext() == true)
{
// 翻页成功,等待事件处理墨迹切换
var currentSlideAfterNavigate = _pptManager?.GetCurrentSlideNumber() ?? 0;
if (previousSlideBeforeNavigate == currentSlideAfterNavigate && previousSlideBeforeNavigate > 0)
{
Thread.Sleep(50);
currentSlideAfterNavigate = _pptManager?.GetCurrentSlideNumber() ?? 0;
}
if (previousSlideBeforeNavigate != currentSlideAfterNavigate && previousSlideBeforeNavigate > 0)
{
if (inkCanvas.Strokes.Count > 0)
{
ClearStrokes(true);
timeMachine.ClearStrokeHistory();
_isInkClearedByButton = true;
}
if (strokesToSave != null && previousSlideBeforeNavigate > 0)
{
Task.Run(() =>
{
try
{
Application.Current.Dispatcher.Invoke(() =>
{
_singlePPTInkManager?.SaveCurrentSlideStrokes(previousSlideBeforeNavigate, strokesToSave);
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"异步保存PPT下一页墨迹失败: {ex}", LogHelper.LogType.Error);
}
});
// 异步保存截图(如果启用)
if (strokesToSave.Count > Settings.Automation.MinimumAutomationStrokeNumber &&
Settings.PowerPointSettings.IsAutoSaveScreenShotInPowerPoint)
{
Task.Run(() =>
{
try
{
Application.Current.Dispatcher.Invoke(() =>
{
var presentationName = _pptManager?.GetPresentationName() ?? "";
SaveScreenShot(true, $"{presentationName}/{previousSlideBeforeNavigate}");
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"异步保存PPT下一页截图失败: {ex}", LogHelper.LogType.Error);
}
});
}
}
}
}
else
{
@@ -1678,14 +1737,7 @@ namespace Ink_Canvas
{
Application.Current.Dispatcher.Invoke(() =>
{
if (Settings.PowerPointSettings.IsSupportWPS)
{
_singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes);
}
else
{
_multiPPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes);
}
_singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes);
timeMachine.ClearStrokeHistory();
});
}
@@ -1707,7 +1759,7 @@ namespace Ink_Canvas
LogHelper.WriteLogToFile("手动更新放映结束UI状态", LogHelper.LogType.Trace);
});
// 手动处理收纳状态恢复,因为OnPPTSlideShowEnd事件可能未触发
// 手动处理自动收纳,因为OnPPTSlideShowEnd事件可能未触发
await HandleManualSlideShowEnd();
}
@@ -1715,28 +1767,8 @@ namespace Ink_Canvas
SetCurrentToolMode(InkCanvasEditingMode.None);
await Task.Delay(150);
if (Settings.Automation.IsAutoFoldAfterPPTSlideShow)
{
if (wasFloatingBarFoldedWhenEnterSlideShow)
{
ViewboxFloatingBarMarginAnimation(-60);
}
else
{
ViewboxFloatingBarMarginAnimation(100, true);
}
}
else
{
if (isFloatingBarFolded)
{
ViewboxFloatingBarMarginAnimation(-60);
}
else
{
ViewboxFloatingBarMarginAnimation(100, true);
}
}
// PPT退出时自动收纳,使用收纳状态的边距动画
ViewboxFloatingBarMarginAnimation(-60);
}
catch (Exception ex)
{
@@ -1749,76 +1781,31 @@ namespace Ink_Canvas
_pptUIManager?.UpdateSidebarExitButtons(false);
});
// 异常情况下也手动处理收纳状态恢复
// 异常情况下也手动处理自动收纳
await HandleManualSlideShowEnd();
// 异常情况下也要根据设置决定浮动栏边距
// 异常情况下也要自动收纳,使用收纳状态的边距动画
await Task.Delay(150);
if (Settings.Automation.IsAutoFoldAfterPPTSlideShow)
{
if (wasFloatingBarFoldedWhenEnterSlideShow)
{
ViewboxFloatingBarMarginAnimation(-60);
}
else
{
ViewboxFloatingBarMarginAnimation(100, true);
}
}
else
{
if (isFloatingBarFolded)
{
ViewboxFloatingBarMarginAnimation(-60);
}
else
{
ViewboxFloatingBarMarginAnimation(100, true);
}
}
ViewboxFloatingBarMarginAnimation(-60);
}
}
/// <summary>
/// 手动处理PPT放映结束时的收纳状态恢复
/// 手动处理PPT放映结束时的自动收纳
/// </summary>
private async Task HandleManualSlideShowEnd()
{
try
{
if (Settings.Automation.IsAutoFoldAfterPPTSlideShow)
// PPT退出时自动收纳浮动栏
if (!isFloatingBarFolded)
{
if (wasFloatingBarFoldedWhenEnterSlideShow)
{
if (!isFloatingBarFolded) FoldFloatingBar_MouseUp(new object(), null);
}
else
{
if (isFloatingBarFolded) await UnFoldFloatingBar(new object());
}
}
else
{
if (Settings.Automation.IsAutoFoldInPPTSlideShow)
{
if (isFloatingBarFolded)
{
await UnFoldFloatingBar(new object());
}
}
else
{
// 如果两个功能都关闭,确保浮动栏展开
if (isFloatingBarFolded)
{
await UnFoldFloatingBar(new object());
}
}
FoldFloatingBar_MouseUp(new object(), null);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"手动处理PPT放映结束收纳状态恢复失败: {ex}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"手动处理PPT放映结束自动收纳失败: {ex}", LogHelper.LogType.Error);
}
}
@@ -7,6 +7,7 @@ using System.Drawing.Imaging;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
@@ -78,7 +79,7 @@ namespace Ink_Canvas
for (int i = 1; i <= totalSlides; i++)
{
var slideStrokes = _multiPPTInkManager?.LoadSlideStrokes(i);
var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i);
if (slideStrokes != null && slideStrokes.Count > 0)
{
allPageStrokes.Add(slideStrokes);
@@ -132,6 +133,23 @@ namespace Ink_Canvas
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePathWithName);
}
catch (Exception)
{
}
});
// 保存元素信息
var elementInfos = new List<CanvasElementInfo>();
foreach (var child in inkCanvas.Children)
@@ -228,6 +246,24 @@ namespace Ink_Canvas
// 使用System.IO.Compression.FileSystem来创建ZIP
ZipFile.CreateFromDirectory(tempDir, zipFileName);
// 异步上传ZIP文件到Dlass
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName);
}
catch (Exception)
{
}
});
if (newNotice) ShowNotification($"多页面墨迹成功保存至压缩包 {zipFileName}");
}
finally
@@ -310,6 +346,23 @@ namespace Ink_Canvas
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(imagePathWithName);
}
catch (Exception)
{
}
});
}
}
@@ -528,7 +581,7 @@ namespace Ink_Canvas
timeMachine.ClearStrokeHistory();
// 重置PPT墨迹存储
_multiPPTInkManager?.ClearAllStrokes();
_singlePPTInkManager?.ClearAllStrokes();
// 读取所有页面的墨迹文件
var files = Directory.GetFiles(tempDir, "page_*.icstk");
@@ -542,7 +595,7 @@ namespace Ink_Canvas
var strokes = new StrokeCollection(fs);
if (strokes.Count > 0)
{
_multiPPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes);
_singlePPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes);
}
}
}
@@ -552,7 +605,7 @@ namespace Ink_Canvas
if (_pptManager?.IsInSlideShow == true)
{
int currentSlide = _pptManager.GetCurrentSlideNumber();
var currentStrokes = _multiPPTInkManager?.LoadSlideStrokes(currentSlide);
var currentStrokes = _singlePPTInkManager?.LoadSlideStrokes(currentSlide);
if (currentStrokes != null && currentStrokes.Count > 0)
{
inkCanvas.Strokes.Add(currentStrokes);
+17
View File
@@ -3,6 +3,7 @@ using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
@@ -65,6 +66,22 @@ namespace Ink_Canvas
{
ShowNotification($"截图成功保存至 {savePath}");
}
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePath);
}
catch (Exception)
{
}
});
}
// 获取日期文件夹路径
@@ -354,7 +354,6 @@ namespace Ink_Canvas
private void BtnSelect_Click(object sender, RoutedEventArgs e)
{
ExitMultiTouchModeIfNeeded();
forceEraser = true;
drawingShapeMode = 0;
inkCanvas.IsManipulationEnabled = false;
@@ -709,7 +708,6 @@ namespace Ink_Canvas
private void LassoSelect_Click(object sender, RoutedEventArgs e)
{
ExitMultiTouchModeIfNeeded();
forceEraser = false;
forcePointEraser = false;
drawingShapeMode = 0;
@@ -720,7 +718,6 @@ namespace Ink_Canvas
private void BtnLassoSelect_Click(object sender, RoutedEventArgs e)
{
ExitMultiTouchModeIfNeeded();
forceEraser = false;
forcePointEraser = false;
drawingShapeMode = 0;
+132 -2
View File
@@ -1846,6 +1846,62 @@ namespace Ink_Canvas
SaveSettingsToFile();
}
private void BtnDlassSettingsManage_Click(object sender, RoutedEventArgs e)
{
if (isOpeningOrHidingSettingsPane) return;
HideSubPanels();
try
{
// 检查是否是第一次打开(检查用户是否已设置Token)
bool hasToken = !string.IsNullOrEmpty(Settings?.Dlass?.UserToken?.Trim());
bool isFirstTime = !hasToken;
if (isFirstTime)
{
// 第一次打开,询问用户是否已注册
var result = MessageBox.Show(
"您是否已经注册了Dlass账号?\n\n" +
"• 如果已注册:将直接打开设置管理页面\n" +
"• 如果未注册:将打开浏览器跳转到注册页面",
"Dlass账号注册",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.No)
{
// 用户未注册,打开浏览器
try
{
Process.Start(new ProcessStartInfo
{
FileName = "https://dlass.tech/dashboard",
UseShellExecute = true
});
LogHelper.WriteLogToFile("已打开浏览器跳转到Dlass注册页面", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"打开浏览器时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"无法打开浏览器。请手动访问: https://dlass.tech/dashboard",
"提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
return; // 不打开设置窗口
}
// 如果用户选择"是",继续打开设置窗口
}
// 打开设置管理窗口
var dlassSettingsWindow = new Windows.DlassSettingsWindow();
dlassSettingsWindow.Owner = this;
dlassSettingsWindow.ShowDialog();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"打开Dlass设置管理窗口时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"打开Dlass设置管理窗口时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ToggleSwitchAutoDelSavedFiles_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
@@ -1877,6 +1933,29 @@ namespace Ink_Canvas
SaveSettingsToFile();
}
private void ToggleSwitchEnableAutoSaveStrokes_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
Settings.Automation.IsEnableAutoSaveStrokes = ToggleSwitchEnableAutoSaveStrokes.IsOn;
SaveSettingsToFile();
// 更新定时器状态
UpdateAutoSaveStrokesTimer();
}
private void ComboBoxAutoSaveStrokesInterval_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!isLoaded || ComboBoxAutoSaveStrokesInterval.SelectedItem == null) return;
var selectedItem = ComboBoxAutoSaveStrokesInterval.SelectedItem as System.Windows.Controls.ComboBoxItem;
if (selectedItem?.Tag != null && int.TryParse(selectedItem.Tag.ToString(), out int intervalMinutes))
{
Settings.Automation.AutoSaveStrokesIntervalMinutes = intervalMinutes;
SaveSettingsToFile();
// 更新定时器间隔
UpdateAutoSaveStrokesTimer();
}
}
#endregion
#region Gesture
@@ -2453,6 +2532,14 @@ namespace Ink_Canvas
SaveSettingsToFile();
}
private void ToggleSwitchWindowMode_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
Settings.Advanced.WindowMode = ToggleSwitchWindowMode.IsOn;
SaveSettingsToFile();
SetWindowMode();
}
private void ToggleSwitchIsAutoBackupBeforeUpdate_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
@@ -2649,8 +2736,8 @@ namespace Ink_Canvas
if (ToggleSwitchEnableOvertimeRedText.IsOn && !ToggleSwitchEnableOvertimeCountUp.IsOn)
{
ToggleSwitchEnableOvertimeRedText.IsOn = false;
return;
ToggleSwitchEnableOvertimeCountUp.IsOn = true;
Settings.RandSettings.EnableOvertimeCountUp = true;
}
Settings.RandSettings.EnableOvertimeRedText = ToggleSwitchEnableOvertimeRedText.IsOn;
@@ -2671,6 +2758,35 @@ namespace Ink_Canvas
SaveSettingsToFile();
}
// 新点名UI设置事件处理
private void ToggleSwitchUseNewRollCallUI_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
Settings.RandSettings.UseNewRollCallUI = ToggleSwitchUseNewRollCallUI.IsOn;
SaveSettingsToFile();
}
private void ToggleSwitchEnableMLAvoidance_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
Settings.RandSettings.EnableMLAvoidance = ToggleSwitchEnableMLAvoidance.IsOn;
SaveSettingsToFile();
}
private void MLAvoidanceHistorySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (!isLoaded) return;
Settings.RandSettings.MLAvoidanceHistoryCount = (int)MLAvoidanceHistorySlider.Value;
SaveSettingsToFile();
}
private void MLAvoidanceWeightSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (!isLoaded) return;
Settings.RandSettings.MLAvoidanceWeight = MLAvoidanceWeightSlider.Value;
SaveSettingsToFile();
}
private void ProgressiveReminderVolumeSlider_ValueChanged(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
@@ -2740,6 +2856,20 @@ namespace Ink_Canvas
SaveSettingsToFile();
}
private void ToggleSwitchEnableQuickDraw_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
// 获取开关状态并保存到设置中
Settings.RandSettings.EnableQuickDraw = ToggleSwitchEnableQuickDraw.IsOn;
// 保存设置到文件
SaveSettingsToFile();
// 根据设置状态显示或隐藏快抽悬浮按钮
ShowQuickDrawFloatingButton();
}
private void ToggleSwitchExternalCaller_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
+27 -1
View File
@@ -592,7 +592,7 @@ namespace Ink_Canvas
ToggleSwitchEnableTwoFingerTranslate.IsOn = false;
BoardToggleSwitchEnableTwoFingerTranslate.IsOn = false;
Settings.Gesture.IsEnableTwoFingerTranslate = false;
if (!isInMultiTouchMode) ToggleSwitchEnableMultiTouchMode.IsOn = true;
// if (!isInMultiTouchMode) ToggleSwitchEnableMultiTouchMode.IsOn = true;
}
else
{
@@ -795,6 +795,7 @@ namespace Ink_Canvas
ToggleSwitchIsLogEnabled.IsOn = Settings.Advanced.IsLogEnabled;
ToggleSwitchIsSaveLogByDate.IsOn = Settings.Advanced.IsSaveLogByDate;
ToggleSwitchIsSecondConfimeWhenShutdownApp.IsOn = Settings.Advanced.IsSecondConfirmWhenShutdownApp;
ToggleSwitchWindowMode.IsOn = Settings.Advanced.WindowMode;
ToggleSwitchIsSpecialScreen.IsOn = Settings.Advanced.IsSpecialScreen;
ToggleSwitchIsQuadIR.IsOn = Settings.Advanced.IsQuadIR;
ToggleSwitchEraserBindTouchMultiplier.IsOn = Settings.Advanced.EraserBindTouchMultiplier;
@@ -876,6 +877,7 @@ namespace Ink_Canvas
RandWindowOnceCloseLatencySlider.Value = Settings.RandSettings.RandWindowOnceCloseLatency;
RandWindowOnceMaxStudentsSlider.Value = Settings.RandSettings.RandWindowOnceMaxStudents;
ToggleSwitchShowRandomAndSingleDraw.IsOn = Settings.RandSettings.ShowRandomAndSingleDraw;
ToggleSwitchEnableQuickDraw.IsOn = Settings.RandSettings.EnableQuickDraw;
ToggleSwitchExternalCaller.IsOn = Settings.RandSettings.DirectCallCiRand;
ComboBoxExternalCallerType.SelectedIndex = Settings.RandSettings.ExternalCallerType;
RandomDrawPanel.Visibility = Settings.RandSettings.ShowRandomAndSingleDraw ? Visibility.Visible : Visibility.Collapsed;
@@ -886,6 +888,12 @@ namespace Ink_Canvas
ToggleSwitchUseNewStyleUI.IsOn = Settings.RandSettings.UseNewStyleUI;
ToggleSwitchEnableOvertimeCountUp.IsOn = Settings.RandSettings.EnableOvertimeCountUp;
// 新点名UI设置
ToggleSwitchUseNewRollCallUI.IsOn = Settings.RandSettings.UseNewRollCallUI;
ToggleSwitchEnableMLAvoidance.IsOn = Settings.RandSettings.EnableMLAvoidance;
MLAvoidanceHistorySlider.Value = Settings.RandSettings.MLAvoidanceHistoryCount;
MLAvoidanceWeightSlider.Value = Settings.RandSettings.MLAvoidanceWeight;
bool canEnableRedText = Settings.RandSettings.EnableOvertimeCountUp && Settings.RandSettings.EnableOvertimeRedText;
ToggleSwitchEnableOvertimeRedText.IsOn = canEnableRedText;
if (!canEnableRedText)
@@ -915,6 +923,7 @@ namespace Ink_Canvas
ToggleSwitchDisplayRandWindowNamesInputBtn.IsOn = Settings.RandSettings.DisplayRandWindowNamesInputBtn;
RandWindowOnceCloseLatencySlider.Value = Settings.RandSettings.RandWindowOnceCloseLatency;
RandWindowOnceMaxStudentsSlider.Value = Settings.RandSettings.RandWindowOnceMaxStudents;
ToggleSwitchEnableQuickDraw.IsOn = Settings.RandSettings.EnableQuickDraw;
ToggleSwitchExternalCaller.IsOn = Settings.RandSettings.DirectCallCiRand;
ComboBoxExternalCallerType.SelectedIndex = Settings.RandSettings.ExternalCallerType;
ToggleSwitchUseLegacyTimerUI.IsOn = Settings.RandSettings.UseLegacyTimerUI;
@@ -1045,6 +1054,23 @@ namespace Ink_Canvas
ToggleSwitchSaveFullPageStrokes.IsOn = Settings.Automation.IsSaveFullPageStrokes;
// 加载定时保存墨迹设置
ToggleSwitchEnableAutoSaveStrokes.IsOn = Settings.Automation.IsEnableAutoSaveStrokes;
// 初始化保存间隔下拉框
if (ComboBoxAutoSaveStrokesInterval != null)
{
int intervalMinutes = Settings.Automation.AutoSaveStrokesIntervalMinutes;
if (intervalMinutes < 1) intervalMinutes = 5; // 默认5分钟
foreach (System.Windows.Controls.ComboBoxItem item in ComboBoxAutoSaveStrokesInterval.Items)
{
if (item.Tag != null && int.TryParse(item.Tag.ToString(), out int tagValue) && tagValue == intervalMinutes)
{
ComboBoxAutoSaveStrokesInterval.SelectedItem = item;
break;
}
}
}
SideControlMinimumAutomationSlider.Value = Settings.Automation.MinimumAutomationStrokeNumber;
AutoSavedStrokesLocation.Text = Settings.Automation.AutoSavedStrokesLocation;
File diff suppressed because it is too large Load Diff
@@ -18,21 +18,19 @@ namespace Ink_Canvas
{
private StrokeCollection newStrokes = new StrokeCollection();
private List<Circle> circles = new List<Circle>();
private const double LINE_STRAIGHTEN_THRESHOLD = 0.20; // 默认灵敏度阈值,与UI默认值对应
private const double LINE_STRAIGHTEN_THRESHOLD = 0.20;
// 矩形参考线系统
private List<RectangleGuideLine> rectangleGuideLines = new List<RectangleGuideLine>();
private const double RECTANGLE_ENDPOINT_THRESHOLD = 30.0; // 端点相交判断阈值
private const double RECTANGLE_ANGLE_THRESHOLD = 15.0; // 角度判断阈值(度)
private const double RECTANGLE_ENDPOINT_THRESHOLD = 30.0;
private const double RECTANGLE_ANGLE_THRESHOLD = 15.0;
// 矩形参考线数据结构
private class RectangleGuideLine
{
public Stroke OriginalStroke { get; set; }
public Point StartPoint { get; set; }
public Point EndPoint { get; set; }
public DateTime CreatedTime { get; set; }
public double Angle { get; set; } // 直线角度(弧度)
public double Angle { get; set; }
public bool IsHorizontal { get; set; }
public bool IsVertical { get; set; }
@@ -64,7 +62,6 @@ namespace Ink_Canvas
var startPoint = e.Stroke.StylusPoints.Count > 0 ? e.Stroke.StylusPoints[0].ToPoint() : new Point();
var endPoint = e.Stroke.StylusPoints.Count > 0 ? e.Stroke.StylusPoints[e.Stroke.StylusPoints.Count - 1].ToPoint() : new Point();
// 确保InkCanvas保持Ink编辑模式,防止自动切换到鼠标模式
if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
@@ -80,19 +77,15 @@ namespace Ink_Canvas
LogHelper.WriteLogToFile("StrokeCollected: 墨迹渐隐管理器为空,无法添加墨迹", LogHelper.LogType.Error);
}
// 延迟移除墨迹,避免立即移除导致模式切换
// 使用Dispatcher.BeginInvoke确保在UI线程上异步执行
Dispatcher.BeginInvoke(new Action(() =>
{
try
{
// 再次确保InkCanvas保持Ink编辑模式
if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
// 从InkCanvas中移除墨迹,因为我们要用渐隐管理器来管理它
if (inkCanvas.Strokes.Contains(e.Stroke))
{
inkCanvas.Strokes.Remove(e.Stroke);
@@ -104,21 +97,18 @@ namespace Ink_Canvas
}
}), DispatcherPriority.Background);
// 墨迹渐隐模式下不参与墨迹纠正和其他处理,直接返回
return;
}
// 标记是否进行了直线拉直
bool wasStraightened = false;
// 禁用原有的FitToCurve,使用新的高级贝塞尔曲线平滑
if (Settings.Canvas.FitToCurve) drawingAttributes.FitToCurve = false;
try
{
inkCanvas.Opacity = 1;
// 应用屏蔽压感功能 - 如果启用,所有笔画都使用统一粗细
if (Settings.Canvas.DisablePressure)
{
var uniformPoints = new StylusPointCollection();
@@ -129,13 +119,11 @@ namespace Ink_Canvas
}
e.Stroke.StylusPoints = uniformPoints;
}
// 应用压感触屏模式 - 如果启用并且检测到触屏输入
else if (Settings.Canvas.EnablePressureTouchMode)
{
bool isTouchInput = true;
foreach (StylusPoint point in e.Stroke.StylusPoints)
{
// 检测是否为压感笔输入(压感笔的PressureFactor不等于0.5或0
if ((point.PressureFactor > 0.501 || point.PressureFactor < 0.5) && point.PressureFactor != 0)
{
isTouchInput = false;
@@ -143,7 +131,6 @@ namespace Ink_Canvas
}
}
// 如果是触屏输入,则应用模拟压感
if (isTouchInput)
{
switch (Settings.Canvas.InkStyle)
@@ -236,39 +223,30 @@ namespace Ink_Canvas
// 检查是否启用了直线自动拉直功能
if (Settings.Canvas.AutoStraightenLine && IsPotentialStraightLine(e.Stroke))
{
// Get start and end points of the stroke
Point startPoint = e.Stroke.StylusPoints[0].ToPoint();
Point endPoint = e.Stroke.StylusPoints[e.Stroke.StylusPoints.Count - 1].ToPoint();
Point endpoint1, endpoint2;
bool shouldStraighten = TryGetStraightLineEndpoints(e.Stroke, out endpoint1, out endpoint2);
// 先完成所有直线判定,再考虑端点吸附
// 读取实际的灵敏度设置值
double sensitivity = Settings.InkToShape.LineStraightenSensitivity;
Debug.WriteLine($"当前灵敏度值: {sensitivity}");
// 判断是否应该拉直线条
bool shouldStraighten = ShouldStraightenLine(e.Stroke);
// 输出一些调试信息,帮助理解灵敏度设置的效果
Debug.WriteLine($"LineStraightenSensitivity: {Settings.InkToShape.LineStraightenSensitivity}, ShouldStraighten: {shouldStraighten}");
// 只有当确定要拉直线条时,才检查端点吸附
if (shouldStraighten && Settings.Canvas.LineEndpointSnapping)
{
// 只有在启用了形状识别(矩形或三角形)时才执行端点吸附
if (Settings.InkToShape.IsInkToShapeRectangle || Settings.InkToShape.IsInkToShapeTriangle)
{
Point[] snappedPoints = GetSnappedEndpoints(startPoint, endPoint);
if (snappedPoints != null)
{
startPoint = snappedPoints[0];
endPoint = snappedPoints[1];
}
}
}
// 如果确定要拉直,则创建直线
if (shouldStraighten)
{
Point startPoint = endpoint1;
Point endPoint = endpoint2;
// 只有当确定要拉直线条时,才检查端点吸附
if (Settings.Canvas.LineEndpointSnapping)
{
// 只有在启用了形状识别(矩形或三角形)时才执行端点吸附
if (Settings.InkToShape.IsInkToShapeRectangle || Settings.InkToShape.IsInkToShapeTriangle)
{
Point[] snappedPoints = GetSnappedEndpoints(startPoint, endPoint);
if (snappedPoints != null)
{
startPoint = snappedPoints[0];
endPoint = snappedPoints[1];
}
}
}
// 创建直线
StylusPointCollection straightLinePoints = CreateStraightLine(startPoint, endPoint);
Stroke straightStroke = new Stroke(straightLinePoints)
{
@@ -857,7 +835,7 @@ namespace Ink_Canvas
// 获取用户设置的灵敏度值,确保使用正确的设置
double sensitivity = Settings.InkToShape.LineStraightenSensitivity;
// 输出当前灵敏度值(调试用)
// 输出当前灵敏度值
Debug.WriteLine($"IsPotentialStraightLine - sensitivity: {sensitivity}, length: {lineLength}");
// 将灵敏度转换为阈值:灵敏度0.05-2.0映射到阈值0.01-0.4
@@ -868,32 +846,61 @@ namespace Ink_Canvas
// 快速检查:计算几个关键点与直线的距离
if (stroke.StylusPoints.Count >= 10)
{
// 取中点和1/4、3/4位置的点,快速检查偏差
int quarterIdx = stroke.StylusPoints.Count / 4;
int midIdx = stroke.StylusPoints.Count / 2;
int threeQuarterIdx = quarterIdx * 3;
Point quarterPoint = stroke.StylusPoints[quarterIdx].ToPoint();
Point midPoint = stroke.StylusPoints[midIdx].ToPoint();
Point threeQuarterPoint = stroke.StylusPoints[threeQuarterIdx].ToPoint();
double quarterDeviation = DistanceFromLineToPoint(start, end, quarterPoint);
double midDeviation = DistanceFromLineToPoint(start, end, midPoint);
double threeQuarterDeviation = DistanceFromLineToPoint(start, end, threeQuarterPoint);
// 使用相对偏差:偏差与线长的比例,并使用灵敏度进行调整
double quickRelativeThreshold = lineLength * quickThreshold;
// 记录检测到的偏差(调试用)
Debug.WriteLine($"Deviations: q={quarterDeviation}, m={midDeviation}, tq={threeQuarterDeviation}, threshold={quickRelativeThreshold}");
// 修复后的逻辑:灵敏度越大,容许的偏差越大
// 如果任一点偏离太大,直接排除(使用统一的判断标准)
if (quarterDeviation > quickRelativeThreshold ||
midDeviation > quickRelativeThreshold ||
threeQuarterDeviation > quickRelativeThreshold)
List<Point> checkPoints;
// 使用采样点进行更准确的判断
if (Settings.Canvas.HighPrecisionLineStraighten)
{
return false;
var allPoints = stroke.StylusPoints.Select(p => p.ToPoint()).ToList();
checkPoints = SamplePointsByDistance(allPoints, 10.0);
Debug.WriteLine($"高精度模式快速检查:原始点数={allPoints.Count}, 采样点数={checkPoints.Count}");
}
else
{
// 取中点和1/4、3/4位置的点
int quarterIdx = stroke.StylusPoints.Count / 4;
int midIdx = stroke.StylusPoints.Count / 2;
int threeQuarterIdx = quarterIdx * 3;
checkPoints = new List<Point>
{
stroke.StylusPoints[quarterIdx].ToPoint(),
stroke.StylusPoints[midIdx].ToPoint(),
stroke.StylusPoints[threeQuarterIdx].ToPoint()
};
}
// 计算所有检查点与直线的平均偏差
double totalDeviation = 0;
double maxDeviation = 0;
int validPointCount = 0;
foreach (Point checkPoint in checkPoints)
{
double deviation = DistanceFromLineToPoint(start, end, checkPoint);
totalDeviation += deviation;
maxDeviation = Math.Max(maxDeviation, deviation);
validPointCount++;
}
if (validPointCount > 0)
{
double avgDeviation = totalDeviation / validPointCount;
// 使用相对偏差:偏差与线长的比例,并使用灵敏度进行调整
double quickRelativeThreshold = lineLength * quickThreshold;
// 使用平均偏差和最大偏差的综合判断
double deviationThreshold = Settings.Canvas.HighPrecisionLineStraighten
? Math.Max(avgDeviation, maxDeviation * 0.7) // 高精度模式更严格
: maxDeviation;
// 记录检测到的偏差
Debug.WriteLine($"Deviations: avg={avgDeviation:F2}, max={maxDeviation:F2}, threshold={quickRelativeThreshold:F2}, highPrecision={Settings.Canvas.HighPrecisionLineStraighten}");
if (deviationThreshold > quickRelativeThreshold)
{
return false;
}
}
}
@@ -901,7 +908,7 @@ namespace Ink_Canvas
}
/// <summary>
/// 检查墨迹是否为复杂形状(如一团墨迹、涂鸦等)
/// 检查墨迹是否为复杂形状
/// </summary>
private bool IsComplexShape(Stroke stroke)
{
@@ -1177,291 +1184,160 @@ namespace Ink_Canvas
lineEnd.X * lineStart.Y - lineEnd.Y * lineStart.X) / lineLength;
}
// New method: Determines if a stroke should be straightened into a line
private bool TryGetStraightLineEndpoints(Stroke stroke, out Point endpoint1, out Point endpoint2)
{
endpoint1 = new Point();
endpoint2 = new Point();
var points = stroke.StylusPoints.Select(p => p.ToPoint()).ToList();
if (points.Count < 10)
{
return false;
}
List<Point> workingPoints = points;
if (Settings.Canvas.HighPrecisionLineStraighten)
{
workingPoints = SamplePointsByDistance(points, 10.0);
Debug.WriteLine($"高精度模式:原始点数={points.Count}, 采样后点数={workingPoints.Count}");
}
// 使用总最小二乘法(TLS/PCA)进行直线拟合
int n = workingPoints.Count - 8;
if (n < 1)
{
// 如果采样后点数太少,回退到原始方法
n = points.Count - 8;
workingPoints = points;
}
List<Point> filteredPoints = new List<Point>();
// 收集过滤后的点(跳过前 4 个和后 4 个点,用于计算直线方向)
int skipCount = Math.Min(4, n / 2); // 确保跳过数量不超过一半
for (int i = skipCount; i < n + skipCount && i < workingPoints.Count; i++)
{
filteredPoints.Add(workingPoints[i]);
}
// 计算中心点(使用过滤后的点)
double centerX = 0, centerY = 0;
foreach (Point p in filteredPoints)
{
centerX += p.X;
centerY += p.Y;
}
centerX /= filteredPoints.Count;
centerY /= filteredPoints.Count;
// 计算协方差矩阵(使用过滤后的点)
double covXX = 0, covYY = 0, covXY = 0;
foreach (Point p in filteredPoints)
{
double dx = p.X - centerX;
double dy = p.Y - centerY;
covXX += dx * dx;
covYY += dy * dy;
covXY += dx * dy;
}
// 计算特征值和特征向量
double trace = covXX + covYY;
double determinant = covXX * covYY - covXY * covXY;
double discriminant = Math.Sqrt(trace * trace - 4 * determinant);
double eigenvalue1 = (trace + discriminant) / 2;
double eigenvalue2 = (trace - discriminant) / 2;
// 最大特征值对应的特征向量即为直线方向
double directionX, directionY;
if (Math.Abs(covXY) > 1e-10)
{
directionX = covXY;
directionY = eigenvalue1 - covXX;
// 归一化
double length = Math.Sqrt(directionX * directionX + directionY * directionY);
directionX /= length;
directionY /= length;
}
else
{
// 如果协方差为 0,则是水平或垂直直线
directionX = (covXX >= covYY) ? 1 : 0;
directionY = (covXX >= covYY) ? 0 : 1;
}
// 计算解释方差比例(拟合优度)
double totalVariance = eigenvalue1 + eigenvalue2;
double explainedVarianceRatio = (totalVariance > 1e-10) ?
Math.Max(eigenvalue1, eigenvalue2) / totalVariance : 1d;
// 使用所有点计算端点
double minProjection = double.MaxValue;
double maxProjection = double.MinValue;
// 计算所有点在直线方向上的投影
List<Point> pointsForProjection = Settings.Canvas.HighPrecisionLineStraighten ? workingPoints : points;
foreach (Point p in pointsForProjection)
{
// 相对于过滤点中心的投影
double projection = (p.X - centerX) * directionX + (p.Y - centerY) * directionY;
minProjection = Math.Min(minProjection, projection);
maxProjection = Math.Max(maxProjection, projection);
}
// 计算端点坐标
endpoint1 = new Point(
centerX + minProjection * directionX,
centerY + minProjection * directionY
);
endpoint2 = new Point(
centerX + maxProjection * directionX,
centerY + maxProjection * directionY
);
// 使用解释方差比例作为判断条件
double threshold = 0.998 + Settings.InkToShape.LineNormalizationThreshold / 500;
return explainedVarianceRatio > threshold;
}
// New method: Determines if a stroke should be straightened into a line
private bool ShouldStraightenLine(Stroke stroke)
{
// 分辨率自适应阈值
Point start = stroke.StylusPoints.First().ToPoint();
Point end = stroke.StylusPoints.Last().ToPoint();
double maxDeviation = 0;
double lineLength = GetDistance(start, end);
// 分辨率自适应阈值
double adaptiveThreshold = Settings.Canvas.AutoStraightenLineThreshold * GetResolutionScale();
// 如果线条太短,不进行拉直处理,使用自适应阈值
// 如果线条太短,不进行拉直处理
if (lineLength < adaptiveThreshold)
{
Debug.WriteLine($"线条太短: {lineLength} < {adaptiveThreshold}");
return false;
}
// 新增:再次检查复杂度(双重保险)
// 检查复杂度
if (IsComplexShape(stroke))
{
Debug.WriteLine("拒绝拉直:检测到复杂形状");
return false;
}
// 新增:检查线条的直线度评分
double straightnessScore = CalculateStraightnessScore(stroke);
double minStraightnessThreshold = 0.7; // 最低直线度要求
if (straightnessScore < minStraightnessThreshold)
Point endpoint1, endpoint2;
bool shouldStraighten = TryGetStraightLineEndpoints(stroke, out endpoint1, out endpoint2);
if (shouldStraighten)
{
Debug.WriteLine($"拒绝拉直:直线度评分过低 {straightnessScore:F3} < {minStraightnessThreshold}");
return false;
}
// 获取用户设置的灵敏度值,确保使用正确的值进行后续判断
double sensitivity = Settings.InkToShape.LineStraightenSensitivity;
// 输出详细的调试信息
Debug.WriteLine($"ShouldStraightenLine - sensitivity: {sensitivity}, length: {lineLength}");
// 临时:显示调试消息框
// MessageBox.Show($"灵敏度值: {sensitivity}", "调试信息");
// 计算点与直线的偏差
double totalDeviation = 0;
int pointCount = 0;
// 检查是否启用了高精度直线拉直
bool useHighPrecision = Settings.Canvas.HighPrecisionLineStraighten;
if (useHighPrecision)
{
Debug.WriteLine("使用高精度直线拉直模式");
// 高精度模式:每隔10像素取一个计数点
double strokeLength = 0;
double sampleInterval = 10.0; // 10像素间隔
// 计算笔画的总长度,用于后续采样
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point p1 = stroke.StylusPoints[i - 1].ToPoint();
Point p2 = stroke.StylusPoints[i].ToPoint();
strokeLength += GetDistance(p1, p2);
}
// 如果笔画太短,直接使用所有点
if (strokeLength < sampleInterval * 5)
{
foreach (StylusPoint sp in stroke.StylusPoints)
{
Point p = sp.ToPoint();
double deviation = DistanceFromLineToPoint(start, end, p);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
}
}
else
{
// 使用等距采样点
double currentLength = 0;
double nextSampleAt = 0;
// 总是包含起点
Point lastPoint = start;
double deviation = DistanceFromLineToPoint(start, end, lastPoint);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
// 采样中间点
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point currentPoint = stroke.StylusPoints[i].ToPoint();
double segmentLength = GetDistance(lastPoint, currentPoint);
// 如果这段线段跨越了下一个采样点
while (currentLength + segmentLength >= nextSampleAt)
{
// 计算采样点在线段上的位置
double t = (nextSampleAt - currentLength) / segmentLength;
Point samplePoint = new Point(
lastPoint.X + t * (currentPoint.X - lastPoint.X),
lastPoint.Y + t * (currentPoint.Y - lastPoint.Y)
);
// 计算采样点的偏差
deviation = DistanceFromLineToPoint(start, end, samplePoint);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
// 设置下一个采样点位置
nextSampleAt += sampleInterval;
// 防止无限循环
if (nextSampleAt > strokeLength) break;
}
currentLength += segmentLength;
lastPoint = currentPoint;
}
// 总是包含终点
deviation = DistanceFromLineToPoint(start, end, end);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
}
Debug.WriteLine($"接受拉直:判断为直线,解释方差比例满足阈值");
}
else
{
// 原始模式:使用所有点
foreach (StylusPoint sp in stroke.StylusPoints)
{
Point p = sp.ToPoint();
double deviation = DistanceFromLineToPoint(start, end, p);
maxDeviation = Math.Max(maxDeviation, deviation);
totalDeviation += deviation;
pointCount++;
}
Debug.WriteLine($"拒绝拉直:判断不满足直线条件");
}
// 计算平均偏差
double avgDeviation = totalDeviation / pointCount;
// 更详细的调试信息
Debug.WriteLine($"Max deviation: {maxDeviation}, Avg: {avgDeviation}, Threshold: {sensitivity * lineLength}, Points: {pointCount}");
// 支持更广泛的灵敏度范围 (0.05-2.0)
// 移除特殊的高灵敏度模式,使用统一的阈值计算逻辑
// 检查点分布的一致性 - 如果有些点偏离很大而其他点很接近直线,表明线条有明显弯曲
double deviationVariance = 0;
// 使用相同的高精度/原始模式来计算方差
if (useHighPrecision)
{
// 高精度模式:重新采样计算方差
double strokeLength = 0;
double sampleInterval = 10.0; // 10像素间隔
// 计算笔画的总长度,用于后续采样
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point p1 = stroke.StylusPoints[i - 1].ToPoint();
Point p2 = stroke.StylusPoints[i].ToPoint();
strokeLength += GetDistance(p1, p2);
}
// 如果笔画太短,直接使用所有点
if (strokeLength < sampleInterval * 5)
{
foreach (StylusPoint sp in stroke.StylusPoints)
{
Point p = sp.ToPoint();
double deviation = DistanceFromLineToPoint(start, end, p);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
}
}
else
{
// 使用等距采样点
double currentLength = 0;
double nextSampleAt = 0;
Point lastPoint = start;
// 起点方差
double deviation = DistanceFromLineToPoint(start, end, lastPoint);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
// 采样中间点
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point currentPoint = stroke.StylusPoints[i].ToPoint();
double segmentLength = GetDistance(lastPoint, currentPoint);
// 如果这段线段跨越了下一个采样点
while (currentLength + segmentLength >= nextSampleAt)
{
// 计算采样点在线段上的位置
double t = (nextSampleAt - currentLength) / segmentLength;
Point samplePoint = new Point(
lastPoint.X + t * (currentPoint.X - lastPoint.X),
lastPoint.Y + t * (currentPoint.Y - lastPoint.Y)
);
// 计算采样点的方差
deviation = DistanceFromLineToPoint(start, end, samplePoint);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
// 设置下一个采样点位置
nextSampleAt += sampleInterval;
// 防止无限循环
if (nextSampleAt > strokeLength) break;
}
currentLength += segmentLength;
lastPoint = currentPoint;
}
// 终点方差
deviation = DistanceFromLineToPoint(start, end, end);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
}
}
else
{
// 原始模式:使用所有点计算方差
foreach (StylusPoint sp in stroke.StylusPoints)
{
Point p = sp.ToPoint();
double deviation = DistanceFromLineToPoint(start, end, p);
deviationVariance += Math.Pow(deviation - avgDeviation, 2);
}
}
deviationVariance /= pointCount;
// 输出更多调试信息
Debug.WriteLine($"Deviation variance: {deviationVariance}, Threshold: {sensitivity * lineLength * 0.05}");
// 修复灵敏度逻辑:灵敏度越大,容许的偏差越大,更容易将线条识别为直线
// 将灵敏度转换为阈值:灵敏度0.05-1.0映射到阈值0.01-0.2
double threshold = Math.Max(0.01, sensitivity * 0.2); // 确保最小阈值为0.01
if ((maxDeviation / lineLength) > threshold)
{
Debug.WriteLine($"拒绝拉直:最大偏差过大 {maxDeviation / lineLength:F3} > {threshold:F3}");
return false;
}
// 如果偏差方差大,说明线条弯曲不均匀
// 灵敏度越大,容许的偏差方差越大
double varianceThreshold = threshold * lineLength * 0.25; // 调整方差阈值比例
if (deviationVariance > varianceThreshold)
{
Debug.WriteLine($"拒绝拉直:偏差方差过大 {deviationVariance:F3} > {varianceThreshold:F3}");
return false;
}
// 检查中点偏离情况 - 针对弧形线条特别有效
if (stroke.StylusPoints.Count > 10)
{
int midIndex = stroke.StylusPoints.Count / 2;
Point midPoint = stroke.StylusPoints[midIndex].ToPoint();
double midDeviation = DistanceFromLineToPoint(start, end, midPoint);
// 输出中点偏差信息
double midThreshold = lineLength * threshold * 0.8;
Debug.WriteLine($"Mid deviation: {midDeviation:F3}, Threshold: {midThreshold:F3}");
// 如果中点偏离过大,不拉直
// 使用调整后的阈值,灵敏度越大,容许的中点偏离越大
if (midDeviation > midThreshold)
{
Debug.WriteLine($"拒绝拉直:中点偏差过大 {midDeviation:F3} > {midThreshold:F3}");
return false;
}
}
Debug.WriteLine($"接受拉直:直线度评分 = {straightnessScore:F3}");
return true;
return shouldStraighten;
}
/// <summary>
@@ -1611,6 +1487,43 @@ namespace Ink_Canvas
return points;
}
/// <summary>
/// 高精度模式
/// </summary>
private List<Point> SamplePointsByDistance(List<Point> points, double sampleInterval = 10.0)
{
if (points == null || points.Count < 2)
return points;
List<Point> sampledPoints = new List<Point>();
sampledPoints.Add(points[0]); // 总是包含起点
double accumulatedDistance = 0;
Point lastSampledPoint = points[0];
for (int i = 1; i < points.Count; i++)
{
double segmentDistance = GetDistance(lastSampledPoint, points[i]);
accumulatedDistance += segmentDistance;
// 当累积距离达到采样间隔时,添加当前点
if (accumulatedDistance >= sampleInterval)
{
sampledPoints.Add(points[i]);
lastSampledPoint = points[i];
accumulatedDistance = 0; // 重置累积距离
}
}
// 总是包含终点(如果还没有包含)
if (sampledPoints.Count == 0 || GetDistance(sampledPoints.Last(), points.Last()) > 1.0)
{
sampledPoints.Add(points.Last());
}
return sampledPoints;
}
// New method: Gets distance from point to a line defined by two points
private double DistanceFromLineToPoint(Point lineStart, Point lineEnd, Point point)
{
@@ -1625,11 +1538,66 @@ namespace Ink_Canvas
return distance;
}
/// <summary>
/// 判断一个 stroke 是否是直线(排除虚线和点线)
/// </summary>
/// <param name="stroke">要检查的 stroke</param>
/// <returns>如果是直线返回 true,否则返回 false</returns>
private bool IsStraightLine(Stroke stroke)
{
if (stroke == null || stroke.StylusPoints.Count == 0)
return false;
int pointCount = stroke.StylusPoints.Count;
if (pointCount == 1)
return false;
// 最简单的直线:只有2个点
if (pointCount == 2)
{
Point p1 = stroke.StylusPoints[0].ToPoint();
Point p2 = stroke.StylusPoints[1].ToPoint();
double lineLength = GetDistance(p1, p2);
if (lineLength < 10)
return false;
return true;
}
if (pointCount > 3)
return false;
// 对于3个点的情况,检查它们是否基本在一条直线上
if (pointCount == 3)
{
Point p1 = stroke.StylusPoints[0].ToPoint();
Point p2 = stroke.StylusPoints[1].ToPoint();
Point p3 = stroke.StylusPoints[2].ToPoint();
double totalLength = GetDistance(p1, p3);
if (totalLength < 10)
return false;
// 计算点到直线的距离
// 使用 p1 和 p3 作为直线端点,检查 p2 是否在这条直线上
double distance = DistanceFromLineToPoint(p1, p3, p2);
// 如果点到直线的距离相对于线段长度很小,认为是直线
// 使用相对误差阈值(比如 1%
if (totalLength > 0 && distance / totalLength < 0.01)
return true;
return false;
}
return false;
}
// New method: Attempts to snap endpoints to existing stroke endpoints
private Point[] GetSnappedEndpoints(Point start, Point end)
{
// 如果端点吸附功能关闭,直接返回null
// 这里不再返回原始点,因为调用此方法的地方会判断返回值是否为null
if (!Settings.Canvas.LineEndpointSnapping)
return null;
@@ -1646,6 +1614,10 @@ namespace Ink_Canvas
{
if (stroke.StylusPoints.Count == 0) continue;
// 只对直线进行端点吸附,跳过虚线和点线
if (!IsStraightLine(stroke))
continue;
// Get stroke endpoints
Point strokeStart = stroke.StylusPoints.First().ToPoint();
Point strokeEnd = stroke.StylusPoints.Last().ToPoint();
+50
View File
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
namespace Ink_Canvas
{
@@ -148,6 +149,55 @@ namespace Ink_Canvas
LogHelper.WriteLogToFile($"程序启动时NTP同步失败: {ex.Message}", LogHelper.LogType.Error);
}
});
// 初始化定时保存墨迹定时器
InitAutoSaveStrokesTimer();
}
// 初始化定时保存墨迹定时器
private void InitAutoSaveStrokesTimer()
{
if (autoSaveStrokesTimer == null)
{
autoSaveStrokesTimer = new DispatcherTimer();
autoSaveStrokesTimer.Tick += AutoSaveStrokesTimer_Tick;
}
// 根据设置更新定时器间隔和启动状态
UpdateAutoSaveStrokesTimer();
}
// 更新定时保存墨迹定时器状态
private void UpdateAutoSaveStrokesTimer()
{
if (autoSaveStrokesTimer == null) return;
autoSaveStrokesTimer.Stop();
if (Settings.Automation.IsEnableAutoSaveStrokes)
{
int intervalMinutes = Settings.Automation.AutoSaveStrokesIntervalMinutes;
if (intervalMinutes < 1) intervalMinutes = 1; // 最小间隔1分钟
autoSaveStrokesTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
autoSaveStrokesTimer.Start();
}
}
// 定时保存墨迹定时器事件处理
private void AutoSaveStrokesTimer_Tick(object sender, EventArgs e)
{
try
{
// 只有在画布可见且有墨迹时才保存
if (inkCanvas.Visibility == Visibility.Visible && inkCanvas.Strokes.Count > 0)
{
// 静默保存
SaveInkCanvasStrokes(false, false);
}
}
catch (Exception)
{
}
}
// NTP同步定时器事件处理
+86 -450
View File
@@ -325,7 +325,7 @@ namespace Ink_Canvas
// 根据当前编辑模式设置不同的光标
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint)
{
inkCanvas.Cursor = Cursors.Cross;
inkCanvas.Cursor = Cursors.Arrow;
}
else if (inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
{
@@ -391,12 +391,20 @@ namespace Ink_Canvas
{
var stroke = GetStrokeVisual(e.StylusDevice.Id).Stroke;
inkCanvas.Strokes.Add(stroke);
await Task.Delay(5);
inkCanvas.Children.Remove(GetVisualCanvas(e.StylusDevice.Id));
if (stroke != null)
{
inkCanvas.Strokes.Add(stroke);
await Task.Delay(5);
inkCanvas.Children.Remove(GetVisualCanvas(e.StylusDevice.Id));
inkCanvas_StrokeCollected(inkCanvas,
new InkCanvasStrokeCollectedEventArgs(stroke));
inkCanvas_StrokeCollected(inkCanvas,
new InkCanvasStrokeCollectedEventArgs(stroke));
}
else
{
await Task.Delay(5);
inkCanvas.Children.Remove(GetVisualCanvas(e.StylusDevice.Id));
}
}
catch (Exception ex)
{
@@ -525,13 +533,6 @@ namespace Ink_Canvas
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint)
{
// 橡皮状态下只return,保证橡皮状态可保持
return;
}
if (inkCanvas.EditingMode == InkCanvasEditingMode.Select)
{
// 套索选状态下不直接return,允许触摸事件继续处理
dec.Add(e.TouchDevice.Id);
return;
}
if (drawingShapeMode != 0)
@@ -548,6 +549,11 @@ namespace Ink_Canvas
return;
}
if (inkCanvas.EditingMode == InkCanvasEditingMode.Select)
{
dec.Add(e.TouchDevice.Id);
return;
}
if (inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
{
return;
@@ -563,171 +569,30 @@ namespace Ink_Canvas
}
}
// 手掌擦相关变量
private bool isPalmEraserActive;
private InkCanvasEditingMode palmEraserLastEditingMode = InkCanvasEditingMode.Ink;
private bool palmEraserLastIsHighlighter;
private bool palmEraserWasEnabledBeforeMultiTouch;
public double GetTouchBoundWidth(TouchEventArgs e)
{
var args = e.GetTouchPoint(null).Bounds;
if (!Settings.Advanced.IsQuadIR) return args.Width;
else return Math.Sqrt(args.Width * args.Height); // 四边红外
double value;
if (!Settings.Advanced.IsQuadIR) value = args.Width;
else value = Math.Sqrt(args.Width * args.Height); //四边红外
if (Settings.Advanced.IsSpecialScreen) value *= Settings.Advanced.TouchMultiplier;
return value;
}
private void inkCanvas_PreviewTouchDown(object sender, TouchEventArgs e)
{
// 检查触摸是否发生在浮动栏区域,如果是则允许事件传播到浮动栏按钮
var touchPoint = e.GetTouchPoint(this);
var floatingBarBounds = ViewboxFloatingBar.TransformToAncestor(this).TransformBounds(
new Rect(0, 0, ViewboxFloatingBar.ActualWidth, ViewboxFloatingBar.ActualHeight));
// 如果触摸发生在浮动栏区域,不阻止事件传播,让浮动栏按钮能够接收触摸事件
if (floatingBarBounds.Contains(touchPoint.Position))
{
// 不设置 ViewboxFloatingBar.IsHitTestVisible = false,让浮动栏按钮能够接收触摸事件
return;
}
// 橡皮状态下不做任何切换,直接return,保证橡皮可持续
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint
|| inkCanvas.EditingMode == InkCanvasEditingMode.EraseByStroke)
{
return;
}
if (drawingShapeMode != 0)
{
inkCanvas.EditingMode = InkCanvasEditingMode.None;
SetCursorBasedOnEditingMode(inkCanvas);
inkCanvas.CaptureTouch(e.TouchDevice);
ViewboxFloatingBar.IsHitTestVisible = false;
BlackboardUIGridForInkReplay.IsHitTestVisible = false;
isTouchDown = true;
if (dec.Count == 0)
{
var inkTouchPoint = e.GetTouchPoint(inkCanvas);
// 对于双曲线绘制,第一笔时记录起点,第二笔时不更新起点
if (drawingShapeMode == 24 || drawingShapeMode == 25)
{
// 双曲线绘制:第一笔记录起点,第二笔保持第一笔的起点
if (drawMultiStepShapeCurrentStep == 0)
{
iniP = inkTouchPoint.Position;
}
// 第二笔时不更新iniP,保持第一笔的起点
}
else
{
// 其他图形正常记录起点
iniP = inkTouchPoint.Position;
}
lastTouchDownStrokeCollection = inkCanvas.Strokes.Clone();
}
dec.Add(e.TouchDevice.Id);
return;
}
// 非几何绘制模式下的正常触摸处理
SetCursorBasedOnEditingMode(inkCanvas);
inkCanvas.CaptureTouch(e.TouchDevice);
ViewboxFloatingBar.IsHitTestVisible = false;
BlackboardUIGridForInkReplay.IsHitTestVisible = false;
lastTouchDownTime = DateTime.Now;
dec.Add(e.TouchDevice.Id);
// Palm Eraser 逻辑
if (Settings.Canvas.EnablePalmEraser && !isPalmEraserActive)
{
touchPoint = e.GetTouchPoint(inkCanvas);
double boundWidth = GetTouchBoundWidth(e);
if ((Settings.Advanced.TouchMultiplier != 0 || !Settings.Advanced.IsSpecialScreen)
&& (boundWidth > BoundsWidth))
{
// 根据敏感度调整阈值倍数
double thresholdMultiplier;
switch (Settings.Canvas.PalmEraserSensitivity)
{
case 0: // 低敏感度
thresholdMultiplier = 3.0;
break;
case 1: // 中敏感度
thresholdMultiplier = 2.5;
break;
case 2: // 高敏感度
default:
thresholdMultiplier = 2.0;
break;
}
double EraserThresholdValue = Settings.Startup.IsEnableNibMode ?
Settings.Advanced.NibModeBoundsWidthThresholdValue :
Settings.Advanced.FingerModeBoundsWidthThresholdValue;
if (boundWidth > BoundsWidth * EraserThresholdValue * thresholdMultiplier)
{
// 记录当前编辑模式和高光状态
palmEraserLastEditingMode = inkCanvas.EditingMode;
palmEraserLastIsHighlighter = drawingAttributes.IsHighlighter;
// 动态调整橡皮大小
boundWidth *= (Settings.Startup.IsEnableNibMode ?
Settings.Advanced.NibModeBoundsWidthEraserSize :
Settings.Advanced.FingerModeBoundsWidthEraserSize);
if (Settings.Advanced.IsSpecialScreen)
boundWidth *= Settings.Advanced.TouchMultiplier;
inkCanvas.EditingMode = InkCanvasEditingMode.EraseByPoint;
isPalmEraserActive = true;
// 启用橡皮擦覆盖层显示手掌擦样式
EnableEraserOverlay();
// 更新橡皮擦大小以匹配手掌擦面积
eraserWidth = boundWidth;
UpdateEraserStyle();
// 显示初始橡皮擦反馈位置
touchPoint = e.GetTouchPoint(inkCanvas);
EraserOverlay_PointerDown(sender);
EraserOverlay_PointerMove(sender, touchPoint.Position);
if (Settings.Canvas.IsShowCursor)
{
inkCanvas.ForceCursor = false;
inkCanvas.UseCustomCursor = false;
}
}
}
}
// 设备1个的时候,记录中心点
//设备1个的时候,记录中心点
if (dec.Count == 1)
{
touchPoint = e.GetTouchPoint(inkCanvas);
var touchPoint = e.GetTouchPoint(inkCanvas);
centerPoint = touchPoint.Position;
if (drawingShapeMode != 0)
{
// 对于双曲线绘制,第一笔时记录起点,第二笔时不更新起点
if (drawingShapeMode == 24 || drawingShapeMode == 25)
{
// 双曲线绘制:第一笔记录起点,第二笔保持第一笔的起点
if (drawMultiStepShapeCurrentStep == 0)
{
iniP = touchPoint.Position;
}
// 第二笔时不更新iniP,保持第一笔的起点
}
else
{
// 其他图形正常记录起点
iniP = touchPoint.Position;
}
}
// 记录第一根手指点击时的 StrokeCollection
//记录第一根手指点击时的 StrokeCollection
lastTouchDownStrokeCollection = inkCanvas.Strokes.Clone();
}
//设备两个及两个以上,将画笔功能关闭
@@ -736,61 +601,25 @@ namespace Ink_Canvas
if (isInMultiTouchMode || !Settings.Gesture.IsEnableTwoFingerGesture) return;
if (inkCanvas.EditingMode == InkCanvasEditingMode.None ||
inkCanvas.EditingMode == InkCanvasEditingMode.Select) return;
var timeSinceLastTouch = (DateTime.Now - lastTouchDownTime).TotalMilliseconds;
if (timeSinceLastTouch < MULTI_TOUCH_DELAY_MS && inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
{
if (!isMultiTouchTimerActive)
{
isMultiTouchTimerActive = true;
var remainingTime = MULTI_TOUCH_DELAY_MS - timeSinceLastTouch;
System.Threading.Tasks.Task.Delay((int)remainingTime).ContinueWith(_ =>
{
Dispatcher.Invoke(() =>
{
if (dec.Count > 1 && inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
{
inkCanvas.EditingMode = InkCanvasEditingMode.None;
}
isMultiTouchTimerActive = false;
});
});
}
return;
}
lastInkCanvasEditingMode = inkCanvas.EditingMode;
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& drawingShapeMode == 0)
{
inkCanvas.EditingMode = InkCanvasEditingMode.None;
}
inkCanvas.EditingMode = InkCanvasEditingMode.None;
}
}
private void inkCanvas_PreviewTouchMove(object sender, TouchEventArgs e)
{
// 如果手掌擦激活,更新橡皮擦反馈位置
if (isPalmEraserActive)
{
var touchPoint = e.GetTouchPoint(inkCanvas);
EraserOverlay_PointerMove(sender, touchPoint.Position);
}
}
private void inkCanvas_PreviewTouchUp(object sender, TouchEventArgs e)
{
// 橡皮状态下不做任何切换,直接return,保证橡皮可持续
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint && !isPalmEraserActive)
{
return;
}
inkCanvas.ReleaseAllTouchCaptures();
ViewboxFloatingBar.IsHitTestVisible = true;
BlackboardUIGridForInkReplay.IsHitTestVisible = true;
// Palm Eraser 逻辑
//手势完成后切回之前的状态
if (dec.Count > 1)
if (inkCanvas.EditingMode == InkCanvasEditingMode.None)
inkCanvas.EditingMode = lastInkCanvasEditingMode;
dec.Remove(e.TouchDevice.Id);
// 重置多触控点定时器状态
@@ -799,56 +628,20 @@ namespace Ink_Canvas
isMultiTouchTimerActive = false;
}
// 当手掌擦激活且所有触摸点都抬起时,恢复原编辑模式
if (isPalmEraserActive && dec.Count == 0)
if (dec.Count == 0)
{
LogHelper.WriteLogToFile($"Palm eraser recovery triggered - Touch points remaining: {dec.Count}");
// 恢复高光状态
drawingAttributes.IsHighlighter = palmEraserLastIsHighlighter;
// 恢复编辑模式
try
isSingleFingerDragMode = false;
if (drawingShapeMode == 0
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& inkCanvas.EditingMode != InkCanvasEditingMode.Select
&& inkCanvas.EditingMode != InkCanvasEditingMode.None)
{
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint)
if (lastInkCanvasEditingMode != InkCanvasEditingMode.None)
{
// 根据之前的状态恢复
switch (palmEraserLastEditingMode)
{
case InkCanvasEditingMode.Ink:
PenIcon_Click(null, null);
break;
case InkCanvasEditingMode.Select:
SymbolIconSelect_MouseUp(null, null);
break;
default:
inkCanvas.EditingMode = palmEraserLastEditingMode;
break;
}
LogHelper.WriteLogToFile($"Palm eraser recovered to mode: {palmEraserLastEditingMode}");
inkCanvas.EditingMode = lastInkCanvasEditingMode;
}
}
catch (Exception ex)
{
// 如果恢复失败,强制切换到批注模式
LogHelper.WriteLogToFile($"Palm eraser recovery failed: {ex.Message}, forcing to Ink mode", LogHelper.LogType.Error);
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
// 重置手掌擦状态
isPalmEraserActive = false;
// 禁用橡皮擦覆盖层
DisableEraserOverlay();
if (Settings.Canvas.IsShowCursor)
{
inkCanvas.ForceCursor = true;
inkCanvas.UseCustomCursor = true;
}
LogHelper.WriteLogToFile("Palm eraser state reset completed");
}
if (drawingShapeMode != 0)
@@ -857,11 +650,8 @@ namespace Ink_Canvas
ViewboxFloatingBar.IsHitTestVisible = true;
BlackboardUIGridForInkReplay.IsHitTestVisible = true;
// 对于双曲线等需要多步绘制的图形,触摸抬手时应该进入下一步
if (drawingShapeMode == 24 || drawingShapeMode == 25)
{
// 双曲线绘制:触摸抬手时进入下一步,但不自动触发鼠标抬起事件
// 让用户继续绘制第二笔
if (drawMultiStepShapeCurrentStep == 0)
{
// 第一笔完成,进入第二笔
@@ -880,7 +670,6 @@ namespace Ink_Canvas
}
else
{
// 其他单步绘制的图形,触摸抬手时完成绘制
var mouseArgs = new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Left)
{
RoutedEvent = MouseLeftButtonUpEvent,
@@ -890,82 +679,6 @@ namespace Ink_Canvas
}
}
// 手势完成后切回之前的状态
if (drawingShapeMode == 0)
{
if (dec.Count > 1)
{
if (inkCanvas.EditingMode == InkCanvasEditingMode.None)
{
if (lastInkCanvasEditingMode != InkCanvasEditingMode.EraseByPoint)
{
inkCanvas.EditingMode = lastInkCanvasEditingMode;
}
}
}
else if (dec.Count == 0)
{
// 当所有触摸点都抬起时,确保正确恢复编辑模式
// 这对于从橡皮擦切换到笔后恢复多指手势功能很重要
if (inkCanvas.EditingMode == InkCanvasEditingMode.None &&
lastInkCanvasEditingMode != InkCanvasEditingMode.None &&
lastInkCanvasEditingMode != InkCanvasEditingMode.EraseByPoint)
{
inkCanvas.EditingMode = lastInkCanvasEditingMode;
}
if (isPalmEraserActive)
{
LogHelper.WriteLogToFile("Palm eraser force recovery - all touch points cleared");
// 恢复高光状态
drawingAttributes.IsHighlighter = palmEraserLastIsHighlighter;
// 恢复编辑模式
try
{
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint)
{
switch (palmEraserLastEditingMode)
{
case InkCanvasEditingMode.Ink:
PenIcon_Click(null, null);
break;
case InkCanvasEditingMode.Select:
SymbolIconSelect_MouseUp(null, null);
break;
default:
inkCanvas.EditingMode = palmEraserLastEditingMode;
break;
}
LogHelper.WriteLogToFile($"Palm eraser force recovered to mode: {palmEraserLastEditingMode}");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"Palm eraser force recovery failed: {ex.Message}, forcing to Ink mode", LogHelper.LogType.Error);
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
// 如果手掌擦还在激活状态但触摸点已清空,强制重置状态
isPalmEraserActive = false;
inkCanvas.IsHitTestVisible = true;
inkCanvas.IsManipulationEnabled = true;
ViewboxFloatingBar.IsHitTestVisible = true;
BlackboardUIGridForInkReplay.IsHitTestVisible = true;
DisableEraserOverlay();
if (Settings.Canvas.IsShowCursor)
{
inkCanvas.ForceCursor = true;
inkCanvas.UseCustomCursor = true;
}
LogHelper.WriteLogToFile("Palm eraser force recovery completed");
}
}
}
inkCanvas.Opacity = 1;
if (dec.Count == 0)
@@ -987,61 +700,37 @@ namespace Ink_Canvas
private void Main_Grid_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
{
if (e.Manipulators.Count() != 0) return;
if (drawingShapeMode == 0
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& inkCanvas.EditingMode != InkCanvasEditingMode.Select)
if (e.Manipulators.Count() == 0)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
lastInkCanvasEditingMode = InkCanvasEditingMode.Ink;
if (dec.Count > 0)
{
dec.Clear();
}
isSingleFingerDragMode = false;
if (drawingShapeMode == 0
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& inkCanvas.EditingMode != InkCanvasEditingMode.Select)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
lastInkCanvasEditingMode = InkCanvasEditingMode.Ink;
}
}
}
private void Main_Grid_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
// 手掌擦时禁止移动/缩放
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint)
return;
// 三指及以上禁止缩放
bool disableScale = dec.Count >= 3;
if (isInMultiTouchMode) return;
if (dec.Count == 0 && (isSingleFingerDragMode || isInMultiTouchMode))
{
ResetTouchStates();
return;
}
// 如果是单指拖动选中的墨迹,允许处理
if (dec.Count == 1 && inkCanvas.GetSelectedStrokes().Count > 0)
{
var md = e.DeltaManipulation;
var trans = md.Translation; // 获得位移矢量
if (trans.X != 0 || trans.Y != 0)
{
var m = new Matrix();
m.Translate(trans.X, trans.Y); // 移动
var strokes = inkCanvas.GetSelectedStrokes();
foreach (var stroke in strokes)
{
stroke.Transform(m, false);
}
// 更新选择框位置
updateBorderStrokeSelectionControlLocation();
}
return;
}
if (!Settings.Gesture.IsEnableTwoFingerGesture) return;
if ((dec.Count >= 2 && (Settings.PowerPointSettings.IsEnableTwoFingerGestureInPresentationMode ||
StackPanelPPTControls.Visibility != Visibility.Visible ||
StackPanelPPTButtons.Visibility == Visibility.Collapsed)) ||
isSingleFingerDragMode)
if (isInMultiTouchMode || !Settings.Gesture.IsEnableTwoFingerGesture) return;
bool hasMultipleManipulators = e.Manipulators.Count() >= 2;
bool shouldUseTwoFingerGesture = (dec.Count >= 2 && hasMultipleManipulators &&
(Settings.PowerPointSettings.IsEnableTwoFingerGestureInPresentationMode ||
StackPanelPPTControls.Visibility != Visibility.Visible ||
StackPanelPPTButtons.Visibility == Visibility.Collapsed)) ||
isSingleFingerDragMode;
if (shouldUseTwoFingerGesture)
{
var md = e.DeltaManipulation;
var trans = md.Translation; // 获得位移矢量
@@ -1051,20 +740,23 @@ namespace Ink_Canvas
if (Settings.Gesture.IsEnableTwoFingerTranslate)
m.Translate(trans.X, trans.Y); // 移动
// 计算中心点(用于缩放和旋转)
var fe = e.Source as FrameworkElement;
var center = new Point(fe.ActualWidth / 2, fe.ActualHeight / 2);
center = m.Transform(center); // 转换为矩阵缩放和旋转的中心点
if (Settings.Gesture.IsEnableTwoFingerGestureTranslateOrRotation)
{
var rotate = md.Rotation; // 获得旋转角度
var scale = md.Scale; // 获得缩放倍数
// Find center of element and then transform to get current location of center
var fe = e.Source as FrameworkElement;
var center = new Point(fe.ActualWidth / 2, fe.ActualHeight / 2);
center = m.Transform(center); // 转换为矩阵缩放和旋转的中心点
if (Settings.Gesture.IsEnableTwoFingerRotation)
m.RotateAt(rotate, center.X, center.Y); // 旋转
if (Settings.Gesture.IsEnableTwoFingerZoom && !disableScale)
m.ScaleAt(scale.X, scale.Y, center.X, center.Y); // 缩放
}
if (Settings.Gesture.IsEnableTwoFingerZoom)
{
var scale = md.Scale; // 获得缩放倍数
m.ScaleAt(scale.X, scale.Y, center.X, center.Y); // 缩放
}
var strokes = inkCanvas.GetSelectedStrokes();
@@ -1087,6 +779,13 @@ namespace Ink_Canvas
break;
}
if (!Settings.Gesture.IsEnableTwoFingerZoom) continue;
try
{
stroke.DrawingAttributes.Width *= md.Scale.X;
stroke.DrawingAttributes.Height *= md.Scale.Y;
}
catch { }
}
}
else
@@ -1209,69 +908,6 @@ namespace Ink_Canvas
LogHelper.WriteLogToFile($"应用媒体元素变换失败: {ex.Message}", LogHelper.LogType.Error);
}
}
// 退出多指书写模式,恢复InkCanvas的TouchDown事件绑定
private void ExitMultiTouchModeIfNeeded()
{
if (isInMultiTouchMode)
{
inkCanvas.StylusDown -= MainWindow_StylusDown;
inkCanvas.StylusMove -= MainWindow_StylusMove;
inkCanvas.StylusUp -= MainWindow_StylusUp;
inkCanvas.TouchDown -= MainWindow_TouchDown;
inkCanvas.TouchDown += Main_Grid_TouchDown;
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& drawingShapeMode == 0)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
// 保存非笔画元素(如图片)
var preservedElements = PreserveNonStrokeElements();
inkCanvas.Children.Clear();
// 恢复非笔画元素
RestoreNonStrokeElements(preservedElements);
isInMultiTouchMode = false;
// 关闭多指书写时,恢复手掌擦开关
if (palmEraserWasEnabledBeforeMultiTouch)
{
Settings.Canvas.EnablePalmEraser = true;
if (ToggleSwitchEnablePalmEraser != null)
ToggleSwitchEnablePalmEraser.IsOn = true;
}
}
}
// 进入多指书写模式,绑定Main_Grid_TouchDown
private void EnterMultiTouchModeIfNeeded()
{
if (!isInMultiTouchMode)
{
inkCanvas.StylusDown += MainWindow_StylusDown;
inkCanvas.StylusMove += MainWindow_StylusMove;
inkCanvas.StylusUp += MainWindow_StylusUp;
inkCanvas.TouchDown += MainWindow_TouchDown;
inkCanvas.TouchDown -= Main_Grid_TouchDown;
if (inkCanvas.EditingMode != InkCanvasEditingMode.EraseByPoint
&& inkCanvas.EditingMode != InkCanvasEditingMode.EraseByStroke
&& drawingShapeMode == 0)
{
inkCanvas.EditingMode = InkCanvasEditingMode.None;
}
// 保存非笔画元素(如图片)
var preservedElements = PreserveNonStrokeElements();
inkCanvas.Children.Clear();
// 恢复非笔画元素
RestoreNonStrokeElements(preservedElements);
isInMultiTouchMode = true;
// 启用多指书写时,自动禁用手掌擦
palmEraserWasEnabledBeforeMultiTouch = Settings.Canvas.EnablePalmEraser;
Settings.Canvas.EnablePalmEraser = false;
if (ToggleSwitchEnablePalmEraser != null)
ToggleSwitchEnablePalmEraser.IsOn = false;
}
}
}
}
+2 -2
View File
@@ -49,5 +49,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.15.0")]
[assembly: AssemblyFileVersion("1.7.15.0")]
[assembly: AssemblyVersion("1.7.18.1")]
[assembly: AssemblyFileVersion("1.7.18.1")]
@@ -0,0 +1,39 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 新点名窗口主题资源 -->
<!-- 浅色主题资源 -->
<SolidColorBrush x:Key="NewRollCallWindowBackgroundLight" Color="#FFFFFF"/>
<SolidColorBrush x:Key="NewRollCallWindowBorderBrushLight" Color="#E4E4E7"/>
<SolidColorBrush x:Key="NewRollCallWindowTitleForegroundLight" Color="#18181B"/>
<SolidColorBrush x:Key="NewRollCallWindowDigitForegroundLight" Color="#18181B"/>
<SolidColorBrush x:Key="NewRollCallWindowButtonBackgroundLight" Color="#F4F4F5"/>
<SolidColorBrush x:Key="NewRollCallWindowButtonForegroundLight" Color="#18181B"/>
<SolidColorBrush x:Key="NewRollCallWindowPrimaryButtonBackgroundLight" Color="#4CAF50"/>
<SolidColorBrush x:Key="NewRollCallWindowPrimaryButtonForegroundLight" Color="#FFFFFF"/>
<SolidColorBrush x:Key="NewRollCallWindowSecondaryTextForegroundLight" Color="#71717A"/>
<!-- 深色主题资源 -->
<SolidColorBrush x:Key="NewRollCallWindowBackgroundDark" Color="#1f1f1f"/>
<SolidColorBrush x:Key="NewRollCallWindowBorderBrushDark" Color="#E0E0E0"/>
<SolidColorBrush x:Key="NewRollCallWindowTitleForegroundDark" Color="White"/>
<SolidColorBrush x:Key="NewRollCallWindowDigitForegroundDark" Color="White"/>
<SolidColorBrush x:Key="NewRollCallWindowButtonBackgroundDark" Color="#2a2a2a"/>
<SolidColorBrush x:Key="NewRollCallWindowButtonForegroundDark" Color="White"/>
<SolidColorBrush x:Key="NewRollCallWindowPrimaryButtonBackgroundDark" Color="#4CAF50"/>
<SolidColorBrush x:Key="NewRollCallWindowPrimaryButtonForegroundDark" Color="White"/>
<SolidColorBrush x:Key="NewRollCallWindowSecondaryTextForegroundDark" Color="#9ca3af"/>
<!-- 默认资源绑定 -->
<SolidColorBrush x:Key="NewRollCallWindowBackground" Color="#1f1f1f"/>
<SolidColorBrush x:Key="NewRollCallWindowBorderBrush" Color="#E0E0E0"/>
<SolidColorBrush x:Key="NewRollCallWindowTitleForeground" Color="White"/>
<SolidColorBrush x:Key="NewRollCallWindowDigitForeground" Color="White"/>
<SolidColorBrush x:Key="NewRollCallWindowButtonBackground" Color="#2a2a2a"/>
<SolidColorBrush x:Key="NewRollCallWindowButtonForeground" Color="White"/>
<SolidColorBrush x:Key="NewRollCallWindowPrimaryButtonBackground" Color="#4CAF50"/>
<SolidColorBrush x:Key="NewRollCallWindowPrimaryButtonForeground" Color="White"/>
<SolidColorBrush x:Key="NewRollCallWindowSecondaryTextForeground" Color="#9ca3af"/>
</ResourceDictionary>
+46 -2
View File
@@ -29,6 +29,8 @@ namespace Ink_Canvas
public ModeSettings ModeSettings { get; set; } = new ModeSettings();
[JsonProperty("camera")]
public CameraSettings Camera { get; set; } = new CameraSettings();
[JsonProperty("dlass")]
public DlassSettings Dlass { get; set; } = new DlassSettings();
}
public class Canvas
@@ -121,7 +123,7 @@ namespace Ink_Canvas
[JsonIgnore]
public bool IsEnableTwoFingerGestureTranslateOrRotation => IsEnableTwoFingerTranslate || IsEnableTwoFingerRotation;
[JsonProperty("isEnableMultiTouchMode")]
public bool IsEnableMultiTouchMode { get; set; } = true;
public bool IsEnableMultiTouchMode { get; set; } = false;
[JsonProperty("isEnableTwoFingerZoom")]
public bool IsEnableTwoFingerZoom { get; set; } = true;
[JsonProperty("isEnableTwoFingerTranslate")]
@@ -462,6 +464,12 @@ namespace Ink_Canvas
[JsonProperty("isAutoEnterAnnotationAfterKillHite")]
public bool IsAutoEnterAnnotationAfterKillHite { get; set; }
[JsonProperty("isEnableAutoSaveStrokes")]
public bool IsEnableAutoSaveStrokes { get; set; } = true;
[JsonProperty("autoSaveStrokesIntervalMinutes")]
public int AutoSaveStrokesIntervalMinutes { get; set; } = 5;
[JsonProperty("floatingWindowInterceptor")]
public FloatingWindowInterceptorSettings FloatingWindowInterceptor { get; set; } = new FloatingWindowInterceptorSettings();
}
@@ -594,6 +602,9 @@ namespace Ink_Canvas
[JsonProperty("enableUIAccessTopMost")]
public bool EnableUIAccessTopMost { get; set; } = false;
[JsonProperty("windowMode")]
public bool WindowMode { get; set; } = true;
}
public class InkToShape
@@ -611,7 +622,9 @@ namespace Ink_Canvas
[JsonProperty("isInkToShapeRounded")]
public bool IsInkToShapeRounded { get; set; } = true;
[JsonProperty("lineStraightenSensitivity")]
public double LineStraightenSensitivity { get; set; } = 0.20; // 直线检测灵敏度,值越小越严格(0.05-2.0)
public double LineStraightenSensitivity { get; set; } = 0.20;
[JsonProperty("lineNormalizationThreshold")]
public double LineNormalizationThreshold { get; set; } = 0.5;
}
public class RandSettings
@@ -650,6 +663,16 @@ namespace Ink_Canvas
public double ProgressiveReminderVolume { get; set; } = 1.0;
[JsonProperty("progressiveReminderSoundPath")]
public string ProgressiveReminderSoundPath { get; set; } = "";
[JsonProperty("useNewRollCallUI")]
public bool UseNewRollCallUI { get; set; } = true;
[JsonProperty("enableMLAvoidance")]
public bool EnableMLAvoidance { get; set; } = true;
[JsonProperty("mlAvoidanceHistoryCount")]
public int MLAvoidanceHistoryCount { get; set; } = 50;
[JsonProperty("mlAvoidanceWeight")]
public double MLAvoidanceWeight { get; set; } = 1.0;
[JsonProperty("enableQuickDraw")]
public bool EnableQuickDraw { get; set; } = true;
}
public class CustomPickNameBackground
@@ -708,4 +731,25 @@ namespace Ink_Canvas
[JsonProperty("selectedCameraIndex")]
public int SelectedCameraIndex { get; set; } = 0;
}
public class DlassSettings
{
[JsonProperty("userToken")]
public string UserToken { get; set; } = string.Empty;
[JsonProperty("savedTokens")]
public List<string> SavedTokens { get; set; } = new List<string>();
[JsonProperty("selectedClassName")]
public string SelectedClassName { get; set; } = string.Empty;
[JsonProperty("apiBaseUrl")]
public string ApiBaseUrl { get; set; } = "https://dlass.tech";
[JsonProperty("isAutoUploadNotes")]
public bool IsAutoUploadNotes { get; set; } = false;
[JsonProperty("autoUploadDelayMinutes")]
public int AutoUploadDelayMinutes { get; set; } = 0;
}
}
+6 -2
View File
@@ -88,8 +88,8 @@
<SolidColorBrush x:Key="RandWindowBackground" Color="#1f1f1f"/>
<SolidColorBrush x:Key="RandWindowBorderBrush" Color="#0066BF"/>
<SolidColorBrush x:Key="RandWindowTextForeground" Color="White"/>
<SolidColorBrush x:Key="RandWindowButtonBackground" Color="#FBFBFD"/>
<SolidColorBrush x:Key="RandWindowButtonForeground" Color="Black"/>
<SolidColorBrush x:Key="RandWindowButtonBackground" Color="#3F3F46"/>
<SolidColorBrush x:Key="RandWindowButtonForeground" Color="White"/>
<SolidColorBrush x:Key="RandWindowPrimaryButtonBackground" Color="#0066BF"/>
<SolidColorBrush x:Key="RandWindowPrimaryButtonForeground" Color="White"/>
<SolidColorBrush x:Key="RandWindowSecondaryButtonBackground" Color="#00B894"/>
@@ -145,4 +145,8 @@
<BitmapImage x:Key="HandMoveIcon" UriSource="/Resources/new-icons/hand-move_white.png"/>
<BitmapImage x:Key="ZoomIcon" UriSource="/Resources/new-icons/zoom_white.png"/>
<BitmapImage x:Key="RotateIcon" UriSource="/Resources/new-icons/rotate_white.png"/>
<!-- 浮动栏手势按钮图标资源 - 深色主题 -->
<BitmapImage x:Key="GestureIcon" UriSource="/Resources/new-icons/gesture_white.png"/>
<BitmapImage x:Key="GestureIconEnabled" UriSource="/Resources/new-icons/gesture-enabled.png"/>
</ResourceDictionary>
+4
View File
@@ -145,4 +145,8 @@
<BitmapImage x:Key="HandMoveIcon" UriSource="/Resources/new-icons/hand-move.png"/>
<BitmapImage x:Key="ZoomIcon" UriSource="/Resources/new-icons/zoom.png"/>
<BitmapImage x:Key="RotateIcon" UriSource="/Resources/new-icons/rotate.png"/>
<!-- 浮动栏手势按钮图标资源 - 浅色主题 -->
<BitmapImage x:Key="GestureIcon" UriSource="/Resources/new-icons/gesture.png"/>
<BitmapImage x:Key="GestureIconEnabled" UriSource="/Resources/new-icons/gesture-enabled.png"/>
</ResourceDictionary>
@@ -8,6 +8,7 @@
xmlns:processbars="clr-namespace:Ink_Canvas.ProcessBars"
ui:ThemeManager.RequestedTheme="Light" Topmost="True" Background="Transparent"
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True"
ResizeMode="CanMinimize"
Loaded="Window_Loaded" Closing="Window_Closing" WindowStartupLocation="CenterScreen"
Title="Ink Canvas 画板 - 计时器" Height="700" Width="1100">
<Border Background="{DynamicResource TimerWindowBackground}" CornerRadius="10" BorderThickness="1" BorderBrush="{DynamicResource TimerWindowBorderBrush}" Margin="60">
@@ -29,14 +29,7 @@ namespace Ink_Canvas
public static Window CreateTimerWindow()
{
if (MainWindow.Settings.RandSettings?.UseNewStyleUI == true)
{
return new NewStyleTimerWindow();
}
else
{
return new CountdownTimerWindow();
}
return new CountdownTimerWindow();
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
+494
View File
@@ -0,0 +1,494 @@
<Window x:Class="Ink_Canvas.Windows.DlassSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:controls="clr-namespace:Ink_Canvas.Windows.Controls"
mc:Ignorable="d"
WindowStyle="None"
Title="Dlass设置管理" Height="600" Width="900"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResize"
AllowsTransparency="True"
Background="Transparent">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Controls/WinUI3CloseButton.xaml" />
</ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="WindowBackground" Color="#1e1e1e"/>
<SolidColorBrush x:Key="BorderBrush" Color="#3f3f46"/>
<SolidColorBrush x:Key="TextForeground" Color="#fafafa"/>
<SolidColorBrush x:Key="TextSecondary" Color="#a1a1aa"/>
<SolidColorBrush x:Key="AccentColor" Color="#3b82f6"/>
<SolidColorBrush x:Key="TitleForeground" Color="#fafafa"/>
<SolidColorBrush x:Key="NewTimerWindowButtonForeground" Color="White"/>
</ResourceDictionary>
</Window.Resources>
<Border Background="{StaticResource WindowBackground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
CornerRadius="15"
Margin="10"
x:Name="MainBorder"
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
<Grid>
<controls:WinUI3CloseButton x:Name="BtnClose"
HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,0,0,0" Cursor="Hand" Click="BtnClose_Click"
Content="✕"/>
<!-- 主要内容区域 -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 标题栏 -->
<Grid Grid.Row="0"
Height="50"
Background="{StaticResource WindowBackground}"
x:Name="TitleBar"
VerticalAlignment="Top"
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown"
Margin="0,0,46,0">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="22,0,0,0">
<!-- 设置图标 -->
<Path Data="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5A3.5 3.5 0 0 1 15.5 12A3.5 3.5 0 0 1 12 15.5M19.43 12.97C19.47 12.65 19.5 12.33 19.5 12C19.5 11.67 19.47 11.34 19.43 11.03L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.54 5.05 19.27 4.96 19.05 5.05L16.56 6.05C16.04 5.65 15.5 5.32 14.87 5.07L14.5 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.5 2.42L9.13 5.07C8.5 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.73 4.96 4.46 5.05 4.35 5.27L2.35 8.73C2.23 8.95 2.27 9.22 2.46 9.37L4.57 11.03C4.53 11.34 4.5 11.67 4.5 12C4.5 12.33 4.53 12.65 4.57 12.97L2.46 14.63C2.27 14.78 2.23 15.05 2.35 15.27L4.35 18.73C4.46 18.95 4.73 19.03 4.95 18.95L7.44 17.95C7.96 18.34 8.5 18.68 9.13 18.93L9.5 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.5 21.58L14.87 18.93C15.5 18.67 16.04 18.34 16.56 17.95L19.05 18.95C19.27 19.03 19.54 18.95 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.97Z"
Stroke="{StaticResource TitleForeground}"
StrokeThickness="1.5"
StrokeLineJoin="Round"
Fill="Transparent"
Width="24" Height="24"
Stretch="Uniform"
Margin="0,0,8,0"/>
<!-- 标题文字 -->
<TextBlock Text="Dlass设置管理"
FontSize="28"
FontWeight="Bold"
Foreground="{StaticResource TitleForeground}"
x:Name="TitleText"/>
</StackPanel>
</Grid>
<!-- 主内容区 -->
<Border Grid.Row="1"
Background="{StaticResource WindowBackground}"
Padding="20,10,20,20">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ui:SimpleStackPanel Spacing="16">
<!-- 内容区域 -->
<Border BorderThickness="1"
BorderBrush="{StaticResource BorderBrush}"
CornerRadius="8"
Padding="20"
Background="#27272a">
<ui:SimpleStackPanel Spacing="16">
<TextBlock Text="Dlass设置管理"
FontSize="18"
FontWeight="SemiBold"
Foreground="{StaticResource TextForeground}"/>
<TextBlock Text="管理您的Dlass服务端连接和设置。"
FontSize="14"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,0,0,8"/>
<Line HorizontalAlignment="Stretch"
X1="0" Y1="0" X2="1" Y2="0"
Stroke="{StaticResource BorderBrush}"
StrokeThickness="1"
Margin="0,2,0,2"/>
<!-- 用户Token设置 -->
<TextBlock Text="用户Token"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource TextForeground}"
Margin="0,0,0,1"/>
<TextBlock Text="设置您的用户Token以访问Dlass服务端功能。您可以从Dlass平台获取您的用户Token。"
FontSize="12"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,0,0,2"/>
<ui:SimpleStackPanel Orientation="Vertical" Spacing="4">
<ComboBox x:Name="CmbSavedTokens"
FontSize="14"
Padding="12,8"
Background="#18181b"
Foreground="{StaticResource TextForeground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
MinHeight="36"
IsEditable="False"
IsReadOnly="True"
SelectionChanged="CmbSavedTokens_SelectionChanged">
</ComboBox>
<TextBox x:Name="TxtNewToken"
FontSize="14"
Padding="12,4"
Background="#18181b"
Foreground="{StaticResource TextForeground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
TextWrapping="Wrap"
AcceptsReturn="False"
MaxLength="500"
MinHeight="36"
Tag="输入新的Token">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ScrollViewer x:Name="PART_ContentHost" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TextBox.Style>
</TextBox>
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="8">
<Button x:Name="BtnSaveToken"
Content="保存Token"
Padding="12,6"
FontSize="13"
Background="{StaticResource AccentColor}"
Foreground="White"
BorderThickness="0"
Cursor="Hand"
Click="BtnSaveToken_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2563eb"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#1d4ed8"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="BtnClearToken"
Content="清除Token"
Padding="12,6"
FontSize="13"
Background="Transparent"
Foreground="{StaticResource TextForeground}"
BorderThickness="1"
BorderBrush="{StaticResource BorderBrush}"
Cursor="Hand"
Click="BtnClearToken_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#27272a"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="BtnTestToken"
Content="测试连接"
Padding="12,6"
FontSize="13"
Background="Transparent"
Foreground="{StaticResource TextForeground}"
BorderThickness="1"
BorderBrush="{StaticResource BorderBrush}"
Cursor="Hand"
Click="BtnTestToken_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#27272a"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</ui:SimpleStackPanel>
<TextBlock x:Name="TxtTokenStatus"
Text=""
FontSize="12"
Foreground="{StaticResource TextSecondary}"
Margin="0,4,0,0"/>
</ui:SimpleStackPanel>
<!-- 连接状态 -->
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="12" Margin="0,4,0,0">
<TextBlock Text="连接状态:"
FontSize="14"
Foreground="{StaticResource TextForeground}"
VerticalAlignment="Center"
Width="100"/>
<TextBlock x:Name="TxtConnectionStatus"
Text="未连接"
FontSize="14"
Foreground="{StaticResource TextSecondary}"
VerticalAlignment="Center"/>
</ui:SimpleStackPanel>
<Line HorizontalAlignment="Stretch"
X1="0" Y1="0" X2="1" Y2="0"
Stroke="{StaticResource BorderBrush}"
StrokeThickness="1"
Margin="0,2,0,1"/>
<!-- 班级选择 -->
<TextBlock Text="班级选择"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource TextForeground}"
Margin="0,0,0,1"/>
<TextBlock Text="连接成功后,将自动加载可用班级列表。"
FontSize="12"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,0,0,2"/>
<ComboBox x:Name="CmbClassSelection"
FontSize="14"
Padding="12,8"
Background="#18181b"
Foreground="{StaticResource TextForeground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
MinHeight="36"
IsEditable="False"
IsReadOnly="True"
SelectionChanged="CmbClassSelection_SelectionChanged">
</ComboBox>
<Line HorizontalAlignment="Stretch"
X1="0" Y1="0" X2="1" Y2="0"
Stroke="{StaticResource BorderBrush}"
StrokeThickness="1"
Margin="0,2,0,1"/>
<!-- 自动上传设置 -->
<TextBlock Text="自动上传设置"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource TextForeground}"
Margin="0,0,0,1"/>
<ui:SimpleStackPanel Orientation="Vertical" Spacing="4">
<ui:ToggleSwitch x:Name="ToggleSwitchAutoUploadNotes"
Header="自动上传笔记"
FontSize="14"
Foreground="White"
Toggled="ToggleSwitchAutoUploadNotes_Toggled">
</ui:ToggleSwitch>
<TextBlock Text="启用后,保存的PNG截图和ICSTK墨迹文件将自动上传到所选班级的白板。"
FontSize="12"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,0,0,2"/>
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="12"
IsEnabled="{Binding ElementName=ToggleSwitchAutoUploadNotes, Path=IsOn}">
<TextBlock Text="上传延迟时间:"
FontSize="14"
Foreground="{StaticResource TextForeground}"
VerticalAlignment="Center"
Width="120"/>
<TextBox x:Name="TxtUploadDelayMinutes"
FontSize="14"
Background="#18181b"
Foreground="{StaticResource TextForeground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
MinWidth="100"
Height="32"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Left"
TextChanged="TxtUploadDelayMinutes_TextChanged"
PreviewTextInput="TxtUploadDelayMinutes_PreviewTextInput">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<ScrollViewer x:Name="PART_ContentHost"
Margin="12,0"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TextBox.Style>
</TextBox>
<TextBlock Text="分钟"
FontSize="14"
Foreground="{StaticResource TextSecondary}"
VerticalAlignment="Center"
Margin="0,0,0,0"/>
</ui:SimpleStackPanel>
<TextBlock Text="设置上传延迟时间(0-60分钟),可以在保存后等待一段时间再上传。"
FontSize="12"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,2,0,0"/>
</ui:SimpleStackPanel>
<Line HorizontalAlignment="Stretch"
X1="0" Y1="0" X2="1" Y2="0"
Stroke="{StaticResource BorderBrush}"
StrokeThickness="1"
Margin="0,2,0,1"/>
<!-- 操作按钮 -->
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="12" Margin="0,8,0,0">
<Button Content="保存"
Padding="16,8"
FontSize="14"
Background="{StaticResource AccentColor}"
Foreground="White"
BorderThickness="0"
Cursor="Hand"
Click="BtnSave_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2563eb"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#1d4ed8"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button Content="取消"
Padding="16,8"
FontSize="14"
Background="Transparent"
Foreground="{StaticResource TextForeground}"
BorderThickness="1"
BorderBrush="{StaticResource BorderBrush}"
Cursor="Hand"
Click="BtnCancel_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#27272a"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</ui:SimpleStackPanel>
</ui:SimpleStackPanel>
</Border>
</ui:SimpleStackPanel>
</ScrollViewer>
</Border>
</Grid>
</Grid>
</Border>
</Window>
@@ -0,0 +1,709 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
namespace Ink_Canvas.Windows
{
/// <summary>
/// DlassSettingsWindow.xaml 的交互逻辑
/// </summary>
public partial class DlassSettingsWindow : Window
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
private DlassApiClient _apiClient;
private List<WhiteboardInfo> _currentWhiteboards = new List<WhiteboardInfo>();
private UserInfo _currentUser;
public DlassSettingsWindow(MainWindow mainWindow = null)
{
InitializeComponent();
// 初始化班级下拉框
CmbClassSelection.Items.Clear();
CmbClassSelection.Items.Add("(等待连接)");
CmbClassSelection.SelectedIndex = 0;
CmbClassSelection.IsEnabled = false;
// 加载保存的token
LoadUserToken();
// 加载自动上传设置
LoadAutoUploadSettings();
// 初始化API客户端(优先使用用户token)
InitializeApiClient();
// 窗口关闭时释放资源
Closed += (s, e) => _apiClient?.Dispose();
// 测试连接
_ = TestConnectionAsync();
}
/// <summary>
/// 初始化API客户端
/// </summary>
private void InitializeApiClient()
{
var userToken = GetUserToken();
var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl;
if (string.IsNullOrEmpty(apiBaseUrl) || apiBaseUrl.Contains("api.dlass.tech"))
{
apiBaseUrl = "https://dlass.tech";
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.ApiBaseUrl = apiBaseUrl;
MainWindow.SaveSettingsToFile();
}
}
if (!string.IsNullOrEmpty(userToken))
{
_apiClient = new DlassApiClient(APP_ID, APP_SECRET, baseUrl: apiBaseUrl, userToken: userToken);
}
else
{
_apiClient = new DlassApiClient(APP_ID, APP_SECRET, baseUrl: apiBaseUrl);
}
}
/// <summary>
/// 获取用户token
/// </summary>
private string GetUserToken()
{
if (MainWindow.Settings?.Dlass != null)
{
return MainWindow.Settings.Dlass.UserToken ?? string.Empty;
}
return string.Empty;
}
/// <summary>
/// 获取保存的Token列表
/// </summary>
private List<string> GetSavedTokens()
{
if (MainWindow.Settings?.Dlass != null)
{
return MainWindow.Settings.Dlass.SavedTokens ?? new List<string>();
}
return new List<string>();
}
/// <summary>
/// 加载用户token到UI
/// </summary>
private void LoadUserToken()
{
var savedTokens = GetSavedTokens();
var currentToken = GetUserToken();
CmbSavedTokens.Items.Clear();
if (savedTokens.Count > 0)
{
foreach (var token in savedTokens)
{
CmbSavedTokens.Items.Add(token);
}
if (!string.IsNullOrEmpty(currentToken))
{
var index = savedTokens.IndexOf(currentToken);
if (index >= 0)
{
CmbSavedTokens.SelectedIndex = index;
}
else
{
CmbSavedTokens.SelectedIndex = 0;
}
}
else if (CmbSavedTokens.Items.Count > 0)
{
CmbSavedTokens.SelectedIndex = 0;
}
}
else
{
CmbSavedTokens.Items.Add("(无保存的Token");
CmbSavedTokens.SelectedIndex = 0;
CmbSavedTokens.IsEnabled = false;
}
TxtNewToken.Text = string.Empty;
if (!string.IsNullOrEmpty(currentToken))
{
TxtTokenStatus.Text = "已选择Token";
TxtTokenStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94));
}
else
{
TxtTokenStatus.Text = "未设置Token";
TxtTokenStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170));
}
}
/// <summary>
/// 保存用户token
/// </summary>
private void SaveUserToken(string token)
{
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.UserToken = token ?? string.Empty;
MainWindow.SaveSettingsToFile();
}
}
/// <summary>
/// 添加Token到保存列表
/// </summary>
private void AddTokenToList(string token)
{
if (MainWindow.Settings?.Dlass != null)
{
if (MainWindow.Settings.Dlass.SavedTokens == null)
{
MainWindow.Settings.Dlass.SavedTokens = new List<string>();
}
if (!string.IsNullOrEmpty(token) && !MainWindow.Settings.Dlass.SavedTokens.Contains(token))
{
MainWindow.Settings.Dlass.SavedTokens.Add(token);
MainWindow.SaveSettingsToFile();
}
}
}
/// <summary>
/// 从列表删除Token
/// </summary>
private void RemoveTokenFromList(string token)
{
if (MainWindow.Settings?.Dlass != null && MainWindow.Settings.Dlass.SavedTokens != null)
{
MainWindow.Settings.Dlass.SavedTokens.Remove(token);
MainWindow.SaveSettingsToFile();
}
}
/// <summary>
/// 加载班级列表到下拉框
/// </summary>
private void LoadClasses(List<WhiteboardInfo> whiteboards, UserInfo user = null)
{
CmbClassSelection.Items.Clear();
if (whiteboards != null && whiteboards.Count > 0)
{
var teacherName = user?.Username ?? "未知教师";
var classGroups = whiteboards
.Where(w => !string.IsNullOrEmpty(w.ClassName))
.GroupBy(w => w.ClassName)
.OrderBy(g => g.Key)
.ToList();
foreach (var group in classGroups)
{
var className = group.Key;
var displayText = $"{teacherName} - {className}";
CmbClassSelection.Items.Add(new ClassSelectionItem
{
DisplayText = displayText,
ClassName = className,
TeacherName = teacherName
});
}
var savedClassName = MainWindow.Settings?.Dlass?.SelectedClassName ?? string.Empty;
if (!string.IsNullOrEmpty(savedClassName))
{
var savedItem = CmbClassSelection.Items.Cast<ClassSelectionItem>()
.FirstOrDefault(item => item.ClassName == savedClassName);
if (savedItem != null)
{
CmbClassSelection.SelectedItem = savedItem;
}
else if (CmbClassSelection.Items.Count > 0)
{
CmbClassSelection.SelectedIndex = 0;
}
}
else if (CmbClassSelection.Items.Count > 0)
{
CmbClassSelection.SelectedIndex = 0;
}
CmbClassSelection.IsEnabled = true;
}
else
{
CmbClassSelection.Items.Add("(无可用班级)");
CmbClassSelection.SelectedIndex = 0;
CmbClassSelection.IsEnabled = false;
}
}
/// <summary>
/// 班级选择改变事件
/// </summary>
private void CmbClassSelection_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
try
{
if (CmbClassSelection.SelectedItem is ClassSelectionItem selectedItem)
{
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.SelectedClassName = selectedItem.ClassName;
MainWindow.SaveSettingsToFile();
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"选择班级时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 加载自动上传设置
/// </summary>
private void LoadAutoUploadSettings()
{
try
{
if (MainWindow.Settings?.Dlass != null)
{
ToggleSwitchAutoUploadNotes.IsOn = MainWindow.Settings.Dlass.IsAutoUploadNotes;
var delayMinutes = MainWindow.Settings.Dlass.AutoUploadDelayMinutes;
if (delayMinutes < 0 || delayMinutes > 60)
{
delayMinutes = 0;
}
TxtUploadDelayMinutes.Text = delayMinutes.ToString();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载自动上传设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 自动上传开关切换事件
/// </summary>
private void ToggleSwitchAutoUploadNotes_Toggled(object sender, RoutedEventArgs e)
{
try
{
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.IsAutoUploadNotes = ToggleSwitchAutoUploadNotes.IsOn;
MainWindow.SaveSettingsToFile();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存自动上传设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 上传延迟时间输入框文本改变事件
/// </summary>
private void TxtUploadDelayMinutes_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
try
{
if (MainWindow.Settings?.Dlass != null && int.TryParse(TxtUploadDelayMinutes.Text, out int delayMinutes))
{
// 限制范围在0-60分钟
if (delayMinutes < 0)
{
delayMinutes = 0;
TxtUploadDelayMinutes.Text = "0";
}
else if (delayMinutes > 60)
{
delayMinutes = 60;
TxtUploadDelayMinutes.Text = "60";
}
MainWindow.Settings.Dlass.AutoUploadDelayMinutes = delayMinutes;
MainWindow.SaveSettingsToFile();
}
else if (string.IsNullOrWhiteSpace(TxtUploadDelayMinutes.Text))
{
// 空文本时设置为0
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.AutoUploadDelayMinutes = 0;
MainWindow.SaveSettingsToFile();
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存上传延迟时间时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 上传延迟时间输入框预览文本输入事件(只允许数字)
/// </summary>
private void TxtUploadDelayMinutes_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
Regex regex = new Regex("[^0-9]+");
e.Handled = regex.IsMatch(e.Text);
}
/// <summary>
/// 标题栏拖动事件
/// </summary>
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed)
{
DragMove();
}
}
/// <summary>
/// 关闭按钮点击事件
/// </summary>
private void BtnClose_Click(object sender, RoutedEventArgs e)
{
Close();
}
/// <summary>
/// 下拉框选择改变事件
/// </summary>
private void CmbSavedTokens_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
try
{
if (CmbSavedTokens.SelectedItem != null && CmbSavedTokens.SelectedItem.ToString() != "(无保存的Token")
{
var selectedToken = CmbSavedTokens.SelectedItem.ToString();
SaveUserToken(selectedToken);
_apiClient?.Dispose();
InitializeApiClient();
TxtTokenStatus.Text = "已选择Token";
TxtTokenStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94));
_ = TestConnectionAsync();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"选择Token时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 保存Token按钮点击事件
/// </summary>
private void BtnSaveToken_Click(object sender, RoutedEventArgs e)
{
try
{
var token = TxtNewToken.Text?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(token))
{
MessageBox.Show("请输入新的用户Token", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
AddTokenToList(token);
SaveUserToken(token);
_apiClient?.Dispose();
InitializeApiClient();
LoadUserToken();
MessageBox.Show("Token已成功保存并已选择", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
_ = TestConnectionAsync();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存Token时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"保存Token时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 清除Token按钮点击事件
/// </summary>
private void BtnClearToken_Click(object sender, RoutedEventArgs e)
{
try
{
if (CmbSavedTokens.SelectedItem == null || CmbSavedTokens.SelectedItem.ToString() == "(无保存的Token")
{
MessageBox.Show("请先选择一个Token", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var selectedToken = CmbSavedTokens.SelectedItem.ToString();
var result = MessageBox.Show($"确定要删除已选中的Token吗?", "确认", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
RemoveTokenFromList(selectedToken);
if (GetUserToken() == selectedToken)
{
SaveUserToken(string.Empty);
}
_apiClient?.Dispose();
InitializeApiClient();
LoadUserToken();
CmbClassSelection.Items.Clear();
CmbClassSelection.Items.Add("(等待连接)");
CmbClassSelection.SelectedIndex = 0;
CmbClassSelection.IsEnabled = false;
_currentWhiteboards.Clear();
_currentUser = null;
TxtConnectionStatus.Text = "未连接";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170));
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"删除Token时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"删除Token时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 测试Token连接按钮点击事件
/// </summary>
private async void BtnTestToken_Click(object sender, RoutedEventArgs e)
{
await TestConnectionAsync();
}
/// <summary>
/// 保存按钮点击事件
/// </summary>
private async void BtnSave_Click(object sender, RoutedEventArgs e)
{
try
{
// TODO: 根据实际API文档实现保存逻辑
// 示例:保存设置到服务器
// var settings = new { ... };
// await _apiClient.PostAsync<ApiResponse>("/api/settings", settings);
MessageBox.Show("设置已保存", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
Close();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存设置时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"保存设置时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 取消按钮点击事件
/// </summary>
private void BtnCancel_Click(object sender, RoutedEventArgs e)
{
Close();
}
/// <summary>
/// 测试API连接
/// </summary>
private async Task TestConnectionAsync()
{
Dispatcher.Invoke(() =>
{
TxtConnectionStatus.Text = "测试中...";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170)); // 灰色
});
try
{
var userToken = GetUserToken();
if (string.IsNullOrEmpty(userToken))
{
Dispatcher.Invoke(() =>
{
TxtConnectionStatus.Text = "未设置Token";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)); // 红色
});
return;
}
// 根据文档,使用 auth-with-token 接口验证token
// 此接口需要POST请求,包含app_id, app_secret和user_token
try
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var result = await _apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (result != null && result.Success)
{
var whiteboards = result.Whiteboards ?? new List<WhiteboardInfo>();
_currentWhiteboards = whiteboards;
_currentUser = result.User;
var whiteboardCount = whiteboards.Count;
Dispatcher.Invoke(() =>
{
TxtConnectionStatus.Text = $"已连接 (找到 {whiteboardCount} 个白板)";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94));
// 加载班级列表
LoadClasses(whiteboards, result.User);
});
}
else
{
throw new Exception("认证响应失败");
}
}
catch (Exception ex)
{
if (userToken.Length < 10)
{
throw new Exception("Token格式可能不正确(长度过短,至少需要10个字符)");
}
LogHelper.WriteLogToFile($"Token验证失败: {ex.Message}", LogHelper.LogType.Error);
throw;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"Dlass API连接测试失败: {ex.Message}", LogHelper.LogType.Error);
Dispatcher.Invoke(() =>
{
TxtConnectionStatus.Text = "连接失败";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68));
// 清空班级列表
CmbClassSelection.Items.Clear();
CmbClassSelection.Items.Add("(无可用班级)");
CmbClassSelection.SelectedIndex = 0;
CmbClassSelection.IsEnabled = false;
_currentWhiteboards.Clear();
});
}
}
}
#region API响应模型
/// <summary>
/// auth-with-token接口响应模型
/// </summary>
public class AuthWithTokenResponse
{
[Newtonsoft.Json.JsonProperty("success")]
public bool Success { get; set; }
[Newtonsoft.Json.JsonProperty("whiteboards")]
public List<WhiteboardInfo> Whiteboards { get; set; }
[Newtonsoft.Json.JsonProperty("count")]
public int Count { get; set; }
[Newtonsoft.Json.JsonProperty("user")]
public UserInfo User { get; set; }
}
/// <summary>
/// 白板信息模型
/// </summary>
public class WhiteboardInfo
{
[Newtonsoft.Json.JsonProperty("id")]
public int Id { get; set; }
[Newtonsoft.Json.JsonProperty("name")]
public string Name { get; set; }
[Newtonsoft.Json.JsonProperty("board_id")]
public string BoardId { get; set; }
[Newtonsoft.Json.JsonProperty("secret_key")]
public string SecretKey { get; set; }
[Newtonsoft.Json.JsonProperty("class_name")]
public string ClassName { get; set; }
[Newtonsoft.Json.JsonProperty("class_id")]
public int ClassId { get; set; }
[Newtonsoft.Json.JsonProperty("is_online")]
public bool IsOnline { get; set; }
[Newtonsoft.Json.JsonProperty("last_heartbeat")]
public string LastHeartbeat { get; set; }
[Newtonsoft.Json.JsonProperty("created_at")]
public string CreatedAt { get; set; }
}
/// <summary>
/// 用户信息模型
/// </summary>
public class UserInfo
{
[Newtonsoft.Json.JsonProperty("id")]
public int Id { get; set; }
[Newtonsoft.Json.JsonProperty("username")]
public string Username { get; set; }
[Newtonsoft.Json.JsonProperty("email")]
public string Email { get; set; }
}
/// <summary>
/// 班级选择项
/// </summary>
public class ClassSelectionItem
{
public string DisplayText { get; set; }
public string ClassName { get; set; }
public string TeacherName { get; set; }
public override string ToString()
{
return DisplayText;
}
}
#endregion
}
@@ -1,9 +1,9 @@
<Window x:Class="Ink_Canvas.FullscreenTimerWindow"
<Window x:Class="Ink_Canvas.Windows.FullscreenTimerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ink_Canvas"
xmlns:local="clr-namespace:Ink_Canvas.Windows"
Topmost="True" Background="Black"
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="False"
WindowState="Maximized" WindowStartupLocation="Manual"
+110 -33
View File
@@ -1,43 +1,115 @@
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Timers;
namespace Ink_Canvas
namespace Ink_Canvas.Windows
{
/// <summary>
/// 全屏计时器窗口
/// </summary>
public partial class FullscreenTimerWindow : Window
{
private NewStyleTimerWindow parentWindow;
private TimerControl parentControl;
private System.Timers.Timer updateTimer;
private Visibility previousTimerContainerVisibility = Visibility.Visible;
public FullscreenTimerWindow(NewStyleTimerWindow parent)
public FullscreenTimerWindow(TimerControl parent)
{
InitializeComponent();
parentWindow = parent;
parentControl = parent;
// 设置窗口位置和大小
this.Left = 0;
this.Top = 0;
this.Width = SystemParameters.PrimaryScreenWidth;
this.Height = SystemParameters.PrimaryScreenHeight;
// 启动更新定时器
updateTimer = new System.Timers.Timer(100);
updateTimer.Elapsed += UpdateTimer_Elapsed;
updateTimer.Start();
parentWindow.TimerCompleted += ParentWindow_TimerCompleted;
parentControl.TimerCompleted += ParentWindow_TimerCompleted;
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
mainWindow.PauseTopmostMaintenance();
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
if (timerContainer != null)
{
previousTimerContainerVisibility = timerContainer.Visibility;
timerContainer.Visibility = Visibility.Collapsed;
}
}
// 确保窗口置顶
Loaded += FullscreenTimerWindow_Loaded;
}
private void FullscreenTimerWindow_Loaded(object sender, RoutedEventArgs e)
{
// 使用延迟确保窗口完全加载后再应用置顶
Dispatcher.BeginInvoke(new Action(() =>
{
ApplyTopmost();
}), System.Windows.Threading.DispatcherPriority.Loaded);
}
#region Win32 API
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
private const int GWL_EXSTYLE = -20;
private const int WS_EX_TOPMOST = 0x00000008;
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint SWP_SHOWWINDOW = 0x0040;
/// <summary>
/// 应用全屏窗口置顶
/// </summary>
private void ApplyTopmost()
{
try
{
var hwnd = new WindowInteropHelper(this).Handle;
if (hwnd == IntPtr.Zero) return;
// 设置WPF的Topmost属性
Topmost = true;
// 使用Win32 API强制置顶
int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
// 使用SetWindowPos确保窗口在最顶层
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"应用全屏窗口置顶失败: {ex.Message}");
}
}
#endregion
private void UpdateTimer_Elapsed(object sender, ElapsedEventArgs e)
{
if (parentWindow != null)
if (parentControl != null)
{
Application.Current.Dispatcher.Invoke(() =>
{
@@ -54,16 +126,16 @@ namespace Ink_Canvas
private bool ShouldCloseWindow()
{
if (parentWindow == null) return true;
if (parentControl == null) return true;
if (MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true)
{
if (parentWindow.IsTimerRunning)
if (parentControl.IsTimerRunning)
{
return false;
}
var remainingTime = parentWindow.GetRemainingTime();
var remainingTime = parentControl.GetRemainingTime();
if (remainingTime.HasValue && remainingTime.Value.TotalSeconds < 0)
{
return false;
@@ -73,16 +145,15 @@ namespace Ink_Canvas
}
else
{
return !parentWindow.IsTimerRunning;
return !parentControl.IsTimerRunning;
}
}
private void UpdateTimeDisplay()
{
if (parentWindow == null) return;
if (parentControl == null) return;
// 获取剩余时间
var remainingTime = parentWindow.GetRemainingTime();
var remainingTime = parentControl.GetRemainingTime();
if (remainingTime.HasValue)
{
var timeSpan = remainingTime.Value;
@@ -93,10 +164,10 @@ namespace Ink_Canvas
if (isOvertimeMode)
{
var totalTimeSpan = parentWindow.GetTotalTimeSpan();
var totalTimeSpan = parentControl.GetTotalTimeSpan();
if (totalTimeSpan.HasValue)
{
var elapsedTime = parentWindow.GetElapsedTime();
var elapsedTime = parentControl.GetElapsedTime();
if (elapsedTime.HasValue)
{
var overtimeSpan = elapsedTime.Value - totalTimeSpan.Value;
@@ -125,15 +196,12 @@ namespace Ink_Canvas
seconds = timeSpan.Seconds;
}
// 更新小时显示
SetDigitDisplay("FullHour1Display", hours / 10, shouldShowRed);
SetDigitDisplay("FullHour2Display", hours % 10, shouldShowRed);
SetDigitDisplay("FullHour1Display", Math.Abs(hours / 10) % 10, shouldShowRed);
SetDigitDisplay("FullHour2Display", (hours % 10 + 10) % 10, shouldShowRed);
// 更新分钟显示
SetDigitDisplay("FullMinute1Display", minutes / 10, shouldShowRed);
SetDigitDisplay("FullMinute2Display", minutes % 10, shouldShowRed);
// 更新秒显示
SetDigitDisplay("FullSecond1Display", seconds / 10, shouldShowRed);
SetDigitDisplay("FullSecond2Display", seconds % 10, shouldShowRed);
@@ -224,23 +292,32 @@ namespace Ink_Canvas
private void ExitFullscreen()
{
// 恢复主窗口
if (parentWindow != null)
{
// 清除全屏模式标志
parentWindow.SetFullscreenMode(false);
parentWindow.Show();
parentWindow.Activate();
parentWindow.WindowState = WindowState.Normal;
}
this.Close();
}
protected override void OnClosed(EventArgs e)
{
if (parentWindow != null)
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
parentWindow.TimerCompleted -= ParentWindow_TimerCompleted;
mainWindow.ResumeTopmostMaintenance();
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
if (timerContainer != null && previousTimerContainerVisibility == Visibility.Visible)
{
timerContainer.Visibility = Visibility.Visible;
// 重置5秒最小化计时
if (parentControl != null)
{
parentControl.UpdateActivityTime();
}
}
}
if (parentControl != null)
{
parentControl.TimerCompleted -= ParentWindow_TimerCompleted;
}
// 清理资源
@@ -1,23 +1,17 @@
<Window x:Class="Ink_Canvas.MinimizedTimerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ink_Canvas"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
Topmost="True" Background="Transparent"
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True"
WindowStartupLocation="Manual" Title="计时器" Height="200" Width="600"
MouseLeftButtonDown="Window_MouseLeftButtonDown" MouseLeftButtonUp="Window_MouseLeftButtonUp"
MouseEnter="Window_MouseEnter" MouseLeave="Window_MouseLeave" MouseMove="Window_MouseMove">
<Window.Resources>
<UserControl x:Class="Ink_Canvas.Windows.MinimizedTimerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="600">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="DigitResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
</UserControl.Resources>
<Border x:Name="MainBorder" Background="{DynamicResource NewTimerWindowBackground}"
CornerRadius="15"
@@ -25,7 +19,9 @@
BorderBrush="{DynamicResource NewTimerWindowBorderBrush}"
Margin="0"
UseLayoutRounding="True"
SnapsToDevicePixels="True">
SnapsToDevicePixels="True"
MouseLeftButtonDown="MainBorder_MouseLeftButtonDown"
Cursor="Hand">
<Grid>
<!-- 时间显示 -->
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20,20,20,20">
@@ -122,4 +118,5 @@
</Button>
</Grid>
</Border>
</Window>
</UserControl>
@@ -0,0 +1,579 @@
using System;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Microsoft.Win32;
using iNKORE.UI.WPF.Modern;
namespace Ink_Canvas.Windows
{
/// <summary>
/// 最小化计时器窗口
/// </summary>
public partial class MinimizedTimerControl : UserControl
{
private TimerControl parentControl;
private System.Timers.Timer updateTimer;
public MinimizedTimerControl()
{
InitializeComponent();
updateTimer = new System.Timers.Timer(100);
updateTimer.Elapsed += UpdateTimer_Elapsed;
updateTimer.Start();
ApplyTheme();
// 监听主题变化事件
SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;
Unloaded += MinimizedTimerControl_Unloaded;
}
private void MinimizedTimerControl_Unloaded(object sender, RoutedEventArgs e)
{
// 取消订阅主题变化事件
SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged;
if (parentControl != null)
{
parentControl.TimerCompleted -= ParentControl_TimerCompleted;
}
if (updateTimer != null)
{
updateTimer.Stop();
updateTimer.Dispose();
}
}
private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
// 当主题变化时,重新应用主题
Application.Current.Dispatcher.Invoke(() =>
{
RefreshTheme();
});
}
/// <summary>
/// 刷新主题
/// </summary>
public void RefreshTheme()
{
try
{
// 重新应用主题
ApplyTheme();
// 强制刷新UI
InvalidateVisual();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"刷新最小化计时器窗口主题出错: {ex.Message}");
}
}
public void SetParentControl(TimerControl parent)
{
if (parentControl != null)
{
parentControl.TimerCompleted -= ParentControl_TimerCompleted;
}
parentControl = parent;
if (parentControl != null)
{
parentControl.TimerCompleted += ParentControl_TimerCompleted;
UpdateTimeDisplay();
}
}
private void UpdateTimer_Elapsed(object sender, ElapsedEventArgs e)
{
if (parentControl != null)
{
Application.Current.Dispatcher.Invoke(() =>
{
if (this.Visibility != Visibility.Visible)
{
return;
}
if (ShouldHide())
{
this.Visibility = Visibility.Collapsed;
var parent = this.Parent as FrameworkElement;
if (parent != null)
{
parent.Visibility = Visibility.Collapsed;
}
return;
}
UpdateTimeDisplay();
});
}
}
private bool ShouldHide()
{
if (parentControl == null) return true;
if (parentControl.IsFullscreenWindowOpen)
{
return true;
}
if (MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true)
{
if (parentControl.IsTimerRunning)
{
return false;
}
var remainingTime = parentControl.GetRemainingTime();
if (remainingTime.HasValue && remainingTime.Value.TotalSeconds < 0)
{
return false;
}
return true;
}
else
{
return !parentControl.IsTimerRunning;
}
}
private void UpdateTimeDisplay()
{
if (parentControl == null) return;
var remainingTime = parentControl.GetRemainingTime();
if (remainingTime.HasValue)
{
var timeSpan = remainingTime.Value;
bool isOvertimeMode = timeSpan.TotalSeconds < 0;
bool shouldShowRed = isOvertimeMode && MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true;
int hours, minutes, seconds;
if (isOvertimeMode)
{
var totalTimeSpan = parentControl.GetTotalTimeSpan();
if (totalTimeSpan.HasValue)
{
var elapsedTime = parentControl.GetElapsedTime();
if (elapsedTime.HasValue)
{
var overtimeSpan = elapsedTime.Value - totalTimeSpan.Value;
hours = (int)overtimeSpan.TotalHours;
minutes = overtimeSpan.Minutes;
seconds = overtimeSpan.Seconds;
}
else
{
hours = 0;
minutes = 0;
seconds = 0;
}
}
else
{
hours = 0;
minutes = 0;
seconds = 0;
}
}
else
{
hours = (int)timeSpan.TotalHours;
minutes = timeSpan.Minutes;
seconds = timeSpan.Seconds;
}
SetDigitDisplay("MinHour1Display", Math.Abs(hours / 10) % 10, shouldShowRed);
SetDigitDisplay("MinHour2Display", (hours % 10 + 10) % 10, shouldShowRed);
SetDigitDisplay("MinMinute1Display", minutes / 10, shouldShowRed);
SetDigitDisplay("MinMinute2Display", minutes % 10, shouldShowRed);
SetDigitDisplay("MinSecond1Display", seconds / 10, shouldShowRed);
SetDigitDisplay("MinSecond2Display", seconds % 10, shouldShowRed);
SetColonDisplay(shouldShowRed);
}
}
private void ParentControl_TimerCompleted(object sender, EventArgs e)
{
Application.Current.Dispatcher.Invoke(() =>
{
Visibility = Visibility.Collapsed;
});
}
private void SetDigitDisplay(string pathName, int digit, bool isRed = false)
{
var path = this.FindName(pathName) as Path;
if (path != null)
{
string resourceKey = $"Digit{digit}";
var geometry = this.FindResource(resourceKey) as Geometry;
if (geometry != null)
{
path.Data = geometry;
}
if (isRed)
{
path.Fill = Brushes.Red;
}
else
{
var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush;
if (defaultBrush != null)
{
path.Fill = defaultBrush;
}
else
{
bool isLightTheme = IsLightTheme();
path.Fill = isLightTheme ? Brushes.Black : Brushes.White;
}
}
}
}
private void SetColonDisplay(bool isRed = false)
{
var colon1 = this.FindName("MinColon1Display") as TextBlock;
var colon2 = this.FindName("MinColon2Display") as TextBlock;
if (colon1 != null)
{
if (isRed)
{
colon1.Foreground = Brushes.Red;
}
else
{
var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush;
if (defaultBrush != null)
{
colon1.Foreground = defaultBrush;
}
else
{
bool isLightTheme = IsLightTheme();
colon1.Foreground = isLightTheme ? Brushes.Black : Brushes.White;
}
}
}
if (colon2 != null)
{
if (isRed)
{
colon2.Foreground = Brushes.Red;
}
else
{
var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush;
if (defaultBrush != null)
{
colon2.Foreground = defaultBrush;
}
else
{
bool isLightTheme = IsLightTheme();
colon2.Foreground = isLightTheme ? Brushes.Black : Brushes.White;
}
}
}
}
private void ApplyTheme()
{
try
{
if (MainWindow.Settings != null)
{
ApplyTheme(MainWindow.Settings);
}
else
{
bool isLightTheme = IsLightTheme();
if (!isLightTheme)
{
SetDarkThemeBorder();
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"应用主题时出错: {ex.Message}");
}
}
private void ApplyTheme(Settings settings)
{
try
{
if (settings.Appearance.Theme == 0) // 浅色主题
{
ThemeManager.SetRequestedTheme(this, ElementTheme.Light);
}
else if (settings.Appearance.Theme == 1) // 深色主题
{
ThemeManager.SetRequestedTheme(this, ElementTheme.Dark);
SetDarkThemeBorder();
}
else // 跟随系统主题
{
bool isSystemLight = IsSystemThemeLight();
if (isSystemLight)
{
ThemeManager.SetRequestedTheme(this, ElementTheme.Light);
}
else
{
ThemeManager.SetRequestedTheme(this, ElementTheme.Dark);
SetDarkThemeBorder();
}
}
// 刷新数字和冒号显示的颜色
if (parentControl != null)
{
UpdateTimeDisplay();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"应用最小化计时器窗口主题出错: {ex.Message}");
}
}
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)
{
var value = themeKey.GetValue("AppsUseLightTheme");
if (value != null)
{
light = (int)value == 1;
}
themeKey.Close();
}
}
catch
{
// 如果读取注册表失败,默认为浅色主题
light = true;
}
return light;
}
private bool IsLightTheme()
{
try
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
var currentModeField = mainWindow.GetType().GetField("currentMode",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (currentModeField != null)
{
var currentMode = currentModeField.GetValue(mainWindow);
return currentMode?.ToString() == "Light";
}
}
}
catch
{
}
return true;
}
private void SetDarkThemeBorder()
{
try
{
var border = this.FindName("MainBorder") as Border;
if (border != null)
{
border.BorderBrush = new SolidColorBrush(Color.FromRgb(64, 64, 64));
}
}
catch
{
}
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
if (parentControl != null)
{
parentControl.StopTimer();
}
Visibility = Visibility.Collapsed;
}
private bool isDragging = false;
private bool isDragStarted = false;
private Point dragStartPoint;
private Point containerStartPosition;
private const double DragThreshold = 5.0; // 拖动阈值,像素
private void MainBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount == 2)
{
// 双击:恢复主窗口
if (parentControl != null)
{
parentControl.UpdateActivityTime();
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement;
if (timerContainer != null && minimizedContainer != null)
{
timerContainer.Visibility = Visibility.Visible;
minimizedContainer.Visibility = Visibility.Collapsed;
}
}
}
e.Handled = true;
}
else if (e.ClickCount == 1)
{
// 单击:准备拖动或点击
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement;
if (minimizedContainer != null)
{
var point = e.GetPosition(minimizedContainer);
var mainWindowPoint = minimizedContainer.TransformToAncestor(mainWindow).Transform(point);
// 初始化拖动状态,但不立即开始拖动
isDragging = false;
isDragStarted = false;
dragStartPoint = mainWindowPoint;
var margin = minimizedContainer.Margin;
containerStartPosition = new Point(margin.Left, margin.Top);
if (double.IsNaN(containerStartPosition.X) || containerStartPosition.X < 0) containerStartPosition.X = 0;
if (double.IsNaN(containerStartPosition.Y) || containerStartPosition.Y < 0) containerStartPosition.Y = 0;
// 捕获鼠标并订阅事件,等待判断是拖动还是点击
minimizedContainer.CaptureMouse();
minimizedContainer.MouseMove += MinimizedContainer_MouseMove;
minimizedContainer.MouseLeftButtonUp += MinimizedContainer_MouseLeftButtonUp;
e.Handled = true;
}
}
}
}
private void MinimizedContainer_MouseMove(object sender, MouseEventArgs e)
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow == null) return;
var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement;
if (minimizedContainer == null) return;
var currentPoint = e.GetPosition(mainWindow);
var deltaX = currentPoint.X - dragStartPoint.X;
var deltaY = currentPoint.Y - dragStartPoint.Y;
var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
// 如果移动距离超过阈值,开始拖动
if (!isDragStarted && distance > DragThreshold)
{
isDragStarted = true;
isDragging = true;
}
// 如果已经开始拖动,更新位置
if (isDragging)
{
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
var newX = containerStartPosition.X + deltaX;
var newY = containerStartPosition.Y + deltaY;
if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
minimizedContainer.Margin = new Thickness(newX, newY, 0, 0);
minimizedContainer.HorizontalAlignment = HorizontalAlignment.Left;
minimizedContainer.VerticalAlignment = VerticalAlignment.Top;
if (timerContainer != null)
{
timerContainer.Margin = new Thickness(newX, newY, 0, 0);
timerContainer.HorizontalAlignment = HorizontalAlignment.Left;
timerContainer.VerticalAlignment = VerticalAlignment.Top;
}
}
}
private void MinimizedContainer_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow == null) return;
var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement;
if (minimizedContainer != null)
{
minimizedContainer.ReleaseMouseCapture();
minimizedContainer.MouseMove -= MinimizedContainer_MouseMove;
minimizedContainer.MouseLeftButtonUp -= MinimizedContainer_MouseLeftButtonUp;
}
// 如果没有开始拖动(移动距离小于阈值),则视为单击,恢复主窗口
if (!isDragStarted)
{
if (parentControl != null)
{
parentControl.UpdateActivityTime();
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
if (timerContainer != null && minimizedContainer != null)
{
timerContainer.Visibility = Visibility.Visible;
minimizedContainer.Visibility = Visibility.Collapsed;
}
}
}
isDragging = false;
isDragStarted = false;
}
}
}
@@ -1,392 +0,0 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Media.Animation;
namespace Ink_Canvas
{
/// <summary>
/// 最小化计时器窗口
/// </summary>
public partial class MinimizedTimerWindow : Window
{
private NewStyleTimerWindow parentWindow;
private System.Timers.Timer updateTimer;
private bool isMouseOver = false;
private bool isDragging = false;
private Point lastMousePosition;
public MinimizedTimerWindow(NewStyleTimerWindow parent)
{
InitializeComponent();
parentWindow = parent;
// 设置窗口位置
this.Left = parent.Left;
this.Top = parent.Top;
// 启动更新定时器
updateTimer = new System.Timers.Timer(100); // 100ms更新一次
updateTimer.Elapsed += UpdateTimer_Elapsed;
updateTimer.Start();
parentWindow.TimerCompleted += ParentWindow_TimerCompleted;
// 应用主题
ApplyTheme();
}
private void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
if (parentWindow != null)
{
Application.Current.Dispatcher.Invoke(() =>
{
if (ShouldCloseWindow())
{
this.Close();
return;
}
UpdateTimeDisplay();
});
}
}
private bool ShouldCloseWindow()
{
if (parentWindow == null) return true;
if (MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true)
{
if (parentWindow.IsTimerRunning)
{
return false;
}
var remainingTime = parentWindow.GetRemainingTime();
if (remainingTime.HasValue && remainingTime.Value.TotalSeconds < 0)
{
return false;
}
return true;
}
else
{
return !parentWindow.IsTimerRunning;
}
}
private void UpdateTimeDisplay()
{
if (parentWindow == null) return;
// 获取剩余时间
var remainingTime = parentWindow.GetRemainingTime();
if (remainingTime.HasValue)
{
var timeSpan = remainingTime.Value;
bool isOvertimeMode = timeSpan.TotalSeconds < 0;
bool shouldShowRed = isOvertimeMode && MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true;
int hours, minutes, seconds;
if (isOvertimeMode)
{
var totalTimeSpan = parentWindow.GetTotalTimeSpan();
if (totalTimeSpan.HasValue)
{
var elapsedTime = parentWindow.GetElapsedTime();
if (elapsedTime.HasValue)
{
var overtimeSpan = elapsedTime.Value - totalTimeSpan.Value;
hours = (int)overtimeSpan.TotalHours;
minutes = overtimeSpan.Minutes;
seconds = overtimeSpan.Seconds;
}
else
{
hours = 0;
minutes = 0;
seconds = 0;
}
}
else
{
hours = 0;
minutes = 0;
seconds = 0;
}
}
else
{
hours = (int)timeSpan.TotalHours;
minutes = timeSpan.Minutes;
seconds = timeSpan.Seconds;
}
// 更新小时显示
SetDigitDisplay("MinHour1Display", hours / 10, shouldShowRed);
SetDigitDisplay("MinHour2Display", hours % 10, shouldShowRed);
// 更新分钟显示
SetDigitDisplay("MinMinute1Display", minutes / 10, shouldShowRed);
SetDigitDisplay("MinMinute2Display", minutes % 10, shouldShowRed);
// 更新秒显示
SetDigitDisplay("MinSecond1Display", seconds / 10, shouldShowRed);
SetDigitDisplay("MinSecond2Display", seconds % 10, shouldShowRed);
SetColonDisplay(shouldShowRed);
}
}
private void ParentWindow_TimerCompleted(object sender, EventArgs e)
{
Application.Current.Dispatcher.Invoke(() =>
{
this.Close();
});
}
private void SetDigitDisplay(string pathName, int digit, bool isRed = false)
{
var path = this.FindName(pathName) as Path;
if (path != null)
{
string resourceKey = $"Digit{digit}";
var geometry = this.FindResource(resourceKey) as Geometry;
if (geometry != null)
{
path.Data = geometry;
}
// 设置颜色
if (isRed)
{
path.Fill = Brushes.Red;
}
else
{
var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush;
if (defaultBrush != null)
{
path.Fill = defaultBrush;
}
else
{
bool isLightTheme = IsLightTheme();
path.Fill = isLightTheme ? Brushes.Black : Brushes.White;
}
}
}
}
/// <summary>
/// 设置最小化窗口冒号显示颜色
/// </summary>
/// <param name="isRed">是否显示为红色</param>
private void SetColonDisplay(bool isRed = false)
{
var colon1 = this.FindName("MinColon1Display") as TextBlock;
var colon2 = this.FindName("MinColon2Display") as TextBlock;
if (colon1 != null)
{
if (isRed)
{
colon1.Foreground = Brushes.Red;
}
else
{
var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush;
if (defaultBrush != null)
{
colon1.Foreground = defaultBrush;
}
else
{
bool isLightTheme = IsLightTheme();
colon1.Foreground = isLightTheme ? Brushes.Black : Brushes.White;
}
}
}
if (colon2 != null)
{
if (isRed)
{
colon2.Foreground = Brushes.Red;
}
else
{
var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush;
if (defaultBrush != null)
{
colon2.Foreground = defaultBrush;
}
else
{
bool isLightTheme = IsLightTheme();
colon2.Foreground = isLightTheme ? Brushes.Black : Brushes.White;
}
}
}
}
private void ApplyTheme()
{
try
{
bool isLightTheme = IsLightTheme();
if (!isLightTheme)
{
SetDarkThemeBorder();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"应用主题时出错: {ex.Message}");
}
}
private bool IsLightTheme()
{
try
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
var currentModeField = mainWindow.GetType().GetField("currentMode",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (currentModeField != null)
{
var currentMode = currentModeField.GetValue(mainWindow);
return currentMode?.ToString() == "Light";
}
}
}
catch
{
// 如果获取主题失败,默认使用浅色主题
}
return true;
}
// 设置深色主题下的灰色边框
private void SetDarkThemeBorder()
{
try
{
// 找到Border元素并设置灰色边框
var border = this.FindName("MainBorder") as Border;
if (border != null)
{
border.BorderBrush = new SolidColorBrush(Color.FromRgb(64, 64, 64));
}
}
catch
{
}
}
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// 记录点击时间
lastClickTime = DateTime.Now;
// 开始拖动
isDragging = true;
lastMousePosition = e.GetPosition(this);
this.CaptureMouse();
}
private void Window_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
var currentPosition = e.GetPosition(this);
var deltaX = currentPosition.X - lastMousePosition.X;
var deltaY = currentPosition.Y - lastMousePosition.Y;
this.Left += deltaX;
this.Top += deltaY;
}
}
private void Window_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (isDragging)
{
isDragging = false;
this.ReleaseMouseCapture();
// 如果点击时间很短,认为是单击,恢复主窗口
var clickDuration = DateTime.Now - lastClickTime;
if (clickDuration.TotalMilliseconds < 200) // 200ms内认为是单击
{
// 恢复主窗口
if (parentWindow != null)
{
parentWindow.Show();
parentWindow.Activate();
parentWindow.WindowState = WindowState.Normal;
this.Close();
}
}
}
}
private DateTime lastClickTime = DateTime.Now;
private void Window_MouseEnter(object sender, MouseEventArgs e)
{
isMouseOver = true;
// 鼠标进入时显示关闭按钮
if (CloseButton != null)
{
CloseButton.Opacity = 1.0;
}
}
private void Window_MouseLeave(object sender, MouseEventArgs e)
{
isMouseOver = false;
// 鼠标离开时隐藏关闭按钮
if (CloseButton != null)
{
CloseButton.Opacity = 0.7;
}
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
// 停止计时器并关闭窗口
if (parentWindow != null)
{
parentWindow.StopTimer();
}
this.Close();
}
protected override void OnClosed(EventArgs e)
{
if (parentWindow != null)
{
parentWindow.TimerCompleted -= ParentWindow_TimerCompleted;
}
// 清理资源
if (updateTimer != null)
{
updateTimer.Stop();
updateTimer.Dispose();
}
base.OnClosed(e);
}
}
}
+32 -7
View File
@@ -5,15 +5,40 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ink_Canvas"
mc:Ignorable="d" FontFamily="Microsoft YaHei UI" ui:WindowHelper.UseModernWindowStyle="True"
ui:ThemeManager.RequestedTheme="Light" WindowStartupLocation="CenterScreen"
WindowStartupLocation="CenterScreen"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" Topmost="True"
Title="Ink Canvas 抽奖 - 名单导入" Height="500" Width="400"
Loaded="Window_Loaded" Closing="Window_Closing">
<Grid>
<Label Content="请在下方输入名单,每行一人(建议直接粘贴表格姓名列)" Margin="10"/>
<TextBox Name="TextBoxNames" FontFamily="Microsoft YaHei UI" VerticalScrollBarVisibility="Auto" AcceptsReturn="True" Margin="10,40,10,50" />
<Button Margin="10" VerticalAlignment="Bottom" HorizontalAlignment="Right"
Content="关闭" FontFamily="Microsoft YaHei UI"
Width="100" Click="Button_Click"/>
<Window.Resources>
<!-- 主题资源 -->
<SolidColorBrush x:Key="NamesInputWindowBackground" Color="White"/>
<SolidColorBrush x:Key="NamesInputWindowForeground" Color="Black"/>
<SolidColorBrush x:Key="NamesInputWindowButtonBackground" Color="#F4F4F5"/>
<SolidColorBrush x:Key="NamesInputWindowButtonForeground" Color="Black"/>
<SolidColorBrush x:Key="NamesInputWindowBorderBrush" Color="#E4E4E7"/>
</Window.Resources>
<Grid Background="{DynamicResource NamesInputWindowBackground}">
<Label Content="请在下方输入名单,每行一人(建议直接粘贴表格姓名列)"
Margin="10"
Foreground="{DynamicResource NamesInputWindowForeground}"
FontFamily="Microsoft YaHei UI"/>
<TextBox Name="TextBoxNames"
FontFamily="Microsoft YaHei UI"
VerticalScrollBarVisibility="Auto"
AcceptsReturn="True"
Margin="10,40,10,50"
Background="{DynamicResource NamesInputWindowBackground}"
Foreground="{DynamicResource NamesInputWindowForeground}"
BorderBrush="{DynamicResource NamesInputWindowBorderBrush}"/>
<Button Margin="10"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Content="关闭"
FontFamily="Microsoft YaHei UI"
Width="100"
Click="Button_Click"
Background="{DynamicResource NamesInputWindowButtonBackground}"
Foreground="{DynamicResource NamesInputWindowButtonForeground}"
BorderBrush="{DynamicResource NamesInputWindowBorderBrush}"/>
</Grid>
</Window>
+110
View File
@@ -1,7 +1,10 @@
using Ink_Canvas.Helpers;
using System;
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Media;
using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
namespace Ink_Canvas
{
@@ -14,6 +17,7 @@ namespace Ink_Canvas
{
InitializeComponent();
AnimationsHelper.ShowWithSlideFromBottomAndFade(this, 0.25);
ApplyTheme();
}
string originText = "";
@@ -43,5 +47,111 @@ namespace Ink_Canvas
{
Close();
}
private void ApplyTheme()
{
try
{
if (MainWindow.Settings != null)
{
ApplyTheme(MainWindow.Settings);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用名单导入窗口主题出错: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ApplyTheme(Settings settings)
{
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["NamesInputWindowBackground"] = new SolidColorBrush(Color.FromRgb(255, 255, 255));
resources["NamesInputWindowForeground"] = new SolidColorBrush(Color.FromRgb(24, 24, 27));
resources["NamesInputWindowButtonBackground"] = new SolidColorBrush(Color.FromRgb(244, 244, 245));
resources["NamesInputWindowButtonForeground"] = new SolidColorBrush(Color.FromRgb(24, 24, 27));
resources["NamesInputWindowBorderBrush"] = new SolidColorBrush(Color.FromRgb(228, 228, 231));
}
else
{
// 应用深色主题资源 - 与新计时器窗口统一
resources["NamesInputWindowBackground"] = new SolidColorBrush(Color.FromRgb(31, 31, 31)); // #1f1f1f
resources["NamesInputWindowForeground"] = new SolidColorBrush(Colors.White);
resources["NamesInputWindowButtonBackground"] = new SolidColorBrush(Color.FromRgb(42, 42, 42)); // #2a2a2a
resources["NamesInputWindowButtonForeground"] = new SolidColorBrush(Colors.White);
resources["NamesInputWindowBorderBrush"] = new SolidColorBrush(Color.FromRgb(224, 224, 224)); // #E0E0E0
}
}
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)
{
var value = themeKey.GetValue("AppsUseLightTheme");
if (value != null)
{
light = (int)value == 1;
}
themeKey.Close();
}
}
catch
{
// 如果无法读取注册表,默认使用浅色主题
light = true;
}
return light;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,541 @@
<Window x:Class="Ink_Canvas.NewStyleRollCallWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ink_Canvas"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:controls="clr-namespace:Ink_Canvas.Windows.Controls"
Topmost="True" Background="Transparent"
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True"
Loaded="RollCallWindow_Loaded" Closing="Window_Closing" WindowStartupLocation="CenterScreen"
MouseMove="Window_MouseMove" MouseEnter="Window_MouseEnter"
Title="Ink Canvas 画板 - 点名" Height="500" Width="800">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Controls/WinUI3CloseButton.xaml" />
<ResourceDictionary Source="../Resources/NewRollCallWindowResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Border Background="{DynamicResource NewRollCallWindowBackground}" CornerRadius="15" BorderThickness="1" BorderBrush="{DynamicResource NewRollCallWindowBorderBrush}" Margin="10" x:Name="MainBorder" MouseLeftButtonDown="WindowDragMove">
<Grid>
<controls:WinUI3CloseButton x:Name="CloseButton"
HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,0,0,0" Cursor="Hand" Click="CloseButton_Click"
Content="✕"/>
<!-- 主要内容区域 -->
<Grid>
<!-- 使用Viewbox自动缩放内容 -->
<Viewbox x:Name="MainViewController" Margin="20,20,20,20">
<Grid Height="550" Width="1000">
<!-- 顶部标题栏 -->
<Grid Height="50" Background="{DynamicResource NewRollCallWindowBackground}" x:Name="TitleBar" VerticalAlignment="Top" MouseLeftButtonDown="WindowDragMove" Margin="0,0,450,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="22,0,0,0">
<!-- 点名图标 -->
<Path Data="M5 7C5 8.06087 5.42143 9.07828 6.17157 9.82843C6.92172 10.5786 7.93913 11 9 11C10.0609 11 11.0783 10.5786 11.8284 9.82843C12.5786 9.07828 13 8.06087 13 7C13 5.93913 12.5786 4.92172 11.8284 4.17157C11.0783 3.42143 10.0609 3 9 3C7.93913 3 6.92172 3.42143 6.17157 4.17157C5.42143 4.92172 5 5.93913 5 7Z M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21 M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88 M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15"
Stroke="{DynamicResource NewRollCallWindowButtonForeground}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="Transparent"
Width="24" Height="24"
Stretch="Uniform"
Margin="0,0,8,0"/>
<!-- 点名文字 -->
<TextBlock Text="点名" FontSize="28" FontWeight="Bold"
Foreground="{DynamicResource NewRollCallWindowTitleForeground}" x:Name="TitleText"/>
</StackPanel>
</Grid>
<!-- 主要内容区域 - 分为左右两部分 -->
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Center"
x:Name="MainContentGrid" Margin="0,-25,0,25">
<!-- 左侧:结果显示区域 -->
<Grid HorizontalAlignment="Left" VerticalAlignment="Center"
x:Name="MainDisplayGrid" Width="500" Margin="-10,0,0,0">
<!-- 结果显示区域 -->
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- 主结果显示 -->
<TextBlock x:Name="MainResultDisplay" Text="点击开始点名" FontSize="48" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" Margin="0,0,0,20"/>
<!-- 多结果显示区域 - 支持最多20个结果 -->
<ScrollViewer x:Name="MultiResultScrollViewer" MaxHeight="200" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled" Visibility="Collapsed">
<StackPanel x:Name="MultiResultPanel" Orientation="Vertical" HorizontalAlignment="Center">
<!-- 第1行:结果1-5 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,8">
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result1Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result2Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result3Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result4Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" ClipToBounds="True">
<TextBlock x:Name="Result5Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
</StackPanel>
<!-- 第2行:结果6-10 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,8">
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result6Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result7Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result8Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result9Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" ClipToBounds="True">
<TextBlock x:Name="Result10Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
</StackPanel>
<!-- 第3行:结果11-15 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,8">
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result11Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result12Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result13Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result14Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" ClipToBounds="True">
<TextBlock x:Name="Result15Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
</StackPanel>
<!-- 第4行:结果16-20 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result16Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result17Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result18Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" Margin="0,0,12,0" ClipToBounds="True">
<TextBlock x:Name="Result19Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
<Viewbox Width="60" Height="36" Stretch="Uniform" ClipToBounds="True">
<TextBlock x:Name="Result20Display" Text="" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
TextAlignment="Center" />
</Viewbox>
</StackPanel>
</StackPanel>
</ScrollViewer>
<!-- 点名状态显示 -->
<TextBlock x:Name="StatusDisplay" Text="准备就绪" FontSize="16"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowSecondaryTextForeground}"
TextAlignment="Center" Margin="0,10,0,0"/>
</StackPanel>
</Grid>
<!-- 分割线 -->
<Border Width="2" Background="{DynamicResource NewRollCallWindowButtonForeground}"
Opacity="0.3" HorizontalAlignment="Left" VerticalAlignment="Stretch"
Margin="500,0,0,0"/>
<!-- 右侧:控制选项区域 -->
<Grid HorizontalAlignment="Left" VerticalAlignment="Center"
x:Name="ControlOptionsGrid" Width="450" Margin="520,0,0,0" Height="400">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<!-- 人数控制区域 -->
<Grid Margin="0,0,0,20">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<TextBlock Text="点名人数" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"
HorizontalAlignment="Center" Margin="0,0,0,10"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="CountMinusBtn" Width="40" Height="40" Background="{DynamicResource NewRollCallWindowButtonBackground}"
BorderThickness="0" Click="CountMinus_Click" Cursor="Hand" Margin="0,0,15,0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M5 12l14 0"
Stroke="{DynamicResource NewRollCallWindowButtonForeground}"
StrokeThickness="2"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
<TextBlock x:Name="CountDisplay" Text="1" FontSize="24" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource NewRollCallWindowDigitForeground}"
Width="60" TextAlignment="Center"/>
<Button x:Name="CountPlusBtn" Width="40" Height="40" Background="{DynamicResource NewRollCallWindowButtonBackground}"
BorderThickness="0" Click="CountPlus_Click" Cursor="Hand" Margin="15,0,0,0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M12 5l0 14 M5 12l14 0"
Stroke="{DynamicResource NewRollCallWindowButtonForeground}"
StrokeThickness="2"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
</StackPanel>
</StackPanel>
</Grid>
<!-- 点名模式选择 -->
<Grid Margin="0,0,0,20">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<TextBlock Text="点名模式" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"
HorizontalAlignment="Center" Margin="0,0,0,10"/>
<Border Background="{DynamicResource NewRollCallWindowButtonBackground}"
CornerRadius="8"
BorderThickness="1"
BorderBrush="#616161"
Width="300" Height="40">
<Grid>
<!-- 背景指示器 -->
<Border x:Name="SegmentedIndicator"
Background="{DynamicResource NewRollCallWindowPrimaryButtonBackground}"
CornerRadius="7.5,0,0,7.5"
Width="100" Height="38"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="0,0,0,0"/>
<!-- 按钮容器 -->
<Grid>
<Button x:Name="RandomModeBtn"
Width="100" Height="40"
Background="Transparent"
BorderThickness="0"
Click="RandomMode_Click"
Cursor="Hand"
HorizontalAlignment="Left">
<TextBlock x:Name="RandomModeText" Text="随机点名" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/>
</Button>
<Button x:Name="SequentialModeBtn"
Width="100" Height="40"
Background="Transparent"
BorderThickness="0"
Click="SequentialMode_Click"
Cursor="Hand"
HorizontalAlignment="Center">
<TextBlock x:Name="SequentialModeText" Text="顺序点名" FontSize="16" FontWeight="Normal"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}" Opacity="0.6"/>
</Button>
<Button x:Name="GroupModeBtn"
Width="100" Height="40"
Background="Transparent"
BorderThickness="0"
Click="GroupMode_Click"
Cursor="Hand"
HorizontalAlignment="Right">
<TextBlock x:Name="GroupModeText" Text="分组点名" FontSize="16" FontWeight="Normal"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}" Opacity="0.6"/>
</Button>
</Grid>
</Grid>
</Border>
</StackPanel>
</Grid>
<!-- 外部点名区域 -->
<Grid Margin="0,0,0,20">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<TextBlock Text="外部点名" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"
HorizontalAlignment="Center" Margin="0,0,0,10"/>
<!-- 外部点名模式选择 -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- 外部点名模式Tab -->
<Border Grid.Column="0"
Background="{DynamicResource NewRollCallWindowButtonBackground}"
CornerRadius="8"
BorderThickness="1"
BorderBrush="#616161"
Width="100" Height="40">
<Grid>
<!-- 背景指示器 -->
<Border x:Name="ExternalCallerModeIndicator"
Background="{DynamicResource NewRollCallWindowPrimaryButtonBackground}"
CornerRadius="7.5"
Width="100" Height="38"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="0,0,0,0"
Visibility="Collapsed"/>
<!-- 按钮容器 -->
<Button x:Name="ExternalCallerModeBtn"
Width="100" Height="40"
Background="Transparent"
BorderThickness="0"
Click="ExternalCallerMode_Click"
Cursor="Hand">
<TextBlock x:Name="ExternalCallerModeText" Text="外部点名" FontSize="16" FontWeight="Normal"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}" Opacity="0.6"/>
</Button>
</Grid>
</Border>
<!-- 外部点名类型下拉框 -->
<ComboBox x:Name="ExternalCallerTypeComboBox"
Grid.Column="2"
Width="160" Height="40"
IsEditable="False"
IsReadOnly="True"
SelectedIndex="0"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"
SelectionChanged="ExternalCallerTypeComboBox_SelectionChanged">
<ComboBoxItem Content="ClassIsland" IsSelected="True"/>
<ComboBoxItem Content="SecRandom"/>
<ComboBoxItem Content="NamePicker"/>
</ComboBox>
</Grid>
</StackPanel>
</Grid>
<!-- 名单管理区域 -->
<Grid Margin="0,0,0,20">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<TextBlock Text="名单管理" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"
HorizontalAlignment="Center" Margin="0,0,0,10"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="ImportListBtn" Width="90" Height="40" Background="{DynamicResource NewRollCallWindowButtonBackground}"
BorderThickness="0" Click="ImportList_Click" Cursor="Hand" Margin="0,0,10,0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="8">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<TextBlock Text="导入名单" FontSize="14"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/>
</Button>
<Button x:Name="ClearListBtn" Width="90" Height="40" Background="{DynamicResource NewRollCallWindowButtonBackground}"
BorderThickness="0" Click="ClearList_Click" Cursor="Hand" Margin="0,0,10,0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="8">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<TextBlock Text="清空名单" FontSize="14"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/>
</Button>
<Button x:Name="ViewHistoryBtn" Width="90" Height="40" Background="{DynamicResource NewRollCallWindowButtonBackground}"
BorderThickness="0" Click="ViewHistory_Click" Cursor="Hand">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="8">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<TextBlock Text="查看历史" FontSize="14"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/>
</Button>
</StackPanel>
</StackPanel>
</Grid>
<!-- 名单统计信息 -->
<Grid Margin="0,0,0,20">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<TextBlock x:Name="ListCountDisplay" Text="名单人数: 0" FontSize="14"
Foreground="{DynamicResource NewRollCallWindowSecondaryTextForeground}"
HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</Grid>
<!-- 底部控制按钮区域 -->
<Grid HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,20">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<!-- 开始点名按钮 -->
<Button x:Name="StartRollCallBtn" Width="140" Height="60" Background="{DynamicResource NewRollCallWindowPrimaryButtonBackground}"
BorderThickness="0" Click="StartRollCall_Click" Cursor="Hand" Margin="0,0,20,0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="30">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Path x:Name="StartRollCallBtnIcon" Data="M5 7C5 8.06087 5.42143 9.07828 6.17157 9.82843C6.92172 10.5786 7.93913 11 9 11C10.0609 11 11.0783 10.5786 11.8284 9.82843C12.5786 9.07828 13 8.06087 13 7C13 5.93913 12.5786 4.92172 11.8284 4.17157C11.0783 3.42143 10.0609 3 9 3C7.93913 3 6.92172 3.42143 6.17157 4.17157C5.42143 4.92172 5 5.93913 5 7Z M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21 M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88 M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15"
Stroke="{DynamicResource NewRollCallWindowPrimaryButtonForeground}"
StrokeThickness="2"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"
Margin="0,0,8,0"/>
<TextBlock x:Name="StartRollCallBtnText" Text="开始点名" FontSize="18" FontWeight="Bold"
Foreground="{DynamicResource NewRollCallWindowPrimaryButtonForeground}"/>
</StackPanel>
</Button>
<!-- 停止点名按钮 -->
<Button x:Name="StopRollCallBtn" Width="100" Height="60" Background="{DynamicResource NewRollCallWindowButtonBackground}"
BorderThickness="0" Click="StopRollCall_Click" Cursor="Hand" Margin="0,0,20,0" Visibility="Collapsed">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="30">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Path Data="M6 6l12 12 M18 6l-12 12"
Stroke="{DynamicResource NewRollCallWindowButtonForeground}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"
Margin="0,0,8,0"/>
<TextBlock Text="停止" FontSize="18" FontWeight="Bold"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/>
</StackPanel>
</Button>
<!-- 重置按钮 -->
<Button x:Name="ResetBtn" Width="140" Height="60" Background="{DynamicResource NewRollCallWindowButtonBackground}"
BorderThickness="0" Click="Reset_Click" Cursor="Hand">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="30">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Path Data="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4 M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
Stroke="{DynamicResource NewRollCallWindowButtonForeground}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"
Margin="0,0,8,0"/>
<TextBlock Text="重置" FontSize="18" FontWeight="Bold"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/>
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Grid>
</Viewbox>
</Grid>
</Grid>
</Border>
</Window>
+303
View File
@@ -0,0 +1,303 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using System.IO;
namespace Ink_Canvas
{
/// <summary>
/// 快抽窗口
/// </summary>
public partial class QuickDrawWindow : Window
{
private Random random = new Random();
private int autoCloseWaitTime = 2500; // 自动关闭等待时间(毫秒)
private List<string> nameList = new List<string>(); // 名单列表
public QuickDrawWindow()
{
InitializeComponent();
this.Focusable = false;
this.ShowInTaskbar = false;
InitializeSettings();
LoadNamesFromFile();
StartQuickDraw();
}
private void InitializeSettings()
{
try
{
if (MainWindow.Settings?.RandSettings != null)
{
autoCloseWaitTime = (int)MainWindow.Settings.RandSettings.RandWindowOnceCloseLatency * 1000;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化快抽窗口设置失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void LoadNamesFromFile()
{
try
{
string namesFilePath = App.RootPath + "Names.txt";
if (File.Exists(namesFilePath))
{
string content = File.ReadAllText(namesFilePath);
nameList = content.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.Select(name => name.Trim())
.Where(name => !string.IsNullOrEmpty(name))
.ToList();
}
else
{
nameList.Clear();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载名单文件失败: {ex.Message}", LogHelper.LogType.Error);
nameList.Clear();
}
}
private void StartQuickDraw()
{
try
{
// 延迟100ms后开始抽选动画
new System.Threading.Thread(() =>
{
System.Threading.Thread.Sleep(100);
Application.Current.Dispatcher.Invoke(() =>
{
StartQuickDrawAnimation();
});
}).Start();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"开始快抽失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 快抽动画
/// </summary>
private void StartQuickDrawAnimation()
{
const int animationTimes = 100; // 动画次数
const int sleepTime = 5; // 每次动画间隔(毫秒)
new System.Threading.Thread(() =>
{
if (nameList.Count > 0)
{
// 有名单时,从名单中抽选
StartNameDrawAnimation(animationTimes, sleepTime);
}
else
{
// 没有名单时,从1-60数字中抽选
StartNumberDrawAnimation(animationTimes, sleepTime);
}
}).Start();
}
/// <summary>
/// 名单抽选动画
/// </summary>
private void StartNameDrawAnimation(int animationTimes, int sleepTime)
{
List<string> usedNames = new List<string>();
for (int i = 0; i < animationTimes; i++)
{
// 随机选择一个名字进行动画显示,避免立即重复
string randomName;
do
{
randomName = nameList[random.Next(0, nameList.Count)];
} while (usedNames.Count > 0 && usedNames[usedNames.Count - 1] == randomName);
usedNames.Add(randomName);
Application.Current.Dispatcher.Invoke(() =>
{
MainResultDisplay.Text = randomName;
});
System.Threading.Thread.Sleep(sleepTime);
}
// 动画结束,显示最终结果
Application.Current.Dispatcher.Invoke(() =>
{
// 使用降重抽选方法选择最终名字
var selectedNames = NewStyleRollCallWindow.SelectNamesWithML(nameList, 1, random);
string finalName = selectedNames.Count > 0 ? selectedNames[0] : nameList[random.Next(0, nameList.Count)];
MainResultDisplay.Text = finalName;
// 更新历史记录
NewStyleRollCallWindow.UpdateRollCallHistory(new List<string> { finalName });
});
// 显示结果后,等待一段时间让用户看到结果,然后关闭窗口
new System.Threading.Thread(() =>
{
System.Threading.Thread.Sleep(autoCloseWaitTime);
Application.Current.Dispatcher.Invoke(() =>
{
Close();
});
}).Start();
}
/// <summary>
/// 数字抽选动画
/// </summary>
private void StartNumberDrawAnimation(int animationTimes, int sleepTime)
{
List<int> usedNumbers = new List<int>();
for (int i = 0; i < animationTimes; i++)
{
// 随机选择一个数字进行动画显示,避免立即重复
int randomNumber;
do
{
randomNumber = random.Next(1, 61); // 1-60
} while (usedNumbers.Count > 0 && usedNumbers[usedNumbers.Count - 1] == randomNumber);
usedNumbers.Add(randomNumber);
Application.Current.Dispatcher.Invoke(() =>
{
MainResultDisplay.Text = randomNumber.ToString();
});
System.Threading.Thread.Sleep(sleepTime);
}
// 动画结束,显示最终结果
Application.Current.Dispatcher.Invoke(() =>
{
// 使用降重抽选方法选择最终数字
var numberList = Enumerable.Range(1, 60).Select(n => n.ToString()).ToList();
var selectedNumbers = NewStyleRollCallWindow.SelectNamesWithML(numberList, 1, random);
string finalNumber = selectedNumbers.Count > 0 ? selectedNumbers[0] : random.Next(1, 61).ToString();
MainResultDisplay.Text = finalNumber;
// 更新历史记录
NewStyleRollCallWindow.UpdateRollCallHistory(new List<string> { finalNumber });
});
// 显示结果后,等待一段时间让用户看到结果,然后关闭窗口
new System.Threading.Thread(() =>
{
System.Threading.Thread.Sleep(autoCloseWaitTime);
Application.Current.Dispatcher.Invoke(() =>
{
Close();
});
}).Start();
}
private void WindowDragMove(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
DragMove();
}
#region Win32 API
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
private const int GWL_EXSTYLE = -20;
private const int WS_EX_TOPMOST = 0x00000008;
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint SWP_SHOWWINDOW = 0x0040;
private const uint SWP_NOOWNERZORDER = 0x0200;
/// <summary>
/// 应用快抽窗口置顶
/// </summary>
private void ApplyQuickDrawWindowTopmost()
{
try
{
var hwnd = new WindowInteropHelper(this).Handle;
if (hwnd == IntPtr.Zero) return;
// 设置WPF的Topmost属性
Topmost = true;
// 使用Win32 API强制置顶
// 1. 设置窗口样式为置顶
int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
// 2. 使用SetWindowPos确保窗口在最顶层
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
LogHelper.WriteLogToFile("快抽窗口已应用置顶", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用快抽窗口置顶失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 窗口加载事件处理,确保置顶
/// </summary>
private void QuickDrawWindow_Loaded(object sender, RoutedEventArgs e)
{
MainWindow mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
mainWindow.PauseTopmostMaintenance();
}
// 使用延迟确保窗口完全加载后再应用置顶
Dispatcher.BeginInvoke(new Action(() =>
{
ApplyQuickDrawWindowTopmost();
}), DispatcherPriority.Loaded);
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
MainWindow mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
mainWindow.ResumeTopmostMaintenance();
}
}
#endregion
}
}
+77
View File
@@ -0,0 +1,77 @@
<Window x:Class="Ink_Canvas.QuickDrawWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ink_Canvas"
Topmost="True" Background="Transparent"
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True"
Loaded="QuickDrawWindow_Loaded" Closing="Window_Closing" WindowStartupLocation="CenterScreen"
Title="快抽窗口" Height="200" Width="400" Focusable="False" ShowInTaskbar="False">
<Window.Resources>
<ResourceDictionary>
<!-- 快抽窗口资源 -->
<SolidColorBrush x:Key="QuickDrawWindowBackground" Color="#1f1f1f"/>
<SolidColorBrush x:Key="QuickDrawWindowBorderBrush" Color="#E0E0E0"/>
<SolidColorBrush x:Key="QuickDrawWindowTitleForeground" Color="White"/>
<SolidColorBrush x:Key="QuickDrawWindowDigitForeground" Color="White"/>
</ResourceDictionary>
</Window.Resources>
<Border Background="{DynamicResource QuickDrawWindowBackground}"
CornerRadius="15"
BorderThickness="1"
BorderBrush="{DynamicResource QuickDrawWindowBorderBrush}"
Margin="10"
x:Name="MainBorder"
MouseLeftButtonDown="WindowDragMove">
<Grid>
<!-- 主要内容区域 -->
<Grid>
<!-- 顶部标题栏 -->
<Grid Height="50" Background="{DynamicResource QuickDrawWindowBackground}"
x:Name="TitleBar"
VerticalAlignment="Top"
MouseLeftButtonDown="WindowDragMove"
Margin="10,8,40,0">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="22,0,0,0">
<!-- 快抽图标 -->
<Path Data="M5 7C5 8.06087 5.42143 9.07828 6.17157 9.82843C6.92172 10.5786 7.93913 11 9 11C10.0609 11 11.0783 10.5786 11.8284 9.82843C12.5786 9.07828 13 8.06087 13 7C13 5.93913 12.5786 4.92172 11.8284 4.17157C11.0783 3.42143 10.0609 3 9 3C7.93913 3 6.92172 3.42143 6.17157 4.17157C5.42143 4.92172 5 5.93913 5 7Z M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21 M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88 M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15"
Stroke="{DynamicResource QuickDrawWindowTitleForeground}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="Transparent"
Width="24" Height="24"
Stretch="Uniform"
Margin="0,0,8,0"/>
<!-- 快抽文字 -->
<TextBlock Text="快抽" FontSize="20" FontWeight="Bold"
Foreground="{DynamicResource QuickDrawWindowTitleForeground}"
x:Name="TitleText"/>
</StackPanel>
</Grid>
<!-- 主要内容区域 -->
<Grid Margin="20,60,20,20">
<!-- 结果显示区域 -->
<Grid x:Name="ResultGrid" HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- 主结果显示 -->
<TextBlock x:Name="MainResultDisplay"
Text="准备抽选..."
FontSize="48"
FontWeight="Bold"
Foreground="{DynamicResource QuickDrawWindowDigitForeground}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center"/>
</Grid>
</Grid>
</Grid>
</Grid>
</Border>
</Window>
@@ -0,0 +1,46 @@
<Window x:Class="Ink_Canvas.RollCallHistoryWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ink_Canvas"
mc:Ignorable="d" FontFamily="Microsoft YaHei UI" ui:WindowHelper.UseModernWindowStyle="True"
WindowStartupLocation="CenterScreen"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" Topmost="True"
Title="Ink Canvas 抽奖 - 点名历史记录" Height="500" Width="400"
Loaded="Window_Loaded">
<Window.Resources>
<!-- 主题资源 -->
<SolidColorBrush x:Key="RollCallHistoryWindowBackground" Color="White"/>
<SolidColorBrush x:Key="RollCallHistoryWindowForeground" Color="Black"/>
<SolidColorBrush x:Key="RollCallHistoryWindowButtonBackground" Color="#F4F4F5"/>
<SolidColorBrush x:Key="RollCallHistoryWindowButtonForeground" Color="Black"/>
<SolidColorBrush x:Key="RollCallHistoryWindowBorderBrush" Color="#E4E4E7"/>
</Window.Resources>
<Grid Background="{DynamicResource RollCallHistoryWindowBackground}">
<Label Content="点名历史记录"
Margin="10"
Foreground="{DynamicResource RollCallHistoryWindowForeground}"
FontFamily="Microsoft YaHei UI"/>
<TextBox Name="TextBoxHistory"
FontFamily="Microsoft YaHei UI"
VerticalScrollBarVisibility="Auto"
AcceptsReturn="True"
IsReadOnly="True"
Margin="10,40,10,50"
Background="{DynamicResource RollCallHistoryWindowBackground}"
Foreground="{DynamicResource RollCallHistoryWindowForeground}"
BorderBrush="{DynamicResource RollCallHistoryWindowBorderBrush}"/>
<Button Margin="10"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Content="关闭"
FontFamily="Microsoft YaHei UI"
Width="100"
Click="Button_Click"
Background="{DynamicResource RollCallHistoryWindowButtonBackground}"
Foreground="{DynamicResource RollCallHistoryWindowButtonForeground}"
BorderBrush="{DynamicResource RollCallHistoryWindowBorderBrush}"/>
</Grid>
</Window>
@@ -0,0 +1,238 @@
using Ink_Canvas.Helpers;
using System;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using Newtonsoft.Json;
using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
namespace Ink_Canvas
{
/// <summary>
/// Interaction logic for RollCallHistoryWindow.xaml
/// </summary>
public partial class RollCallHistoryWindow : Window
{
public RollCallHistoryWindow()
{
InitializeComponent();
AnimationsHelper.ShowWithSlideFromBottomAndFade(this, 0.25);
ApplyTheme();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
LoadHistory();
}
private void LoadHistory()
{
try
{
string configsFolder = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configs");
string historyJsonPath = System.IO.Path.Combine(configsFolder, "RollCallHistory.json");
if (!File.Exists(historyJsonPath))
{
TextBoxHistory.Text = "暂无历史记录";
return;
}
string jsonContent = File.ReadAllText(historyJsonPath);
var historyData = JsonConvert.DeserializeObject<RollCallHistoryData>(jsonContent);
if (historyData == null || historyData.History == null || historyData.History.Count == 0)
{
TextBoxHistory.Text = "暂无历史记录";
return;
}
// 计算每个名字的总累计抽选次数(用于统计信息)
var nameCountDict = new System.Collections.Generic.Dictionary<string, int>();
if (historyData.NameFrequency != null && historyData.NameFrequency.Count > 0)
{
// 使用已保存的频率统计
foreach (var kvp in historyData.NameFrequency)
{
nameCountDict[kvp.Key] = kvp.Value;
}
}
else
{
// 如果没有频率统计,从历史记录中计算
foreach (var name in historyData.History)
{
if (nameCountDict.ContainsKey(name))
nameCountDict[name]++;
else
nameCountDict[name] = 1;
}
}
// 计算历史记录中每条记录出现时的累计次数(按时间正序)
var historyWithCount = new System.Collections.Generic.List<System.Tuple<string, int>>();
var runningCount = new System.Collections.Generic.Dictionary<string, int>();
foreach (var name in historyData.History)
{
if (runningCount.ContainsKey(name))
runningCount[name]++;
else
runningCount[name] = 1;
historyWithCount.Add(new System.Tuple<string, int>(name, runningCount[name]));
}
// 按时间倒序显示(最新的在上方)
historyWithCount.Reverse();
// 显示历史记录,每行显示:名字 (累计X次)
var historyLines = new System.Collections.Generic.List<string>();
foreach (var item in historyWithCount)
{
historyLines.Add($"{item.Item1} (最近累计{item.Item2}次)");
}
// 显示统计信息
int totalCount = historyData.History.Count;
string lastUpdate = historyData.LastUpdate.ToString("yyyy-MM-dd HH:mm:ss");
// 计算累计统计信息
var statsLines = new System.Collections.Generic.List<string>();
statsLines.Add($"");
statsLines.Add($"");
statsLines.Add($"累计抽选次数统计:");
// 按累计次数降序排序显示
var sortedStats = nameCountDict.OrderByDescending(kvp => kvp.Value).ToList();
foreach (var kvp in sortedStats)
{
statsLines.Add($" {kvp.Key}: {kvp.Value}次");
}
statsLines.Add($"");
statsLines.Add($"共 {totalCount} 条记录,最后更新:{lastUpdate}");
// 组合历史记录和统计信息
TextBoxHistory.Text = string.Join(Environment.NewLine, historyLines) +
Environment.NewLine +
string.Join(Environment.NewLine, statsLines);
}
catch (Exception ex)
{
TextBoxHistory.Text = $"加载历史记录失败: {ex.Message}";
LogHelper.WriteLogToFile($"加载点名历史记录失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void ApplyTheme()
{
try
{
if (MainWindow.Settings != null)
{
ApplyTheme(MainWindow.Settings);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用历史记录窗口主题出错: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ApplyTheme(Settings settings)
{
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["RollCallHistoryWindowBackground"] = new SolidColorBrush(Color.FromRgb(255, 255, 255));
resources["RollCallHistoryWindowForeground"] = new SolidColorBrush(Color.FromRgb(24, 24, 27));
resources["RollCallHistoryWindowButtonBackground"] = new SolidColorBrush(Color.FromRgb(244, 244, 245));
resources["RollCallHistoryWindowButtonForeground"] = new SolidColorBrush(Color.FromRgb(24, 24, 27));
resources["RollCallHistoryWindowBorderBrush"] = new SolidColorBrush(Color.FromRgb(228, 228, 231));
}
else
{
// 应用深色主题资源
resources["RollCallHistoryWindowBackground"] = new SolidColorBrush(Color.FromRgb(31, 31, 31)); // #1f1f1f
resources["RollCallHistoryWindowForeground"] = new SolidColorBrush(Colors.White);
resources["RollCallHistoryWindowButtonBackground"] = new SolidColorBrush(Color.FromRgb(42, 42, 42)); // #2a2a2a
resources["RollCallHistoryWindowButtonForeground"] = new SolidColorBrush(Colors.White);
resources["RollCallHistoryWindowBorderBrush"] = new SolidColorBrush(Color.FromRgb(224, 224, 224)); // #E0E0E0
}
}
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)
{
var value = themeKey.GetValue("AppsUseLightTheme");
if (value != null)
{
light = (int)value == 1;
}
themeKey.Close();
}
}
catch
{
light = true;
}
return light;
}
}
}
+3 -3
View File
@@ -393,9 +393,9 @@ namespace Ink_Canvas.Windows
case 1: // 跟随四季
var month = DateTime.Now.Month;
if (month >= 3 && month <= 5) return GetImageNameByStyle(2); // 春季
if (month >= 6 && month <= 8) return GetImageNameByStyle(3); // 夏季
if (month >= 9 && month <= 11) return GetImageNameByStyle(4); // 秋季
if (month >= 2 && month <= 4) return GetImageNameByStyle(2); // 春季
if (month >= 5 && month <= 7) return GetImageNameByStyle(3); // 夏季
if (month >= 8 && month <= 10) return GetImageNameByStyle(4); // 秋季
return GetImageNameByStyle(5); // 冬季
case 2: // 春季
@@ -1,31 +1,35 @@
<Window x:Class="Ink_Canvas.NewStyleTimerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ink_Canvas"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:controls="clr-namespace:Ink_Canvas.Windows.Controls"
Topmost="True" Background="Transparent"
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True"
Loaded="Window_Loaded" Closing="Window_Closing" WindowStartupLocation="CenterScreen"
MouseMove="Window_MouseMove" MouseEnter="Window_MouseEnter"
Title="Ink Canvas 画板 - 计时器" Height="450" Width="900">
<Window.Resources>
<UserControl x:Class="Ink_Canvas.Windows.TimerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:controls="clr-namespace:Ink_Canvas.Windows.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="900">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Controls/WinUI3CloseButton.xaml" />
<ResourceDictionary Source="DigitResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
</UserControl.Resources>
<Border Background="{DynamicResource NewTimerWindowBackground}" CornerRadius="15" BorderThickness="1" BorderBrush="{DynamicResource NewTimerWindowBorderBrush}" Margin="10" x:Name="MainBorder" MouseLeftButtonDown="WindowDragMove">
<Border Background="{DynamicResource NewTimerWindowBackground}"
CornerRadius="15"
BorderThickness="1"
BorderBrush="{DynamicResource NewTimerWindowBorderBrush}"
Margin="10"
x:Name="MainBorder"
MouseLeftButtonDown="MainBorder_MouseLeftButtonDown">
<Grid>
<controls:WinUI3CloseButton x:Name="CloseButton"
HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,0,0,0" Cursor="Hand" Click="CloseButton_Click"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,0,0,0"
Cursor="Hand"
Click="CloseButton_Click"
Content="✕"/>
<!-- 主要内容区域 -->
<Grid>
@@ -33,8 +37,16 @@
<Viewbox x:Name="MainViewController" Margin="20,20,20,20">
<Grid Height="400" Width="900">
<!-- 顶部标题栏 -->
<Grid Height="50" Background="{DynamicResource NewTimerWindowBackground}" x:Name="TitleBar" VerticalAlignment="Top" MouseLeftButtonDown="WindowDragMove" Margin="0,0,450,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="22,0,0,0">
<Grid Height="50"
Background="{DynamicResource NewTimerWindowBackground}"
x:Name="TitleBar"
VerticalAlignment="Top"
Margin="0,0,450,0"
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="22,0,0,0">
<!-- 计时器图标 -->
<Path Data="M5 13a7 7 0 1 0 14 0a7 7 0 0 0 -14 0z M14.5 10.5l-2.5 2.5 M17 8l1 -1 M14 3h-4"
Stroke="{DynamicResource NewTimerWindowTitleForeground}"
@@ -45,35 +57,56 @@
Stretch="Uniform"
Margin="0,0,8,0"/>
<!-- 计时文字 -->
<TextBlock Text="计时" FontSize="28" FontWeight="Bold"
Foreground="{DynamicResource NewTimerWindowTitleForeground}" x:Name="TitleText"/>
<TextBlock Text="计时"
FontSize="28"
FontWeight="Bold"
Foreground="{DynamicResource NewTimerWindowTitleForeground}"
x:Name="TitleText"/>
</StackPanel>
</Grid>
<!-- 主要内容区域 - 分为左右两部分 -->
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Center"
x:Name="MainContentGrid" Margin="0,-25,0,25">
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Center"
x:Name="MainContentGrid"
Margin="0,-25,0,25">
<!-- 左侧:6位数字显示区域 -->
<Grid HorizontalAlignment="Left" VerticalAlignment="Center"
x:Name="MainDisplayGrid" Width="500" Margin="-10,0,0,0">
<!-- 6位数字水平排列 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
x:Name="DigitsPanel">
<!-- 小时组(十位和个位) -->
<Grid Margin="0,0,20,0" HorizontalAlignment="Center" Width="120">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<!-- 第1位数字(小时十位) -->
<Grid Margin="0,0,15,0">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit1PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit1Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Grid HorizontalAlignment="Left"
VerticalAlignment="Center"
x:Name="MainDisplayGrid"
Width="500"
Margin="-10,0,0,0">
<!-- 6位数字水平排列 -->
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
x:Name="DigitsPanel">
<!-- 小时组(十位和个位) -->
<Grid Margin="0,0,20,0"
HorizontalAlignment="Center"
Width="120">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<!-- 第1位数字(小时十位) -->
<Grid Margin="0,0,15,0">
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit1PlusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit1Plus_Click"
Cursor="Hand"
Margin="0,0,0,15"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M12 5l0 14 M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -83,25 +116,33 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit1Display" Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit1MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit1Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit1Display"
Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit1MinusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit1Minus_Click"
Cursor="Hand"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -111,23 +152,31 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
</StackPanel>
</Grid>
<!-- 第2位数字(小时个位) -->
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit2PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit2Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
</Grid>
<!-- 第2位数字(小时个位) -->
<Grid>
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit2PlusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit2Plus_Click"
Cursor="Hand"
Margin="0,0,0,15"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M12 5l0 14 M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -137,25 +186,33 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit2Display" Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit2MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit2Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit2Display"
Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit2MinusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit2Minus_Click"
Cursor="Hand"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -165,34 +222,48 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
<!-- 冒号分隔符 -->
<TextBlock x:Name="Colon1Display" Text=":" FontSize="48" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
<TextBlock x:Name="Colon1Display"
Text=":"
FontSize="48"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource NewTimerWindowDigitForeground}"
Margin="-20,0,0,0"/>
<!-- 分钟组(十位和个位) -->
<Grid Margin="0,0,20,0" HorizontalAlignment="Center" Width="120">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<!-- 第3位数字(分钟十位) -->
<Grid Margin="0,0,15,0">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit3PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit3Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<!-- 分钟组(十位和个位) -->
<Grid Margin="0,0,20,0"
HorizontalAlignment="Center"
Width="120">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<!-- 第3位数字(分钟十位) -->
<Grid Margin="0,0,15,0">
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit3PlusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit3Plus_Click"
Cursor="Hand"
Margin="0,0,0,15"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M12 5l0 14 M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -202,25 +273,33 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit3Display" Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit3MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit3Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit3Display"
Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit3MinusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit3Minus_Click"
Cursor="Hand"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -230,23 +309,31 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
</StackPanel>
</Grid>
<!-- 第4位数字(分钟个位) -->
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit4PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit4Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
</Grid>
<!-- 第4位数字(分钟个位) -->
<Grid>
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit4PlusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit4Plus_Click"
Cursor="Hand"
Margin="0,0,0,15"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M12 5l0 14 M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -256,25 +343,33 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit4Display" Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit4MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit4Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit4Display"
Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit4MinusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit4Minus_Click"
Cursor="Hand"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -284,34 +379,46 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
<!-- 冒号分隔符 -->
<TextBlock x:Name="Colon2Display" Text=":" FontSize="48" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
<TextBlock x:Name="Colon2Display"
Text=":"
FontSize="48"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource NewTimerWindowDigitForeground}"
Margin="-20,0,0,0"/>
<!-- 秒组(十位和个位) -->
<Grid Width="120">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<!-- 第5位数字(秒十位) -->
<Grid Margin="0,0,15,0">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit5PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit5Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<!-- 秒组(十位和个位) -->
<Grid Width="120">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<!-- 第5位数字(秒十位) -->
<Grid Margin="0,0,15,0">
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit5PlusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit5Plus_Click"
Cursor="Hand"
Margin="0,0,0,15"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M12 5l0 14 M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -321,25 +428,33 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit5Display" Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit5MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit5Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit5Display"
Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit5MinusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit5Minus_Click"
Cursor="Hand"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -349,23 +464,31 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
</StackPanel>
</Grid>
<!-- 第6位数字(秒个位) -->
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit6PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit6Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
</Grid>
<!-- 第6位数字(秒个位) -->
<Grid>
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center">
<!-- 上方+按钮 -->
<Button x:Name="Digit6PlusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit6Plus_Click"
Cursor="Hand"
Margin="0,0,0,15"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M12 5l0 14 M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -375,25 +498,33 @@
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"/>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit6Display" Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit6MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Digit6Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<!-- 数字显示 -->
<Path x:Name="Digit6Display"
Data="{StaticResource Digit0}"
Fill="{DynamicResource NewTimerWindowDigitForeground}"
Width="48" Height="48"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Margin="0,0,0,15"/>
<!-- 下方-按钮 -->
<Button x:Name="Digit6MinusBtn"
Width="40" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Digit6Minus_Click"
Cursor="Hand"
HorizontalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path Data="M5 12l14 0"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -410,16 +541,22 @@
</Grid>
</StackPanel>
</Grid>
<!-- 分割线 -->
<Border Width="2" Background="{DynamicResource NewTimerWindowButtonForeground}"
Opacity="0.3" HorizontalAlignment="Left" VerticalAlignment="Stretch"
<Border Width="2"
Background="{DynamicResource NewTimerWindowButtonForeground}"
Opacity="0.3"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Margin="465,0,0,0"/>
<!-- 右侧:快捷选项区域 -->
<Grid HorizontalAlignment="Left" VerticalAlignment="Center"
x:Name="QuickOptionsGrid" Width="400" Margin="470,0,0,0" Height="200">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<Grid HorizontalAlignment="Left"
VerticalAlignment="Center"
x:Name="QuickOptionsGrid"
Width="400"
Margin="470,0,0,0"
Height="200">
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center">
<!-- Segmented Control -->
<Grid Margin="0,0,0,20">
<Border Background="{DynamicResource NewTimerWindowButtonBackground}"
@@ -436,7 +573,6 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="0,0,0,0"/>
<!-- 按钮容器 -->
<Grid>
<Button x:Name="CommonTabBtn"
@@ -446,7 +582,10 @@
Click="CommonTab_Click"
Cursor="Hand"
HorizontalAlignment="Left">
<TextBlock x:Name="CommonTabText" Text="常用" FontSize="16" FontWeight="Bold"
<TextBlock x:Name="CommonTabText"
Text="常用"
FontSize="16"
FontWeight="Bold"
Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
</Button>
<Button x:Name="RecentTabBtn"
@@ -456,14 +595,17 @@
Click="RecentTab_Click"
Cursor="Hand"
HorizontalAlignment="Right">
<TextBlock x:Name="RecentTabText" Text="最近" FontSize="16" FontWeight="Normal"
Foreground="{DynamicResource NewTimerWindowButtonForeground}" Opacity="0.6"/>
<TextBlock x:Name="RecentTabText"
Text="最近"
FontSize="16"
FontWeight="Normal"
Foreground="{DynamicResource NewTimerWindowButtonForeground}"
Opacity="0.6"/>
</Button>
</Grid>
</Grid>
</Border>
</Grid>
<!-- 常用计时区域 -->
<Grid x:Name="CommonTimersGrid" Visibility="Visible">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
@@ -618,9 +760,8 @@
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
<!-- 最近计时区域 -->
<Grid x:Name="RecentTimersGrid" Visibility="Collapsed">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
@@ -662,7 +803,7 @@
<TextBlock x:Name="RecentTimer3Text" Text="--:--" FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
</Button>
</StackPanel>
</StackPanel>
<!-- 第二排:最近3个 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
@@ -708,21 +849,39 @@
</StackPanel>
</Grid>
</Grid>
<!-- 底部控制按钮区域 -->
<Grid HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,20">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Grid HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Margin="0,0,0,20">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<!-- 全屏按钮 -->
<Button x:Name="FullscreenBtn" Width="80" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Fullscreen_Click" Cursor="Hand" Margin="0,0,30,0">
<Button x:Name="FullscreenBtn"
Width="80" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Fullscreen_Click"
Cursor="Hand"
Margin="0,0,30,0"
IsEnabled="False">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="8">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Border x:Name="border"
Background="{TemplateBinding Background}"
CornerRadius="8"
Opacity="1">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<Path Data="M4 8v-2a2 2 0 0 1 2 -2h2 M4 16v2a2 2 0 0 0 2 2h2 M16 4h2a2 2 0 0 1 2 2v2 M16 20h2a2 2 0 0 0 2 -2v-2"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -731,37 +890,55 @@
Width="20" Height="20"
Stretch="Uniform"
Margin="0,0,5,0"/>
<TextBlock Text="全屏" FontSize="16" Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
<TextBlock Text="全屏"
FontSize="16"
Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
</StackPanel>
</Button>
<!-- 开始/暂停按钮 -->
<Button x:Name="StartPauseBtn" Width="80" Height="80" Background="{DynamicResource NewTimerWindowPrimaryButtonBackground}"
BorderThickness="0" Click="StartPause_Click" Cursor="Hand" Margin="0,0,30,0">
<Button x:Name="StartPauseBtn"
Width="80" Height="80"
Background="{DynamicResource NewTimerWindowPrimaryButtonBackground}"
BorderThickness="0"
Click="StartPause_Click"
Cursor="Hand"
Margin="0,0,30,0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="40">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Border Background="{TemplateBinding Background}"
CornerRadius="40">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Path x:Name="StartPauseIcon" Data="M6.5 4.00004V20C6.49995 20.178 6.54737 20.3527 6.63738 20.5062C6.72739 20.6597 6.85672 20.7864 7.01202 20.8732C7.16733 20.96 7.34299 21.0038 7.52088 21.0001C7.69878 20.9964 7.87245 20.9453 8.024 20.852L21.024 12.852C21.1696 12.7626 21.2898 12.6373 21.3733 12.4881C21.4567 12.339 21.5005 12.1709 21.5005 12C21.5005 11.8291 21.4567 11.6611 21.3733 11.512C21.2898 11.3628 21.1696 11.2375 21.024 11.148L8.024 3.14804C7.87245 3.0548 7.69878 3.00369 7.52088 2.99997C7.34299 2.99626 7.16733 3.04007 7.01202 3.1269C6.85672 3.21372 6.72739 3.34042 6.63738 3.4939C6.54737 3.64739 6.49995 3.82211 6.5 4.00004Z"
Fill="{DynamicResource NewTimerWindowPrimaryButtonForeground}"
Width="24" Height="24" Margin="2,0,0,0"
HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="Fill"/>
<Path x:Name="StartPauseIcon"
Data="M6.5 4.00004V20C6.49995 20.178 6.54737 20.3527 6.63738 20.5062C6.72739 20.6597 6.85672 20.7864 7.01202 20.8732C7.16733 20.96 7.34299 21.0038 7.52088 21.0001C7.69878 20.9964 7.87245 20.9453 8.024 20.852L21.024 12.852C21.1696 12.7626 21.2898 12.6373 21.3733 12.4881C21.4567 12.339 21.5005 12.1709 21.5005 12C21.5005 11.8291 21.4567 11.6611 21.3733 11.512C21.2898 11.3628 21.1696 11.2375 21.024 11.148L8.024 3.14804C7.87245 3.0548 7.69878 3.00369 7.52088 2.99997C7.34299 2.99626 7.16733 3.04007 7.01202 3.1269C6.85672 3.21372 6.72739 3.34042 6.63738 3.4939C6.54737 3.64739 6.49995 3.82211 6.5 4.00004Z"
Fill="{DynamicResource NewTimerWindowPrimaryButtonForeground}"
Width="24" Height="24"
Margin="2,0,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Fill"/>
</Button>
<!-- 重置按钮 -->
<Button x:Name="ResetBtn" Width="80" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0" Click="Reset_Click" Cursor="Hand">
<Button x:Name="ResetBtn"
Width="80" Height="40"
Background="{DynamicResource NewTimerWindowButtonBackground}"
BorderThickness="0"
Click="Reset_Click"
Cursor="Hand">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="8">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Border Background="{TemplateBinding Background}"
CornerRadius="8">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<Path Data="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4 M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
StrokeThickness="2"
@@ -770,7 +947,9 @@
Width="20" Height="20"
Stretch="Uniform"
Margin="0,0,5,0"/>
<TextBlock Text="重置" FontSize="16" Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
<TextBlock Text="重置"
FontSize="16"
Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
</StackPanel>
</Button>
</StackPanel>
@@ -780,4 +959,5 @@
</Grid>
</Grid>
</Border>
</Window>
</UserControl>
@@ -6,14 +6,12 @@ using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Shapes;
using Newtonsoft.Json;
using System.Runtime.InteropServices;
using System.Windows.Threading;
namespace Ink_Canvas
using Microsoft.Win32;
namespace Ink_Canvas.Windows
{
/// <summary>
/// 最近计时记录数据模型
@@ -31,12 +29,11 @@ namespace Ink_Canvas
/// <summary>
/// 新计时器UI风格的倒计时器窗口
/// </summary>
public partial class NewStyleTimerWindow : Window
public partial class TimerControl : UserControl
{
public NewStyleTimerWindow()
public TimerControl()
{
InitializeComponent();
AnimationsHelper.ShowWithSlideFromBottomAndFade(this, 0.25);
timer.Elapsed += Timer_Elapsed;
timer.Interval = 50;
@@ -49,10 +46,69 @@ namespace Ink_Canvas
hideTimer = new Timer(1000); // 每秒检查一次
hideTimer.Elapsed += HideTimer_Elapsed;
lastActivityTime = DateTime.Now;
// 添加窗口加载事件处理,确保置顶
Loaded += TimerWindow_Loaded;
// 监听主题变化事件
SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;
// 监听卸载事件,清理资源
Unloaded += TimerControl_Unloaded;
}
private void TimerControl_Unloaded(object sender, RoutedEventArgs e)
{
// 取消订阅主题变化事件
SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged;
}
private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
// 当主题变化时,重新应用主题
Application.Current.Dispatcher.Invoke(() =>
{
RefreshTheme();
});
}
/// <summary>
/// 刷新主题
/// </summary>
public void RefreshTheme()
{
try
{
// 重新应用主题
ApplyTheme();
// 强制刷新UI
InvalidateVisual();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"刷新计时器窗口主题出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#region
/// <summary>
/// 计时器完成事件
/// </summary>
public event EventHandler TimerCompleted;
/// <summary>
/// 关闭事件 - 通知主窗口隐藏容器
/// </summary>
public event EventHandler CloseRequested;
/// <summary>
/// 显示最小化视图事件
/// </summary>
public event EventHandler ShowMinimizedRequested;
/// <summary>
/// 隐藏最小化视图事件
/// </summary>
public event EventHandler HideMinimizedRequested;
#endregion
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
@@ -116,6 +172,12 @@ namespace Ink_Canvas
StartPauseIcon.Data = Geometry.Parse(PlayIconData);
PlayTimerSound();
// 禁用全屏按钮
if (FullscreenBtn != null)
{
FullscreenBtn.IsEnabled = false;
}
TimerCompleted?.Invoke(this, EventArgs.Empty);
HandleTimerCompletion();
}
@@ -127,15 +189,23 @@ namespace Ink_Canvas
int displayHours = totalHours;
if (displayHours > 99) displayHours = 99;
if (displayHours < 0) displayHours = 0;
bool shouldShowRed = MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true;
SetDigitDisplay("Digit1Display", displayHours / 10, shouldShowRed);
SetDigitDisplay("Digit2Display", displayHours % 10, shouldShowRed);
SetDigitDisplay("Digit3Display", overtimeSpan.Minutes / 10, shouldShowRed);
SetDigitDisplay("Digit4Display", overtimeSpan.Minutes % 10, shouldShowRed);
SetDigitDisplay("Digit5Display", overtimeSpan.Seconds / 10, shouldShowRed);
SetDigitDisplay("Digit6Display", overtimeSpan.Seconds % 10, shouldShowRed);
int hoursTens = Math.Max(0, Math.Min(9, Math.Abs(displayHours / 10) % 10));
int hoursOnes = Math.Max(0, Math.Min(9, (displayHours % 10 + 10) % 10));
int minutesTens = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Minutes) / 10));
int minutesOnes = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Minutes) % 10));
int secondsTens = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Seconds) / 10));
int secondsOnes = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Seconds) % 10));
SetDigitDisplay("Digit1Display", hoursTens, shouldShowRed);
SetDigitDisplay("Digit2Display", hoursOnes, shouldShowRed);
SetDigitDisplay("Digit3Display", minutesTens, shouldShowRed);
SetDigitDisplay("Digit4Display", minutesOnes, shouldShowRed);
SetDigitDisplay("Digit5Display", secondsTens, shouldShowRed);
SetDigitDisplay("Digit6Display", secondsOnes, shouldShowRed);
SetColonDisplay(shouldShowRed);
}
@@ -160,11 +230,7 @@ namespace Ink_Canvas
Timer timer = new Timer();
private Timer hideTimer;
private MinimizedTimerWindow minimizedWindow;
private DateTime lastActivityTime;
private bool isFullscreenMode = false;
private FullscreenTimerWindow fullscreenWindow;
public event EventHandler TimerCompleted;
private DateTime lastActivityTime;
public TimeSpan? GetTotalTimeSpan()
{
return new TimeSpan(hour, minute, second);
@@ -270,6 +336,9 @@ namespace Ink_Canvas
SetDarkThemeBorder();
}
}
// 刷新数字和冒号显示的颜色
UpdateDigitDisplays();
}
catch (Exception ex)
{
@@ -314,47 +383,6 @@ namespace Ink_Canvas
SetColonDisplay(false);
}
private void HideTimer_Elapsed(object sender, ElapsedEventArgs e)
{
Application.Current.Dispatcher.Invoke(() =>
{
// 只有在计时器运行时且不在全屏模式下才检查自动隐藏
if (isTimerRunning && !isPaused && !isFullscreenMode)
{
var timeSinceLastActivity = DateTime.Now - lastActivityTime;
if (timeSinceLastActivity.TotalSeconds >= 5)
{
ShowMinimizedWindow();
}
}
});
}
private void ShowMinimizedWindow()
{
if (minimizedWindow == null || !minimizedWindow.IsVisible)
{
minimizedWindow = new MinimizedTimerWindow(this);
minimizedWindow.Show();
// 确保最小化窗口也置顶
minimizedWindow.Topmost = true;
// 隐藏主窗口
this.Hide();
}
}
public void UpdateActivityTime()
{
lastActivityTime = DateTime.Now;
}
public void SetFullscreenMode(bool isFullscreen)
{
isFullscreenMode = isFullscreen;
}
// 更新剩余时间
private void UpdateRemainingTime()
{
@@ -430,10 +458,12 @@ namespace Ink_Canvas
if (isPaused) return null;
var elapsed = DateTime.Now - startTime;
var totalSeconds = hour * 3600 + minute * 60 + second;
var remaining = totalSeconds - elapsed.TotalSeconds;
var totalTimeSpan = new TimeSpan(hour, minute, second);
var leftTimeSpan = totalTimeSpan - elapsed;
return TimeSpan.FromSeconds(remaining);
if (leftTimeSpan.Milliseconds > 0) leftTimeSpan += new TimeSpan(0, 0, 1);
return leftTimeSpan;
}
public void StopTimer()
@@ -443,15 +473,6 @@ namespace Ink_Canvas
StartPauseIcon.Data = Geometry.Parse(PlayIconData);
}
private void Window_MouseMove(object sender, MouseEventArgs e)
{
UpdateActivityTime();
}
private void Window_MouseEnter(object sender, MouseEventArgs e)
{
UpdateActivityTime();
}
/// <summary>
/// 根据数字值设置SVG数字显示
@@ -464,6 +485,8 @@ namespace Ink_Canvas
var path = this.FindName(pathName) as System.Windows.Shapes.Path;
if (path != null)
{
digit = Math.Max(0, Math.Min(9, digit));
string resourceKey = $"Digit{digit}";
var geometry = this.FindResource(resourceKey) as Geometry;
if (geometry != null)
@@ -477,7 +500,7 @@ namespace Ink_Canvas
}
else
{
var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush;
var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush;
if (defaultBrush != null)
{
path.Fill = defaultBrush;
@@ -507,7 +530,7 @@ namespace Ink_Canvas
}
else
{
var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush;
var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush;
if (defaultBrush != null)
{
colon1.Foreground = defaultBrush;
@@ -527,7 +550,7 @@ namespace Ink_Canvas
}
else
{
var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush;
var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush;
if (defaultBrush != null)
{
colon2.Foreground = defaultBrush;
@@ -544,6 +567,7 @@ namespace Ink_Canvas
private void Digit1Plus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentHour = hour;
int hourTens = currentHour / 10;
int hourOnes = currentHour % 10;
@@ -558,6 +582,7 @@ namespace Ink_Canvas
private void Digit1Minus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentHour = hour;
int hourTens = currentHour / 10;
int hourOnes = currentHour % 10;
@@ -573,6 +598,7 @@ namespace Ink_Canvas
private void Digit2Plus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentHour = hour;
int hourTens = currentHour / 10;
int hourOnes = currentHour % 10;
@@ -592,6 +618,7 @@ namespace Ink_Canvas
private void Digit2Minus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentHour = hour;
int hourTens = currentHour / 10;
int hourOnes = currentHour % 10;
@@ -612,6 +639,7 @@ namespace Ink_Canvas
private void Digit3Plus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentMinute = minute;
int minuteTens = currentMinute / 10;
int minuteOnes = currentMinute % 10;
@@ -626,6 +654,7 @@ namespace Ink_Canvas
private void Digit3Minus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentMinute = minute;
int minuteTens = currentMinute / 10;
int minuteOnes = currentMinute % 10;
@@ -641,6 +670,7 @@ namespace Ink_Canvas
private void Digit4Plus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentMinute = minute;
int minuteTens = currentMinute / 10;
int minuteOnes = currentMinute % 10;
@@ -660,6 +690,7 @@ namespace Ink_Canvas
private void Digit4Minus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentMinute = minute;
int minuteTens = currentMinute / 10;
int minuteOnes = currentMinute % 10;
@@ -680,6 +711,7 @@ namespace Ink_Canvas
private void Digit5Plus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentSecond = second;
int secondTens = currentSecond / 10;
int secondOnes = currentSecond % 10;
@@ -694,6 +726,7 @@ namespace Ink_Canvas
private void Digit5Minus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentSecond = second;
int secondTens = currentSecond / 10;
int secondOnes = currentSecond % 10;
@@ -709,6 +742,7 @@ namespace Ink_Canvas
private void Digit6Plus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentSecond = second;
int secondTens = currentSecond / 10;
int secondOnes = currentSecond % 10;
@@ -728,6 +762,7 @@ namespace Ink_Canvas
private void Digit6Minus_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning) return;
UpdateActivityTime();
int currentSecond = second;
int secondTens = currentSecond / 10;
int secondOnes = currentSecond % 10;
@@ -750,6 +785,7 @@ namespace Ink_Canvas
private void StartPause_Click(object sender, RoutedEventArgs e)
{
UpdateActivityTime();
if (isPaused && isTimerRunning)
{
// 继续计时
@@ -786,34 +822,49 @@ namespace Ink_Canvas
// 启动隐藏定时器
hideTimer.Start();
// 确保计时器窗口置顶
ApplyTimerWindowTopmost();
// 保存到最近计时记录
SaveRecentTimer();
// 启用全屏按钮
if (FullscreenBtn != null)
{
FullscreenBtn.IsEnabled = true;
}
}
}
private void Reset_Click(object sender, RoutedEventArgs e)
{
if (!isTimerRunning)
UpdateActivityTime();
if (isTimerRunning)
{
UpdateDigitDisplays();
isOvertimeMode = false;
}
else if (isTimerRunning && isPaused)
{
UpdateDigitDisplays();
StartPauseIcon.Data = Geometry.Parse(PlayIconData);
isTimerRunning = false;
// 停止计时器
timer.Stop();
isTimerRunning = false;
isPaused = false;
isOvertimeMode = false;
if (hideTimer != null)
{
hideTimer.Stop();
}
}
else
UpdateDigitDisplays();
SetColonDisplay(false);
if (StartPauseIcon != null)
{
startTime = DateTime.Now;
Timer_Elapsed(timer, null);
StartPauseIcon.Data = Geometry.Parse(PlayIconData);
}
isOvertimeMode = false;
hasPlayedProgressiveReminder = false;
// 禁用全屏按钮
if (FullscreenBtn != null)
{
FullscreenBtn.IsEnabled = false;
}
}
@@ -883,29 +934,15 @@ namespace Ink_Canvas
}
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// 窗口加载时的初始化
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
isTimerRunning = false;
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void WindowDragMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
DragMove();
StopTimer();
CloseRequested?.Invoke(this, EventArgs.Empty);
}
private void CommonTab_Click(object sender, RoutedEventArgs e)
{
UpdateActivityTime();
CommonTimersGrid.Visibility = Visibility.Visible;
RecentTimersGrid.Visibility = Visibility.Collapsed;
@@ -940,6 +977,7 @@ namespace Ink_Canvas
private void RecentTab_Click(object sender, RoutedEventArgs e)
{
UpdateActivityTime();
CommonTimersGrid.Visibility = Visibility.Collapsed;
RecentTimersGrid.Visibility = Visibility.Visible;
@@ -976,36 +1014,42 @@ namespace Ink_Canvas
private void Common5Min_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning && !isPaused) return;
UpdateActivityTime();
SetQuickTime(0, 5, 0);
}
private void Common10Min_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning && !isPaused) return;
UpdateActivityTime();
SetQuickTime(0, 10, 0);
}
private void Common15Min_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning && !isPaused) return;
UpdateActivityTime();
SetQuickTime(0, 15, 0);
}
private void Common30Min_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning && !isPaused) return;
UpdateActivityTime();
SetQuickTime(0, 30, 0);
}
private void Common45Min_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning && !isPaused) return;
UpdateActivityTime();
SetQuickTime(0, 45, 0);
}
private void Common60Min_Click(object sender, RoutedEventArgs e)
{
if (isTimerRunning && !isPaused) return;
UpdateActivityTime();
SetQuickTime(1, 0, 0);
}
@@ -1013,36 +1057,42 @@ namespace Ink_Canvas
private void RecentTimer1_Click(object sender, RoutedEventArgs e)
{
if ((isTimerRunning && !isPaused) || recentTimer1 == "--:--") return;
UpdateActivityTime();
ApplyRecentTimer(recentTimer1);
}
private void RecentTimer2_Click(object sender, RoutedEventArgs e)
{
if ((isTimerRunning && !isPaused) || recentTimer2 == "--:--") return;
UpdateActivityTime();
ApplyRecentTimer(recentTimer2);
}
private void RecentTimer3_Click(object sender, RoutedEventArgs e)
{
if ((isTimerRunning && !isPaused) || recentTimer3 == "--:--") return;
UpdateActivityTime();
ApplyRecentTimer(recentTimer3);
}
private void RecentTimer4_Click(object sender, RoutedEventArgs e)
{
if ((isTimerRunning && !isPaused) || recentTimer4 == "--:--") return;
UpdateActivityTime();
ApplyRecentTimer(recentTimer4);
}
private void RecentTimer5_Click(object sender, RoutedEventArgs e)
{
if ((isTimerRunning && !isPaused) || recentTimer5 == "--:--") return;
UpdateActivityTime();
ApplyRecentTimer(recentTimer5);
}
private void RecentTimer6_Click(object sender, RoutedEventArgs e)
{
if ((isTimerRunning && !isPaused) || recentTimer6 == "--:--") return;
UpdateActivityTime();
ApplyRecentTimer(recentTimer6);
}
@@ -1182,16 +1232,22 @@ namespace Ink_Canvas
{
try
{
RecentTimer1Text.Text = recentTimer1;
RecentTimer2Text.Text = recentTimer2;
RecentTimer3Text.Text = recentTimer3;
RecentTimer4Text.Text = recentTimer4;
RecentTimer5Text.Text = recentTimer5;
RecentTimer6Text.Text = recentTimer6;
var timer1Text = this.FindName("RecentTimer1Text") as TextBlock;
var timer2Text = this.FindName("RecentTimer2Text") as TextBlock;
var timer3Text = this.FindName("RecentTimer3Text") as TextBlock;
var timer4Text = this.FindName("RecentTimer4Text") as TextBlock;
var timer5Text = this.FindName("RecentTimer5Text") as TextBlock;
var timer6Text = this.FindName("RecentTimer6Text") as TextBlock;
if (timer1Text != null) timer1Text.Text = recentTimer1;
if (timer2Text != null) timer2Text.Text = recentTimer2;
if (timer3Text != null) timer3Text.Text = recentTimer3;
if (timer4Text != null) timer4Text.Text = recentTimer4;
if (timer5Text != null) timer5Text.Text = recentTimer5;
if (timer6Text != null) timer6Text.Text = recentTimer6;
}
catch
{
// 如果UI元素还未初始化,忽略错误
}
}
@@ -1297,135 +1353,268 @@ namespace Ink_Canvas
}
}
private FullscreenTimerWindow fullscreenWindow;
public bool IsFullscreenWindowOpen => fullscreenWindow != null && fullscreenWindow.IsVisible;
private void Fullscreen_Click(object sender, RoutedEventArgs e)
{
ShowFullscreenTimer();
if (fullscreenWindow != null && fullscreenWindow.IsVisible)
{
fullscreenWindow.Close();
fullscreenWindow = null;
return;
}
if (isTimerRunning && !isPaused)
{
fullscreenWindow = new FullscreenTimerWindow(this);
fullscreenWindow.Closed += (s, args) => { fullscreenWindow = null; };
fullscreenWindow.Show();
HideMinimizedRequested?.Invoke(this, EventArgs.Empty);
}
}
private void ShowFullscreenTimer()
private void MainBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// 设置全屏模式标志
isFullscreenMode = true;
UpdateActivityTime();
if (e.ClickCount == 1)
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
if (timerContainer != null)
{
var point = e.GetPosition(timerContainer);
var mainWindowPoint = timerContainer.TransformToAncestor(mainWindow).Transform(point);
DragTimerContainer(mainWindow, mainWindowPoint, e);
}
}
}
}
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
UpdateActivityTime();
if (e.ClickCount == 1)
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
if (timerContainer != null)
{
var point = e.GetPosition(timerContainer);
var mainWindowPoint = timerContainer.TransformToAncestor(mainWindow).Transform(point);
DragTimerContainer(mainWindow, mainWindowPoint, e);
}
}
}
}
private bool isDragging = false;
private Point dragStartPoint;
private Point containerStartPosition;
private void DragTimerContainer(MainWindow mainWindow, Point startPoint, MouseButtonEventArgs e)
{
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
if (timerContainer == null) return;
// 创建全屏计时器窗口
fullscreenWindow = new FullscreenTimerWindow(this);
fullscreenWindow.Show();
isDragging = true;
dragStartPoint = startPoint;
// 确保全屏窗口也置顶
fullscreenWindow.Topmost = true;
if (timerContainer.HorizontalAlignment == HorizontalAlignment.Center ||
timerContainer.VerticalAlignment == VerticalAlignment.Center)
{
var timerPoint = timerContainer.TransformToAncestor(mainWindow).Transform(new Point(0, 0));
containerStartPosition = new Point(timerPoint.X, timerPoint.Y);
timerContainer.Margin = new Thickness(containerStartPosition.X, containerStartPosition.Y, 0, 0);
timerContainer.HorizontalAlignment = HorizontalAlignment.Left;
timerContainer.VerticalAlignment = VerticalAlignment.Top;
}
else
{
var margin = timerContainer.Margin;
containerStartPosition = new Point(margin.Left, margin.Top);
if (double.IsNaN(containerStartPosition.X) || containerStartPosition.X < 0) containerStartPosition.X = 0;
if (double.IsNaN(containerStartPosition.Y) || containerStartPosition.Y < 0) containerStartPosition.Y = 0;
}
// 隐藏主窗口
this.Hide();
timerContainer.CaptureMouse();
timerContainer.MouseMove += TimerContainer_MouseMove;
timerContainer.MouseLeftButtonUp += TimerContainer_MouseLeftButtonUp;
e.Handled = true;
}
private void TimerContainer_MouseMove(object sender, MouseEventArgs e)
{
if (!isDragging) return;
UpdateActivityTime();
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow == null) return;
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement;
if (timerContainer == null) return;
var currentPoint = e.GetPosition(mainWindow);
var deltaX = currentPoint.X - dragStartPoint.X;
var deltaY = currentPoint.Y - dragStartPoint.Y;
var newX = containerStartPosition.X + deltaX;
var newY = containerStartPosition.Y + deltaY;
if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
timerContainer.Margin = new Thickness(newX, newY, 0, 0);
timerContainer.HorizontalAlignment = HorizontalAlignment.Left;
timerContainer.VerticalAlignment = VerticalAlignment.Top;
if (minimizedContainer != null && minimizedContainer.Visibility == Visibility.Visible)
{
minimizedContainer.Margin = new Thickness(newX, newY, 0, 0);
minimizedContainer.HorizontalAlignment = HorizontalAlignment.Left;
minimizedContainer.VerticalAlignment = VerticalAlignment.Top;
}
}
private void TimerContainer_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (!isDragging) return;
isDragging = false;
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow == null) return;
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
if (timerContainer != null)
{
timerContainer.ReleaseMouseCapture();
timerContainer.MouseMove -= TimerContainer_MouseMove;
timerContainer.MouseLeftButtonUp -= TimerContainer_MouseLeftButtonUp;
}
}
private void HandleTimerCompletion()
{
if (minimizedWindow != null)
// 计时器结束时,如果显示的是最小化视图,恢复到主窗口视图
Application.Current.Dispatcher.Invoke(() =>
{
minimizedWindow.Close();
minimizedWindow = null;
this.Show();
this.Activate();
this.WindowState = WindowState.Normal;
// 重新应用置顶
ApplyTimerWindowTopmost();
}
else if (fullscreenWindow != null)
{
fullscreenWindow.Close();
fullscreenWindow = null;
isFullscreenMode = false;
this.Show();
this.Activate();
this.WindowState = WindowState.Normal;
// 重新应用置顶
ApplyTimerWindowTopmost();
}
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement;
// 如果最小化视图可见,恢复到主窗口视图
if (minimizedContainer != null && minimizedContainer.Visibility == Visibility.Visible)
{
HideMinimizedRequested?.Invoke(this, EventArgs.Empty);
}
}
});
// 重置计时器状态
ResetTimerState();
}
#region Win32 API
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll")]
private static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentProcessId();
private const int GWL_EXSTYLE = -20;
private const int WS_EX_TOPMOST = 0x00000008;
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint SWP_SHOWWINDOW = 0x0040;
private const uint SWP_NOOWNERZORDER = 0x0200;
/// <summary>
/// 应用计时器窗口置顶
/// 重置计时器状态
/// </summary>
private void ApplyTimerWindowTopmost()
public void ResetTimerState()
{
try
Application.Current.Dispatcher.Invoke(() =>
{
var hwnd = new WindowInteropHelper(this).Handle;
if (hwnd == IntPtr.Zero) return;
// 强制激活窗口
Activate();
Focus();
// 设置WPF的Topmost属性
Topmost = true;
// 使用Win32 API强制置顶
// 1. 设置窗口样式为置顶
int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
// 2. 使用SetWindowPos确保窗口在最顶层
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
LogHelper.WriteLogToFile("计时器窗口已应用置顶", LogHelper.LogType.Trace);
}
catch (Exception ex)
// 停止计时器
if (isTimerRunning)
{
timer.Stop();
isTimerRunning = false;
isPaused = false;
if (hideTimer != null)
{
hideTimer.Stop();
}
}
// 重置时间到默认值
hour = 0;
minute = 5;
second = 0;
// 更新显示
UpdateDigitDisplays();
SetColonDisplay(false);
// 重置图标
if (StartPauseIcon != null)
{
StartPauseIcon.Data = Geometry.Parse(PlayIconData);
}
// 重置状态标志
isOvertimeMode = false;
hasPlayedProgressiveReminder = false;
// 禁用全屏按钮
if (FullscreenBtn != null)
{
FullscreenBtn.IsEnabled = false;
}
});
}
private void HideTimer_Elapsed(object sender, ElapsedEventArgs e)
{
if (!isTimerRunning || isPaused) return;
Application.Current.Dispatcher.Invoke(() =>
{
LogHelper.WriteLogToFile($"应用计时器窗口置顶失败: {ex.Message}", LogHelper.LogType.Error);
var timeSinceLastActivity = DateTime.Now - lastActivityTime;
if (timeSinceLastActivity.TotalSeconds >= 5)
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
if (timerContainer != null && timerContainer.Visibility == Visibility.Visible)
{
ShowMinimizedRequested?.Invoke(this, EventArgs.Empty);
}
}
}
});
}
public void UpdateActivityTime()
{
lastActivityTime = DateTime.Now;
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement;
var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement;
if (timerContainer != null && minimizedContainer != null)
{
if (timerContainer.Visibility == Visibility.Collapsed && minimizedContainer.Visibility == Visibility.Visible)
{
HideMinimizedRequested?.Invoke(this, EventArgs.Empty);
}
}
}
}
/// <summary>
/// 窗口加载事件处理,确保置顶
/// </summary>
private void TimerWindow_Loaded(object sender, RoutedEventArgs e)
{
// 使用延迟确保窗口完全加载后再应用置顶
Dispatcher.BeginInvoke(new Action(() =>
{
ApplyTimerWindowTopmost();
}), DispatcherPriority.Loaded);
}
#endregion
}
}
+6 -16
View File
@@ -20,21 +20,17 @@
</div>
## 🤔 发生了什么?
## 💫 软件说明
由于众所周知的原因,[DotteringDoge471](https://github.com/DotteringDoge471) 不再积极负责 InkCanvasForClass 旧时代版本的开发与维护工作,而刚好 [CJKmkp](https://github.com/CJKmkp) 又维护了这个社区版本的 icc,经过沟通后就顺理成章地成为了 icc 的官方版本。该分支版本 **目前还在开发之中** ,可能会有潜在的问题/ Bug 出现,请在出现 Bug 后与开发者或与 [DotteringDoge471](https://github.com/DotteringDoge471) 上报,方便我们迅速诊断并解决问题。
> ⚠️ 请注意:[DotteringDoge471](https://github.com/DotteringDoge471) 不积极负责 **本社区版本** 的开发与维护工作,仅会在有空的时候对本项目开发新功能或修复 Bug。因此,任何问题反馈/Bug反馈/建议等,请优先找本项目主要维护者 [CJKmkp](https://github.com/CJKmkp) 反馈或在 GitHub 仓库内提出 Issue
使用该版本 InkCanvasForClass,意味着您同意自行承担任何可能存在的问题与风险。建议不要在公众场合(例如公开课,录播课,线上直播课,大型会议)使用未经广泛测试和优化的 Beta 版本,对使用 Beta 版本而带来的任何问题和风险(例如:被班主任批斗,被校长处罚,崩溃而导致的场面混乱,全球海平面上升等),**将由使用者自行承担**,[CJKmkp](https://github.com/CJKmkp) 和 [DotteringDoge471](https://github.com/DotteringDoge471) 及其项目的所有维护者不提供任何担保。
使用该版本 InkCanvasForClass,意味着您同意自行承担任何可能存在的问题与风险。建议不要在公众场合(例如公开课,录播课,线上直播课,大型会议)使用未经广泛测试和优化的 Beta 版本,对使用 Beta 版本而带来的任何问题和风险(例如:被班主任批斗,被校长处罚,崩溃而导致的场面混乱,全球海平面上升等),**将由使用者自行承担**,[CJKmkp](https://github.com/CJKmkp) 及其项目的所有维护者不提供任何担保。
♥️ **本项目版权归 [CJKmkp](https://github.com/CJKmkp) 所有。[CJKmkp](https://github.com/CJKmkp) 拥有最终解释权。**
**智教联盟 InkCanvasForClass Community Edition 板块:** [forum.smart-teach.cn/t/icc-ce](https://forum.smart-teach.cn/t/icc-ce) ,我们会在此处发布版本更新日志,同时,您也可以在遵守论坛对应管理规则与InkCanvasForClass Community Edition 板块管理条约的情况下,在该板块内提问或发表自己的使用体验。
## ⚠️ 使用须知
在使用和分发本软件前,请务必了解相关开源协议。本软件基于 https://github.com/Awesome-Iwb/icc-0610fix 修改而来,而 icc-0610fix 基于 https://github.com/ChangSakura/Ink-Canvas 修改,ica 则基于 https://github.com/WXRIW/Ink-Canvas 修改,增加了包括但不限于隐藏到侧边栏等功能,更改了相关UI和软件操作逻辑。对于墨迹书写功能以及 ica 独有功能的相关问题反馈,建议优先查阅 https://github.com/WXRIW/Ink-Canvas/issues 。**使用前建议戴上大脑使用。**
在使用和分发本软件前,请务必了解相关开源协议。本软件基于 <https://github.com/InkCanvasForClass/icc-20240610-stable> 修改而来,而 icc-20240610-stable 基于 <https://github.com/ChangSakura/Ink-Canvas> 修改,ica 则基于 <https://github.com/WXRIW/Ink-Canvas> 修改,增加了包括但不限于隐藏到侧边栏等功能,更改了相关UI和软件操作逻辑。对于墨迹书写功能以及 ica 独有功能的相关问题反馈,建议优先查阅 <https://github.com/WXRIW/Ink-Canvas/issues> 。**使用前建议戴上大脑使用。**
# 💬 提示
- 对于新功能的有效意见和合理建议,开发者会适时回复并进行开发。本软件并非商业性质软件或由营利性机构驱动,请不要催促开发者,耐心等待能让功能少些Bug,更加稳定。
@@ -62,13 +58,8 @@
如果仍无法运行,请[安装 `Microsoft Office`](https://www.coolhub.top/archives/11)。
### 程序能在 Wine 环境中运行吗?
不能,但是你可以期待 icc-gtk4,是正在开发的仅支持 Linux 平台的 icc 移植版本。
## ✏️ 贡献指南
请前往 InkCanvasForClass/dubious-notes
**请注意,在贡献代码时,_务必_ 将所有代码提交到 _beta_ 分支,以保证beta版本总是新于main版本。**
## TODO LIST
@@ -105,13 +96,12 @@
<!-- ALL-CONTRIBUTORS-LIST:END -->
## 🤝 感谢
感谢 [DotteringDoge471](https://github.com/DotteringDoge471) 创造了 `InkCanvasForClass`
感谢 [yuwenhui2020](https://github.com/yuwenhui2020) 为 `Ink Canvas 使用说明` 做出的贡献!
感谢 [CN-Ironegg](https://github.com/CN-Ironegg)、[jiajiaxd](https://github.com/jiajiaxd)、[Kengwang](https://github.com/kengwang)、[Raspberry Kan](https://github.com/Raspberry-Monster)、[clover-yan](https://github.com/clover-yan)、[STBBRD](https://github.com/STBBRD)、[ChangSakura](https://github.com/WuChanging)、[DotteringDoge471](https://github.com/DotteringDoge471) 为本项目贡献代码!
感谢 [CN-Ironegg](https://github.com/CN-Ironegg)、[jiajiaxd](https://github.com/jiajiaxd)、[Kengwang](https://github.com/kengwang)、[Raspberry Kan](https://github.com/Raspberry-Monster)、[clover-yan](https://github.com/clover-yan)、[STBBRD](https://github.com/STBBRD)、[ChangSakura](https://github.com/WuChanging) 为本项目贡献代码!
## License
GPLv3
## 项目引用
[Alan-CRL/DesktopDrawpadBlocker](https://github.com/Alan-CRL/DesktopDrawpadBlocker)
[Awesome-Iwb/iwbicons-gallery](https://github.com/awesome-iwb/awesome-iwb/wiki/iwbicons-gallery)「本项目部分图标来自 Awesome Iwb 的 IwbIcons 图标库,由 Douxiba 制作。」
+3
View File
@@ -100,3 +100,6 @@ ICC CE 1.7.X.X更新日志
99. 修复仅调色盘状态下浮动栏不居中
100. 修复希沃白板查杀与思锐希沃启动器导致的重复启动
101. 新增UIA窗口置顶
102. 新增新点名窗口
103. 新增点名快抽
104. 新增墨迹自动保存