diff --git a/.all-contributorsrc b/.all-contributorsrc index 41dcfa6e..3ac1bb16 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -86,7 +86,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/129855423?v=4", "profile": "https://github.com/PrefacedCorg", "contributions": [ - "code" + "code", + "design" ] } ] diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index e3ac9b51..3a953b57 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -45,7 +45,7 @@ namespace Ink_Canvas // 新增:进程ID private static int currentProcessId = Process.GetCurrentProcess().Id; // 新增:应用启动时间 - private static DateTime appStartTime = DateTime.Now; + internal static DateTime appStartTime { get; private set; } // 新增:最后一次错误信息 private static string lastErrorMessage = string.Empty; // 新增:是否已初始化崩溃监听器 @@ -287,6 +287,7 @@ namespace Ink_Canvas { string reason = e.Reason == SessionEndReasons.Logoff ? "用户注销" : "系统关机"; WriteCrashLog($"系统会话即将结束: {reason}"); + DeviceIdentifier.SaveUsageStatsOnShutdown(); } // 新增:控制台取消事件处理 @@ -330,7 +331,8 @@ namespace Ink_Canvas private void CurrentDomain_ProcessExit(object sender, EventArgs e) { TimeSpan runDuration = DateTime.Now - appStartTime; - WriteCrashLog($"应用程序退出,运行时长: {runDuration}"); + string durationText = FormatTimeSpan(runDuration); + WriteCrashLog($"应用程序退出,运行时长: {durationText}"); // 如果有最后错误消息,记录到日志 if (!string.IsNullOrEmpty(lastErrorMessage)) @@ -339,6 +341,27 @@ namespace Ink_Canvas } } + // 新增:格式化时间跨度 + private static string FormatTimeSpan(TimeSpan timeSpan) + { + if (timeSpan.TotalDays >= 1) + { + return $"{timeSpan.Days}天 {timeSpan.Hours}小时 {timeSpan.Minutes}分钟"; + } + else if (timeSpan.TotalHours >= 1) + { + return $"{timeSpan.Hours}小时 {timeSpan.Minutes}分钟"; + } + else if (timeSpan.TotalMinutes >= 1) + { + return $"{timeSpan.Minutes}分钟 {timeSpan.Seconds}秒"; + } + else + { + return $"{timeSpan.Seconds}秒"; + } + } + // 新增:记录崩溃日志 private static void WriteCrashLog(string message) { @@ -355,7 +378,7 @@ namespace Ink_Canvas // 收集系统状态信息 string memoryUsage = (Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024)) + " MB"; string cpuTime = Process.GetCurrentProcess().TotalProcessorTime.ToString(); - string processUptime = (DateTime.Now - Process.GetCurrentProcess().StartTime).ToString(); + string processUptime = FormatTimeSpan(DateTime.Now - Process.GetCurrentProcess().StartTime); string statusInfo = $"[内存: {memoryUsage}, CPU时间: {cpuTime}, 运行时长: {processUptime}]"; @@ -435,6 +458,9 @@ namespace Ink_Canvas void App_Startup(object sender, StartupEventArgs e) { + // 初始化应用启动时间 + appStartTime = DateTime.Now; + /*if (!StoreHelper.IsStoreApp) */ RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; diff --git a/Ink Canvas/AssemblyInfo.cs b/Ink Canvas/AssemblyInfo.cs index 858d6840..00caa8a6 100644 --- a/Ink Canvas/AssemblyInfo.cs +++ b/Ink Canvas/AssemblyInfo.cs @@ -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.7.5")] -[assembly: AssemblyFileVersion("1.7.7.5")] +[assembly: AssemblyVersion("1.7.7.7")] +[assembly: AssemblyFileVersion("1.7.7.7")] diff --git a/Ink Canvas/Helpers/AutoUpdateHelper.cs b/Ink Canvas/Helpers/AutoUpdateHelper.cs index 138c0090..d0910eb9 100644 --- a/Ink Canvas/Helpers/AutoUpdateHelper.cs +++ b/Ink Canvas/Helpers/AutoUpdateHelper.cs @@ -45,9 +45,9 @@ namespace Ink_Canvas.Helpers new UpdateLineGroup { GroupName = "GitHub主线", - VersionUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt", - DownloadUrlFormat = "https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip", - LogUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md" + VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt", + DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip", + LogUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md" }, new UpdateLineGroup { @@ -80,9 +80,9 @@ namespace Ink_Canvas.Helpers new UpdateLineGroup { GroupName = "GitHub主线", - VersionUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt", - DownloadUrlFormat = "https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip", - LogUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md" + VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt", + DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip", + LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md" }, new UpdateLineGroup { @@ -1195,7 +1195,7 @@ namespace Ink_Canvas.Helpers StringBuilder batchContent = new StringBuilder(); batchContent.AppendLine("@echo off"); - + batchContent.AppendLine("echo Set objShell = CreateObject(\"WScript.Shell\") > \"%temp%\\hideme.vbs\""); batchContent.AppendLine("echo objShell.Run \"cmd /c \"\"\" ^& WScript.Arguments(0) ^& \"\"\"\", 0, True >> \"%temp%\\hideme.vbs\""); batchContent.AppendLine("echo Wscript.Sleep 100 >> \"%temp%\\hideme.vbs\""); @@ -1210,6 +1210,7 @@ namespace Ink_Canvas.Helpers batchContent.AppendLine($"echo goto CHECK_PROCESS >> \"{updateBatPath}\""); batchContent.AppendLine($"echo ) >> \"{updateBatPath}\""); + batchContent.AppendLine($"echo timeout /t 1 /nobreak > nul >> \"{updateBatPath}\""); batchContent.AppendLine($"echo echo Application closed, starting update process... >> \"{updateBatPath}\""); batchContent.AppendLine($"echo timeout /t 2 /nobreak ^> nul >> \"{updateBatPath}\""); @@ -1514,7 +1515,7 @@ namespace Ink_Canvas.Helpers LogHelper.WriteLogToFile("AutoUpdate | 开始测试Windows 7 TLS连接..."); // 测试GitHub连接 - var testUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt"; + var testUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt"; using (var handler = new HttpClientHandler()) { diff --git a/Ink Canvas/Helpers/DeviceIdentifier.cs b/Ink Canvas/Helpers/DeviceIdentifier.cs index a82fe3a8..79140029 100644 --- a/Ink Canvas/Helpers/DeviceIdentifier.cs +++ b/Ink Canvas/Helpers/DeviceIdentifier.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using System.Security.Cryptography; using System.Text; +using System.Threading; namespace Ink_Canvas.Helpers { @@ -2745,5 +2746,303 @@ namespace Ink_Canvas.Helpers return errorMsg; } } + /// + /// 关机时保存使用时间数据 + /// + public static void SaveUsageStatsOnShutdown() + { + // 使用超时锁防止死锁 + if (!Monitor.TryEnter(fileLock, TimeSpan.FromSeconds(30))) + { + LogHelper.WriteLogToFile("DeviceIdentifier | 关机保存超时,使用备用保存策略", LogHelper.LogType.Warning); + SaveUsageStatsOnShutdownFallback(); + return; + } + + try + { + LogHelper.WriteLogToFile("DeviceIdentifier | 开始关机时保存使用时间数据", LogHelper.LogType.Info); + + // 1. 加载现有使用统计数据(多重恢复策略) + UsageStats stats = LoadUsageStatsWithFallback(); + if (stats == null) + { + stats = new UsageStats { DeviceId = DeviceId }; + LogHelper.WriteLogToFile("DeviceIdentifier | 创建新的使用统计数据", LogHelper.LogType.Info); + } + + // 2. 计算本次会话时长(防止异常值) + TimeSpan sessionDuration = DateTime.Now - App.appStartTime; + long sessionSeconds = Math.Max(0, (long)sessionDuration.TotalSeconds); + + // 防止异常大的会话时长(超过24小时) + if (sessionSeconds > 86400) + { + sessionSeconds = 86400; + LogHelper.WriteLogToFile($"DeviceIdentifier | 会话时长异常,已限制为24小时: {sessionSeconds}秒", LogHelper.LogType.Warning); + } + + // 3. 更新统计数据 + stats.TotalUsageSeconds += sessionSeconds; + stats.LaunchCount++; + stats.AverageSessionSeconds = stats.TotalUsageSeconds / (double)Math.Max(1, stats.LaunchCount); + stats.LastLaunchTime = DateTime.Now; + + // 更新数据哈希值 + stats.UpdateDataHash(); + + // 4. 多重保存策略 - 确保数据不丢失 + var saveResults = new List(); + + // 4.1 保存到所有文件位置 + saveResults.Add(SaveUsageStatsToAllFileLocations(stats)); + + // 4.2 保存到所有注册表位置 + saveResults.Add(SaveUsageStatsToAllRegistryLocations(stats)); + + // 4.3 保存到内存缓存(作为最后防线) + SaveUsageStatsToMemoryCache(stats); + + // 4.4 强制刷新文件系统缓存 + ForceFlushFileSystem(); + + // 4.5 验证保存结果 + var verificationResult = VerifyDataSaveResults(stats, saveResults); + + LogHelper.WriteLogToFile($"DeviceIdentifier | 关机保存完成,验证结果: {verificationResult}", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"DeviceIdentifier | 关机时保存使用时间数据失败: {ex.Message}", LogHelper.LogType.Error); + + // 即使主保存失败,也要尝试备用保存 + try + { + SaveUsageStatsOnShutdownFallback(); + } + catch (Exception fallbackEx) + { + LogHelper.WriteLogToFile($"DeviceIdentifier | 备用保存也失败: {fallbackEx.Message}", LogHelper.LogType.Error); + } + } + finally + { + Monitor.Exit(fileLock); + } + } + + /// + /// 关机保存的备用策略 + /// + private static void SaveUsageStatsOnShutdownFallback() + { + try + { + LogHelper.WriteLogToFile("DeviceIdentifier | 执行关机保存备用策略", LogHelper.LogType.Warning); + + // 使用最基本的保存方式 + var stats = new UsageStats { DeviceId = DeviceId }; + stats.TotalUsageSeconds = 1; // 最小记录 + stats.LaunchCount = 1; + stats.LastLaunchTime = DateTime.Now; + + // 只保存到最可靠的位置 + SaveUsageStatsToFile(BackupUsageStatsPath, stats); + SaveUsageStatsToRegistry(stats); + + LogHelper.WriteLogToFile("DeviceIdentifier | 备用策略执行完成", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"DeviceIdentifier | 备用策略执行失败: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 加载使用统计数据(带多重恢复策略) + /// + private static UsageStats LoadUsageStatsWithFallback() + { + try + { + // 1. 尝试从主文件加载 + var stats = LoadUsageStats(); + if (stats != null) return stats; + + // 2. 尝试从备份文件加载 + stats = LoadUsageStatsFromFile(BackupUsageStatsPath); + if (stats != null) return stats; + + // 3. 尝试从其他备份位置加载 + var backupPaths = new[] { SecondaryUsageBackupPath, TertiaryUsageBackupPath, QuaternaryUsageBackupPath }; + foreach (var path in backupPaths) + { + stats = LoadUsageStatsFromFile(path); + if (stats != null) return stats; + } + + // 4. 尝试从注册表恢复 + stats = LoadUsageStatsFromRegistry(); + if (stats != null) return stats; + + return null; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"DeviceIdentifier | 多重恢复加载失败: {ex.Message}", LogHelper.LogType.Error); + return null; + } + } + + /// + /// 保存使用统计到所有文件位置 + /// + private static string SaveUsageStatsToAllFileLocations(UsageStats stats) + { + var results = new List(); + var filePaths = new[] + { + UsageStatsFilePath, + BackupUsageStatsPath, + SecondaryUsageBackupPath, + TertiaryUsageBackupPath, + QuaternaryUsageBackupPath + }; + + foreach (var filePath in filePaths) + { + try + { + SaveUsageStatsToFile(filePath, stats); + results.Add($"✓ {Path.GetFileName(filePath)}"); + } + catch (Exception ex) + { + results.Add($"✗ {Path.GetFileName(filePath)}: {ex.Message}"); + } + } + + return string.Join("\n", results); + } + + /// + /// 保存使用统计到所有注册表位置 + /// + private static string SaveUsageStatsToAllRegistryLocations(UsageStats stats) + { + var results = new List(); + + try + { + // 主注册表位置 + SaveUsageStatsToRegistry(stats); + results.Add("✓ 主注册表位置"); + } + catch (Exception ex) + { + results.Add($"✗ 主注册表位置: {ex.Message}"); + } + + try + { + // 备用注册表位置 + SaveUsageStatsToMultipleRegistryLocations(stats); + results.Add("✓ 备用注册表位置"); + } + catch (Exception ex) + { + results.Add($"✗ 备用注册表位置: {ex.Message}"); + } + + return string.Join("\n", results); + } + + /// + /// 保存使用统计到内存缓存 + /// + private static void SaveUsageStatsToMemoryCache(UsageStats stats) + { + try + { + // 将数据保存到静态变量作为内存备份 + _cachedUsageStats = stats; + LogHelper.WriteLogToFile("DeviceIdentifier | 数据已保存到内存缓存", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"DeviceIdentifier | 保存到内存缓存失败: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 强制刷新文件系统缓存 + /// + private static void ForceFlushFileSystem() + { + try + { + // 强制刷新所有相关目录 + var directories = new[] + { + Path.GetDirectoryName(UsageStatsFilePath), + Path.GetDirectoryName(BackupUsageStatsPath), + Path.GetDirectoryName(SecondaryUsageBackupPath), + Path.GetDirectoryName(TertiaryUsageBackupPath), + Path.GetDirectoryName(QuaternaryUsageBackupPath) + }; + + foreach (var dir in directories.Where(d => !string.IsNullOrEmpty(d) && Directory.Exists(d))) + { + try + { + // 创建临时文件来强制刷新 + var tempFile = Path.Combine(dir, ".flush.tmp"); + File.WriteAllText(tempFile, DateTime.Now.ToString()); + File.Delete(tempFile); + } + catch { /* 忽略刷新错误 */ } + } + + LogHelper.WriteLogToFile("DeviceIdentifier | 文件系统缓存刷新完成", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"DeviceIdentifier | 文件系统缓存刷新失败: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 验证数据保存结果 + /// + private static string VerifyDataSaveResults(UsageStats stats, List saveResults) + { + var verification = new StringBuilder(); + verification.AppendLine("数据保存验证结果:"); + verification.AppendLine(string.Join("\n", saveResults)); + + // 验证关键数据是否保存成功 + try + { + var savedStats = LoadUsageStats(); + if (savedStats != null && savedStats.DeviceId == stats.DeviceId) + { + verification.AppendLine("✓ 主数据文件验证成功"); + } + else + { + verification.AppendLine("✗ 主数据文件验证失败"); + } + } + catch (Exception ex) + { + verification.AppendLine($"✗ 主数据文件验证异常: {ex.Message}"); + } + + return verification.ToString(); + } + + // 内存缓存变量 + private static UsageStats _cachedUsageStats; } -} \ No newline at end of file +} + diff --git a/Ink Canvas/Helpers/GlobalHotkeyManager.cs b/Ink Canvas/Helpers/GlobalHotkeyManager.cs new file mode 100644 index 00000000..66d7cba2 --- /dev/null +++ b/Ink Canvas/Helpers/GlobalHotkeyManager.cs @@ -0,0 +1,854 @@ +using System; +using System.Collections.Generic; +using System.Windows.Input; +using System.IO; +using System.Reflection; +using Newtonsoft.Json; +using NHotkey.Wpf; + +namespace Ink_Canvas.Helpers +{ + /// + /// 全局快捷键管理器 - 使用NHotkey库实现全局快捷键功能 + /// + public class GlobalHotkeyManager : IDisposable + { + #region Private Fields + private readonly Dictionary _registeredHotkeys; + private readonly MainWindow _mainWindow; + private bool _isDisposed = false; + private bool _hotkeysShouldBeRegistered = false; // 启动时不注册热键,等待需要时再注册 + + // 配置文件路径 + private static readonly string HotkeyConfigFile = Path.Combine(App.RootPath, "HotkeyConfig.json"); + #endregion + + #region Constructor + public GlobalHotkeyManager(MainWindow mainWindow) + { + _mainWindow = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow)); + _registeredHotkeys = new Dictionary(); + _hotkeysShouldBeRegistered = false; // 启动时不注册热键,等待需要时再注册 + } + #endregion + + #region Public Methods + /// + /// 注册全局快捷键 + /// + /// 快捷键名称 + /// 按键 + /// 修饰键 + /// 执行动作 + /// 是否注册成功 + public bool RegisterHotkey(string hotkeyName, Key key, ModifierKeys modifiers, Action action) + { + try + { + if (_isDisposed) + return false; + + // 如果快捷键已存在,先注销 + if (_registeredHotkeys.ContainsKey(hotkeyName)) + { + UnregisterHotkey(hotkeyName); + } + + // 创建快捷键信息 + var hotkeyInfo = new HotkeyInfo + { + Name = hotkeyName, + Key = key, + Modifiers = modifiers, + Action = action + }; + + // 注册快捷键 + HotkeyManager.Current.AddOrReplace(hotkeyName, key, modifiers, (sender, e) => + { + try + { + // 确保在主线程中执行 + _mainWindow.Dispatcher.Invoke(() => + { + action?.Invoke(); + }); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"执行快捷键 {hotkeyName} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + }); + + _registeredHotkeys[hotkeyName] = hotkeyInfo; + // 成功注册全局快捷键 + return true; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"注册全局快捷键 {hotkeyName} 失败: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + + /// + /// 注销指定快捷键 + /// + /// 快捷键名称 + /// 是否注销成功 + public bool UnregisterHotkey(string hotkeyName) + { + try + { + if (_isDisposed || !_registeredHotkeys.ContainsKey(hotkeyName)) + return false; + + HotkeyManager.Current.Remove(hotkeyName); + _registeredHotkeys.Remove(hotkeyName); + // 成功注销全局快捷键 + return true; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"注销全局快捷键 {hotkeyName} 失败: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + + /// + /// 注销所有快捷键 + /// + public void UnregisterAllHotkeys() + { + try + { + if (_isDisposed) + return; + + foreach (var hotkeyName in _registeredHotkeys.Keys) + { + try + { + HotkeyManager.Current.Remove(hotkeyName); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"注销快捷键 {hotkeyName} 时出错: {ex.Message}", LogHelper.LogType.Warning); + } + } + + _registeredHotkeys.Clear(); + // 已注销所有全局快捷键,集合已清空 + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"注销所有快捷键时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 检查快捷键是否已注册 + /// + /// 快捷键名称 + /// 是否已注册 + public bool IsHotkeyRegistered(string hotkeyName) + { + return _registeredHotkeys.ContainsKey(hotkeyName); + } + + /// + /// 获取已注册的快捷键列表 + /// + /// 快捷键信息列表 + public List GetRegisteredHotkeys() + { + return new List(_registeredHotkeys.Values); + } + + /// + /// 获取配置文件中的快捷键信息(不注册,仅用于显示) + /// + /// 配置文件中的快捷键列表 + public List GetHotkeysFromConfigFile() + { + try + { + if (!File.Exists(HotkeyConfigFile)) + { + LogHelper.WriteLogToFile("快捷键配置文件不存在", LogHelper.LogType.Info); + return new List(); + } + + // 读取配置文件内容 + string jsonContent = File.ReadAllText(HotkeyConfigFile, System.Text.Encoding.UTF8); + if (string.IsNullOrEmpty(jsonContent)) + { + LogHelper.WriteLogToFile("快捷键配置文件为空", LogHelper.LogType.Warning); + return new List(); + } + + // 反序列化配置 + var config = JsonConvert.DeserializeObject(jsonContent); + if (config?.Hotkeys == null || config.Hotkeys.Count == 0) + { + LogHelper.WriteLogToFile("快捷键配置为空或格式错误", LogHelper.LogType.Warning); + return new List(); + } + + // 转换为HotkeyInfo列表(不注册,仅用于显示) + var hotkeyList = new List(); + foreach (var hotkeyConfig in config.Hotkeys) + { + hotkeyList.Add(new HotkeyInfo + { + Name = hotkeyConfig.Name, + Key = hotkeyConfig.Key, + Modifiers = hotkeyConfig.Modifiers, + Action = null // 不设置动作,仅用于显示 + }); + } + + LogHelper.WriteLogToFile($"从配置文件读取到 {hotkeyList.Count} 个快捷键信息", LogHelper.LogType.Info); + return hotkeyList; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"从配置文件读取快捷键信息时出错: {ex.Message}", LogHelper.LogType.Error); + return new List(); + } + } + + /// + /// 注册默认快捷键集合 + /// + public void RegisterDefaultHotkeys() + { + try + { + // 开始注册默认快捷键集合 + + // 基本操作快捷键 + RegisterHotkey("Undo", Key.Z, ModifierKeys.Control, () => _mainWindow.SymbolIconUndo_MouseUp(null, null)); + RegisterHotkey("Redo", Key.Y, ModifierKeys.Control, () => _mainWindow.SymbolIconRedo_MouseUp(null, null)); + RegisterHotkey("Clear", Key.E, ModifierKeys.Control, () => _mainWindow.SymbolIconDelete_MouseUp(null, null)); + RegisterHotkey("Paste", Key.V, ModifierKeys.Control, () => _mainWindow.HandleGlobalPaste(null, null)); + + // 工具切换快捷键 + RegisterHotkey("SelectTool", Key.S, ModifierKeys.Alt, () => _mainWindow.SymbolIconSelect_MouseUp(null, null)); + RegisterHotkey("DrawTool", Key.D, ModifierKeys.Alt, () => _mainWindow.PenIcon_Click(null, null)); + RegisterHotkey("EraserTool", Key.E, ModifierKeys.Alt, () => _mainWindow.EraserIcon_Click(null, null)); + RegisterHotkey("BlackboardTool", Key.B, ModifierKeys.Alt, () => _mainWindow.ImageBlackboard_MouseUp(null, null)); + RegisterHotkey("QuitDrawTool", Key.Q, ModifierKeys.Alt, () => _mainWindow.CursorIcon_Click(null, null)); + + // 画笔快捷键 - 使用反射访问penType字段 + RegisterHotkey("Pen1", Key.D1, ModifierKeys.Alt, () => SwitchToPenType(0)); + RegisterHotkey("Pen2", Key.D2, ModifierKeys.Alt, () => SwitchToPenType(1)); + RegisterHotkey("Pen3", Key.D3, ModifierKeys.Alt, () => SwitchToPenType(2)); + RegisterHotkey("Pen4", Key.D4, ModifierKeys.Alt, () => SwitchToPenType(3)); + RegisterHotkey("Pen5", Key.D5, ModifierKeys.Alt, () => SwitchToPenType(4)); + + // 功能快捷键 + RegisterHotkey("DrawLine", Key.L, ModifierKeys.Alt, () => _mainWindow.BtnDrawLine_Click(null, null)); + RegisterHotkey("Screenshot", Key.C, ModifierKeys.Alt, () => _mainWindow.SaveScreenShotToDesktop()); + RegisterHotkey("Hide", Key.V, ModifierKeys.Alt, () => _mainWindow.SymbolIconEmoji_MouseUp(null, null)); + + // 退出快捷键 + RegisterHotkey("Exit", Key.Escape, ModifierKeys.None, () => _mainWindow.KeyExit(null, null)); + + // 已注册默认全局快捷键集合 + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"注册默认快捷键时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 从配置文件加载快捷键 + /// + public void LoadHotkeysFromSettings() + { + try + { + // 开始从配置文件加载快捷键设置 + + // 检查是否应该注册快捷键 + if (!_hotkeysShouldBeRegistered) + { + // 当前状态不允许注册快捷键,跳过加载 + return; + } + + // 尝试从配置文件加载 + if (LoadHotkeysFromConfigFile()) + { + // 成功从配置文件加载快捷键设置 + _hotkeysShouldBeRegistered = true; + LogHelper.WriteLogToFile("成功从配置文件加载快捷键设置", LogHelper.LogType.Info); + } + else + { + // 如果配置文件不存在,才使用默认快捷键 + if (!File.Exists(HotkeyConfigFile)) + { + LogHelper.WriteLogToFile("配置文件不存在,注册默认快捷键", LogHelper.LogType.Info); + RegisterDefaultHotkeys(); + _hotkeysShouldBeRegistered = true; + } + else + { + LogHelper.WriteLogToFile("配置文件存在但加载失败,保持当前状态", LogHelper.LogType.Warning); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"从设置加载快捷键时出错: {ex.Message}", LogHelper.LogType.Error); + // 出错时不自动使用默认快捷键,保持当前状态 + } + } + + /// + /// 保存快捷键配置到设置 + /// + public void SaveHotkeysToSettings() + { + try + { + LogHelper.WriteLogToFile("开始保存快捷键配置到配置文件", LogHelper.LogType.Event); + + if (SaveHotkeysToConfigFile()) + { + LogHelper.WriteLogToFile("快捷键配置已成功保存到配置文件", LogHelper.LogType.Event); + } + else + { + LogHelper.WriteLogToFile("保存快捷键配置失败", LogHelper.LogType.Error); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存快捷键配置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 启用快捷键注册功能 + /// 调用此方法后,快捷键将被允许注册 + /// + public void EnableHotkeyRegistration() + { + try + { + if (!_hotkeysShouldBeRegistered) + { + _hotkeysShouldBeRegistered = true; + LogHelper.WriteLogToFile("启用快捷键注册功能", LogHelper.LogType.Info); + + // 立即加载快捷键设置 + LoadHotkeysFromSettings(); + } + else + { + LogHelper.WriteLogToFile("快捷键注册功能已经启用", LogHelper.LogType.Info); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"启用快捷键注册功能时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 更新快捷键配置 + /// + /// 快捷键名称 + /// 新按键 + /// 新修饰键 + /// 是否更新成功 + public bool UpdateHotkey(string hotkeyName, Key key, ModifierKeys modifiers) + { + try + { + if (!_registeredHotkeys.ContainsKey(hotkeyName)) + { + LogHelper.WriteLogToFile($"快捷键 {hotkeyName} 不存在,无法更新", LogHelper.LogType.Warning); + return false; + } + + // 获取原有的动作 + var originalAction = _registeredHotkeys[hotkeyName].Action; + + // 注销原有快捷键 + UnregisterHotkey(hotkeyName); + + // 注册新的快捷键 + var success = RegisterHotkey(hotkeyName, key, modifiers, originalAction); + + if (success) + { + LogHelper.WriteLogToFile($"成功更新快捷键 {hotkeyName}: {modifiers}+{key}", LogHelper.LogType.Event); + // 自动保存配置 + SaveHotkeysToSettings(); + } + + return success; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"更新快捷键 {hotkeyName} 时出错: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + #endregion + + #region Private Helper Methods + /// + /// 切换到指定笔类型 + /// + /// 笔类型索引 + private void SwitchToPenType(int penTypeIndex) + { + try + { + // 通过反射访问主窗口的penType字段 + var penTypeField = _mainWindow.GetType().GetField("penType", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + if (penTypeField != null) + { + penTypeField.SetValue(_mainWindow, penTypeIndex); + + // 调用CheckPenTypeUIState方法更新UI状态 + var checkPenTypeMethod = _mainWindow.GetType().GetMethod("CheckPenTypeUIState", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + if (checkPenTypeMethod != null) + { + checkPenTypeMethod.Invoke(_mainWindow, null); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"切换到笔类型{penTypeIndex}时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 从配置文件加载快捷键设置 + /// + /// 是否加载成功 + private bool LoadHotkeysFromConfigFile() + { + try + { + if (!File.Exists(HotkeyConfigFile)) + { + LogHelper.WriteLogToFile($"快捷键配置文件不存在: {HotkeyConfigFile}", LogHelper.LogType.Warning); + return false; + } + + // 读取配置文件内容 + string jsonContent = File.ReadAllText(HotkeyConfigFile, System.Text.Encoding.UTF8); + if (string.IsNullOrEmpty(jsonContent)) + { + LogHelper.WriteLogToFile("快捷键配置文件为空", LogHelper.LogType.Warning); + return false; + } + + // 反序列化配置 + var config = JsonConvert.DeserializeObject(jsonContent); + if (config?.Hotkeys == null || config.Hotkeys.Count == 0) + { + LogHelper.WriteLogToFile("快捷键配置为空或格式错误", LogHelper.LogType.Warning); + return false; + } + + // 注册配置中的快捷键 + int successCount = 0; + foreach (var hotkeyConfig in config.Hotkeys) + { + try + { + // 根据快捷键名称获取对应的动作 + var action = GetActionByName(hotkeyConfig.Name); + if (action != null) + { + if (RegisterHotkey(hotkeyConfig.Name, hotkeyConfig.Key, hotkeyConfig.Modifiers, action)) + { + successCount++; + } + } + else + { + LogHelper.WriteLogToFile($"未找到快捷键 {hotkeyConfig.Name} 对应的动作", LogHelper.LogType.Warning); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"注册快捷键 {hotkeyConfig.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + LogHelper.WriteLogToFile($"成功加载 {successCount}/{config.Hotkeys.Count} 个快捷键配置", LogHelper.LogType.Event); + if (successCount > 0) + { + _hotkeysShouldBeRegistered = true; + } + return successCount > 0; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"从配置文件加载快捷键时出错: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + + /// + /// 保存快捷键配置到配置文件 + /// + /// 是否保存成功 + private bool SaveHotkeysToConfigFile() + { + try + { + // 确保配置目录存在 + string configDir = Path.GetDirectoryName(HotkeyConfigFile); + if (!Directory.Exists(configDir)) + { + Directory.CreateDirectory(configDir); + } + + // 创建配置对象 + var config = new HotkeyConfig + { + Version = "1.0", + LastModified = DateTime.Now, + Hotkeys = new List() + }; + + // 添加所有已注册的快捷键 + foreach (var hotkey in _registeredHotkeys.Values) + { + config.Hotkeys.Add(new HotkeyConfigItem + { + Name = hotkey.Name, + Key = hotkey.Key, + Modifiers = hotkey.Modifiers + }); + } + + // 序列化为JSON + var settings = new JsonSerializerSettings + { + Formatting = Formatting.Indented + }; + + string jsonContent = JsonConvert.SerializeObject(config, settings); + + // 直接写入原文件,覆盖原有内容 + File.WriteAllText(HotkeyConfigFile, jsonContent, System.Text.Encoding.UTF8); + + LogHelper.WriteLogToFile($"快捷键配置已保存到: {HotkeyConfigFile}", LogHelper.LogType.Event); + return true; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存快捷键配置到配置文件时出错: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + + /// + /// 根据快捷键名称获取对应的动作 + /// + /// 快捷键名称 + /// 对应的动作,如果不存在则返回null + private Action GetActionByName(string hotkeyName) + { + try + { + switch (hotkeyName) + { + case "Undo": + return () => _mainWindow.SymbolIconUndo_MouseUp(null, null); + case "Redo": + return () => _mainWindow.SymbolIconRedo_MouseUp(null, null); + case "Clear": + return () => _mainWindow.SymbolIconDelete_MouseUp(null, null); + case "Paste": + return () => _mainWindow.HandleGlobalPaste(null, null); + case "SelectTool": + return () => _mainWindow.SymbolIconSelect_MouseUp(null, null); + case "DrawTool": + return () => _mainWindow.PenIcon_Click(null, null); + case "EraserTool": + return () => _mainWindow.EraserIcon_Click(null, null); + case "BlackboardTool": + return () => _mainWindow.ImageBlackboard_MouseUp(null, null); + case "QuitDrawTool": + return () => _mainWindow.CursorIcon_Click(null, null); + case "Pen1": + return () => SwitchToPenType(0); + case "Pen2": + return () => SwitchToPenType(1); + case "Pen3": + return () => SwitchToPenType(2); + case "Pen4": + return () => SwitchToPenType(3); + case "Pen5": + return () => SwitchToPenType(4); + case "DrawLine": + return () => _mainWindow.BtnDrawLine_Click(null, null); + case "Screenshot": + return () => _mainWindow.SaveScreenShotToDesktop(); + case "Hide": + return () => _mainWindow.SymbolIconEmoji_MouseUp(null, null); + case "Exit": + return () => _mainWindow.KeyExit(null, null); + default: + LogHelper.WriteLogToFile($"未知的快捷键名称: {hotkeyName}", LogHelper.LogType.Warning); + return null; + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"获取快捷键 {hotkeyName} 对应动作时出错: {ex.Message}", LogHelper.LogType.Error); + return null; + } + } + + /// + /// 检查当前是否处于鼠标模式(选择模式) + /// + /// 如果处于鼠标模式则返回true(不应该注册快捷键),否则返回false(应该注册快捷键) + private bool IsInSelectMode() + { + try + { + // 通过反射访问主窗口的FloatingbarSelectionBG字段 + var floatingbarSelectionBGField = _mainWindow.GetType().GetField("FloatingbarSelectionBG", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (floatingbarSelectionBGField != null) + { + var floatingbarSelectionBG = floatingbarSelectionBGField.GetValue(_mainWindow); + if (floatingbarSelectionBG != null) + { + // 检查高光是否可见 + var visibilityProperty = floatingbarSelectionBG.GetType().GetProperty("Visibility"); + if (visibilityProperty != null) + { + var visibility = visibilityProperty.GetValue(floatingbarSelectionBG); + if (visibility != null && visibility.ToString() == "Hidden") + { + // 高光隐藏,说明没有选中任何工具,此时应该注销快捷键以释放系统快捷键 + return true; // 返回true表示应该注销快捷键 + } + } + + // 通过反射访问Canvas.GetLeft方法来获取高光位置 + var canvasType = Type.GetType("System.Windows.Controls.Canvas, PresentationFramework"); + if (canvasType != null) + { + var getLeftMethod = canvasType.GetMethod("GetLeft", BindingFlags.Public | BindingFlags.Static); + if (getLeftMethod != null) + { + var leftPosition = getLeftMethod.Invoke(null, new object[] { floatingbarSelectionBG }); + if (leftPosition != null) + { + var position = Convert.ToDouble(leftPosition); + + // 根据高光位置判断当前选中的工具 + // 位置计算基于SetFloatingBarHighlightPosition方法中的逻辑 + bool isMouseMode = false; + string currentTool = "unknown"; + + // 简化判断:如果位置接近0,说明是鼠标模式 + // 如果位置接近28,说明是批注模式 + // 如果位置更大,说明是其他工具 + if (position < 5) // 鼠标模式:marginOffset + (cursorWidth - actualHighlightWidth) / 2 ≈ 0 + { + isMouseMode = true; + currentTool = "鼠标"; + } + else if (position < 35) // 批注模式:marginOffset + cursorWidth + (penWidth - actualHighlightWidth) / 2 ≈ 28 + { + isMouseMode = false; + currentTool = "批注"; + } + else // 其他工具(橡皮擦、选择等) + { + isMouseMode = false; + currentTool = "其他工具"; + } + + return isMouseMode; + } + } + } + } + } + + // 如果无法获取高光状态,则回退到inkCanvas.EditingMode判断 + + // 通过反射访问主窗口的inkCanvas字段 + var inkCanvasField = _mainWindow.GetType().GetField("inkCanvas", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (inkCanvasField != null) + { + var inkCanvas = inkCanvasField.GetValue(_mainWindow); + if (inkCanvas != null) + { + // 通过反射访问inkCanvas的EditingMode属性 + var editingModeProperty = inkCanvas.GetType().GetProperty("EditingMode"); + if (editingModeProperty != null) + { + var editingMode = editingModeProperty.GetValue(inkCanvas); + if (editingMode != null) + { + // 检查是否为批注模式 + var isInkMode = editingMode.ToString().Contains("Ink"); + var isSelectMode = editingMode.ToString().Contains("Select"); + + // 如果是批注模式或选择模式,则应该注册快捷键(返回false) + // 如果是橡皮擦模式或其他模式,则不应该注册快捷键(返回true) + var shouldNotRegisterHotkeys = !isInkMode && !isSelectMode; + + return shouldNotRegisterHotkeys; + } + } + } + } + + // 如果无法获取任何状态信息,则回退到原来的判断逻辑 + + // 通过反射访问主窗口的currentMode字段(作为最后的备用方案) + var currentModeField = _mainWindow.GetType().GetField("currentMode", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (currentModeField != null) + { + var currentMode = currentModeField.GetValue(_mainWindow); + if (currentMode != null) + { + var modeValue = currentMode.ToString(); + // 注意:这里的逻辑需要修正 + // currentMode == 0 表示屏幕模式(PPT放映),此时应该允许快捷键 + // currentMode == 1 表示黑板/白板模式,此时也应该允许快捷键 + var isSelectMode = false; // 修正:所有模式都应该允许快捷键 + return isSelectMode; + } + } + + return false; // 默认允许快捷键 + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"检查鼠标模式状态时出错: {ex.Message}", LogHelper.LogType.Warning); + return false; // 出错时默认允许快捷键 + } + } + + /// + /// 动态管理快捷键注册状态 + /// 根据当前工具选择状态自动注册或注销快捷键 + /// + public void UpdateHotkeyRegistrationState() + { + try + { + bool isMouseMode = IsInSelectMode(); + + if (isMouseMode) + { + // 在鼠标模式下,注销所有快捷键以释放系统快捷键 + if (_hotkeysShouldBeRegistered) + { + UnregisterAllHotkeys(); + _hotkeysShouldBeRegistered = false; + } + else + { + // 快捷键已经处于注销状态,无需重复注销 + } + } + else + { + // 在批注/选择/其他工具模式下,重新注册所有快捷键 + if (!_hotkeysShouldBeRegistered) + { + // 第一次切换到批注/选择/其他工具模式,启用快捷键注册 + EnableHotkeyRegistration(); + } + else if (_registeredHotkeys.Count == 0) + { + // 快捷键已启用但数量为0,重新注册 + LoadHotkeysFromSettings(); + } + else + { + // 当前已有快捷键注册,无需重新注册 + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"更新快捷键注册状态时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + #endregion + + #region IDisposable Implementation + public void Dispose() + { + if (!_isDisposed) + { + UnregisterAllHotkeys(); + _isDisposed = true; + } + } + #endregion + + #region Nested Classes + /// + /// 快捷键信息类 + /// + public class HotkeyInfo + { + public string Name { get; set; } + public Key Key { get; set; } + public ModifierKeys Modifiers { get; set; } + public Action Action { get; set; } + + public override string ToString() + { + var modifiersText = Modifiers == ModifierKeys.None ? "" : $"{Modifiers}+"; + return $"{modifiersText}{Key}"; + } + } + + /// + /// 快捷键配置类 + /// + private class HotkeyConfig + { + public string Version { get; set; } + public DateTime LastModified { get; set; } + public List Hotkeys { get; set; } + } + + /// + /// 快捷键配置项类 + /// + private class HotkeyConfigItem + { + public string Name { get; set; } + public Key Key { get; set; } + public ModifierKeys Modifiers { get; set; } + } + #endregion + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/InkFadeManager.cs b/Ink Canvas/Helpers/InkFadeManager.cs new file mode 100644 index 00000000..1a1e6b49 --- /dev/null +++ b/Ink Canvas/Helpers/InkFadeManager.cs @@ -0,0 +1,833 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Media.Animation; +using System.Windows.Threading; +using System.Windows.Ink; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace Ink_Canvas.Helpers +{ + /// + /// 墨迹渐隐管理器 - 管理墨迹的渐隐动画和状态 + /// + public class InkFadeManager + { + #region Properties + /// + /// 是否启用墨迹渐隐功能 + /// + public bool IsEnabled { get; set; } = false; + + /// + /// 墨迹渐隐时间(毫秒) + /// + public int FadeTime { get; set; } = 3000; + + /// + /// 渐隐动画持续时间(毫秒) + /// + public int AnimationDuration { get; set; } = 1000; + #endregion + + #region Private Fields + private readonly MainWindow _mainWindow; + private readonly Dispatcher _dispatcher; + private readonly Dictionary _fadeTimers; + private readonly Dictionary _strokeVisuals; + private readonly Dictionary _strokeStartPoints; + private readonly Dictionary _strokeEndPoints; + #endregion + + #region Constructor + public InkFadeManager(MainWindow mainWindow) + { + _mainWindow = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow)); + _dispatcher = _mainWindow.Dispatcher; + _fadeTimers = new Dictionary(); + _strokeVisuals = new Dictionary(); + _strokeStartPoints = new Dictionary(); + _strokeEndPoints = new Dictionary(); + } + #endregion + + #region Public Methods + /// + /// 添加需要渐隐的墨迹 + /// + /// 墨迹对象 + /// 落笔点 + /// 抬笔点 + public void AddFadingStroke(Stroke stroke, Point startPoint, Point endPoint) + { + if (!IsEnabled || stroke == null) + { + return; + } + + try + { + + // 记录墨迹的起点和终点 + _strokeStartPoints[stroke] = startPoint; + _strokeEndPoints[stroke] = endPoint; + + // 创建墨迹的视觉元素(湿墨迹状态) + var strokeVisual = CreateStrokeVisual(stroke); + if (strokeVisual == null) return; + + _strokeVisuals[stroke] = strokeVisual; + + // 创建定时器,在指定时间后开始渐隐动画 + var timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(FadeTime) + }; + + timer.Tick += (sender, e) => + { + StartFadeAnimation(stroke); + timer.Stop(); + _fadeTimers.Remove(stroke); + }; + + _fadeTimers[stroke] = timer; + timer.Start(); + + // 将视觉元素添加到画布上 + _dispatcher.InvokeAsync(() => + { + try + { + if (_mainWindow.inkCanvas != null) + { + // 将墨迹添加到 inkCanvas 的父容器中,而不是 inkCanvas.Children + // 这样可以避免坐标系统问题 + var parent = _mainWindow.inkCanvas.Parent as System.Windows.Controls.Panel; + if (parent != null) + { + parent.Children.Add(strokeVisual); + } + else + { + // 如果无法获取父容器,则添加到 inkCanvas.Children + _mainWindow.inkCanvas.Children.Add(strokeVisual); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"添加墨迹视觉元素到画布失败: {ex}", LogHelper.LogType.Error); + } + }); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"添加渐隐墨迹失败: {ex}", LogHelper.LogType.Error); + } + } + + /// + /// 移除墨迹 + /// + /// 要移除的墨迹 + public void RemoveStroke(Stroke stroke) + { + if (stroke == null) return; + + try + { + if (_fadeTimers.TryGetValue(stroke, out var timer)) + { + timer.Stop(); + _fadeTimers.Remove(stroke); + } + + if (_strokeVisuals.TryGetValue(stroke, out var visual)) + { + _dispatcher.InvokeAsync(() => + { + try + { + // 从父容器中移除墨迹 + var parent = _mainWindow.inkCanvas?.Parent as System.Windows.Controls.Panel; + if (parent != null && parent.Children.Contains(visual)) + { + parent.Children.Remove(visual); + } + else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(visual)) + { + _mainWindow.inkCanvas.Children.Remove(visual); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"从画布移除墨迹视觉元素失败: {ex}", LogHelper.LogType.Error); + } + }); + + _strokeVisuals.Remove(stroke); + } + + _strokeStartPoints.Remove(stroke); + _strokeEndPoints.Remove(stroke); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"移除渐隐墨迹失败: {ex}", LogHelper.LogType.Error); + } + } + + /// + /// 清除所有渐隐墨迹 + /// + public void ClearAllFadingStrokes() + { + try + { + foreach (var timer in _fadeTimers.Values) + { + timer.Stop(); + } + + _fadeTimers.Clear(); + + _dispatcher.InvokeAsync(() => + { + try + { + if (_mainWindow.inkCanvas != null) + { + var parent = _mainWindow.inkCanvas.Parent as System.Windows.Controls.Panel; + foreach (var visual in _strokeVisuals.Values) + { + if (parent != null && parent.Children.Contains(visual)) + { + parent.Children.Remove(visual); + } + else if (_mainWindow.inkCanvas.Children.Contains(visual)) + { + _mainWindow.inkCanvas.Children.Remove(visual); + } + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"清除所有墨迹视觉元素失败: {ex}", LogHelper.LogType.Error); + } + }); + + _strokeVisuals.Clear(); + _strokeStartPoints.Clear(); + _strokeEndPoints.Clear(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"清除所有渐隐墨迹失败: {ex}", LogHelper.LogType.Error); + } + } + + /// + /// 更新渐隐时间设置 + /// + /// 新的渐隐时间(毫秒) + public void UpdateFadeTime(int fadeTime) + { + FadeTime = fadeTime; + + foreach (var kvp in _fadeTimers) + { + var stroke = kvp.Key; + var timer = kvp.Value; + + timer.Stop(); + timer.Interval = TimeSpan.FromMilliseconds(FadeTime); + timer.Start(); + } + } + + + + /// + /// 启用墨迹渐隐功能 + /// + public void Enable() + { + IsEnabled = true; + LogHelper.WriteLogToFile("墨迹渐隐功能已启用", LogHelper.LogType.Info); + } + + /// + /// 禁用墨迹渐隐功能 + /// + public void Disable() + { + IsEnabled = false; + LogHelper.WriteLogToFile("墨迹渐隐功能已禁用", LogHelper.LogType.Info); + } + #endregion + + #region Private Methods + /// + /// 创建墨迹的视觉元素 + /// + /// 墨迹对象 + /// 视觉元素 + private UIElement CreateStrokeVisual(Stroke stroke) + { + try + { + // 创建路径几何,使用墨迹的实际位置 + var geometry = stroke.GetGeometry(); + if (geometry == null) + { + return null; + } + + // 获取绘画属性 + var drawingAttribs = stroke.DrawingAttributes; + + // 创建路径元素,确保使用正确的绘画属性 + var path = new Path + { + Data = geometry, + Stroke = new SolidColorBrush(drawingAttribs.Color), + StrokeThickness = drawingAttribs.Width, // 使用原始墨迹的粗细 + StrokeStartLineCap = PenLineCap.Round, + StrokeEndLineCap = PenLineCap.Round, + StrokeLineJoin = PenLineJoin.Round, + Fill = drawingAttribs.IsHighlighter ? new SolidColorBrush(drawingAttribs.Color) : null, // 高亮笔需要填充 + Opacity = 0.95, // 初始透明度更高,显得更自然 + + // 优化渲染质量 + UseLayoutRounding = false, + SnapsToDevicePixels = false + }; + + // 如果是高亮笔,调整透明度和混合模式 + if (drawingAttribs.IsHighlighter) + { + path.Opacity = 0.4; // 高亮笔初始透明度更低,更符合荧光笔特性 + + // 为高亮笔添加特殊的混合效果 + // 使用更柔和的笔触样式 + path.StrokeStartLineCap = PenLineCap.Flat; + path.StrokeEndLineCap = PenLineCap.Flat; + path.StrokeLineJoin = PenLineJoin.Miter; + + // 高亮笔通常需要更宽的笔触来覆盖下面的内容 + if (drawingAttribs.Width < 20) + { + path.StrokeThickness = Math.Max(drawingAttribs.Width * 1.5, 20); + } + } + + // 不设置任何变换,保持墨迹原有粗细 + var bounds = geometry.Bounds; + + // 设置墨迹的初始位置 + System.Windows.Controls.Canvas.SetLeft(path, bounds.Left); + System.Windows.Controls.Canvas.SetTop(path, bounds.Top); + + return path; + } + catch (Exception ex) + { + return null; + } + } + + /// + /// 开始渐隐动画 + /// + /// 要渐隐的墨迹 + private void StartFadeAnimation(Stroke stroke) + { + if (!_strokeVisuals.TryGetValue(stroke, out var visual)) return; + + try + { + _dispatcher.InvokeAsync(() => + { + // 获取当前透明度和判断是否为高亮笔 + var currentOpacity = visual.Opacity; + var isHighlighter = stroke.DrawingAttributes.IsHighlighter; + + // 根据墨迹类型选择不同的动画效果 + if (isHighlighter) + { + StartHighlighterFadeAnimation(visual, stroke, currentOpacity); + } + else + { + StartNormalStrokeFadeAnimation(visual, stroke, currentOpacity); + } + }); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"开始渐隐动画失败: {ex}", LogHelper.LogType.Error); + } + } + + /// + /// 开始普通墨迹的渐隐动画 + /// + private void StartNormalStrokeFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity) + { + try + { + StartProgressiveFadeAnimation(visual, stroke, currentOpacity, AnimationDuration); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"开始普通墨迹渐隐动画失败: {ex}", LogHelper.LogType.Error); + } + } + + /// + /// 开始高亮笔的渐隐动画 + /// + private void StartHighlighterFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity) + { + try + { + StartProgressiveFadeAnimation(visual, stroke, currentOpacity, (int)(AnimationDuration * 1.5)); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"开始高亮笔渐隐动画失败: {ex}", LogHelper.LogType.Error); + } + } + + /// + /// 渐进式渐隐动画 - 从起点到终点逐渐消失 + /// + private void StartProgressiveFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration) + { + try + { + // 确保所有墨迹都能显示动画,包括短墨迹 + if (stroke.StylusPoints.Count < 2) + { + // 只有1个点的墨迹也使用分段动画,确保视觉效果 + CreateSegmentedStroke(visual, stroke, currentOpacity, duration); + return; + } + + // 将墨迹分段并创建多个 Path + CreateSegmentedStroke(visual, stroke, currentOpacity, duration); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"渐进式渐隐动画失败: {ex}", LogHelper.LogType.Error); + // 失败时回退到简单动画 + StartSimpleFadeAnimation(visual, stroke, currentOpacity, duration); + } + } + + /// + /// 创建分段墨迹并开始渐进消失 + /// + private void CreateSegmentedStroke(UIElement originalVisual, Stroke stroke, double opacity, int duration) + { + try + { + var stylusPoints = stroke.StylusPoints; + var totalPoints = stylusPoints.Count; + + // 分段算法 - 确保所有墨迹都有足够的动画效果 + var strokeLength = CalculateStrokeLength(stylusPoints); + var segmentCount = CalculateOptimalSegmentCount(totalPoints, strokeLength); + + // 强制最小分段数量,确保短墨迹也有动画效果 + segmentCount = Math.Max(segmentCount, 4); + + var pointsPerSegment = Math.Max(1, totalPoints / segmentCount); + + // 隐藏原始视觉元素 + originalVisual.Visibility = Visibility.Hidden; + + var segments = new List(); + var parent = _mainWindow.inkCanvas?.Parent as System.Windows.Controls.Panel; + if (parent == null) + { + // 如果父容器不是Panel,直接使用InkCanvas + parent = null; // 稍后会检查并使用InkCanvas.Children + } + + // 创建各个分段 - 确保短墨迹也能正确分段 + for (int i = 0; i < segmentCount; i++) + { + var startIndex = i * pointsPerSegment; + var endIndex = (i == segmentCount - 1) ? totalPoints - 1 : (i + 1) * pointsPerSegment; + + // 确保有足够的点来创建分段,对于短墨迹特殊处理 + if (endIndex <= startIndex && totalPoints > 1) + { + // 短墨迹:每个点作为一个分段 + startIndex = i; + endIndex = Math.Min(i + 1, totalPoints - 1); + } + + // 为每个分段添加重叠,确保连接处平滑 + var overlap = Math.Max(1, pointsPerSegment / 6); // 15%的重叠,平衡平滑与速度 + var actualStartIndex = Math.Max(0, startIndex - overlap); + var actualEndIndex = Math.Min(totalPoints - 1, endIndex + overlap); + + var segment = CreateStrokeSegment(stroke, actualStartIndex, actualEndIndex, opacity); + if (segment != null) + { + segments.Add(segment); + if (parent != null) + { + parent.Children.Add(segment); + } + else if (_mainWindow.inkCanvas != null) + { + _mainWindow.inkCanvas.Children.Add(segment); + } + } + } + + // 开始分段渐隐动画 + StartSegmentedFadeAnimation(segments, stroke, originalVisual, duration); + } + catch (Exception ex) + { + StartSimpleFadeAnimation(originalVisual, stroke, opacity, duration); + } + } + + /// + /// 创建墨迹分段 + /// + private UIElement CreateStrokeSegment(Stroke originalStroke, int startIndex, int endIndex, double opacity) + { + try + { + // 创建分段的 StylusPoint 集合 + var segmentPoints = new StylusPointCollection(); + for (int i = startIndex; i <= endIndex && i < originalStroke.StylusPoints.Count; i++) + { + segmentPoints.Add(originalStroke.StylusPoints[i]); + } + + if (segmentPoints.Count < 2) return null; + + // 创建分段墨迹 + var segmentStroke = new Stroke(segmentPoints) + { + DrawingAttributes = originalStroke.DrawingAttributes.Clone() + }; + + // 创建分段的视觉元素 + var geometry = segmentStroke.GetGeometry(); + if (geometry == null) return null; + + var drawingAttribs = segmentStroke.DrawingAttributes; + var path = new Path + { + Data = geometry, + Stroke = new SolidColorBrush(drawingAttribs.Color), + StrokeThickness = drawingAttribs.Width, + StrokeStartLineCap = drawingAttribs.IsHighlighter ? PenLineCap.Flat : PenLineCap.Round, + StrokeEndLineCap = drawingAttribs.IsHighlighter ? PenLineCap.Flat : PenLineCap.Round, + StrokeLineJoin = drawingAttribs.IsHighlighter ? PenLineJoin.Miter : PenLineJoin.Round, + Fill = drawingAttribs.IsHighlighter ? new SolidColorBrush(drawingAttribs.Color) : null, + Opacity = opacity, + UseLayoutRounding = false, + SnapsToDevicePixels = false + }; + + // 设置位置 + var bounds = geometry.Bounds; + System.Windows.Controls.Canvas.SetLeft(path, bounds.Left); + System.Windows.Controls.Canvas.SetTop(path, bounds.Top); + + return path; + } + catch (Exception ex) + { + return null; + } + } + + /// + /// 开始分段渐隐动画 + /// + private void StartSegmentedFadeAnimation(List segments, Stroke originalStroke, UIElement originalVisual, int totalDuration) + { + try + { + // 动画时序算法 + var segmentDuration = CalculateOptimalSegmentDuration(totalDuration, segments.Count); + var animationCurve = CreateAppleStyleAnimationCurve(segments.Count, totalDuration); + + // 跟踪动画完成状态 + var completedSegments = new HashSet(); + var totalSegments = segments.Count; + + // 渐隐效果 - 使用自然的动画曲线 + for (int i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + + // 使用预计算的动画曲线获取延迟时间 + var delay = animationCurve[i]; + + // 使用定时器延迟启动每个分段的动画 + var timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(delay) + }; + + int segmentIndex = i; // 捕获当前索引 + timer.Tick += (sender, e) => + { + StartSingleSegmentFadeAnimation(segment, segmentDuration, () => + { + // 动画完成回调 + lock (completedSegments) + { + completedSegments.Add(segment); + + // 检查是否所有分段都完成了 + if (completedSegments.Count >= totalSegments) + { + CleanupSegmentedAnimation(segments, originalStroke, originalVisual); + } + } + }); + timer.Stop(); + }; + + timer.Start(); + } + + // 设置一个安全超时定时器,防止无限等待 + var safetyTimeout = totalDuration + (segments.Count * segmentDuration) + 1200; // 额外1.2秒缓冲,确保动画完整 + var safetyTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(safetyTimeout) + }; + + safetyTimer.Tick += (sender, e) => + { + CleanupSegmentedAnimation(segments, originalStroke, originalVisual); + safetyTimer.Stop(); + }; + + safetyTimer.Start(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"分段渐隐动画失败: {ex}", LogHelper.LogType.Error); + CleanupSegmentedAnimation(segments, originalStroke, originalVisual); + } + } + + /// + /// 单个分段的渐隐动画 + /// + private void StartSingleSegmentFadeAnimation(UIElement segment, int duration, Action onCompleted = null) + { + try + { + // 只使用透明度动画,保持墨迹原有粗细 + var fadeAnimation = new DoubleAnimation + { + From = segment.Opacity, + To = 0.0, + Duration = TimeSpan.FromMilliseconds(duration), + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut } // 更平滑的缓动 + }; + + // 添加动画完成事件 + if (onCompleted != null) + { + fadeAnimation.Completed += (sender, e) => + { + onCompleted?.Invoke(); + }; + } + + // 只应用透明度动画,不改变墨迹大小 + segment.BeginAnimation(UIElement.OpacityProperty, fadeAnimation); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"单个分段渐隐动画失败: {ex}", LogHelper.LogType.Error); + // 即使失败也要调用完成回调 + onCompleted?.Invoke(); + } + } + + /// + /// 清理分段动画 + /// + private void CleanupSegmentedAnimation(List segments, Stroke originalStroke, UIElement originalVisual) + { + try + { + // 移除所有分段 + var parent = _mainWindow.inkCanvas?.Parent as System.Windows.Controls.Panel; + + foreach (var segment in segments) + { + if (parent != null && parent.Children.Contains(segment)) + { + parent.Children.Remove(segment); + } + else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(segment)) + { + _mainWindow.inkCanvas.Children.Remove(segment); + } + } + + // 清理原始墨迹 + OnAnimationCompleted(originalVisual, originalStroke); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"清理分段动画失败: {ex}", LogHelper.LogType.Error); + } + } + + /// + /// 简单渐隐动画(备用方案) + /// + private void StartSimpleFadeAnimation(UIElement visual, Stroke stroke, double currentOpacity, int duration) + { + try + { + var fadeAnimation = new DoubleAnimation + { + From = currentOpacity, + To = 0.0, + Duration = TimeSpan.FromMilliseconds(duration), + EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn } + }; + + fadeAnimation.Completed += (sender, e) => OnAnimationCompleted(visual, stroke); + visual.BeginAnimation(UIElement.OpacityProperty, fadeAnimation); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"简单渐隐动画失败: {ex}", LogHelper.LogType.Error); + OnAnimationCompleted(visual, stroke); + } + } + + /// + /// 计算墨迹的实际长度 + /// + private double CalculateStrokeLength(StylusPointCollection points) + { + if (points.Count < 2) return 0; + + double totalLength = 0; + for (int i = 1; i < points.Count; i++) + { + var p1 = points[i - 1].ToPoint(); + var p2 = points[i].ToPoint(); + totalLength += Math.Sqrt(Math.Pow(p2.X - p1.X, 2) + Math.Pow(p2.Y - p1.Y, 2)); + } + return totalLength; + } + + /// + /// 根据墨迹特性计算最优分段数量 - 平衡速度与完整性 + /// + private int CalculateOptimalSegmentCount(int pointCount, double strokeLength) + { + // 平衡速度与完整性,确保动画效果的同时提高速度 + const double PIXELS_PER_SEGMENT = 12.0; // 每段适中长度,平衡效果与速度 + const int MIN_SEGMENTS = 5; // 适当的最小分段数,确保动画效果 + const int MAX_SEGMENTS = 100; // 适中的最大分段数,平衡性能与效果 + + // 根据长度计算基础分段数 + var lengthBasedSegments = Math.Max(MIN_SEGMENTS, (int)(strokeLength / PIXELS_PER_SEGMENT)); + + // 根据点密度调整,平衡效果与速度 + var density = pointCount > 0 ? strokeLength / pointCount : 1; + var densityFactor = Math.Max(0.4, Math.Min(2.5, density / 1.8)); + + var finalSegments = (int)(lengthBasedSegments * densityFactor); + + // 对于短墨迹,确保至少有4个分段 + if (pointCount <= 5) + { + finalSegments = Math.Max(finalSegments, 4); + } + + // 限制在合理范围内 + return Math.Min(MAX_SEGMENTS, Math.Max(MIN_SEGMENTS, finalSegments)); + } + + /// + /// 计算最优的单段动画持续时间 - 平衡速度与完整性 + /// + private int CalculateOptimalSegmentDuration(int totalDuration, int segmentCount) + { + // 平衡速度与动画完整性 + var baseDuration = totalDuration / Math.Max(segmentCount, 1); + var minDuration = 150; // 每段最少150ms,确保动画完整显示 + var maxDuration = 500; // 每段最多500ms,平衡速度与完整性 + + return Math.Max(minDuration, Math.Min(maxDuration, baseDuration)); + } + + /// + /// 创建优化的动画时间曲线 - 平衡速度与完整性 + /// + private int[] CreateAppleStyleAnimationCurve(int segmentCount, int totalDuration) + { + var curve = new int[segmentCount]; + + // 平衡速度与完整性,确保动画有足够时间播放 + var availableTime = totalDuration * 0.6; // 使用60%的总时间,给动画留足够缓冲 + var delayBetweenSegments = Math.Max(60, availableTime / Math.Max(segmentCount, 1)); + + for (int i = 0; i < segmentCount; i++) + { + // 线性延迟,确保每个分段都有足够时间 + curve[i] = (int)(i * delayBetweenSegments); + } + + return curve; + } + + /// + /// 动画完成后的统一处理 + /// + private void OnAnimationCompleted(UIElement visual, Stroke stroke) + { + try + { + // 从父容器中移除墨迹 + var parent = _mainWindow.inkCanvas?.Parent as System.Windows.Controls.Panel; + if (parent != null && parent.Children.Contains(visual)) + { + parent.Children.Remove(visual); + } + else if (_mainWindow.inkCanvas != null && _mainWindow.inkCanvas.Children.Contains(visual)) + { + _mainWindow.inkCanvas.Children.Remove(visual); + } + + RemoveStroke(stroke); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"渐隐动画完成后清理墨迹失败: {ex}", LogHelper.LogType.Error); + } + } + #endregion + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/PPTInkManager.cs b/Ink Canvas/Helpers/PPTInkManager.cs index 0c548829..f8c62ccb 100644 --- a/Ink Canvas/Helpers/PPTInkManager.cs +++ b/Ink Canvas/Helpers/PPTInkManager.cs @@ -55,6 +55,13 @@ namespace Ink_Canvas.Helpers { try { + // 完全清理之前的墨迹状态 + ClearAllStrokes(); + + // 重置墨迹锁定状态 + _inkLockUntil = DateTime.MinValue; + _lockedSlideIndex = -1; + // 生成演示文稿唯一标识符 _currentPresentationId = GeneratePresentationId(presentation); @@ -152,17 +159,28 @@ namespace Ink_Canvas.Helpers { try { - // 如果有当前墨迹,先保存 + // 如果有当前墨迹,先保存到正确的页面 if (currentStrokes != null && currentStrokes.Count > 0) { - SaveCurrentSlideStrokes(_lockedSlideIndex > 0 ? _lockedSlideIndex : slideIndex, currentStrokes); + // 确定要保存的页面索引 + int saveToSlideIndex = _lockedSlideIndex > 0 ? _lockedSlideIndex : slideIndex; + + // 确保页面索引有效 + if (saveToSlideIndex > 0 && saveToSlideIndex < _memoryStreams.Length) + { + SaveCurrentSlideStrokes(saveToSlideIndex, currentStrokes); + LogHelper.WriteLogToFile($"已保存第{saveToSlideIndex}页墨迹,墨迹数量: {currentStrokes.Count}", LogHelper.LogType.Trace); + } } // 设置墨迹锁定 LockInkForSlide(slideIndex); // 加载新页面的墨迹 - return LoadSlideStrokes(slideIndex); + var newStrokes = LoadSlideStrokes(slideIndex); + LogHelper.WriteLogToFile($"已切换到第{slideIndex}页,加载墨迹数量: {newStrokes.Count}", LogHelper.LogType.Trace); + + return newStrokes; } catch (Exception ex) { diff --git a/Ink Canvas/Helpers/PPTManager.cs b/Ink Canvas/Helpers/PPTManager.cs index 1a2d5a90..c51fa589 100644 --- a/Ink Canvas/Helpers/PPTManager.cs +++ b/Ink Canvas/Helpers/PPTManager.cs @@ -25,6 +25,7 @@ namespace Ink_Canvas.Helpers public event Action PresentationOpen; public event Action PresentationClose; public event Action PPTConnectionChanged; + public event Action SlideShowStateChanged; #endregion #region Properties @@ -92,6 +93,7 @@ namespace Ink_Canvas.Helpers #region Private Fields private Timer _connectionCheckTimer; + private Timer _slideShowStateCheckTimer; private Timer _wpsProcessCheckTimer; private Process _wpsProcess; private bool _hasWpsProcessId; @@ -99,6 +101,7 @@ namespace Ink_Canvas.Helpers private int _wpsProcessCheckCount; private WpsWindowInfo _lastForegroundWpsWindow; private DateTime _lastWindowCheckTime = DateTime.MinValue; + private bool _lastSlideShowState = false; private readonly object _lockObject = new object(); private bool _disposed = false; #endregion @@ -114,6 +117,10 @@ namespace Ink_Canvas.Helpers _connectionCheckTimer = new Timer(500); _connectionCheckTimer.Elapsed += OnConnectionCheckTimerElapsed; _connectionCheckTimer.AutoReset = true; + + _slideShowStateCheckTimer = new Timer(1000); + _slideShowStateCheckTimer.Elapsed += OnSlideShowStateCheckTimerElapsed; + _slideShowStateCheckTimer.AutoReset = true; } public void StartMonitoring() @@ -121,6 +128,7 @@ namespace Ink_Canvas.Helpers if (!_disposed) { _connectionCheckTimer?.Start(); + _slideShowStateCheckTimer?.Start(); LogHelper.WriteLogToFile("PPT监控已启动", LogHelper.LogType.Trace); } } @@ -128,6 +136,7 @@ namespace Ink_Canvas.Helpers public void StopMonitoring() { _connectionCheckTimer?.Stop(); + _slideShowStateCheckTimer?.Stop(); DisconnectFromPPT(); LogHelper.WriteLogToFile("PPT监控已停止", LogHelper.LogType.Trace); } @@ -146,6 +155,18 @@ namespace Ink_Canvas.Helpers } } + private void OnSlideShowStateCheckTimerElapsed(object sender, ElapsedEventArgs e) + { + try + { + CheckSlideShowState(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"PPT放映状态检查失败: {ex}", LogHelper.LogType.Error); + } + } + private void CheckAndConnectToPPT() { lock (_lockObject) @@ -187,6 +208,30 @@ namespace Ink_Canvas.Helpers } } + private void CheckSlideShowState() + { + try + { + if (!IsConnected) return; + + var currentSlideShowState = IsInSlideShow; + if (currentSlideShowState != _lastSlideShowState) + { + _lastSlideShowState = currentSlideShowState; + SlideShowStateChanged?.Invoke(currentSlideShowState); + + if (!currentSlideShowState) + { + LogHelper.WriteLogToFile("检测到PPT放映已结束", LogHelper.LogType.Trace); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"检查PPT放映状态异常: {ex}", LogHelper.LogType.Error); + } + } + private Microsoft.Office.Interop.PowerPoint.Application TryConnectToPowerPoint() { try @@ -1627,6 +1672,7 @@ namespace Ink_Canvas.Helpers StopWpsProcessCheckTimer(); _connectionCheckTimer?.Dispose(); + _slideShowStateCheckTimer?.Dispose(); _wpsProcessCheckTimer?.Dispose(); _disposed = true; diff --git a/Ink Canvas/Helpers/PPTUIManager.cs b/Ink Canvas/Helpers/PPTUIManager.cs index ca235958..5bf4b636 100644 --- a/Ink Canvas/Helpers/PPTUIManager.cs +++ b/Ink Canvas/Helpers/PPTUIManager.cs @@ -18,7 +18,10 @@ namespace Ink_Canvas.Helpers public int PPTBButtonsOption { get; set; } = 121; public int PPTLSButtonPosition { get; set; } = 0; public int PPTRSButtonPosition { get; set; } = 0; + public int PPTLBButtonPosition { get; set; } = 0; + public int PPTRBButtonPosition { get; set; } = 0; public bool EnablePPTButtonPageClickable { get; set; } = true; + public bool EnablePPTButtonLongPressPageTurn { get; set; } = true; #endregion #region Private Fields @@ -120,6 +123,29 @@ namespace Ink_Canvas.Helpers }); } + /// + /// 处理PPT放映状态变化 + /// + public void OnSlideShowStateChanged(bool isInSlideShow) + { + _dispatcher.InvokeAsync(() => + { + try + { + if (!isInSlideShow) + { + // 如果不在放映模式,隐藏所有导航面板 + HideAllNavigationPanels(); + LogHelper.WriteLogToFile("PPT放映状态变化:隐藏导航面板", LogHelper.LogType.Trace); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"处理PPT放映状态变化失败: {ex}", LogHelper.LogType.Error); + } + }); + } + /// /// 更新导航面板显示状态 /// @@ -130,7 +156,10 @@ namespace Ink_Canvas.Helpers try { // 检查是否应该显示PPT按钮 - bool shouldShowButtons = ShowPPTButton && _mainWindow.BtnPPTSlideShowEnd.Visibility == Visibility.Visible; + // 不仅要检查按钮设置,还要确保确实在PPT放映模式下 + bool shouldShowButtons = ShowPPTButton && + _mainWindow.BtnPPTSlideShowEnd.Visibility == Visibility.Visible && + _mainWindow.PPTManager?.IsInSlideShow == true; if (!shouldShowButtons) { @@ -142,6 +171,10 @@ namespace Ink_Canvas.Helpers _mainWindow.LeftSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTLSButtonPosition * 2); _mainWindow.RightSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTRSButtonPosition * 2); + // 设置底部按钮水平位置 + _mainWindow.LeftBottomPanelForPPTNavigation.Margin = new Thickness(6 + PPTLBButtonPosition, 0, 0, 6); + _mainWindow.RightBottomPanelForPPTNavigation.Margin = new Thickness(0, 0, 6 + PPTRBButtonPosition, 6); + // 根据显示选项设置面板可见性 var displayOption = PPTButtonsDisplayOption.ToString(); if (displayOption.Length >= 4) diff --git a/Ink Canvas/HotkeyConfig.json b/Ink Canvas/HotkeyConfig.json new file mode 100644 index 00000000..4ce2d1ee --- /dev/null +++ b/Ink Canvas/HotkeyConfig.json @@ -0,0 +1,96 @@ +{ + "Version": "1.0", + "LastModified": "2025-01-28T15:30:00", + "Hotkeys": [ + { + "Name": "Undo", + "Key": "Z", + "Modifiers": "Control" + }, + { + "Name": "Redo", + "Key": "Y", + "Modifiers": "Control" + }, + { + "Name": "Clear", + "Key": "E", + "Modifiers": "Control" + }, + { + "Name": "Paste", + "Key": "V", + "Modifiers": "Control" + }, + { + "Name": "SelectTool", + "Key": "S", + "Modifiers": "Alt" + }, + { + "Name": "DrawTool", + "Key": "D", + "Modifiers": "Alt" + }, + { + "Name": "EraserTool", + "Key": "E", + "Modifiers": "Alt" + }, + { + "Name": "BlackboardTool", + "Key": "B", + "Modifiers": "Alt" + }, + { + "Name": "QuitDrawTool", + "Key": "Q", + "Modifiers": "Alt" + }, + { + "Name": "Pen1", + "Key": "D1", + "Modifiers": "Alt" + }, + { + "Name": "Pen2", + "Key": "D2", + "Modifiers": "Alt" + }, + { + "Name": "Pen3", + "Key": "D3", + "Modifiers": "Alt" + }, + { + "Name": "Pen4", + "Key": "D4", + "Modifiers": "Alt" + }, + { + "Name": "Pen5", + "Key": "D5", + "Modifiers": "Alt" + }, + { + "Name": "DrawLine", + "Key": "L", + "Modifiers": "Alt" + }, + { + "Name": "Screenshot", + "Key": "C", + "Modifiers": "Alt" + }, + { + "Name": "Hide", + "Key": "V", + "Modifiers": "Alt" + }, + { + "Name": "Exit", + "Key": "Escape", + "Modifiers": "None" + } + ] +} \ No newline at end of file diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index ae433f8f..da24ff88 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -832,6 +832,27 @@ IsOn="True" FontFamily="Microsoft YaHei UI" FontWeight="Bold" Toggled="ToggleSwitchAdvancedBezierSmoothing_Toggled" /> + + + + + + + + + + + + @@ -1197,16 +1218,16 @@ - + - + @@ -1215,16 +1236,16 @@ - + - + @@ -1233,16 +1254,16 @@ - + - + @@ -1251,16 +1272,16 @@ - + - + @@ -1268,9 +1289,9 @@ - + @@ -1430,9 +1451,21 @@ + VerticalAlignment="Bottom" HorizontalAlignment="Left" RenderTransformOrigin="0.5,0.5"> + + + + + + + VerticalAlignment="Bottom" HorizontalAlignment="Right" RenderTransformOrigin="0.5,0.5"> + + + + + + @@ -1618,6 +1651,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -3860,7 +4061,7 @@ - @@ -3874,7 +4075,7 @@ Background="#fafafa" Opacity="1" BorderBrush="#2563eb" BorderThickness="1" CornerRadius="8"> - + - + + - - - - - - - + + + + @@ -6900,7 +7113,7 @@ - + - - - + + + - - - - - + + + + @@ -7768,7 +7991,7 @@ + CornerRadius="5" Margin="-170,-140,-147,37"> + + + + + + + + + + + +