diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index ffacb8e95..e401b8322 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -1,6 +1,7 @@ using H.NotifyIcon; using Ink_Canvas.Helpers; using Ink_Canvas.Plugins; +using Ink_Canvas.Services; using Ink_Canvas.Properties; using iNKORE.UI.WPF.Modern.Controls; using Microsoft.Win32; @@ -209,6 +210,8 @@ public App() } catch { + // 此处守卫的正是日志写入本身:日志系统尚未就绪或写盘失败时无法再记录, + // 只能静默吞掉以保证启动流程不被日志故障中断(这是全仓唯一允许无日志的空 catch)。 } System.Windows.Forms.Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); @@ -237,7 +240,7 @@ public App() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[App] 设置 AppUserModelID 失败(任务栏分组可能异常): {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } // 配置TLS协议以支持Windows 7 @@ -434,7 +437,7 @@ private void ConfigureTlsForWindows7() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[App] TLS 兼容配置失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -524,7 +527,7 @@ private void BindMainWindowLifecycle() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Crash] 绑定主窗口 SourceInitialized 事件失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -547,7 +550,7 @@ private void MainWindow_SourceInitialized(object sender, EventArgs e) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Crash] 注册主窗口销毁监听失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -605,7 +608,7 @@ private void CleanupTerminationMonitoring() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Crash] 卸载窗口销毁 WinEvent 钩子失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -1039,16 +1042,15 @@ private static void SyncCrashActionFromParsed(dynamic parsedSettings) try { int crashAction = 2; - try { crashAction = (int)(parsedSettings?["startup"]?["crashAction"] ?? 2); } - catch (Exception ex) + try { crashAction = (int)(parsedSettings?["startup"]?["crashAction"] ?? 2); } catch (Exception ex) { - LogHelper.WriteLogToFile($"[App] 解析崩溃后动作设置失败,改用默认值 2: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } CrashAction = (CrashActionType)crashAction; } catch (Exception ex) { - LogHelper.WriteLogToFile($"[App] 同步崩溃后动作失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -2260,9 +2262,74 @@ public static void RunWatchdogIfNeeded() } } + // 装配托盘服务的依赖委托。MainWindow/App 私有成员的访问全部经此处注入, + // TrayIconService 不引用 MainWindow 类型(事件出抛方向唯一:Service → App)。 + private TrayIconService CreateTrayIconService() + { + var service = new TrayIconService(new TrayIconService.Hooks + { + GetMainWindow = () => Current.MainWindow, + GetTrayLeftClickAction = () => Ink_Canvas.MainWindow.Settings.Appearance.TrayLeftClickAction, + GetTrayRightClickAction = () => Ink_Canvas.MainWindow.Settings.Appearance.TrayRightClickAction, + IsAlwaysOnTop = () => Ink_Canvas.MainWindow.Settings.Advanced.IsAlwaysOnTop, + IsNoFocusMode = () => Ink_Canvas.MainWindow.Settings.Advanced.IsNoFocusMode, + IsFloatingBarFolded = () => ((MainWindow)Current.MainWindow).isFloatingBarFolded, + FoldFloatingBar = () => ((MainWindow)Current.MainWindow).FoldFloatingBar_MouseUp(new object(), null), + UnfoldFloatingBar = () => ((MainWindow)Current.MainWindow).UnFoldFloatingBar_MouseUp(new object(), null), + ResetFloatingBarPosition = () => + { + var mw = (MainWindow)Current.MainWindow; + var isInPPTPresentationMode = mw.IsInPPTPresentationMode; + if (!mw.isFloatingBarFolded) + { + // 清空保存的状态,强制动画走默认位置分支 + mw._userHasDraggedFloatingBar = false; + mw.pointDesktop = new Point(-1, -1); + mw.pointPPT = new Point(-1, -1); + + if (!isInPPTPresentationMode) mw.PureViewboxFloatingBarMarginAnimationInDesktopMode(); + else mw.PureViewboxFloatingBarMarginAnimationInPPTMode(); + } + }, + SetTrayTemporaryShowUntilUtc = value => Ink_Canvas.MainWindow.TrayTemporaryShowUntilUtc = value, + UiInvoke = action => Dispatcher.BeginInvoke(action), + OpenSettings = () => + { + try + { + var method = typeof(MainWindow).GetMethod("BtnSettings_Click", BindingFlags.NonPublic | BindingFlags.Instance); + method?.Invoke(Current.MainWindow, new object[] { null, null }); + } + catch (Exception ex) { LogHelper.WriteLogToFile($"Open settings from tray failed: {ex.Message}", LogHelper.LogType.Error); } + }, + MarkExitByUser = () => IsAppExitByUser = true, + ExitApplicationByUser = () => + { + IsAppExitByUser = true; + ((MainWindow)Current.MainWindow).ExitApplication(null, null); + }, + Shutdown = () => Current.Shutdown(), + ForceFullScreen = () => + { + var mw = (MainWindow)Current.MainWindow; + Ink_Canvas.MainWindow.MoveWindow(new WindowInteropHelper(mw).Handle, 0, 0, + Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, true); + Ink_Canvas.MainWindow.ShowNewMessage($"已强制全屏化:{Screen.PrimaryScreen.Bounds.Width}x{Screen.PrimaryScreen.Bounds.Height}(缩放比例为{Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth}x{Screen.PrimaryScreen.Bounds.Height / SystemParameters.PrimaryScreenHeight})"); + }, + CheckMainWindowVisibility = () => ((MainWindow)Current.MainWindow).CheckMainWindowVisibility(), + GetGlobalHotkeyManager = () => typeof(MainWindow) + .GetField("_globalHotkeyManager", BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetValue(Current.MainWindow) as GlobalHotkeyManager, + }); + service.TrayLeftClicked += () => PluginTrayLeftClicked?.Invoke(); + service.TrayRightClicked += () => PluginTrayRightClicked?.Invoke(); + return service; + } + private void App_Exit(object sender, ExitEventArgs e) { isAppExiting = true; + _trayIconService?.Dispose(); LogHelper.WriteLogToFile( $"[Exit] 开始应用退出清理: user={IsAppExitByUser}, code={e.ApplicationExitCode}, crashAction={CrashAction}", LogHelper.LogType.Info); @@ -2279,11 +2346,11 @@ private void App_Exit(object sender, ExitEventArgs e) try { heartbeatTimer?.Stop(); } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Exit] 停止心跳计时器失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } try { watchdogTimer?.Change(Timeout.Infinite, Timeout.Infinite); watchdogTimer?.Dispose(); } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Exit] 释放看门狗计时器失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } MemoryBreakdownHelper.StopAutomaticDumpMonitor(); @@ -2311,7 +2378,7 @@ private void App_Exit(object sender, ExitEventArgs e) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Exit] 释放单实例互斥体失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } // 卸载所有插件 diff --git a/Ink Canvas/Automation/ActionHandlers/KillProcessActionHandler.cs b/Ink Canvas/Automation/ActionHandlers/KillProcessActionHandler.cs index e21af0f9d..534b97b5a 100644 --- a/Ink Canvas/Automation/ActionHandlers/KillProcessActionHandler.cs +++ b/Ink Canvas/Automation/ActionHandlers/KillProcessActionHandler.cs @@ -19,16 +19,15 @@ public KillProcessActionHandler(IActionService actionService) { foreach (var process in Process.GetProcessesByName(s.ProcessName)) { - try { process.Kill(); } - catch (Exception ex) + try { process.Kill(); } catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Automation] 结束进程 处理器: 结束进程 \"{s.ProcessName}\" 中的某个进程失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Automation] 结束进程 处理器: 枚举进程 \"{s.ProcessName}\" 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } }); } diff --git a/Ink Canvas/Automation/Actions/KillProcessAction.cs b/Ink Canvas/Automation/Actions/KillProcessAction.cs index 77a2f0cd5..0612d337e 100644 --- a/Ink Canvas/Automation/Actions/KillProcessAction.cs +++ b/Ink Canvas/Automation/Actions/KillProcessAction.cs @@ -43,15 +43,15 @@ public static ActionRegistryInfo Register() { process.Kill(); } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Automation] 结束进程 行动: 结束进程 \"{s.ProcessName}\" 中的某个进程失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Automation] 结束进程 行动: 枚举进程 \"{s.ProcessName}\" 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } }; diff --git a/Ink Canvas/Automation/AutomationBootstrap.cs b/Ink Canvas/Automation/AutomationBootstrap.cs index 847a0bd5a..53c3961b0 100644 --- a/Ink Canvas/Automation/AutomationBootstrap.cs +++ b/Ink Canvas/Automation/AutomationBootstrap.cs @@ -158,10 +158,9 @@ public static void Initialize() { // 任意步骤失败时整体回滚到未初始化状态,避免后续 AutomationBootstrap 调用走错误路径 LogHelper.WriteLogToFile($"[Automation] 初始化失败,已回滚: {ex.Message}", LogHelper.LogType.Error); - try { Shutdown(); } - catch (Exception shutdownEx) + try { Shutdown(); } catch (Exception shutdownEx) { - LogHelper.WriteLogToFile($"[Automation] 初始化失败后回滚 Shutdown 也失败: {shutdownEx.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(shutdownEx); } throw; } diff --git a/Ink Canvas/Automation/Services/AutomationService.cs b/Ink Canvas/Automation/Services/AutomationService.cs index e5c8fc4eb..5831f01ad 100644 --- a/Ink Canvas/Automation/Services/AutomationService.cs +++ b/Ink Canvas/Automation/Services/AutomationService.cs @@ -329,7 +329,7 @@ private void UnloadTrigger(Workflow workflow, TriggerSettings trigger) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Automation] 卸载触发器 \"{trigger.Id}\" 的 UnLoaded() 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } trigger.TriggerInstance.Triggered -= TriggerTriggered; diff --git a/Ink Canvas/Automation/Services/SystemEventMonitor.cs b/Ink Canvas/Automation/Services/SystemEventMonitor.cs index 11a911c9c..7faec40de 100644 --- a/Ink Canvas/Automation/Services/SystemEventMonitor.cs +++ b/Ink Canvas/Automation/Services/SystemEventMonitor.cs @@ -258,7 +258,7 @@ private void OnForegroundFallbackElapsed(object sender, ElapsedEventArgs e) } catch (Exception ex) { - LogCallbackException("前台窗口兜底轮询获取当前前台窗口", ex); + Helpers.LogService.LogException(ex); } } @@ -284,10 +284,9 @@ public void Dispose() if (_foregroundHook != HWINEVENTHOOK.Null) { - try { PInvoke.UnhookWinEvent(_foregroundHook); } - catch (Exception ex) + try { PInvoke.UnhookWinEvent(_foregroundHook); } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Automation] SystemEventMonitor 释放时卸载前台窗口 WinEvent 钩子失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } _foregroundHook = HWINEVENTHOOK.Null; } diff --git a/Ink Canvas/Automation/Triggers/AnnotationModeEnterTrigger.cs b/Ink Canvas/Automation/Triggers/AnnotationModeEnterTrigger.cs index a80307d12..37d2e271f 100644 --- a/Ink Canvas/Automation/Triggers/AnnotationModeEnterTrigger.cs +++ b/Ink Canvas/Automation/Triggers/AnnotationModeEnterTrigger.cs @@ -65,9 +65,9 @@ private void TrySubscribeInkCanvas() mw.inkCanvas.EditingModeChanged += OnEditingModeChanged; } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Automation] 批注模式进入触发器订阅 inkCanvas.EditingModeChanged 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -81,9 +81,9 @@ private void TryUnsubscribeInkCanvas() mw.inkCanvas.EditingModeChanged -= OnEditingModeChanged; } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Automation] 批注模式进入触发器退订 inkCanvas.EditingModeChanged 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } diff --git a/Ink Canvas/Automation/Triggers/AnnotationModeExitTrigger.cs b/Ink Canvas/Automation/Triggers/AnnotationModeExitTrigger.cs index e487f3c36..9f3bb8218 100644 --- a/Ink Canvas/Automation/Triggers/AnnotationModeExitTrigger.cs +++ b/Ink Canvas/Automation/Triggers/AnnotationModeExitTrigger.cs @@ -65,9 +65,9 @@ private void TrySubscribeInkCanvas() mw.inkCanvas.EditingModeChanged += OnEditingModeChanged; } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Automation] 批注模式退出触发器订阅 inkCanvas.EditingModeChanged 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -81,9 +81,9 @@ private void TryUnsubscribeInkCanvas() mw.inkCanvas.EditingModeChanged -= OnEditingModeChanged; } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Automation] 批注模式退出触发器退订 inkCanvas.EditingModeChanged 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } diff --git a/Ink Canvas/Automation/Triggers/PptSlideShowEnterTrigger.cs b/Ink Canvas/Automation/Triggers/PptSlideShowEnterTrigger.cs index 3d09ebd90..717983c4c 100644 --- a/Ink Canvas/Automation/Triggers/PptSlideShowEnterTrigger.cs +++ b/Ink Canvas/Automation/Triggers/PptSlideShowEnterTrigger.cs @@ -83,9 +83,9 @@ private static bool IsPowerPointInSlideShow() return true; } } - catch (Exception ex) + catch (System.Exception ex) { - LogCallbackException("读取 POWERPNT 进程主窗口标题判断是否处于放映", ex); + Helpers.LogService.LogException(ex); } } return false; diff --git a/Ink Canvas/Automation/Triggers/PptSlideShowExitTrigger.cs b/Ink Canvas/Automation/Triggers/PptSlideShowExitTrigger.cs index 1dc6d7ebf..dfd97a228 100644 --- a/Ink Canvas/Automation/Triggers/PptSlideShowExitTrigger.cs +++ b/Ink Canvas/Automation/Triggers/PptSlideShowExitTrigger.cs @@ -83,9 +83,9 @@ private static bool IsPowerPointInSlideShow() return true; } } - catch (Exception ex) + catch (System.Exception ex) { - LogCallbackException("读取 POWERPNT 进程主窗口标题判断是否处于放映", ex); + Helpers.LogService.LogException(ex); } } return false; diff --git a/Ink Canvas/Automation/Triggers/PptSlideShowTrigger.cs b/Ink Canvas/Automation/Triggers/PptSlideShowTrigger.cs index 5279cf342..1dc9d5f89 100644 --- a/Ink Canvas/Automation/Triggers/PptSlideShowTrigger.cs +++ b/Ink Canvas/Automation/Triggers/PptSlideShowTrigger.cs @@ -84,9 +84,9 @@ private static bool IsPowerPointInSlideShow() return true; } } - catch (Exception ex) + catch (System.Exception ex) { - LogCallbackException("读取 POWERPNT 进程主窗口标题判断是否处于放映", ex); + Helpers.LogService.LogException(ex); } } return false; diff --git a/Ink Canvas/Controls/CanvasMediaControl.xaml.cs b/Ink Canvas/Controls/CanvasMediaControl.xaml.cs index 0f9f241dd..315a42503 100644 --- a/Ink Canvas/Controls/CanvasMediaControl.xaml.cs +++ b/Ink Canvas/Controls/CanvasMediaControl.xaml.cs @@ -160,9 +160,9 @@ public void PausePlayback() _isPlaying = false; UpdatePlayPauseGlyph(); } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Media] 暂停播放失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -179,9 +179,9 @@ public void StopPlayback() SeekTo(TimeSpan.Zero); } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Media] 停止播放失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -193,9 +193,9 @@ public void Shutdown() Player.Stop(); Player.Source = null; } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Media] 关闭媒体控件(卸载渲染钩子/停止播放/释放源)失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -299,9 +299,9 @@ private void Player_MediaEnded(object sender, RoutedEventArgs e) { Player.Stop(); } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Media] 播放结束时停止播放器失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } SeekTo(TimeSpan.Zero); UpdatePlayPauseGlyph(); @@ -330,9 +330,9 @@ private void TogglePlayback() } UpdatePlayPauseGlyph(); } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Media] 切换播放/暂停状态失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } @@ -401,9 +401,9 @@ private void SeekTo(TimeSpan position) { Player.Position = position; } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Media] 定位播放位置到 {position} 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } UpdateTimeText(position); @@ -465,9 +465,9 @@ private void ApplySelectedSpeed() { Player.SpeedRatio = ratio; } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Media] 应用播放倍速 {ratio} 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } diff --git a/Ink Canvas/Controls/DynamicNotificationControl.xaml.cs b/Ink Canvas/Controls/DynamicNotificationControl.xaml.cs index 731dcaf1b..ad3e64d20 100644 --- a/Ink Canvas/Controls/DynamicNotificationControl.xaml.cs +++ b/Ink Canvas/Controls/DynamicNotificationControl.xaml.cs @@ -179,9 +179,9 @@ private void ActionButton_Click(object sender, RoutedEventArgs e) Process.Start(new ProcessStartInfo(currentMessage.ActionUrl) { UseShellExecute = true }); } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Notification] 执行动态通知的动作回调/打开动作链接失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } Close(); diff --git a/Ink Canvas/Controls/Toolbar/BoardToolbar/BoardToolbarRegistry.cs b/Ink Canvas/Controls/Toolbar/BoardToolbar/BoardToolbarRegistry.cs index 970112fb8..43e1fbc66 100644 --- a/Ink Canvas/Controls/Toolbar/BoardToolbar/BoardToolbarRegistry.cs +++ b/Ink Canvas/Controls/Toolbar/BoardToolbar/BoardToolbarRegistry.cs @@ -447,10 +447,9 @@ public static void SaveConfigFile(string name, BoardToolbarLayoutSettings layout } catch (Exception innerEx) { - try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } - catch (Exception ex) + try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[Toolbar] BoardToolbarRegistry 原子写入失败后删除临时文件 {Path.GetFileName(tmpPath)} 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } throw new Exception($"原子写入失败: {innerEx.Message}", innerEx); } diff --git a/Ink Canvas/Helpers/AutoFontSizeHelper.cs b/Ink Canvas/Helpers/AutoFontSizeHelper.cs index 2e523c213..a03d821f9 100644 --- a/Ink Canvas/Helpers/AutoFontSizeHelper.cs +++ b/Ink Canvas/Helpers/AutoFontSizeHelper.cs @@ -418,9 +418,9 @@ private static double MeasureTextWidth(FrameworkElement fe, string text, double { culture = fe.Language.GetEquivalentCulture(); } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[UI] 取控件语言对应的 Culture 失败,回退到当前 UI Culture: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } diff --git a/Ink Canvas/Helpers/CapturableVideoCaptureElement.cs b/Ink Canvas/Helpers/CapturableVideoCaptureElement.cs index 8fbe10506..4ee864e49 100644 --- a/Ink Canvas/Helpers/CapturableVideoCaptureElement.cs +++ b/Ink Canvas/Helpers/CapturableVideoCaptureElement.cs @@ -137,7 +137,10 @@ private static void LogCaptureDiag(string message) LogHelper.WriteLogToFile($"[CapturableVideoCaptureElement] {message}", LogHelper.LogType.Info); Debug.WriteLine($"[CapturableVideoCaptureElement] {message}"); } - catch { } + catch (System.Exception ex) + { + Helpers.LogService.LogException(ex); + } } } } diff --git a/Ink Canvas/Helpers/Converters.cs b/Ink Canvas/Helpers/Converters.cs index 860bce55d..d7f833378 100644 --- a/Ink Canvas/Helpers/Converters.cs +++ b/Ink Canvas/Helpers/Converters.cs @@ -209,7 +209,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn } catch (Exception ex) { - LogHelper.WriteLogToFile($"[UI] 几何字符串解析失败,图标将不显示: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } return null; } diff --git a/Ink Canvas/Helpers/DebugConsoleManager.cs b/Ink Canvas/Helpers/DebugConsoleManager.cs index a6f5328c5..3ca2cced4 100644 --- a/Ink Canvas/Helpers/DebugConsoleManager.cs +++ b/Ink Canvas/Helpers/DebugConsoleManager.cs @@ -103,9 +103,9 @@ public static void WriteLine(string line, ConsoleColor? color = null) Console.WriteLine(line); } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[UI] 写入调试控制台失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } } diff --git a/Ink Canvas/Helpers/DeviceIdentifier.cs b/Ink Canvas/Helpers/DeviceIdentifier.cs index 99dd3827c..4c2d44b01 100644 --- a/Ink Canvas/Helpers/DeviceIdentifier.cs +++ b/Ink Canvas/Helpers/DeviceIdentifier.cs @@ -260,9 +260,9 @@ private static string GetSystemDriveVolumeSerial() return serialNumber.ToString("X8"); } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[DeviceId] 读取系统盘卷序列号失败(硬件指纹的 DISK 段将退化为其它来源): {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } return null; @@ -380,9 +380,9 @@ private static DeviceIdInfo LoadDeviceIdFromFile(string filePath) return info; } } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile($"[DeviceId] 解析设备ID文件({filePath})中的 JSON 内容失败,将回退按纯文本读取: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } if (IsValidDeviceId(content)) diff --git a/Ink Canvas/Helpers/DirectShowCameraService.cs b/Ink Canvas/Helpers/DirectShowCameraService.cs index 93b5b9168..ed55ca41a 100644 --- a/Ink Canvas/Helpers/DirectShowCameraService.cs +++ b/Ink Canvas/Helpers/DirectShowCameraService.cs @@ -208,7 +208,7 @@ public Task ProbeCameraPropertiesAsync() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Booth] 探测摄像头属性时从采集 pin 获取 IAMVideoProcAmp/IAMCameraControl 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -389,38 +389,22 @@ public void ReleaseCameraPropertyControl() { if (_propVideoProcAmp != null) { - try { Marshal.ReleaseComObject(_propVideoProcAmp); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 释放 IAMVideoProcAmp COM 引用失败: {ex.Message}", LogHelper.LogType.Info); - } + try { Marshal.ReleaseComObject(_propVideoProcAmp); } catch (Exception ex) { LogService.LogException(ex); } _propVideoProcAmp = null; } if (_propCameraControl != null) { - try { Marshal.ReleaseComObject(_propCameraControl); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 释放 IAMCameraControl COM 引用失败: {ex.Message}", LogHelper.LogType.Info); - } + try { Marshal.ReleaseComObject(_propCameraControl); } catch (Exception ex) { LogService.LogException(ex); } _propCameraControl = null; } if (_propSourceFilter != null) { - try { Marshal.ReleaseComObject(_propSourceFilter); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 释放属性探测用 source filter COM 引用失败: {ex.Message}", LogHelper.LogType.Info); - } + try { Marshal.ReleaseComObject(_propSourceFilter); } catch (Exception ex) { LogService.LogException(ex); } _propSourceFilter = null; } if (_propGraph != null) { - try { Marshal.ReleaseComObject(_propGraph); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 释放属性探测用 FilterGraphNoThread COM 引用失败: {ex.Message}", LogHelper.LogType.Info); - } + try { Marshal.ReleaseComObject(_propGraph); } catch (Exception ex) { LogService.LogException(ex); } _propGraph = null; } // 不清 _cameraPropStates 的 Supported 状态:调用方切换摄像头后会重新 Probe 覆盖; @@ -493,7 +477,7 @@ public IReadOnlyList GetFrameratesFor(int width, int height) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Booth] 汇总指定分辨率({width}x{height})支持的帧率列表失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return result; } @@ -864,7 +848,10 @@ private void EnumerateCapabilitiesFromConfig(IAMStreamConfig config) $"formatType={mt.formatType}", LogHelper.LogType.Info); } - catch { } + catch (Exception ex) + { + LogService.LogException(ex); + } // === 帧率枚举策略(修复 50fps 误判) === // 驱动通过两种方式暴露帧率: @@ -1294,11 +1281,7 @@ private Task StopPreviewAsyncInternal() { if (_mediaControl != null) { - try { _mediaControl.Stop(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 内部停止预览时停止 IMediaControl 图失败: {ex.Message}", LogHelper.LogType.Info); - } + try { _mediaControl.Stop(); } catch (Exception ex) { LogService.LogException(ex); } } CleanupGraph(); _isCapturing = false; @@ -1320,11 +1303,7 @@ public void StopPreview() if (_mediaControl != null) { - try { _mediaControl.Stop(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 停止摄像头预览时停止 IMediaControl 图失败: {ex.Message}", LogHelper.LogType.Info); - } + try { _mediaControl.Stop(); } catch (Exception ex) { LogService.LogException(ex); } } CleanupGraph(); @@ -1356,11 +1335,7 @@ private void CleanupGraph() // 先断开 sample grabber 回调,避免释放过程中触发 if (_sampleGrabber != null) { - try { _sampleGrabber.SetCallback(null, 0); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 清理 DirectShow 图前断开 SampleGrabber 回调失败: {ex.Message}", LogHelper.LogType.Info); - } + try { _sampleGrabber.SetCallback(null, 0); } catch (Exception ex) { LogService.LogException(ex); } } // 释放子 filter RCW(每个是独立的 RCW) diff --git a/Ink Canvas/Helpers/FloatingBarThemeMarketService.cs b/Ink Canvas/Helpers/FloatingBarThemeMarketService.cs index c18793da4..c74b297fa 100644 --- a/Ink Canvas/Helpers/FloatingBarThemeMarketService.cs +++ b/Ink Canvas/Helpers/FloatingBarThemeMarketService.cs @@ -100,14 +100,7 @@ public async Task InstallAsync(ThemeMarketEntry entry) } finally { - try - { - if (File.Exists(tempPath)) File.Delete(tempPath); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Theme] 清理主题安装临时包 {tempPath} 失败(将残留于临时目录): {ex.Message}", LogHelper.LogType.Info); - } + try { if (File.Exists(tempPath)) File.Delete(tempPath); } catch (Exception ex) { LogService.LogException(ex); } } } } diff --git a/Ink Canvas/Helpers/GlobalHotkeyManager.cs b/Ink Canvas/Helpers/GlobalHotkeyManager.cs index e4bc400e1..b9f3eff40 100644 --- a/Ink Canvas/Helpers/GlobalHotkeyManager.cs +++ b/Ink Canvas/Helpers/GlobalHotkeyManager.cs @@ -1,8 +1,6 @@ -using Newtonsoft.Json; -using NHotkey.Wpf; +using NHotkey.Wpf; using System; using System.Collections.Generic; -using System.IO; using System.Reflection; using System.Text; using System.Windows; @@ -34,8 +32,19 @@ public class GlobalHotkeyManager : IDisposable private bool _isMouseOverWindow = false; private System.Windows.Threading.DispatcherTimer _mousePositionTimer; - // 配置文件路径 - private static readonly string HotkeyConfigFile = Path.Combine(App.RootPath, "Configs", "HotkeyConfig.json"); + // HotkeyConfig.json 读写与加载编排已提取至 Services.Shell.HotkeyService。 + // 以下委托由 HotkeyService 构造时接线;原位 public 方法为转发壳,对外签名与语义不变。 + internal Action LoadHotkeysHandler { get; set; } + internal Action SaveHotkeysHandler { get; set; } + internal Func> ConfigHotkeysProvider { get; set; } + internal Action RegisterDefaultsHandler { get; set; } + + // 供 HotkeyService 读写(原私有字段,等价改写为 internal 访问器) + internal bool HotkeysShouldBeRegistered + { + get => _hotkeysShouldBeRegistered; + set => _hotkeysShouldBeRegistered = value; + } #endregion #region Constructor @@ -48,8 +57,8 @@ public GlobalHotkeyManager(MainWindow mainWindow) // 初始化多屏幕支持 InitializeMultiScreenSupport(); - // 启动时确保配置文件存在 - EnsureConfigFileExists(); + // 启动时确保配置文件存在的职责随配置层搬入 HotkeyService 构造函数, + // 由主窗口在创建本管理器后立即创建服务(同一调用栈,时序不变)。 } #endregion @@ -88,7 +97,7 @@ public bool RegisterHotkey(string hotkeyName, Key key, ModifierKeys modifiers, A } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Hotkey] 注册前清理全局热键 {hotkeyName} 的旧注册失败,将直接覆盖: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -154,7 +163,7 @@ public bool RegisterPluginHotkey(string hotkeyName, Key key, ModifierKeys modifi } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Hotkey] 注册插件热键前清理同名全局热键 {hotkeyName} 失败,将直接覆盖: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } _registeredHotkeys.Remove(hotkeyName); @@ -300,167 +309,39 @@ public List GetRegisteredHotkeys() /// /// 获取配置文件中的快捷键信息(不注册,仅用于显示) + /// 实现已提取至 Services.Shell.HotkeyService,本方法为转发壳。 /// /// 配置文件中的快捷键列表 public List GetHotkeysFromConfigFile() { - try - { - if (!File.Exists(HotkeyConfigFile)) - { - return new List(); - } - - // 读取配置文件内容 - string jsonContent = File.ReadAllText(HotkeyConfigFile, 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 // 不设置动作,仅用于显示 - }); - } - - return hotkeyList; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"从配置文件读取快捷键信息时出错: {ex.Message}", LogHelper.LogType.Error); - return new List(); - } + return ConfigHotkeysProvider?.Invoke() ?? new List(); } /// /// 注册默认快捷键集合 + /// 实现已提取至 Services.Shell.HotkeyService,本方法为转发壳。 /// 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.SwitchToSelectFromHotkey()); - RegisterHotkey("DrawTool", Key.D, ModifierKeys.Alt, () => _mainWindow.PenIcon_Click(null, null)); - RegisterHotkey("EraserTool", Key.E, ModifierKeys.Alt, () => _mainWindow.SwitchToEraserFromHotkey()); - RegisterHotkey("BlackboardTool", Key.B, ModifierKeys.Alt, () => _mainWindow.ImageBlackboard_MouseUp(null, null)); - RegisterHotkey("QuitDrawTool", Key.Q, ModifierKeys.Alt, () => _mainWindow.KeyChangeToQuitDrawTool(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.DrawLineFromHotkey()); - RegisterHotkey("Screenshot", Key.C, ModifierKeys.Alt, () => _mainWindow.SaveScreenShotToDesktop()); - RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, () => _mainWindow.OpenQuickDrawFromHotkey()); - RegisterHotkey("Hide", Key.V, ModifierKeys.Alt, () => _mainWindow.SymbolIconEmoji_MouseUp(null, null)); - - // 退出快捷键 - RegisterHotkey("Exit", Key.Escape, ModifierKeys.None, () => _mainWindow.KeyExit(null, null)); - - // 已注册默认全局快捷键集合 - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"注册默认快捷键时出错: {ex.Message}", LogHelper.LogType.Error); - } + RegisterDefaultsHandler?.Invoke(); } /// /// 从配置文件加载快捷键 + /// 实现已提取至 Services.Shell.HotkeyService,本方法为转发壳。 /// public void LoadHotkeysFromSettings() { - try - { - // 开始从配置文件加载快捷键设置 - - // 检查是否应该注册快捷键 - if (!_hotkeysShouldBeRegistered) - { - // 当前状态不允许注册快捷键,跳过加载 - return; - } - - // 如果配置文件不存在,先创建默认配置文件 - if (!File.Exists(HotkeyConfigFile)) - { - LogHelper.WriteLogToFile($"快捷键配置文件不存在: {HotkeyConfigFile}", LogHelper.LogType.Warning); - CreateDefaultConfigFile(); - RegisterDefaultHotkeys(); - _hotkeysShouldBeRegistered = true; - return; - } - - // 尝试从配置文件加载 - if (LoadHotkeysFromConfigFile()) - { - // 成功从配置文件加载快捷键设置 - _hotkeysShouldBeRegistered = true; - } - else - { - LogHelper.WriteLogToFile("配置文件存在但加载失败,回退到默认快捷键", LogHelper.LogType.Warning); - RegisterDefaultHotkeys(); - _hotkeysShouldBeRegistered = true; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"从设置加载快捷键时出错: {ex.Message}", LogHelper.LogType.Error); - // 出错时不自动使用默认快捷键,保持当前状态 - } + LoadHotkeysHandler?.Invoke(); } /// /// 保存快捷键配置到设置 + /// 实现已提取至 Services.Shell.HotkeyService,本方法为转发壳。 /// public void SaveHotkeysToSettings() { - try - { - - if (SaveHotkeysToConfigFile()) - { - } - else - { - LogHelper.WriteLogToFile("保存快捷键配置失败", LogHelper.LogType.Error); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存快捷键配置时出错: {ex.Message}", LogHelper.LogType.Error); - } + SaveHotkeysHandler?.Invoke(); } /// @@ -1114,304 +995,6 @@ private bool ShouldEnableHotkeysBasedOnContext() } } - /// - /// 切换到指定笔类型 - /// - /// 笔类型索引 - private void SwitchToPenType(int penTypeIndex) - { - try - { - var switchMethod = _mainWindow.GetType().GetMethod( - penTypeIndex == 2 ? "SwitchToLaserPen" : (penTypeIndex == 1 ? "SwitchToHighlighterPen" : "SwitchToDefaultPen"), - BindingFlags.NonPublic | BindingFlags.Instance); - - if (switchMethod != null) - { - switchMethod.Invoke(_mainWindow, new object[] { null, null }); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"切换到笔类型{penTypeIndex}时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 确保配置文件存在,如果不存在则创建 - /// - private void EnsureConfigFileExists() - { - try - { - // 如果配置文件不存在,创建默认配置文件 - if (!File.Exists(HotkeyConfigFile)) - { - CreateDefaultConfigFile(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"确保快捷键配置文件存在时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 创建默认的快捷键配置文件 - /// - private void CreateDefaultConfigFile() - { - try - { - // 确保配置目录存在 - string configDir = Path.GetDirectoryName(HotkeyConfigFile); - if (!Directory.Exists(configDir)) - { - Directory.CreateDirectory(configDir); - } - - // 创建默认配置对象 - var config = new HotkeyConfig - { - Version = "1.0", - LastModified = DateTime.Now, - Hotkeys = new List() - }; - - // 添加默认快捷键配置 - config.Hotkeys.AddRange(new[] - { - new HotkeyConfigItem { Name = "Undo", Key = Key.Z, Modifiers = ModifierKeys.Control }, - new HotkeyConfigItem { Name = "Redo", Key = Key.Y, Modifiers = ModifierKeys.Control }, - new HotkeyConfigItem { Name = "Clear", Key = Key.E, Modifiers = ModifierKeys.Control }, - new HotkeyConfigItem { Name = "Paste", Key = Key.V, Modifiers = ModifierKeys.Control }, - new HotkeyConfigItem { Name = "SelectTool", Key = Key.S, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "DrawTool", Key = Key.D, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "EraserTool", Key = Key.E, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "BlackboardTool", Key = Key.B, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "QuitDrawTool", Key = Key.Q, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "Pen1", Key = Key.D1, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "Pen2", Key = Key.D2, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "Pen3", Key = Key.D3, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "Pen4", Key = Key.D4, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "Pen5", Key = Key.D5, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "DrawLine", Key = Key.L, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "Screenshot", Key = Key.C, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "QuickDraw", Key = Key.K, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "Hide", Key = Key.V, Modifiers = ModifierKeys.Alt }, - new HotkeyConfigItem { Name = "Exit", Key = Key.Escape, Modifiers = ModifierKeys.None } - }); - - // 序列化为JSON - var settings = new JsonSerializerSettings - { - Formatting = Formatting.Indented - }; - - string jsonContent = JsonConvert.SerializeObject(config, settings); - - // 写入配置文件 - File.WriteAllText(HotkeyConfigFile, jsonContent, Encoding.UTF8); - - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建默认快捷键配置文件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 从配置文件加载快捷键设置 - /// - /// 是否加载成功 - private bool LoadHotkeysFromConfigFile() - { - try - { - if (!File.Exists(HotkeyConfigFile)) - { - LogHelper.WriteLogToFile($"快捷键配置文件不存在: {HotkeyConfigFile}", LogHelper.LogType.Warning); - return false; - } - - // 读取配置文件内容 - string jsonContent = File.ReadAllText(HotkeyConfigFile, 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); - } - } - - // 旧版 HotkeyConfig.json 无「快抽」项时补注册默认组合,避免升级后无快捷键 - if (successCount > 0 && !IsHotkeyRegistered("QuickDraw")) - { - var quickDrawAction = GetActionByName("QuickDraw"); - if (quickDrawAction != null && RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, quickDrawAction)) - successCount++; - } - - if (successCount > 0) - { - _hotkeysShouldBeRegistered = true; - } - 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, Encoding.UTF8); - - 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.SwitchToSelectFromHotkey(); - case "DrawTool": - return () => _mainWindow.PenIcon_Click(null, null); - case "EraserTool": - return () => _mainWindow.SwitchToEraserFromHotkey(); - case "BlackboardTool": - return () => _mainWindow.ImageBlackboard_MouseUp(null, null); - case "QuitDrawTool": - return () => _mainWindow.KeyChangeToQuitDrawTool(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.DrawLineFromHotkey(); - case "Screenshot": - return () => _mainWindow.SaveScreenShotToDesktop(); - case "QuickDraw": - return () => _mainWindow.OpenQuickDrawFromHotkey(); - 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; - } - } - /// /// 检查当前是否处于鼠标模式(选择模式) /// @@ -1553,26 +1136,6 @@ public override string ToString() 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 } } diff --git a/Ink Canvas/Helpers/HardwareAcceleratedInkProcessor.cs b/Ink Canvas/Helpers/HardwareAcceleratedInkProcessor.cs index 8b22005c7..ae8a4a14c 100644 --- a/Ink Canvas/Helpers/HardwareAcceleratedInkProcessor.cs +++ b/Ink Canvas/Helpers/HardwareAcceleratedInkProcessor.cs @@ -208,7 +208,7 @@ public void Dispose() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Ink] 释放 GPU 墨迹渲染资源失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } } diff --git a/Ink Canvas/Helpers/AdvancedBezierSmoothing.cs b/Ink Canvas/Helpers/Ink/AdvancedBezierSmoothing.cs similarity index 100% rename from Ink Canvas/Helpers/AdvancedBezierSmoothing.cs rename to Ink Canvas/Helpers/Ink/AdvancedBezierSmoothing.cs diff --git a/Ink Canvas/Helpers/PerformanceMonitorHelper.cs b/Ink Canvas/Helpers/Ink/PerformanceMonitorHelper.cs similarity index 100% rename from Ink Canvas/Helpers/PerformanceMonitorHelper.cs rename to Ink Canvas/Helpers/Ink/PerformanceMonitorHelper.cs diff --git a/Ink Canvas/Helpers/PerformanceTransparentWin.cs b/Ink Canvas/Helpers/Ink/PerformanceTransparentWin.cs similarity index 100% rename from Ink Canvas/Helpers/PerformanceTransparentWin.cs rename to Ink Canvas/Helpers/Ink/PerformanceTransparentWin.cs diff --git a/Ink Canvas/Helpers/RealtimeInkFrameScheduler.cs b/Ink Canvas/Helpers/Ink/RealtimeInkFrameScheduler.cs similarity index 100% rename from Ink Canvas/Helpers/RealtimeInkFrameScheduler.cs rename to Ink Canvas/Helpers/Ink/RealtimeInkFrameScheduler.cs diff --git a/Ink Canvas/Helpers/RealtimeInkPerformanceMonitor.cs b/Ink Canvas/Helpers/Ink/RealtimeInkPerformanceMonitor.cs similarity index 100% rename from Ink Canvas/Helpers/RealtimeInkPerformanceMonitor.cs rename to Ink Canvas/Helpers/Ink/RealtimeInkPerformanceMonitor.cs diff --git a/Ink Canvas/Helpers/LiquidGlassMagnifier.cs b/Ink Canvas/Helpers/LiquidGlassMagnifier.cs index c015fb29f..75458367f 100644 --- a/Ink Canvas/Helpers/LiquidGlassMagnifier.cs +++ b/Ink Canvas/Helpers/LiquidGlassMagnifier.cs @@ -177,17 +177,13 @@ internal static void Shutdown() { if (!host.HasExited) { - try { host.StandardInput.Close(); } catch (Exception ex) { LogHelper.WriteLogToFile($"[LiquidGlass] 关闭放大镜子进程标准输入失败: {ex.Message}", LogHelper.LogType.Info); } - try { host.Kill(); } catch (Exception ex) { LogHelper.WriteLogToFile($"[LiquidGlass] 强制结束放大镜子进程失败: {ex.Message}", LogHelper.LogType.Info); } + try { host.StandardInput.Close(); } catch (Exception ex) { LogService.LogException(ex); } + try { host.Kill(); } catch (Exception ex) { LogService.LogException(ex); } host.WaitForExit(1000); } } - catch (Exception ex) - { - /* 退出期 */ - LogHelper.WriteLogToFile($"[LiquidGlass] 关闭放大镜子进程(标准输入/结束/等待退出)失败: {ex.Message}", LogHelper.LogType.Info); - } - try { host.Dispose(); } catch (Exception ex) { LogHelper.WriteLogToFile($"[LiquidGlass] 释放放大镜子进程对象失败: {ex.Message}", LogHelper.LogType.Info); } + catch (Exception ex) { /* 退出期 */ LogService.LogException(ex); } + try { host.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } LogHelper.WriteLogToFile("[LiquidGlass] 放大镜辅助进程已关闭", LogHelper.LogType.Info); } @@ -247,7 +243,7 @@ private static bool EnsureHost() { if (!_available) { - try { proc.Kill(); } catch (Exception ex) { LogHelper.WriteLogToFile($"[LiquidGlass] 启动后判定放大镜已不可用,结束刚拉起的子进程失败: {ex.Message}", LogHelper.LogType.Info); } + try { proc.Kill(); } catch (Exception ex) { LogService.LogException(ex); } return false; } _host = proc; diff --git a/Ink Canvas/Helpers/LocalizationHelper.cs b/Ink Canvas/Helpers/LocalizationHelper.cs index 2b3fbf556..559f1c44c 100644 --- a/Ink Canvas/Helpers/LocalizationHelper.cs +++ b/Ink Canvas/Helpers/LocalizationHelper.cs @@ -61,7 +61,7 @@ internal static void SyncCommonResources() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 应用 ToggleSwitch 本地化失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -106,7 +106,7 @@ private static void PatchModernStrings(string onText, string offText) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 反射替换 iNKORE Strings 的 ResourceManager 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -161,7 +161,7 @@ private static void UpdateAllToggleSwitches(string onText, string offText) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 遍历窗口更新 ToggleSwitch 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -185,7 +185,7 @@ private static void UpdateToggleSwitchesInLogicalTree(DependencyObject parent, s } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 遍历逻辑树更新 ToggleSwitch 失败 (node={parent?.GetType().Name}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -211,7 +211,7 @@ private static void UpdateSingleToggleSwitch(ToggleSwitch ts, string onText, str } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 更新单个 ToggleSwitch 文本失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -228,7 +228,7 @@ internal static void BindToggleSwitchesInWindow(System.Windows.Window window) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 为设置窗口绑定 ToggleSwitch 文本失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -278,7 +278,7 @@ private static CultureInfo CreateCustomCulture(string name) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 按名称创建自定义 Culture 失败 (name={name}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } try @@ -301,7 +301,7 @@ private static CultureInfo CreateCustomCulture(string name) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 反射克隆父 Culture 失败,将回退 zh-CN (name={name}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return CultureInfo.GetCultureInfo("zh-CN"); @@ -409,7 +409,7 @@ private static Dictionary LoadEmbeddedResource(Assembly asm, str } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 加载内嵌 .resources 失败 ({resName}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } if (result.Count > 0) @@ -429,7 +429,7 @@ private static Dictionary LoadEmbeddedResource(Assembly asm, str } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 加载程序集内嵌 .resx 失败 ({resxName}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } if (result.Count > 0) @@ -452,7 +452,7 @@ private static Dictionary LoadEmbeddedResource(Assembly asm, str } catch (Exception ex) { - LogHelper.WriteLogToFile($"[I18n] 从程序目录加载 .resx 失败 ({className}.{cultureName}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } _embeddedCache[cacheKey] = result; diff --git a/Ink Canvas/Helpers/LogService.cs b/Ink Canvas/Helpers/LogService.cs new file mode 100644 index 000000000..84e316cb4 --- /dev/null +++ b/Ink Canvas/Helpers/LogService.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Ink_Canvas.Helpers +{ + /// + /// 重构期吞异常治理用最小日志设施。 + /// 写入 %AppData%/InkCanvasForClass/logs/refactor.log,单文件上限 1MB,滚动保留 3 个。 + /// + internal static class LogService + { + private const long MaxFileSizeBytes = 1024 * 1024; + private const int RetainedFiles = 3; + private static readonly object WriteLock = new object(); + + private static string LogDirectory => + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "InkCanvasForClass", "logs"); + + private static string LogFile => Path.Combine(LogDirectory, "refactor.log"); + + public static void LogException(Exception ex, [CallerMemberName] string member = "") + { + try + { + lock (WriteLock) + { + Directory.CreateDirectory(LogDirectory); + RollIfNeeded(); + File.AppendAllText(LogFile, + $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{member}] {ex}{Environment.NewLine}"); + } + } + catch + { + // 日志设施自身失败时静默放弃,绝不影响主流程。 + } + } + + private static void RollIfNeeded() + { + if (!File.Exists(LogFile) || new FileInfo(LogFile).Length < MaxFileSizeBytes) return; + for (var i = RetainedFiles - 1; i >= 1; i--) + { + var older = $"{LogFile}.{i}"; + var newer = $"{LogFile}.{i + 1}"; + if (File.Exists(newer)) File.Delete(newer); + if (File.Exists(older)) File.Move(older, newer); + } + File.Move(LogFile, $"{LogFile}.1"); + } + } +} diff --git a/Ink Canvas/Helpers/MemoryBreakdownHelper.cs b/Ink Canvas/Helpers/MemoryBreakdownHelper.cs index eed574d8d..060dd87e8 100644 --- a/Ink Canvas/Helpers/MemoryBreakdownHelper.cs +++ b/Ink Canvas/Helpers/MemoryBreakdownHelper.cs @@ -441,7 +441,7 @@ private static int ReadInstanceCollectionCount(object instance, string fieldName } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Memory] 反射读取实例集合计数失败 (field={fieldName}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return 0; } @@ -461,7 +461,7 @@ private static T ReadInstanceField(object instance, string fieldName) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Memory] 反射读取实例字段失败 (field={fieldName}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return default; } @@ -514,7 +514,7 @@ private static long ReadTimeMachineCount(Ink_Canvas.MainWindow mw, out string be } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Memory] 读取 TimeMachine 字段失败 (field={f.Name}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } return best; @@ -557,7 +557,7 @@ private static long ReadInkSmoothingSampleCount(Ink_Canvas.MainWindow mw) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Memory] 读取墨迹平滑采样数失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return 0; } @@ -585,7 +585,7 @@ private static void AppendAssembliesSection(StringBuilder sb) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Memory] 读取程序集大小失败 (asm={asm.GetName().Name}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } sb2.AppendLine($" {asm.GetName().Name,-50} v{asm.GetName().Version?.ToString() ?? "?"} ({bytes / 1024} KB)"); @@ -655,10 +655,7 @@ private static void CountStrokeValue(object value, ref long strokeCount, ref lon foreach (System.Windows.Ink.Stroke stroke in collection) { strokeCount++; - try { pointCount += stroke.StylusPoints.Count; } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Memory] 读取 Stroke 采样点数失败: {ex.Message}", LogHelper.LogType.Info); - } + try { pointCount += stroke.StylusPoints.Count; } catch (Exception ex) { LogService.LogException(ex); } } return; } @@ -666,10 +663,7 @@ private static void CountStrokeValue(object value, ref long strokeCount, ref lon if (value is System.Windows.Ink.Stroke strokeValue) { strokeCount++; - try { pointCount += strokeValue.StylusPoints.Count; } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Memory] 读取 Stroke 采样点数失败: {ex.Message}", LogHelper.LogType.Info); - } + try { pointCount += strokeValue.StylusPoints.Count; } catch (Exception ex) { LogService.LogException(ex); } return; } @@ -766,7 +760,7 @@ private static int ReadSettingsPageCacheCount(Ink_Canvas.Windows.SettingsViews.S } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Memory] 读取设置窗口页面缓存数量失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return 0; } @@ -781,7 +775,7 @@ private static long ReadStaticField(Type type, string fieldName) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Memory] 读取静态集合字段失败 (type={type?.Name}, field={fieldName}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return 0; } diff --git a/Ink Canvas/Helpers/MessageBoxHelper.cs b/Ink Canvas/Helpers/MessageBoxHelper.cs index e6d48aa3b..e89c9347a 100644 --- a/Ink Canvas/Helpers/MessageBoxHelper.cs +++ b/Ink Canvas/Helpers/MessageBoxHelper.cs @@ -1,3 +1,4 @@ +using iNKORE.UI.WPF.Modern; using iNKORE.UI.WPF.Modern.Common; using iNKORE.UI.WPF.Modern.Common.IconKeys; using System; @@ -94,12 +95,13 @@ public static MessageBoxResult Show( } var owner = GetDefaultOwner(context); - if (owner != null && owner.IsLoaded && owner.IsVisible) - { - return MessageBox.Show(owner, messageBoxText, caption, button, icon, defaultResult); - } + if (owner != null && (!owner.IsLoaded || !owner.IsVisible)) owner = null; - return MessageBox.Show(messageBoxText, caption, button, icon, defaultResult); + // 自行实例化(镜像库内静态 Show 的初始化逻辑),以便锁定弹窗主题: + // 库的静态 Show 拿不到实例,新窗口只能按应用级 ApplicationTheme 渲染, + // 而白板逻辑会将其强制设为 Dark 且不恢复,导致浅色窗口中弹出深色对话框。 + var box = CreateThemedMessageBox(owner, messageBoxText, caption, button, icon); + return box.ShowDialog(); } public static MessageBoxResult Show( @@ -145,12 +147,12 @@ public static async Task ShowAsync( } var owner = GetDefaultOwner(context); - if (owner != null && owner.IsLoaded && owner.IsVisible) - { - return await MessageBox.ShowAsync(owner, messageBoxText, caption, button, icon, defaultResult); - } + if (owner != null && (!owner.IsLoaded || !owner.IsVisible)) owner = null; - return await MessageBox.ShowAsync(messageBoxText, caption, button, icon, defaultResult); + // 与同步 Show 一致:库的 ShowAsync 内部也是 Dispatcher.Invoke(同步 Show), + // 模态语义不变,只是改为自行实例化以锁定主题。 + var box = CreateThemedMessageBox(owner, messageBoxText, caption, button, icon); + return box.ShowDialog(); } public static Task ShowAsync( @@ -280,10 +282,7 @@ public static MessageBox ShowAtNonBlocking( }; autoCloseTimer.Tick += (s, e) => { - try { box.Close(MessageBoxResult.None); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[UI] 自动关闭消息框失败: {ex.Message}", LogHelper.LogType.Info); - } + try { box.Close(MessageBoxResult.None); } catch (Exception ex) { LogService.LogException(ex); } }; autoCloseTimer.Start(); } @@ -315,6 +314,58 @@ void Complete() return box; } + /// + /// 构建 MessageBox 实例(镜像库内静态 Show 的初始化逻辑),并将弹窗主题锁定为 + /// Owner 窗口的实际主题;Owner 缺失或其主题为 Default 时回落到用户设置的有效主题。 + /// + private static MessageBox CreateThemedMessageBox( + Window owner, + string messageBoxText, string caption, + MessageBoxButton button, MessageBoxImage icon) + { + var box = new MessageBox + { + Owner = owner, + Content = messageBoxText, + Caption = caption ?? string.Empty, + MessageBoxButtons = button, + IconSource = CreateIconSource(icon), + WindowStartupLocation = owner != null ? WindowStartupLocation.CenterOwner : WindowStartupLocation.CenterScreen, + }; + + if (MessageBox.MakeSound) + { + box.SystemSoundOnLoaded = CreateSystemSound(icon); + } + + ApplyEffectiveTheme(box, owner); + return box; + } + + /// + /// 将弹窗主题锁定为 Owner 窗口的实际主题(窗口级 RequestedTheme 解析结果), + /// 使其不随应用级 ApplicationTheme 漂移(白板进出会将其强制设为 Dark 且不恢复)。 + /// + private static void ApplyEffectiveTheme(MessageBox box, Window owner) + { + try + { + var theme = owner != null ? ThemeManager.GetActualTheme(owner) : ElementTheme.Default; + if (theme == ElementTheme.Default) + { + theme = ThemeHelper.GetEffectiveTheme(Windows.SettingsViews.Helpers.SettingsManager.Settings); + } + if (theme != ElementTheme.Default) + { + ThemeManager.SetRequestedTheme(box, theme); + } + } + catch (Exception ex) + { + LogService.LogException(ex); + } + } + /// /// 构建以 Manual 方式定位到屏幕指定位置(DIP)的 MessageBox 实例。 /// 镜像 Owner 的置顶状态(置顶窗口的弹窗需同置顶才能保持可见), @@ -351,6 +402,7 @@ private static MessageBox CreatePositionedMessageBox( box.SystemSoundOnLoaded = CreateSystemSound(icon); } + ApplyEffectiveTheme(box, owner); box.ContentRendered += (s, e) => ClampToWorkArea(box); return box; } diff --git a/Ink Canvas/Helpers/MicroBinClient.cs b/Ink Canvas/Helpers/MicroBinClient.cs index a0ee83f8d..3ea5c7efa 100644 --- a/Ink Canvas/Helpers/MicroBinClient.cs +++ b/Ink Canvas/Helpers/MicroBinClient.cs @@ -168,8 +168,7 @@ private static string ParsePasteUrl(string responseBody, string serverUrl) } catch (Exception ex) { - // 非 JSON 响应属预期分支(纯 URL / HTML 页面),仅留痕便于排查上传异常 - LogHelper.WriteLogToFile($"[Upload] 上传响应不是 JSON,按纯文本/HTML 继续解析: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } // 纯 URL diff --git a/Ink Canvas/Helpers/NameRosterManager.cs b/Ink Canvas/Helpers/NameRosterManager.cs index 59e455a7c..1530d9c5f 100644 --- a/Ink Canvas/Helpers/NameRosterManager.cs +++ b/Ink Canvas/Helpers/NameRosterManager.cs @@ -38,22 +38,8 @@ public static (string namesContent, string replaceContent) ReadCurrentFiles() { string names = ""; string replace = ""; - try - { - if (File.Exists(NamesFilePath)) names = File.ReadAllText(NamesFilePath); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Roster] 读取当前名单文件 Names.txt 失败,保存为方案时该部分将为空: {ex.Message}", LogHelper.LogType.Info); - } - try - { - if (File.Exists(ReplaceFilePath)) replace = File.ReadAllText(ReplaceFilePath); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Roster] 读取当前替换表文件 Replace.txt 失败,保存为方案时该部分将为空: {ex.Message}", LogHelper.LogType.Info); - } + try { if (File.Exists(NamesFilePath)) names = File.ReadAllText(NamesFilePath); } catch (Exception ex) { LogService.LogException(ex); } + try { if (File.Exists(ReplaceFilePath)) replace = File.ReadAllText(ReplaceFilePath); } catch (Exception ex) { LogService.LogException(ex); } return (names, replace); } diff --git a/Ink Canvas/Helpers/ComPPTLinkManager.cs b/Ink Canvas/Helpers/Ppt/ComPPTLinkManager.cs similarity index 100% rename from Ink Canvas/Helpers/ComPPTLinkManager.cs rename to Ink Canvas/Helpers/Ppt/ComPPTLinkManager.cs diff --git a/Ink Canvas/Helpers/PPTPageKeyHook.cs b/Ink Canvas/Helpers/Ppt/PPTPageKeyHook.cs similarity index 100% rename from Ink Canvas/Helpers/PPTPageKeyHook.cs rename to Ink Canvas/Helpers/Ppt/PPTPageKeyHook.cs diff --git a/Ink Canvas/Helpers/PPTROTConnectionHelper.cs b/Ink Canvas/Helpers/Ppt/PPTROTConnectionHelper.cs similarity index 97% rename from Ink Canvas/Helpers/PPTROTConnectionHelper.cs rename to Ink Canvas/Helpers/Ppt/PPTROTConnectionHelper.cs index ba4eba37e..17264bdc8 100644 --- a/Ink Canvas/Helpers/PPTROTConnectionHelper.cs +++ b/Ink Canvas/Helpers/Ppt/PPTROTConnectionHelper.cs @@ -116,14 +116,15 @@ public static Microsoft.Office.Interop.PowerPoint.Application TryConnectViaROT(b { var _ = pptApp.Name; } - catch (COMException) + catch (COMException ex) { + LogService.LogException(ex); } return pptApp; } } - catch (COMException) { } - catch (InvalidCastException) { } + catch (COMException ex) { LogService.LogException(ex); } + catch (InvalidCastException ex) { LogService.LogException(ex); } if (isSupportWPS) { @@ -136,14 +137,15 @@ public static Microsoft.Office.Interop.PowerPoint.Application TryConnectViaROT(b { var _ = wpsApp.Name; } - catch (COMException) + catch (COMException ex) { + LogService.LogException(ex); } return wpsApp; } } - catch (COMException) { } - catch (InvalidCastException) { } + catch (COMException ex) { LogService.LogException(ex); } + catch (InvalidCastException ex) { LogService.LogException(ex); } } return null; diff --git a/Ink Canvas/Helpers/PPTUIManager.cs b/Ink Canvas/Helpers/Ppt/PPTUIManager.cs similarity index 100% rename from Ink Canvas/Helpers/PPTUIManager.cs rename to Ink Canvas/Helpers/Ppt/PPTUIManager.cs diff --git a/Ink Canvas/Helpers/ROTPPTManager.cs b/Ink Canvas/Helpers/Ppt/ROTPPTManager.cs similarity index 98% rename from Ink Canvas/Helpers/ROTPPTManager.cs rename to Ink Canvas/Helpers/Ppt/ROTPPTManager.cs index 17e3c0eda..0a4a2a454 100644 --- a/Ink Canvas/Helpers/ROTPPTManager.cs +++ b/Ink Canvas/Helpers/Ppt/ROTPPTManager.cs @@ -764,7 +764,7 @@ private void TryRaiseSlideShowBeginOnConnect() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 连接后补发 SlideShowBegin 时访问 SlideShowWindows 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } finally { @@ -808,7 +808,7 @@ private void UnbindEvents() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 解绑 COM 事件(SlideShowNextSlide/Begin/End/PresentationBeforeClose)失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } _bindingEvents = false; @@ -816,7 +816,7 @@ private void UnbindEvents() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] UnbindEvents 重置事件绑定标记失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -998,12 +998,13 @@ private void SafeFinalReleaseComObject(object comObject, string objectName) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 终态释放回退为循环 ReleaseComObject 失败({objectName}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } } - catch (InvalidComObjectException) + catch (InvalidComObjectException ex) { + LogService.LogException(ex); } catch (COMException comEx) when (IsIgnorableDisconnectComException(comEx)) { @@ -1235,7 +1236,7 @@ private void OnPresentationOpen(Presentation pres) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] PresentationOpen 时读取 ActivePresentation 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -1278,7 +1279,7 @@ private void OnPresentationBeforeClose(Presentation pres, ref bool cancel) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] PresentationBeforeClose 时解绑 COM 事件失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } _bindingEvents = false; @@ -2436,7 +2437,7 @@ private bool HasWpsWindowInTaskbar() } catch (Exception ex) { - LogCallbackException("EnumWindows 回调中读取单个窗口信息失败", ex); + LogService.LogException(ex); } return true; }, IntPtr.Zero); @@ -2575,23 +2576,19 @@ private void SafeTerminateWpsProcess() // 确保清理状态 if (CurrentSlide != null && Marshal.IsComObject(CurrentSlide)) { - try { Marshal.ReleaseComObject(CurrentSlide); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT] 结束WPS进程后释放 CurrentSlide 失败: {ex.Message}", LogHelper.LogType.Info); } + try { Marshal.ReleaseComObject(CurrentSlide); } catch (Exception ex) { LogService.LogException(ex); } } if (CurrentSlides != null && Marshal.IsComObject(CurrentSlides)) { - try { Marshal.ReleaseComObject(CurrentSlides); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT] 结束WPS进程后释放 CurrentSlides 失败: {ex.Message}", LogHelper.LogType.Info); } + try { Marshal.ReleaseComObject(CurrentSlides); } catch (Exception ex) { LogService.LogException(ex); } } if (CurrentPresentation != null && Marshal.IsComObject(CurrentPresentation)) { - try { Marshal.ReleaseComObject(CurrentPresentation); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT] 结束WPS进程后释放 CurrentPresentation 失败: {ex.Message}", LogHelper.LogType.Info); } + try { Marshal.ReleaseComObject(CurrentPresentation); } catch (Exception ex) { LogService.LogException(ex); } } if (PPTApplication != null && Marshal.IsComObject(PPTApplication)) { - try { Marshal.ReleaseComObject(PPTApplication); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT] 结束WPS进程后释放 PPTApplication 失败: {ex.Message}", LogHelper.LogType.Info); } + try { Marshal.ReleaseComObject(PPTApplication); } catch (Exception ex) { LogService.LogException(ex); } } CurrentSlide = null; @@ -2754,7 +2751,7 @@ private WpsWindowInfo GetWindowInfo(IntPtr hWnd) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] GetWindowInfo 取进程名失败(pid={processId},进程可能已退出): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return windowInfo; @@ -2972,7 +2969,7 @@ public IntPtr GetPPTHwnd() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] GetPPTHwnd 备用路径读取 pres.FullName/PPTApplication.Name 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -3009,13 +3006,13 @@ private IntPtr GetPPTHwndFromSlideShowWindow(object pptSlideShowWindowObj) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] GetPPTHwndFromSlideShowWindow 走 dynamic 读 HWND 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] GetPPTHwndFromSlideShowWindow 强类型转换或读取 HWND 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return hwnd; diff --git a/Ink Canvas/Helpers/ProcessProtectionManager.cs b/Ink Canvas/Helpers/ProcessProtectionManager.cs index bdfaf7992..ceb6a3ad4 100644 --- a/Ink Canvas/Helpers/ProcessProtectionManager.cs +++ b/Ink Canvas/Helpers/ProcessProtectionManager.cs @@ -72,7 +72,10 @@ public static void SetEnabled(bool enabled) { LogHelper.WriteLogToFile($"ProcessProtectionManager.SetEnabled 后台执行失败: {ex.Message}", LogHelper.LogType.Warning); } - catch { } + catch (Exception ex2) + { + LogService.LogException(ex2); + } } }); } @@ -100,10 +103,7 @@ public static void ReleaseLocksForPath(string targetPath) if (_lockedDirs.TryGetValue(d, out var handle)) { _lockedDirs.Remove(d); - try { handle.Dispose(); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[ProcGuard] 释放目录锁句柄失败 (path={d}): {ex.Message}", LogHelper.LogType.Info); - } + try { handle.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } } } @@ -117,10 +117,7 @@ public static void ReleaseLocksForPath(string targetPath) if (_lockedFiles.TryGetValue(fp, out var fs)) { _lockedFiles.Remove(fp); - try { fs.Dispose(); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[ProcGuard] 释放文件锁句柄失败 (path={fp}): {ex.Message}", LogHelper.LogType.Info); - } + try { fs.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } } } } @@ -228,7 +225,7 @@ public static void WithWriteAccess(string targetPath, Action action) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 写入门控结束后按路径链恢复文件锁失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } return; @@ -295,7 +292,7 @@ public static void WithWriteAccess(string targetPath, Action action) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 写入门控结束后恢复文件锁失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } Interlocked.Exchange(ref _writeGate, 0); @@ -380,9 +377,7 @@ private static void Enable(bool rescanRoot, IEnumerable rescanDirs) } catch (Exception ex) { - LogHelper.WriteLogToFile( - $"[ProcGuard] 建立文件/目录锁失败 (rescanRoot={rescanRoot}): {ex.Message}", - LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -446,7 +441,7 @@ private static void LockDirectoryRecursive(string root) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 递归锁定目录失败 (root={root}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -483,7 +478,7 @@ private static void LockFilesRecursive(string root) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 递归锁定文件失败 (root={root}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -504,7 +499,7 @@ private static void LockFile(string filePath) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 锁定文件失败(可能被占用): {filePath} - {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } } @@ -529,7 +524,7 @@ private static void LockDirectory(string dirPath) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 锁定目录失败: {dirPath} - {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } } @@ -576,7 +571,7 @@ private static List GetDirChainToRoot(string path) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 计算到应用根目录的路径链失败 (path={path}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return list; } @@ -604,7 +599,7 @@ private static bool IsExcludedPath(string path) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 判断路径是否属于排除目录失败 (path={path}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return false; } diff --git a/Ink Canvas/Helpers/FeedbackSanitizer.cs b/Ink Canvas/Helpers/Security/FeedbackSanitizer.cs similarity index 100% rename from Ink Canvas/Helpers/FeedbackSanitizer.cs rename to Ink Canvas/Helpers/Security/FeedbackSanitizer.cs diff --git a/Ink Canvas/Helpers/SafeZipExtractor.cs b/Ink Canvas/Helpers/Security/SafeZipExtractor.cs similarity index 100% rename from Ink Canvas/Helpers/SafeZipExtractor.cs rename to Ink Canvas/Helpers/Security/SafeZipExtractor.cs diff --git a/Ink Canvas/Helpers/SecurityManager.cs b/Ink Canvas/Helpers/Security/SecurityManager.cs similarity index 100% rename from Ink Canvas/Helpers/SecurityManager.cs rename to Ink Canvas/Helpers/Security/SecurityManager.cs diff --git a/Ink Canvas/Helpers/UsbSecurityManager.cs b/Ink Canvas/Helpers/Security/UsbSecurityManager.cs similarity index 100% rename from Ink Canvas/Helpers/UsbSecurityManager.cs rename to Ink Canvas/Helpers/Security/UsbSecurityManager.cs diff --git a/Ink Canvas/Helpers/UIAccessHelper.cs b/Ink Canvas/Helpers/UIAccessHelper.cs index f7bbad8bb..ca20b3ba4 100644 --- a/Ink Canvas/Helpers/UIAccessHelper.cs +++ b/Ink Canvas/Helpers/UIAccessHelper.cs @@ -1197,7 +1197,7 @@ private static string GetExecutablePathForRelaunch() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 以 UIAccess/普通用户身份重启前读取当前主模块路径失败,将回退到 Environment.ProcessPath: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } if (!string.IsNullOrEmpty(mainModulePath) && System.IO.File.Exists(mainModulePath)) diff --git a/Ink Canvas/Helpers/AnimationsHelper.cs b/Ink Canvas/Helpers/UiShell/AnimationsHelper.cs similarity index 100% rename from Ink Canvas/Helpers/AnimationsHelper.cs rename to Ink Canvas/Helpers/UiShell/AnimationsHelper.cs diff --git a/Ink Canvas/Helpers/AvoidFullScreenHelper.cs b/Ink Canvas/Helpers/UiShell/AvoidFullScreenHelper.cs similarity index 100% rename from Ink Canvas/Helpers/AvoidFullScreenHelper.cs rename to Ink Canvas/Helpers/UiShell/AvoidFullScreenHelper.cs diff --git a/Ink Canvas/Helpers/DwmCompositionHelper.cs b/Ink Canvas/Helpers/UiShell/DwmCompositionHelper.cs similarity index 100% rename from Ink Canvas/Helpers/DwmCompositionHelper.cs rename to Ink Canvas/Helpers/UiShell/DwmCompositionHelper.cs diff --git a/Ink Canvas/Helpers/ExceptionHandler.cs b/Ink Canvas/Helpers/UiShell/ExceptionHandler.cs similarity index 100% rename from Ink Canvas/Helpers/ExceptionHandler.cs rename to Ink Canvas/Helpers/UiShell/ExceptionHandler.cs diff --git a/Ink Canvas/Helpers/FullScreenHelper.Win32.cs b/Ink Canvas/Helpers/UiShell/FullScreenHelper.Win32.cs similarity index 100% rename from Ink Canvas/Helpers/FullScreenHelper.Win32.cs rename to Ink Canvas/Helpers/UiShell/FullScreenHelper.Win32.cs diff --git a/Ink Canvas/Helpers/FullScreenHelper.cs b/Ink Canvas/Helpers/UiShell/FullScreenHelper.cs similarity index 100% rename from Ink Canvas/Helpers/FullScreenHelper.cs rename to Ink Canvas/Helpers/UiShell/FullScreenHelper.cs diff --git a/Ink Canvas/Helpers/PopupManagerHelper.cs b/Ink Canvas/Helpers/UiShell/PopupManagerHelper.cs similarity index 100% rename from Ink Canvas/Helpers/PopupManagerHelper.cs rename to Ink Canvas/Helpers/UiShell/PopupManagerHelper.cs diff --git a/Ink Canvas/Helpers/ScreenDetectionHelper.cs b/Ink Canvas/Helpers/UiShell/ScreenDetectionHelper.cs similarity index 100% rename from Ink Canvas/Helpers/ScreenDetectionHelper.cs rename to Ink Canvas/Helpers/UiShell/ScreenDetectionHelper.cs diff --git a/Ink Canvas/Helpers/SliderTouchHelper.cs b/Ink Canvas/Helpers/UiShell/SliderTouchHelper.cs similarity index 98% rename from Ink Canvas/Helpers/SliderTouchHelper.cs rename to Ink Canvas/Helpers/UiShell/SliderTouchHelper.cs index 9ba01aeef..16af78c9e 100644 --- a/Ink Canvas/Helpers/SliderTouchHelper.cs +++ b/Ink Canvas/Helpers/UiShell/SliderTouchHelper.cs @@ -247,7 +247,7 @@ private static void UpdateSliderValueFromPositionFallback(Slider slider, Point p } catch (Exception ex) { - LogHelper.WriteLogToFile($"[UI] 按触摸位置更新滑块值失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } } diff --git a/Ink Canvas/Helpers/ThemeHelper.cs b/Ink Canvas/Helpers/UiShell/ThemeHelper.cs similarity index 91% rename from Ink Canvas/Helpers/ThemeHelper.cs rename to Ink Canvas/Helpers/UiShell/ThemeHelper.cs index 431f18bf2..e4d4bac9d 100644 --- a/Ink Canvas/Helpers/ThemeHelper.cs +++ b/Ink Canvas/Helpers/UiShell/ThemeHelper.cs @@ -25,7 +25,7 @@ static ThemeHelper() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 订阅系统外观偏好变化事件失败,系统深浅色/强调色将不再自动跟随: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -67,7 +67,7 @@ public static Color GetSystemAccentColor() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 读取 DWM 强调色注册表值失败,改用下一级回退来源: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } try @@ -92,7 +92,7 @@ public static Color GetSystemAccentColor() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 读取资源管理器 Accent 注册表值失败,改用窗口玻璃色作为最终来源: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } try @@ -142,7 +142,7 @@ public static bool IsSystemThemeLight() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 读取系统深浅色注册表值(AppsUseLightTheme)失败,按浅色处理: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return true; } diff --git a/Ink Canvas/Helpers/TouchAwareDragDropHelper.cs b/Ink Canvas/Helpers/UiShell/TouchAwareDragDropHelper.cs similarity index 100% rename from Ink Canvas/Helpers/TouchAwareDragDropHelper.cs rename to Ink Canvas/Helpers/UiShell/TouchAwareDragDropHelper.cs diff --git a/Ink Canvas/Helpers/WindowBackdropHelper.cs b/Ink Canvas/Helpers/UiShell/WindowBackdropHelper.cs similarity index 87% rename from Ink Canvas/Helpers/WindowBackdropHelper.cs rename to Ink Canvas/Helpers/UiShell/WindowBackdropHelper.cs index 8aaff8d1f..0dd7b9b71 100644 --- a/Ink Canvas/Helpers/WindowBackdropHelper.cs +++ b/Ink Canvas/Helpers/UiShell/WindowBackdropHelper.cs @@ -105,8 +105,10 @@ private static void ApplyNativeBackdrop(Window window, BackdropType backdropType SyncWindowDarkMode(window); } - catch + catch (Exception ex) { + // DWM 背景应用为尽力而为的视觉增强,失败不影响窗口功能,但需留可诊断记录。 + LogService.LogException(ex); } } @@ -130,8 +132,10 @@ private static void SyncWindowDarkMode(Window window) BackdropHelper.RemoveDarkMode(window); } } - catch + catch (Exception ex) { + // DWM 深色模式同步为尽力而为的视觉增强,失败不影响窗口功能,但需留可诊断记录。 + LogService.LogException(ex); } } @@ -161,8 +165,10 @@ private static void AttachReapplyAfterShow(Window window, BackdropType backdropT }; window.Loaded += onLoaded; } - catch + catch (Exception ex) { + // 重应用挂载失败仅意味着系统背景不在首帧后刷新,不影响既有背景,但需留可诊断记录。 + LogService.LogException(ex); } } @@ -182,8 +188,10 @@ private static bool TrySetWindowHelperBackdrop(Window window, BackdropType backd method.Invoke(null, new object[] { window, backdropType }); return true; } - catch + catch (Exception ex) { + // 反射探测失败按"路径不可用"回落(返回 false),属预期分支,但仍记录以便排查库版本漂移。 + LogService.LogException(ex); return false; } } @@ -206,8 +214,10 @@ private static bool TryGetWindowHelperBackdropType(Window window, out BackdropTy return true; } } - catch + catch (Exception ex) { + // 反射探测失败按"路径不可用"回落(返回 false + None),属预期分支,但仍记录以便排查库版本漂移。 + LogService.LogException(ex); } return false; diff --git a/Ink Canvas/Helpers/WindowOverviewModel.cs b/Ink Canvas/Helpers/UiShell/WindowOverviewModel.cs similarity index 100% rename from Ink Canvas/Helpers/WindowOverviewModel.cs rename to Ink Canvas/Helpers/UiShell/WindowOverviewModel.cs diff --git a/Ink Canvas/Helpers/WindowTopmostManager.cs b/Ink Canvas/Helpers/UiShell/WindowTopmostManager.cs similarity index 100% rename from Ink Canvas/Helpers/WindowTopmostManager.cs rename to Ink Canvas/Helpers/UiShell/WindowTopmostManager.cs diff --git a/Ink Canvas/Helpers/AutoUpdateHelper.cs b/Ink Canvas/Helpers/Update/AutoUpdateHelper.cs similarity index 98% rename from Ink Canvas/Helpers/AutoUpdateHelper.cs rename to Ink Canvas/Helpers/Update/AutoUpdateHelper.cs index b1d55534f..4e4df6122 100644 --- a/Ink Canvas/Helpers/AutoUpdateHelper.cs +++ b/Ink Canvas/Helpers/Update/AutoUpdateHelper.cs @@ -74,11 +74,7 @@ public static void RequestCancelDownload() { foreach (var cts in _activeDownloadSessions) { - try { cts?.Cancel(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Update] 请求取消下载时取消 CancellationTokenSource 失败: {ex.Message}", LogHelper.LogType.Info); - } + try { cts?.Cancel(); } catch (Exception ex) { LogService.LogException(ex); } } } } @@ -99,11 +95,7 @@ private static void EndDownloadSession(CancellationTokenSource cts) { _activeDownloadSessions.Remove(cts); } - try { cts?.Dispose(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Update] 释放下载会话 CancellationTokenSource 失败: {ex.Message}", LogHelper.LogType.Info); - } + try { cts?.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } } public static bool IsX64UpdatePackageSelected() @@ -1310,11 +1302,7 @@ public static async Task DownloadSetupFileWithFallback(string version, Lis return true; } // zip 不存在则回退下载,并清理状态文件以避免下次再误判。 - try { File.Delete(statusFilePath); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Update] 删除失效的下载状态文件 {statusFilePath} 失败,下次检查可能仍误判为已下载: {ex.Message}", LogHelper.LogType.Info); - } + try { File.Delete(statusFilePath); } catch (Exception ex) { LogService.LogException(ex); } LogHelper.WriteLogToFile("AutoUpdate | 状态文件存在但 zip 缺失,重新下载", LogHelper.LogType.Warning); } @@ -1624,11 +1612,7 @@ public static async Task DownloadFile(string fileUrl, string destinationPa LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载已被用户取消", LogHelper.LogType.Warning); if (File.Exists(tempPath)) { - try { File.Delete(tempPath); } - catch (Exception deleteEx) - { - LogHelper.WriteLogToFile($"[Update] 取消分块{block.Index}下载后删除临时分块文件 {tempPath} 失败: {deleteEx.Message}", LogHelper.LogType.Info); - } + try { File.Delete(tempPath); } catch (Exception ex2) { LogService.LogException(ex2); } } cts.Cancel(); return; @@ -1800,14 +1784,7 @@ private static void TryDeleteFileWithRetry(string path, int maxRetry = 3) for (int i = 0; i < maxRetry; i++) { try { File.Delete(path); return; } - catch (IOException) - { - try { Thread.Sleep(500); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Update] 删除文件 {path} 占用重试时的等待被打断,放弃继续重试: {ex.Message}", LogHelper.LogType.Info); - } - } + catch (IOException) { try { Thread.Sleep(500); } catch (Exception ex) { LogService.LogException(ex); } } catch { return; } } } @@ -1873,11 +1850,7 @@ private static async Task DownloadSingleThread(string fileUrl, string dest { LogHelper.WriteLogToFile("AutoUpdate | 单线程下载已被取消", LogHelper.LogType.Warning); progressCallback?.Invoke(0, "下载已取消"); - try { if (File.Exists(destinationPath)) File.Delete(destinationPath); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Update] 取消单线程下载后删除半成品文件 {destinationPath} 失败(下次更新会重新下载): {ex.Message}", LogHelper.LogType.Info); - } + try { if (File.Exists(destinationPath)) File.Delete(destinationPath); } catch (Exception ex) { LogService.LogException(ex); } SetDownloadFailure(DownloadFailureReason.Cancelled); return false; } @@ -1955,7 +1928,7 @@ public static void InstallNewVersionApp(string version, bool isInSilence) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Update] 读取进程保护当前开关状态失败,将按“未开启”处理并继续安装: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } try @@ -2140,7 +2113,7 @@ public static void InstallNewVersionApp(string version, bool isInSilence) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Update] 安装结束后恢复进程保护开关状态失败(可能残留禁用状态): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } } diff --git a/Ink Canvas/Helpers/BaseUploadQueue.cs b/Ink Canvas/Helpers/Update/BaseUploadQueue.cs similarity index 100% rename from Ink Canvas/Helpers/BaseUploadQueue.cs rename to Ink Canvas/Helpers/Update/BaseUploadQueue.cs diff --git a/Ink Canvas/Helpers/DlassApiClient.cs b/Ink Canvas/Helpers/Update/DlassApiClient.cs similarity index 100% rename from Ink Canvas/Helpers/DlassApiClient.cs rename to Ink Canvas/Helpers/Update/DlassApiClient.cs diff --git a/Ink Canvas/Helpers/DlassUploadQueue.cs b/Ink Canvas/Helpers/Update/DlassUploadQueue.cs similarity index 100% rename from Ink Canvas/Helpers/DlassUploadQueue.cs rename to Ink Canvas/Helpers/Update/DlassUploadQueue.cs diff --git a/Ink Canvas/Helpers/TelemetryUploader.cs b/Ink Canvas/Helpers/Update/TelemetryUploader.cs similarity index 98% rename from Ink Canvas/Helpers/TelemetryUploader.cs rename to Ink Canvas/Helpers/Update/TelemetryUploader.cs index 6210e517e..696589ac1 100644 --- a/Ink Canvas/Helpers/TelemetryUploader.cs +++ b/Ink Canvas/Helpers/Update/TelemetryUploader.cs @@ -236,7 +236,7 @@ private static string GetTelemetryToken() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Upload] 向上查找遥测令牌文件失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return string.Empty; diff --git a/Ink Canvas/Helpers/UploadHelper.cs b/Ink Canvas/Helpers/Update/UploadHelper.cs similarity index 100% rename from Ink Canvas/Helpers/UploadHelper.cs rename to Ink Canvas/Helpers/Update/UploadHelper.cs diff --git a/Ink Canvas/Helpers/UploadQueueHelper.cs b/Ink Canvas/Helpers/Update/UploadQueueHelper.cs similarity index 100% rename from Ink Canvas/Helpers/UploadQueueHelper.cs rename to Ink Canvas/Helpers/Update/UploadQueueHelper.cs diff --git a/Ink Canvas/Helpers/WebDavUploadQueue.cs b/Ink Canvas/Helpers/Update/WebDavUploadQueue.cs similarity index 100% rename from Ink Canvas/Helpers/WebDavUploadQueue.cs rename to Ink Canvas/Helpers/Update/WebDavUploadQueue.cs diff --git a/Ink Canvas/Helpers/WebDavUploader.cs b/Ink Canvas/Helpers/Update/WebDavUploader.cs similarity index 100% rename from Ink Canvas/Helpers/WebDavUploader.cs rename to Ink Canvas/Helpers/Update/WebDavUploader.cs diff --git a/Ink Canvas/Helpers/VstoRegistrationHelper.cs b/Ink Canvas/Helpers/VstoRegistrationHelper.cs index 1f59587dc..825c95a84 100644 --- a/Ink Canvas/Helpers/VstoRegistrationHelper.cs +++ b/Ink Canvas/Helpers/VstoRegistrationHelper.cs @@ -107,10 +107,7 @@ public static void CleanupRegistry() LogHelper.WriteLogToFile("VSTO 旧注册表项已清理", LogHelper.LogType.Trace); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[PPT] 清理 VSTO 旧注册表项 {AddInKeyName} 失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } public static bool Unregister() diff --git a/Ink Canvas/Helpers/WindowsNotificationHelper.cs b/Ink Canvas/Helpers/WindowsNotificationHelper.cs index c309205a0..54357fb65 100644 --- a/Ink Canvas/Helpers/WindowsNotificationHelper.cs +++ b/Ink Canvas/Helpers/WindowsNotificationHelper.cs @@ -41,7 +41,7 @@ public static void ShowToast(NotificationMessage message) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Notify] 弹出系统通知({message?.Type})失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -61,7 +61,7 @@ private static void ShowBalloonForWin7(NotificationMessage message) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Notify] 通过任务栏气泡显示通知({message?.Type})失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } }); } diff --git a/Ink Canvas/IPC/PPTAgentPipeClient.cs b/Ink Canvas/IPC/PPTAgentPipeClient.cs index 7a6378181..53f138558 100644 --- a/Ink Canvas/IPC/PPTAgentPipeClient.cs +++ b/Ink Canvas/IPC/PPTAgentPipeClient.cs @@ -280,8 +280,7 @@ private void SetConnected(bool connected) private void ClosePipe() { - try { _pipe?.Dispose(); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT-Agent] 关闭并释放命名管道失败: {ex.Message}", LogHelper.LogType.Info); } + try { _pipe?.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } _pipe = null; SetConnected(false); } diff --git a/Ink Canvas/Ink/WinRT/WetInkOverlayWindow.cs b/Ink Canvas/Ink/WinRT/WetInkOverlayWindow.cs index 0e92c938c..fb546e71a 100644 --- a/Ink Canvas/Ink/WinRT/WetInkOverlayWindow.cs +++ b/Ink Canvas/Ink/WinRT/WetInkOverlayWindow.cs @@ -255,34 +255,22 @@ public void Dispose() if (_compositionVisual != null) { - try { _compositionVisual.Dispose(); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[WinRTInk] 释放合成视觉对象失败: {ex.Message}", LogHelper.LogType.Info); - } + try { _compositionVisual.Dispose(); } catch (Exception ex) { Helpers.LogService.LogException(ex); } _compositionVisual = null; } if (_compositionTarget != null) { - try { _compositionTarget.Dispose(); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[WinRTInk] 释放合成目标失败: {ex.Message}", LogHelper.LogType.Info); - } + try { _compositionTarget.Dispose(); } catch (Exception ex) { Helpers.LogService.LogException(ex); } _compositionTarget = null; } if (_compositionDevice3 != null) { - try { _compositionDevice3.Dispose(); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[WinRTInk] 释放合成设备(D3D11)失败: {ex.Message}", LogHelper.LogType.Info); - } + try { _compositionDevice3.Dispose(); } catch (Exception ex) { Helpers.LogService.LogException(ex); } _compositionDevice3 = null; } if (_compositionDevice != null) { - try { _compositionDevice.Dispose(); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[WinRTInk] 释放合成设备失败: {ex.Message}", LogHelper.LogType.Info); - } + try { _compositionDevice.Dispose(); } catch (Exception ex) { Helpers.LogService.LogException(ex); } _compositionDevice = null; } diff --git a/Ink Canvas/Ink/WinRT/WinRTInkHost.cs b/Ink Canvas/Ink/WinRT/WinRTInkHost.cs index 511a40008..4321b3283 100644 --- a/Ink Canvas/Ink/WinRT/WinRTInkHost.cs +++ b/Ink Canvas/Ink/WinRT/WinRTInkHost.cs @@ -403,10 +403,7 @@ public void Dispose() _independentInput.PointerLost -= _onPointerLost; _independentInput.PointerExiting -= _onPointerExiting; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[WinRTInk] 取消输入源事件订阅失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } _independentInput = null; } @@ -418,10 +415,7 @@ public void Dispose() _presenter.StrokeInput.StrokeCanceled -= _onStrokeCanceled; _presenter.StrokesCollected -= _onStrokesCollected; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[WinRTInk] 取消 Presenter 事件订阅失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } _presenter = null; } @@ -440,10 +434,7 @@ public void Dispose() if (_host != null) { try { Marshal.FinalReleaseComObject(_host); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[WinRTInk] 释放 InkDesktopHost COM 对象失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } _host = null; } } diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs index 2f44a9c1e..955b9d2b1 100644 --- a/Ink Canvas/MainWindow.xaml.cs +++ b/Ink Canvas/MainWindow.xaml.cs @@ -3,6 +3,7 @@ using Ink_Canvas.Helpers; using Ink_Canvas.Models; using Ink_Canvas.Properties; +using Ink_Canvas.Services; using Ink_Canvas.Windows; using Ink_Canvas.Windows.SettingsViews; using Ink_Canvas.Windows.SettingsViews.Helpers; @@ -53,6 +54,9 @@ public partial class MainWindow : Ink_Canvas.Helpers.PerformanceTransparentWin private GlobalHotkeyManager _globalHotkeyManager; internal GlobalHotkeyManager GlobalHotkeyManagerInstance => _globalHotkeyManager; + // 全局热键配置服务:HotkeyConfig.json 读写与加载编排,随管理器一并创建 + private Services.Shell.HotkeyService _hotkeyService; + // PPT PageUp/PageDown 低级键盘钩子 private PPTPageKeyHook _pptPageKeyHook; @@ -60,8 +64,8 @@ public partial class MainWindow : Ink_Canvas.Helpers.PerformanceTransparentWin private InkFadeManager _inkFadeManager; // 暴露给插件墨迹特效服务的入口(未初始化时为 null) internal InkFadeManager InkFadeManagerInstance => _inkFadeManager; - private readonly CancellationTokenSource _notificationProviderCancellation = new CancellationTokenSource(); - private AnnouncementService _announcementService; + private readonly NotificationService _notificationService; + private readonly Services.Shell.ScreenshotService _screenshotService; // 悬浮窗拦截管理器 public FloatingWindowInterceptorManager _floatingWindowInterceptorManager; @@ -535,6 +539,9 @@ public MainWindow() */ InitializeComponent(); + _notificationService = CreateNotificationService(); + _screenshotService = CreateScreenshotService(); + if (BorderTools.Child is FrameworkElement btChild) btChild.Visibility = Visibility.Collapsed; if (BorderDrawShape.Child is FrameworkElement bdsChild) bdsChild.Visibility = Visibility.Collapsed; if (BoardBorderToolsPopup.Child is FrameworkElement bbtpChild) bbtpChild.Visibility = Visibility.Collapsed; @@ -830,6 +837,9 @@ public MainWindow() Deactivated += Window_Deactivated; Dispatcher.BeginInvoke(new Action(CheckAndShowOobe), DispatcherPriority.Loaded); + + // 设置存储服务(寄生提取),供各 partial 中的转发壳使用 + _settingsStore = new Services.SettingsStore(); } /// @@ -1904,7 +1914,7 @@ await Dispatcher.BeginInvoke(new Action(async () => catch (Exception ex) { // 走到这里说明验证流程中途失败且未置位放行标志,窗口会表现为"点关闭没反应" - LogHelper.WriteLogToFile($"[Exit] 退出密码验证流程异常,本次关闭未放行: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } finally { @@ -1916,7 +1926,7 @@ await Dispatcher.BeginInvoke(new Action(async () => } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Exit] 检查是否需要退出密码验证失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } if (!CloseIsFromButton && Settings.Advanced.IsSecondConfirmWhenShutdownApp) @@ -2160,7 +2170,7 @@ public async void AutoUpdate() { LogHelper.WriteLogToFile($"AutoUpdate | 暂停期已过,恢复自动更新检查"); Settings.Startup.AutoUpdatePauseUntilDate = ""; - try { await Dispatcher.InvokeAsync(() => SaveSettingsToFile()); } catch (TaskCanceledException) { } catch (ObjectDisposedException) { } + try { await Dispatcher.InvokeAsync(() => SaveSettingsToFile()); } catch (TaskCanceledException ex) { LogService.LogException(ex); } catch (ObjectDisposedException ex) { LogService.LogException(ex); } } } } @@ -2191,8 +2201,8 @@ await Dispatcher.InvokeAsync(() => updateCheckRetryCount = 0; }); } - catch (TaskCanceledException) { } - catch (ObjectDisposedException) { } + catch (TaskCanceledException ex) { LogService.LogException(ex); } + catch (ObjectDisposedException ex) { LogService.LogException(ex); } // 检测到新版本 LogHelper.WriteLogToFile($"AutoUpdate | New version available: {AvailableLatestVersion}"); @@ -2241,7 +2251,7 @@ await Dispatcher.InvokeAsync(() => { LogHelper.WriteLogToFile($"AutoUpdate | Detected new version {AvailableLatestVersion} different from skipped version {Settings.Startup.SkippedVersion}, clearing skip record"); Settings.Startup.SkippedVersion = ""; - try { await Dispatcher.InvokeAsync(() => SaveSettingsToFile()); } catch (TaskCanceledException) { } catch (ObjectDisposedException) { } + try { await Dispatcher.InvokeAsync(() => SaveSettingsToFile()); } catch (TaskCanceledException ex) { LogService.LogException(ex); } catch (ObjectDisposedException ex) { LogService.LogException(ex); } } // 如果启用了静默更新,则自动下载更新而不显示提示 @@ -2257,7 +2267,7 @@ await Dispatcher.InvokeAsync(() => LogHelper.WriteLogToFile("AutoUpdate | Update downloaded successfully, will install when conditions are met"); // 启动检查定时器,定期检查是否可以安装 - try { await Dispatcher.InvokeAsync(() => timerCheckAutoUpdateWithSilence.Start()); } catch (TaskCanceledException) { } catch (ObjectDisposedException) { } + try { await Dispatcher.InvokeAsync(() => timerCheckAutoUpdateWithSilence.Start()); } catch (TaskCanceledException ex) { LogService.LogException(ex); } catch (ObjectDisposedException ex) { LogService.LogException(ex); } } else { @@ -2284,8 +2294,8 @@ await Dispatcher.InvokeAsync(() => updateCheckRetryCount = 0; }); } - catch (TaskCanceledException) { } - catch (ObjectDisposedException) { } + catch (TaskCanceledException ex) { LogService.LogException(ex); } + catch (ObjectDisposedException ex) { LogService.LogException(ex); } } else { @@ -2296,7 +2306,7 @@ await Dispatcher.InvokeAsync(() => updateCheckRetryCount = 0; // 启动重试定时器,10分钟后重新检查 - try { await Dispatcher.InvokeAsync(() => timerCheckAutoUpdateRetry.Start()); } catch (TaskCanceledException) { } catch (ObjectDisposedException) { } + try { await Dispatcher.InvokeAsync(() => timerCheckAutoUpdateRetry.Start()); } catch (TaskCanceledException ex) { LogService.LogException(ex); } catch (ObjectDisposedException ex) { LogService.LogException(ex); } // 清理更新文件夹 AutoUpdateHelper.DeleteUpdatesFolder(); @@ -2756,6 +2766,52 @@ private void Window_Activated(object sender, EventArgs e) _popupManager?.OnOwnerActivated(); } + /// + /// 装配通知服务:注入 x:Name 控件强类型引用与访问主窗口私有状态的委托, + /// 服务本身不引用 MainWindow 类型、不直接引用 UI 线程调度器。 + /// + private NotificationService CreateNotificationService() + { + return new NotificationService( + DynamicNotification, + TextBlockNotice, + GridNotifications, + ViewboxFloatingBar, + new NotificationService.Hooks + { + GetSettings = () => Settings, + IsCurrentThemeDark = IsCurrentThemeDark, + IsInPptPresentationMode = () => IsInPPTPresentationMode, + GetCurrentMode = () => currentMode, + IsFloatingBarFolded = () => isFloatingBarFolded, + IsFloatingBarChangingHideMode = () => isFloatingBarChangingHideMode, + GetHostWindow = () => this, + UiInvoke = a => Dispatcher.BeginInvoke(a), + UiInvokeContextIdle = a => Dispatcher.BeginInvoke(a, DispatcherPriority.ContextIdle) + }); + } + + /// + /// 装配截图服务:注入设置读取、墨迹快照、白板页索引、DPI 缩放、通知与 + /// UI 线程调度委托,服务本身不引用 MainWindow 类型、不直接引用 UI 线程调度器。 + /// 通知文本(MainWindowStrings 资源)保留在主窗口侧格式化,服务只接收格式化入口。 + /// + private Services.Shell.ScreenshotService CreateScreenshotService() + { + return new Services.Shell.ScreenshotService( + new Services.Shell.ScreenshotService.Hooks + { + GetSettings = () => Settings, + GetInkStrokeCount = () => inkCanvas?.Strokes?.Count, + CloneInkStrokes = () => inkCanvas.Strokes.Clone(), + GetCurrentWhiteboardIndex = () => CurrentWhiteboardIndex, + GetDpiScale = GetDpiScale, + ShowScreenshotSaveSuccess = path => ShowNotification(string.Format(Properties.MainWindowStrings.Main_Screenshot_SaveSuccess, path)), + UiInvoke = a => Dispatcher.Invoke(a), + SaveInkStrokesAtScreenshot = () => SaveInkCanvasStrokes(false) + }); + } + private void InitializeNotificationAndAutomationForStartup() { try @@ -3058,6 +3114,8 @@ internal GlobalHotkeyManager EnsureGlobalHotkeyManagerCreated() if (_globalHotkeyManager == null) { _globalHotkeyManager = new GlobalHotkeyManager(this); + // 热键配置服务与管理器同栈创建,内置热键回调以字典注入(回调本体在 MW_Hotkeys.cs) + _hotkeyService = new Services.Shell.HotkeyService(_globalHotkeyManager, BuildBuiltinHotkeyActions()); // 启动时加载快捷键,但默认为鼠标模式,禁用快捷键以放行键盘操作 _globalHotkeyManager.EnableHotkeyRegistration(); // 启动时默认为鼠标模式,禁用快捷键 diff --git a/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs b/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs index fe6d930c9..33e1f59de 100644 --- a/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs +++ b/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs @@ -1,6 +1,7 @@ using Ink_Canvas.Controls; using Ink_Canvas.Controls.Toolbar.FloatingToolbar; using Ink_Canvas.Helpers; +using Ink_Canvas.Models; using iNKORE.UI.WPF.Modern; using Microsoft.Win32; using System; @@ -114,7 +115,7 @@ private void RefreshQuickPanelIcons() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 刷新快捷面板图标失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -165,7 +166,7 @@ private void RefreshFloatingBarHighlightColors() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 刷新浮动栏高亮配色失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -234,29 +235,29 @@ void SetSelectedFloatingBarButtonBrush(ToolbarImageButton btn) if (btn != null && !ToolbarRegistry.GetUseRedStyle(btn)) btn.Icon.Brush = new SolidColorBrush(selectedColor); } - switch (_currentToolMode) + switch (_currentToolModeEnum) { - case "cursor": + case ToolMode.Cursor: SetSelectedFloatingBarButtonBrush(Cursor_Icon); break; - case "pen": - case "color": + case ToolMode.Pen: + case ToolMode.Color: SetSelectedFloatingBarButtonBrush(Pen_Icon); break; - case "eraser": + case ToolMode.Eraser: SetSelectedFloatingBarButtonBrush(Eraser_Icon); break; - case "eraserByStrokes": + case ToolMode.EraserByStrokes: SetSelectedFloatingBarButtonBrush(EraserByStrokes_Icon); break; - case "select": + case ToolMode.Select: SetSelectedFloatingBarButtonBrush(SymbolIconSelect); break; } } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 刷新浮动栏按钮配色失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -375,7 +376,7 @@ private void AutoSwitchFloatingBarIconForTheme(string theme) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 按主题自动切换浮动栏图标失败 (theme={theme}): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -397,7 +398,7 @@ private void UpdateFloatingBarIconComboBox() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 同步浮动栏图标下拉框失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -426,7 +427,7 @@ private void RefreshStrokeSelectionIcons() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 刷新墨迹选择栏图标失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -441,7 +442,7 @@ private void RefreshImageSelectionIcons() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 刷新图片选择栏图标失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -456,7 +457,7 @@ private void RefreshGestureButtonIcon() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 刷新手势按钮图标失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -477,7 +478,7 @@ private void RefreshOtherWindowsTheme() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Theme] 刷新其它窗口主题失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } } diff --git a/Ink Canvas/MainWindow_cs/MW_BoardRoaming.cs b/Ink Canvas/MainWindow_cs/MW_BoardRoaming.cs index ca52af3f7..c6988e7f3 100644 --- a/Ink Canvas/MainWindow_cs/MW_BoardRoaming.cs +++ b/Ink Canvas/MainWindow_cs/MW_BoardRoaming.cs @@ -1,5 +1,6 @@ using Ink_Canvas.Controls; using Ink_Canvas.Helpers; +using Ink_Canvas.Models; using Ink_Canvas.Properties; using System; using System.Collections.Generic; @@ -74,7 +75,7 @@ internal void ActivateBoardRoamingMode() } private bool IsBoardRoamingMode - => currentMode == 1 && string.Equals(_currentToolMode, "roaming", StringComparison.Ordinal); + => currentMode == 1 && _currentToolModeEnum == ToolMode.Roaming; private void UpdateBoardRoamingButtonState() { @@ -618,7 +619,7 @@ private Rect GetBoardRoamingContentBounds() catch (InvalidOperationException ex) { // TransformToAncestor 失败通常意味着该元素不在 inkCanvas 可视树内(状态不同步) - LogHelper.WriteLogToFile($"[Board] 计算子元素边界失败,元素可能不在画布可视树内: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } diff --git a/Ink Canvas/MainWindow_cs/MW_EdgeExpandHint.cs b/Ink Canvas/MainWindow_cs/MW_EdgeExpandHint.cs index 76f66047b..4a14d58b8 100644 --- a/Ink Canvas/MainWindow_cs/MW_EdgeExpandHint.cs +++ b/Ink Canvas/MainWindow_cs/MW_EdgeExpandHint.cs @@ -1,4 +1,5 @@ using Ink_Canvas.Helpers; +using Ink_Canvas.Models; using System; using System.Collections.Generic; using System.Windows; @@ -148,11 +149,11 @@ private bool IsEdgeExpandHintEligible() // 工具选择不是笔(橡皮/框选/图形/漫游/鼠标)时不提示。 // 注意:原生湿墨水下笔工具的物理 EditingMode 为 None,因此以逻辑工具状态判定。 // 特例:进入白板的部分入口(启动时恢复白板状态等)直接调用 SwitchBackground, - // 绕过了 ImageBlackboard_MouseUp 的切笔逻辑,_currentToolMode 会停留在 "cursor", + // 绕过了 ImageBlackboard_MouseUp 的切笔逻辑,_currentToolModeEnum 会停留在 cursor, // 导致进入白板后的首次书写被误判为非笔工具。白板模式没有"鼠标"工具且进入时 // 必然切回笔,因此白板下的 cursor 按笔对待。 - bool isPenToolSelected = _currentToolMode == "pen" || _currentToolMode == "color"; - if (!isPenToolSelected && !(IsWhiteboardMode && _currentToolMode == "cursor")) return false; + bool isPenToolSelected = _currentToolModeEnum == ToolMode.Pen || _currentToolModeEnum == ToolMode.Color; + if (!isPenToolSelected && !(IsWhiteboardMode && _currentToolModeEnum == ToolMode.Cursor)) return false; if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink && inkCanvas.EditingMode != InkCanvasEditingMode.None) return false; diff --git a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs index 488ffcaf4..63cf6187e 100644 --- a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs +++ b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs @@ -1,6 +1,7 @@ using Ink_Canvas.Controls; using Ink_Canvas.Controls.Toolbar.FloatingToolbar; using Ink_Canvas.Helpers; +using Ink_Canvas.Models; using Ink_Canvas.Properties; using Ink_Canvas.WorkflowAutomation; using iNKORE.UI.WPF.Modern; @@ -35,7 +36,7 @@ public partial class MainWindow : Ink_Canvas.Helpers.PerformanceTransparentWin /// /// 当前工具模式 /// - private string _currentToolMode = "cursor"; + private ToolMode _currentToolModeEnum = ToolMode.Cursor; private static Windows.SettingsViews.SettingsWindow _settingsWindow = null; @@ -486,7 +487,7 @@ internal void SymbolIconEmoji_MouseUp(object sender, MouseButtonEventArgs e) } // 每次点击或拖动结束后都重新定位高光 - SetFloatingBarHighlightPosition(_currentToolMode); + SetFloatingBarHighlightPosition(ToolModeMapping.ToInternalString(_currentToolModeEnum)); GridForFloatingBarDraging.Visibility = Visibility.Collapsed; } @@ -1014,8 +1015,9 @@ internal void ImageBlackboard_MouseUp(object sender, MouseButtonEventArgs e) { LogHelper.WriteLogToFile($"进入白板模式时更新名言失败: {t.Exception?.GetBaseException().Message}", LogHelper.LogType.Warning); } - catch + catch (Exception ex) { + LogService.LogException(ex); } if (Settings.Appearance.EnableChickenSoupInWhiteboardMode && Settings.Appearance.ChickenSoupSource != 3) { @@ -1188,7 +1190,7 @@ internal void ImageBlackboard_MouseUp(object sender, MouseButtonEventArgs e) { UpdateIndexInfoDisplay(); - // 进入白板模式时显式切换到笔模式:SwitchBackground 不会更新 _currentToolMode, + // 进入白板模式时显式切换到笔模式:SwitchBackground 不会更新 _currentToolModeEnum, // 这里需要复用 PenIcon_Click 的完整切笔逻辑,但应避免被误判为"再次点击笔"而弹出笔设置面板。 if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink && inkCanvas.EditingMode != InkCanvasEditingMode.Select) @@ -2196,7 +2198,7 @@ private void SetFloatingBarHeadPlacement(bool headOnRight) isFloatingBarHeadOnRight = headOnRight; - SetFloatingBarHighlightPosition(_currentToolMode); + SetFloatingBarHighlightPosition(ToolModeMapping.ToInternalString(_currentToolModeEnum)); } private void SetFloatingBarHeadPlacementVertical(bool headOnBottom) @@ -2275,7 +2277,7 @@ private void SetFloatingBarHeadPlacementVertical(bool headOnBottom) isFloatingBarHeadOnBottom = headOnBottom; - SetFloatingBarHighlightPosition(_currentToolMode); + SetFloatingBarHighlightPosition(ToolModeMapping.ToInternalString(_currentToolModeEnum)); } internal void UpdateToolbarPosition() @@ -2749,9 +2751,9 @@ await Dispatcher.InvokeAsync(() => isViewboxFloatingBarMarginAnimationRunning = false; if (!Topmost) ViewboxFloatingBar.Visibility = Visibility.Hidden; - if (!string.IsNullOrEmpty(_currentToolMode)) + if (_currentToolModeEnum != ToolMode.Unknown) { - SetFloatingBarHighlightPosition(_currentToolMode); + SetFloatingBarHighlightPosition(ToolModeMapping.ToInternalString(_currentToolModeEnum)); } // Issue #285:动画会重新显示完整浮动栏,若此时应处于迷你栏状态则重新接管 @@ -3152,9 +3154,9 @@ await Dispatcher.InvokeAsync(() => { ViewboxFloatingBar.Margin = new Thickness(pos.X, pos.Y, -2000, -200); - if (!string.IsNullOrEmpty(_currentToolMode)) + if (_currentToolModeEnum != ToolMode.Unknown) { - SetFloatingBarHighlightPosition(_currentToolMode); + SetFloatingBarHighlightPosition(ToolModeMapping.ToInternalString(_currentToolModeEnum)); } }); } @@ -3268,9 +3270,9 @@ await Dispatcher.InvokeAsync(() => { ViewboxFloatingBar.Margin = new Thickness(pos.X, pos.Y, -2000, -200); - if (!string.IsNullOrEmpty(_currentToolMode)) + if (_currentToolModeEnum != ToolMode.Unknown) { - SetFloatingBarHighlightPosition(_currentToolMode); + SetFloatingBarHighlightPosition(ToolModeMapping.ToInternalString(_currentToolModeEnum)); } }); @@ -5420,7 +5422,7 @@ internal void UpdatePenIconColor() if (Pen_Icon == null || Pen_Icon.Icon == null) return; if (!Settings.Appearance.ShowPenColorOnFloatingBarIcon - || (_currentToolMode != "pen" && _currentToolMode != "color")) + || (_currentToolModeEnum != ToolMode.Pen && _currentToolModeEnum != ToolMode.Color)) { Pen_Icon.ClearIconInnerOutline(); return; @@ -5432,10 +5434,7 @@ internal void UpdatePenIconColor() Pen_Icon.Icon.Brush = new SolidColorBrush(inkColor); ApplyPenIconOutlineForContrast(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Toolbar] 更新浮动栏批注图标颜色为当前画笔颜色失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -5469,10 +5468,7 @@ private Color GetFloatingBarBackgroundColor() if (Application.Current.TryFindResource("FloatBarBackground") is SolidColorBrush backgroundBrush) return backgroundBrush.Color; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Toolbar] 读取浮动栏背景色资源 FloatBarBackground 失败,将按主题推断: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } bool isDarkTheme = Settings.Appearance.Theme == 1 || (Settings.Appearance.Theme == 2 && !ThemeHelper.IsSystemThemeLight()); @@ -5502,10 +5498,7 @@ private void ApplyPenIconOutlineForContrast() Pen_Icon.ClearIconInnerOutline(); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Toolbar] 按对比度为浮动栏批注图标添加/移除内描边失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -5830,10 +5823,7 @@ private void AnimateFloatingBarHighlightTo(string mode) _activeIndicatorCompletedHandler = null; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Toolbar] 切换浮动栏高亮指示条位置时停止旧动画并解绑 Completed 失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } indicatorBar.RenderTransform = null; indicatorBar.Opacity = 1.0; } @@ -6248,9 +6238,9 @@ public string GetCurrentSelectedMode() try { // 优先使用缓存的模式,避免在浮动栏刷新时返回过时的模式信息 - if (!string.IsNullOrEmpty(_currentToolMode)) + if (_currentToolModeEnum != ToolMode.Unknown) { - return _currentToolMode; + return ToolModeMapping.ToInternalString(_currentToolModeEnum); } // 如果缓存为空,则从inkCanvas状态推断模式 @@ -6287,9 +6277,9 @@ public string GetCurrentSelectedMode() { // Native freehand keeps physical EditingMode at None while logical // tool remains pen/color. Prefer cached mode; fall back to cursor. - if (!string.IsNullOrEmpty(_currentToolMode) - && !string.Equals(_currentToolMode, "cursor", StringComparison.OrdinalIgnoreCase)) - return _currentToolMode; + if (_currentToolModeEnum != ToolMode.Unknown + && _currentToolModeEnum != ToolMode.Cursor) + return ToolModeMapping.ToInternalString(_currentToolModeEnum); return "cursor"; } else if (drawingShapeMode != 0) @@ -6311,10 +6301,10 @@ public string GetCurrentSelectedMode() /// 模式名称 private void UpdateCurrentToolMode(string mode) { - _currentToolMode = NormalizeToolModeForFreeze(mode); + _currentToolModeEnum = ToolModeMapping.FromInternalString(NormalizeToolModeForFreeze(mode)); // 工具切换为非笔模式(橡皮/框选/图形/漫游/鼠标)时,扩展画布提示立即消失 - if (_currentToolMode != "pen" && _currentToolMode != "color") + if (_currentToolModeEnum != ToolMode.Pen && _currentToolModeEnum != ToolMode.Color) { HideEdgeExpandHint(); } @@ -6341,7 +6331,7 @@ private void UpdateCurrentToolMode(string mode) /// /// 自动化「切换批注模式」动作入口:进入/退出批注模式。 /// 与画笔/光标按钮保持一致的 SetCurrentToolMode + UpdateCurrentToolMode 序列, - /// 保证 _currentToolMode(逻辑工具)与原生湿墨迹管线同步。 + /// 保证 _currentToolModeEnum(逻辑工具)与原生湿墨迹管线同步。 /// internal void SetAnnotationModeFromAutomation(bool enterAnnotation) { diff --git a/Ink Canvas/MainWindow_cs/MW_Hotkeys.cs b/Ink Canvas/MainWindow_cs/MW_Hotkeys.cs index a444b795c..1f1a3980b 100644 --- a/Ink Canvas/MainWindow_cs/MW_Hotkeys.cs +++ b/Ink Canvas/MainWindow_cs/MW_Hotkeys.cs @@ -1,5 +1,6 @@ using Ink_Canvas.Helpers; using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using System.Windows.Input; using System.Windows.Interop; @@ -174,7 +175,7 @@ private void ForwardMouseWheelToUnderlyingWindow(MouseWheelEventArgs e) // 记录原始 ExStyle prevExStyle = GetWindowLongPtr(handle, GWL_EXSTYLE); long originalStyle = prevExStyle.ToInt64(); - try { LogHelper.WriteLogToFile($"[MouseWheel] origExStyle=0x{originalStyle:X}", LogHelper.LogType.Trace); } catch { } + try { LogHelper.WriteLogToFile($"[MouseWheel] origExStyle=0x{originalStyle:X}", LogHelper.LogType.Trace); } catch (Exception ex) { LogService.LogException(ex); } // 1) 加 WS_EX_TRANSPARENT,让命中测试能临时"看穿"我们到下层 long newStyle = originalStyle | WS_EX_TRANSPARENT; @@ -183,7 +184,7 @@ private void ForwardMouseWheelToUnderlyingWindow(MouseWheelEventArgs e) SetWindowLongPtr(handle, GWL_EXSTYLE, new IntPtr(newStyle)); exStyleChanged = true; long after = GetWindowLongPtr(handle, GWL_EXSTYLE).ToInt64(); - try { LogHelper.WriteLogToFile($"[MouseWheel] afterExStyle=0x{after:X}", LogHelper.LogType.Trace); } catch { } + try { LogHelper.WriteLogToFile($"[MouseWheel] afterExStyle=0x{after:X}", LogHelper.LogType.Trace); } catch (Exception ex) { LogService.LogException(ex); } } // 2) 注入滚轮事件(系统会按 Z 序派发到下层,因为我们已 WS_EX_TRANSPARENT) @@ -202,13 +203,13 @@ private void ForwardMouseWheelToUnderlyingWindow(MouseWheelEventArgs e) } }; bool ok = SendInput(1, new[] { input }, Marshal.SizeOf(typeof(INPUT))); - try { LogHelper.WriteLogToFile($"[MouseWheel] SendInput ret={ok} delta={e.Delta}", LogHelper.LogType.Trace); } catch { } + try { LogHelper.WriteLogToFile($"[MouseWheel] SendInput ret={ok} delta={e.Delta}", LogHelper.LogType.Trace); } catch (Exception ex) { LogService.LogException(ex); } e.Handled = true; } catch (Exception ex) { - try { LogHelper.WriteLogToFile($"[MouseWheel] 注入滚轮失败: {ex.Message}", LogHelper.LogType.Error); } catch { } + try { LogHelper.WriteLogToFile($"[MouseWheel] 注入滚轮失败: {ex.Message}", LogHelper.LogType.Error); } catch (Exception ex2) { LogService.LogException(ex2); } } finally { @@ -224,7 +225,7 @@ private void ForwardMouseWheelToUnderlyingWindow(MouseWheelEventArgs e) } catch (Exception ex) { - try { LogHelper.WriteLogToFile($"[MouseWheel] 恢复窗口样式失败: {ex.Message}", LogHelper.LogType.Error); } catch { } + try { LogHelper.WriteLogToFile($"[MouseWheel] 恢复窗口样式失败: {ex.Message}", LogHelper.LogType.Error); } catch (Exception ex2) { LogService.LogException(ex2); } } finally { @@ -472,5 +473,37 @@ private void KeyHide(object sender, ExecutedRoutedEventArgs e) { SymbolIconEmoji_MouseUp(null, null); } + + /// + /// 构建内置热键「名称 → 触发回调」字典(HotkeyService 构造注入, + /// 替代原 GlobalHotkeyManager.GetActionByName 的 switch 硬编码)。 + /// 键集合与原 switch 完全一致;Pen1-5 的笔型映射复刻原 SwitchToPenType: + /// 0/3/4→默认笔、1→荧光笔、2→激光笔(原为反射调用,此处直接调用,等价)。 + /// + private Dictionary BuildBuiltinHotkeyActions() + { + return new Dictionary(StringComparer.Ordinal) + { + { "Undo", () => SymbolIconUndo_MouseUp(null, null) }, + { "Redo", () => SymbolIconRedo_MouseUp(null, null) }, + { "Clear", () => SymbolIconDelete_MouseUp(null, null) }, + { "Paste", () => HandleGlobalPaste(null, null) }, + { "SelectTool", () => SwitchToSelectFromHotkey() }, + { "DrawTool", () => PenIcon_Click(null, null) }, + { "EraserTool", () => SwitchToEraserFromHotkey() }, + { "BlackboardTool", () => ImageBlackboard_MouseUp(null, null) }, + { "QuitDrawTool", () => KeyChangeToQuitDrawTool(null, null) }, + { "Pen1", () => SwitchToDefaultPen(null, null) }, + { "Pen2", () => SwitchToHighlighterPen(null, null) }, + { "Pen3", () => SwitchToLaserPen(null, null) }, + { "Pen4", () => SwitchToDefaultPen(null, null) }, + { "Pen5", () => SwitchToDefaultPen(null, null) }, + { "DrawLine", () => DrawLineFromHotkey() }, + { "Screenshot", () => SaveScreenShotToDesktop() }, + { "QuickDraw", () => OpenQuickDrawFromHotkey() }, + { "Hide", () => SymbolIconEmoji_MouseUp(null, null) }, + { "Exit", () => KeyExit(null, null) }, + }; + } } } diff --git a/Ink Canvas/MainWindow_cs/MW_IdleMiniBar.cs b/Ink Canvas/MainWindow_cs/MW_IdleMiniBar.cs index ef55df200..dfeb56097 100644 --- a/Ink Canvas/MainWindow_cs/MW_IdleMiniBar.cs +++ b/Ink Canvas/MainWindow_cs/MW_IdleMiniBar.cs @@ -1,4 +1,5 @@ using Ink_Canvas.Helpers; +using Ink_Canvas.Models; using System; using System.Windows; using System.Windows.Input; @@ -49,7 +50,7 @@ private bool IsIdleMiniBarEligible() if (IdleMiniBar == null) return false; if (isFloatingBarFolded) return false; if (currentMode == 1) return false; // 白板模式使用独立工具栏 - if (_currentToolMode != "cursor") return false; // 仅闲置(光标)状态 + if (_currentToolModeEnum != ToolMode.Cursor) return false; // 仅闲置(光标)状态 return true; } diff --git a/Ink Canvas/MainWindow_cs/MW_ImageInsert.cs b/Ink Canvas/MainWindow_cs/MW_ImageInsert.cs index 33fc50263..08c68b0fb 100644 --- a/Ink Canvas/MainWindow_cs/MW_ImageInsert.cs +++ b/Ink Canvas/MainWindow_cs/MW_ImageInsert.cs @@ -1,4 +1,5 @@ using Ink_Canvas.Helpers; +using Ink_Canvas.Services.Shell; using System; using System.Collections.Generic; using System.Drawing; @@ -292,56 +293,12 @@ await Application.Current.Dispatcher.InvokeAsync(() => } /// - /// 截取指定屏幕区域 + /// 截取指定屏幕区域。 + /// 壳:本体已搬入 。 /// /// 要截取的屏幕区域 /// 截取的位图 - /// - /// 该方法会: - /// 1. 确保区域在有效范围内 - /// 2. 调整区域边界,确保不超出屏幕范围 - /// 3. 创建支持透明度的位图 - /// 4. 设置高质量渲染 - /// 5. 截取屏幕区域 - /// - private Bitmap CaptureScreenArea(Rectangle area) - { - try - { - // 确保区域在有效范围内 - var virtualScreen = SystemInformation.VirtualScreen; - - // 调整区域边界,确保不超出屏幕范围 - int x = Math.Max(area.X, virtualScreen.X); - int y = Math.Max(area.Y, virtualScreen.Y); - int right = Math.Min(area.Right, virtualScreen.Right); - int bottom = Math.Min(area.Bottom, virtualScreen.Bottom); - - int width = Math.Max(1, right - x); - int height = Math.Max(1, bottom - y); - - // 创建支持透明度的位图 - var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb); - using (var graphics = Graphics.FromImage(bitmap)) - { - // 设置高质量渲染 - graphics.CompositingQuality = CompositingQuality.HighQuality; - graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphics.SmoothingMode = SmoothingMode.HighQuality; - graphics.CompositingMode = CompositingMode.SourceOver; - - // 截取屏幕区域 - graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy); - } - - return bitmap; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"截取屏幕区域失败: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } + private Bitmap CaptureScreenArea(Rectangle area) => ScreenshotService.CaptureScreenArea(area); private BitmapSource CreateInkOverlayPreviewBitmapSource() { @@ -402,82 +359,19 @@ private BitmapSource CreateInkOverlayPreviewBitmapSource() } } + /// + /// 在捕获位图上叠加墨迹预览。 + /// 壳:本体已搬入 。 + /// private Bitmap OverlayInkOnCapturedBitmap(Bitmap capturedBitmap, Rectangle captureArea, BitmapSource inkOverlayBitmapSource) - { - if (capturedBitmap == null || inkOverlayBitmapSource == null) - { - return capturedBitmap; - } - - try - { - var virtualScreen = SystemInformation.VirtualScreen; - var sourceRect = new Rectangle( - captureArea.X - virtualScreen.X, - captureArea.Y - virtualScreen.Y, - captureArea.Width, - captureArea.Height); - - sourceRect.Intersect(new Rectangle(0, 0, inkOverlayBitmapSource.PixelWidth, inkOverlayBitmapSource.PixelHeight)); - if (sourceRect.Width <= 0 || sourceRect.Height <= 0) - { - return capturedBitmap; - } - - using (var inkOverlayBitmap = ConvertBitmapSourceToBitmap(inkOverlayBitmapSource)) - { - if (inkOverlayBitmap == null) - { - return capturedBitmap; - } - - Bitmap resultBitmap = null; - try - { - resultBitmap = new Bitmap(capturedBitmap.Width, capturedBitmap.Height, PixelFormat.Format32bppArgb); - using (var g = Graphics.FromImage(resultBitmap)) - { - g.DrawImage(capturedBitmap, 0, 0, capturedBitmap.Width, capturedBitmap.Height); - - var targetRect = new Rectangle(0, 0, Math.Min(sourceRect.Width, capturedBitmap.Width), Math.Min(sourceRect.Height, capturedBitmap.Height)); - g.DrawImage(inkOverlayBitmap, targetRect, sourceRect, GraphicsUnit.Pixel); - } - - return resultBitmap; - } - catch - { - resultBitmap?.Dispose(); - throw; - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"叠加截图墨迹失败: {ex.Message}", LogHelper.LogType.Warning); - return capturedBitmap; - } - } + => ScreenshotService.OverlayInkOnCapturedBitmap(capturedBitmap, captureArea, inkOverlayBitmapSource); + /// + /// 将 WPF BitmapSource 转换为 System.Drawing.Bitmap。 + /// 壳:本体已搬入 。 + /// private Bitmap ConvertBitmapSourceToBitmap(BitmapSource bitmapSource) - { - if (bitmapSource == null) - { - return null; - } - - using (var memoryStream = new MemoryStream()) - { - var encoder = new PngBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(bitmapSource)); - encoder.Save(memoryStream); - memoryStream.Position = 0; - using (var tempBitmap = new Bitmap(memoryStream)) - { - return new Bitmap(tempBitmap); - } - } - } + => ScreenshotService.ConvertBitmapSourceToBitmap(bitmapSource); /// /// 将截图插入到画布 @@ -845,302 +739,16 @@ private void CenterAndScaleScreenshot(Image image) /// 11. 重置裁剪区域,确保后续操作不受影响 /// private Bitmap ApplyShapeMask(Bitmap bitmap, List path, Rectangle area) - { - try - { - // 验证路径参数 - if (path == null || path.Count < 3) - { - LogHelper.WriteLogToFile("路径点数不足,无法应用形状遮罩", LogHelper.LogType.Warning); - return bitmap; - } - - // 获取DPI缩放比例 - var dpiScale = GetDpiScale(); - var virtualScreen = SystemInformation.VirtualScreen; - - // 创建结果位图,确保支持透明度 - var resultBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb); - - // 首先将整个位图设置为透明 - using (var resultGraphics = Graphics.FromImage(resultBitmap)) - { - // 清除位图,设置为完全透明 - resultGraphics.Clear(Color.Transparent); - - // 设置高质量渲染 - resultGraphics.SmoothingMode = SmoothingMode.AntiAlias; - resultGraphics.CompositingQuality = CompositingQuality.HighQuality; - resultGraphics.CompositingMode = CompositingMode.SourceOver; - - // 创建路径 - using (var pathGraphics = new GraphicsPath()) - { - // 转换WPF坐标到GDI+坐标,考虑DPI缩放和屏幕偏移 - var points = new PointF[path.Count]; - for (int i = 0; i < path.Count; i++) - { - // 将WPF坐标转换为实际屏幕坐标,然后相对于截图区域计算偏移 - double screenX = (path[i].X * dpiScale) + virtualScreen.Left; - double screenY = (path[i].Y * dpiScale) + virtualScreen.Top; - - // 计算相对于截图区域的坐标 - float relativeX = (float)(screenX - area.X); - float relativeY = (float)(screenY - area.Y); - - // 确保坐标在有效范围内 - relativeX = Math.Max(0, Math.Min(relativeX, bitmap.Width - 1)); - relativeY = Math.Max(0, Math.Min(relativeY, bitmap.Height - 1)); - - points[i] = new PointF(relativeX, relativeY); - } - - // 添加路径 - 使用FillMode.Winding确保路径正确填充 - pathGraphics.FillMode = FillMode.Winding; - pathGraphics.AddPolygon(points); - - // 验证路径是否有效 - if (!pathGraphics.IsVisible(0, 0) && pathGraphics.GetBounds().Width > 0 && pathGraphics.GetBounds().Height > 0) - { - // 设置裁剪区域为路径内部 - resultGraphics.SetClip(pathGraphics); - - // 在裁剪区域内绘制原始图像 - resultGraphics.DrawImage(bitmap, 0, 0); - - // 重置裁剪区域,确保后续操作不受影响 - resultGraphics.ResetClip(); - } - else - { - LogHelper.WriteLogToFile("生成的路径无效,返回透明图像", LogHelper.LogType.Warning); - // 如果路径无效,返回透明图像 - return resultBitmap; - } - } - } - - return resultBitmap; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用形状遮罩失败: {ex.Message}", LogHelper.LogType.Error); - return bitmap; - } - } + => _screenshotService.ApplyShapeMask(bitmap, path, area); /// - /// 将System.Drawing.Bitmap转换为WPF BitmapSource + /// 将System.Drawing.Bitmap转换为WPF BitmapSource。 + /// 壳:本体(含备用/最简两条降级路径)已搬入 。 /// /// 要转换的位图 /// 转换后的BitmapSource - /// - /// 该方法会: - /// 1. 验证位图有效性 - /// 2. 验证位图尺寸 - /// 3. 使用更安全的方法转换位图 - /// 4. 根据像素格式选择合适的WPF像素格式 - /// 5. 创建BitmapSource - /// 6. 冻结BitmapSource以提高性能 - /// 7. 如果转换失败,尝试使用备用方法 - /// private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap) - { - try - { - // 验证位图有效性 - if (bitmap == null) - return null; - - // 验证位图尺寸 - if (bitmap.Width <= 0 || bitmap.Height <= 0) - return null; - - // 使用更安全的方法转换位图 - var bitmapData = bitmap.LockBits( - new Rectangle(0, 0, bitmap.Width, bitmap.Height), - ImageLockMode.ReadOnly, - bitmap.PixelFormat); - - try - { - // 根据像素格式选择合适的WPF像素格式 - System.Windows.Media.PixelFormat wpfPixelFormat; - switch (bitmap.PixelFormat) - { - case PixelFormat.Format24bppRgb: - wpfPixelFormat = PixelFormats.Bgr24; - break; - case PixelFormat.Format32bppArgb: - wpfPixelFormat = PixelFormats.Bgra32; - break; - case PixelFormat.Format32bppRgb: - wpfPixelFormat = PixelFormats.Bgr32; - break; - default: - wpfPixelFormat = PixelFormats.Bgr24; - break; - } - - var bitmapSource = BitmapSource.Create( - bitmapData.Width, - bitmapData.Height, - bitmap.HorizontalResolution, - bitmap.VerticalResolution, - wpfPixelFormat, - null, - bitmapData.Scan0, - bitmapData.Stride * bitmapData.Height, - bitmapData.Stride); - - bitmapSource.Freeze(); - return bitmapSource; - } - finally - { - bitmap.UnlockBits(bitmapData); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"转换位图失败: {ex.Message}", LogHelper.LogType.Error); - - // 尝试使用备用方法:内存流转换 - try - { - return ConvertBitmapToBitmapSourceFallback(bitmap); - } - catch (Exception fallbackEx) - { - LogHelper.WriteLogToFile($"备用转换方法也失败: {fallbackEx.Message}", LogHelper.LogType.Error); - - // 最后尝试:使用最简单的转换方法 - try - { - return ConvertBitmapToBitmapSourceSimple(bitmap); - } - catch (Exception simpleEx) - { - LogHelper.WriteLogToFile($"简单转换方法也失败: {simpleEx.Message}", LogHelper.LogType.Error); - throw; - } - } - } - } - - /// - /// 备用的位图转换方法(使用内存流) - /// - /// 要转换的位图 - /// 转换后的BitmapSource - /// - /// 该方法会: - /// 1. 验证位图有效性 - /// 2. 创建一个新的位图,确保格式正确 - /// 3. 在内存流中保存为PNG格式 - /// 4. 创建BitmapImage并加载内存流中的数据 - /// 5. 冻结BitmapImage以提高性能 - /// - private BitmapSource ConvertBitmapToBitmapSourceFallback(Bitmap bitmap) - { - try - { - // 验证位图有效性 - if (bitmap == null || bitmap.Width <= 0 || bitmap.Height <= 0) - return null; - - // 创建一个新的位图,确保保留Alpha通道 - using (var convertedBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb)) - { - using (var graphics = Graphics.FromImage(convertedBitmap)) - { - graphics.CompositingMode = CompositingMode.SourceCopy; - graphics.DrawImage(bitmap, 0, 0); - } - - using (var memory = new MemoryStream()) - { - convertedBitmap.Save(memory, ImageFormat.Png); - memory.Position = 0; - - var bitmapImage = new BitmapImage(); - bitmapImage.BeginInit(); - bitmapImage.CacheOption = BitmapCacheOption.OnLoad; - bitmapImage.StreamSource = memory; - bitmapImage.EndInit(); - bitmapImage.Freeze(); - - return bitmapImage; - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"备用转换方法失败: {ex.Message}", LogHelper.LogType.Error); - throw; - } - } - - /// - /// 最简单的位图转换方法 - /// - /// 要转换的位图 - /// 转换后的BitmapSource - /// - /// 该方法会: - /// 1. 验证位图有效性 - /// 2. 使用最基础的方法:直接保存为PNG然后加载 - /// 3. 创建临时文件 - /// 4. 将位图保存为PNG格式到临时文件 - /// 5. 创建BitmapImage并加载临时文件 - /// 6. 冻结BitmapImage以提高性能 - /// 7. 清理临时文件 - /// - private BitmapSource ConvertBitmapToBitmapSourceSimple(Bitmap bitmap) - { - try - { - if (bitmap == null) - return null; - - // 使用最基础的方法:直接保存为PNG然后加载 - var tempFile = Path.GetTempFileName() + ".png"; - - try - { - bitmap.Save(tempFile, ImageFormat.Png); - - var bitmapImage = new BitmapImage(); - bitmapImage.BeginInit(); - bitmapImage.UriSource = new Uri(tempFile); - bitmapImage.CacheOption = BitmapCacheOption.OnLoad; - bitmapImage.EndInit(); - bitmapImage.Freeze(); - - return bitmapImage; - } - finally - { - // 清理临时文件 - try - { - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - } - catch (Exception deleteEx) - { - LogHelper.WriteLogToFile($"删除临时文件失败: {deleteEx.Message}", LogHelper.LogType.Warning); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"简单转换方法失败: {ex.Message}", LogHelper.LogType.Error); - throw; - } - } + => ScreenshotService.ConvertBitmapToBitmapSource(bitmap); /// /// 获取DPI缩放比例 diff --git a/Ink Canvas/MainWindow_cs/MW_LiquidGlassBar.cs b/Ink Canvas/MainWindow_cs/MW_LiquidGlassBar.cs index 5271ca477..700ccc9d9 100644 --- a/Ink Canvas/MainWindow_cs/MW_LiquidGlassBar.cs +++ b/Ink Canvas/MainWindow_cs/MW_LiquidGlassBar.cs @@ -1,4 +1,5 @@ using Ink_Canvas.Helpers; +using Ink_Canvas.Models; using Ink_Canvas.Windows.SettingsViews.Helpers; using System; using System.Windows; @@ -176,7 +177,7 @@ internal void RefreshLiquidGlassBarActiveState() try { - _liquidGlassBar.SyncActiveState(_currentToolMode, penType, drawingAttributes?.Color); + _liquidGlassBar.SyncActiveState(ToolModeMapping.ToInternalString(_currentToolModeEnum), penType, drawingAttributes?.Color); } catch (Exception ex) { diff --git a/Ink Canvas/MainWindow_cs/MW_Notification.cs b/Ink Canvas/MainWindow_cs/MW_Notification.cs index 334008f12..b3f4ef5fb 100644 --- a/Ink Canvas/MainWindow_cs/MW_Notification.cs +++ b/Ink Canvas/MainWindow_cs/MW_Notification.cs @@ -1,21 +1,15 @@ using Ink_Canvas.Helpers; using Ink_Canvas.Models; -using Ink_Canvas.Properties; -using Ink_Canvas.Windows; -using System; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Threading; namespace Ink_Canvas { + /// + /// 通知转发壳:实现已整体搬至 , + /// 本文件仅保留对外入口签名,方法体为一行转发。 + /// 通知队列与去重逻辑由 (纯逻辑静态类)承载。 + /// public partial class MainWindow : Ink_Canvas.Helpers.PerformanceTransparentWin { - private int lastNotificationShowTime; - private int notificationShowTime = 2500; - private bool _startupUnreadNotificationShown; - public static void ShowNewMessage(string notice, bool isShowImmediately = true) { NotificationCenterService.EnqueueText(notice, NotificationMessageLevel.Normal, 3); @@ -23,289 +17,22 @@ public static void ShowNewMessage(string notice, bool isShowImmediately = true) public void ShowNotification(string notice, bool isShowImmediately = true) { - NotificationCenterService.EnqueueText(notice, NotificationMessageLevel.Normal, Math.Max(1, notificationShowTime / 1000)); + _notificationService.ShowText(notice); } public void ShowPPTModePromptNotification() { - if (Settings?.PowerPointSettings?.ShowPPTModePrompt != true) return; - - NotificationCenterService.Enqueue(new NotificationMessage - { - Id = "ppt-mode-prompt-" + Guid.NewGuid().ToString("N"), - Type = NotificationMessageType.Reminder, - Level = NotificationMessageLevel.Normal, - Title = PPTStrings.PPT_ModePrompt_Title, - Summary = PPTStrings.PPT_ModePrompt_Message, - Icon = "Info", - DisplaySeconds = 4, - Priority = 20, - Source = "ppt-mode-prompt", - ProviderId = "local" - }); + _notificationService.ShowPptModePromptNotification(); } private void InitializeNotificationProviders() { - if (DynamicNotification != null) - { - DynamicNotification.Closed -= DynamicNotification_Closed; - DynamicNotification.Closed += DynamicNotification_Closed; - } - - NotificationCenterService.NotificationRequested -= NotificationCenterService_NotificationRequested; - NotificationCenterService.NotificationRequested += NotificationCenterService_NotificationRequested; - - if (_announcementService == null && Settings?.Notification?.IsAnnouncementEnabled == true) - { - _announcementService = new AnnouncementService(Settings); - - AnnouncementService.UnreadCountChanged -= OnAnnouncementUnreadCountChanged; - AnnouncementService.UnreadCountChanged += OnAnnouncementUnreadCountChanged; - - Dispatcher.BeginInvoke(new Action(async () => - { - try - { - await Task.Delay(TimeSpan.FromSeconds(3), _notificationProviderCancellation.Token); - await _announcementService.StartAsync(_notificationProviderCancellation.Token); - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"公告通知提供商启动失败: {ex.Message}", LogHelper.LogType.Warning); - } - }), DispatcherPriority.ContextIdle); - } - - LogHelper.WriteLogToFile( - $"[Notification] 通知提供商已初始化: 公告已启用={Settings?.Notification?.IsAnnouncementEnabled == true}, 公告服务实例={_announcementService != null}", - LogHelper.LogType.Info); + _notificationService.InitializeProviders(); } - /// - /// 拆除通知控件上挂的插件 Action 回调。插件热重载时由 - /// 调用:当前显示的通知若来自该插件,currentMessage.Action 直接指向插件 ALC, - /// 留着会阻止热重载时被回收。 - /// internal void DetachPluginNotificationAction(string pluginId) { - if (string.IsNullOrEmpty(pluginId)) return; - try - { - DynamicNotification?.DetachPluginActionIfMatches(pluginId); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"摘除插件通知 Action 失败: {ex.Message}", LogHelper.LogType.Warning); - } - } - - private void NotificationCenterService_NotificationRequested(NotificationMessage message) - { - Dispatcher.BeginInvoke(new Action(() => - { - try - { - if (IsNotificationSuppressedByDictationDoNotDisturb()) - { - NotificationCenterService.NotifyCurrentClosed(); - return; - } - - if (Settings?.Notification?.IsWindowsToastEnabled == true) - { - WindowsNotificationHelper.ShowToast(message); - } - - if (Settings?.Notification?.IsDynamicNotificationEnabled == true && DynamicNotification != null) - { - DynamicNotification.RefreshTheme(IsCurrentThemeDark()); - ApplyDynamicNotificationPlacement(message); - DynamicNotification.Show(message); - } - else - { - ShowLegacyNotification(message.Title, message.DisplaySeconds); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"灵动通知显示失败: {ex.Message}", LogHelper.LogType.Error); - NotificationCenterService.NotifyCurrentClosed(); - } - })); - } - - private bool IsNotificationSuppressedByDictationDoNotDisturb() - { - var notification = Settings?.Notification; - if (notification?.IsDictationDoNotDisturbEnabled != true) return false; - - if (notification.IsDictationDoNotDisturbInPPTEnabled && IsInPPTPresentationMode) - { - return true; - } - - return notification.IsDictationDoNotDisturbInWhiteboardEnabled && currentMode == 1; - } - - private void DynamicNotification_Closed(object sender, EventArgs e) - { - NotificationCenterService.NotifyCurrentClosed(); - } - - private void ApplyDynamicNotificationPlacement(NotificationMessage message = null) - { - if (DynamicNotification == null) return; - - DynamicNotification.HorizontalAlignment = HorizontalAlignment.Center; - DynamicNotification.VerticalAlignment = VerticalAlignment.Top; - DynamicNotification.Margin = new Thickness(0); - - if (message?.Source == "ppt-mode-prompt") - { - ApplyDynamicNotificationFloatingBarPlacement(); - return; - } - - switch (Settings?.Notification?.Placement) - { - case "TopLeft": - DynamicNotification.HorizontalAlignment = HorizontalAlignment.Left; - DynamicNotification.Margin = new Thickness(16, 0, 0, 0); - break; - case "TopRight": - DynamicNotification.HorizontalAlignment = HorizontalAlignment.Right; - DynamicNotification.Margin = new Thickness(0, 0, 16, 0); - break; - case "FloatingBarAbove": - ApplyDynamicNotificationFloatingBarPlacement(); - break; - } - } - - private void ApplyDynamicNotificationFloatingBarPlacement() - { - // 收纳或收纳动画期间浮动栏已移出可用区域,保留默认的顶部居中位置,避免通知跟随到屏幕外。 - if (DynamicNotification == null || ViewboxFloatingBar == null || - ViewboxFloatingBar.Visibility != Visibility.Visible || - isFloatingBarFolded || isFloatingBarChangingHideMode) - { - return; - } - - try - { - var position = ViewboxFloatingBar.TransformToAncestor(this).Transform(new Point(0, 0)); - double notificationWidth = DynamicNotification.ActualWidth > 0 ? DynamicNotification.ActualWidth : DynamicNotification.Width; - double notificationHeight = DynamicNotification.ActualHeight > 0 ? DynamicNotification.ActualHeight : 72; - double floatingBarWidth = ViewboxFloatingBar.ActualWidth; - double left = position.X + floatingBarWidth / 2 - notificationWidth / 2; - double top = position.Y - notificationHeight - 12; - - left = Math.Max(12, Math.Min(ActualWidth - notificationWidth - 12, left)); - top = Math.Max(12, top); - - DynamicNotification.HorizontalAlignment = HorizontalAlignment.Left; - DynamicNotification.VerticalAlignment = VerticalAlignment.Top; - DynamicNotification.Margin = new Thickness(left, top, 0, 0); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Notification] 计算通知悬浮栏位置失败: {ex.Message}", LogHelper.LogType.Info); - } - } - - private void OnAnnouncementUnreadCountChanged() - { - if (_startupUnreadNotificationShown) return; - - Dispatcher.BeginInvoke(new Action(() => - { - if (_startupUnreadNotificationShown) return; - ShowStartupUnreadNotification(); - })); - } - - private void ShowStartupUnreadNotification() - { - try - { - if (_startupUnreadNotificationShown) return; - _startupUnreadNotificationShown = true; - - var count = AnnouncementService.GetUnreadCount(Settings); - if (count <= 0) return; - - NotificationCenterService.Enqueue(new NotificationMessage - { - Id = "startup-unread-announcements", - Type = NotificationMessageType.Reminder, - Level = NotificationMessageLevel.Normal, - Title = AnnouncementStrings.StartupUnreadTitle, - Summary = string.Format(AnnouncementStrings.StartupUnreadSummary, count), - ActionText = AnnouncementStrings.StartupUnreadAction, - Icon = "Info", - DisplaySeconds = 8, - Priority = 50, - Source = "startup-unread", - ProviderId = "announcement", - Action = () => - { - try - { - var window = new AnnouncementCenterWindow { Owner = this }; - window.Show(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"打开公告中心失败: {ex.Message}", LogHelper.LogType.Warning); - } - } - }); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启动未读公告通知失败: {ex.Message}", LogHelper.LogType.Warning); - } - } - - private void ShowLegacyNotification(string notice, int displaySeconds) - { - try - { - if (TextBlockNotice == null || GridNotifications == null) - { - NotificationCenterService.NotifyCurrentClosed(); - return; - } - - lastNotificationShowTime = Environment.TickCount; - notificationShowTime = Math.Max(1, displaySeconds) * 1000; - TextBlockNotice.Text = notice; - AnimationsHelper.ShowWithSlideFromBottomAndFade(GridNotifications); - - new Thread(() => - { - Thread.Sleep(notificationShowTime + 300); - if (Environment.TickCount - lastNotificationShowTime >= notificationShowTime) - { - Application.Current.Dispatcher.Invoke(() => - { - AnimationsHelper.HideWithSlideAndFade(GridNotifications); - NotificationCenterService.NotifyCurrentClosed(); - }); - } - }).Start(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"ShowNotification 异常: {ex.Message}", LogHelper.LogType.Error); - NotificationCenterService.NotifyCurrentClosed(); - } + _notificationService.DetachPluginNotificationAction(pluginId); } } } diff --git a/Ink Canvas/MainWindow_cs/MW_PPT.cs b/Ink Canvas/MainWindow_cs/MW_PPT.cs index add2a3015..06cdfefb0 100644 --- a/Ink Canvas/MainWindow_cs/MW_PPT.cs +++ b/Ink Canvas/MainWindow_cs/MW_PPT.cs @@ -363,7 +363,7 @@ public void StopPPTMonitoring() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 停止监控时清理延迟退出定时器/预览缓存失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } _pptManager?.StopMonitoring(); @@ -885,7 +885,7 @@ internal void StopPPTOnlyVisibilityProbeTimer() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 停止「仅PPT模式」可见性探测定时器失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -928,8 +928,7 @@ internal bool IsPowerPointSlideshowSurfacePresentWin32() } catch (Exception ex) { - // 枚举回调内:拿不到进程名就可能漏判放映窗口 - LogHelper.WriteLogToFile($"[PPT] 读取 POWERPNT 进程信息失败,放映窗口检测可能漏判: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return true; @@ -1111,7 +1110,7 @@ private void OnPPTPresentationClose(object payload) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 处理演示文稿关闭事件时发生非 COM 异常: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -1317,7 +1316,7 @@ await Application.Current.Dispatcher.InvokeAsync(async () => } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 放映开始时初始化演示文稿墨迹管理器失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -1688,10 +1687,7 @@ private List GetVideoRegionsViaCom() MediaType = (int)shape.MediaType }); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[SmartMode] 读取形状视频区域信息失败,该形状会被跳过: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } return regions; } @@ -1785,10 +1781,7 @@ private static bool IsVideoShape(Microsoft.Office.Interop.PowerPoint.Shape shape return true; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[SmartMode] 读取 OLE 控件 ProgID 失败,将按非视频处理: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } // 无法确认是否为媒体播放器时,不视为视频,避免把普通 ActiveX 控件误判为视频 return false; } @@ -1801,16 +1794,10 @@ private static bool IsVideoShape(Microsoft.Office.Interop.PowerPoint.Shape shape if ((int)(object)shape.MediaType == 3) // ppMediaTypeMovie return true; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[SmartMode] 读取嵌入式 OLE 形状 MediaType 失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[SmartMode] 判定形状是否为视频时发生异常: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } return false; } @@ -2802,8 +2789,9 @@ private async Task OnPPTNavBarPageClickAsync(Controls.PPTNavBar bar) ViewboxFloatingBarMarginAnimation(60); } } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { + LogService.LogException(ex); } catch (Exception ex) { @@ -3111,7 +3099,7 @@ private void ResetPPTEnhancedPreviewCache() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 取消增强预览构建任务失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } try @@ -3120,7 +3108,7 @@ private void ResetPPTEnhancedPreviewCache() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 释放增强预览取消令牌失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } DisposePPTEnhancedPreviewItems(cacheToDispose); @@ -3175,8 +3163,9 @@ await Application.Current.Dispatcher.InvokeAsync(() => LogHelper.WriteLogToFile($"PPT enhanced preview preloaded {slides.Count} thumbnails.", LogHelper.LogType.Trace); } } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { + LogService.LogException(ex); } catch (Exception ex) { diff --git a/Ink Canvas/MainWindow_cs/MW_PluginCanvasExtensions.cs b/Ink Canvas/MainWindow_cs/MW_PluginCanvasExtensions.cs index 1124e918c..11de95c10 100644 --- a/Ink Canvas/MainWindow_cs/MW_PluginCanvasExtensions.cs +++ b/Ink Canvas/MainWindow_cs/MW_PluginCanvasExtensions.cs @@ -1,4 +1,5 @@ using Ink_Canvas.Helpers; +using Ink_Canvas.Models; using Ink_Canvas.Plugins; using System; using System.Collections.Generic; @@ -19,7 +20,7 @@ public partial class MainWindow new HashSet(StringComparer.OrdinalIgnoreCase); private PluginCanvasToolSession _activePluginCanvasToolSession; - private string _pluginToolPreviousLogicalMode; + private ToolMode _pluginToolPreviousLogicalMode; private bool _pluginCanvasToolActive; internal bool IsPluginCanvasToolActive => _pluginCanvasToolActive; @@ -164,7 +165,7 @@ internal bool TryActivatePluginCanvasTool( EndPluginCanvasTool(_activePluginCanvasToolSession); } - _pluginToolPreviousLogicalMode = _currentToolMode; + _pluginToolPreviousLogicalMode = _currentToolModeEnum; _pluginCanvasToolActive = true; // 插件画布工具接管输入前取消 WinRT 墨迹管线在途湿墨(如启用), // 并以 WPF 实时墨迹的等价取消兜底,避免残留半截笔迹。 @@ -235,26 +236,26 @@ private void EndPluginCanvasTool(PluginCanvasToolSession session) _pluginCanvasToolActive = false; RestoreToolModeAfterPluginSession(_pluginToolPreviousLogicalMode); - _pluginToolPreviousLogicalMode = null; + _pluginToolPreviousLogicalMode = ToolMode.Unknown; }); } - private void RestoreToolModeAfterPluginSession(string mode) + private void RestoreToolModeAfterPluginSession(ToolMode mode) { var editingMode = InkCanvasEditingMode.None; switch (mode) { - case "pen": - case "color": + case ToolMode.Pen: + case ToolMode.Color: editingMode = InkCanvasEditingMode.Ink; break; - case "select": + case ToolMode.Select: editingMode = InkCanvasEditingMode.Select; break; - case "eraser": + case ToolMode.Eraser: editingMode = InkCanvasEditingMode.EraseByPoint; break; - case "eraserByStrokes": + case ToolMode.EraserByStrokes: editingMode = InkCanvasEditingMode.EraseByStroke; break; } @@ -262,10 +263,10 @@ private void RestoreToolModeAfterPluginSession(string mode) if (!SetCurrentToolMode(editingMode)) { SetCurrentToolMode(InkCanvasEditingMode.None); - mode = "cursor"; + mode = ToolMode.Cursor; } - UpdateCurrentToolMode(string.IsNullOrWhiteSpace(mode) ? "cursor" : mode); + UpdateCurrentToolMode(mode == ToolMode.Unknown ? "cursor" : ToolModeMapping.ToInternalString(mode)); } private void AttachPluginCanvasToolInput() diff --git a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs b/Ink Canvas/MainWindow_cs/MW_SaveAndOpenStrokes.cs similarity index 98% rename from Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs rename to Ink Canvas/MainWindow_cs/MW_SaveAndOpenStrokes.cs index ff034d72d..f2414bd83 100644 --- a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs +++ b/Ink Canvas/MainWindow_cs/MW_SaveAndOpenStrokes.cs @@ -598,7 +598,7 @@ public void SaveInkCanvasStrokes(bool newNotice = true, bool saveByUser = false) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Settings] 异步上传分页 XML 墨迹文件失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } }); } @@ -779,7 +779,7 @@ public void SaveInkCanvasStrokes(bool newNotice = true, bool saveByUser = false) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Settings] 异步上传分页 icstk 墨迹文件失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } }); } @@ -835,11 +835,7 @@ public void SaveInkCanvasStrokes(bool newNotice = true, bool saveByUser = false) } catch { - try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 原子写入墨迹文件失败后删除临时文件 {Path.GetFileName(tmpPath)} 失败: {ex.Message}", LogHelper.LogType.Info); - } + try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch (Exception ex) { LogService.LogException(ex); } throw; } SavePluginPageDocumentSidecar(savePathWithName, CurrentWhiteboardIndex); @@ -865,7 +861,7 @@ public void SaveInkCanvasStrokes(bool newNotice = true, bool saveByUser = false) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Settings] 异步上传单页墨迹文件失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } }); @@ -936,7 +932,7 @@ from point in stroke.StylusPoints } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Settings] 异步上传 XML 墨迹文件失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } }); } @@ -1031,7 +1027,7 @@ private void SaveMultiPageStrokesAsXMLZip(List allPageStrokes, } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Settings] 异步上传多页 XML 压缩包失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } }); @@ -1144,7 +1140,7 @@ private void SaveMultiPageStrokesAsZip(List allPageStrokes, st } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Settings] 异步上传多页墨迹压缩包失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } }); @@ -1251,7 +1247,7 @@ private void SaveSinglePageStrokesAsImage(string savePathWithName, bool newNotic } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Screenshot] 异步上传带墨迹截图 PNG 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } }); } // using imgBitmap diff --git a/Ink Canvas/MainWindow_cs/MW_Screenshot.cs b/Ink Canvas/MainWindow_cs/MW_Screenshot.cs index 794a5d15a..b66e6fe0a 100644 --- a/Ink Canvas/MainWindow_cs/MW_Screenshot.cs +++ b/Ink Canvas/MainWindow_cs/MW_Screenshot.cs @@ -1,14 +1,9 @@ -using Ink_Canvas.Helpers; +using Ink_Canvas.Services.Shell; using System; using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; using System.IO; -using System.Linq; using System.Threading.Tasks; using System.Windows; -using System.Windows.Forms; -using System.Windows.Ink; using System.Windows.Media.Imaging; namespace Ink_Canvas @@ -18,147 +13,28 @@ public partial class MainWindow : Ink_Canvas.Helpers.PerformanceTransparentWin /// /// 在切页/加页场景下使用:先捕获当前画面到内存并克隆墨迹,然后立即返回;截图与墨迹保存在后台异步执行,不阻塞切页。 /// 调用方应在调用本方法后立即执行 SaveStrokes、ClearStrokes、切页、RestoreStrokes 等逻辑。 + /// 壳:本体已搬入 。 /// /// 是否隐藏保存成功通知 /// 截图文件名(可选) private void CaptureAndEnqueueScreenshotSave(bool isHideNotification, string fileName = null) - { - var savePath = Settings.Automation.IsSaveScreenshotsInDateFolders - ? GetDateFolderPath(fileName) - : GetDefaultFolderPath(); - - System.Drawing.Bitmap bitmap = null; - StrokeCollection strokesToSave = null; - int pageIndexForStrokes = 0; - string strokeSavePath = null; - - try - { - bitmap = CaptureScreenshotToBitmap(); - if (bitmap == null) return; - - if (Settings.Automation.IsAutoSaveStrokesAtScreenshot && inkCanvas.Strokes.Count > 0) - { - strokesToSave = inkCanvas.Strokes.Clone(); - pageIndexForStrokes = CurrentWhiteboardIndex; - var basePath = Settings.Automation.AutoSavedStrokesLocation - + @"\Auto Saved - BlackBoard Strokes"; - if (!Directory.Exists(basePath)) Directory.CreateDirectory(basePath); - string stem; - if (Settings.Automation.IsUseCustomSaveFileName) - { - stem = SaveFileNameHelper.Render(Settings.Automation.CustomSaveFileNameTemplate, - new SaveFileNameContext - { - Mode = "BlackBoard", - Type = "Auto", - Page = pageIndexForStrokes, - Count = strokesToSave.Count - }); - } - else - { - stem = $"{DateTime.Now:yyyy-MM-dd HH-mm-ss-fff} Page-{pageIndexForStrokes} StrokesCount-{strokesToSave.Count}"; - } - strokeSavePath = Path.Combine(basePath, stem + ".icstk"); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"CaptureAndEnqueueScreenshotSave 捕获失败: {ex}", LogHelper.LogType.Error); - bitmap?.Dispose(); - return; - } - - var bitmapToSave = bitmap; - var path = savePath; - var hideNotification = isHideNotification; - - _ = Task.Run(async () => - { - try - { - if (bitmapToSave != null) - { - var directory = Path.GetDirectoryName(path); - if (!Directory.Exists(directory)) - Directory.CreateDirectory(directory); - Helpers.ScreenshotImageSaveHelper.Save(bitmapToSave, path); - bitmapToSave.Dispose(); - } - - if (strokesToSave != null && !string.IsNullOrEmpty(strokeSavePath)) - { - // 原子写:tmp + File.Replace/Move,避免 FileMode.Create 直接截断后 - // 写入中途失败(磁盘满/进程被杀)让 .icstk 停在 0 字节。 - var tmpStrokePath = strokeSavePath + ".tmp"; - try - { - using (var fs = new FileStream(tmpStrokePath, FileMode.Create)) - { - strokesToSave.Save(fs); - } - if (File.Exists(strokeSavePath)) - File.Replace(tmpStrokePath, strokeSavePath, null); - else - File.Move(tmpStrokePath, strokeSavePath); - } - catch - { - try { if (File.Exists(tmpStrokePath)) File.Delete(tmpStrokePath); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Screenshot] 清理临时笔画文件失败: {ex.Message}", LogHelper.LogType.Info); - } - throw; - } - } - - if (!hideNotification && !string.IsNullOrEmpty(path)) - { - Dispatcher.Invoke(() => ShowNotification(string.Format(Properties.MainWindowStrings.Main_Screenshot_SaveSuccess, path))); - } - - // 使用上传帮助类上传到所有启用的服务 - await Helpers.UploadHelper.UploadFileAsync(path); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"后台保存截图/墨迹失败: {ex}", LogHelper.LogType.Error); - bitmapToSave?.Dispose(); - } - }); - } - - /// - /// 将当前屏幕内容捕获为位图(仅内存,不写文件)。调用方或后台任务负责 Dispose。 - /// - private System.Drawing.Bitmap CaptureScreenshotToBitmap() - { - var rc = SystemInformation.VirtualScreen; - var bitmap = new System.Drawing.Bitmap(rc.Width, rc.Height, PixelFormat.Format32bppArgb); - using (var memoryGraphics = Graphics.FromImage(bitmap)) - { - memoryGraphics.CompositingQuality = CompositingQuality.HighQuality; - memoryGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - memoryGraphics.SmoothingMode = SmoothingMode.HighQuality; - memoryGraphics.CompositingMode = CompositingMode.SourceOver; - memoryGraphics.CopyFromScreen(rc.X, rc.Y, 0, 0, rc.Size, CopyPixelOperation.SourceCopy); - } - return bitmap; - } + => _screenshotService.CaptureAndEnqueueScreenshotSave(isHideNotification, fileName); /// /// 供插件截图服务调用的全屏捕获入口(调用方负责 Dispose 返回值)。 + /// 壳:本体已搬入 。 /// - internal System.Drawing.Bitmap CapturePluginFullScreen() => CaptureScreenshotToBitmap(); + internal System.Drawing.Bitmap CapturePluginFullScreen() => ScreenshotService.CaptureScreenshotToBitmap(); /// /// 供插件截图服务调用的区域捕获入口(调用方负责 Dispose 返回值)。 + /// 壳:本体已搬入 。 /// - internal System.Drawing.Bitmap CapturePluginScreenArea(Rectangle area) => CaptureScreenArea(area); + internal System.Drawing.Bitmap CapturePluginScreenArea(Rectangle area) => ScreenshotService.CaptureScreenArea(area); /// /// 保存截图 + /// 壳:本体已搬入 。 /// /// 是否隐藏通知 /// 文件名 @@ -169,63 +45,11 @@ private System.Drawing.Bitmap CaptureScreenshotToBitmap() /// 3. 如果设置了自动保存墨迹,调用SaveInkCanvasStrokes方法保存墨迹 /// private void SaveScreenShot(bool isHideNotification, string fileName = null) - { - var savePath = Settings.Automation.IsSaveScreenshotsInDateFolders - ? GetDateFolderPath(fileName) - : GetDefaultFolderPath(); - - CaptureAndSaveScreenshot(savePath, isHideNotification); - - if (Settings.Automation.IsAutoSaveStrokesAtScreenshot) - SaveInkCanvasStrokes(false); - } - - /// - /// 获取截图保存目录。 - /// 仅当"截图保存到指定位置"开关开启时使用自定义位置,否则回退到桌面。 - /// - private string GetScreenshotSaveDirectory() - { - if (Settings.Automation.IsSaveScreenshotToCustomLocation) - { - var location = Settings.Automation.ScreenshotSaveLocation; - if (!string.IsNullOrWhiteSpace(location)) - return location; - } - return Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); - } - - /// - /// 将截图写入剪贴板,同时提供 PNG 流、Bitmap、FileDrop 三种格式。 - /// 仅写入 Bitmap 格式时 Win+V 剪贴板历史不收录、资源管理器右键也不能粘贴为文件, - /// 故用 DataObject 多格式写入: - /// - PNG 流:Win+V 历史 / 现代应用可识别 - /// - Bitmap:老式应用兼容 - /// - FileDrop:资源管理器右键可粘贴为 .png 文件(需传入已保存的文件路径) - /// 注意:带上 FileDrop 后,部分聊天软件可能优先把贴图当作发送文件处理。 - /// - private static void CopyImageToClipboard(BitmapSource image, string filePath = null) - { - if (image == null) return; - - var pngStream = new MemoryStream(); - var encoder = new PngBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(image)); - encoder.Save(pngStream); - pngStream.Position = 0; - - var data = new System.Windows.DataObject(); - data.SetData("PNG", pngStream); // Win+V 历史 / 现代应用 - data.SetData(System.Windows.DataFormats.Bitmap, image); // 老式 Bitmap 兼容 - if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) - { - data.SetData(System.Windows.DataFormats.FileDrop, new[] { filePath }); // 资源管理器右键粘贴为文件 - } - System.Windows.Clipboard.SetDataObject(data, true); - } + => _screenshotService.SaveScreenShot(isHideNotification, fileName); /// /// 保存截图到配置的保存目录 + /// 壳:本体已搬入 。 /// /// /// 该方法会: @@ -234,17 +58,7 @@ private static void CopyImageToClipboard(BitmapSource image, string filePath = n /// 3. 如果设置了截图后复制到剪贴板,将截图复制到剪贴板 /// 4. 如果设置了自动保存墨迹,调用SaveInkCanvasStrokes方法保存墨迹 /// - internal void SaveScreenShotToDesktop() - { - var savePath = Path.Combine( - GetScreenshotSaveDirectory(), - $"{GetScreenshotFileNameStem()}.png"); - - CaptureAndSaveScreenshot(savePath, false, Settings.Automation.IsCopyScreenshotToClipboard); - - if (Settings.Automation.IsAutoSaveStrokesAtScreenshot) - SaveInkCanvasStrokes(false); - } + internal void SaveScreenShotToDesktop() => _screenshotService.SaveScreenShotToDesktop(); internal async Task SaveAreaScreenShotToDesktop() { @@ -276,76 +90,19 @@ internal async Task SaveAreaScreenShotToDesktop() } var savePath = Path.Combine( - GetScreenshotSaveDirectory(), - $"{GetScreenshotFileNameStem()}.png"); + _screenshotService.GetScreenshotSaveDirectory(), + $"{_screenshotService.GetScreenshotFileNameStem()}.png"); - using (var originalBitmap = CaptureScreenArea(screenshotResult.Value.Area)) + // 执行段(捕获→叠墨→遮罩→落盘→剪贴板)已搬入 ScreenshotService; + // 成功/失败通知保持原有相对顺序:落盘(+剪贴板)之后、自动保存墨迹之前。 + if (!_screenshotService.SaveAreaScreenshot(screenshotResult.Value, savePath, Settings.Automation.IsCopyScreenshotToClipboard)) { - if (originalBitmap == null) - { - ShowNotification(Properties.MainWindowStrings.Main_Screenshot_Failed); - return; - } - - Bitmap finalBitmap = originalBitmap; - bool needDisposeFinalBitmap = false; - - try - { - if (screenshotResult.Value.IncludeInk && screenshotResult.Value.InkOverlayBitmapSource != null) - { - var withInkBitmap = OverlayInkOnCapturedBitmap(finalBitmap, screenshotResult.Value.Area, screenshotResult.Value.InkOverlayBitmapSource); - if (withInkBitmap != null && withInkBitmap != finalBitmap) - { - if (needDisposeFinalBitmap && finalBitmap != originalBitmap) - { - finalBitmap.Dispose(); - } - finalBitmap = withInkBitmap; - needDisposeFinalBitmap = true; - } - } - - if (screenshotResult.Value.Path != null && screenshotResult.Value.Path.Count > 0) - { - var maskedBitmap = ApplyShapeMask(finalBitmap, screenshotResult.Value.Path, screenshotResult.Value.Area); - if (maskedBitmap != null && maskedBitmap != finalBitmap) - { - if (needDisposeFinalBitmap && finalBitmap != originalBitmap) - { - finalBitmap.Dispose(); - } - finalBitmap = maskedBitmap; - needDisposeFinalBitmap = true; - } - } - - var directory = Path.GetDirectoryName(savePath); - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - finalBitmap.Save(savePath, ImageFormat.Png); - - // 截图后复制到剪贴板 - if (Settings.Automation.IsCopyScreenshotToClipboard) - { - try { CopyImageToClipboard(ConvertBitmapToBitmapSource(finalBitmap), savePath); } - catch (Exception ex) { LogHelper.WriteLogToFile($"截图复制到剪贴板失败: {ex}", LogHelper.LogType.Warning); } - } - - ShowNotification(string.Format(Properties.MainWindowStrings.Main_Screenshot_SaveSuccess, savePath)); - } - finally - { - if (needDisposeFinalBitmap && finalBitmap != originalBitmap) - { - finalBitmap.Dispose(); - } - } + ShowNotification(Properties.MainWindowStrings.Main_Screenshot_Failed); + return; } + ShowNotification(string.Format(Properties.MainWindowStrings.Main_Screenshot_SaveSuccess, savePath)); + if (Settings.Automation.IsAutoSaveStrokesAtScreenshot) SaveInkCanvasStrokes(false); } @@ -454,172 +211,5 @@ private async Task AddScreenshotToNewWhiteboardPage(ScreenshotResult screenshotR await InsertBitmapSourceToCanvas(bitmapSourceForClipboard); } - - /// - /// 提取公共的截图和保存逻辑 - /// - /// 保存路径 - /// 是否隐藏通知 - /// 是否在保存后复制到剪贴板 - /// - /// 该方法会: - /// 1. 获取虚拟屏幕边界 - /// 2. 创建位图并设置高质量渲染 - /// 3. 从屏幕复制内容到位图 - /// 4. 确保保存目录存在 - /// 5. 保存为PNG格式 - /// 6. 若 copyToClipboard 为真,将截图复制到剪贴板 - /// 7. 如果不隐藏通知,显示保存成功通知 - /// 8. 异步上传截图到Dlass - /// - private void CaptureAndSaveScreenshot(string savePath, bool isHideNotification, bool copyToClipboard = false) - { - var rc = SystemInformation.VirtualScreen; - - using (var bitmap = new Bitmap(rc.Width, rc.Height, PixelFormat.Format32bppArgb)) - using (var memoryGraphics = Graphics.FromImage(bitmap)) - { - // 设置高质量渲染 - memoryGraphics.CompositingQuality = CompositingQuality.HighQuality; - memoryGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - memoryGraphics.SmoothingMode = SmoothingMode.HighQuality; - memoryGraphics.CompositingMode = CompositingMode.SourceOver; - - memoryGraphics.CopyFromScreen(rc.X, rc.Y, 0, 0, rc.Size, CopyPixelOperation.SourceCopy); - - // 确保目录存在 - var directory = Path.GetDirectoryName(savePath); - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - // 使用PNG格式保存,确保透明度信息不丢失 - bitmap.Save(savePath, ImageFormat.Png); - LogHelper.WriteLogToFile( - $"[Screenshot] 截图已保存: {savePath} ({rc.Width}x{rc.Height}), 复制到剪贴板={copyToClipboard}", - LogHelper.LogType.Info); - - // 截图后复制到剪贴板 - if (copyToClipboard) - { - try { CopyImageToClipboard(ConvertBitmapToBitmapSource(bitmap), savePath); } - catch (Exception ex) { LogHelper.WriteLogToFile($"截图复制到剪贴板失败: {ex}", LogHelper.LogType.Warning); } - } - } - - if (!isHideNotification) - { - Task.Delay(100).ContinueWith(t => - { - Dispatcher.Invoke(() => - { - ShowNotification(string.Format(Properties.MainWindowStrings.Main_Screenshot_SaveSuccess, savePath)); - }); - }); - } - _ = Task.Run(async () => - { - try - { - // 使用上传帮助类上传到所有启用的服务 - await Helpers.UploadHelper.UploadFileAsync(savePath); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Screenshot] 截图上传失败(本地文件已保存): {ex.Message}", LogHelper.LogType.Info); - } - }); - } - - /// - /// 获取日期文件夹路径 - /// - /// 文件名 - /// 日期文件夹路径 - /// - /// 该方法会: - /// 1. 如果文件名为空,使用当前时间作为文件名 - /// 2. 获取基础路径和日期文件夹名 - /// 3. 组合路径并返回 - /// - private string GetDateFolderPath(string fileName) - { - if (string.IsNullOrWhiteSpace(fileName)) - { - fileName = Settings.Automation.IsUseCustomSaveFileName - ? SaveFileNameHelper.Render(Settings.Automation.CustomSaveFileNameTemplate, - new SaveFileNameContext { Mode = "Screenshot", Type = "Auto", Count = inkCanvas?.Strokes?.Count }) - : DateTime.Now.ToString("HH-mm-ss"); - } - - var basePath = Settings.Automation.AutoSavedStrokesLocation; - var dateFolder = DateTime.Now.ToString("yyyyMMdd"); - var safeRelativePath = SanitizeScreenshotRelativePath(fileName); - - return Path.Combine( - basePath, - "Auto Saved - Screenshots", - dateFolder, - safeRelativePath + Helpers.ScreenshotImageSaveHelper.GetExtension()); - } - - private static string SanitizeScreenshotRelativePath(string relativePath) - { - if (string.IsNullOrWhiteSpace(relativePath)) return DateTime.Now.ToString("HH-mm-ss"); - - var separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; - var parts = relativePath - .Split(separators, StringSplitOptions.RemoveEmptyEntries) - .Select(SanitizePathPart) - .Where(part => !string.IsNullOrWhiteSpace(part)); - - var sanitized = Path.Combine(parts.ToArray()); - return string.IsNullOrWhiteSpace(sanitized) ? DateTime.Now.ToString("HH-mm-ss") : sanitized; - } - - private static string SanitizePathPart(string pathPart) - { - var invalidChars = Path.GetInvalidFileNameChars(); - var chars = pathPart.Select(c => invalidChars.Contains(c) ? '_' : c).ToArray(); - var sanitized = new string(chars).Trim().TrimEnd('.', ' '); - return sanitized == "." || sanitized == ".." ? "_" : sanitized; - } - - /// - /// 获取默认文件夹路径 - /// - /// 默认文件夹路径 - /// - /// 该方法会: - /// 1. 获取基础路径 - /// 2. 组合截图文件夹路径 - /// 3. 确保截图文件夹存在 - /// 4. 生成文件名并组合完整路径返回 - /// - private string GetDefaultFolderPath() - { - var basePath = Settings.Automation.AutoSavedStrokesLocation; - var screenshotsFolder = Path.Combine(basePath, "Auto Saved - Screenshots"); - - if (!Directory.Exists(screenshotsFolder)) - { - Directory.CreateDirectory(screenshotsFolder); - } - - return Path.Combine( - screenshotsFolder, - GetScreenshotFileNameStem() + Helpers.ScreenshotImageSaveHelper.GetExtension()); - } - - private string GetScreenshotFileNameStem() - { - if (Settings.Automation.IsUseCustomSaveFileName) - { - return SaveFileNameHelper.Render(Settings.Automation.CustomSaveFileNameTemplate, - new SaveFileNameContext { Mode = "Screenshot", Type = "Auto", Count = inkCanvas?.Strokes?.Count }); - } - return DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); - } } } diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index 9220f9994..7ceacca2b 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -22,6 +22,9 @@ namespace Ink_Canvas { public partial class MainWindow : Ink_Canvas.Helpers.PerformanceTransparentWin { + // 寄生提取:设置存储服务,构造函数末尾初始化;壳在全部提取模块完成后删除。 + private readonly Services.SettingsStore _settingsStore; + #region Behavior @@ -121,7 +124,7 @@ private static object CreateHitokotoClient() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Settings] 创建一言 HttpClient 时设置 User-Agent 头失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } return client; } @@ -131,8 +134,9 @@ private static object CreateHitokotoClient() { LogHelper.WriteLogToFile($"无法创建 HttpClient (System.Net.Http 可能缺失): {ex.Message}", LogHelper.LogType.Warning); } - catch + catch (Exception ex2) { + LogService.LogException(ex2); } return null; } @@ -154,20 +158,8 @@ private HttpClient GetHitokotoHttpClientOrNull(bool writeLog = true) } } - private string BuildHitokotoRequestUrl() - { - var cats = Settings.Appearance.HitokotoCategories; - if (cats == null || cats.Count == 0) - cats = new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; - - var urlBuilder = new StringBuilder("https://v1.hitokoto.cn/?encode=text"); - foreach (var category in cats) - { - urlBuilder.Append($"&c={category}"); - } - - return urlBuilder.ToString(); - } + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + private string BuildHitokotoRequestUrl() => _settingsStore.BuildHitokotoRequestUrl(); private async Task FetchHitokotoTextCoreAsync(HttpClient client, string requestUrl) { @@ -663,29 +655,11 @@ public void UpdatePPTUIManagerSettings() #region Canvas /// 笔锋下拉 UI 顺序:0 实时笔锋,1 基于点集,2 基于速率,3 关闭。与存储值 InkStyle:3,0,1,2 对应。 - private static int PenStyleUiIndexFromInkStyle(int inkStyle) - { - switch (inkStyle) - { - case 3: return 0; - case 0: return 1; - case 1: return 2; - case 2: return 3; - default: return 1; - } - } + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + private static int PenStyleUiIndexFromInkStyle(int inkStyle) => Services.SettingsStore.PenStyleUiIndexFromInkStyle(inkStyle); - private static int InkStyleFromPenStyleUiIndex(int uiIndex) - { - switch (uiIndex) - { - case 0: return 3; - case 1: return 0; - case 2: return 1; - case 3: return 2; - default: return 0; - } - } + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + private static int InkStyleFromPenStyleUiIndex(int uiIndex) => Services.SettingsStore.InkStyleFromPenStyleUiIndex(uiIndex); private void ComboBoxPenStyle_SelectionChanged(object sender, SelectionChangedEventArgs e) { @@ -1060,156 +1034,8 @@ private void ToggleSwitchEnableTwoFingerRotation_Toggled(object sender, RoutedEv /// 该方法会重新创建全局 Settings 实例并应用推荐值,覆盖大部分子模块配置(如外观、画布、自动化、PPT、手势、高级选项等)。 /// 在重置过程中会保留并恢复当前 Settings.Automation 中的 AutoDelSavedFiles 与 AutoDelSavedFilesDaysThreshold 两项值以避免意外删除策略变化。 /// - public static void SetSettingsToRecommendation() - { - var AutoDelSavedFilesDays = Settings.Automation.AutoDelSavedFiles; - var AutoDelSavedFilesDaysThreshold = Settings.Automation.AutoDelSavedFilesDaysThreshold; - Settings = new Settings(); - Settings.Advanced.IsSpecialScreen = true; - Settings.Advanced.IsQuadIR = false; - Settings.Advanced.TouchMultiplier = 0.3; - Settings.Advanced.NibModeBoundsWidth = 5; - Settings.Advanced.FingerModeBoundsWidth = 20; - Settings.Advanced.NibModeBoundsWidthThresholdValue = 2.5; - Settings.Advanced.FingerModeBoundsWidthThresholdValue = 2.5; - Settings.Advanced.NibModeBoundsWidthEraserSize = 0.8; - Settings.Advanced.FingerModeBoundsWidthEraserSize = 0.8; - Settings.Advanced.EraserBindTouchMultiplier = true; - Settings.Advanced.IsLogEnabled = true; - Settings.Advanced.IsSecondConfirmWhenShutdownApp = false; - Settings.Advanced.IsEnableEdgeGestureUtil = false; - Settings.Advanced.EdgeGestureUtilOnlyAffectBlackboardMode = false; - Settings.Advanced.IsEnableFullScreenHelper = false; - Settings.Advanced.IsEnableAvoidFullScreenHelper = OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows11; - Settings.Advanced.IsEnableForceFullScreen = false; - Settings.Advanced.IsEnableDPIChangeDetection = false; - Settings.Advanced.IsEnableResolutionChangeDetection = false; - Settings.Advanced.EnableMultiScreenSupport = true; - Settings.Advanced.FollowMouseForScreenSelection = true; - - Settings.Appearance.IsColorfulViewboxFloatingBar = false; - Settings.Appearance.ViewboxFloatingBarScaleTransformValue = 1; - Settings.Appearance.ViewboxBlackBoardScaleTransformValue = 0.8; - Settings.Appearance.IsTransparentButtonBackground = true; - Settings.Appearance.IsShowExitButton = true; - Settings.Appearance.IsShowEraserButton = true; - Settings.Appearance.IsShowHideControlButton = false; - Settings.Appearance.IsShowLRSwitchButton = false; - Settings.Appearance.IsShowModeFingerToggleSwitch = true; - Settings.Appearance.IsShowQuickPanel = true; - Settings.Appearance.Theme = 0; - Settings.Appearance.EnableChickenSoupInWhiteboardMode = true; - Settings.Appearance.EnableTimeDisplayInWhiteboardMode = true; - Settings.Appearance.ChickenSoupSource = 1; - Settings.Appearance.ViewboxFloatingBarOpacityValue = 1.0; - Settings.Appearance.ViewboxFloatingBarOpacityInPPTValue = 1.0; - Settings.Appearance.EnableTrayIcon = true; - - // 浮动栏按钮显示控制默认值 - Settings.Appearance.IsShowQuickColorPalette = false; - Settings.Appearance.QuickColorPaletteDisplayMode = 1; - Settings.Appearance.EraserDisplayOption = 0; - - Settings.Automation.IsAutoFoldInEasiNote = true; - Settings.Automation.IsAutoFoldInEasiNoteIgnoreDesktopAnno = true; - Settings.Automation.IsAutoFoldInEasiCamera = true; - Settings.Automation.IsAutoFoldInEasiNote3C = false; - Settings.Automation.IsAutoFoldInEasiNote3 = false; - Settings.Automation.IsAutoFoldInEasiNote5C = true; - Settings.Automation.IsAutoFoldInSeewoPincoTeacher = false; - Settings.Automation.IsAutoFoldInHiteTouchPro = false; - Settings.Automation.IsAutoFoldInHiteCamera = false; - Settings.Automation.IsAutoFoldInWxBoardMain = false; - Settings.Automation.IsAutoFoldInOldZyBoard = false; - Settings.Automation.IsAutoFoldInMSWhiteboard = false; - Settings.Automation.IsAutoFoldInAdmoxWhiteboard = false; - Settings.Automation.IsAutoFoldInAdmoxBooth = false; - Settings.Automation.IsAutoFoldInQPoint = false; - Settings.Automation.IsAutoFoldInYiYunVisualPresenter = false; - Settings.Automation.IsAutoFoldInMaxHubWhiteboard = false; - Settings.Automation.IsAutoFoldInPPTSlideShow = false; - Settings.Automation.IsAutoKillPPTService = false; - Settings.Automation.IsAutoKillEasiNote = false; - Settings.Automation.IsAutoKillVComYouJiao = false; - Settings.Automation.IsAutoKillInkCanvas = false; - Settings.Automation.IsAutoKillICA = false; - Settings.Automation.IsAutoKillIDT = false; - Settings.Automation.IsAutoKillSeewoLauncher2DesktopAnnotation = false; - Settings.Automation.IsSaveScreenshotsInDateFolders = false; - Settings.Automation.ScreenshotSaveFormat = 0; - Settings.Automation.ScreenshotJpegQuality = 90; - Settings.Automation.ScreenshotScaleMode = 0; - Settings.Automation.IsAutoSaveStrokesAtScreenshot = true; - Settings.Automation.IsAutoSaveScreenshotAtClear = true; - Settings.Automation.IsAutoClearWhenExitingWritingMode = false; - Settings.Automation.MinimumAutomationStrokeNumber = 0; - Settings.Automation.AutoDelSavedFiles = AutoDelSavedFilesDays; - Settings.Automation.AutoDelSavedFilesDaysThreshold = AutoDelSavedFilesDaysThreshold; - - //Settings.PowerPointSettings.IsShowPPTNavigation = true; - //Settings.PowerPointSettings.IsShowBottomPPTNavigationPanel = false; - //Settings.PowerPointSettings.IsShowSidePPTNavigationPanel = true; - Settings.PowerPointSettings.PowerPointSupport = true; - Settings.PowerPointSettings.IsShowCanvasAtNewSlideShow = false; - Settings.PowerPointSettings.IsNoClearStrokeOnSelectWhenInPowerPoint = true; - Settings.PowerPointSettings.IsShowStrokeOnSelectInPowerPoint = false; - Settings.PowerPointSettings.IsAutoSaveStrokesInPowerPoint = true; - Settings.PowerPointSettings.IsAutoSaveScreenShotInPowerPoint = true; - Settings.PowerPointSettings.IsNotifyPreviousPage = false; - Settings.PowerPointSettings.IsNotifyHiddenPage = false; - Settings.PowerPointSettings.IsEnableTwoFingerGestureInPresentationMode = false; - Settings.PowerPointSettings.IsEnableFingerGestureSlideShowControl = false; - Settings.PowerPointSettings.IsSupportWPS = false; - Settings.PowerPointSettings.EnablePPTButtonEnhancedPreview = false; - Settings.PowerPointSettings.ShowPPTEnhancedPreviewLoadingAnimation = true; - - Settings.Canvas.InkWidth = 2.5; - Settings.Canvas.IsShowCursor = false; - Settings.Canvas.InkStyle = 0; - Settings.Canvas.HighlighterWidth = 20; - Settings.Canvas.EraserSize = 1; - Settings.Canvas.EraserType = 0; - Settings.Canvas.EraserShapeType = 1; - Settings.Canvas.HideStrokeWhenSelecting = false; - Settings.Canvas.ClearCanvasAndClearTimeMachine = false; - Settings.Canvas.FitToCurve = false; - Settings.Canvas.UseAdvancedBezierSmoothing = true; - Settings.Canvas.MergeInkSmoothingWithUndo = false; - Settings.Canvas.EnablePressureTouchMode = false; - Settings.Canvas.DisablePressure = false; - Settings.Canvas.AutoStraightenLine = true; - Settings.Canvas.AutoStraightenLineThreshold = 80; - Settings.Canvas.PauseStraightenLine = false; - Settings.Canvas.PauseStraightenDelay = 300; - Settings.Canvas.LineEndpointSnapping = true; - Settings.Canvas.LineEndpointSnappingThreshold = 15; - Settings.Canvas.UsingWhiteboard = false; - Settings.Canvas.HyperbolaAsymptoteOption = 0; - - Settings.Gesture.IsEnableTwoFingerTranslate = true; - Settings.Gesture.IsEnableTwoFingerZoom = false; - Settings.Gesture.IsEnableTwoFingerRotation = false; - Settings.Gesture.IsEnableTwoFingerRotationOnSelection = false; - - Settings.InkToShape.IsInkToShapeEnabled = true; - Settings.InkToShape.IsInkToShapeNoFakePressureRectangle = false; - Settings.InkToShape.IsInkToShapeNoFakePressureTriangle = false; - Settings.InkToShape.IsInkToShapeTriangle = true; - Settings.InkToShape.IsInkToShapeRectangle = true; - Settings.InkToShape.IsInkToShapeRounded = true; - Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify = false; - Settings.InkToShape.HandwritingCorrectionFontFamily = "Ink Free,KaiTi,Segoe Script"; - Settings.InkToShape.HandwritingLanguageOverrideLcid = 0; - Settings.InkToShape.HandwritingBeautifyDebounceMs = 2000; - - Settings.Startup.IsEnableNibMode = false; - Settings.Startup.IsAutoUpdate = true; - Settings.Startup.IsAutoUpdateWithSilence = true; - Settings.Startup.AutoUpdateWithSilenceStartTime = "06:00"; - Settings.Startup.AutoUpdateWithSilenceEndTime = "22:00"; - Settings.Startup.IsFoldAtStartup = false; - Settings.Startup.StartupMode = StartupMode.Default; - } + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + public static void SetSettingsToRecommendation() => Services.SettingsStore.SetSettingsToRecommendation(); /// /// 将应用设置重置为推荐的默认值,并保存与重新加载配置以应用更改。 @@ -1229,7 +1055,7 @@ public async void BtnResetToSuggestion_Click(object sender, RoutedEventArgs e) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Settings] 重置为推荐设置前的密码/TOTP 校验调用失败,将继续重置: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } try @@ -1251,11 +1077,7 @@ public async void BtnResetToSuggestion_Click(object sender, RoutedEventArgs e) } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - try { ShowNotification(Properties.MainWindowStrings.Main_Settings_ResetDone); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 重置为推荐设置完成后弹出「重置完成」通知失败: {ex.Message}", LogHelper.LogType.Info); - } + try { ShowNotification(Properties.MainWindowStrings.Main_Settings_ResetDone); } catch (Exception ex) { LogService.LogException(ex); } } private async void SpecialVersionResetToSuggestion_Click() @@ -1328,44 +1150,8 @@ void SetIcon(ToolbarImageButton btn, bool isSolid, string solidGeom, string line } } - public string GetCorrectIcon(string iconType, bool isSolid = false) - { - if (Settings.Appearance.UseLegacyFloatingBarUI) - { - // 使用老版图标 - switch (iconType) - { - case "cursor": - return isSolid ? XamlGraphicsIconGeometries.LegacySolidCursorIcon : XamlGraphicsIconGeometries.LegacyLinedCursorIcon; - case "pen": - return isSolid ? XamlGraphicsIconGeometries.LegacySolidPenIcon : XamlGraphicsIconGeometries.LegacyLinedPenIcon; - case "eraserStroke": - return isSolid ? XamlGraphicsIconGeometries.LegacySolidEraserStrokeIcon : XamlGraphicsIconGeometries.LegacyLinedEraserStrokeIcon; - case "eraserCircle": - return isSolid ? XamlGraphicsIconGeometries.LegacySolidEraserCircleIcon : XamlGraphicsIconGeometries.LegacyLinedEraserCircleIcon; - case "lassoSelect": - return isSolid ? XamlGraphicsIconGeometries.LegacySolidLassoSelectIcon : XamlGraphicsIconGeometries.LegacyLinedLassoSelectIcon; - } - } - else - { - // 使用新版图标 - switch (iconType) - { - case "cursor": - return isSolid ? XamlGraphicsIconGeometries.SolidCursorIcon : XamlGraphicsIconGeometries.LinedCursorIcon; - case "pen": - return isSolid ? XamlGraphicsIconGeometries.SolidPenIcon : XamlGraphicsIconGeometries.LinedPenIcon; - case "eraserStroke": - return isSolid ? XamlGraphicsIconGeometries.SolidEraserStrokeIcon : XamlGraphicsIconGeometries.LinedEraserStrokeIcon; - case "eraserCircle": - return isSolid ? XamlGraphicsIconGeometries.SolidEraserCircleIcon : XamlGraphicsIconGeometries.LinedEraserCircleIcon; - case "lassoSelect": - return isSolid ? XamlGraphicsIconGeometries.SolidLassoSelectIcon : XamlGraphicsIconGeometries.LinedLassoSelectIcon; - } - } - return ""; - } + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + public string GetCorrectIcon(string iconType, bool isSolid = false) => _settingsStore.GetCorrectIcon(iconType, isSolid); #region 浮动栏按钮显示控制 @@ -1405,7 +1191,8 @@ internal void UpdateFloatingBarButtonsVisibility() /// /// 在写入前会确保目标目录/文件具有写入权限(使用 ProcessProtectionManager)。任何写入失败或异常都会被吞掉,调用方不会收到异常抛出。 /// - public static void SaveSettingsToFile() => SettingsManager.SaveSettingsToFile(); + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + public static void SaveSettingsToFile() => Services.SettingsStore.SaveSettingsToFile(); private void SCManipulationBoundaryFeedback(object sender, ManipulationBoundaryFeedbackEventArgs e) { diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index 763e66e74..db62e5073 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -176,7 +176,7 @@ private void LoadSettings(bool isStartup = false, bool skipAutoUpdateCheck = fal } catch (Exception ex) { - LogHelper.WriteLogToFile($"[ProcGuard] 按设置应用进程保护失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } // Startup @@ -734,39 +734,8 @@ private void LoadInkFadeSettings() /// 4. 如果有清理操作,重新反序列化并保存 /// 5. 记录清理结果到日志 /// - private void CleanupObsoleteSettings(string userConfigJson) - { - try - { - // 创建默认配置对象 - Settings defaultSettings = new Settings(); - - // 将默认配置和用户配置都序列化为JObject - JObject defaultConfigObj = JObject.FromObject(defaultSettings); EnsureDefaultConfigSchemaIncludesIgnoredNullKeys(defaultConfigObj); - JObject userConfigObj = JObject.Parse(userConfigJson); - - // 记录是否有清理或迁移操作 - bool hasChanges = false; - MigrateLegacyStartupMode(userConfigObj, ref hasChanges); - - // 递归比较并删除用户配置中多余的键 - RemoveObsoleteProperties(userConfigObj, defaultConfigObj, ref hasChanges); - - // 如果有清理操作,重新反序列化并保存 - if (hasChanges) - { - string cleanedJson = userConfigObj.ToString(Formatting.Indented); - Settings = JsonConvert.DeserializeObject(cleanedJson); - SaveSettingsToFile(); - App.UpdateCachedSettingsJson(cleanedJson); - LogHelper.WriteLogToFile("已清理过期配置项", LogHelper.LogType.Event); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清理过期配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + private void CleanupObsoleteSettings(string userConfigJson) => _settingsStore.CleanupObsoleteSettings(userConfigJson); /// /// 递归删除用户配置中多余的属性 @@ -785,101 +754,17 @@ private void CleanupObsoleteSettings(string userConfigJson) /// 7. 删除标记的键 /// 8. 设置变更标志 /// - private static void MigrateLegacyStartupMode(JObject userConfigObj, ref bool hasChanges) - { - if (!(userConfigObj?["startup"] is JObject startup)) return; - - if (startup["startupMode"] == null && startup["enableFastStartup"]?.Type == JTokenType.Boolean) - { - startup["startupMode"] = startup["enableFastStartup"].Value() - ? (int)StartupMode.Fastest - : (int)StartupMode.Faster; - hasChanges = true; - } - - if (startup.Remove("enableFastStartup")) - { - hasChanges = true; - } - } - - private static void EnsureDefaultConfigSchemaIncludesIgnoredNullKeys(JObject defaultConfigObj) - { - if (defaultConfigObj == null) return; - if (defaultConfigObj["appearance"] is JObject appearance) - { - // 这些属性同时具备 NullValueHandling.Ignore 且默认值为 null, - // 不会出现在默认 JObject 中。CleanupObsoleteSettings 会把它们 - // 误判为"过期"并立即 SaveSettingsToFile 删除,于是用户自建语录 - // 被静默清空。补成 null 占位让 RemoveObsoleteProperties 放行。 - foreach (var ignoredKey in new[] { "hitokotoCategories", "customTipsSchemes", "enabledPresetTipsSources" }) - { - if (!appearance.ContainsKey(ignoredKey)) - appearance[ignoredKey] = JValue.CreateNull(); - } - } - } - - private void RemoveObsoleteProperties(JObject userObj, JObject defaultObj, ref bool hasChanges) - { - if (userObj == null || defaultObj == null) - return; - - // 获取需要删除的键列表(避免在遍历时修改集合) - List keysToRemove = new List(); - - foreach (var property in userObj.Properties()) - { - string propertyName = property.Name; - - // 如果默认配置中不存在该属性,标记为删除 - if (!defaultObj.ContainsKey(propertyName)) - { - keysToRemove.Add(propertyName); - continue; - } + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + private static void MigrateLegacyStartupMode(JObject userConfigObj, ref bool hasChanges) => + Services.SettingsStore.MigrateLegacyStartupMode(userConfigObj, ref hasChanges); - // 如果两个属性都是对象类型,递归比较 - JToken userValue = property.Value; - JToken defaultValue = defaultObj[propertyName]; - - if (userValue != null && defaultValue != null) - { - if (userValue.Type == JTokenType.Object && defaultValue.Type == JTokenType.Object) - { - RemoveObsoleteProperties(userValue as JObject, defaultValue as JObject, ref hasChanges); - } - // 处理数组中的对象(如自定义图标列表等) - else if (userValue.Type == JTokenType.Array && defaultValue.Type == JTokenType.Array) - { - JArray userArray = userValue as JArray; - JArray defaultArray = defaultValue as JArray; + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + private static void EnsureDefaultConfigSchemaIncludesIgnoredNullKeys(JObject defaultConfigObj) => + Services.SettingsStore.EnsureDefaultConfigSchemaIncludesIgnoredNullKeys(defaultConfigObj); - if (userArray != null && defaultArray != null && userArray.Count > 0 && defaultArray.Count > 0) - { - // 如果数组元素是对象,比较第一个元素的属性结构 - if (userArray[0].Type == JTokenType.Object && defaultArray[0].Type == JTokenType.Object) - { - for (int i = 0; i < userArray.Count; i++) - { - if (userArray[i] is JObject userItemObj && defaultArray[0] is JObject defaultItemObj) - { - RemoveObsoleteProperties(userItemObj, defaultItemObj, ref hasChanges); - } - } - } - } - } - } - } - - // 删除标记的键 - foreach (string key in keysToRemove) - { - userObj.Remove(key); - hasChanges = true; - } - } + // 壳:实现已搬移至 Services/Settings/SettingsStore.cs + private void RemoveObsoleteProperties(JObject userObj, JObject defaultObj, ref bool hasChanges) => + _settingsStore.RemoveObsoleteProperties(userObj, defaultObj, ref hasChanges); internal void ApplyQuickPanelBottomOffset(double offset) { diff --git a/Ink Canvas/MainWindow_cs/MW_Timer.cs b/Ink Canvas/MainWindow_cs/MW_Timer.cs index 3e7f32f81..b121a92e8 100644 --- a/Ink Canvas/MainWindow_cs/MW_Timer.cs +++ b/Ink Canvas/MainWindow_cs/MW_Timer.cs @@ -355,7 +355,7 @@ private void AutoSaveStrokesTimer_Tick(object sender, EventArgs e) } catch (Exception ex) { - LogHelper.WriteLogToFile($"[Timer] 自动保存墨迹失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -936,10 +936,7 @@ private bool HasAnyFullScreenAutoFoldApp() else if (prodName.Contains("3C") && Settings.Automation.IsAutoFoldInEasiNote3C) return true; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Timer] 读取易记笔记版本信息失败,自动收纳判定可能不准: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } } else if (Settings.Automation.IsAutoFoldInEasiCamera && windowProcessName == "EasiCamera") @@ -980,10 +977,7 @@ private bool HasAnyFullScreenAutoFoldApp() if (version.StartsWith("6.") && prodName == "WhiteBoard") return true; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Timer] 读取希沃白板版本信息失败,自动收纳判定可能不准: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } } } @@ -1485,10 +1479,7 @@ private void timerCheckAutoUpdateWithSilence_Elapsed(object sender, ElapsedEvent // 使用 BeginInvoke 避免在 Invoke 闭包内同步等待自身调度导致 UI 死锁 Dispatcher.BeginInvoke(new Action(() => { Application.Current.Shutdown(); })); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 静默更新安装后投递 Application.Shutdown 失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } else { diff --git a/Ink Canvas/MainWindow_cs/MW_Toolbar.cs b/Ink Canvas/MainWindow_cs/MW_Toolbar.cs index 838284752..e241de158 100644 --- a/Ink Canvas/MainWindow_cs/MW_Toolbar.cs +++ b/Ink Canvas/MainWindow_cs/MW_Toolbar.cs @@ -1,6 +1,7 @@ using Ink_Canvas.Controls; using Ink_Canvas.Controls.Toolbar.FloatingToolbar; using Ink_Canvas.Helpers; +using Ink_Canvas.Models; using System; using System.Linq; using System.Windows; @@ -417,7 +418,7 @@ private void SetToolbarHeadPosition(ToolbarPosition position) break; } - SetFloatingBarHighlightPosition(_currentToolMode); + SetFloatingBarHighlightPosition(ToolModeMapping.ToInternalString(_currentToolModeEnum)); } internal void RebuildToolbar() @@ -430,10 +431,10 @@ internal void RebuildToolbar() ToolbarRegistry.ClearInjected(StackPanelFloatingBarRoot); InitializeToolbarPlugins(); UpdateToolbarComponentVisibility(); - ApplyFloatingBarIconHighlightImmediate(_currentToolMode); + ApplyFloatingBarIconHighlightImmediate(ToolModeMapping.ToInternalString(_currentToolModeEnum)); RefreshFloatingBarButtonColors(); RefreshGestureButtonIcon(); - SetFloatingBarHighlightPosition(_currentToolMode); + SetFloatingBarHighlightPosition(ToolModeMapping.ToInternalString(_currentToolModeEnum)); ApplyCompactFloatingBarMode(Settings.Appearance.CompactFloatingBar); ApplyHideFloatingBarBorder(Settings.Appearance.HideFloatingBarBorder); ApplyFloatingBarBorderColor(); @@ -608,7 +609,7 @@ internal void ApplyFloatingBarBorderColor() } } - internal bool IsAnnotating => _currentToolMode != "cursor"; + internal bool IsAnnotating => _currentToolModeEnum != ToolMode.Cursor; internal void UpdateToolbarComponentVisibility() { diff --git a/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs b/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs index b409a728c..1980c7bcc 100644 --- a/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs +++ b/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs @@ -1,806 +1,46 @@ using H.NotifyIcon; -using Ink_Canvas.Helpers; -using Ink_Canvas.Properties; -using iNKORE.UI.WPF.Controls; +using Ink_Canvas.Services; using System; -using System.Diagnostics; -using System.Linq; -using System.Reflection; using System.Windows; -using System.Windows.Controls; -using System.Windows.Forms; -using System.Windows.Interop; -using System.Windows.Media; -using System.Windows.Threading; using Application = System.Windows.Application; -using ContextMenu = System.Windows.Controls.ContextMenu; -using MenuItem = System.Windows.Controls.MenuItem; namespace Ink_Canvas { public partial class App : Application { - private const int TrayTemporaryShowMinutes = 2; + // 托盘逻辑已提取至 Services/Shell/TrayIconService.cs,本文件仅保留 + // XAML wired 事件处理器与插件托盘 API 的转发壳(壳删除试点)。 + // 服务的依赖装配在 App.xaml.cs 的 CreateTrayIconService,释放在 App_Exit。 + private TrayIconService _trayIconService; - private DispatcherTimer _trayTemporaryShowTimer; - - private bool _trayTemporaryShowRestoreHideChecked; - - private void TaskbarTrayIcon_TrayLeftMouseDown(object sender, RoutedEventArgs e) - { - var action = Ink_Canvas.MainWindow.Settings.Appearance.TrayLeftClickAction; - ExecuteTrayClickAction(action); - - try { PluginTrayLeftClicked?.Invoke(); } - catch (Exception ex) { LogHelper.WriteLogToFile($"转发插件托盘左键事件失败: {ex.Message}", LogHelper.LogType.Warning); } - } - - private void TaskbarTrayIcon_TrayRightMouseDown(object sender, RoutedEventArgs e) - { - var action = Ink_Canvas.MainWindow.Settings.Appearance.TrayRightClickAction; - ExecuteTrayClickAction(action); - - try { PluginTrayRightClicked?.Invoke(); } - catch (Exception ex) { LogHelper.WriteLogToFile($"转发插件托盘右键事件失败: {ex.Message}", LogHelper.LogType.Warning); } - } - - private void ExecuteTrayClickAction(TrayClickAction action) - { - switch (action) - { - case TrayClickAction.ShowMenu: - ShowTrayContextMenu(); - break; - case TrayClickAction.HideShowMainWindow: - ToggleMainWindowVisibility(); - break; - case TrayClickAction.TempShowMainWindow: - TempShowMainWindowTrayIconMenuItem_Clicked(null, null); - break; - case TrayClickAction.OpenSettings: - OpenSettingsTrayIconMenuItem_Clicked(null, null); - break; - case TrayClickAction.DisableAllHotkeys: - DisableAllHotkeysMenuItem_Clicked(FindTrayMenuItem("DisableAllHotkeysMenuItem"), null); - break; - case TrayClickAction.ForceFullScreen: - ForceFullScreenTrayIconMenuItem_Clicked(null, null); - break; - case TrayClickAction.ToggleFoldFloatingBar: - FoldFloatingBarTrayIconMenuItem_Clicked(null, null); - break; - case TrayClickAction.ResetFloatingBarPosition: - ResetFloatingBarPositionTrayIconMenuItem_Clicked(null, null); - break; - case TrayClickAction.RestartApp: - RestartAppTrayIconMenuItem_Clicked(null, null); - break; - case TrayClickAction.CloseApp: - CloseAppTrayIconMenuItem_Clicked(null, null); - break; - case TrayClickAction.NoAction: - break; - } - } - - private MenuItem FindTrayMenuItem(string name) - { - try - { - var trayMenu = ((TaskbarIcon)Current.Resources["TaskbarTrayIcon"]).ContextMenu; - return trayMenu?.Items.OfType().FirstOrDefault(mi => mi.Name == name); - } - catch - { - return null; - } - } - - internal void ShowTrayContextMenu() - { - try - { - var taskbarIcon = (TaskbarIcon)Current.Resources["TaskbarTrayIcon"]; - if (taskbarIcon?.ContextMenu != null) - { - taskbarIcon.ContextMenu.IsOpen = true; - } - } - catch (Exception ex) - { - ExceptionHandler.HandleException(ex, "显示托盘右键菜单失败", LogHelper.LogType.Warning); - } - } - - private void ToggleMainWindowVisibility() - { - var mainWin = Current.MainWindow as MainWindow; - if (mainWin?.IsLoaded != true) return; - - var hideItem = FindTrayMenuItem("HideICCMainWindowTrayIconMenuItem"); - if (hideItem != null) - { - if (hideItem.IsChecked) - { - hideItem.IsChecked = false; - HideICCMainWindowTrayIconMenuItem_UnChecked(hideItem, null); - } - else - { - hideItem.IsChecked = true; - HideICCMainWindowTrayIconMenuItem_Checked(hideItem, null); - } - } - else - { - if (mainWin.IsVisible) - mainWin.Hide(); - else - mainWin.Show(); - } - } - - /// - /// 系统托盘菜单打开时的事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理系统托盘菜单打开时的逻辑,包括以下步骤: - /// 1. 获取系统托盘菜单及其相关菜单项和图标 - /// 2. 获取主窗口实例 - /// 3. 如果主窗口已加载: - /// - 在无焦点模式下,暂时取消主窗口置顶,让系统菜单能够正常显示 - /// - 根据浮动栏是否处于收纳模式,更新菜单项图标和文本 - /// - 根据浮动栏状态和主窗口是否隐藏,更新重置浮动栏位置菜单项的启用状态 - /// - private void SysTrayMenu_Opened(object sender, RoutedEventArgs e) - { - var s = (ContextMenu)sender; - var FoldFloatingBarTrayIconMenuItemIconEyeOff = (Image)((Grid)((MenuItem)s.Items[s.Items.Count - 5]).Icon).Children[0]; - var FoldFloatingBarTrayIconMenuItemIconEyeOn = (Image)((Grid)((MenuItem)s.Items[s.Items.Count - 5]).Icon).Children[1]; - var FoldFloatingBarTrayIconMenuItemHeaderText = (TextBlock)((SimpleStackPanel)((MenuItem)s.Items[s.Items.Count - 5]).Header).Children[0]; - var ResetFloatingBarPositionTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 4]; - var HideICCMainWindowTrayIconMenuItem = s.Items.OfType() - .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); - if (HideICCMainWindowTrayIconMenuItem == null) return; - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin.IsLoaded) - { - // 在无焦点模式下,暂时取消主窗口置顶,让系统菜单能够正常显示 - if (Ink_Canvas.MainWindow.Settings.Advanced.IsAlwaysOnTop && Ink_Canvas.MainWindow.Settings.Advanced.IsNoFocusMode) - { - mainWin.Topmost = false; - } - - // 判斷是否在收納模式中 - if (mainWin.isFloatingBarFolded) - { - FoldFloatingBarTrayIconMenuItemIconEyeOff.Visibility = Visibility.Hidden; - FoldFloatingBarTrayIconMenuItemIconEyeOn.Visibility = Visibility.Visible; - FoldFloatingBarTrayIconMenuItemHeaderText.Text = MainWindowStrings.Main_Tray_ExitFoldMode; - if (!HideICCMainWindowTrayIconMenuItem.IsChecked) - { - ResetFloatingBarPositionTrayIconMenuItem.IsEnabled = false; - ResetFloatingBarPositionTrayIconMenuItem.Opacity = 0.5; - } - } - else - { - FoldFloatingBarTrayIconMenuItemIconEyeOff.Visibility = Visibility.Visible; - FoldFloatingBarTrayIconMenuItemIconEyeOn.Visibility = Visibility.Hidden; - FoldFloatingBarTrayIconMenuItemHeaderText.Text = MainWindowStrings.Main_Tray_EnterFoldMode; - if (!HideICCMainWindowTrayIconMenuItem.IsChecked) - { - ResetFloatingBarPositionTrayIconMenuItem.IsEnabled = true; - ResetFloatingBarPositionTrayIconMenuItem.Opacity = 1; - } - - } - } - } - - /// - /// 系统托盘菜单关闭时的事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理系统托盘菜单关闭时的逻辑,包括以下步骤: - /// 1. 获取主窗口实例 - /// 2. 如果主窗口已加载,且在无焦点模式下启用了始终置顶,则恢复主窗口的置顶状态 - /// - private bool EnsureMainWindowReadyForSettings(MainWindow mainWin) - { - if (mainWin?.IsLoaded != true) - { - return false; - } - - var trayMenu = ((TaskbarIcon)Current.Resources["TaskbarTrayIcon"]).ContextMenu; - var hideMainWindowMenuItem = trayMenu?.Items.OfType() - .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); - - if (hideMainWindowMenuItem != null && hideMainWindowMenuItem.IsChecked) - { - hideMainWindowMenuItem.IsChecked = false; - } - else if (!mainWin.IsVisible) - { - mainWin.Show(); - } - - if (mainWin.WindowState == WindowState.Minimized) - { - mainWin.WindowState = WindowState.Normal; - } - - mainWin.Activate(); - return true; - } - - private void TempShowMainWindowTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) - { - var mainWin = Current.MainWindow as MainWindow; - if (mainWin?.IsLoaded != true) - return; - - MenuItem hideItem = null; - try - { - var trayMenu = ((TaskbarIcon)Current.Resources["TaskbarTrayIcon"]).ContextMenu; - hideItem = trayMenu?.Items.OfType() - .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Tray] 查找托盘「隐藏主窗口」菜单项失败: {ex.Message}", LogHelper.LogType.Info); - } - - _trayTemporaryShowRestoreHideChecked = hideItem?.IsChecked == true; - - EnsureMainWindowReadyForSettings(mainWin); - - global::Ink_Canvas.MainWindow.TrayTemporaryShowUntilUtc = DateTime.UtcNow.AddMinutes(TrayTemporaryShowMinutes); - - _trayTemporaryShowTimer?.Stop(); - if (_trayTemporaryShowTimer == null) - { - _trayTemporaryShowTimer = new DispatcherTimer - { - Interval = TimeSpan.FromMinutes(TrayTemporaryShowMinutes) - }; - _trayTemporaryShowTimer.Tick += TrayTemporaryShowTimer_OnTick; - } - else - { - _trayTemporaryShowTimer.Interval = TimeSpan.FromMinutes(TrayTemporaryShowMinutes); - } - - _trayTemporaryShowTimer.Start(); - } - - private void TrayTemporaryShowTimer_OnTick(object sender, EventArgs e) - { - _trayTemporaryShowTimer?.Stop(); - global::Ink_Canvas.MainWindow.TrayTemporaryShowUntilUtc = null; - - var mainWin = Current.MainWindow as MainWindow; - if (mainWin?.IsLoaded != true) - { - _trayTemporaryShowRestoreHideChecked = false; - return; - } - - try - { - if (_trayTemporaryShowRestoreHideChecked) - { - var trayMenu = ((TaskbarIcon)Current.Resources["TaskbarTrayIcon"]).ContextMenu; - var hideItem = trayMenu?.Items.OfType() - .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); - if (hideItem != null) - hideItem.IsChecked = true; - else - mainWin.Hide(); - } - else - { - mainWin.CheckMainWindowVisibility(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"托盘临时显示计时结束处理失败: {ex.Message}", LogHelper.LogType.Warning); - } - finally - { - _trayTemporaryShowRestoreHideChecked = false; - } - } - - private void OpenSettingsTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) - { - var mainWin = Current.MainWindow as MainWindow; - if (!EnsureMainWindowReadyForSettings(mainWin)) - { - return; - } - - try - { - var method = typeof(MainWindow).GetMethod("BtnSettings_Click", BindingFlags.NonPublic | BindingFlags.Instance); - method?.Invoke(mainWin, new object[] { null, null }); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"Open settings from tray failed: {ex.Message}", LogHelper.LogType.Error); - } - } - - private void SysTrayMenu_Closed(object sender, RoutedEventArgs e) - { - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin != null && mainWin.IsLoaded) - { - // 菜单关闭后,恢复主窗口的置顶状态 - if (Ink_Canvas.MainWindow.Settings.Advanced.IsAlwaysOnTop && Ink_Canvas.MainWindow.Settings.Advanced.IsNoFocusMode) - { - mainWin.Topmost = true; - } - } - } - - /// - /// 关闭应用程序托盘菜单项点击事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理关闭应用程序托盘菜单项的点击事件,包括以下步骤: - /// 1. 获取主窗口实例 - /// 2. 如果主窗口已加载: - /// - 设置IsAppExitByUser为true,表示用户主动退出 - /// - 关闭应用程序 - /// - private void CloseAppTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) - { - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin != null && mainWin.IsLoaded) - { - IsAppExitByUser = true; - mainWin.ExitApplication(null, null); - } - } - - /// - /// 重启应用程序托盘菜单项点击事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理重启应用程序托盘菜单项的点击事件,包括以下步骤: - /// 1. 获取主窗口实例 - /// 2. 如果主窗口已加载: - /// - 设置IsAppExitByUser为true,表示用户主动退出 - /// - 尝试启动应用程序的新实例,带延迟参数 - /// - 捕获并记录启动新实例时可能出现的异常 - /// - 关闭当前应用程序实例 - /// - private void RestartAppTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) - { - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin != null && mainWin.IsLoaded) - { - IsAppExitByUser = true; - - try - { - // 启动新实例 - string exePath = Process.GetCurrentProcess().MainModule.FileName; - ProcessStartInfo startInfo = new ProcessStartInfo(); - startInfo.FileName = exePath; - startInfo.UseShellExecute = true; - - // 启动进程但不等待 - Process.Start(new ProcessStartInfo(exePath, "-delay 2000") { UseShellExecute = true }); - } - catch (Exception ex) - { - LogHelper.NewLog($"重启程序时出错: {ex.Message}"); - } - - // 退出当前实例 - Current.Shutdown(); - } - } - - /// - /// 强制全屏化托盘菜单项点击事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理强制全屏化托盘菜单项的点击事件,包括以下步骤: - /// 1. 获取主窗口实例 - /// 2. 如果主窗口已加载: - /// - 调用MoveWindow方法将主窗口移动到屏幕左上角并设置为全屏大小 - /// - 显示强制全屏化的消息,包含屏幕分辨率和缩放比例信息 - /// - private void ForceFullScreenTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) - { - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin != null && mainWin.IsLoaded) - { - Ink_Canvas.MainWindow.MoveWindow(new WindowInteropHelper(mainWin).Handle, 0, 0, - Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, true); - Ink_Canvas.MainWindow.ShowNewMessage($"已强制全屏化:{Screen.PrimaryScreen.Bounds.Width}x{Screen.PrimaryScreen.Bounds.Height}(缩放比例为{Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth}x{Screen.PrimaryScreen.Bounds.Height / SystemParameters.PrimaryScreenHeight})"); - } - } - - /// - /// 切换浮动栏收纳模式托盘菜单项点击事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理切换浮动栏收纳模式托盘菜单项的点击事件,包括以下步骤: - /// 1. 获取主窗口实例 - /// 2. 如果主窗口已加载: - /// - 如果浮动栏当前处于收纳模式,则调用UnFoldFloatingBar_MouseUp方法退出收纳模式 - /// - 否则,调用FoldFloatingBar_MouseUp方法进入收纳模式 - /// - private void FoldFloatingBarTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) - { - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin != null && mainWin.IsLoaded) - if (mainWin.isFloatingBarFolded) mainWin.UnFoldFloatingBar_MouseUp(new object(), null); - else mainWin.FoldFloatingBar_MouseUp(new object(), null); - } - - /// - /// 重置浮动栏位置托盘菜单项点击事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理重置浮动栏位置托盘菜单项的点击事件,包括以下步骤: - /// 1. 获取主窗口实例 - /// 2. 如果主窗口已加载: - /// - 清空用户拖动标志和已保存的位置坐标,让动画走默认位置分支 - /// - 如果浮动栏当前未处于收纳模式: - /// - 如果不处于PPT演示模式,调用PureViewboxFloatingBarMarginAnimationInDesktopMode方法重置浮动栏位置 - /// - 否则,调用PureViewboxFloatingBarMarginAnimationInPPTMode方法重置浮动栏位置 - /// - private void ResetFloatingBarPositionTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) - { - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin != null && mainWin.IsLoaded) - { - var isInPPTPresentationMode = false; - // 托盘菜单的 Click 事件可能在 UI 线程触发;此处读取主窗口属性已在 UI 线程完成, - // 改为本地直接读取,避免在 UI 线程上同步等待自身的 Dispatcher.Invoke 形成自锁。 - isInPPTPresentationMode = mainWin.IsInPPTPresentationMode; - if (!mainWin.isFloatingBarFolded) - { - // 清空保存的状态,强制动画走默认位置分支 - mainWin._userHasDraggedFloatingBar = false; - mainWin.pointDesktop = new Point(-1, -1); - mainWin.pointPPT = new Point(-1, -1); - - if (!isInPPTPresentationMode) mainWin.PureViewboxFloatingBarMarginAnimationInDesktopMode(); - else mainWin.PureViewboxFloatingBarMarginAnimationInPPTMode(); - } - } - } - - /// - /// 隐藏主窗口托盘菜单项选中事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理隐藏主窗口托盘菜单项的选中事件,包括以下步骤: - /// 1. 获取菜单项和主窗口实例 - /// 2. 如果主窗口已加载: - /// - 隐藏主窗口 - /// - 获取系统托盘菜单 - /// - 禁用并设置半透明效果给以下菜单项: - /// - 重置浮动栏位置 - /// - 切换浮动栏收纳模式 - /// - 强制全屏化 - /// 3. 否则,取消菜单项的选中状态 - /// - private void HideICCMainWindowTrayIconMenuItem_Checked(object sender, RoutedEventArgs e) - { - _trayTemporaryShowTimer?.Stop(); - global::Ink_Canvas.MainWindow.TrayTemporaryShowUntilUtc = null; - _trayTemporaryShowRestoreHideChecked = false; - - var mi = (MenuItem)sender; - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin != null && mainWin.IsLoaded) - { - mainWin.Hide(); - var s = ((TaskbarIcon)Current.Resources["TaskbarTrayIcon"]).ContextMenu; - if (s != null) - { - var ResetFloatingBarPositionTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 4]; - var FoldFloatingBarTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 5]; - var ForceFullScreenTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 6]; - ResetFloatingBarPositionTrayIconMenuItem.IsEnabled = false; - FoldFloatingBarTrayIconMenuItem.IsEnabled = false; - ForceFullScreenTrayIconMenuItem.IsEnabled = false; - ResetFloatingBarPositionTrayIconMenuItem.Opacity = 0.5; - FoldFloatingBarTrayIconMenuItem.Opacity = 0.5; - ForceFullScreenTrayIconMenuItem.Opacity = 0.5; - } - } - else - { - mi.IsChecked = false; - } - - } - - /// - /// 显示主窗口托盘菜单项取消选中事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理显示主窗口托盘菜单项的取消选中事件,包括以下步骤: - /// 1. 获取菜单项和主窗口实例 - /// 2. 如果主窗口已加载: - /// - 显示主窗口 - /// - 获取系统托盘菜单 - /// - 启用并设置正常透明度给以下菜单项: - /// - 重置浮动栏位置 - /// - 切换浮动栏收纳模式 - /// - 强制全屏化 - /// 3. 否则,取消菜单项的选中状态 - /// - private void HideICCMainWindowTrayIconMenuItem_UnChecked(object sender, RoutedEventArgs e) - { - var mi = (MenuItem)sender; - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin != null && mainWin.IsLoaded) - { - mainWin.Show(); - var s = ((TaskbarIcon)Current.Resources["TaskbarTrayIcon"]).ContextMenu; - if (s != null) - { - var ResetFloatingBarPositionTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 4]; - var FoldFloatingBarTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 5]; - var ForceFullScreenTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 6]; - ResetFloatingBarPositionTrayIconMenuItem.IsEnabled = true; - FoldFloatingBarTrayIconMenuItem.IsEnabled = true; - ForceFullScreenTrayIconMenuItem.IsEnabled = true; - ResetFloatingBarPositionTrayIconMenuItem.Opacity = 1; - FoldFloatingBarTrayIconMenuItem.Opacity = 1; - ForceFullScreenTrayIconMenuItem.Opacity = 1; - } - } - else - { - mi.IsChecked = false; - } - } - - /// - /// 禁用/启用所有快捷键托盘菜单项点击事件处理方法 - /// - /// 事件发送者 - /// 路由事件参数 - /// - /// 处理禁用/启用所有快捷键托盘菜单项的点击事件,包括以下步骤: - /// 1. 获取主窗口实例 - /// 2. 如果主窗口已加载,尝试: - /// - 通过反射获取全局快捷键管理器 - /// - 如果获取成功: - /// - 禁用快捷键注册 - /// - 更新菜单项文本和状态: - /// - 如果当前文本是"禁用所有快捷键",则更改为"启用所有快捷键"并记录日志 - /// - 否则,更改为"禁用所有快捷键",重新启用快捷键注册并记录日志 - /// - 如果获取失败,记录错误日志 - /// 3. 捕获并记录可能出现的异常 - /// - private void DisableAllHotkeysMenuItem_Clicked(object sender, RoutedEventArgs e) - { - var mainWin = (MainWindow)Current.MainWindow; - if (mainWin != null && mainWin.IsLoaded) - { - try - { - // 获取全局快捷键管理器 - var hotkeyManagerField = typeof(MainWindow).GetField("_globalHotkeyManager", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var hotkeyManager = hotkeyManagerField?.GetValue(mainWin) as GlobalHotkeyManager; - - if (hotkeyManager != null) - { - // 禁用所有快捷键 - hotkeyManager.DisableHotkeyRegistration(); - - // 更新菜单项文本和状态 - var menuItem = sender as MenuItem; - if (menuItem != null) - { - var headerPanel = menuItem.Header as SimpleStackPanel; - if (headerPanel != null) - { - var textBlock = headerPanel.Children[0] as TextBlock; - if (textBlock != null) - { - if (textBlock.Text == MainWindowStrings.Main_Tray_DisableHotkeys) - { - textBlock.Text = MainWindowStrings.Main_Tray_EnableHotkeys; - LogHelper.WriteLogToFile("已禁用所有快捷键", LogHelper.LogType.Event); - } - else - { - textBlock.Text = MainWindowStrings.Main_Tray_DisableHotkeys; - // 重新启用快捷键 - hotkeyManager.EnableHotkeyRegistration(); - LogHelper.WriteLogToFile("已启用所有快捷键", LogHelper.LogType.Event); - } - } - } - } - } - else - { - LogHelper.WriteLogToFile("无法获取全局快捷键管理器", LogHelper.LogType.Error); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用/启用快捷键时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } - - #region 插件托盘服务(ITrayService 宿主支持) + private TrayIconService TrayIcon => _trayIconService ??= CreateTrayIconService(); internal event Action PluginTrayLeftClicked; internal event Action PluginTrayRightClicked; - internal H.NotifyIcon.TaskbarIcon GetPluginTaskbarIcon() - { - try { return Current.Resources["TaskbarTrayIcon"] as H.NotifyIcon.TaskbarIcon; } - catch { return null; } - } - - internal void SetPluginTrayIconVisibility(bool visible) - { - var icon = GetPluginTaskbarIcon(); - if (icon != null) icon.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; - } - - /// - /// 向托盘右键菜单注入一个插件菜单项,插入到「重启程序」之前, - /// 保持 SysTrayMenu_Opened / HideICCMainWindowTrayIconMenuItem_Checked 等 - /// 用倒数索引访问固定菜单项的兼容性。 - /// - internal bool AddPluginTrayMenuItem(string id, string text, Action onClicked) - { - if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(text) || onClicked == null) return false; - try - { - var trayMenu = GetPluginTaskbarIcon()?.ContextMenu; - if (trayMenu == null) return false; - - string menuName = "PluginTray." + id; - if (trayMenu.Items.OfType().Any(mi => mi.Name == menuName)) return false; - - var item = new MenuItem - { - Name = menuName, - Header = new TextBlock - { - Text = text, - FontSize = 14, - VerticalAlignment = VerticalAlignment.Center, - Foreground = new SolidColorBrush(Color.FromRgb(0x18, 0x18, 0x1b)) - } - }; - item.Click += (s, e) => onClicked(); - - var restartItem = trayMenu.Items.OfType() - .FirstOrDefault(mi => mi.Name == "RestartAppTrayIconMenuItem"); - int insertIndex = restartItem != null ? trayMenu.Items.IndexOf(restartItem) : trayMenu.Items.Count; - trayMenu.Items.Insert(insertIndex, item); - return true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"添加插件托盘菜单项失败: {ex.Message}", LogHelper.LogType.Warning); - return false; - } - } - - internal bool RemovePluginTrayMenuItem(string id) - { - if (string.IsNullOrWhiteSpace(id)) return false; - try - { - var trayMenu = GetPluginTaskbarIcon()?.ContextMenu; - if (trayMenu == null) return false; - - var item = trayMenu.Items.OfType() - .FirstOrDefault(mi => mi.Name == "PluginTray." + id); - if (item == null) return false; - - trayMenu.Items.Remove(item); - return true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"移除插件托盘菜单项失败: {ex.Message}", LogHelper.LogType.Warning); - return false; - } - } - - internal bool HasPluginTrayMenuItem(string id) - { - if (string.IsNullOrWhiteSpace(id)) return false; - try - { - var trayMenu = GetPluginTaskbarIcon()?.ContextMenu; - return trayMenu?.Items.OfType().Any(mi => mi.Name == "PluginTray." + id) == true; - } - catch { return false; } - } - - /// - /// 移除某个插件注册的所有托盘菜单项。插件热重载时调用:菜单项的 Click 处理器 - /// 闭包持有插件程序集中的回调,留着会阻止插件 AssemblyLoadContext 卸载。 - /// 按 "PluginTray.<pluginId>" 前缀匹配,覆盖插件注册多个菜单项的情况。 - /// - internal int RemovePluginTrayMenuItemsByPrefix(string pluginId) - { - if (string.IsNullOrWhiteSpace(pluginId)) return 0; - try - { - var trayMenu = GetPluginTaskbarIcon()?.ContextMenu; - if (trayMenu == null) return 0; - - var prefix = "PluginTray." + pluginId; - var doomed = trayMenu.Items.OfType() - .Where(mi => mi.Name != null - && (mi.Name == prefix || mi.Name.StartsWith(prefix + ".", StringComparison.Ordinal))) - .ToList(); - - foreach (var item in doomed) trayMenu.Items.Remove(item); - return doomed.Count; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"按插件移除托盘菜单项失败: {ex.Message}", LogHelper.LogType.Warning); - return 0; - } - } - - internal void ShowPluginMainWindow() - { - var mainWin = Current.MainWindow as MainWindow; - if (mainWin?.IsLoaded != true) return; - - var hideItem = FindTrayMenuItem("HideICCMainWindowTrayIconMenuItem"); - if (hideItem != null) hideItem.IsChecked = false; // 触发 UnChecked → Show + 启用相关菜单项 - else if (!mainWin.IsVisible) mainWin.Show(); - } - - internal void HidePluginMainWindow() - { - var mainWin = Current.MainWindow as MainWindow; - if (mainWin?.IsLoaded != true) return; - - var hideItem = FindTrayMenuItem("HideICCMainWindowTrayIconMenuItem"); - if (hideItem != null) hideItem.IsChecked = true; // 触发 Checked → Hide + 禁用相关菜单项 - else if (mainWin.IsVisible) mainWin.Hide(); - } - - #endregion - + private void TaskbarTrayIcon_TrayLeftMouseDown(object sender, RoutedEventArgs e) => TrayIcon.TrayLeftMouseDown(sender, e); + private void TaskbarTrayIcon_TrayRightMouseDown(object sender, RoutedEventArgs e) => TrayIcon.TrayRightMouseDown(sender, e); + private void SysTrayMenu_Opened(object sender, RoutedEventArgs e) => TrayIcon.SysTrayMenuOpened(sender, e); + private void SysTrayMenu_Closed(object sender, RoutedEventArgs e) => TrayIcon.SysTrayMenuClosed(sender, e); + private void TempShowMainWindowTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) => TrayIcon.TempShowMainWindowClicked(sender, e); + private void OpenSettingsTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) => TrayIcon.OpenSettingsClicked(sender, e); + private void DisableAllHotkeysMenuItem_Clicked(object sender, RoutedEventArgs e) => TrayIcon.DisableAllHotkeysClicked(sender, e); + private void ForceFullScreenTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) => TrayIcon.ForceFullScreenClicked(sender, e); + private void FoldFloatingBarTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) => TrayIcon.FoldFloatingBarClicked(sender, e); + private void ResetFloatingBarPositionTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) => TrayIcon.ResetFloatingBarPositionClicked(sender, e); + private void RestartAppTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) => TrayIcon.RestartAppClicked(sender, e); + private void CloseAppTrayIconMenuItem_Clicked(object sender, RoutedEventArgs e) => TrayIcon.CloseAppClicked(sender, e); + private void HideICCMainWindowTrayIconMenuItem_Checked(object sender, RoutedEventArgs e) => TrayIcon.HideMainWindowChecked(sender, e); + private void HideICCMainWindowTrayIconMenuItem_UnChecked(object sender, RoutedEventArgs e) => TrayIcon.HideMainWindowUnchecked(sender, e); + + internal void ShowTrayContextMenu() => TrayIcon.ShowTrayContextMenu(); + internal TaskbarIcon GetPluginTaskbarIcon() => TrayIcon.GetPluginTaskbarIcon(); + internal void SetPluginTrayIconVisibility(bool visible) => TrayIcon.SetPluginTrayIconVisibility(visible); + internal bool AddPluginTrayMenuItem(string id, string text, Action onClicked) => TrayIcon.AddPluginTrayMenuItem(id, text, onClicked); + internal bool RemovePluginTrayMenuItem(string id) => TrayIcon.RemovePluginTrayMenuItem(id); + internal bool HasPluginTrayMenuItem(string id) => TrayIcon.HasPluginTrayMenuItem(id); + internal int RemovePluginTrayMenuItemsByPrefix(string pluginId) => TrayIcon.RemovePluginTrayMenuItemsByPrefix(pluginId); + internal void ShowPluginMainWindow() => TrayIcon.ShowPluginMainWindow(); + internal void HidePluginMainWindow() => TrayIcon.HidePluginMainWindow(); } } diff --git a/Ink Canvas/MainWindow_cs/MW_UInk.cs b/Ink Canvas/MainWindow_cs/MW_UInk.cs index 59c844649..7e48fa503 100644 --- a/Ink Canvas/MainWindow_cs/MW_UInk.cs +++ b/Ink Canvas/MainWindow_cs/MW_UInk.cs @@ -265,10 +265,7 @@ private void OpenUInkFile(string path) { if (extractDir != null) { - try { if (Directory.Exists(extractDir)) Directory.Delete(extractDir, true); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[UInk] 清理临时解压目录失败: {ex.Message}", LogHelper.LogType.Info); - } + try { if (Directory.Exists(extractDir)) Directory.Delete(extractDir, true); } catch (Exception ex) { LogService.LogException(ex); } } } } @@ -348,10 +345,7 @@ private void RestoreUInkMediaForPage(IEnumerable mediaList, Dictionar { if (mediaList == null) return; var cacheDir = Path.Combine(Settings.Automation.AutoSavedStrokesLocation, "UInk Media"); - try { Directory.CreateDirectory(cacheDir); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[UInk] 创建媒体缓存目录失败: {ex.Message}", LogHelper.LogType.Info); - } + try { Directory.CreateDirectory(cacheDir); } catch (Exception ex) { LogService.LogException(ex); } foreach (var m in mediaList) { diff --git a/Ink Canvas/MainWindow_cs/MW_UriHandler.cs b/Ink Canvas/MainWindow_cs/MW_UriHandler.cs index 114f2791e..54be7f316 100644 --- a/Ink Canvas/MainWindow_cs/MW_UriHandler.cs +++ b/Ink Canvas/MainWindow_cs/MW_UriHandler.cs @@ -444,10 +444,7 @@ private void HandleUriConfigProfileSwitch(string profileName) } catch (Exception ex) { - try { File.WriteAllText(resultPath, "error: " + ex.Message, System.Text.Encoding.UTF8); } catch (Exception inner) - { - LogHelper.WriteLogToFile($"[Nav] 写入 URI 结果文件失败: {inner.Message}", LogHelper.LogType.Info); - } + try { File.WriteAllText(resultPath, "error: " + ex.Message, System.Text.Encoding.UTF8); } catch (Exception ex2) { LogService.LogException(ex2); } LogHelper.WriteLogToFile($"URI 切换配置方案失败: {ex.Message}", LogHelper.LogType.Error); } } diff --git a/Ink Canvas/MainWindow_cs/MW_VideoPresenter.cs b/Ink Canvas/MainWindow_cs/MW_VideoPresenter.cs index e9a09e141..784b89e28 100644 --- a/Ink Canvas/MainWindow_cs/MW_VideoPresenter.cs +++ b/Ink Canvas/MainWindow_cs/MW_VideoPresenter.cs @@ -462,11 +462,7 @@ private void EnterVideoPresenterSpecialMode() } // VideoCaptureElement 自己管理渲染(VMR9 + D3DImage),不需要清空 Source // 若之前的预览仍在运行,先停止避免设备占用 - try { VideoPresenterFullCanvasImage?.Stop(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 进入展台特殊模式前停止上一次的实时预览失败: {ex.Message}", LogHelper.LogType.Info); - } + try { VideoPresenterFullCanvasImage?.Stop(); } catch (Exception ex) { LogService.LogException(ex); } // 重置缩放 _boothPreviewScale = 1.0; @@ -540,31 +536,19 @@ private void ExitVideoPresenterSpecialMode() _liveStrokesSnapshot.Clear(); foreach (var p in _capturedPhotos) { - try { p?.Strokes?.Clear(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 退出特殊模式时清空已拍摄照片槽位中的墨迹失败: {ex.Message}", LogHelper.LogType.Info); - } + try { p?.Strokes?.Clear(); } catch (Exception ex) { LogService.LogException(ex); } } // 清理鼠标拖动状态(防止退出时鼠标仍被捕获) if (_isBoothMouseDragging) { _isBoothMouseDragging = false; - try { inkCanvas?.ReleaseMouseCapture(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 退出特殊模式时释放画布鼠标捕获失败: {ex.Message}", LogHelper.LogType.Info); - } + try { inkCanvas?.ReleaseMouseCapture(); } catch (Exception ex) { LogService.LogException(ex); } } _boothTouchSavedInkEditingMode = null; // 停止 VideoCaptureElement 预览,释放 DirectShow 图 - try { VideoPresenterFullCanvasImage?.Stop(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 退出特殊模式时停止展台实时预览失败: {ex.Message}", LogHelper.LogType.Info); - } + try { VideoPresenterFullCanvasImage?.Stop(); } catch (Exception ex) { LogService.LogException(ex); } // 释放摄像头属性控制占用的常驻 FilterGraphNoThread 资源。 // 必须延迟到后台线程执行:VideoCaptureElement.Stop() 是异步的(WPFMediaKit 内部异步清理 FilterGraph), // 立即同步释放 source filter 的 COM 对象会与 VideoCaptureElement 的 DirectShow 图清理竞争设备锁, @@ -846,7 +830,7 @@ private void RotateBoothStrokesFromBaseline(double targetAngleDegrees) LogHelper.LogType.Info); } } - catch { } + catch (Exception ex) { LogService.LogException(ex); } _isApplyingRotationToStrokes = true; var prevCommitType = _currentCommitType; @@ -882,7 +866,7 @@ private void RotateBoothStrokesFromBaseline(double targetAngleDegrees) $"afterCenter=({afterBBox.X+afterBBox.Width/2:F1},{afterBBox.Y+afterBBox.Height/2:F1})", LogHelper.LogType.Info); } - catch { } + catch (Exception ex) { LogService.LogException(ex); } } finally { @@ -938,17 +922,11 @@ private void ShowBoothTransientMessage(string message) VideoPresenterSearchingText.Text = "正在查找展台设备..."; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 展台临时提示 2.5s 后自动隐藏失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } }; timer.Start(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 显示展台临时提示失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -1176,10 +1154,7 @@ private void VideoPresenterFullCanvasImage_MediaOpened(object sender, RoutedEven "[VideoPresenter] MediaOpened: 摄像头已打开,隐藏占位文字", LogHelper.LogType.Info); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] MediaOpened 处理失败(隐藏占位文字/启用拍照按钮): {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -1205,10 +1180,7 @@ private void VideoPresenterFullCanvasImage_MediaFailed(object sender, WPFMediaKi LogHelper.LogType.Error); ErrorOccurredRelay?.Invoke(this, $"摄像头打开失败: {msg}"); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] MediaFailed 处理失败(写日志/抛出失败通知): {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -2140,20 +2112,12 @@ private Task StartVideoCaptureElementPreviewAsync(int cameraIndex) } // 若已在播放同一设备,先停止 - try { VideoPresenterFullCanvasImage.Stop(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 重建预览图前停止同设备的实时预览失败: {ex.Message}", LogHelper.LogType.Info); - } + try { VideoPresenterFullCanvasImage.Stop(); } catch (Exception ex) { LogService.LogException(ex); } // 关键:显式将 VideoCaptureDevice 置为 null,强制依赖属性变化回调触发 CleanUp()。 // 否则后续 VideoCaptureDevice = device(同一个 DsDevice 引用)会被 WPF 依赖属性系统判定为 // "未变化",OnVideoCaptureDeviceChanged 不触发,WPFMediaKit 不会重建 FilterGraph, // 导致切换分辨率/帧率时 DesiredPixelWidth/Height/FPS 不生效(仍是旧分辨率)。 - try { VideoPresenterFullCanvasImage.VideoCaptureDevice = null; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 重建预览图前把 VideoCaptureDevice 置空以强制 CleanUp 失败: {ex.Message}", LogHelper.LogType.Info); - } + try { VideoPresenterFullCanvasImage.VideoCaptureDevice = null; } catch (Exception ex) { LogService.LogException(ex); } // 等待 FilterGraph 完全释放设备占用: // WPFMediaKit 的 Stop() + CleanUp() 是异步的,FilterGraph 释放需要时间。 // 立即 BeginInit/EndInit/Play 会导致设备仍被占用,新图构建失败, @@ -2268,10 +2232,7 @@ private Task StartVideoCaptureElementPreviewAsync(int cameraIndex) _ = StartVideoCaptureElementPreviewAsync(camIdx).ConfigureAwait(false); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] MediaOpened 兜底定时器(1.5s 未触发则重启预览)处理失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } }; fallbackTimer.Start(); } @@ -2800,7 +2761,7 @@ private void BtnRotateImage_Click(object sender, RoutedEventArgs e) $"scale={_boothPreviewScale}, translate=({_boothPreviewTranslateX},{_boothPreviewTranslateY})", LogHelper.LogType.Info); } - catch { } + catch (Exception ex) { LogService.LogException(ex); } VideoPresenterFullCanvasRotation.Angle = (VideoPresenterFullCanvasRotation.Angle + 90.0) % 360.0; // 传视觉角度(cameraRotation*90 + Angle)给墨迹变换, // 因为 GetBoothRotationMatrix 按"实时画面 0° 基准"算缩放,照片预览需用视觉角度 @@ -2813,7 +2774,7 @@ private void BtnRotateImage_Click(object sender, RoutedEventArgs e) $"photo.ActualSize={VideoPresenterFrozenFrameImage.ActualWidth}x{VideoPresenterFrozenFrameImage.ActualHeight}", LogHelper.LogType.Info); } - catch { } + catch (Exception ex) { LogService.LogException(ex); } return; } @@ -2882,10 +2843,7 @@ private void SyncPhotoCorrectionAccelerationComboBox() if (accelIdx >= 0 && accelIdx < cb.Items.Count && cb.SelectedIndex != accelIdx) cb.SelectedIndex = accelIdx; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 同步相片校正加速度下拉框选中项失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } // ==================================================================== @@ -3004,10 +2962,7 @@ private void SyncBoothBrightnessSlider() RefreshBoothPropSlidersEnabled(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 刷新摄像头属性滑块取值失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// 按 _cameraService 支持情况启用/禁用全部属性滑块并刷新 tooltip。 @@ -3026,10 +2981,7 @@ private void RefreshBoothPropSlidersEnabled() : Ink_Canvas.Properties.BoothStrings.ResourceManager.GetString(b.notSupportedTooltipKey, Ink_Canvas.Properties.BoothStrings.Culture); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 按摄像头支持情况刷新属性滑块启用状态与 tooltip 失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -3826,10 +3778,7 @@ private void ClearFrozenFrame() VideoPresenterFrozenFrameImage.RenderTransform = _frozenFrameOriginalRenderTransform; // VideoPresenterFrozenOverlay(旧全屏蒙版)和 VideoPresenterFrozenThumbnail(侧栏小预览)均已从 XAML 移除 } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 清除展台冻结画面(恢复实时预览)失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -3880,7 +3829,7 @@ private void SwitchBoothToPhotoPage(int photoIndex) $"strokesCount={inkCanvas?.Strokes?.Count ?? 0}", LogHelper.LogType.Info); } - catch { } + catch (Exception ex) { LogService.LogException(ex); } VideoPresenterFrozenFrameImage.Source = photo.Image; VideoPresenterFrozenFrameImage.Visibility = Visibility.Visible; @@ -3922,18 +3871,14 @@ private void SwitchBoothToPhotoPage(int photoIndex) $"photo.ActualSize={VideoPresenterFrozenFrameImage.ActualWidth}x{VideoPresenterFrozenFrameImage.ActualHeight}", LogHelper.LogType.Info); } - catch { } + catch (Exception ex) { LogService.LogException(ex); } } // 停止并隐藏 VideoCaptureElement,只显示冻结照片 // (Stretch=Uniform 留黑边处会看到底层实时画面,必须停止+隐藏) if (VideoPresenterFullCanvasImage != null) { - try { VideoPresenterFullCanvasImage.Stop(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Booth] 切换到照片预览页时停止展台实时预览失败: {ex.Message}", LogHelper.LogType.Info); - } + try { VideoPresenterFullCanvasImage.Stop(); } catch (Exception ex) { LogService.LogException(ex); } VideoPresenterFullCanvasImage.Visibility = Visibility.Collapsed; } diff --git a/Ink Canvas/MainWindow_cs/MW_WinRTInk.cs b/Ink Canvas/MainWindow_cs/MW_WinRTInk.cs index 6b40d1349..48f383162 100644 --- a/Ink Canvas/MainWindow_cs/MW_WinRTInk.cs +++ b/Ink Canvas/MainWindow_cs/MW_WinRTInk.cs @@ -1,6 +1,7 @@ using Ink_Canvas.Helpers; using Ink_Canvas.Ink; using Ink_Canvas.Ink.WinRT; +using Ink_Canvas.Models; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -1079,23 +1080,23 @@ private LogicalInkTool ResolveLogicalInkTool() if (IsBoardRoamingMode) return LogicalInkTool.BoardRoam; if (drawingShapeMode != 0 - || string.Equals(_currentToolMode, "shape", StringComparison.OrdinalIgnoreCase)) + || _currentToolModeEnum == ToolMode.Shape) return LogicalInkTool.Shape; - switch (_currentToolMode) + switch (_currentToolModeEnum) { - case "pen": - case "color": + case ToolMode.Pen: + case ToolMode.Color: return LogicalInkTool.Pen; - case "eraser": + case ToolMode.Eraser: return LogicalInkTool.PointEraser; - case "eraserByStrokes": + case ToolMode.EraserByStrokes: return LogicalInkTool.StrokeEraser; - case "select": + case ToolMode.Select: return LogicalInkTool.Select; - case "roaming": + case ToolMode.Roaming: return LogicalInkTool.BoardRoam; - case "cursor": + case ToolMode.Cursor: default: return LogicalInkTool.Cursor; } diff --git a/Ink Canvas/MainWindow_cs/MW_WindowChromeHitTest.cs b/Ink Canvas/MainWindow_cs/MW_WindowChromeHitTest.cs index 1d7fb163a..b9711cf37 100644 --- a/Ink Canvas/MainWindow_cs/MW_WindowChromeHitTest.cs +++ b/Ink Canvas/MainWindow_cs/MW_WindowChromeHitTest.cs @@ -59,10 +59,7 @@ internal void BuildSmartModeRects() LogHelper.WriteLogToFile($"[SmartMode] COM 回退: Slide={_smartModeSlideWidth}x{_smartModeSlideHeight}磅", LogHelper.LogType.Info); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 智慧模式 BuildSmartModeRects 回退1 经 COM 读取幻灯片尺寸失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } // 回退2:通过 _pptManager if (_smartModeSlideWidth <= 0) @@ -77,10 +74,7 @@ internal void BuildSmartModeRects() LogHelper.WriteLogToFile($"[SmartMode] PPTManager 回退: Slide={_smartModeSlideWidth}x{_smartModeSlideHeight}磅", LogHelper.LogType.Info); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 智慧模式 BuildSmartModeRects 回退2 经 _pptManager 读取幻灯片尺寸失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } // 回退3:使用标准 16:9 尺寸 @@ -102,25 +96,14 @@ internal void BuildSmartModeRects() { dynamic app = appObj; dynamic ssw = null; - try { ssw = app.ActivePresentation?.SlideShowWindow; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 智慧模式 BuildSmartModeRects 经 dynamic 读取 ActivePresentation.SlideShowWindow 失败: {ex.Message}", LogHelper.LogType.Info); - } + try { ssw = app.ActivePresentation?.SlideShowWindow; } catch (Exception ex) { LogService.LogException(ex); } if (ssw != null) { - try { _smartModeSlideShowHwnd = new IntPtr(ssw.HWND); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 智慧模式 BuildSmartModeRects 读取放映窗口 HWND 失败: {ex.Message}", LogHelper.LogType.Info); - } + try { _smartModeSlideShowHwnd = new IntPtr(ssw.HWND); } catch (Exception ex) { LogService.LogException(ex); } } } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 智慧模式 BuildSmartModeRects 经 _pptManager 取 PPT 应用对象失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } if (_smartModeSlideShowHwnd == IntPtr.Zero) { @@ -143,11 +126,7 @@ internal void BuildSmartModeRects() } uint dpi = 96; - try { dpi = PInvoke.GetDpiForWindow(new HWND(_smartModeSlideShowHwnd)); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 智慧模式 BuildSmartModeRects 获取放映窗口 DPI 失败,按 96 DPI 处理: {ex.Message}", LogHelper.LogType.Info); - } + try { dpi = PInvoke.GetDpiForWindow(new HWND(_smartModeSlideShowHwnd)); } catch (Exception ex) { LogService.LogException(ex); } double dpiScale = dpi / 96.0; double winLeft = winRect.left; diff --git a/Ink Canvas/Models/TimerState.cs b/Ink Canvas/Models/TimerState.cs new file mode 100644 index 000000000..4b99423e8 --- /dev/null +++ b/Ink Canvas/Models/TimerState.cs @@ -0,0 +1,85 @@ +using System; + +namespace Ink_Canvas.Models +{ + /// + /// 计时器模式。描述倒计时状态机的当前阶段。 + /// + internal enum TimerMode + { + /// 未启动或已结束。 + Idle, + /// 倒计时进行中。 + Running, + /// 已暂停。 + Paused, + /// 超时正计时( overtime count-up )模式。 + Overtime + } + + /// + /// 计时器状态快照(POCO)。由 在每次 tick + /// 或状态变更时产生,经 StateChanged 事件推送给窗口壳;窗口只读渲染,不回写。 + /// + internal sealed class TimerState + { + public TimerState( + TimerMode mode, + TimeSpan remaining, + TimeSpan overtime, + TimeSpan elapsed, + TimeSpan total, + double spentPercent, + bool isRunning, + bool isPaused, + bool isOvertimeMode, + bool justCompleted, + bool justEnteredOvertime, + bool progressiveReminderDue) + { + Mode = mode; + Remaining = remaining; + Overtime = overtime; + Elapsed = elapsed; + Total = total; + SpentPercent = spentPercent; + IsRunning = isRunning; + IsPaused = isPaused; + IsOvertimeMode = isOvertimeMode; + JustCompleted = justCompleted; + JustEnteredOvertime = justEnteredOvertime; + ProgressiveReminderDue = progressiveReminderDue; + } + + /// 当前模式。 + public TimerMode Mode { get; } + + /// 剩余时间(已按原窗口规则做 +1s 取整;超时模式下可能为负)。 + public TimeSpan Remaining { get; } + + /// 超时正计时时长(仅 Overtime 模式有效)。 + public TimeSpan Overtime { get; } + + /// 已流逝时间(暂停补偿后)。 + public TimeSpan Elapsed { get; } + + /// 设定总时长。 + public TimeSpan Total { get; } + + /// 已流逝比例(Elapsed / Total)。 + public double SpentPercent { get; } + + public bool IsRunning { get; } + public bool IsPaused { get; } + public bool IsOvertimeMode { get; } + + /// 本帧刚自然结束(未启用超时正计时)。一次性边沿标志。 + public bool JustCompleted { get; } + + /// 本帧刚进入超时正计时模式。一次性边沿标志。 + public bool JustEnteredOvertime { get; } + + /// 本帧触发渐进提醒(剩余 ≤6s 且 >0,每次启动仅一次)。一次性边沿标志。 + public bool ProgressiveReminderDue { get; } + } +} diff --git a/Ink Canvas/Models/ToolMode.cs b/Ink Canvas/Models/ToolMode.cs new file mode 100644 index 000000000..40efb9668 --- /dev/null +++ b/Ink Canvas/Models/ToolMode.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; + +namespace Ink_Canvas.Models +{ + /// + /// 工具模式。MainWindow 当前以字符串字面量表示工具模式(_currentToolMode), + /// 本枚举是其类型化对应物,用于逐步替换字符串比较点。 + /// + /// + /// 枚举成员名 = 字面量的 PascalCase。字面量集合经 MainWindow/ 全量 grep 穷举确认, + /// 其中 tool/state URI 会把当前模式字符串原样写入临时文件对外暴露, + /// 因此这些字符串是持久化锚点,永不可改名。 + /// + internal enum ToolMode + { + /// 未识别/未注册的模式字符串。 对未知输入返回此值。 + Unknown = 0, + /// 光标(闲置)模式,字面量 "cursor"。 + Cursor, + /// 画笔模式,字面量 "pen"。 + Pen, + /// 快速颜色笔模式,字面量 "color"。 + Color, + /// 点擦除模式,字面量 "eraser"。 + Eraser, + /// 笔画擦除模式,字面量 "eraserByStrokes"。 + EraserByStrokes, + /// 选择工具模式,字面量 "select"。 + Select, + /// 形状绘制模式,字面量 "shape"。 + Shape, + /// 白板漫游模式,字面量 "roaming"。 + Roaming + } + + /// + /// 与现有字符串字面量之间的无损双向往返映射。 + /// + internal static class ToolModeMapping + { + private static readonly (ToolMode Mode, string Literal)[] Entries = + { + (ToolMode.Cursor, "cursor"), + (ToolMode.Pen, "pen"), + (ToolMode.Color, "color"), + (ToolMode.Eraser, "eraser"), + (ToolMode.EraserByStrokes, "eraserByStrokes"), + (ToolMode.Select, "select"), + (ToolMode.Shape, "shape"), + (ToolMode.Roaming, "roaming"), + }; + + private static readonly Dictionary ModeToString = BuildModeToString(); + private static readonly Dictionary StringToMode = BuildStringToMode(); + + private static Dictionary BuildModeToString() + { + var map = new Dictionary(); + foreach (var (mode, literal) in Entries) + { + map[mode] = literal; + } + return map; + } + + private static Dictionary BuildStringToMode() + { + var map = new Dictionary(System.StringComparer.Ordinal); + foreach (var (mode, literal) in Entries) + { + map[literal] = mode; + } + return map; + } + + /// + /// 枚举 → 内部字符串字面量。 返回 。 + /// + internal static string ToInternalString(ToolMode mode) + => ModeToString.TryGetValue(mode, out var literal) ? literal : string.Empty; + + /// + /// 内部字符串字面量 → 枚举。对未注册的字符串(含 null/空串)返回 + /// ,不抛异常;比较为序数、大小写敏感。 + /// + internal static ToolMode FromInternalString(string mode) + => mode != null && StringToMode.TryGetValue(mode, out var result) ? result : ToolMode.Unknown; + } +} diff --git a/Ink Canvas/Plugins/PluginAuthorizationService.cs b/Ink Canvas/Plugins/PluginAuthorizationService.cs index 32d301abf..9e0766102 100644 --- a/Ink Canvas/Plugins/PluginAuthorizationService.cs +++ b/Ink Canvas/Plugins/PluginAuthorizationService.cs @@ -296,10 +296,7 @@ private void Load() return; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] 授权记录按 v2 版本化格式解析失败,回退尝试旧平铺格式: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { Helpers.LogService.LogException(ex); } // 旧平铺格式(key = pluginId|fullPath,value = SHA-256)。 // 迁移:插件只要曾授权过任意一个 DLL,即升级为插件级信任, @@ -320,10 +317,7 @@ private void Load() } } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] 读取插件授权记录失败(已授权列表可能为空): {_filePath}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { Helpers.LogService.LogException(ex); } } private void Save() @@ -341,10 +335,7 @@ private void Save() File.WriteAllText(_filePath, JsonSerializer.Serialize(store, new JsonSerializerOptions { WriteIndented = true })); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] 保存插件授权记录失败(本次授权可能无法在下次启动后免询问): {_filePath}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { Helpers.LogService.LogException(ex); } } private static string ComputeHash(string path) diff --git a/Ink Canvas/Plugins/PluginIpcService.cs b/Ink Canvas/Plugins/PluginIpcService.cs index 8ce9777da..2e12ad75d 100644 --- a/Ink Canvas/Plugins/PluginIpcService.cs +++ b/Ink Canvas/Plugins/PluginIpcService.cs @@ -316,21 +316,9 @@ public void Dispose() if (_disposed) return; _disposed = true; - try { _cts.Cancel(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] PluginIpc 释放时取消 CancellationTokenSource 失败: {ex.Message}", LogHelper.LogType.Info); - } - try { _listenTask?.Wait(TimeSpan.FromSeconds(1)); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] PluginIpc 释放时等待监听任务结束失败(监听循环可能仍在运行): {ex.Message}", LogHelper.LogType.Info); - } - try { _cts.Dispose(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] PluginIpc 释放时释放 CancellationTokenSource 失败: {ex.Message}", LogHelper.LogType.Info); - } + try { _cts.Cancel(); } catch (Exception ex) { LogService.LogException(ex); } + try { _listenTask?.Wait(TimeSpan.FromSeconds(1)); } catch (Exception ex) { LogService.LogException(ex); } + try { _cts.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } } } } diff --git a/Ink Canvas/Plugins/PluginManager.cs b/Ink Canvas/Plugins/PluginManager.cs index 7ea4be82d..ee20f7e26 100644 --- a/Ink Canvas/Plugins/PluginManager.cs +++ b/Ink Canvas/Plugins/PluginManager.cs @@ -235,10 +235,7 @@ private void LoadDisabledPlugins() foreach (var id in list) _disabledPlugins.Add(id); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] 读取已禁用插件列表失败(禁用状态可能丢失): {_disabledPluginsFile}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } private void SaveDisabledPlugins() @@ -251,10 +248,7 @@ private void SaveDisabledPlugins() System.Text.Json.JsonSerializer.Serialize(_disabledPlugins.ToList(), new System.Text.Json.JsonSerializerOptions { WriteIndented = true })); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] 保存已禁用插件列表失败(禁用状态可能无法持久化): {_disabledPluginsFile}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -299,10 +293,7 @@ public void LogPlugin(string pluginId, string level, string message) DateTime.Now, level, message, Environment.NewLine); File.AppendAllText(logFile, line); } - catch (Exception ex) - { - LogCallbackException($"写入插件 \"{pluginId}\" 的独立日志文件", ex); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -1489,10 +1480,7 @@ private void TryWriteUninstallMarker(string folder) File.WriteAllText(Path.Combine(folder, ".uninstall"), ""); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] 写入 .uninstall 标记文件失败(该插件目录下次启动不会被兜底清理): {folder}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -1680,10 +1668,7 @@ private void DiagnoseLivePluginReferences(string pluginId) if (string.IsNullOrEmpty(module.FileName)) continue; loaded.Add(module.FileName); } - catch (Exception ex) - { - LogCallbackException("诊断插件占用: 读取进程已加载模块信息失败(跳过该模块)", ex); - } + catch (Exception ex) { LogService.LogException(ex); } } } catch (Exception ex) @@ -1797,10 +1782,7 @@ private void WalkAndReportPinning(object obj, AssemblyLoadContext target, if (AssemblyLoadContext.GetLoadContext(t.Assembly) == target && seenPath.Add(path)) Log(string.Format("Pinning reference: {0} (Type={1})", path, t.FullName)); } - catch (Exception ex) - { - LogCallbackException("诊断插件占用: 检查类型是否归属目标 ALC", ex); - } + catch (Exception ex) { LogService.LogException(ex); } } else if (depth > 0) { @@ -2490,11 +2472,7 @@ protected override Assembly Load(AssemblyName assemblyName) var assembly = depContext.Load(assemblyName); if (assembly != null) return assembly; } - catch (Exception ex) - { - LogCallbackException( - $"插件 ALC 解析: 从依赖插件 \"{dep.Id}\" 的上下文加载程序集 \"{assemblyName.Name}\"", ex); - } + catch (Exception ex) { LogService.LogException(ex); } } } } diff --git a/Ink Canvas/Plugins/PluginMarketService.cs b/Ink Canvas/Plugins/PluginMarketService.cs index ca54e4ae4..306db3b7d 100644 --- a/Ink Canvas/Plugins/PluginMarketService.cs +++ b/Ink Canvas/Plugins/PluginMarketService.cs @@ -533,11 +533,7 @@ public async Task RequestDownloadPluginAsync(string id) if (task.IsCancelled) { - try { File.Delete(tempFile); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] 插件下载取消后删除临时文件失败: {tempFile}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + try { File.Delete(tempFile); } catch (Exception ex) { LogService.LogException(ex); } return false; } @@ -548,11 +544,7 @@ public async Task RequestDownloadPluginAsync(string id) if (!string.Equals(hash, merged.MarketEntry.DownloadSha256, StringComparison.OrdinalIgnoreCase)) { task.Error = "文件校验失败,可能已损坏。"; - try { File.Delete(tempFile); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] 插件包 SHA256 校验失败后删除临时文件失败: {tempFile}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + try { File.Delete(tempFile); } catch (Exception ex) { LogService.LogException(ex); } return false; } } @@ -758,10 +750,7 @@ private static string ComputeSha256(string filePath) var json = JsonConvert.DeserializeAnonymousType(File.ReadAllText(IndexMetaPath), new { lastRefresh = "" }); if (DateTime.TryParse(json?.lastRefresh, out var dt)) return dt; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Plugin] 读取插件市场索引最后刷新时间失败(将视为无刷新时间): {IndexMetaPath}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } return null; } diff --git a/Ink Canvas/Services/Classroom/TimerService.cs b/Ink Canvas/Services/Classroom/TimerService.cs new file mode 100644 index 000000000..412ffd6c4 --- /dev/null +++ b/Ink Canvas/Services/Classroom/TimerService.cs @@ -0,0 +1,415 @@ +using Ink_Canvas.Models; +using System; +using System.Timers; + +namespace Ink_Canvas.Services.Classroom +{ + /// + /// 课堂倒计时状态机(寄生提取)。承载 CountdownTimerWindow / NewStyleTimerWindow + /// 共有的纯计时逻辑:设定时间、开始/暂停/继续/重置、tick 递减、超时正计时模式。 + /// 纪律:禁止引用 UI 调度器 / 主窗体 / 任何 Visual 类型;设置项经 Func 注入; + /// 定时器保持 System.Timers.Timer 原类型(回调在线程池),UI 触碰由订阅端自行调度。 + /// + internal sealed class TimerService : IDisposable + { + private readonly Timer timer; + private readonly Func _tickIntervalPolicy; + private readonly Func _isOvertimeCountUpEnabled; + private readonly Func _isProgressiveReminderEnabled; + private readonly Func _now; + private readonly bool _enableRealTimer; + + /// + /// 状态变更事件。在定时器回调线程(线程池)上引发,订阅方需自行封送至 UI 线程。 + /// + public event Action StateChanged; + + /// 根据设定总时长决定 tick 间隔(毫秒)的策略。 + /// 是否启用超时正计时。 + /// 是否启用结束前渐进提醒。 + /// 时钟源,默认 DateTime.Now;测试可注入假时钟。 + /// false 时真实定时器不启停,仅供测试以假时钟手动驱动 TickCore。 + public TimerService( + Func tickIntervalPolicy, + Func isOvertimeCountUpEnabled, + Func isProgressiveReminderEnabled, + Func nowProvider = null, + bool enableRealTimer = true) + { + _tickIntervalPolicy = tickIntervalPolicy ?? throw new ArgumentNullException(nameof(tickIntervalPolicy)); + _isOvertimeCountUpEnabled = isOvertimeCountUpEnabled ?? throw new ArgumentNullException(nameof(isOvertimeCountUpEnabled)); + _isProgressiveReminderEnabled = isProgressiveReminderEnabled ?? throw new ArgumentNullException(nameof(isProgressiveReminderEnabled)); + _now = nowProvider ?? (() => DateTime.Now); + _enableRealTimer = enableRealTimer; + + timer = new Timer(); + timer.Elapsed += OnTimerElapsed; + timer.Interval = 50; + } + + /// + /// 旧版倒计时窗口(CountdownTimerWindow)的 tick 间隔策略:按总时长自适应。 + /// + public static double LegacyIntervalForTotal(TimeSpan total) + { + if (total.TotalSeconds <= 10) return 20; + if (total.TotalSeconds <= 60) return 30; + if (total.TotalSeconds <= 120) return 50; + return 100; + } + + // ---- 设定时间与状态字段(原样搬自两个计时器窗口)---- + + private int hour = 0; + private int minute = 0; + private int second = 0; + private int cachedStartHour = 0; + private int cachedStartMinute = 0; + private int cachedStartSecond = 0; + + private DateTime startTime = DateTime.Now; + private DateTime pauseTime = DateTime.Now; + + private bool isTimerRunning = false; + private bool isPaused = false; + private bool isOvertimeMode = false; + private bool hasPlayedProgressiveReminder = false; + + // 无调用方的历史遗留成员,随状态机原样搬入 + private TimeSpan remainingTime = TimeSpan.Zero; + + public int Hour { get => hour; set => hour = value; } + public int Minute { get => minute; set => minute = value; } + public int Second { get => second; set => second = value; } + + public bool IsRunning => isTimerRunning; + public bool IsPaused => isPaused; + public bool IsOvertimeMode => isOvertimeMode; + + /// 当前计时的起始时刻(暂停补偿后)。供窗口推算预计结束时刻。 + public DateTime StartTime => startTime; + + // ---- 状态机操作 ---- + + /// + /// 从空闲状态开始计时:缓存起始设定时间、复位标志、按策略设定 tick 间隔并启动定时器。 + /// + public void Start() + { + cachedStartHour = hour; + cachedStartMinute = minute; + cachedStartSecond = second; + + startTime = _now(); + isPaused = false; + isTimerRunning = true; + isOvertimeMode = false; + hasPlayedProgressiveReminder = false; + + timer.Interval = _tickIntervalPolicy(GetTotalTimeSpan()); + StartTimer(); + } + + /// 暂停计时。 + public void Pause() + { + pauseTime = _now(); + isPaused = true; + StopTimer(); + } + + /// 继续计时:把暂停时长补偿进起始时刻。 + public void Resume() + { + startTime += _now() - pauseTime; + isPaused = false; + StartTimer(); + } + + /// + /// 重置:停止定时器并复位运行/暂停/超时/提醒标志,保留当前设定时间。 + /// + public void Reset() + { + if (isTimerRunning) + { + StopTimer(); + } + isTimerRunning = false; + isPaused = false; + isOvertimeMode = false; + hasPlayedProgressiveReminder = false; + } + + /// + /// 运行中重新开始:起始时刻重置为现在并立即触发一次 tick(旧版窗口重置按钮第三分支)。 + /// + public void Restart() + { + startTime = _now(); + TickCore(); + } + + /// 仅停止:定时器停止、运行标志复位(新版窗口 StopTimer 语义)。 + public void Stop() + { + StopTimer(); + isTimerRunning = false; + } + + /// 仅复位运行标志(旧版窗口 Closing 语义,定时器下一次 tick 自停)。 + public void MarkStopped() + { + isTimerRunning = false; + } + + /// + /// 复位到起始设定时间快照(新版窗口 ApplyResetTimerState 的状态部分)。 + /// + public void ResetToStartSnapshot() + { + if (isTimerRunning) + { + StopTimer(); + isTimerRunning = false; + isPaused = false; + } + + hour = cachedStartHour; + minute = cachedStartMinute; + second = cachedStartSecond; + + isOvertimeMode = false; + hasPlayedProgressiveReminder = false; + } + + /// 复位渐进提醒标志(新版窗口 ApplyResetStateAfterStop 的状态部分)。 + public void ResetProgressiveReminderFlag() + { + hasPlayedProgressiveReminder = false; + } + + // ---- 查询(原 NewStyleTimerWindow 公开语义:暂停时返回 null,不做负值钳制)---- + + public TimeSpan GetTotalTimeSpan() + { + return new TimeSpan(hour, minute, second); + } + + public TimeSpan? GetElapsedTime() + { + if (isPaused) return null; + + return _now() - startTime; + } + + public TimeSpan? GetRemainingTime() + { + if (isPaused) return null; + + var elapsed = _now() - startTime; + var totalTimeSpan = new TimeSpan(hour, minute, second); + var leftTimeSpan = totalTimeSpan - elapsed; + + if (leftTimeSpan.Milliseconds > 0) leftTimeSpan += new TimeSpan(0, 0, 1); + + return leftTimeSpan; + } + + /// + /// 当前状态的渲染快照(供主题切换等场景复现 tick 渲染分支): + /// 运行中按当前时钟、暂停中按暂停时刻推算;剩余/超时值做非负钳制,不含边沿标志。 + /// + public TimerState Snapshot() + { + DateTime referenceTime = isPaused ? pauseTime : _now(); + TimeSpan elapsed = referenceTime - startTime; + TimeSpan total = GetTotalTimeSpan(); + double spentPercent = elapsed.TotalMilliseconds / total.TotalMilliseconds; + + if (!isOvertimeMode) + { + TimeSpan left = total - elapsed; + if (left.Milliseconds > 0) left += new TimeSpan(0, 0, 1); + if (left < TimeSpan.Zero) left = TimeSpan.Zero; + + return new TimerState(CurrentMode(), left, TimeSpan.Zero, elapsed, total, spentPercent, + isTimerRunning, isPaused, isOvertimeMode, false, false, false); + } + else + { + TimeSpan overtime = elapsed - total; + if (overtime < TimeSpan.Zero) overtime = TimeSpan.Zero; + + return new TimerState(CurrentMode(), TimeSpan.Zero, overtime, elapsed, total, spentPercent, + isTimerRunning, isPaused, isOvertimeMode, false, false, false); + } + } + + // ---- 历史遗留(无调用方,原样搬入,不改一行)---- + + // 更新剩余时间 + private void UpdateRemainingTime() + { + if (isTimerRunning && !isPaused) + { + // 获取当前剩余时间 + TimeSpan? currentRemaining = GetRemainingTime(); + if (currentRemaining.HasValue) + { + // 计算已经过去的时间 + TimeSpan elapsedTime = _now() - startTime; + + // 计算新的总时间 + TimeSpan newTotalTime = new TimeSpan(hour, minute, second); + + // 如果新设置的时间小于已经过去的时间,则设置为0 + if (newTotalTime <= elapsedTime) + { + remainingTime = TimeSpan.Zero; + } + else + { + // 否则,剩余时间 = 新总时间 - 已经过去的时间 + remainingTime = newTotalTime - elapsedTime; + } + } + else + { + // 如果没有剩余时间信息,直接设置新的剩余时间 + remainingTime = new TimeSpan(hour, minute, second); + } + } + } + + // 更新特定时间单位的剩余时间 + private void UpdateSpecificTimeUnit(int newHour, int newMinute, int newSecond) + { + if (isTimerRunning && !isPaused) + { + // 获取当前剩余时间 + TimeSpan? currentRemaining = GetRemainingTime(); + if (currentRemaining.HasValue) + { + // 计算已经过去的时间 + TimeSpan elapsedTime = _now() - startTime; + + // 计算新的总时间 + TimeSpan newTotalTime = new TimeSpan(newHour, newMinute, newSecond); + + // 如果新设置的时间小于已经过去的时间,则设置为0 + if (newTotalTime <= elapsedTime) + { + remainingTime = TimeSpan.Zero; + } + else + { + // 否则,剩余时间 = 新总时间 - 已经过去的时间 + remainingTime = newTotalTime - elapsedTime; + } + } + else + { + // 如果没有剩余时间信息,直接设置新的剩余时间 + remainingTime = new TimeSpan(newHour, newMinute, newSecond); + } + } + } + + // ---- tick 核心 ---- + + private void OnTimerElapsed(object sender, ElapsedEventArgs e) + { + TickCore(); + } + + /// + /// tick 核心逻辑。internal 以便等价性验证与后续单元测试在假时钟下直接驱动。 + /// + internal void TickCore() + { + if (!isTimerRunning || isPaused) + { + StopTimer(); + return; + } + + DateTime now = _now(); + TimeSpan elapsed = now - startTime; + TimeSpan total = GetTotalTimeSpan(); + double spentPercent = elapsed.TotalMilliseconds / total.TotalMilliseconds; + + if (!isOvertimeMode) + { + TimeSpan left = total - elapsed; + if (left.Milliseconds > 0) left += new TimeSpan(0, 0, 1); + + // 渐进提醒:剩余 ≤6s 且 >0,每次启动仅触发一次(仅新版窗口启用) + bool progressiveDue = left.TotalSeconds <= 6 && left.TotalSeconds > 0 && + _isProgressiveReminderEnabled() && !hasPlayedProgressiveReminder; + if (progressiveDue) hasPlayedProgressiveReminder = true; + + // 结束判定:旧版窗口谓词 spentTimePercent >= 1 与新版窗口谓词 left.TotalSeconds <= 0 + // 经推导均恰好等价于「未取整剩余时间 <= 0」,此处统一,行为不变。 + bool finished = (total - elapsed) <= TimeSpan.Zero; + + bool justCompleted = false; + bool justEnteredOvertime = false; + if (finished) + { + if (_isOvertimeCountUpEnabled()) + { + isOvertimeMode = true; + justEnteredOvertime = true; + } + else + { + StopTimer(); + isTimerRunning = false; + isPaused = false; + justCompleted = true; + } + } + + RaiseStateChanged(new TimerState(CurrentMode(), left, TimeSpan.Zero, elapsed, total, spentPercent, + isTimerRunning, isPaused, isOvertimeMode, justCompleted, justEnteredOvertime, progressiveDue)); + } + else + { + TimeSpan overtime = elapsed - total; + + RaiseStateChanged(new TimerState(CurrentMode(), total - elapsed, overtime, elapsed, total, spentPercent, + isTimerRunning, isPaused, isOvertimeMode, false, false, false)); + } + } + + private TimerMode CurrentMode() + { + if (isOvertimeMode) return TimerMode.Overtime; + if (isTimerRunning) return isPaused ? TimerMode.Paused : TimerMode.Running; + return TimerMode.Idle; + } + + private void RaiseStateChanged(TimerState state) + { + StateChanged?.Invoke(state); + } + + private void StartTimer() + { + if (_enableRealTimer) timer.Start(); + } + + private void StopTimer() + { + if (_enableRealTimer) timer.Stop(); + } + + public void Dispose() + { + timer.Stop(); + timer.Elapsed -= OnTimerElapsed; + timer.Dispose(); + } + } +} diff --git a/Ink Canvas/Services/Settings/SettingsStore.cs b/Ink Canvas/Services/Settings/SettingsStore.cs new file mode 100644 index 000000000..1acc8c7ac --- /dev/null +++ b/Ink Canvas/Services/Settings/SettingsStore.cs @@ -0,0 +1,423 @@ +using Ink_Canvas.Helpers; +using Ink_Canvas.Windows.SettingsViews.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OSVersionExtension; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ink_Canvas.Services +{ + /// + /// 设置存储服务(寄生提取首站)。承载从 MainWindow 搬出的纯设置读写逻辑。 + /// 全局设置实例经 静态可达,故本类零依赖注入; + /// 禁止持有 MainWindow 或任何 Visual 类型引用。 + /// + internal sealed class SettingsStore + { + /// + /// 将当前内存中的 Settings 序列化为格式化的 JSON 并写入应用程序配置文件(位于 App.RootPath 下的 Configs 目录或根设置文件)。 + /// + /// + /// 在写入前会确保目标目录/文件具有写入权限(使用 ProcessProtectionManager)。任何写入失败或异常都会被吞掉,调用方不会收到异常抛出。 + /// + public static void SaveSettingsToFile() => SettingsManager.SaveSettingsToFile(); + + /// + /// 将应用设置重置为推荐的默认配置。 + /// + /// + /// 该方法会重新创建全局 Settings 实例并应用推荐值,覆盖大部分子模块配置(如外观、画布、自动化、PPT、手势、高级选项等)。 + /// 在重置过程中会保留并恢复当前 Settings.Automation 中的 AutoDelSavedFiles 与 AutoDelSavedFilesDaysThreshold 两项值以避免意外删除策略变化。 + /// + public static void SetSettingsToRecommendation() + { + var AutoDelSavedFilesDays = SettingsManager.Settings.Automation.AutoDelSavedFiles; + var AutoDelSavedFilesDaysThreshold = SettingsManager.Settings.Automation.AutoDelSavedFilesDaysThreshold; + SettingsManager.Settings = new Settings(); + SettingsManager.Settings.Advanced.IsSpecialScreen = true; + SettingsManager.Settings.Advanced.IsQuadIR = false; + SettingsManager.Settings.Advanced.TouchMultiplier = 0.3; + SettingsManager.Settings.Advanced.NibModeBoundsWidth = 5; + SettingsManager.Settings.Advanced.FingerModeBoundsWidth = 20; + SettingsManager.Settings.Advanced.NibModeBoundsWidthThresholdValue = 2.5; + SettingsManager.Settings.Advanced.FingerModeBoundsWidthThresholdValue = 2.5; + SettingsManager.Settings.Advanced.NibModeBoundsWidthEraserSize = 0.8; + SettingsManager.Settings.Advanced.FingerModeBoundsWidthEraserSize = 0.8; + SettingsManager.Settings.Advanced.EraserBindTouchMultiplier = true; + SettingsManager.Settings.Advanced.IsLogEnabled = true; + SettingsManager.Settings.Advanced.IsSecondConfirmWhenShutdownApp = false; + SettingsManager.Settings.Advanced.IsEnableEdgeGestureUtil = false; + SettingsManager.Settings.Advanced.EdgeGestureUtilOnlyAffectBlackboardMode = false; + SettingsManager.Settings.Advanced.IsEnableFullScreenHelper = false; + SettingsManager.Settings.Advanced.IsEnableAvoidFullScreenHelper = OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows11; + SettingsManager.Settings.Advanced.IsEnableForceFullScreen = false; + SettingsManager.Settings.Advanced.IsEnableDPIChangeDetection = false; + SettingsManager.Settings.Advanced.IsEnableResolutionChangeDetection = false; + SettingsManager.Settings.Advanced.EnableMultiScreenSupport = true; + SettingsManager.Settings.Advanced.FollowMouseForScreenSelection = true; + + SettingsManager.Settings.Appearance.IsColorfulViewboxFloatingBar = false; + SettingsManager.Settings.Appearance.ViewboxFloatingBarScaleTransformValue = 1; + SettingsManager.Settings.Appearance.ViewboxBlackBoardScaleTransformValue = 0.8; + SettingsManager.Settings.Appearance.IsTransparentButtonBackground = true; + SettingsManager.Settings.Appearance.IsShowExitButton = true; + SettingsManager.Settings.Appearance.IsShowEraserButton = true; + SettingsManager.Settings.Appearance.IsShowHideControlButton = false; + SettingsManager.Settings.Appearance.IsShowLRSwitchButton = false; + SettingsManager.Settings.Appearance.IsShowModeFingerToggleSwitch = true; + SettingsManager.Settings.Appearance.IsShowQuickPanel = true; + SettingsManager.Settings.Appearance.Theme = 0; + SettingsManager.Settings.Appearance.EnableChickenSoupInWhiteboardMode = true; + SettingsManager.Settings.Appearance.EnableTimeDisplayInWhiteboardMode = true; + SettingsManager.Settings.Appearance.ChickenSoupSource = 1; + SettingsManager.Settings.Appearance.ViewboxFloatingBarOpacityValue = 1.0; + SettingsManager.Settings.Appearance.ViewboxFloatingBarOpacityInPPTValue = 1.0; + SettingsManager.Settings.Appearance.EnableTrayIcon = true; + + // 浮动栏按钮显示控制默认值 + SettingsManager.Settings.Appearance.IsShowQuickColorPalette = false; + SettingsManager.Settings.Appearance.QuickColorPaletteDisplayMode = 1; + SettingsManager.Settings.Appearance.EraserDisplayOption = 0; + + SettingsManager.Settings.Automation.IsAutoFoldInEasiNote = true; + SettingsManager.Settings.Automation.IsAutoFoldInEasiNoteIgnoreDesktopAnno = true; + SettingsManager.Settings.Automation.IsAutoFoldInEasiCamera = true; + SettingsManager.Settings.Automation.IsAutoFoldInEasiNote3C = false; + SettingsManager.Settings.Automation.IsAutoFoldInEasiNote3 = false; + SettingsManager.Settings.Automation.IsAutoFoldInEasiNote5C = true; + SettingsManager.Settings.Automation.IsAutoFoldInSeewoPincoTeacher = false; + SettingsManager.Settings.Automation.IsAutoFoldInHiteTouchPro = false; + SettingsManager.Settings.Automation.IsAutoFoldInHiteCamera = false; + SettingsManager.Settings.Automation.IsAutoFoldInWxBoardMain = false; + SettingsManager.Settings.Automation.IsAutoFoldInOldZyBoard = false; + SettingsManager.Settings.Automation.IsAutoFoldInMSWhiteboard = false; + SettingsManager.Settings.Automation.IsAutoFoldInAdmoxWhiteboard = false; + SettingsManager.Settings.Automation.IsAutoFoldInAdmoxBooth = false; + SettingsManager.Settings.Automation.IsAutoFoldInQPoint = false; + SettingsManager.Settings.Automation.IsAutoFoldInYiYunVisualPresenter = false; + SettingsManager.Settings.Automation.IsAutoFoldInMaxHubWhiteboard = false; + SettingsManager.Settings.Automation.IsAutoFoldInPPTSlideShow = false; + SettingsManager.Settings.Automation.IsAutoKillPPTService = false; + SettingsManager.Settings.Automation.IsAutoKillEasiNote = false; + SettingsManager.Settings.Automation.IsAutoKillVComYouJiao = false; + SettingsManager.Settings.Automation.IsAutoKillInkCanvas = false; + SettingsManager.Settings.Automation.IsAutoKillICA = false; + SettingsManager.Settings.Automation.IsAutoKillIDT = false; + SettingsManager.Settings.Automation.IsAutoKillSeewoLauncher2DesktopAnnotation = false; + SettingsManager.Settings.Automation.IsSaveScreenshotsInDateFolders = false; + SettingsManager.Settings.Automation.ScreenshotSaveFormat = 0; + SettingsManager.Settings.Automation.ScreenshotJpegQuality = 90; + SettingsManager.Settings.Automation.ScreenshotScaleMode = 0; + SettingsManager.Settings.Automation.IsAutoSaveStrokesAtScreenshot = true; + SettingsManager.Settings.Automation.IsAutoSaveScreenshotAtClear = true; + SettingsManager.Settings.Automation.IsAutoClearWhenExitingWritingMode = false; + SettingsManager.Settings.Automation.MinimumAutomationStrokeNumber = 0; + SettingsManager.Settings.Automation.AutoDelSavedFiles = AutoDelSavedFilesDays; + SettingsManager.Settings.Automation.AutoDelSavedFilesDaysThreshold = AutoDelSavedFilesDaysThreshold; + + //Settings.PowerPointSettings.IsShowPPTNavigation = true; + //Settings.PowerPointSettings.IsShowBottomPPTNavigationPanel = false; + //Settings.PowerPointSettings.IsShowSidePPTNavigationPanel = true; + SettingsManager.Settings.PowerPointSettings.PowerPointSupport = true; + SettingsManager.Settings.PowerPointSettings.IsShowCanvasAtNewSlideShow = false; + SettingsManager.Settings.PowerPointSettings.IsNoClearStrokeOnSelectWhenInPowerPoint = true; + SettingsManager.Settings.PowerPointSettings.IsShowStrokeOnSelectInPowerPoint = false; + SettingsManager.Settings.PowerPointSettings.IsAutoSaveStrokesInPowerPoint = true; + SettingsManager.Settings.PowerPointSettings.IsAutoSaveScreenShotInPowerPoint = true; + SettingsManager.Settings.PowerPointSettings.IsNotifyPreviousPage = false; + SettingsManager.Settings.PowerPointSettings.IsNotifyHiddenPage = false; + SettingsManager.Settings.PowerPointSettings.IsEnableTwoFingerGestureInPresentationMode = false; + SettingsManager.Settings.PowerPointSettings.IsEnableFingerGestureSlideShowControl = false; + SettingsManager.Settings.PowerPointSettings.IsSupportWPS = false; + SettingsManager.Settings.PowerPointSettings.EnablePPTButtonEnhancedPreview = false; + SettingsManager.Settings.PowerPointSettings.ShowPPTEnhancedPreviewLoadingAnimation = true; + + SettingsManager.Settings.Canvas.InkWidth = 2.5; + SettingsManager.Settings.Canvas.IsShowCursor = false; + SettingsManager.Settings.Canvas.InkStyle = 0; + SettingsManager.Settings.Canvas.HighlighterWidth = 20; + SettingsManager.Settings.Canvas.EraserSize = 1; + SettingsManager.Settings.Canvas.EraserType = 0; + SettingsManager.Settings.Canvas.EraserShapeType = 1; + SettingsManager.Settings.Canvas.HideStrokeWhenSelecting = false; + SettingsManager.Settings.Canvas.ClearCanvasAndClearTimeMachine = false; + SettingsManager.Settings.Canvas.FitToCurve = false; + SettingsManager.Settings.Canvas.UseAdvancedBezierSmoothing = true; + SettingsManager.Settings.Canvas.MergeInkSmoothingWithUndo = false; + SettingsManager.Settings.Canvas.EnablePressureTouchMode = false; + SettingsManager.Settings.Canvas.DisablePressure = false; + SettingsManager.Settings.Canvas.AutoStraightenLine = true; + SettingsManager.Settings.Canvas.AutoStraightenLineThreshold = 80; + SettingsManager.Settings.Canvas.PauseStraightenLine = false; + SettingsManager.Settings.Canvas.PauseStraightenDelay = 300; + SettingsManager.Settings.Canvas.LineEndpointSnapping = true; + SettingsManager.Settings.Canvas.LineEndpointSnappingThreshold = 15; + SettingsManager.Settings.Canvas.UsingWhiteboard = false; + SettingsManager.Settings.Canvas.HyperbolaAsymptoteOption = 0; + + SettingsManager.Settings.Gesture.IsEnableTwoFingerTranslate = true; + SettingsManager.Settings.Gesture.IsEnableTwoFingerZoom = false; + SettingsManager.Settings.Gesture.IsEnableTwoFingerRotation = false; + SettingsManager.Settings.Gesture.IsEnableTwoFingerRotationOnSelection = false; + + SettingsManager.Settings.InkToShape.IsInkToShapeEnabled = true; + SettingsManager.Settings.InkToShape.IsInkToShapeNoFakePressureRectangle = false; + SettingsManager.Settings.InkToShape.IsInkToShapeNoFakePressureTriangle = false; + SettingsManager.Settings.InkToShape.IsInkToShapeTriangle = true; + SettingsManager.Settings.InkToShape.IsInkToShapeRectangle = true; + SettingsManager.Settings.InkToShape.IsInkToShapeRounded = true; + SettingsManager.Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify = false; + SettingsManager.Settings.InkToShape.HandwritingCorrectionFontFamily = "Ink Free,KaiTi,Segoe Script"; + SettingsManager.Settings.InkToShape.HandwritingLanguageOverrideLcid = 0; + SettingsManager.Settings.InkToShape.HandwritingBeautifyDebounceMs = 2000; + + SettingsManager.Settings.Startup.IsEnableNibMode = false; + SettingsManager.Settings.Startup.IsAutoUpdate = true; + SettingsManager.Settings.Startup.IsAutoUpdateWithSilence = true; + SettingsManager.Settings.Startup.AutoUpdateWithSilenceStartTime = "06:00"; + SettingsManager.Settings.Startup.AutoUpdateWithSilenceEndTime = "22:00"; + SettingsManager.Settings.Startup.IsFoldAtStartup = false; + SettingsManager.Settings.Startup.StartupMode = StartupMode.Default; + } + + public string GetCorrectIcon(string iconType, bool isSolid = false) + { + if (SettingsManager.Settings.Appearance.UseLegacyFloatingBarUI) + { + // 使用老版图标 + switch (iconType) + { + case "cursor": + return isSolid ? XamlGraphicsIconGeometries.LegacySolidCursorIcon : XamlGraphicsIconGeometries.LegacyLinedCursorIcon; + case "pen": + return isSolid ? XamlGraphicsIconGeometries.LegacySolidPenIcon : XamlGraphicsIconGeometries.LegacyLinedPenIcon; + case "eraserStroke": + return isSolid ? XamlGraphicsIconGeometries.LegacySolidEraserStrokeIcon : XamlGraphicsIconGeometries.LegacyLinedEraserStrokeIcon; + case "eraserCircle": + return isSolid ? XamlGraphicsIconGeometries.LegacySolidEraserCircleIcon : XamlGraphicsIconGeometries.LegacyLinedEraserCircleIcon; + case "lassoSelect": + return isSolid ? XamlGraphicsIconGeometries.LegacySolidLassoSelectIcon : XamlGraphicsIconGeometries.LegacyLinedLassoSelectIcon; + } + } + else + { + // 使用新版图标 + switch (iconType) + { + case "cursor": + return isSolid ? XamlGraphicsIconGeometries.SolidCursorIcon : XamlGraphicsIconGeometries.LinedCursorIcon; + case "pen": + return isSolid ? XamlGraphicsIconGeometries.SolidPenIcon : XamlGraphicsIconGeometries.LinedPenIcon; + case "eraserStroke": + return isSolid ? XamlGraphicsIconGeometries.SolidEraserStrokeIcon : XamlGraphicsIconGeometries.LinedEraserStrokeIcon; + case "eraserCircle": + return isSolid ? XamlGraphicsIconGeometries.SolidEraserCircleIcon : XamlGraphicsIconGeometries.LinedEraserCircleIcon; + case "lassoSelect": + return isSolid ? XamlGraphicsIconGeometries.SolidLassoSelectIcon : XamlGraphicsIconGeometries.LinedLassoSelectIcon; + } + } + return ""; + } + + /// 笔锋下拉 UI 顺序:0 实时笔锋,1 基于点集,2 基于速率,3 关闭。与存储值 InkStyle:3,0,1,2 对应。 + internal static int PenStyleUiIndexFromInkStyle(int inkStyle) + { + switch (inkStyle) + { + case 3: return 0; + case 0: return 1; + case 1: return 2; + case 2: return 3; + default: return 1; + } + } + + internal static int InkStyleFromPenStyleUiIndex(int uiIndex) + { + switch (uiIndex) + { + case 0: return 3; + case 1: return 0; + case 2: return 1; + case 3: return 2; + default: return 0; + } + } + + internal string BuildHitokotoRequestUrl() + { + var cats = SettingsManager.Settings.Appearance.HitokotoCategories; + if (cats == null || cats.Count == 0) + cats = new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; + + var urlBuilder = new StringBuilder("https://v1.hitokoto.cn/?encode=text"); + foreach (var category in cats) + { + urlBuilder.Append($"&c={category}"); + } + + return urlBuilder.ToString(); + } + + /// + /// 清理配置文件中的过期设置 + /// + /// 用户配置的JSON字符串 + /// + /// 清理过期设置时: + /// 1. 创建默认配置对象 + /// 2. 将默认配置和用户配置都序列化为JObject + /// 3. 递归比较并删除用户配置中多余的键 + /// 4. 如果有清理操作,重新反序列化并保存 + /// 5. 记录清理结果到日志 + /// + internal void CleanupObsoleteSettings(string userConfigJson) + { + try + { + // 创建默认配置对象 + Settings defaultSettings = new Settings(); + + // 将默认配置和用户配置都序列化为JObject + JObject defaultConfigObj = JObject.FromObject(defaultSettings); EnsureDefaultConfigSchemaIncludesIgnoredNullKeys(defaultConfigObj); + JObject userConfigObj = JObject.Parse(userConfigJson); + + // 记录是否有清理或迁移操作 + bool hasChanges = false; + MigrateLegacyStartupMode(userConfigObj, ref hasChanges); + + // 递归比较并删除用户配置中多余的键 + RemoveObsoleteProperties(userConfigObj, defaultConfigObj, ref hasChanges); + + // 如果有清理操作,重新反序列化并保存 + if (hasChanges) + { + string cleanedJson = userConfigObj.ToString(Formatting.Indented); + SettingsManager.Settings = JsonConvert.DeserializeObject(cleanedJson); + SaveSettingsToFile(); + App.UpdateCachedSettingsJson(cleanedJson); + LogHelper.WriteLogToFile("已清理过期配置项", LogHelper.LogType.Event); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"清理过期配置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 递归删除用户配置中多余的属性 + /// + /// 用户配置的JObject + /// 默认配置的JObject + /// 是否有变更的引用标志 + /// + /// 递归删除多余属性时: + /// 1. 检查用户配置和默认配置是否为空 + /// 2. 获取需要删除的键列表 + /// 3. 遍历用户配置的所有属性 + /// 4. 如果默认配置中不存在该属性,标记为删除 + /// 5. 如果两个属性都是对象类型,递归比较 + /// 6. 处理数组中的对象(如自定义图标列表等) + /// 7. 删除标记的键 + /// 8. 设置变更标志 + /// + internal static void MigrateLegacyStartupMode(JObject userConfigObj, ref bool hasChanges) + { + if (!(userConfigObj?["startup"] is JObject startup)) return; + + if (startup["startupMode"] == null && startup["enableFastStartup"]?.Type == JTokenType.Boolean) + { + startup["startupMode"] = startup["enableFastStartup"].Value() + ? (int)StartupMode.Fastest + : (int)StartupMode.Faster; + hasChanges = true; + } + + if (startup.Remove("enableFastStartup")) + { + hasChanges = true; + } + } + + internal static void EnsureDefaultConfigSchemaIncludesIgnoredNullKeys(JObject defaultConfigObj) + { + if (defaultConfigObj == null) return; + if (defaultConfigObj["appearance"] is JObject appearance) + { + // 这些属性同时具备 NullValueHandling.Ignore 且默认值为 null, + // 不会出现在默认 JObject 中。CleanupObsoleteSettings 会把它们 + // 误判为"过期"并立即 SaveSettingsToFile 删除,于是用户自建语录 + // 被静默清空。补成 null 占位让 RemoveObsoleteProperties 放行。 + foreach (var ignoredKey in new[] { "hitokotoCategories", "customTipsSchemes", "enabledPresetTipsSources" }) + { + if (!appearance.ContainsKey(ignoredKey)) + appearance[ignoredKey] = JValue.CreateNull(); + } + } + } + + internal void RemoveObsoleteProperties(JObject userObj, JObject defaultObj, ref bool hasChanges) + { + if (userObj == null || defaultObj == null) + return; + + // 获取需要删除的键列表(避免在遍历时修改集合) + List keysToRemove = new List(); + + foreach (var property in userObj.Properties()) + { + string propertyName = property.Name; + + // 如果默认配置中不存在该属性,标记为删除 + if (!defaultObj.ContainsKey(propertyName)) + { + keysToRemove.Add(propertyName); + continue; + } + + // 如果两个属性都是对象类型,递归比较 + JToken userValue = property.Value; + JToken defaultValue = defaultObj[propertyName]; + + if (userValue != null && defaultValue != null) + { + if (userValue.Type == JTokenType.Object && defaultValue.Type == JTokenType.Object) + { + RemoveObsoleteProperties(userValue as JObject, defaultValue as JObject, ref hasChanges); + } + // 处理数组中的对象(如自定义图标列表等) + else if (userValue.Type == JTokenType.Array && defaultValue.Type == JTokenType.Array) + { + JArray userArray = userValue as JArray; + JArray defaultArray = defaultValue as JArray; + + if (userArray != null && defaultArray != null && userArray.Count > 0 && defaultArray.Count > 0) + { + // 如果数组元素是对象,比较第一个元素的属性结构 + if (userArray[0].Type == JTokenType.Object && defaultArray[0].Type == JTokenType.Object) + { + for (int i = 0; i < userArray.Count; i++) + { + if (userArray[i] is JObject userItemObj && defaultArray[0] is JObject defaultItemObj) + { + RemoveObsoleteProperties(userItemObj, defaultItemObj, ref hasChanges); + } + } + } + } + } + } + } + + // 删除标记的键 + foreach (string key in keysToRemove) + { + userObj.Remove(key); + hasChanges = true; + } + } + } +} diff --git a/Ink Canvas/Services/Shell/HotkeyService.cs b/Ink Canvas/Services/Shell/HotkeyService.cs new file mode 100644 index 000000000..fe7cb5056 --- /dev/null +++ b/Ink Canvas/Services/Shell/HotkeyService.cs @@ -0,0 +1,474 @@ +using Ink_Canvas.Helpers; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Windows.Input; + +namespace Ink_Canvas.Services.Shell +{ + /// + /// 全局热键配置服务(寄生提取)。承载从 Helpers/GlobalHotkeyManager 搬出的 + /// 配置层:HotkeyConfig.json 的读写解析、默认配置定义、从配置/默认集合到注册引擎的 + /// 加载编排。热键触发回调本体留在主窗口壳(MW_Hotkeys.cs),以「热键名 → 」 + /// 字典在构造时注入,替代原 GetActionByName switch 的硬编码回调。 + /// NHotkey 注册引擎与多屏/焦点上下文门控仍在 中, + /// 本服务不引用主窗口类型、不做任何 UI 线程调度。 + /// 注意与插件服务 Ink_Canvas.Plugins.HotkeyService 同名不同命名空间,勿混淆。 + /// + internal sealed class HotkeyService + { + private readonly GlobalHotkeyManager _manager; + private readonly IReadOnlyDictionary _builtinActions; + + // 配置文件路径 + private static readonly string HotkeyConfigFile = Path.Combine(App.RootPath, "Configs", "HotkeyConfig.json"); + + /// + /// 创建热键配置服务并接线到注册引擎。 + /// + /// NHotkey 注册引擎(GlobalHotkeyManager,由主窗口创建)。 + /// 内置热键名 → 触发回调字典(回调本体留在主窗口壳)。 + public HotkeyService(GlobalHotkeyManager manager, IReadOnlyDictionary builtinActions) + { + _manager = manager ?? throw new ArgumentNullException(nameof(manager)); + _builtinActions = builtinActions ?? throw new ArgumentNullException(nameof(builtinActions)); + + // 接线:管理器原位的 public 配置方法为转发壳,经这些委托回到本服务 + _manager.LoadHotkeysHandler = LoadHotkeysFromSettings; + _manager.SaveHotkeysHandler = SaveHotkeysToSettings; + _manager.ConfigHotkeysProvider = GetHotkeysFromConfigFile; + _manager.RegisterDefaultsHandler = RegisterDefaultHotkeys; + + // 启动时确保配置文件存在(原在 GlobalHotkeyManager 构造函数中,时序不变: + // 主窗口先创建管理器、紧接着创建本服务,同一调用栈) + EnsureConfigFileExists(); + } + + /// + /// 获取配置文件中的快捷键信息(不注册,仅用于显示) + /// + /// 配置文件中的快捷键列表 + internal List GetHotkeysFromConfigFile() + { + try + { + if (!File.Exists(HotkeyConfigFile)) + { + return new List(); + } + + // 读取配置文件内容 + string jsonContent = File.ReadAllText(HotkeyConfigFile, 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 GlobalHotkeyManager.HotkeyInfo + { + Name = hotkeyConfig.Name, + Key = hotkeyConfig.Key, + Modifiers = hotkeyConfig.Modifiers, + Action = null // 不设置动作,仅用于显示 + }); + } + + return hotkeyList; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"从配置文件读取快捷键信息时出错: {ex.Message}", LogHelper.LogType.Error); + return new List(); + } + } + + /// + /// 注册默认快捷键集合 + /// + internal void RegisterDefaultHotkeys() + { + try + { + // 开始注册默认快捷键集合 + + // 基本操作快捷键 + _manager.RegisterHotkey("Undo", Key.Z, ModifierKeys.Control, GetActionByName("Undo")); + _manager.RegisterHotkey("Redo", Key.Y, ModifierKeys.Control, GetActionByName("Redo")); + _manager.RegisterHotkey("Clear", Key.E, ModifierKeys.Control, GetActionByName("Clear")); + _manager.RegisterHotkey("Paste", Key.V, ModifierKeys.Control, GetActionByName("Paste")); + + // 工具切换快捷键 + _manager.RegisterHotkey("SelectTool", Key.S, ModifierKeys.Alt, GetActionByName("SelectTool")); + _manager.RegisterHotkey("DrawTool", Key.D, ModifierKeys.Alt, GetActionByName("DrawTool")); + _manager.RegisterHotkey("EraserTool", Key.E, ModifierKeys.Alt, GetActionByName("EraserTool")); + _manager.RegisterHotkey("BlackboardTool", Key.B, ModifierKeys.Alt, GetActionByName("BlackboardTool")); + _manager.RegisterHotkey("QuitDrawTool", Key.Q, ModifierKeys.Alt, GetActionByName("QuitDrawTool")); + + // 画笔快捷键 - 使用反射访问penType字段 + _manager.RegisterHotkey("Pen1", Key.D1, ModifierKeys.Alt, GetActionByName("Pen1")); + _manager.RegisterHotkey("Pen2", Key.D2, ModifierKeys.Alt, GetActionByName("Pen2")); + _manager.RegisterHotkey("Pen3", Key.D3, ModifierKeys.Alt, GetActionByName("Pen3")); + _manager.RegisterHotkey("Pen4", Key.D4, ModifierKeys.Alt, GetActionByName("Pen4")); + _manager.RegisterHotkey("Pen5", Key.D5, ModifierKeys.Alt, GetActionByName("Pen5")); + + // 功能快捷键 + _manager.RegisterHotkey("DrawLine", Key.L, ModifierKeys.Alt, GetActionByName("DrawLine")); + _manager.RegisterHotkey("Screenshot", Key.C, ModifierKeys.Alt, GetActionByName("Screenshot")); + _manager.RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, GetActionByName("QuickDraw")); + _manager.RegisterHotkey("Hide", Key.V, ModifierKeys.Alt, GetActionByName("Hide")); + + // 退出快捷键 + _manager.RegisterHotkey("Exit", Key.Escape, ModifierKeys.None, GetActionByName("Exit")); + + // 已注册默认全局快捷键集合 + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"注册默认快捷键时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 从配置文件加载快捷键 + /// + internal void LoadHotkeysFromSettings() + { + try + { + // 开始从配置文件加载快捷键设置 + + // 检查是否应该注册快捷键 + if (!_manager.HotkeysShouldBeRegistered) + { + // 当前状态不允许注册快捷键,跳过加载 + return; + } + + // 如果配置文件不存在,先创建默认配置文件 + if (!File.Exists(HotkeyConfigFile)) + { + LogHelper.WriteLogToFile($"快捷键配置文件不存在: {HotkeyConfigFile}", LogHelper.LogType.Warning); + CreateDefaultConfigFile(); + RegisterDefaultHotkeys(); + _manager.HotkeysShouldBeRegistered = true; + return; + } + + // 尝试从配置文件加载 + if (LoadHotkeysFromConfigFile()) + { + // 成功从配置文件加载快捷键设置 + _manager.HotkeysShouldBeRegistered = true; + } + else + { + LogHelper.WriteLogToFile("配置文件存在但加载失败,回退到默认快捷键", LogHelper.LogType.Warning); + RegisterDefaultHotkeys(); + _manager.HotkeysShouldBeRegistered = true; + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"从设置加载快捷键时出错: {ex.Message}", LogHelper.LogType.Error); + // 出错时不自动使用默认快捷键,保持当前状态 + } + } + + /// + /// 保存快捷键配置到设置 + /// + internal void SaveHotkeysToSettings() + { + try + { + + if (SaveHotkeysToConfigFile()) + { + } + else + { + LogHelper.WriteLogToFile("保存快捷键配置失败", LogHelper.LogType.Error); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存快捷键配置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 确保配置文件存在,如果不存在则创建 + /// + private void EnsureConfigFileExists() + { + try + { + // 如果配置文件不存在,创建默认配置文件 + if (!File.Exists(HotkeyConfigFile)) + { + CreateDefaultConfigFile(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"确保快捷键配置文件存在时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 创建默认的快捷键配置文件 + /// + private void CreateDefaultConfigFile() + { + try + { + // 确保配置目录存在 + string configDir = Path.GetDirectoryName(HotkeyConfigFile); + if (!Directory.Exists(configDir)) + { + Directory.CreateDirectory(configDir); + } + + // 创建默认配置对象 + var config = new HotkeyConfig + { + Version = "1.0", + LastModified = DateTime.Now, + Hotkeys = new List() + }; + + // 添加默认快捷键配置 + config.Hotkeys.AddRange(new[] + { + new HotkeyConfigItem { Name = "Undo", Key = Key.Z, Modifiers = ModifierKeys.Control }, + new HotkeyConfigItem { Name = "Redo", Key = Key.Y, Modifiers = ModifierKeys.Control }, + new HotkeyConfigItem { Name = "Clear", Key = Key.E, Modifiers = ModifierKeys.Control }, + new HotkeyConfigItem { Name = "Paste", Key = Key.V, Modifiers = ModifierKeys.Control }, + new HotkeyConfigItem { Name = "SelectTool", Key = Key.S, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "DrawTool", Key = Key.D, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "EraserTool", Key = Key.E, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "BlackboardTool", Key = Key.B, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "QuitDrawTool", Key = Key.Q, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "Pen1", Key = Key.D1, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "Pen2", Key = Key.D2, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "Pen3", Key = Key.D3, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "Pen4", Key = Key.D4, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "Pen5", Key = Key.D5, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "DrawLine", Key = Key.L, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "Screenshot", Key = Key.C, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "QuickDraw", Key = Key.K, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "Hide", Key = Key.V, Modifiers = ModifierKeys.Alt }, + new HotkeyConfigItem { Name = "Exit", Key = Key.Escape, Modifiers = ModifierKeys.None } + }); + + // 序列化为JSON + var settings = new JsonSerializerSettings + { + Formatting = Formatting.Indented + }; + + string jsonContent = JsonConvert.SerializeObject(config, settings); + + // 写入配置文件 + File.WriteAllText(HotkeyConfigFile, jsonContent, Encoding.UTF8); + + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"创建默认快捷键配置文件时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 从配置文件加载快捷键设置 + /// + /// 是否加载成功 + private bool LoadHotkeysFromConfigFile() + { + try + { + if (!File.Exists(HotkeyConfigFile)) + { + LogHelper.WriteLogToFile($"快捷键配置文件不存在: {HotkeyConfigFile}", LogHelper.LogType.Warning); + return false; + } + + // 读取配置文件内容 + string jsonContent = File.ReadAllText(HotkeyConfigFile, 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 (_manager.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); + } + } + + // 旧版 HotkeyConfig.json 无「快抽」项时补注册默认组合,避免升级后无快捷键 + if (successCount > 0 && !_manager.IsHotkeyRegistered("QuickDraw")) + { + var quickDrawAction = GetActionByName("QuickDraw"); + if (quickDrawAction != null && _manager.RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, quickDrawAction)) + successCount++; + } + + if (successCount > 0) + { + _manager.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 _manager.GetRegisteredHotkeys()) + { + 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, Encoding.UTF8); + + return true; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存快捷键配置到配置文件时出错: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + + /// + /// 根据快捷键名称获取对应的动作(原 switch 硬编码改为构造注入的动作字典查表) + /// + /// 快捷键名称 + /// 对应的动作,如果不存在则返回null + private Action GetActionByName(string hotkeyName) + { + try + { + if (hotkeyName != null && _builtinActions.TryGetValue(hotkeyName, out var action)) + { + return action; + } + + LogHelper.WriteLogToFile($"未知的快捷键名称: {hotkeyName}", LogHelper.LogType.Warning); + return null; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"获取快捷键 {hotkeyName} 对应动作时出错: {ex.Message}", LogHelper.LogType.Error); + return null; + } + } + + #region Nested Classes + /// + /// 快捷键配置类 + /// + 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 + } +} diff --git a/Ink Canvas/Services/Shell/NotificationService.cs b/Ink Canvas/Services/Shell/NotificationService.cs new file mode 100644 index 000000000..0743ab1af --- /dev/null +++ b/Ink Canvas/Services/Shell/NotificationService.cs @@ -0,0 +1,389 @@ +using Ink_Canvas.Controls; +using Ink_Canvas.Helpers; +using Ink_Canvas.Models; +using Ink_Canvas.Properties; +using Ink_Canvas.Windows; +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; + +namespace Ink_Canvas.Services +{ + /// + /// 通知服务(寄生提取)。承载从 MW_Notification.cs 搬出的通知 UI 编排逻辑: + /// 提供者初始化、通知请求分发(Windows Toast / 灵动通知 / 旧版动画通知)、位置计算、 + /// 听写勿扰抑制、启动未读公告提示与插件回调摘除。 + /// 通知队列、去重与历史由既有 (纯逻辑静态类, + /// 不接触任何 UI 线程对象)承载,本服务只做 UI 呈现编排。 + /// 禁止引用主窗口类型:主窗口私有状态经 委托注入,x:Name 控件经 + /// 构造函数以强类型引用注入,禁止按名查找视觉树。 + /// UI 线程切换经注入的 UiInvoke 委托完成(统一替换为 ISyncService 前的过渡形态)。 + /// + internal sealed class NotificationService + { + /// + /// 通知服务的外部依赖委托集合。由主窗口在 CreateNotificationService 中装配。 + /// + internal sealed class Hooks + { + /// 读取应用设置(SettingsManager.Settings)。 + public Func GetSettings { get; set; } + /// 判断当前主题是否为深色。 + public Func IsCurrentThemeDark { get; set; } + /// 读取是否处于 PPT 放映模式。 + public Func IsInPptPresentationMode { get; set; } + /// 读取当前模式(0=批注,1=白板)。 + public Func GetCurrentMode { get; set; } + /// 读取浮动栏是否处于收纳模式。 + public Func IsFloatingBarFolded { get; set; } + /// 读取浮动栏是否正在切换隐藏模式动画。 + public Func IsFloatingBarChangingHideMode { get; set; } + /// 获取宿主窗口(以 Window 基类形态返回,服务不接触主窗口类型)。 + public Func GetHostWindow { get; set; } + /// 在 UI 线程上异步执行委托(默认优先级;过渡形态,后续收敛为 ISyncService)。 + public Action UiInvoke { get; set; } + /// 在 UI 线程空闲优先级异步执行委托(用于公告服务延迟启动)。 + public Action UiInvokeContextIdle { get; set; } + } + + private readonly DynamicNotificationControl _dynamicNotification; + private readonly TextBlock _textBlockNotice; + private readonly Grid _gridNotifications; + private readonly Viewbox _viewboxFloatingBar; + private readonly Hooks _hooks; + + private int _lastNotificationShowTime; + private int _notificationShowTime = 2500; + private bool _startupUnreadNotificationShown; + private readonly CancellationTokenSource _providerCancellation = new CancellationTokenSource(); + private AnnouncementService _announcementService; + private Timer _legacyHideTimer; + + public NotificationService( + DynamicNotificationControl dynamicNotification, + TextBlock textBlockNotice, + Grid gridNotifications, + Viewbox viewboxFloatingBar, + Hooks hooks) + { + _dynamicNotification = dynamicNotification; + _textBlockNotice = textBlockNotice; + _gridNotifications = gridNotifications; + _viewboxFloatingBar = viewboxFloatingBar; + _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); + } + + /// 旧版通知显示时长换算(秒 → 毫秒,下限 1 秒),纯逻辑。 + private static int DisplayMillisecondsFromSeconds(int displaySeconds) => Math.Max(1, displaySeconds) * 1000; + + /// + /// 显示一条普通文本通知(原主窗口 ShowNotification 方法的本体)。 + /// + public void ShowText(string notice) + { + NotificationCenterService.EnqueueText(notice, NotificationMessageLevel.Normal, Math.Max(1, _notificationShowTime / 1000)); + } + + /// + /// 显示 PPT 放映模式提示通知(受设置项 ShowPPTModePrompt 控制)。 + /// + public void ShowPptModePromptNotification() + { + if (_hooks.GetSettings()?.PowerPointSettings?.ShowPPTModePrompt != true) return; + + NotificationCenterService.Enqueue(new NotificationMessage + { + Id = "ppt-mode-prompt-" + Guid.NewGuid().ToString("N"), + Type = NotificationMessageType.Reminder, + Level = NotificationMessageLevel.Normal, + Title = PPTStrings.PPT_ModePrompt_Title, + Summary = PPTStrings.PPT_ModePrompt_Message, + Icon = "Info", + DisplaySeconds = 4, + Priority = 20, + Source = "ppt-mode-prompt", + ProviderId = "local" + }); + } + + /// + /// 初始化通知提供者:挂接灵动通知关闭事件、订阅通知中心请求, + /// 并在启用公告时创建公告服务(UI 线程空闲优先级延迟 3 秒启动)。 + /// + public void InitializeProviders() + { + if (_dynamicNotification != null) + { + _dynamicNotification.Closed -= DynamicNotification_Closed; + _dynamicNotification.Closed += DynamicNotification_Closed; + } + + NotificationCenterService.NotificationRequested -= NotificationCenterService_NotificationRequested; + NotificationCenterService.NotificationRequested += NotificationCenterService_NotificationRequested; + + if (_announcementService == null && _hooks.GetSettings()?.Notification?.IsAnnouncementEnabled == true) + { + _announcementService = new AnnouncementService(_hooks.GetSettings()); + + AnnouncementService.UnreadCountChanged -= OnAnnouncementUnreadCountChanged; + AnnouncementService.UnreadCountChanged += OnAnnouncementUnreadCountChanged; + + _hooks.UiInvokeContextIdle(async () => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(3), _providerCancellation.Token); + await _announcementService.StartAsync(_providerCancellation.Token); + } + catch (OperationCanceledException oce) + { + LogService.LogException(oce); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"公告通知提供商启动失败: {ex.Message}", LogHelper.LogType.Warning); + } + }); + } + + LogHelper.WriteLogToFile( + $"[Notification] 通知提供商已初始化: 公告已启用={_hooks.GetSettings()?.Notification?.IsAnnouncementEnabled == true}, 公告服务实例={_announcementService != null}", + LogHelper.LogType.Info); + } + + /// + /// 拆除通知控件上挂的插件 Action 回调。插件热重载时由 + /// 调用:当前显示的通知若来自该插件,currentMessage.Action 直接指向插件 ALC, + /// 留着会阻止热重载时被回收。 + /// + internal void DetachPluginNotificationAction(string pluginId) + { + if (string.IsNullOrEmpty(pluginId)) return; + try + { + _dynamicNotification?.DetachPluginActionIfMatches(pluginId); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"摘除插件通知 Action 失败: {ex.Message}", LogHelper.LogType.Warning); + } + } + + private void NotificationCenterService_NotificationRequested(NotificationMessage message) + { + _hooks.UiInvoke(() => + { + try + { + if (IsNotificationSuppressedByDictationDoNotDisturb()) + { + NotificationCenterService.NotifyCurrentClosed(); + return; + } + + if (_hooks.GetSettings()?.Notification?.IsWindowsToastEnabled == true) + { + WindowsNotificationHelper.ShowToast(message); + } + + if (_hooks.GetSettings()?.Notification?.IsDynamicNotificationEnabled == true && _dynamicNotification != null) + { + _dynamicNotification.RefreshTheme(_hooks.IsCurrentThemeDark()); + ApplyDynamicNotificationPlacement(message); + _dynamicNotification.Show(message); + } + else + { + ShowLegacyNotification(message.Title, message.DisplaySeconds); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"灵动通知显示失败: {ex.Message}", LogHelper.LogType.Error); + NotificationCenterService.NotifyCurrentClosed(); + } + }); + } + + private bool IsNotificationSuppressedByDictationDoNotDisturb() + { + var notification = _hooks.GetSettings()?.Notification; + if (notification?.IsDictationDoNotDisturbEnabled != true) return false; + + if (notification.IsDictationDoNotDisturbInPPTEnabled && _hooks.IsInPptPresentationMode()) + { + return true; + } + + return notification.IsDictationDoNotDisturbInWhiteboardEnabled && _hooks.GetCurrentMode() == 1; + } + + private void DynamicNotification_Closed(object sender, EventArgs e) + { + NotificationCenterService.NotifyCurrentClosed(); + } + + private void ApplyDynamicNotificationPlacement(NotificationMessage message = null) + { + if (_dynamicNotification == null) return; + + _dynamicNotification.HorizontalAlignment = HorizontalAlignment.Center; + _dynamicNotification.VerticalAlignment = VerticalAlignment.Top; + _dynamicNotification.Margin = new Thickness(0); + + if (message?.Source == "ppt-mode-prompt") + { + ApplyDynamicNotificationFloatingBarPlacement(); + return; + } + + switch (_hooks.GetSettings()?.Notification?.Placement) + { + case "TopLeft": + _dynamicNotification.HorizontalAlignment = HorizontalAlignment.Left; + _dynamicNotification.Margin = new Thickness(16, 0, 0, 0); + break; + case "TopRight": + _dynamicNotification.HorizontalAlignment = HorizontalAlignment.Right; + _dynamicNotification.Margin = new Thickness(0, 0, 16, 0); + break; + case "FloatingBarAbove": + ApplyDynamicNotificationFloatingBarPlacement(); + break; + } + } + + private void ApplyDynamicNotificationFloatingBarPlacement() + { + // 收纳或收纳动画期间浮动栏已移出可用区域,保留默认的顶部居中位置,避免通知跟随到屏幕外。 + if (_dynamicNotification == null || _viewboxFloatingBar == null || + _viewboxFloatingBar.Visibility != Visibility.Visible || + _hooks.IsFloatingBarFolded() || _hooks.IsFloatingBarChangingHideMode()) + { + return; + } + + try + { + var hostWindow = _hooks.GetHostWindow(); + var position = _viewboxFloatingBar.TransformToAncestor(hostWindow).Transform(new Point(0, 0)); + double notificationWidth = _dynamicNotification.ActualWidth > 0 ? _dynamicNotification.ActualWidth : _dynamicNotification.Width; + double notificationHeight = _dynamicNotification.ActualHeight > 0 ? _dynamicNotification.ActualHeight : 72; + double floatingBarWidth = _viewboxFloatingBar.ActualWidth; + double left = position.X + floatingBarWidth / 2 - notificationWidth / 2; + double top = position.Y - notificationHeight - 12; + + left = Math.Max(12, Math.Min(hostWindow.ActualWidth - notificationWidth - 12, left)); + top = Math.Max(12, top); + + _dynamicNotification.HorizontalAlignment = HorizontalAlignment.Left; + _dynamicNotification.VerticalAlignment = VerticalAlignment.Top; + _dynamicNotification.Margin = new Thickness(left, top, 0, 0); + } + catch (Exception ex) + { + LogService.LogException(ex); + } + } + + private void OnAnnouncementUnreadCountChanged() + { + if (_startupUnreadNotificationShown) return; + + _hooks.UiInvoke(() => + { + if (_startupUnreadNotificationShown) return; + ShowStartupUnreadNotification(); + }); + } + + private void ShowStartupUnreadNotification() + { + try + { + if (_startupUnreadNotificationShown) return; + _startupUnreadNotificationShown = true; + + var count = AnnouncementService.GetUnreadCount(_hooks.GetSettings()); + if (count <= 0) return; + + NotificationCenterService.Enqueue(new NotificationMessage + { + Id = "startup-unread-announcements", + Type = NotificationMessageType.Reminder, + Level = NotificationMessageLevel.Normal, + Title = AnnouncementStrings.StartupUnreadTitle, + Summary = string.Format(AnnouncementStrings.StartupUnreadSummary, count), + ActionText = AnnouncementStrings.StartupUnreadAction, + Icon = "Info", + DisplaySeconds = 8, + Priority = 50, + Source = "startup-unread", + ProviderId = "announcement", + Action = () => + { + try + { + var window = new AnnouncementCenterWindow { Owner = _hooks.GetHostWindow() }; + window.Show(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"打开公告中心失败: {ex.Message}", LogHelper.LogType.Warning); + } + } + }); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"启动未读公告通知失败: {ex.Message}", LogHelper.LogType.Warning); + } + } + + private void ShowLegacyNotification(string notice, int displaySeconds) + { + try + { + if (_textBlockNotice == null || _gridNotifications == null) + { + NotificationCenterService.NotifyCurrentClosed(); + return; + } + + _lastNotificationShowTime = Environment.TickCount; + _notificationShowTime = DisplayMillisecondsFromSeconds(displaySeconds); + _textBlockNotice.Text = notice; + AnimationsHelper.ShowWithSlideFromBottomAndFade(_gridNotifications); + + // 原实现为独立线程 Sleep 后经 UI 线程调度器隐藏;等价改写为单次计时器 + + // UiInvoke 回 UI 线程(新类不得创建 Thread 访问 UI 线程对象,同为过渡形态)。 + StopLegacyHideTimer(); + _legacyHideTimer = new Timer(_ => + { + if (Environment.TickCount - _lastNotificationShowTime >= _notificationShowTime) + { + _hooks.UiInvoke(() => + { + AnimationsHelper.HideWithSlideAndFade(_gridNotifications); + NotificationCenterService.NotifyCurrentClosed(); + }); + } + }, null, _notificationShowTime + 300, Timeout.Infinite); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"ShowNotification 异常: {ex.Message}", LogHelper.LogType.Error); + NotificationCenterService.NotifyCurrentClosed(); + } + } + + private void StopLegacyHideTimer() + { + _legacyHideTimer?.Change(Timeout.Infinite, Timeout.Infinite); + _legacyHideTimer?.Dispose(); + _legacyHideTimer = null; + } + } +} diff --git a/Ink Canvas/Services/Shell/ScreenshotService.cs b/Ink Canvas/Services/Shell/ScreenshotService.cs new file mode 100644 index 000000000..28a8e9b8d --- /dev/null +++ b/Ink Canvas/Services/Shell/ScreenshotService.cs @@ -0,0 +1,966 @@ +using Ink_Canvas.Helpers; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.Windows.Ink; +using System.Windows.Media.Imaging; +using Color = System.Drawing.Color; +using PixelFormat = System.Drawing.Imaging.PixelFormat; +using Point = System.Windows.Point; +using Size = System.Drawing.Size; + +namespace Ink_Canvas.Services.Shell +{ + /// + /// 截图服务(寄生提取)。承载从主窗口截图 partial(MW_Screenshot.cs)与 + /// 插图 partial(MW_ImageInsert.cs)搬出的截图执行段:屏幕捕获(GDI CopyFromScreen)、 + /// 位图加工(墨迹叠加、形状遮罩、格式转换)、编码落盘与剪贴板写入。 + /// 区域选择的交互段(选区窗口、窗口显隐编排、贴图回白板编排)仍留在主窗口壳中。 + /// 禁止引用主窗口类型:设置、墨迹快照、白板页索引、DPI 缩放、通知与 UI 线程调度 + /// 均经 委托注入(统一收敛为 ISyncService 前的过渡形态)。 + /// + internal sealed class ScreenshotService + { + /// + /// 截图服务的外部依赖委托集合。由主窗口在 CreateScreenshotService 中装配。 + /// + internal sealed class Hooks + { + /// 读取应用设置(SettingsManager.Settings)。 + public Func GetSettings { get; set; } + /// 读取当前画布墨迹笔画数(可空,语义同 inkCanvas?.Strokes?.Count)。 + public Func GetInkStrokeCount { get; set; } + /// 克隆当前画布墨迹集合(调用方保证笔画数大于 0)。 + public Func CloneInkStrokes { get; set; } + /// 读取当前白板页索引。 + public Func GetCurrentWhiteboardIndex { get; set; } + /// 读取当前窗口的 DPI 缩放比例。 + public Func GetDpiScale { get; set; } + /// 显示截图保存成功通知(参数为保存路径,资源格式化由主窗口侧完成)。 + public Action ShowScreenshotSaveSuccess { get; set; } + /// 在 UI 线程上执行委托(过渡形态,后续收敛为 ISyncService)。 + public Action UiInvoke { get; set; } + /// 截图后自动保存墨迹(对应主窗口 SaveInkCanvasStrokes(false))。 + public Action SaveInkStrokesAtScreenshot { get; set; } + } + + private readonly Hooks _hooks; + + public ScreenshotService(Hooks hooks) + { + _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); + } + + /// + /// 在切页/加页场景下使用:先捕获当前画面到内存并克隆墨迹,然后立即返回;截图与墨迹保存在后台异步执行,不阻塞切页。 + /// 调用方应在调用本方法后立即执行 SaveStrokes、ClearStrokes、切页、RestoreStrokes 等逻辑。 + /// + /// 是否隐藏保存成功通知 + /// 截图文件名(可选) + internal void CaptureAndEnqueueScreenshotSave(bool isHideNotification, string fileName = null) + { + var savePath = _hooks.GetSettings().Automation.IsSaveScreenshotsInDateFolders + ? GetDateFolderPath(fileName) + : GetDefaultFolderPath(); + + System.Drawing.Bitmap bitmap = null; + StrokeCollection strokesToSave = null; + int pageIndexForStrokes = 0; + string strokeSavePath = null; + + try + { + bitmap = CaptureScreenshotToBitmap(); + if (bitmap == null) return; + + if (_hooks.GetSettings().Automation.IsAutoSaveStrokesAtScreenshot && _hooks.GetInkStrokeCount() > 0) + { + strokesToSave = _hooks.CloneInkStrokes(); + pageIndexForStrokes = _hooks.GetCurrentWhiteboardIndex(); + var basePath = _hooks.GetSettings().Automation.AutoSavedStrokesLocation + + @"\Auto Saved - BlackBoard Strokes"; + if (!Directory.Exists(basePath)) Directory.CreateDirectory(basePath); + string stem; + if (_hooks.GetSettings().Automation.IsUseCustomSaveFileName) + { + stem = SaveFileNameHelper.Render(_hooks.GetSettings().Automation.CustomSaveFileNameTemplate, + new SaveFileNameContext + { + Mode = "BlackBoard", + Type = "Auto", + Page = pageIndexForStrokes, + Count = strokesToSave.Count + }); + } + else + { + stem = $"{DateTime.Now:yyyy-MM-dd HH-mm-ss-fff} Page-{pageIndexForStrokes} StrokesCount-{strokesToSave.Count}"; + } + strokeSavePath = Path.Combine(basePath, stem + ".icstk"); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"CaptureAndEnqueueScreenshotSave 捕获失败: {ex}", LogHelper.LogType.Error); + bitmap?.Dispose(); + return; + } + + var bitmapToSave = bitmap; + var path = savePath; + var hideNotification = isHideNotification; + + _ = Task.Run(async () => + { + try + { + if (bitmapToSave != null) + { + var directory = Path.GetDirectoryName(path); + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + Helpers.ScreenshotImageSaveHelper.Save(bitmapToSave, path); + bitmapToSave.Dispose(); + } + + if (strokesToSave != null && !string.IsNullOrEmpty(strokeSavePath)) + { + // 原子写:tmp + File.Replace/Move,避免 FileMode.Create 直接截断后 + // 写入中途失败(磁盘满/进程被杀)让 .icstk 停在 0 字节。 + var tmpStrokePath = strokeSavePath + ".tmp"; + try + { + using (var fs = new FileStream(tmpStrokePath, FileMode.Create)) + { + strokesToSave.Save(fs); + } + if (File.Exists(strokeSavePath)) + File.Replace(tmpStrokePath, strokeSavePath, null); + else + File.Move(tmpStrokePath, strokeSavePath); + } + catch + { + try { if (File.Exists(tmpStrokePath)) File.Delete(tmpStrokePath); } catch (Exception ex) { LogService.LogException(ex); } + throw; + } + } + + if (!hideNotification && !string.IsNullOrEmpty(path)) + { + _hooks.UiInvoke(() => _hooks.ShowScreenshotSaveSuccess(path)); + } + + // 使用上传帮助类上传到所有启用的服务 + await Helpers.UploadHelper.UploadFileAsync(path); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"后台保存截图/墨迹失败: {ex}", LogHelper.LogType.Error); + bitmapToSave?.Dispose(); + } + }); + } + + /// + /// 将当前屏幕内容捕获为位图(仅内存,不写文件)。调用方或后台任务负责 Dispose。 + /// + internal static System.Drawing.Bitmap CaptureScreenshotToBitmap() + { + var rc = SystemInformation.VirtualScreen; + var bitmap = new System.Drawing.Bitmap(rc.Width, rc.Height, PixelFormat.Format32bppArgb); + using (var memoryGraphics = Graphics.FromImage(bitmap)) + { + memoryGraphics.CompositingQuality = CompositingQuality.HighQuality; + memoryGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + memoryGraphics.SmoothingMode = SmoothingMode.HighQuality; + memoryGraphics.CompositingMode = CompositingMode.SourceOver; + memoryGraphics.CopyFromScreen(rc.X, rc.Y, 0, 0, rc.Size, CopyPixelOperation.SourceCopy); + } + return bitmap; + } + + /// + /// 截取指定屏幕区域 + /// + /// 要截取的屏幕区域 + /// 截取的位图 + /// + /// 该方法会: + /// 1. 确保区域在有效范围内 + /// 2. 调整区域边界,确保不超出屏幕范围 + /// 3. 创建支持透明度的位图 + /// 4. 设置高质量渲染 + /// 5. 截取屏幕区域 + /// + internal static Bitmap CaptureScreenArea(Rectangle area) + { + try + { + // 确保区域在有效范围内 + var virtualScreen = SystemInformation.VirtualScreen; + + // 调整区域边界,确保不超出屏幕范围 + int x = Math.Max(area.X, virtualScreen.X); + int y = Math.Max(area.Y, virtualScreen.Y); + int right = Math.Min(area.Right, virtualScreen.Right); + int bottom = Math.Min(area.Bottom, virtualScreen.Bottom); + + int width = Math.Max(1, right - x); + int height = Math.Max(1, bottom - y); + + // 创建支持透明度的位图 + var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb); + using (var graphics = Graphics.FromImage(bitmap)) + { + // 设置高质量渲染 + graphics.CompositingQuality = CompositingQuality.HighQuality; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.CompositingMode = CompositingMode.SourceOver; + + // 截取屏幕区域 + graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy); + } + + return bitmap; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"截取屏幕区域失败: {ex.Message}", LogHelper.LogType.Error); + return null; + } + } + + /// + /// 将截图写入剪贴板,同时提供 PNG 流、Bitmap、FileDrop 三种格式。 + /// 仅写入 Bitmap 格式时 Win+V 剪贴板历史不收录、资源管理器右键也不能粘贴为文件, + /// 故用 DataObject 多格式写入: + /// - PNG 流:Win+V 历史 / 现代应用可识别 + /// - Bitmap:老式应用兼容 + /// - FileDrop:资源管理器右键可粘贴为 .png 文件(需传入已保存的文件路径) + /// 注意:带上 FileDrop 后,部分聊天软件可能优先把贴图当作发送文件处理。 + /// + internal static void CopyImageToClipboard(BitmapSource image, string filePath = null) + { + if (image == null) return; + + var pngStream = new MemoryStream(); + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(image)); + encoder.Save(pngStream); + pngStream.Position = 0; + + var data = new System.Windows.DataObject(); + data.SetData("PNG", pngStream); // Win+V 历史 / 现代应用 + data.SetData(System.Windows.DataFormats.Bitmap, image); // 老式 Bitmap 兼容 + if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) + { + data.SetData(System.Windows.DataFormats.FileDrop, new[] { filePath }); // 资源管理器右键粘贴为文件 + } + System.Windows.Clipboard.SetDataObject(data, true); + } + + /// + /// 区域截图执行段:捕获区域位图 → 按需叠加墨迹 → 按需应用形状遮罩 → PNG 落盘 → 按需写剪贴板。 + /// + /// 选区窗口返回的截图结果(区域、路径、墨迹叠加信息)。 + /// 保存路径。 + /// 是否在保存后复制到剪贴板。 + /// 捕获与保存成功返回 true;捕获失败返回 false(通知由调用方壳发出)。 + internal bool SaveAreaScreenshot(ScreenshotResult screenshotResult, string savePath, bool copyToClipboard) + { + using (var originalBitmap = CaptureScreenArea(screenshotResult.Area)) + { + if (originalBitmap == null) + { + return false; + } + + Bitmap finalBitmap = originalBitmap; + bool needDisposeFinalBitmap = false; + + try + { + if (screenshotResult.IncludeInk && screenshotResult.InkOverlayBitmapSource != null) + { + var withInkBitmap = OverlayInkOnCapturedBitmap(finalBitmap, screenshotResult.Area, screenshotResult.InkOverlayBitmapSource); + if (withInkBitmap != null && withInkBitmap != finalBitmap) + { + if (needDisposeFinalBitmap && finalBitmap != originalBitmap) + { + finalBitmap.Dispose(); + } + finalBitmap = withInkBitmap; + needDisposeFinalBitmap = true; + } + } + + if (screenshotResult.Path != null && screenshotResult.Path.Count > 0) + { + var maskedBitmap = ApplyShapeMask(finalBitmap, screenshotResult.Path, screenshotResult.Area); + if (maskedBitmap != null && maskedBitmap != finalBitmap) + { + if (needDisposeFinalBitmap && finalBitmap != originalBitmap) + { + finalBitmap.Dispose(); + } + finalBitmap = maskedBitmap; + needDisposeFinalBitmap = true; + } + } + + var directory = Path.GetDirectoryName(savePath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + finalBitmap.Save(savePath, ImageFormat.Png); + + // 截图后复制到剪贴板 + if (copyToClipboard) + { + try { CopyImageToClipboard(ConvertBitmapToBitmapSource(finalBitmap), savePath); } + catch (Exception ex) { LogHelper.WriteLogToFile($"截图复制到剪贴板失败: {ex}", LogHelper.LogType.Warning); } + } + + return true; + } + finally + { + if (needDisposeFinalBitmap && finalBitmap != originalBitmap) + { + finalBitmap.Dispose(); + } + } + } + } + + /// + /// 提取公共的截图和保存逻辑 + /// + /// 保存路径 + /// 是否隐藏通知 + /// 是否在保存后复制到剪贴板 + /// + /// 该方法会: + /// 1. 获取虚拟屏幕边界 + /// 2. 创建位图并设置高质量渲染 + /// 3. 从屏幕复制内容到位图 + /// 4. 确保保存目录存在 + /// 5. 保存为PNG格式 + /// 6. 若 copyToClipboard 为真,将截图复制到剪贴板 + /// 7. 如果不隐藏通知,显示保存成功通知 + /// 8. 异步上传截图到Dlass + /// + internal void CaptureAndSaveScreenshot(string savePath, bool isHideNotification, bool copyToClipboard = false) + { + var rc = SystemInformation.VirtualScreen; + + using (var bitmap = new Bitmap(rc.Width, rc.Height, PixelFormat.Format32bppArgb)) + using (var memoryGraphics = Graphics.FromImage(bitmap)) + { + // 设置高质量渲染 + memoryGraphics.CompositingQuality = CompositingQuality.HighQuality; + memoryGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + memoryGraphics.SmoothingMode = SmoothingMode.HighQuality; + memoryGraphics.CompositingMode = CompositingMode.SourceOver; + + memoryGraphics.CopyFromScreen(rc.X, rc.Y, 0, 0, rc.Size, CopyPixelOperation.SourceCopy); + + // 确保目录存在 + var directory = Path.GetDirectoryName(savePath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // 使用PNG格式保存,确保透明度信息不丢失 + bitmap.Save(savePath, ImageFormat.Png); + LogHelper.WriteLogToFile( + $"[Screenshot] 截图已保存: {savePath} ({rc.Width}x{rc.Height}), 复制到剪贴板={copyToClipboard}", + LogHelper.LogType.Info); + + // 截图后复制到剪贴板 + if (copyToClipboard) + { + try { CopyImageToClipboard(ConvertBitmapToBitmapSource(bitmap), savePath); } + catch (Exception ex) { LogHelper.WriteLogToFile($"截图复制到剪贴板失败: {ex}", LogHelper.LogType.Warning); } + } + } + + if (!isHideNotification) + { + Task.Delay(100).ContinueWith(t => + { + _hooks.UiInvoke(() => _hooks.ShowScreenshotSaveSuccess(savePath)); + }); + } + _ = Task.Run(async () => + { + try + { + // 使用上传帮助类上传到所有启用的服务 + await Helpers.UploadHelper.UploadFileAsync(savePath); + } + catch (Exception ex) + { + LogService.LogException(ex); + } + }); + } + + /// + /// 保存截图 + /// + /// 是否隐藏通知 + /// 文件名 + /// + /// 该方法会: + /// 1. 根据设置确定保存路径 + /// 2. 调用CaptureAndSaveScreenshot方法捕获并保存截图 + /// 3. 如果设置了自动保存墨迹,调用保存墨迹逻辑 + /// + internal void SaveScreenShot(bool isHideNotification, string fileName = null) + { + var savePath = _hooks.GetSettings().Automation.IsSaveScreenshotsInDateFolders + ? GetDateFolderPath(fileName) + : GetDefaultFolderPath(); + + CaptureAndSaveScreenshot(savePath, isHideNotification); + + if (_hooks.GetSettings().Automation.IsAutoSaveStrokesAtScreenshot) + _hooks.SaveInkStrokesAtScreenshot(); + } + + /// + /// 保存截图到配置的保存目录 + /// + /// + /// 该方法会: + /// 1. 根据截图组件设置生成保存路径和文件名 + /// 2. 调用CaptureAndSaveScreenshot方法捕获并保存截图 + /// 3. 如果设置了截图后复制到剪贴板,将截图复制到剪贴板 + /// 4. 如果设置了自动保存墨迹,调用保存墨迹逻辑 + /// + internal void SaveScreenShotToDesktop() + { + var savePath = Path.Combine( + GetScreenshotSaveDirectory(), + $"{GetScreenshotFileNameStem()}.png"); + + CaptureAndSaveScreenshot(savePath, false, _hooks.GetSettings().Automation.IsCopyScreenshotToClipboard); + + if (_hooks.GetSettings().Automation.IsAutoSaveStrokesAtScreenshot) + _hooks.SaveInkStrokesAtScreenshot(); + } + + /// + /// 获取截图保存目录。 + /// 仅当"截图保存到指定位置"开关开启时使用自定义位置,否则回退到桌面。 + /// + internal string GetScreenshotSaveDirectory() + { + if (_hooks.GetSettings().Automation.IsSaveScreenshotToCustomLocation) + { + var location = _hooks.GetSettings().Automation.ScreenshotSaveLocation; + if (!string.IsNullOrWhiteSpace(location)) + return location; + } + return Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + } + + /// + /// 获取日期文件夹路径 + /// + /// 文件名 + /// 日期文件夹路径 + /// + /// 该方法会: + /// 1. 如果文件名为空,使用当前时间作为文件名 + /// 2. 获取基础路径和日期文件夹名 + /// 3. 组合路径并返回 + /// + internal string GetDateFolderPath(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + fileName = _hooks.GetSettings().Automation.IsUseCustomSaveFileName + ? SaveFileNameHelper.Render(_hooks.GetSettings().Automation.CustomSaveFileNameTemplate, + new SaveFileNameContext { Mode = "Screenshot", Type = "Auto", Count = _hooks.GetInkStrokeCount() }) + : DateTime.Now.ToString("HH-mm-ss"); + } + + var basePath = _hooks.GetSettings().Automation.AutoSavedStrokesLocation; + var dateFolder = DateTime.Now.ToString("yyyyMMdd"); + var safeRelativePath = SanitizeScreenshotRelativePath(fileName); + + return Path.Combine( + basePath, + "Auto Saved - Screenshots", + dateFolder, + safeRelativePath + Helpers.ScreenshotImageSaveHelper.GetExtension()); + } + + private static string SanitizeScreenshotRelativePath(string relativePath) + { + if (string.IsNullOrWhiteSpace(relativePath)) return DateTime.Now.ToString("HH-mm-ss"); + + var separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; + var parts = relativePath + .Split(separators, StringSplitOptions.RemoveEmptyEntries) + .Select(SanitizePathPart) + .Where(part => !string.IsNullOrWhiteSpace(part)); + + var sanitized = Path.Combine(parts.ToArray()); + return string.IsNullOrWhiteSpace(sanitized) ? DateTime.Now.ToString("HH-mm-ss") : sanitized; + } + + private static string SanitizePathPart(string pathPart) + { + var invalidChars = Path.GetInvalidFileNameChars(); + var chars = pathPart.Select(c => invalidChars.Contains(c) ? '_' : c).ToArray(); + var sanitized = new string(chars).Trim().TrimEnd('.', ' '); + return sanitized == "." || sanitized == ".." ? "_" : sanitized; + } + + /// + /// 获取默认文件夹路径 + /// + /// 默认文件夹路径 + /// + /// 该方法会: + /// 1. 获取基础路径 + /// 2. 组合截图文件夹路径 + /// 3. 确保截图文件夹存在 + /// 4. 生成文件名并组合完整路径返回 + /// + internal string GetDefaultFolderPath() + { + var basePath = _hooks.GetSettings().Automation.AutoSavedStrokesLocation; + var screenshotsFolder = Path.Combine(basePath, "Auto Saved - Screenshots"); + + if (!Directory.Exists(screenshotsFolder)) + { + Directory.CreateDirectory(screenshotsFolder); + } + + return Path.Combine( + screenshotsFolder, + GetScreenshotFileNameStem() + Helpers.ScreenshotImageSaveHelper.GetExtension()); + } + + internal string GetScreenshotFileNameStem() + { + if (_hooks.GetSettings().Automation.IsUseCustomSaveFileName) + { + return SaveFileNameHelper.Render(_hooks.GetSettings().Automation.CustomSaveFileNameTemplate, + new SaveFileNameContext { Mode = "Screenshot", Type = "Auto", Count = _hooks.GetInkStrokeCount() }); + } + return DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + } + + internal static Bitmap OverlayInkOnCapturedBitmap(Bitmap capturedBitmap, Rectangle captureArea, BitmapSource inkOverlayBitmapSource) + { + if (capturedBitmap == null || inkOverlayBitmapSource == null) + { + return capturedBitmap; + } + + try + { + var virtualScreen = SystemInformation.VirtualScreen; + var sourceRect = new Rectangle( + captureArea.X - virtualScreen.X, + captureArea.Y - virtualScreen.Y, + captureArea.Width, + captureArea.Height); + + sourceRect.Intersect(new Rectangle(0, 0, inkOverlayBitmapSource.PixelWidth, inkOverlayBitmapSource.PixelHeight)); + if (sourceRect.Width <= 0 || sourceRect.Height <= 0) + { + return capturedBitmap; + } + + using (var inkOverlayBitmap = ConvertBitmapSourceToBitmap(inkOverlayBitmapSource)) + { + if (inkOverlayBitmap == null) + { + return capturedBitmap; + } + + Bitmap resultBitmap = null; + try + { + resultBitmap = new Bitmap(capturedBitmap.Width, capturedBitmap.Height, PixelFormat.Format32bppArgb); + using (var g = Graphics.FromImage(resultBitmap)) + { + g.DrawImage(capturedBitmap, 0, 0, capturedBitmap.Width, capturedBitmap.Height); + + var targetRect = new Rectangle(0, 0, Math.Min(sourceRect.Width, capturedBitmap.Width), Math.Min(sourceRect.Height, capturedBitmap.Height)); + g.DrawImage(inkOverlayBitmap, targetRect, sourceRect, GraphicsUnit.Pixel); + } + + return resultBitmap; + } + catch + { + resultBitmap?.Dispose(); + throw; + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"叠加截图墨迹失败: {ex.Message}", LogHelper.LogType.Warning); + return capturedBitmap; + } + } + + internal static Bitmap ConvertBitmapSourceToBitmap(BitmapSource bitmapSource) + { + if (bitmapSource == null) + { + return null; + } + + using (var memoryStream = new MemoryStream()) + { + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(bitmapSource)); + encoder.Save(memoryStream); + memoryStream.Position = 0; + using (var tempBitmap = new Bitmap(memoryStream)) + { + return new Bitmap(tempBitmap); + } + } + } + + /// + /// 应用形状遮罩到位图 + /// + /// 要遮罩的位图 + /// 遮罩路径(WPF 坐标点集) + /// 截图区域 + /// 遮罩后的位图,或路径无效/失败时的原位图 + /// + /// 该方法会: + /// 1. 验证路径参数 + /// 2. 获取DPI缩放比例 + /// 3. 创建结果位图,确保支持透明度 + /// 4. 将整个位图设置为透明 + /// 5. 设置高质量渲染 + /// 6. 转换WPF坐标到GDI+坐标,考虑DPI缩放和屏幕偏移 + /// 7. 添加路径 + /// 8. 验证路径是否有效 + /// 9. 设置裁剪区域为路径内部 + /// 10. 在裁剪区域内绘制原始图像 + /// 11. 重置裁剪区域,确保后续操作不受影响 + /// + internal Bitmap ApplyShapeMask(Bitmap bitmap, List path, Rectangle area) + { + try + { + // 验证路径参数 + if (path == null || path.Count < 3) + { + LogHelper.WriteLogToFile("路径点数不足,无法应用形状遮罩", LogHelper.LogType.Warning); + return bitmap; + } + + // 获取DPI缩放比例 + var dpiScale = _hooks.GetDpiScale(); + var virtualScreen = SystemInformation.VirtualScreen; + + // 创建结果位图,确保支持透明度 + var resultBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb); + + // 首先将整个位图设置为透明 + using (var resultGraphics = Graphics.FromImage(resultBitmap)) + { + // 清除位图,设置为完全透明 + resultGraphics.Clear(Color.Transparent); + + // 设置高质量渲染 + resultGraphics.SmoothingMode = SmoothingMode.AntiAlias; + resultGraphics.CompositingQuality = CompositingQuality.HighQuality; + resultGraphics.CompositingMode = CompositingMode.SourceOver; + + // 创建路径 + using (var pathGraphics = new GraphicsPath()) + { + // 转换WPF坐标到GDI+坐标,考虑DPI缩放和屏幕偏移 + var points = new PointF[path.Count]; + for (int i = 0; i < path.Count; i++) + { + // 将WPF坐标转换为实际屏幕坐标,然后相对于截图区域计算偏移 + double screenX = (path[i].X * dpiScale) + virtualScreen.Left; + double screenY = (path[i].Y * dpiScale) + virtualScreen.Top; + + // 计算相对于截图区域的坐标 + float relativeX = (float)(screenX - area.X); + float relativeY = (float)(screenY - area.Y); + + // 确保坐标在有效范围内 + relativeX = Math.Max(0, Math.Min(relativeX, bitmap.Width - 1)); + relativeY = Math.Max(0, Math.Min(relativeY, bitmap.Height - 1)); + + points[i] = new PointF(relativeX, relativeY); + } + + // 添加路径 - 使用FillMode.Winding确保路径正确填充 + pathGraphics.FillMode = FillMode.Winding; + pathGraphics.AddPolygon(points); + + // 验证路径是否有效 + if (!pathGraphics.IsVisible(0, 0) && pathGraphics.GetBounds().Width > 0 && pathGraphics.GetBounds().Height > 0) + { + // 设置裁剪区域为路径内部 + resultGraphics.SetClip(pathGraphics); + + // 在裁剪区域内绘制原始图像 + resultGraphics.DrawImage(bitmap, 0, 0); + + // 重置裁剪区域,确保后续操作不受影响 + resultGraphics.ResetClip(); + } + else + { + LogHelper.WriteLogToFile("生成的路径无效,返回透明图像", LogHelper.LogType.Warning); + // 如果路径无效,返回透明图像 + return resultBitmap; + } + } + } + + return resultBitmap; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"应用形状遮罩失败: {ex.Message}", LogHelper.LogType.Error); + return bitmap; + } + } + + /// + /// 将System.Drawing.Bitmap转换为WPF BitmapSource + /// + /// 要转换的位图 + /// 转换后的BitmapSource + /// + /// 该方法会: + /// 1. 验证位图有效性 + /// 2. 验证位图尺寸 + /// 3. 使用更安全的方法转换位图 + /// 4. 根据像素格式选择合适的WPF像素格式 + /// 5. 创建BitmapSource + /// 6. 冻结BitmapSource以提高性能 + /// 7. 如果转换失败,尝试使用备用方法 + /// + internal static BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap) + { + try + { + // 验证位图有效性 + if (bitmap == null) + return null; + + // 验证位图尺寸 + if (bitmap.Width <= 0 || bitmap.Height <= 0) + return null; + + // 使用更安全的方法转换位图 + var bitmapData = bitmap.LockBits( + new Rectangle(0, 0, bitmap.Width, bitmap.Height), + ImageLockMode.ReadOnly, + bitmap.PixelFormat); + + try + { + // 根据像素格式选择合适的WPF像素格式 + System.Windows.Media.PixelFormat wpfPixelFormat; + switch (bitmap.PixelFormat) + { + case PixelFormat.Format24bppRgb: + wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24; + break; + case PixelFormat.Format32bppArgb: + wpfPixelFormat = System.Windows.Media.PixelFormats.Bgra32; + break; + case PixelFormat.Format32bppRgb: + wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr32; + break; + default: + wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24; + break; + } + + var bitmapSource = BitmapSource.Create( + bitmapData.Width, + bitmapData.Height, + bitmap.HorizontalResolution, + bitmap.VerticalResolution, + wpfPixelFormat, + null, + bitmapData.Scan0, + bitmapData.Stride * bitmapData.Height, + bitmapData.Stride); + + bitmapSource.Freeze(); + return bitmapSource; + } + finally + { + bitmap.UnlockBits(bitmapData); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"转换位图失败: {ex.Message}", LogHelper.LogType.Error); + + // 尝试使用备用方法:内存流转换 + try + { + return ConvertBitmapToBitmapSourceFallback(bitmap); + } + catch (Exception fallbackEx) + { + LogHelper.WriteLogToFile($"备用转换方法也失败: {fallbackEx.Message}", LogHelper.LogType.Error); + + // 最后尝试:使用最简单的转换方法 + try + { + return ConvertBitmapToBitmapSourceSimple(bitmap); + } + catch (Exception simpleEx) + { + LogHelper.WriteLogToFile($"简单转换方法也失败: {simpleEx.Message}", LogHelper.LogType.Error); + throw; + } + } + } + } + + /// + /// 备用的位图转换方法(使用内存流) + /// + /// 要转换的位图 + /// 转换后的BitmapSource + /// + /// 该方法会: + /// 1. 验证位图有效性 + /// 2. 创建一个新的位图,确保格式正确 + /// 3. 在内存流中保存为PNG格式 + /// 4. 创建BitmapImage并加载内存流中的数据 + /// 5. 冻结BitmapImage以提高性能 + /// + private static BitmapSource ConvertBitmapToBitmapSourceFallback(Bitmap bitmap) + { + try + { + // 验证位图有效性 + if (bitmap == null || bitmap.Width <= 0 || bitmap.Height <= 0) + return null; + + // 创建一个新的位图,确保保留Alpha通道 + using (var convertedBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb)) + { + using (var graphics = Graphics.FromImage(convertedBitmap)) + { + graphics.CompositingMode = CompositingMode.SourceCopy; + graphics.DrawImage(bitmap, 0, 0); + } + + using (var memory = new MemoryStream()) + { + convertedBitmap.Save(memory, ImageFormat.Png); + memory.Position = 0; + + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.StreamSource = memory; + bitmapImage.EndInit(); + bitmapImage.Freeze(); + + return bitmapImage; + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"备用转换方法失败: {ex.Message}", LogHelper.LogType.Error); + throw; + } + } + + /// + /// 最简单的位图转换方法 + /// + /// 要转换的位图 + /// 转换后的BitmapSource + /// + /// 该方法会: + /// 1. 验证位图有效性 + /// 2. 使用最基础的方法:直接保存为PNG然后加载 + /// 3. 创建临时文件 + /// 4. 将位图保存为PNG格式到临时文件 + /// 5. 创建BitmapImage并加载临时文件 + /// 6. 冻结BitmapImage以提高性能 + /// 7. 清理临时文件 + /// + private static BitmapSource ConvertBitmapToBitmapSourceSimple(Bitmap bitmap) + { + try + { + if (bitmap == null) + return null; + + // 使用最基础的方法:直接保存为PNG然后加载 + var tempFile = Path.GetTempFileName() + ".png"; + + try + { + bitmap.Save(tempFile, ImageFormat.Png); + + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.UriSource = new Uri(tempFile); + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.EndInit(); + bitmapImage.Freeze(); + + return bitmapImage; + } + finally + { + // 清理临时文件 + try + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + catch (Exception deleteEx) + { + LogHelper.WriteLogToFile($"删除临时文件失败: {deleteEx.Message}", LogHelper.LogType.Warning); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"简单转换方法失败: {ex.Message}", LogHelper.LogType.Error); + throw; + } + } + } +} diff --git a/Ink Canvas/Services/Shell/TrayIconService.cs b/Ink Canvas/Services/Shell/TrayIconService.cs new file mode 100644 index 000000000..7767cb03e --- /dev/null +++ b/Ink Canvas/Services/Shell/TrayIconService.cs @@ -0,0 +1,840 @@ +using H.NotifyIcon; +using Ink_Canvas.Helpers; +using Ink_Canvas.Properties; +using iNKORE.UI.WPF.Controls; +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Application = System.Windows.Application; + +namespace Ink_Canvas.Services +{ + /// + /// 系统托盘服务(寄生提取)。承载从 App(原 MW_TrayIcon.cs)搬出的托盘图标与右键菜单逻辑。 + /// 禁止引用 MainWindow 类型:所有对 MainWindow/App 私有成员的调用经 委托注入, + /// 事件出抛方向唯一(Service → App)。 + /// 计时器使用 + 注入的 UiInvoke 回到 UI 线程 + ///(统一替换为 ISyncService 前的过渡形态)。 + /// + internal sealed class TrayIconService : IDisposable + { + /// + /// 托盘服务的外部依赖委托集合。由 App 在 CreateTrayIconService 中装配。 + /// + internal sealed class Hooks + { + /// 获取主窗口(以 Window 基类形态返回,服务不接触 MainWindow 类型)。 + public Func GetMainWindow { get; set; } + /// 读取托盘左键单击动作设置。 + public Func GetTrayLeftClickAction { get; set; } + /// 读取托盘右键单击动作设置。 + public Func GetTrayRightClickAction { get; set; } + /// 读取"始终置顶"设置。 + public Func IsAlwaysOnTop { get; set; } + /// 读取"无焦点模式"设置。 + public Func IsNoFocusMode { get; set; } + /// 读取浮动栏是否处于收纳模式。 + public Func IsFloatingBarFolded { get; set; } + /// 进入收纳模式(调用 MainWindow 的 UnFold/Fold 反向操作由服务侧判断)。 + public Action FoldFloatingBar { get; set; } + /// 退出收纳模式。 + public Action UnfoldFloatingBar { get; set; } + /// 重置浮动栏位置(清空拖动标志与坐标后按 PPT/桌面模式播放归位动画)。 + public Action ResetFloatingBarPosition { get; set; } + /// 写入托盘临时显示的截止时间(MainWindow 静态字段)。 + public Action SetTrayTemporaryShowUntilUtc { get; set; } + /// 在 UI 线程上执行委托(过渡形态,后续收敛为 ISyncService)。 + public Action UiInvoke { get; set; } + /// 打开设置窗口(反射调用 MainWindow.BtnSettings_Click)。 + public Action OpenSettings { get; set; } + /// 标记本次退出为用户主动退出。 + public Action MarkExitByUser { get; set; } + /// 标记用户主动退出并调用 MainWindow.ExitApplication。 + public Action ExitApplicationByUser { get; set; } + /// 关闭当前应用实例(Application.Shutdown)。 + public Action Shutdown { get; set; } + /// 强制全屏化主窗口并显示提示消息。 + public Action ForceFullScreen { get; set; } + /// 触发 MainWindow.CheckMainWindowVisibility。 + public Action CheckMainWindowVisibility { get; set; } + /// 反射获取 MainWindow 的全局快捷键管理器。 + public Func GetGlobalHotkeyManager { get; set; } + } + + private const int TrayTemporaryShowMinutes = 2; + + private readonly Hooks _hooks; + + private Timer _trayTemporaryShowTimer; + + private bool _trayTemporaryShowRestoreHideChecked; + + public TrayIconService(Hooks hooks) + { + _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); + } + + /// + /// 停止并释放托盘临时显示计时器。 + /// + public void Dispose() => StopTemporaryShowTimer(); + + /// + /// 插件托盘左键单击事件(App 转发为 PluginTrayLeftClicked 供插件订阅)。 + /// + internal event Action TrayLeftClicked; + + /// + /// 插件托盘右键单击事件(App 转发为 PluginTrayRightClicked 供插件订阅)。 + /// + internal event Action TrayRightClicked; + + public void TrayLeftMouseDown(object sender, RoutedEventArgs e) + { + var action = _hooks.GetTrayLeftClickAction(); + ExecuteTrayClickAction(action); + + try { TrayLeftClicked?.Invoke(); } + catch (Exception ex) { LogHelper.WriteLogToFile($"转发插件托盘左键事件失败: {ex.Message}", LogHelper.LogType.Warning); } + } + + public void TrayRightMouseDown(object sender, RoutedEventArgs e) + { + var action = _hooks.GetTrayRightClickAction(); + ExecuteTrayClickAction(action); + + try { TrayRightClicked?.Invoke(); } + catch (Exception ex) { LogHelper.WriteLogToFile($"转发插件托盘右键事件失败: {ex.Message}", LogHelper.LogType.Warning); } + } + + private void ExecuteTrayClickAction(TrayClickAction action) + { + switch (action) + { + case TrayClickAction.ShowMenu: + ShowTrayContextMenu(); + break; + case TrayClickAction.HideShowMainWindow: + ToggleMainWindowVisibility(); + break; + case TrayClickAction.TempShowMainWindow: + TempShowMainWindowClicked(null, null); + break; + case TrayClickAction.OpenSettings: + OpenSettingsClicked(null, null); + break; + case TrayClickAction.DisableAllHotkeys: + DisableAllHotkeysClicked(FindTrayMenuItem("DisableAllHotkeysMenuItem"), null); + break; + case TrayClickAction.ForceFullScreen: + ForceFullScreenClicked(null, null); + break; + case TrayClickAction.ToggleFoldFloatingBar: + FoldFloatingBarClicked(null, null); + break; + case TrayClickAction.ResetFloatingBarPosition: + ResetFloatingBarPositionClicked(null, null); + break; + case TrayClickAction.RestartApp: + RestartAppClicked(null, null); + break; + case TrayClickAction.CloseApp: + CloseAppClicked(null, null); + break; + case TrayClickAction.NoAction: + break; + } + } + + private MenuItem FindTrayMenuItem(string name) + { + try + { + var trayMenu = ((TaskbarIcon)Application.Current.Resources["TaskbarTrayIcon"]).ContextMenu; + return trayMenu?.Items.OfType().FirstOrDefault(mi => mi.Name == name); + } + catch + { + return null; + } + } + + public void ShowTrayContextMenu() + { + try + { + var taskbarIcon = (TaskbarIcon)Application.Current.Resources["TaskbarTrayIcon"]; + if (taskbarIcon?.ContextMenu != null) + { + taskbarIcon.ContextMenu.IsOpen = true; + } + } + catch (Exception ex) + { + ExceptionHandler.HandleException(ex, "显示托盘右键菜单失败", LogHelper.LogType.Warning); + } + } + + private void ToggleMainWindowVisibility() + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin?.IsLoaded != true) return; + + var hideItem = FindTrayMenuItem("HideICCMainWindowTrayIconMenuItem"); + if (hideItem != null) + { + if (hideItem.IsChecked) + { + hideItem.IsChecked = false; + HideMainWindowUnchecked(hideItem, null); + } + else + { + hideItem.IsChecked = true; + HideMainWindowChecked(hideItem, null); + } + } + else + { + if (mainWin.IsVisible) + mainWin.Hide(); + else + mainWin.Show(); + } + } + + /// + /// 系统托盘菜单打开时的事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理系统托盘菜单打开时的逻辑,包括以下步骤: + /// 1. 获取系统托盘菜单及其相关菜单项和图标 + /// 2. 获取主窗口实例 + /// 3. 如果主窗口已加载: + /// - 在无焦点模式下,暂时取消主窗口置顶,让系统菜单能够正常显示 + /// - 根据浮动栏是否处于收纳模式,更新菜单项图标和文本 + /// - 根据浮动栏状态和主窗口是否隐藏,更新重置浮动栏位置菜单项的启用状态 + /// + public void SysTrayMenuOpened(object sender, RoutedEventArgs e) + { + var s = (ContextMenu)sender; + var FoldFloatingBarTrayIconMenuItemIconEyeOff = (Image)((Grid)((MenuItem)s.Items[s.Items.Count - 5]).Icon).Children[0]; + var FoldFloatingBarTrayIconMenuItemIconEyeOn = (Image)((Grid)((MenuItem)s.Items[s.Items.Count - 5]).Icon).Children[1]; + var FoldFloatingBarTrayIconMenuItemHeaderText = (TextBlock)((SimpleStackPanel)((MenuItem)s.Items[s.Items.Count - 5]).Header).Children[0]; + var ResetFloatingBarPositionTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 4]; + var HideICCMainWindowTrayIconMenuItem = s.Items.OfType() + .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); + if (HideICCMainWindowTrayIconMenuItem == null) return; + var mainWin = _hooks.GetMainWindow(); + if (mainWin.IsLoaded) + { + // 在无焦点模式下,暂时取消主窗口置顶,让系统菜单能够正常显示 + if (_hooks.IsAlwaysOnTop() && _hooks.IsNoFocusMode()) + { + mainWin.Topmost = false; + } + + // 判斷是否在收納模式中 + if (_hooks.IsFloatingBarFolded()) + { + FoldFloatingBarTrayIconMenuItemIconEyeOff.Visibility = Visibility.Hidden; + FoldFloatingBarTrayIconMenuItemIconEyeOn.Visibility = Visibility.Visible; + FoldFloatingBarTrayIconMenuItemHeaderText.Text = MainWindowStrings.Main_Tray_ExitFoldMode; + if (!HideICCMainWindowTrayIconMenuItem.IsChecked) + { + ResetFloatingBarPositionTrayIconMenuItem.IsEnabled = false; + ResetFloatingBarPositionTrayIconMenuItem.Opacity = 0.5; + } + } + else + { + FoldFloatingBarTrayIconMenuItemIconEyeOff.Visibility = Visibility.Visible; + FoldFloatingBarTrayIconMenuItemIconEyeOn.Visibility = Visibility.Hidden; + FoldFloatingBarTrayIconMenuItemHeaderText.Text = MainWindowStrings.Main_Tray_EnterFoldMode; + if (!HideICCMainWindowTrayIconMenuItem.IsChecked) + { + ResetFloatingBarPositionTrayIconMenuItem.IsEnabled = true; + ResetFloatingBarPositionTrayIconMenuItem.Opacity = 1; + } + + } + } + } + + /// + /// 系统托盘菜单关闭时的事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理系统托盘菜单关闭时的逻辑,包括以下步骤: + /// 1. 获取主窗口实例 + /// 2. 如果主窗口已加载,且在无焦点模式下启用了始终置顶,则恢复主窗口的置顶状态 + /// + public void SysTrayMenuClosed(object sender, RoutedEventArgs e) + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin != null && mainWin.IsLoaded) + { + // 菜单关闭后,恢复主窗口的置顶状态 + if (_hooks.IsAlwaysOnTop() && _hooks.IsNoFocusMode()) + { + mainWin.Topmost = true; + } + } + } + + private bool EnsureMainWindowReadyForSettings(Window mainWin) + { + if (mainWin?.IsLoaded != true) + { + return false; + } + + var trayMenu = ((TaskbarIcon)Application.Current.Resources["TaskbarTrayIcon"]).ContextMenu; + var hideMainWindowMenuItem = trayMenu?.Items.OfType() + .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); + + if (hideMainWindowMenuItem != null && hideMainWindowMenuItem.IsChecked) + { + hideMainWindowMenuItem.IsChecked = false; + } + else if (!mainWin.IsVisible) + { + mainWin.Show(); + } + + if (mainWin.WindowState == WindowState.Minimized) + { + mainWin.WindowState = WindowState.Normal; + } + + mainWin.Activate(); + return true; + } + + public void TempShowMainWindowClicked(object sender, RoutedEventArgs e) + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin?.IsLoaded != true) + return; + + MenuItem hideItem = null; + try + { + var trayMenu = ((TaskbarIcon)Application.Current.Resources["TaskbarTrayIcon"]).ContextMenu; + hideItem = trayMenu?.Items.OfType() + .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); + } + catch (Exception ex) + { + LogService.LogException(ex); + } + + _trayTemporaryShowRestoreHideChecked = hideItem?.IsChecked == true; + + EnsureMainWindowReadyForSettings(mainWin); + + _hooks.SetTrayTemporaryShowUntilUtc(DateTime.UtcNow.AddMinutes(TrayTemporaryShowMinutes)); + + StopTemporaryShowTimer(); + _trayTemporaryShowTimer = new Timer( + _ => _hooks.UiInvoke(TemporaryShowTimerTick), null, + TimeSpan.FromMinutes(TrayTemporaryShowMinutes), Timeout.InfiniteTimeSpan); + } + + private void TemporaryShowTimerTick() + { + StopTemporaryShowTimer(); + _hooks.SetTrayTemporaryShowUntilUtc(null); + + var mainWin = _hooks.GetMainWindow(); + if (mainWin?.IsLoaded != true) + { + _trayTemporaryShowRestoreHideChecked = false; + return; + } + + try + { + if (_trayTemporaryShowRestoreHideChecked) + { + var trayMenu = ((TaskbarIcon)Application.Current.Resources["TaskbarTrayIcon"]).ContextMenu; + var hideItem = trayMenu?.Items.OfType() + .FirstOrDefault(mi => mi.Name == "HideICCMainWindowTrayIconMenuItem"); + if (hideItem != null) + hideItem.IsChecked = true; + else + mainWin.Hide(); + } + else + { + _hooks.CheckMainWindowVisibility(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"托盘临时显示计时结束处理失败: {ex.Message}", LogHelper.LogType.Warning); + } + finally + { + _trayTemporaryShowRestoreHideChecked = false; + } + } + + private void StopTemporaryShowTimer() + { + _trayTemporaryShowTimer?.Change(Timeout.Infinite, Timeout.Infinite); + _trayTemporaryShowTimer?.Dispose(); + _trayTemporaryShowTimer = null; + } + + public void OpenSettingsClicked(object sender, RoutedEventArgs e) + { + var mainWin = _hooks.GetMainWindow(); + if (!EnsureMainWindowReadyForSettings(mainWin)) + { + return; + } + + _hooks.OpenSettings(); + } + + /// + /// 关闭应用程序托盘菜单项点击事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理关闭应用程序托盘菜单项的点击事件,包括以下步骤: + /// 1. 获取主窗口实例 + /// 2. 如果主窗口已加载: + /// - 设置IsAppExitByUser为true,表示用户主动退出 + /// - 关闭应用程序 + /// + public void CloseAppClicked(object sender, RoutedEventArgs e) + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin != null && mainWin.IsLoaded) + { + _hooks.ExitApplicationByUser(); + } + } + + /// + /// 重启应用程序托盘菜单项点击事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理重启应用程序托盘菜单项的点击事件,包括以下步骤: + /// 1. 获取主窗口实例 + /// 2. 如果主窗口已加载: + /// - 设置IsAppExitByUser为true,表示用户主动退出 + /// - 尝试启动应用程序的新实例,带延迟参数 + /// - 捕获并记录启动新实例时可能出现的异常 + /// - 关闭当前应用程序实例 + /// + public void RestartAppClicked(object sender, RoutedEventArgs e) + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin != null && mainWin.IsLoaded) + { + _hooks.MarkExitByUser(); + + try + { + // 启动新实例 + string exePath = Process.GetCurrentProcess().MainModule.FileName; + ProcessStartInfo startInfo = new ProcessStartInfo(); + startInfo.FileName = exePath; + startInfo.UseShellExecute = true; + + // 启动进程但不等待 + Process.Start(new ProcessStartInfo(exePath, "-delay 2000") { UseShellExecute = true }); + } + catch (Exception ex) + { + LogHelper.NewLog($"重启程序时出错: {ex.Message}"); + } + + // 退出当前实例 + _hooks.Shutdown(); + } + } + + /// + /// 强制全屏化托盘菜单项点击事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理强制全屏化托盘菜单项的点击事件,包括以下步骤: + /// 1. 获取主窗口实例 + /// 2. 如果主窗口已加载: + /// - 调用MoveWindow方法将主窗口移动到屏幕左上角并设置为全屏大小 + /// - 显示强制全屏化的消息,包含屏幕分辨率和缩放比例信息 + /// + public void ForceFullScreenClicked(object sender, RoutedEventArgs e) + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin != null && mainWin.IsLoaded) + { + _hooks.ForceFullScreen(); + } + } + + /// + /// 切换浮动栏收纳模式托盘菜单项点击事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理切换浮动栏收纳模式托盘菜单项的点击事件,包括以下步骤: + /// 1. 获取主窗口实例 + /// 2. 如果主窗口已加载: + /// - 如果浮动栏当前处于收纳模式,则调用UnFoldFloatingBar_MouseUp方法退出收纳模式 + /// - 否则,调用FoldFloatingBar_MouseUp方法进入收纳模式 + /// + public void FoldFloatingBarClicked(object sender, RoutedEventArgs e) + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin != null && mainWin.IsLoaded) + if (_hooks.IsFloatingBarFolded()) _hooks.UnfoldFloatingBar(); + else _hooks.FoldFloatingBar(); + } + + /// + /// 重置浮动栏位置托盘菜单项点击事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理重置浮动栏位置托盘菜单项的点击事件,包括以下步骤: + /// 1. 获取主窗口实例 + /// 2. 如果主窗口已加载: + /// - 清空用户拖动标志和已保存的位置坐标,让动画走默认位置分支 + /// - 如果浮动栏当前未处于收纳模式: + /// - 如果不处于PPT演示模式,调用PureViewboxFloatingBarMarginAnimationInDesktopMode方法重置浮动栏位置 + /// - 否则,调用PureViewboxFloatingBarMarginAnimationInPPTMode方法重置浮动栏位置 + /// + public void ResetFloatingBarPositionClicked(object sender, RoutedEventArgs e) + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin != null && mainWin.IsLoaded) + { + _hooks.ResetFloatingBarPosition(); + } + } + + /// + /// 隐藏主窗口托盘菜单项选中事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理隐藏主窗口托盘菜单项的选中事件,包括以下步骤: + /// 1. 获取菜单项和主窗口实例 + /// 2. 如果主窗口已加载: + /// - 隐藏主窗口 + /// - 获取系统托盘菜单 + /// - 禁用并设置半透明效果给以下菜单项: + /// - 重置浮动栏位置 + /// - 切换浮动栏收纳模式 + /// - 强制全屏化 + /// 3. 否则,取消菜单项的选中状态 + /// + public void HideMainWindowChecked(object sender, RoutedEventArgs e) + { + StopTemporaryShowTimer(); + _hooks.SetTrayTemporaryShowUntilUtc(null); + _trayTemporaryShowRestoreHideChecked = false; + + var mi = (MenuItem)sender; + var mainWin = _hooks.GetMainWindow(); + if (mainWin != null && mainWin.IsLoaded) + { + mainWin.Hide(); + var s = ((TaskbarIcon)Application.Current.Resources["TaskbarTrayIcon"]).ContextMenu; + if (s != null) + { + var ResetFloatingBarPositionTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 4]; + var FoldFloatingBarTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 5]; + var ForceFullScreenTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 6]; + ResetFloatingBarPositionTrayIconMenuItem.IsEnabled = false; + FoldFloatingBarTrayIconMenuItem.IsEnabled = false; + ForceFullScreenTrayIconMenuItem.IsEnabled = false; + ResetFloatingBarPositionTrayIconMenuItem.Opacity = 0.5; + FoldFloatingBarTrayIconMenuItem.Opacity = 0.5; + ForceFullScreenTrayIconMenuItem.Opacity = 0.5; + } + } + else + { + mi.IsChecked = false; + } + + } + + /// + /// 显示主窗口托盘菜单项取消选中事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理显示主窗口托盘菜单项的取消选中事件,包括以下步骤: + /// 1. 获取菜单项和主窗口实例 + /// 2. 如果主窗口已加载: + /// - 显示主窗口 + /// - 获取系统托盘菜单 + /// - 启用并设置正常透明度给以下菜单项: + /// - 重置浮动栏位置 + /// - 切换浮动栏收纳模式 + /// - 强制全屏化 + /// 3. 否则,取消菜单项的选中状态 + /// + public void HideMainWindowUnchecked(object sender, RoutedEventArgs e) + { + var mi = (MenuItem)sender; + var mainWin = _hooks.GetMainWindow(); + if (mainWin != null && mainWin.IsLoaded) + { + mainWin.Show(); + var s = ((TaskbarIcon)Application.Current.Resources["TaskbarTrayIcon"]).ContextMenu; + if (s != null) + { + var ResetFloatingBarPositionTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 4]; + var FoldFloatingBarTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 5]; + var ForceFullScreenTrayIconMenuItem = (MenuItem)s.Items[s.Items.Count - 6]; + ResetFloatingBarPositionTrayIconMenuItem.IsEnabled = true; + FoldFloatingBarTrayIconMenuItem.IsEnabled = true; + ForceFullScreenTrayIconMenuItem.IsEnabled = true; + ResetFloatingBarPositionTrayIconMenuItem.Opacity = 1; + FoldFloatingBarTrayIconMenuItem.Opacity = 1; + ForceFullScreenTrayIconMenuItem.Opacity = 1; + } + } + else + { + mi.IsChecked = false; + } + } + + /// + /// 禁用/启用所有快捷键托盘菜单项点击事件处理方法 + /// + /// 事件发送者 + /// 路由事件参数 + /// + /// 处理禁用/启用所有快捷键托盘菜单项的点击事件,包括以下步骤: + /// 1. 获取主窗口实例 + /// 2. 如果主窗口已加载,尝试: + /// - 通过反射获取全局快捷键管理器 + /// - 如果获取成功: + /// - 禁用快捷键注册 + /// - 更新菜单项文本和状态: + /// - 如果当前文本是"禁用所有快捷键",则更改为"启用所有快捷键"并记录日志 + /// - 否则,更改为"禁用所有快捷键",重新启用快捷键注册并记录日志 + /// - 如果获取失败,记录错误日志 + /// 3. 捕获并记录可能出现的异常 + /// + public void DisableAllHotkeysClicked(object sender, RoutedEventArgs e) + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin != null && mainWin.IsLoaded) + { + try + { + // 获取全局快捷键管理器 + var hotkeyManager = _hooks.GetGlobalHotkeyManager(); + + if (hotkeyManager != null) + { + // 禁用所有快捷键 + hotkeyManager.DisableHotkeyRegistration(); + + // 更新菜单项文本和状态 + var menuItem = sender as MenuItem; + if (menuItem != null) + { + var headerPanel = menuItem.Header as SimpleStackPanel; + if (headerPanel != null) + { + var textBlock = headerPanel.Children[0] as TextBlock; + if (textBlock != null) + { + if (textBlock.Text == MainWindowStrings.Main_Tray_DisableHotkeys) + { + textBlock.Text = MainWindowStrings.Main_Tray_EnableHotkeys; + LogHelper.WriteLogToFile("已禁用所有快捷键", LogHelper.LogType.Event); + } + else + { + textBlock.Text = MainWindowStrings.Main_Tray_DisableHotkeys; + // 重新启用快捷键 + hotkeyManager.EnableHotkeyRegistration(); + LogHelper.WriteLogToFile("已启用所有快捷键", LogHelper.LogType.Event); + } + } + } + } + } + else + { + LogHelper.WriteLogToFile("无法获取全局快捷键管理器", LogHelper.LogType.Error); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"禁用/启用快捷键时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + } + + #region 插件托盘服务(ITrayService 宿主支持) + + public TaskbarIcon GetPluginTaskbarIcon() + { + try { return Application.Current.Resources["TaskbarTrayIcon"] as TaskbarIcon; } + catch { return null; } + } + + public void SetPluginTrayIconVisibility(bool visible) + { + var icon = GetPluginTaskbarIcon(); + if (icon != null) icon.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; + } + + /// + /// 向托盘右键菜单注入一个插件菜单项,插入到「重启程序」之前, + /// 保持 SysTrayMenuOpened / HideMainWindowChecked 等 + /// 用倒数索引访问固定菜单项的兼容性。 + /// + public bool AddPluginTrayMenuItem(string id, string text, Action onClicked) + { + if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(text) || onClicked == null) return false; + try + { + var trayMenu = GetPluginTaskbarIcon()?.ContextMenu; + if (trayMenu == null) return false; + + string menuName = "PluginTray." + id; + if (trayMenu.Items.OfType().Any(mi => mi.Name == menuName)) return false; + + var item = new MenuItem + { + Name = menuName, + Header = new TextBlock + { + Text = text, + FontSize = 14, + VerticalAlignment = VerticalAlignment.Center, + Foreground = new SolidColorBrush(Color.FromRgb(0x18, 0x18, 0x1b)) + } + }; + item.Click += (s, e) => onClicked(); + + var restartItem = trayMenu.Items.OfType() + .FirstOrDefault(mi => mi.Name == "RestartAppTrayIconMenuItem"); + int insertIndex = restartItem != null ? trayMenu.Items.IndexOf(restartItem) : trayMenu.Items.Count; + trayMenu.Items.Insert(insertIndex, item); + return true; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"添加插件托盘菜单项失败: {ex.Message}", LogHelper.LogType.Warning); + return false; + } + } + + public bool RemovePluginTrayMenuItem(string id) + { + if (string.IsNullOrWhiteSpace(id)) return false; + try + { + var trayMenu = GetPluginTaskbarIcon()?.ContextMenu; + if (trayMenu == null) return false; + + var item = trayMenu.Items.OfType() + .FirstOrDefault(mi => mi.Name == "PluginTray." + id); + if (item == null) return false; + + trayMenu.Items.Remove(item); + return true; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"移除插件托盘菜单项失败: {ex.Message}", LogHelper.LogType.Warning); + return false; + } + } + + public bool HasPluginTrayMenuItem(string id) + { + if (string.IsNullOrWhiteSpace(id)) return false; + try + { + var trayMenu = GetPluginTaskbarIcon()?.ContextMenu; + return trayMenu?.Items.OfType().Any(mi => mi.Name == "PluginTray." + id) == true; + } + catch { return false; } + } + + /// + /// 移除某个插件注册的所有托盘菜单项。插件热重载时调用:菜单项的 Click 处理器 + /// 闭包持有插件程序集中的回调,留着会阻止插件 AssemblyLoadContext 卸载。 + /// 按 "PluginTray.<pluginId>" 前缀匹配,覆盖插件注册多个菜单项的情况。 + /// + public int RemovePluginTrayMenuItemsByPrefix(string pluginId) + { + if (string.IsNullOrWhiteSpace(pluginId)) return 0; + try + { + var trayMenu = GetPluginTaskbarIcon()?.ContextMenu; + if (trayMenu == null) return 0; + + var prefix = "PluginTray." + pluginId; + var doomed = trayMenu.Items.OfType() + .Where(mi => mi.Name != null + && (mi.Name == prefix || mi.Name.StartsWith(prefix + ".", StringComparison.Ordinal))) + .ToList(); + + foreach (var item in doomed) trayMenu.Items.Remove(item); + return doomed.Count; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"按插件移除托盘菜单项失败: {ex.Message}", LogHelper.LogType.Warning); + return 0; + } + } + + public void ShowPluginMainWindow() + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin?.IsLoaded != true) return; + + var hideItem = FindTrayMenuItem("HideICCMainWindowTrayIconMenuItem"); + if (hideItem != null) hideItem.IsChecked = false; // 触发 Unchecked → Show + 启用相关菜单项 + else if (!mainWin.IsVisible) mainWin.Show(); + } + + public void HidePluginMainWindow() + { + var mainWin = _hooks.GetMainWindow(); + if (mainWin?.IsLoaded != true) return; + + var hideItem = FindTrayMenuItem("HideICCMainWindowTrayIconMenuItem"); + if (hideItem != null) hideItem.IsChecked = true; // 触发 Checked → Hide + 禁用相关菜单项 + else if (mainWin.IsVisible) mainWin.Hide(); + } + + #endregion + } +} diff --git a/Ink Canvas/UInk/UInkSaveService.cs b/Ink Canvas/UInk/UInkSaveService.cs index 11d96e265..168d6f36b 100644 --- a/Ink Canvas/UInk/UInkSaveService.cs +++ b/Ink Canvas/UInk/UInkSaveService.cs @@ -65,11 +65,7 @@ public static void SaveFull(UInkDocument doc, string mainPath, { if (workDir != null) { - try { if (Directory.Exists(workDir)) Directory.Delete(workDir, true); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[UInk] 两阶段提交后删除资源并集临时目录失败: {workDir}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + try { if (Directory.Exists(workDir)) Directory.Delete(workDir, true); } catch (Exception ex) { LogService.LogException(ex); } } } } @@ -130,10 +126,7 @@ public static void SaveFull(UInkDocument doc, string mainPath, } } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[UInk] 提取旧资源包以保留旧主文件引用失败: {extraPath}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } } } @@ -150,11 +143,7 @@ private static void AtomicReplace(string tmp, string target) private static void TryDelete(string path) { - try { if (File.Exists(path)) File.Delete(path); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[UInk] 删除临时文件失败: {path}, 原因: {ex.Message}", LogHelper.LogType.Info); - } + try { if (File.Exists(path)) File.Delete(path); } catch (Exception ex) { LogService.LogException(ex); } } } } diff --git a/Ink Canvas/UInk/UInkWriter.cs b/Ink Canvas/UInk/UInkWriter.cs index c118e5d93..ea5424884 100644 --- a/Ink Canvas/UInk/UInkWriter.cs +++ b/Ink Canvas/UInk/UInkWriter.cs @@ -27,14 +27,9 @@ public static void Save(UInkDocument doc, string path) else File.Move(tmp, path); } - catch (Exception ex) + catch { - LogHelper.WriteLogToFile($"[UInk] 写入主文件失败(临时文件 {tmp} 未提交): {path}, 原因: {ex.Message}", LogHelper.LogType.Info); - try { if (File.Exists(tmp)) File.Delete(tmp); } - catch (Exception cleanupEx) - { - LogHelper.WriteLogToFile($"[UInk] 写入主文件失败后删除临时文件失败: {tmp}, 原因: {cleanupEx.Message}", LogHelper.LogType.Info); - } + try { if (File.Exists(tmp)) File.Delete(tmp); } catch (Exception cleanupEx) { Helpers.LogService.LogException(cleanupEx); } throw; } } diff --git a/Ink Canvas/Windows/AnnouncementCenterWindow.xaml.cs b/Ink Canvas/Windows/AnnouncementCenterWindow.xaml.cs index 35cd8a6a7..92d6ac5a4 100644 --- a/Ink Canvas/Windows/AnnouncementCenterWindow.xaml.cs +++ b/Ink Canvas/Windows/AnnouncementCenterWindow.xaml.cs @@ -55,11 +55,9 @@ private void ViewDetailsButton_Click(object sender, RoutedEventArgs e) Process.Start(new ProcessStartInfo(item.ActionUrl) { UseShellExecute = true }); return; } - catch (Exception ex) + catch (System.Exception ex) { - LogHelper.WriteLogToFile( - $"[Announcement] 公告详情点击打开链接 {item.ActionUrl} 失败: {ex.Message}", - LogHelper.LogType.Info); + LogService.LogException(ex); } } diff --git a/Ink Canvas/Windows/CountdownTimerWindow.xaml.cs b/Ink Canvas/Windows/CountdownTimerWindow.xaml.cs index f28cb1b2c..425b03657 100644 --- a/Ink Canvas/Windows/CountdownTimerWindow.xaml.cs +++ b/Ink Canvas/Windows/CountdownTimerWindow.xaml.cs @@ -1,8 +1,9 @@ using Ink_Canvas.Helpers; +using Ink_Canvas.Models; +using Ink_Canvas.Services.Classroom; using iNKORE.UI.WPF.Modern.Common.IconKeys; using System; using System.Media; -using System.Timers; using System.Windows; using System.Windows.Input; using System.Windows.Interop; @@ -20,8 +21,14 @@ public CountdownTimerWindow() InitializeComponent(); AnimationsHelper.ShowWithSlideFromBottomAndFade(this, 0.25); - timer.Elapsed += Timer_Elapsed; - timer.Interval = 50; + _timerService = new TimerService( + TimerService.LegacyIntervalForTotal, + () => MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true, + () => false); + // 原字段初始值:hour=0, minute=1, second=0 + _timerService.Minute = 1; + _timerService.StateChanged += OnTimerStateChanged; + InitializeUI(); // 应用主题 @@ -33,109 +40,87 @@ public static Window CreateTimerWindow() return new CountdownTimerWindow(); } - private void Timer_Elapsed(object sender, ElapsedEventArgs e) + // 计时状态机已提取至 TimerService;本窗口仅订阅状态并渲染。 + private readonly TimerService _timerService; + + private void OnTimerStateChanged(TimerState state) + { + Application.Current.Dispatcher.Invoke(() => RenderTick(state)); + } + + private void RenderTick(TimerState state) { - if (!isTimerRunning || isPaused) + if (!state.IsOvertimeMode || state.JustEnteredOvertime) { - timer.Stop(); - return; - } + TimeSpan leftTimeSpan = state.Remaining; - TimeSpan timeSpan = DateTime.Now - startTime; - TimeSpan totalTimeSpan = new TimeSpan(hour, minute, second); - double spentTimePercent = timeSpan.TotalMilliseconds / (totalSeconds * 1000.0); + ProcessBarTime.CurrentValue = 1 - state.SpentPercent; + TextBlockHour.Text = leftTimeSpan.Hours.ToString("00"); + TextBlockMinute.Text = leftTimeSpan.Minutes.ToString("00"); + TextBlockSecond.Text = leftTimeSpan.Seconds.ToString("00"); + TbCurrentTime.Text = leftTimeSpan.ToString(@"hh\:mm\:ss"); - Application.Current.Dispatcher.Invoke(() => - { - if (!isOvertimeMode) + if (state.JustEnteredOvertime) { - TimeSpan leftTimeSpan = totalTimeSpan - timeSpan; - if (leftTimeSpan.Milliseconds > 0) leftTimeSpan += new TimeSpan(0, 0, 1); - - ProcessBarTime.CurrentValue = 1 - spentTimePercent; - TextBlockHour.Text = leftTimeSpan.Hours.ToString("00"); - TextBlockMinute.Text = leftTimeSpan.Minutes.ToString("00"); - TextBlockSecond.Text = leftTimeSpan.Seconds.ToString("00"); - TbCurrentTime.Text = leftTimeSpan.ToString(@"hh\:mm\:ss"); + ProcessBarTime.CurrentValue = 0; + ProcessBarTime.Visibility = Visibility.Collapsed; + BorderStopTime.Visibility = Visibility.Collapsed; - if (spentTimePercent >= 1 && MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true) + // 播放提醒音 + PlayTimerSound(); + } + else if (state.JustCompleted) + { + ProcessBarTime.CurrentValue = 0; + TextBlockHour.Text = "00"; + TextBlockMinute.Text = "00"; + TextBlockSecond.Text = "00"; + FontIconStart.Icon = SegoeFluentIcons.Play; + BtnStartCover.Visibility = Visibility.Visible; + var textForeground = Application.Current.FindResource("TimerWindowTextForeground") as SolidColorBrush; + if (textForeground != null) { - isOvertimeMode = true; - ProcessBarTime.CurrentValue = 0; - ProcessBarTime.Visibility = Visibility.Collapsed; - BorderStopTime.Visibility = Visibility.Collapsed; - - // 播放提醒音 - PlayTimerSound(); + TextBlockHour.Foreground = textForeground; } - else if (spentTimePercent >= 1) + else { - ProcessBarTime.CurrentValue = 0; - TextBlockHour.Text = "00"; - TextBlockMinute.Text = "00"; - TextBlockSecond.Text = "00"; - timer.Stop(); - isTimerRunning = false; - FontIconStart.Icon = SegoeFluentIcons.Play; - BtnStartCover.Visibility = Visibility.Visible; - var textForeground = Application.Current.FindResource("TimerWindowTextForeground") as SolidColorBrush; - if (textForeground != null) - { - TextBlockHour.Foreground = textForeground; - } - else - { - TextBlockHour.Foreground = new SolidColorBrush(StringToColor("#FF5B5D5F")); - } - BorderStopTime.Visibility = Visibility.Collapsed; - - // 播放提醒音 - PlayTimerSound(); + TextBlockHour.Foreground = new SolidColorBrush(StringToColor("#FF5B5D5F")); } + BorderStopTime.Visibility = Visibility.Collapsed; + + // 播放提醒音 + PlayTimerSound(); } - else - { - TimeSpan overtimeSpan = timeSpan - totalTimeSpan; - TextBlockHour.Text = overtimeSpan.Hours.ToString("00"); - TextBlockMinute.Text = overtimeSpan.Minutes.ToString("00"); - TextBlockSecond.Text = overtimeSpan.Seconds.ToString("00"); - TbCurrentTime.Text = overtimeSpan.ToString(@"hh\:mm\:ss"); + } + else + { + TimeSpan overtimeSpan = state.Overtime; + TextBlockHour.Text = overtimeSpan.Hours.ToString("00"); + TextBlockMinute.Text = overtimeSpan.Minutes.ToString("00"); + TextBlockSecond.Text = overtimeSpan.Seconds.ToString("00"); + TbCurrentTime.Text = overtimeSpan.ToString(@"hh\:mm\:ss"); - if (MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true) - { - TextBlockHour.Foreground = Brushes.Red; - TextBlockMinute.Foreground = Brushes.Red; - TextBlockSecond.Foreground = Brushes.Red; - } + if (MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true) + { + TextBlockHour.Foreground = Brushes.Red; + TextBlockMinute.Foreground = Brushes.Red; + TextBlockSecond.Foreground = Brushes.Red; } - }); + } } SoundPlayer player = new SoundPlayer(); MediaPlayer mediaPlayer = new MediaPlayer(); - int hour = 0; - int minute = 1; - int second = 0; - int totalSeconds = 60; - - DateTime startTime = DateTime.Now; - DateTime pauseTime = DateTime.Now; - - bool isTimerRunning = false; - bool isPaused = false; bool useLegacyUI = false; - bool isOvertimeMode = false; - - Timer timer = new Timer(); private void Grid_MouseUp(object sender, MouseButtonEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; var textForeground = Application.Current.FindResource("TimerWindowTextForeground") as SolidColorBrush; - if (ProcessBarTime.Visibility == Visibility.Visible && isTimerRunning == false) + if (ProcessBarTime.Visibility == Visibility.Visible && _timerService.IsRunning == false) { ProcessBarTime.Visibility = Visibility.Collapsed; GridAdjustHour.Visibility = Visibility.Visible; @@ -161,96 +146,96 @@ private void Grid_MouseUp(object sender, MouseButtonEventArgs e) TextBlockHour.Foreground = new SolidColorBrush(StringToColor("#FF5B5D5F")); } - if (hour == 0 && minute == 0 && second == 0) + if (_timerService.Hour == 0 && _timerService.Minute == 0 && _timerService.Second == 0) { - second = 1; - TextBlockSecond.Text = second.ToString("00"); + _timerService.Second = 1; + TextBlockSecond.Text = _timerService.Second.ToString("00"); } } } private void Button_Click(object sender, RoutedEventArgs e) { - hour++; - if (hour >= 100) hour = 0; - TextBlockHour.Text = hour.ToString("00"); + _timerService.Hour++; + if (_timerService.Hour >= 100) _timerService.Hour = 0; + TextBlockHour.Text = _timerService.Hour.ToString("00"); } private void Button_Click_1(object sender, RoutedEventArgs e) { - hour += 5; - if (hour >= 100) hour = 0; - TextBlockHour.Text = hour.ToString("00"); + _timerService.Hour += 5; + if (_timerService.Hour >= 100) _timerService.Hour = 0; + TextBlockHour.Text = _timerService.Hour.ToString("00"); } private void Button_Click_2(object sender, RoutedEventArgs e) { - hour--; - if (hour < 0) hour = 99; - TextBlockHour.Text = hour.ToString("00"); + _timerService.Hour--; + if (_timerService.Hour < 0) _timerService.Hour = 99; + TextBlockHour.Text = _timerService.Hour.ToString("00"); } private void Button_Click_3(object sender, RoutedEventArgs e) { - hour -= 5; - if (hour < 0) hour = 99; - TextBlockHour.Text = hour.ToString("00"); + _timerService.Hour -= 5; + if (_timerService.Hour < 0) _timerService.Hour = 99; + TextBlockHour.Text = _timerService.Hour.ToString("00"); } private void Button_Click_4(object sender, RoutedEventArgs e) { - minute++; - if (minute >= 60) minute = 0; - TextBlockMinute.Text = minute.ToString("00"); + _timerService.Minute++; + if (_timerService.Minute >= 60) _timerService.Minute = 0; + TextBlockMinute.Text = _timerService.Minute.ToString("00"); } private void Button_Click_5(object sender, RoutedEventArgs e) { - minute += 5; - if (minute >= 60) minute = 0; - TextBlockMinute.Text = minute.ToString("00"); + _timerService.Minute += 5; + if (_timerService.Minute >= 60) _timerService.Minute = 0; + TextBlockMinute.Text = _timerService.Minute.ToString("00"); } private void Button_Click_6(object sender, RoutedEventArgs e) { - minute--; - if (minute < 0) minute = 59; - TextBlockMinute.Text = minute.ToString("00"); + _timerService.Minute--; + if (_timerService.Minute < 0) _timerService.Minute = 59; + TextBlockMinute.Text = _timerService.Minute.ToString("00"); } private void Button_Click_7(object sender, RoutedEventArgs e) { - minute -= 5; - if (minute < 0) minute = 59; - TextBlockMinute.Text = minute.ToString("00"); + _timerService.Minute -= 5; + if (_timerService.Minute < 0) _timerService.Minute = 59; + TextBlockMinute.Text = _timerService.Minute.ToString("00"); } private void Button_Click_8(object sender, RoutedEventArgs e) { - second += 5; - if (second >= 60) second = 0; - TextBlockSecond.Text = second.ToString("00"); + _timerService.Second += 5; + if (_timerService.Second >= 60) _timerService.Second = 0; + TextBlockSecond.Text = _timerService.Second.ToString("00"); } private void Button_Click_9(object sender, RoutedEventArgs e) { - second++; - if (second >= 60) second = 0; - TextBlockSecond.Text = second.ToString("00"); + _timerService.Second++; + if (_timerService.Second >= 60) _timerService.Second = 0; + TextBlockSecond.Text = _timerService.Second.ToString("00"); } private void Button_Click_10(object sender, RoutedEventArgs e) { - second--; - if (second < 0) second = 59; - TextBlockSecond.Text = second.ToString("00"); + _timerService.Second--; + if (_timerService.Second < 0) _timerService.Second = 59; + TextBlockSecond.Text = _timerService.Second.ToString("00"); } private void Button_Click_11(object sender, RoutedEventArgs e) { - second -= 5; - if (second < 0) second = 59; - TextBlockSecond.Text = second.ToString("00"); + _timerService.Second -= 5; + if (_timerService.Second < 0) _timerService.Second = 59; + TextBlockSecond.Text = _timerService.Second.ToString("00"); } private void Window_Loaded(object sender, RoutedEventArgs e) @@ -276,11 +261,11 @@ private void BtnFullscreen_MouseUp(object sender, MouseButtonEventArgs e) private void BtnReset_MouseUp(object sender, MouseButtonEventArgs e) { - if (!isTimerRunning) + if (!_timerService.IsRunning) { - TextBlockHour.Text = hour.ToString("00"); - TextBlockMinute.Text = minute.ToString("00"); - TextBlockSecond.Text = second.ToString("00"); + TextBlockHour.Text = _timerService.Hour.ToString("00"); + TextBlockMinute.Text = _timerService.Minute.ToString("00"); + TextBlockSecond.Text = _timerService.Second.ToString("00"); BtnResetCover.Visibility = Visibility.Visible; BtnStartCover.Visibility = Visibility.Collapsed; BorderStopTime.Visibility = Visibility.Collapsed; @@ -290,14 +275,14 @@ private void BtnReset_MouseUp(object sender, MouseButtonEventArgs e) else TextBlockHour.Foreground = new SolidColorBrush(StringToColor("#FF5B5D5F")); - isOvertimeMode = false; + _timerService.Reset(); ProcessBarTime.Visibility = Visibility.Visible; } - else if (isTimerRunning && isPaused) + else if (_timerService.IsRunning && _timerService.IsPaused) { - TextBlockHour.Text = hour.ToString("00"); - TextBlockMinute.Text = minute.ToString("00"); - TextBlockSecond.Text = second.ToString("00"); + TextBlockHour.Text = _timerService.Hour.ToString("00"); + TextBlockMinute.Text = _timerService.Minute.ToString("00"); + TextBlockSecond.Text = _timerService.Second.ToString("00"); BtnResetCover.Visibility = Visibility.Visible; BtnStartCover.Visibility = Visibility.Collapsed; BorderStopTime.Visibility = Visibility.Collapsed; @@ -307,27 +292,22 @@ private void BtnReset_MouseUp(object sender, MouseButtonEventArgs e) else TextBlockHour.Foreground = new SolidColorBrush(StringToColor("#FF5B5D5F")); FontIconStart.Icon = SegoeFluentIcons.Play; - isTimerRunning = false; - timer.Stop(); - isPaused = false; + _timerService.Reset(); ProcessBarTime.CurrentValue = 0; ProcessBarTime.IsPaused = false; - isOvertimeMode = false; ProcessBarTime.Visibility = Visibility.Visible; } else { UpdateStopTime(); - startTime = DateTime.Now; - Timer_Elapsed(timer, null); + _timerService.Restart(); } } void UpdateStopTime() { - TimeSpan totalTimeSpan = new TimeSpan(hour, minute, second); - TextBlockStopTime.Text = (startTime + totalTimeSpan).ToString("t"); + TextBlockStopTime.Text = (_timerService.StartTime + _timerService.GetTotalTimeSpan()).ToString("t"); } private Color StringToColor(string colorStr) @@ -353,10 +333,10 @@ private static byte toByte(char c) private void BtnStart_MouseUp(object sender, MouseButtonEventArgs e) { - if (isPaused && isTimerRunning) + if (_timerService.IsPaused && _timerService.IsRunning) { //继续 - startTime += DateTime.Now - pauseTime; + _timerService.Resume(); ProcessBarTime.IsPaused = false; var textForeground1 = Application.Current.FindResource("TimerWindowTextForeground") as SolidColorBrush; if (textForeground1 != null) @@ -364,15 +344,13 @@ private void BtnStart_MouseUp(object sender, MouseButtonEventArgs e) else TextBlockHour.Foreground = Brushes.Black; FontIconStart.Icon = SegoeFluentIcons.Pause; - isPaused = false; - timer.Start(); UpdateStopTime(); BorderStopTime.Visibility = Visibility.Visible; } - else if (isTimerRunning) + else if (_timerService.IsRunning) { //暂停 - pauseTime = DateTime.Now; + _timerService.Pause(); ProcessBarTime.IsPaused = true; var textForeground3 = Application.Current.FindResource("TimerWindowTextForeground") as SolidColorBrush; if (textForeground3 != null) @@ -381,14 +359,11 @@ private void BtnStart_MouseUp(object sender, MouseButtonEventArgs e) TextBlockHour.Foreground = new SolidColorBrush(StringToColor("#FF5B5D5F")); FontIconStart.Icon = SegoeFluentIcons.Play; BorderStopTime.Visibility = Visibility.Collapsed; - isPaused = true; - timer.Stop(); } else { //从头开始 - startTime = DateTime.Now; - totalSeconds = ((hour * 60) + minute) * 60 + second; + _timerService.Start(); ProcessBarTime.IsPaused = false; var textForeground2 = Application.Current.FindResource("TimerWindowTextForeground") as SolidColorBrush; if (textForeground2 != null) @@ -397,29 +372,7 @@ private void BtnStart_MouseUp(object sender, MouseButtonEventArgs e) TextBlockHour.Foreground = Brushes.Black; FontIconStart.Icon = SegoeFluentIcons.Pause; BtnResetCover.Visibility = Visibility.Collapsed; - - if (totalSeconds <= 10) - { - timer.Interval = 20; - } - else if (totalSeconds <= 60) - { - timer.Interval = 30; - } - else if (totalSeconds <= 120) - { - timer.Interval = 50; - } - else - { - timer.Interval = 100; - } - - isPaused = false; - isTimerRunning = true; - isOvertimeMode = false; ProcessBarTime.Visibility = Visibility.Visible; - timer.Start(); UpdateStopTime(); BorderStopTime.Visibility = Visibility.Visible; } @@ -556,7 +509,7 @@ private void PlayTimerSound() private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { - isTimerRunning = false; + _timerService.MarkStopped(); } private void BtnClose_MouseUp(object sender, MouseButtonEventArgs e) @@ -609,4 +562,4 @@ private void WindowDragMove(object sender, MouseEventArgs e) DragMove(); } } -} \ No newline at end of file +} diff --git a/Ink Canvas/Windows/CrashWindow.xaml.cs b/Ink Canvas/Windows/CrashWindow.xaml.cs index 88c388fee..835b0e7a0 100644 --- a/Ink Canvas/Windows/CrashWindow.xaml.cs +++ b/Ink Canvas/Windows/CrashWindow.xaml.cs @@ -113,11 +113,7 @@ private static bool IsSystemThemeLight() if (value is int i) return i == 1; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[Crash] 崩溃窗口读取系统主题注册表项失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } return true; } } diff --git a/Ink Canvas/Windows/LiquidGlassBarWindow.xaml.cs b/Ink Canvas/Windows/LiquidGlassBarWindow.xaml.cs index 1b3199f92..477684fa5 100644 --- a/Ink Canvas/Windows/LiquidGlassBarWindow.xaml.cs +++ b/Ink Canvas/Windows/LiquidGlassBarWindow.xaml.cs @@ -611,7 +611,7 @@ internal void ClampIntoWorkingArea() /// 按主窗当前状态点亮对应按钮。选中用 Tag="on" 驱动样式里的填充胶囊, /// 参考 GitHub 移动端底栏:玻璃背景下只换图标颜色区分度不够。 /// - /// 主窗的 _currentToolMode。 + /// 主窗当前工具模式的内部字符串(ToolModeMapping.ToInternalString 输出)。 /// 0 = 普通笔,1 = 荧光笔。 /// 当前画笔颜色,用于点亮颜色圆点。 internal void SyncActiveState(string toolMode, int penType, Color? color) diff --git a/Ink Canvas/Windows/MiniWhiteboardWindow.xaml.cs b/Ink Canvas/Windows/MiniWhiteboardWindow.xaml.cs index 444a858eb..64bacfa69 100644 --- a/Ink Canvas/Windows/MiniWhiteboardWindow.xaml.cs +++ b/Ink Canvas/Windows/MiniWhiteboardWindow.xaml.cs @@ -277,11 +277,7 @@ private void ApplyPenSettings() { MiniInkCanvas.DefaultDrawingAttributes.Color = (Color)ColorConverter.ConvertFromString(settings.PenColor); } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[MiniBoard] 迷你白板按颜色字符串设置笔色失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } MiniInkCanvas.DefaultDrawingAttributes.Width = settings.PenWidth; diff --git a/Ink Canvas/Windows/NewStyleMinimizedTimerWindow.xaml.cs b/Ink Canvas/Windows/NewStyleMinimizedTimerWindow.xaml.cs index e867972f7..2582aed93 100644 --- a/Ink Canvas/Windows/NewStyleMinimizedTimerWindow.xaml.cs +++ b/Ink Canvas/Windows/NewStyleMinimizedTimerWindow.xaml.cs @@ -139,8 +139,7 @@ private void MainBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs } catch (Exception ex) { - LogHelper.WriteLogToFile( - $"[Timer] 最小化计时器窗口拖动 DragMove 失败: {ex.Message}", LogHelper.LogType.Info); + Helpers.LogService.LogException(ex); } } diff --git a/Ink Canvas/Windows/NewStyleTimerWindow.xaml.cs b/Ink Canvas/Windows/NewStyleTimerWindow.xaml.cs index 60eee5e15..e61f03dbf 100644 --- a/Ink Canvas/Windows/NewStyleTimerWindow.xaml.cs +++ b/Ink Canvas/Windows/NewStyleTimerWindow.xaml.cs @@ -1,4 +1,6 @@ using Ink_Canvas.Helpers; +using Ink_Canvas.Models; +using Ink_Canvas.Services.Classroom; using Microsoft.Win32; using Newtonsoft.Json; using System; @@ -33,8 +35,14 @@ public NewStyleTimerWindow() { InitializeComponent(); - timer.Elapsed += Timer_Elapsed; - timer.Interval = 50; + _timerService = new TimerService( + _ => 50, + () => MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true, + () => MainWindow.Settings.RandSettings?.EnableProgressiveReminder == true); + // 原字段初始值:hour=0, minute=5, second=0 + _timerService.Minute = 5; + _timerService.StateChanged += OnTimerStateChanged; + InitializeUI(); // 应用主题 @@ -117,10 +125,9 @@ private void NewStyleTimerWindow_Closed(object sender, EventArgs e) } catch (Exception ex) { - LogHelper.WriteLogToFile($"NewStyleTimerWindow | 取消系统事件订阅失败: {ex.Message}", LogHelper.LogType.Warning); + LogService.LogException(ex); } - timer?.Stop(); - timer?.Dispose(); + _timerService.Dispose(); hideTimer?.Stop(); hideTimer?.Dispose(); mediaPlayer?.Close(); @@ -163,129 +170,97 @@ public void RefreshTheme() #endregion - private void Timer_Elapsed(object sender, ElapsedEventArgs e) + // 计时状态机已提取至 TimerService;本窗口仅订阅状态并渲染。 + private void OnTimerStateChanged(TimerState state) + { + Application.Current.Dispatcher.Invoke(() => RenderTick(state)); + } + + private void RenderTick(TimerState state) { - if (!isTimerRunning || isPaused) + if (!state.IsOvertimeMode || state.JustEnteredOvertime) { - timer.Stop(); - return; - } + TimeSpan leftTimeSpan = state.Remaining; - TimeSpan timeSpan = DateTime.Now - startTime; - TimeSpan totalTimeSpan = new TimeSpan(hour, minute, second); - double spentTimePercent = timeSpan.TotalMilliseconds / (totalTimeSpan.TotalMilliseconds); + int totalHours = (int)leftTimeSpan.TotalHours; + int displayHours = totalHours; - Application.Current.Dispatcher.Invoke(() => - { - if (!isOvertimeMode) - { - TimeSpan leftTimeSpan = totalTimeSpan - timeSpan; - if (leftTimeSpan.Milliseconds > 0) leftTimeSpan += new TimeSpan(0, 0, 1); + if (displayHours > 99) displayHours = 99; - int totalHours = (int)leftTimeSpan.TotalHours; - int displayHours = totalHours; + SetDigitDisplay("Digit1Display", displayHours / 10); + SetDigitDisplay("Digit2Display", displayHours % 10); + SetDigitDisplay("Digit3Display", leftTimeSpan.Minutes / 10); + SetDigitDisplay("Digit4Display", leftTimeSpan.Minutes % 10); + SetDigitDisplay("Digit5Display", leftTimeSpan.Seconds / 10); + SetDigitDisplay("Digit6Display", leftTimeSpan.Seconds % 10); - if (displayHours > 99) displayHours = 99; + SetColonDisplay(false); - SetDigitDisplay("Digit1Display", displayHours / 10); - SetDigitDisplay("Digit2Display", displayHours % 10); - SetDigitDisplay("Digit3Display", leftTimeSpan.Minutes / 10); - SetDigitDisplay("Digit4Display", leftTimeSpan.Minutes % 10); - SetDigitDisplay("Digit5Display", leftTimeSpan.Seconds / 10); - SetDigitDisplay("Digit6Display", leftTimeSpan.Seconds % 10); + if (state.ProgressiveReminderDue) + { + PlayProgressiveReminderSound(); + } - SetColonDisplay(false); + if (state.JustEnteredOvertime) + { + PlayTimerSound(); + } + else if (state.JustCompleted) + { + ApplyResetStateAfterStop(); - if (leftTimeSpan.TotalSeconds <= 6 && leftTimeSpan.TotalSeconds > 0 && - MainWindow.Settings.RandSettings?.EnableProgressiveReminder == true && - !hasPlayedProgressiveReminder) - { - PlayProgressiveReminderSound(); - hasPlayedProgressiveReminder = true; - } + PlayTimerSound(); - if (leftTimeSpan.TotalSeconds <= 0 && MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true) - { - isOvertimeMode = true; - PlayTimerSound(); - } - else if (leftTimeSpan.TotalSeconds <= 0) - { - timer.Stop(); - isTimerRunning = false; - isPaused = false; - isOvertimeMode = false; + TimerCompleted?.Invoke(this, EventArgs.Empty); + HandleTimerCompletion(); + } + } + else + { + TimeSpan overtimeSpan = state.Overtime; + int totalHours = (int)overtimeSpan.TotalHours; + int displayHours = totalHours; - ApplyResetStateAfterStop(); + if (displayHours > 99) displayHours = 99; + if (displayHours < 0) displayHours = 0; - PlayTimerSound(); + bool shouldShowRed = MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true; - TimerCompleted?.Invoke(this, EventArgs.Empty); - HandleTimerCompletion(); - } - } - else - { - TimeSpan overtimeSpan = timeSpan - totalTimeSpan; - int totalHours = (int)overtimeSpan.TotalHours; - int displayHours = totalHours; - - if (displayHours > 99) displayHours = 99; - if (displayHours < 0) displayHours = 0; - - bool shouldShowRed = MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true; - - 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); - } - }); + 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); + } } SoundPlayer player = new SoundPlayer(); MediaPlayer mediaPlayer = new MediaPlayer(); - int hour = 0; - int minute = 5; - int second = 0; - int cachedStartHour = 0; - int cachedStartMinute = 5; - int cachedStartSecond = 0; - - DateTime startTime = DateTime.Now; - DateTime pauseTime = DateTime.Now; - - bool isTimerRunning = false; - bool isPaused = false; - bool isOvertimeMode = false; - TimeSpan remainingTime = TimeSpan.Zero; - bool hasPlayedProgressiveReminder = false; - - Timer timer = new Timer(); private Timer hideTimer; private DateTime lastActivityTime; + + // 计时状态机。设定时间经 Hour/Minute/Second 属性读写。 + private readonly TimerService _timerService; + public TimeSpan? GetTotalTimeSpan() { - return new TimeSpan(hour, minute, second); + return _timerService.GetTotalTimeSpan(); } public TimeSpan? GetElapsedTime() { - if (isPaused) return null; - - return DateTime.Now - startTime; + return _timerService.GetElapsedTime(); } // 最近计时记录(使用数组简化重复代码) @@ -341,27 +316,23 @@ private void ApplyTheme(Settings settings) } /// - /// 依据当前计时器状态刷新数字显示: - /// 运行中按 DateTime.Now、暂停中按 pauseTime 推算;处于超时模式时按超时值渲染;未启动时回退到初始设定值。 - /// 供主题切换等场景直接复用 Timer_Elapsed 的渲染分支。 + /// 依据当前计时器状态刷新数字显示:渲染数据取自 TimerService 的快照 + /// (运行中按当前时钟、暂停中按暂停时刻推算;超时模式按超时值渲染;未启动时回退到初始设定值)。 + /// 供主题切换等场景直接复用 tick 的渲染分支。 /// private void RefreshDigitDisplayForCurrentState() { - if (!isTimerRunning) + if (!_timerService.IsRunning) { UpdateDigitDisplays(); return; } - DateTime referenceTime = isPaused ? pauseTime : DateTime.Now; - TimeSpan timeSpan = referenceTime - startTime; - TimeSpan totalTimeSpan = new TimeSpan(hour, minute, second); + TimerState snapshot = _timerService.Snapshot(); - if (!isOvertimeMode) + if (!snapshot.IsOvertimeMode) { - TimeSpan leftTimeSpan = totalTimeSpan - timeSpan; - if (leftTimeSpan.Milliseconds > 0) leftTimeSpan += new TimeSpan(0, 0, 1); - if (leftTimeSpan < TimeSpan.Zero) leftTimeSpan = TimeSpan.Zero; + TimeSpan leftTimeSpan = snapshot.Remaining; int displayHours = Math.Min(99, (int)leftTimeSpan.TotalHours); SetDigitDisplay("Digit1Display", displayHours / 10); @@ -374,8 +345,7 @@ private void RefreshDigitDisplayForCurrentState() } else { - TimeSpan overtimeSpan = timeSpan - totalTimeSpan; - if (overtimeSpan < TimeSpan.Zero) overtimeSpan = TimeSpan.Zero; + TimeSpan overtimeSpan = snapshot.Overtime; int displayHours = Math.Max(0, Math.Min(99, (int)overtimeSpan.TotalHours)); bool shouldShowRed = MainWindow.Settings?.RandSettings?.EnableOvertimeRedText == true; @@ -399,107 +369,29 @@ private void RefreshDigitDisplayForCurrentState() private void UpdateDigitDisplays() { - SetDigitDisplay("Digit1Display", hour / 10); - SetDigitDisplay("Digit2Display", hour % 10); - SetDigitDisplay("Digit3Display", minute / 10); - SetDigitDisplay("Digit4Display", minute % 10); - SetDigitDisplay("Digit5Display", second / 10); - SetDigitDisplay("Digit6Display", second % 10); + SetDigitDisplay("Digit1Display", _timerService.Hour / 10); + SetDigitDisplay("Digit2Display", _timerService.Hour % 10); + SetDigitDisplay("Digit3Display", _timerService.Minute / 10); + SetDigitDisplay("Digit4Display", _timerService.Minute % 10); + SetDigitDisplay("Digit5Display", _timerService.Second / 10); + SetDigitDisplay("Digit6Display", _timerService.Second % 10); SetColonDisplay(false); } - // 更新剩余时间 - private void UpdateRemainingTime() - { - if (isTimerRunning && !isPaused) - { - // 获取当前剩余时间 - TimeSpan? currentRemaining = GetRemainingTime(); - if (currentRemaining.HasValue) - { - // 计算已经过去的时间 - TimeSpan elapsedTime = DateTime.Now - startTime; - - // 计算新的总时间 - TimeSpan newTotalTime = new TimeSpan(hour, minute, second); - - // 如果新设置的时间小于已经过去的时间,则设置为0 - if (newTotalTime <= elapsedTime) - { - remainingTime = TimeSpan.Zero; - } - else - { - // 否则,剩余时间 = 新总时间 - 已经过去的时间 - remainingTime = newTotalTime - elapsedTime; - } - } - else - { - // 如果没有剩余时间信息,直接设置新的剩余时间 - remainingTime = new TimeSpan(hour, minute, second); - } - } - } - - // 更新特定时间单位的剩余时间 - private void UpdateSpecificTimeUnit(int newHour, int newMinute, int newSecond) - { - if (isTimerRunning && !isPaused) - { - // 获取当前剩余时间 - TimeSpan? currentRemaining = GetRemainingTime(); - if (currentRemaining.HasValue) - { - // 计算已经过去的时间 - TimeSpan elapsedTime = DateTime.Now - startTime; - - // 计算新的总时间 - TimeSpan newTotalTime = new TimeSpan(newHour, newMinute, newSecond); - - // 如果新设置的时间小于已经过去的时间,则设置为0 - if (newTotalTime <= elapsedTime) - { - remainingTime = TimeSpan.Zero; - } - else - { - // 否则,剩余时间 = 新总时间 - 已经过去的时间 - remainingTime = newTotalTime - elapsedTime; - } - } - else - { - // 如果没有剩余时间信息,直接设置新的剩余时间 - remainingTime = new TimeSpan(newHour, newMinute, newSecond); - } - } - } - - public bool IsTimerRunning => isTimerRunning; + public bool IsTimerRunning => _timerService.IsRunning; public TimeSpan? GetRemainingTime() { - if (isPaused) return null; - - var elapsed = DateTime.Now - startTime; - var totalTimeSpan = new TimeSpan(hour, minute, second); - var leftTimeSpan = totalTimeSpan - elapsed; - - if (leftTimeSpan.Milliseconds > 0) leftTimeSpan += new TimeSpan(0, 0, 1); - - return leftTimeSpan; + return _timerService.GetRemainingTime(); } public void StopTimer() { - timer.Stop(); - isTimerRunning = false; + _timerService.Stop(); StartPauseIcon.Data = Geometry.Parse(PlayIconData); } - /// /// 根据数字值设置SVG数字显示 /// @@ -592,40 +484,40 @@ private void SetColonDisplay(bool isRed = false) // 第1位数字(小时十位) private void Digit1Plus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentHour = hour; + int currentHour = _timerService.Hour; int hourTens = currentHour / 10; int hourOnes = currentHour % 10; hourTens++; if (hourTens >= 10) hourTens = 0; - hour = hourTens * 10 + hourOnes; + _timerService.Hour = hourTens * 10 + hourOnes; UpdateDigitDisplays(); } private void Digit1Minus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentHour = hour; + int currentHour = _timerService.Hour; int hourTens = currentHour / 10; int hourOnes = currentHour % 10; hourTens--; if (hourTens < 0) hourTens = 9; - hour = hourTens * 10 + hourOnes; + _timerService.Hour = hourTens * 10 + hourOnes; UpdateDigitDisplays(); } // 第2位数字(小时个位) private void Digit2Plus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentHour = hour; + int currentHour = _timerService.Hour; int hourTens = currentHour / 10; int hourOnes = currentHour % 10; @@ -637,15 +529,15 @@ private void Digit2Plus_Click(object sender, RoutedEventArgs e) if (hourTens >= 10) hourTens = 0; } - hour = hourTens * 10 + hourOnes; + _timerService.Hour = hourTens * 10 + hourOnes; UpdateDigitDisplays(); } private void Digit2Minus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentHour = hour; + int currentHour = _timerService.Hour; int hourTens = currentHour / 10; int hourOnes = currentHour % 10; @@ -657,47 +549,47 @@ private void Digit2Minus_Click(object sender, RoutedEventArgs e) if (hourTens < 0) hourTens = 9; } - hour = hourTens * 10 + hourOnes; + _timerService.Hour = hourTens * 10 + hourOnes; UpdateDigitDisplays(); } // 第3位数字(分钟十位) private void Digit3Plus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentMinute = minute; + int currentMinute = _timerService.Minute; int minuteTens = currentMinute / 10; int minuteOnes = currentMinute % 10; minuteTens++; if (minuteTens >= 6) minuteTens = 0; - minute = minuteTens * 10 + minuteOnes; + _timerService.Minute = minuteTens * 10 + minuteOnes; UpdateDigitDisplays(); } private void Digit3Minus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentMinute = minute; + int currentMinute = _timerService.Minute; int minuteTens = currentMinute / 10; int minuteOnes = currentMinute % 10; minuteTens--; if (minuteTens < 0) minuteTens = 5; - minute = minuteTens * 10 + minuteOnes; + _timerService.Minute = minuteTens * 10 + minuteOnes; UpdateDigitDisplays(); } // 第4位数字(分钟个位) private void Digit4Plus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentMinute = minute; + int currentMinute = _timerService.Minute; int minuteTens = currentMinute / 10; int minuteOnes = currentMinute % 10; @@ -709,15 +601,15 @@ private void Digit4Plus_Click(object sender, RoutedEventArgs e) if (minuteTens >= 6) minuteTens = 0; } - minute = minuteTens * 10 + minuteOnes; + _timerService.Minute = minuteTens * 10 + minuteOnes; UpdateDigitDisplays(); } private void Digit4Minus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentMinute = minute; + int currentMinute = _timerService.Minute; int minuteTens = currentMinute / 10; int minuteOnes = currentMinute % 10; @@ -729,47 +621,47 @@ private void Digit4Minus_Click(object sender, RoutedEventArgs e) if (minuteTens < 0) minuteTens = 5; } - minute = minuteTens * 10 + minuteOnes; + _timerService.Minute = minuteTens * 10 + minuteOnes; UpdateDigitDisplays(); } // 第5位数字(秒十位) private void Digit5Plus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentSecond = second; + int currentSecond = _timerService.Second; int secondTens = currentSecond / 10; int secondOnes = currentSecond % 10; secondTens++; if (secondTens >= 6) secondTens = 0; - second = secondTens * 10 + secondOnes; + _timerService.Second = secondTens * 10 + secondOnes; UpdateDigitDisplays(); } private void Digit5Minus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentSecond = second; + int currentSecond = _timerService.Second; int secondTens = currentSecond / 10; int secondOnes = currentSecond % 10; secondTens--; if (secondTens < 0) secondTens = 5; - second = secondTens * 10 + secondOnes; + _timerService.Second = secondTens * 10 + secondOnes; UpdateDigitDisplays(); } // 第6位数字(秒个位) private void Digit6Plus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentSecond = second; + int currentSecond = _timerService.Second; int secondTens = currentSecond / 10; int secondOnes = currentSecond % 10; @@ -781,15 +673,15 @@ private void Digit6Plus_Click(object sender, RoutedEventArgs e) if (secondTens >= 6) secondTens = 0; } - second = secondTens * 10 + secondOnes; + _timerService.Second = secondTens * 10 + secondOnes; UpdateDigitDisplays(); } private void Digit6Minus_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning) return; + if (_timerService.IsRunning) return; UpdateActivityTime(); - int currentSecond = second; + int currentSecond = _timerService.Second; int secondTens = currentSecond / 10; int secondOnes = currentSecond % 10; @@ -801,7 +693,7 @@ private void Digit6Minus_Click(object sender, RoutedEventArgs e) if (secondTens < 0) secondTens = 5; } - second = secondTens * 10 + secondOnes; + _timerService.Second = secondTens * 10 + secondOnes; UpdateDigitDisplays(); } @@ -812,42 +704,29 @@ private void Digit6Minus_Click(object sender, RoutedEventArgs e) private void StartPause_Click(object sender, RoutedEventArgs e) { UpdateActivityTime(); - if (isPaused && isTimerRunning) + if (_timerService.IsPaused && _timerService.IsRunning) { // 继续计时 - startTime += DateTime.Now - pauseTime; + _timerService.Resume(); StartPauseIcon.Data = Geometry.Parse(PauseIconData); - isPaused = false; - timer.Start(); } - else if (isTimerRunning) + else if (_timerService.IsRunning) { // 暂停计时 - pauseTime = DateTime.Now; + _timerService.Pause(); StartPauseIcon.Data = Geometry.Parse(PlayIconData); - isPaused = true; - timer.Stop(); } else { // 开始计时 - if (hour == 0 && minute == 0 && second == 0) + if (_timerService.Hour == 0 && _timerService.Minute == 0 && _timerService.Second == 0) { - second = 1; + _timerService.Second = 1; UpdateDigitDisplays(); } - cachedStartHour = hour; - cachedStartMinute = minute; - cachedStartSecond = second; - - startTime = DateTime.Now; + _timerService.Start(); StartPauseIcon.Data = Geometry.Parse(PauseIconData); - isPaused = false; - isTimerRunning = true; - isOvertimeMode = false; - hasPlayedProgressiveReminder = false; - timer.Start(); // 启动隐藏定时器 hideTimer.Start(); @@ -873,7 +752,7 @@ private void ApplyResetStateAfterStop() StartPauseIcon.Data = Geometry.Parse(PlayIconData); } - hasPlayedProgressiveReminder = false; + _timerService.ResetProgressiveReminderFlag(); // 禁用全屏按钮 if (FullscreenBtn != null) @@ -886,12 +765,10 @@ private void Reset_Click(object sender, RoutedEventArgs e) { UpdateActivityTime(); - if (isTimerRunning) + if (_timerService.IsRunning) { - // 停止计时器 - timer.Stop(); - isTimerRunning = false; - isPaused = false; + // 停止计时器并复位运行/暂停/超时标志 + _timerService.Reset(); if (hideTimer != null) { @@ -899,8 +776,10 @@ private void Reset_Click(object sender, RoutedEventArgs e) } _minimizedWindow?.Close(); } - - isOvertimeMode = false; + else + { + _timerService.Reset(); + } ApplyResetStateAfterStop(); } @@ -987,42 +866,42 @@ private void TimerQuickTabControl_SelectionChanged(object sender, SelectionChang // 常用计时事件处理 private void Common5Min_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning && !isPaused) return; + if (_timerService.IsRunning && !_timerService.IsPaused) return; UpdateActivityTime(); SetQuickTime(0, 5, 0); } private void Common10Min_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning && !isPaused) return; + if (_timerService.IsRunning && !_timerService.IsPaused) return; UpdateActivityTime(); SetQuickTime(0, 10, 0); } private void Common15Min_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning && !isPaused) return; + if (_timerService.IsRunning && !_timerService.IsPaused) return; UpdateActivityTime(); SetQuickTime(0, 15, 0); } private void Common30Min_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning && !isPaused) return; + if (_timerService.IsRunning && !_timerService.IsPaused) return; UpdateActivityTime(); SetQuickTime(0, 30, 0); } private void Common45Min_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning && !isPaused) return; + if (_timerService.IsRunning && !_timerService.IsPaused) return; UpdateActivityTime(); SetQuickTime(0, 45, 0); } private void Common60Min_Click(object sender, RoutedEventArgs e) { - if (isTimerRunning && !isPaused) return; + if (_timerService.IsRunning && !_timerService.IsPaused) return; UpdateActivityTime(); SetQuickTime(1, 0, 0); } @@ -1030,7 +909,7 @@ private void Common60Min_Click(object sender, RoutedEventArgs e) // 最近计时事件处理(统一处理方法) private void HandleRecentTimerClick(int index) { - if ((isTimerRunning && !isPaused) || _recentTimers[index] == "--:--") return; + if ((_timerService.IsRunning && !_timerService.IsPaused) || _recentTimers[index] == "--:--") return; UpdateActivityTime(); ApplyRecentTimer(_recentTimers[index]); } @@ -1045,9 +924,9 @@ private void HandleRecentTimerClick(int index) // 设置快捷时间 private void SetQuickTime(int h, int m, int s) { - hour = h; - minute = m; - second = s; + _timerService.Hour = h; + _timerService.Minute = m; + _timerService.Second = s; UpdateDigitDisplays(); } @@ -1077,9 +956,9 @@ private void ApplyRecentTimer(string timeString) // 保存最近计时记录 private void SaveRecentTimer() { - if (hour == 0 && minute == 0 && second == 0) return; + if (_timerService.Hour == 0 && _timerService.Minute == 0 && _timerService.Second == 0) return; - string currentTime = $"{minute:D2}:{second:D2}"; + string currentTime = $"{_timerService.Minute:D2}:{_timerService.Second:D2}"; // 检查是否已存在相同的时间 int existingIndex = Array.IndexOf(_recentTimers, currentTime); @@ -1125,8 +1004,7 @@ private void UpdateRecentTimerDisplays() } catch (Exception ex) { - LogHelper.WriteLogToFile( - $"[Timer] 刷新最近计时显示失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -1202,9 +1080,7 @@ private void SaveRecentTimersToRegistry() } catch (Exception ex) { - LogHelper.WriteLogToFile( - $"[Timer] 保存最近计时记录到 {RecentTimersJsonPath} 失败: {ex.Message}", - LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -1219,8 +1095,7 @@ private void RefreshModernBorder() } catch (Exception ex) { - LogHelper.WriteLogToFile( - $"[Timer] 刷新计时器窗口边框资源失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -1237,7 +1112,7 @@ private void Fullscreen_Click(object sender, RoutedEventArgs e) return; } - if (isTimerRunning && !isPaused) + if (_timerService.IsRunning && !_timerService.IsPaused) { fullscreenWindow = new FullscreenTimerWindow(this); fullscreenWindow.Closed += (s, args) => { fullscreenWindow = null; }; @@ -1268,8 +1143,7 @@ private void DragTimerWindow(MouseButtonEventArgs e) } catch (Exception ex) { - LogHelper.WriteLogToFile( - $"[Timer] 计时器窗口拖动 DragMove 失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } @@ -1298,24 +1172,18 @@ public void ResetTimerState() private void ApplyResetTimerState() { - // 停止计时器 - if (isTimerRunning) - { - timer.Stop(); - isTimerRunning = false; - isPaused = false; + // 停止计时器并复位到起始设定时间快照(状态部分在 TimerService) + bool wasRunning = _timerService.IsRunning; + _timerService.ResetToStartSnapshot(); + if (wasRunning) + { if (hideTimer != null) { hideTimer.Stop(); } } - // 重置时间到默认值 - hour = cachedStartHour; - minute = cachedStartMinute; - second = cachedStartSecond; - // 更新显示 UpdateDigitDisplays(); SetColonDisplay(false); @@ -1326,10 +1194,6 @@ private void ApplyResetTimerState() StartPauseIcon.Data = Geometry.Parse(PlayIconData); } - // 重置状态标志 - isOvertimeMode = false; - hasPlayedProgressiveReminder = false; - // 禁用全屏按钮 if (FullscreenBtn != null) { @@ -1341,7 +1205,7 @@ private void ApplyResetTimerState() private void HideTimer_Elapsed(object sender, ElapsedEventArgs e) { - if (!isTimerRunning || isPaused) return; + if (!_timerService.IsRunning || _timerService.IsPaused) return; var timeSinceLastActivity = DateTime.Now - lastActivityTime; if (timeSinceLastActivity.TotalSeconds >= 5) @@ -1360,7 +1224,7 @@ private void HideTimer_Elapsed(object sender, ElapsedEventArgs e) { _minimizedWindow = new NewStyleMinimizedTimerWindow( () => GetRemainingTime(), - () => !isTimerRunning || isPaused, + () => !_timerService.IsRunning || _timerService.IsPaused, () => { Show(); diff --git a/Ink Canvas/Windows/OperatingGuideWindow.xaml.cs b/Ink Canvas/Windows/OperatingGuideWindow.xaml.cs index fbfef9cf2..9e8f2f3a4 100644 --- a/Ink Canvas/Windows/OperatingGuideWindow.xaml.cs +++ b/Ink Canvas/Windows/OperatingGuideWindow.xaml.cs @@ -32,8 +32,7 @@ public void RefreshTheme() } catch (Exception ex) { - LogHelper.WriteLogToFile( - $"[UI] 操作指引窗口 RefreshTheme 应用主题失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } } diff --git a/Ink Canvas/Windows/PPTQuickPanel.xaml.cs b/Ink Canvas/Windows/PPTQuickPanel.xaml.cs index bb9a417e1..bbe7f7f0f 100644 --- a/Ink Canvas/Windows/PPTQuickPanel.xaml.cs +++ b/Ink Canvas/Windows/PPTQuickPanel.xaml.cs @@ -564,7 +564,7 @@ private void InitializeAudio() } catch (Exception ex) { - LogHelper.WriteLogToFile($"[PPT] 初始化默认音频端点音量接口失败(音量控制将不可用): {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } } diff --git a/Ink Canvas/Windows/PPTTimeCapsule.xaml.cs b/Ink Canvas/Windows/PPTTimeCapsule.xaml.cs index 2e251812e..c4983c51d 100644 --- a/Ink Canvas/Windows/PPTTimeCapsule.xaml.cs +++ b/Ink Canvas/Windows/PPTTimeCapsule.xaml.cs @@ -122,10 +122,7 @@ public void Dispose() { SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[PPT] 释放时取消 SystemEvents.UserPreferenceChanged 订阅失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } // 停止并释放定时器,确保不再触发回调 try @@ -133,38 +130,27 @@ public void Dispose() if (timeUpdateTimer != null) { timeUpdateTimer.Elapsed -= TimeUpdateTimer_Elapsed; - try { timeUpdateTimer.Stop(); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT] 释放时停止 timeUpdateTimer 失败: {ex.Message}", LogHelper.LogType.Info); } - try { timeUpdateTimer.Dispose(); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT] 释放时处置 timeUpdateTimer 失败: {ex.Message}", LogHelper.LogType.Info); } + try { timeUpdateTimer.Stop(); } catch (Exception ex) { LogService.LogException(ex); } + try { timeUpdateTimer.Dispose(); } catch (Exception ex2) { LogService.LogException(ex2); } timeUpdateTimer = null; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[PPT] 释放 timeUpdateTimer 整体失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex3) { LogService.LogException(ex3); } try { if (countdownUpdateTimer != null) { countdownUpdateTimer.Elapsed -= CountdownUpdateTimer_Elapsed; - try { countdownUpdateTimer.Stop(); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT] 释放时停止 countdownUpdateTimer 失败: {ex.Message}", LogHelper.LogType.Info); } - try { countdownUpdateTimer.Dispose(); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT] 释放时处置 countdownUpdateTimer 失败: {ex.Message}", LogHelper.LogType.Info); } + try { countdownUpdateTimer.Stop(); } catch (Exception ex) { LogService.LogException(ex); } + try { countdownUpdateTimer.Dispose(); } catch (Exception ex2) { LogService.LogException(ex2); } countdownUpdateTimer = null; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[PPT] 释放 countdownUpdateTimer 整体失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex3) { LogService.LogException(ex3); } // 停止动画 - try { StopColonBlinkAnimation(); } - catch (Exception ex) { LogHelper.WriteLogToFile($"[PPT] 释放时停止冒号闪烁动画失败: {ex.Message}", LogHelper.LogType.Info); } + try { StopColonBlinkAnimation(); } catch (Exception ex) { LogService.LogException(ex); } } private void InitializeTimers() @@ -893,10 +879,7 @@ private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceCha ApplyTheme(); }), DispatcherPriority.Normal); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[PPT] 系统主题变更后向 UI 线程投递 ApplyTheme 失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } private void ApplyTheme() diff --git a/Ink Canvas/Windows/PrivacyAgreementWindow.xaml.cs b/Ink Canvas/Windows/PrivacyAgreementWindow.xaml.cs index d7ad426bb..b6f064c86 100644 --- a/Ink Canvas/Windows/PrivacyAgreementWindow.xaml.cs +++ b/Ink Canvas/Windows/PrivacyAgreementWindow.xaml.cs @@ -129,11 +129,7 @@ private static bool IsSystemThemeLight() if (value is int i) return i == 1; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[UI] 隐私说明窗口读取系统主题注册表项失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } return true; } } diff --git a/Ink Canvas/Windows/SettingsViews/Helpers/AutoStartHelper.cs b/Ink Canvas/Windows/SettingsViews/Helpers/AutoStartHelper.cs index c7ae054be..75128b270 100644 --- a/Ink Canvas/Windows/SettingsViews/Helpers/AutoStartHelper.cs +++ b/Ink Canvas/Windows/SettingsViews/Helpers/AutoStartHelper.cs @@ -21,11 +21,7 @@ public static bool StartAutomaticallyCreate(string exeName) shortcut.Save(); return true; } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[Settings] 创建开机自启快捷方式 {exeName}.lnk 失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { Ink_Canvas.Helpers.LogService.LogException(ex); } return false; } @@ -38,11 +34,7 @@ public static bool StartAutomaticallyDel(string exeName) ".lnk"); return true; } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[Settings] 删除开机自启快捷方式 {exeName}.lnk 失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { Ink_Canvas.Helpers.LogService.LogException(ex); } return false; } diff --git a/Ink Canvas/Windows/SettingsViews/Helpers/SettingsManager.cs b/Ink Canvas/Windows/SettingsViews/Helpers/SettingsManager.cs index 93b2c0ac7..5855ef8ea 100644 --- a/Ink Canvas/Windows/SettingsViews/Helpers/SettingsManager.cs +++ b/Ink Canvas/Windows/SettingsViews/Helpers/SettingsManager.cs @@ -67,13 +67,7 @@ public static void SaveSettingsToFile() } catch { - try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[Settings] 保存设置失败后清理临时文件 {tmpPath} 失败: {ex.Message}", - LogHelper.LogType.Info); - } + try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch (Exception ex) { LogService.LogException(ex); } // 回退到直接覆盖,保持旧行为仍可用 ProcessProtectionManager.WithWriteAccess(path, () => File.WriteAllText(path, text)); } diff --git a/Ink Canvas/Windows/SettingsViews/Helpers/WindowSettingsHelper.cs b/Ink Canvas/Windows/SettingsViews/Helpers/WindowSettingsHelper.cs index 1d02d8b61..e276f95e2 100644 --- a/Ink Canvas/Windows/SettingsViews/Helpers/WindowSettingsHelper.cs +++ b/Ink Canvas/Windows/SettingsViews/Helpers/WindowSettingsHelper.cs @@ -89,10 +89,7 @@ private static void StopPPTOnlyVisibilityProbeTimer() { _pptOnlyVisibilityProbeTimer?.Stop(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"停止仅PPT可见性探测计时器失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } private static void PPTOnlyVisibilityProbeTimer_Tick(object sender, EventArgs e) diff --git a/Ink Canvas/Windows/SettingsViews/Pages/AboutPage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/AboutPage.xaml.cs index e59225649..3404b68a4 100644 --- a/Ink Canvas/Windows/SettingsViews/Pages/AboutPage.xaml.cs +++ b/Ink Canvas/Windows/SettingsViews/Pages/AboutPage.xaml.cs @@ -101,9 +101,7 @@ private void UsageRefreshTimer_Tick(object sender, EventArgs e) var n = System.Threading.Interlocked.Increment(ref _usageRefreshDiagCount); if (n == 1 || n % 100 == 0) { - LogHelper.WriteLogToFile( - $"[Settings] 关于页面刷新累计使用时长失败(累计 {n} 次): {ex.Message}", - LogHelper.LogType.Info); + LogService.LogException(ex); } } } diff --git a/Ink Canvas/Windows/SettingsViews/Pages/CloudStoragePage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/CloudStoragePage.xaml.cs index 9330c9470..8d4468295 100644 --- a/Ink Canvas/Windows/SettingsViews/Pages/CloudStoragePage.xaml.cs +++ b/Ink Canvas/Windows/SettingsViews/Pages/CloudStoragePage.xaml.cs @@ -639,8 +639,9 @@ private async Task TestConnectionAsync() SetConnectionStatus(string.Format(CloudStorageStrings.CloudStorage_ConnectedFormat, whiteboards.Count), Color.FromRgb(34, 197, 94)); LoadClasses(whiteboards, result.User); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { + LogService.LogException(ex); } catch (Exception ex) { @@ -669,8 +670,7 @@ private void CancelConnectionTest() } catch (Exception ex) { - LogHelper.WriteLogToFile( - $"[Settings] 取消云存储连接测试失败: {ex.Message}", LogHelper.LogType.Info); + LogService.LogException(ex); } finally { diff --git a/Ink Canvas/Windows/SettingsViews/Pages/FloatingBarThemeMarketPage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/FloatingBarThemeMarketPage.xaml.cs index fdd2fe6bf..42deda203 100644 --- a/Ink Canvas/Windows/SettingsViews/Pages/FloatingBarThemeMarketPage.xaml.cs +++ b/Ink Canvas/Windows/SettingsViews/Pages/FloatingBarThemeMarketPage.xaml.cs @@ -49,13 +49,7 @@ private async void InstallButton_Click(object sender, RoutedEventArgs e) if (installed) { // mark this entry as installed immediately so UI reflects state even if subsequent refresh fails - try { entry.IsInstalled = true; } - catch (System.Exception ex) - { - LogHelper.WriteLogToFile( - $"[UI] 主题市场安装成功后回写条目 {entry?.Manifest?.Id} 已安装标记失败: {ex.Message}", - LogHelper.LogType.Info); - } + try { entry.IsInstalled = true; } catch (System.Exception ex) { LogService.LogException(ex); } var mainWindow = Application.Current.MainWindow as MainWindow; mainWindow?.FloatingBarThemeService?.LoadThemes(); @@ -68,20 +62,8 @@ private async void InstallButton_Click(object sender, RoutedEventArgs e) { Debug.WriteLine($"FloatingBarThemeMarketPage | Refresh after install failed: {ex}"); // refresh failed: revert temporary installed flag and re-enable button so user can retry - try { entry.IsInstalled = false; } - catch (System.Exception rollbackEx) - { - LogHelper.WriteLogToFile( - $"[UI] 主题市场刷新失败后回滚条目 {entry?.Manifest?.Id} 已安装标记失败: {rollbackEx.Message}", - LogHelper.LogType.Info); - } - try { button.IsEnabled = true; } - catch (System.Exception buttonEx) - { - LogHelper.WriteLogToFile( - $"[UI] 主题市场刷新失败后恢复安装按钮可用状态失败: {buttonEx.Message}", - LogHelper.LogType.Info); - } + try { entry.IsInstalled = false; } catch (System.Exception ex2) { LogService.LogException(ex2); } + try { button.IsEnabled = true; } catch (System.Exception ex3) { LogService.LogException(ex3); } } // 如果设置窗口中的主题管理页存在,则让它也刷新(使安装的主题立刻在管理页可见) diff --git a/Ink Canvas/Windows/SettingsViews/Pages/PPTPageFlipPreviewPage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/PPTPageFlipPreviewPage.xaml.cs index 955a2b906..e742b8562 100644 --- a/Ink Canvas/Windows/SettingsViews/Pages/PPTPageFlipPreviewPage.xaml.cs +++ b/Ink Canvas/Windows/SettingsViews/Pages/PPTPageFlipPreviewPage.xaml.cs @@ -175,11 +175,7 @@ private void ClosePreviewWindow() { PPTPageFlipPreviewOverlayWindow.ActiveInstance.Close(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[UI] 关闭 PPT 翻页预览浮层窗口失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } if (PPTPageFlipPreviewWindow.ActiveInstance != null) { @@ -188,11 +184,7 @@ private void ClosePreviewWindow() WindowTopmostManager.UnregisterWindow(PPTPageFlipPreviewWindow.ActiveInstance); PPTPageFlipPreviewWindow.ActiveInstance.Close(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[UI] 注销置顶并关闭 PPT 翻页预览窗口失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } } diff --git a/Ink Canvas/Windows/SettingsViews/Pages/PluginPage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/PluginPage.xaml.cs index cc7c1c16a..26e9d8d68 100644 --- a/Ink Canvas/Windows/SettingsViews/Pages/PluginPage.xaml.cs +++ b/Ink Canvas/Windows/SettingsViews/Pages/PluginPage.xaml.cs @@ -354,12 +354,7 @@ private void OpenFolder_Click(object sender, RoutedEventArgs e) if (Directory.Exists(info.PluginFolderPath)) Process.Start(new ProcessStartInfo { FileName = info.PluginFolderPath, UseShellExecute = true }); } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[Settings] 打开插件目录 {info.PluginFolderPath} 失败: {ex.Message}", - LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } private void TogglePluginLoad_Click(object sender, RoutedEventArgs e) diff --git a/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml.cs index 9c65b5eb3..8ef0c9e43 100644 --- a/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml.cs +++ b/Ink Canvas/Windows/SettingsViews/Pages/StoragePage.xaml.cs @@ -180,11 +180,7 @@ private void UpdateUI() } } } - catch (Exception ex) - { - LogHelper.WriteLogToFile( - $"[Settings] 统计存储占用时读取磁盘容量失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } DiskPercentTextBlock.Text = "—"; } @@ -356,8 +352,7 @@ private static void TryDeleteDirectory(string path) } // 上述遍历/删除辅助方法对磁盘上每个文件调用一次,坏设备(掉盘、权限拒绝、文件被占) - // 下会连续抛成千上万次,直接写日志会瞬间刷爆 5MB 日志并触发 LogHelper 清空整个 - // Logs 目录把现场证据一起清掉,因此统一节流:只记第 1 次和每 100 次。 + // 下会连续抛成千上万次,直接写日志会瞬间刷爆日志文件,因此统一节流:只记第 1 次和每 100 次。 private static int _storageDiagCount; private static void LogStorageDiagException(string what, Exception ex) @@ -365,9 +360,7 @@ private static void LogStorageDiagException(string what, Exception ex) var n = System.Threading.Interlocked.Increment(ref _storageDiagCount); if (n == 1 || n % 100 == 0) { - LogHelper.WriteLogToFile( - $"[Settings] 存储清理/统计时 {what}(累计 {n} 次): {ex.Message}", - LogHelper.LogType.Info); + LogService.LogException(ex); } } diff --git a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs index 68a237947..35290789b 100644 --- a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs +++ b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs @@ -266,10 +266,7 @@ private void ApplyCurrentTheme() }; iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, elementTheme); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 应用设置窗口主题失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } private static bool IsSystemThemeLight() @@ -282,10 +279,7 @@ private static bool IsSystemThemeLight() if (themeKey?.GetValue("AppsUseLightTheme") is int v) return v == 1; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 读取系统深浅色注册表项失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } return false; } @@ -537,10 +531,7 @@ private void OnRootFrameNavigated(object sender, NavigationEventArgs e) (rootFrame.Content as FloatingBarThemePage)?.RefreshThemes(); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 导航后刷新浮动栏主题列表失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -552,10 +543,7 @@ public void RefreshFloatingBarThemePage() { (rootFrame.Content as FloatingBarThemePage)?.RefreshThemes(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 外部请求刷新浮动栏主题页失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -761,10 +749,7 @@ private void EnsureSearchIndexBuilt() { if (!feRoot.IsLoaded) { - try { feRoot.ApplyTemplate(); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 预展开页面模板失败,搜索索引可能不完整 (tag={tag}): {ex.Message}", LogHelper.LogType.Info); - } + try { feRoot.ApplyTemplate(); } catch (Exception ex) { LogService.LogException(ex); } } CollectEntriesFromPage(feRoot, tag); } @@ -867,10 +852,7 @@ private void NavigateToSearchEntry(SearchEntry entry) { Dispatcher.BeginInvoke(new Action(() => { - try { fe.BringIntoView(); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 滚动到搜索命中的设置项失败: {ex.Message}", LogHelper.LogType.Info); - } + try { fe.BringIntoView(); } catch (Exception ex) { LogService.LogException(ex); } }), System.Windows.Threading.DispatcherPriority.Background); } } @@ -1098,10 +1080,7 @@ public void HighlightSetting(string settingKey) private void FlashHighlight(FrameworkElement target) { - try { target.BringIntoView(); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 高亮前滚动到目标设置项失败: {ex.Message}", LogHelper.LogType.Info); - } + try { target.BringIntoView(); } catch (Exception ex) { LogService.LogException(ex); } Dispatcher.BeginInvoke(new Action(() => { @@ -1140,17 +1119,11 @@ private void FlashHighlight(FrameworkElement target) layer.Remove(adorner); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 高亮闪烁切换可见性失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } }; timer.Start(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 挂载设置项高亮装饰器失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex2) { LogService.LogException(ex2); } }), DispatcherPriority.Background); } @@ -1188,10 +1161,7 @@ private void FlashWithEffect(FrameworkElement target) }; timer.Start(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 设置项高亮(Effect 退路方案)失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -1247,10 +1217,7 @@ private void HookSettingsCardInputHandlers(FrameworkElement root) root.PreviewTouchUp -= SettingsCard_TouchUp; root.PreviewTouchUp += SettingsCard_TouchUp; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 挂载设置项右键/长按事件失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } private void SettingsCard_RightButtonUp(object sender, MouseButtonEventArgs e) @@ -1295,10 +1262,7 @@ private void SettingsCard_TouchDown(object sender, TouchEventArgs e) // 长按触发后取消后续的触摸提升(避免立即触发点击) e.TouchDevice.Capture(null); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 长按后释放触摸捕获失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } CopySettingUriFromElement(target); }; @@ -1325,10 +1289,7 @@ private void SettingsCard_TouchMove(object sender, TouchEventArgs e) _longPressTimer.Stop(); } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 处理设置项触摸移动失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } private void SettingsCard_TouchUp(object sender, TouchEventArgs e) @@ -1338,10 +1299,7 @@ private void SettingsCard_TouchUp(object sender, TouchEventArgs e) _longPressTimer?.Stop(); _longPressTimer = null; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 停止设置项长按计时器失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } /// @@ -1380,10 +1338,7 @@ private void CopySettingUriFromElement(FrameworkElement target) string pageTag = GetCurrentPageTag(); string uri = BuildSettingsUri(pageTag, key); - try { Clipboard.SetText(uri); } catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 复制设置项 URL 到剪贴板失败: {ex.Message}", LogHelper.LogType.Info); - } + try { Clipboard.SetText(uri); } catch (Exception ex2) { LogService.LogException(ex2); } ShowCopyUriInfoBar(); } @@ -1404,10 +1359,7 @@ private static string GetSettingsHeaderText(FrameworkElement target) if (target is iNKORE.UI.WPF.Modern.Controls.SettingsExpander se) return (se.Header as string)?.Trim() ?? se.Header?.ToString()?.Trim(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 读取设置项标题文本失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } return null; } @@ -1433,17 +1385,11 @@ private void ShowCopyUriInfoBar() CopyUriInfoBar.IsOpen = false; CopyUriInfoBar.Visibility = Visibility.Collapsed; } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 收起复制成功提示条失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } }; _copyUriInfoBarTimer.Start(); } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 显示复制成功提示条失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex2) { LogService.LogException(ex2); } } #endregion @@ -1508,10 +1454,7 @@ public void UpdateUpdateBadgeVisibility() badge.Visibility = (hasUpdate && !_updateBadgeDismissed) ? Visibility.Visible : Visibility.Collapsed; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 更新「更新」导航项徽标失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } } public void UpdateAnnouncementUnreadBadge() @@ -1527,10 +1470,7 @@ public void UpdateAnnouncementUnreadBadge() AnnouncementUnreadInfoBadge.Visibility = count > 0 ? Visibility.Visible : Visibility.Collapsed; } } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"[Settings] 更新公告未读徽标失败: {ex.Message}", LogHelper.LogType.Info); - } + catch (Exception ex) { LogService.LogException(ex); } }); } } diff --git a/InkCanvas.Controls/InkCanvas.Controls.csproj b/InkCanvas.Controls/InkCanvas.Controls.csproj index de4777a06..922ac1c79 100644 --- a/InkCanvas.Controls/InkCanvas.Controls.csproj +++ b/InkCanvas.Controls/InkCanvas.Controls.csproj @@ -8,7 +8,7 @@ InkCanvas.Controls true true - 10 + 12 CA1416;CS1591 false diff --git a/InkCanvas.IACoreHelper/InkCanvas.IACoreHelper.csproj b/InkCanvas.IACoreHelper/InkCanvas.IACoreHelper.csproj index 80109aa91..368a3af8b 100644 --- a/InkCanvas.IACoreHelper/InkCanvas.IACoreHelper.csproj +++ b/InkCanvas.IACoreHelper/InkCanvas.IACoreHelper.csproj @@ -61,6 +61,7 @@ + diff --git a/InkCanvas.IACoreHelper/LogService.cs b/InkCanvas.IACoreHelper/LogService.cs new file mode 100644 index 000000000..2e498c48b --- /dev/null +++ b/InkCanvas.IACoreHelper/LogService.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace InkCanvas.IACoreHelper +{ + /// + /// 重构期吞异常治理用最小日志设施。 + /// 写入 %AppData%/InkCanvasForClass/logs/refactor.log,单文件上限 1MB,滚动保留 3 个。 + /// 与主程序集 Ink_Canvas.Helpers.LogService 同逻辑;本程序集未引用主程序集,故保留本地副本。 + /// + internal static class LogService + { + private const long MaxFileSizeBytes = 1024 * 1024; + private const int RetainedFiles = 3; + private static readonly object WriteLock = new object(); + + private static string LogDirectory => + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "InkCanvasForClass", "logs"); + + private static string LogFile => Path.Combine(LogDirectory, "refactor.log"); + + public static void LogException(Exception ex, [CallerMemberName] string member = "") + { + try + { + lock (WriteLock) + { + Directory.CreateDirectory(LogDirectory); + RollIfNeeded(); + File.AppendAllText(LogFile, + $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{member}] {ex}{Environment.NewLine}"); + } + } + catch + { + // 日志设施自身失败时静默放弃,绝不影响主流程。 + } + } + + private static void RollIfNeeded() + { + if (!File.Exists(LogFile) || new FileInfo(LogFile).Length < MaxFileSizeBytes) return; + for (var i = RetainedFiles - 1; i >= 1; i--) + { + var older = $"{LogFile}.{i}"; + var newer = $"{LogFile}.{i + 1}"; + if (File.Exists(newer)) File.Delete(newer); + if (File.Exists(older)) File.Move(older, newer); + } + File.Move(LogFile, $"{LogFile}.1"); + } + } +} diff --git a/InkCanvas.IACoreHelper/Program.cs b/InkCanvas.IACoreHelper/Program.cs index bebdb0662..7465b11d9 100644 --- a/InkCanvas.IACoreHelper/Program.cs +++ b/InkCanvas.IACoreHelper/Program.cs @@ -49,7 +49,7 @@ static void Main(string[] args) { parent.WaitForExit(); } - catch { } + catch (Exception ex) { LogService.LogException(ex); } finally { parentExited.Set(); @@ -76,7 +76,7 @@ static void Main(string[] args) // 主线程阻塞直到父进程消失。守护线程一旦 Set,主线程退出,进程随之终止。 parentExited.Wait(); } - try { parent.Dispose(); } catch { } + try { parent.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } Environment.Exit(0); } catch (Exception ex) @@ -104,7 +104,7 @@ private static void RunPipeServer(string pipeName, string sharedMemoryNamePrefix if (!shouldExit()) continue; var server = currentServer; - try { server?.Dispose(); } catch { } + try { server?.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } return; } }) @@ -226,7 +226,7 @@ private static void RunPipeServer(string pipeName, string sharedMemoryNamePrefix } finally { - try { server.Dispose(); } catch { } + try { server.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } currentServer = null; } } diff --git a/InkCanvas.LiquidGlassMagHost/LogService.cs b/InkCanvas.LiquidGlassMagHost/LogService.cs new file mode 100644 index 000000000..aecd47fa4 --- /dev/null +++ b/InkCanvas.LiquidGlassMagHost/LogService.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace InkCanvas.LiquidGlassMagHost +{ + /// + /// 重构期吞异常治理用最小日志设施。 + /// 写入 %AppData%/InkCanvasForClass/logs/refactor.log,单文件上限 1MB,滚动保留 3 个。 + /// 与主程序集 Ink_Canvas.Helpers.LogService 同逻辑;本程序集未引用主程序集,故保留本地副本。 + /// + internal static class LogService + { + private const long MaxFileSizeBytes = 1024 * 1024; + private const int RetainedFiles = 3; + private static readonly object WriteLock = new object(); + + private static string LogDirectory => + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "InkCanvasForClass", "logs"); + + private static string LogFile => Path.Combine(LogDirectory, "refactor.log"); + + public static void LogException(Exception ex, [CallerMemberName] string member = "") + { + try + { + lock (WriteLock) + { + Directory.CreateDirectory(LogDirectory); + RollIfNeeded(); + File.AppendAllText(LogFile, + $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{member}] {ex}{Environment.NewLine}"); + } + } + catch + { + // 日志设施自身失败时静默放弃,绝不影响主流程。 + } + } + + private static void RollIfNeeded() + { + if (!File.Exists(LogFile) || new FileInfo(LogFile).Length < MaxFileSizeBytes) return; + for (var i = RetainedFiles - 1; i >= 1; i--) + { + var older = $"{LogFile}.{i}"; + var newer = $"{LogFile}.{i + 1}"; + if (File.Exists(newer)) File.Delete(newer); + if (File.Exists(older)) File.Move(older, newer); + } + File.Move(LogFile, $"{LogFile}.1"); + } + } +} diff --git a/InkCanvas.LiquidGlassMagHost/MagnifierHost.cs b/InkCanvas.LiquidGlassMagHost/MagnifierHost.cs index 38adc2bf3..50f4ac648 100644 --- a/InkCanvas.LiquidGlassMagHost/MagnifierHost.cs +++ b/InkCanvas.LiquidGlassMagHost/MagnifierHost.cs @@ -243,7 +243,7 @@ private static void ShutdownMag() DestroyHost(); if (_magInitialized) { - try { MagUninitialize(); } catch { } + try { MagUninitialize(); } catch (Exception ex) { LogService.LogException(ex); } _magInitialized = false; } } diff --git a/InkCanvas.PowerPointAddIn/Core/PPTController.cs b/InkCanvas.PowerPointAddIn/Core/PPTController.cs index 5f2d30ffd..d88375b6a 100644 --- a/InkCanvas.PowerPointAddIn/Core/PPTController.cs +++ b/InkCanvas.PowerPointAddIn/Core/PPTController.cs @@ -43,13 +43,13 @@ public PPTState GetState() { var pres = _application.ActivePresentation; state.PresentationName = pres.Name; - try { state.PresentationFullName = pres.FullName; } catch { } + try { state.PresentationFullName = pres.FullName; } catch (Exception ex) { LogService.LogException(ex); } state.TotalSlides = pres.Slides.Count; state.HasHiddenSlides = HasHiddenSlides(pres); state.HasAutoPlayTimings = HasAutoPlayTimings(pres); } } - catch { } + catch (Exception ex) { LogService.LogException(ex); } try { @@ -59,7 +59,7 @@ public PPTState GetState() state.SlideIndex = _application.SlideShowWindows[1].View.CurrentShowPosition; } } - catch { } + catch (Exception ex) { LogService.LogException(ex); } return state; } @@ -314,7 +314,7 @@ private static bool IsVideoShape(Microsoft.Office.Interop.PowerPoint.Shape shape return true; } } - catch { } + catch (Exception ex) { LogService.LogException(ex); } // 无法确认是否为媒体播放器时,不视为视频,避免把普通 ActiveX 控件误判为视频 return false; } @@ -327,10 +327,10 @@ private static bool IsVideoShape(Microsoft.Office.Interop.PowerPoint.Shape shape if ((int)(object)shape.MediaType == 3) // ppMediaTypeMovie return true; } - catch { } + catch (Exception ex) { LogService.LogException(ex); } } } - catch { } + catch (Exception ex2) { LogService.LogException(ex2); } return false; } @@ -345,7 +345,7 @@ private static bool HasHiddenSlides(Microsoft.Office.Interop.PowerPoint.Presenta return true; } } - catch { } + catch (Exception ex) { LogService.LogException(ex); } return false; } @@ -360,7 +360,7 @@ private static bool HasAutoPlayTimings(Microsoft.Office.Interop.PowerPoint.Prese return true; } } - catch { } + catch (Exception ex) { LogService.LogException(ex); } return false; } } diff --git a/InkCanvas.PowerPointAddIn/IPC/PipeHost.cs b/InkCanvas.PowerPointAddIn/IPC/PipeHost.cs index 2ea47d7a8..ad2a543d8 100644 --- a/InkCanvas.PowerPointAddIn/IPC/PipeHost.cs +++ b/InkCanvas.PowerPointAddIn/IPC/PipeHost.cs @@ -55,7 +55,7 @@ public void Start() public void Stop() { - try { _cts?.Cancel(); } catch { } + try { _cts?.Cancel(); } catch (Exception ex) { LogService.LogException(ex); } _cts?.Dispose(); _cts = null; _clientConnected = false; @@ -98,7 +98,7 @@ private async Task AcceptLoop(CancellationToken token) { _clientConnected = false; _currentPipe = null; - try { pipe?.Dispose(); } catch { } + try { pipe?.Dispose(); } catch (Exception ex) { LogService.LogException(ex); } } } } diff --git a/InkCanvas.PowerPointAddIn/InkCanvas.PowerPointAddIn.csproj b/InkCanvas.PowerPointAddIn/InkCanvas.PowerPointAddIn.csproj index 78ebd198c..aa29b2b0f 100644 --- a/InkCanvas.PowerPointAddIn/InkCanvas.PowerPointAddIn.csproj +++ b/InkCanvas.PowerPointAddIn/InkCanvas.PowerPointAddIn.csproj @@ -215,6 +215,9 @@ ThisAddIn.Designer.xml + + Code + Code diff --git a/InkCanvas.PowerPointAddIn/LogService.cs b/InkCanvas.PowerPointAddIn/LogService.cs new file mode 100644 index 000000000..5028093fd --- /dev/null +++ b/InkCanvas.PowerPointAddIn/LogService.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace InkCanvas.PowerPointAddIn +{ + /// + /// 重构期吞异常治理用最小日志设施。 + /// 写入 %AppData%/InkCanvasForClass/logs/refactor.log,单文件上限 1MB,滚动保留 3 个。 + /// 与主程序集 Ink_Canvas.Helpers.LogService 同逻辑;本程序集未引用主程序集,故保留本地副本。 + /// + internal static class LogService + { + private const long MaxFileSizeBytes = 1024 * 1024; + private const int RetainedFiles = 3; + private static readonly object WriteLock = new object(); + + private static string LogDirectory => + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "InkCanvasForClass", "logs"); + + private static string LogFile => Path.Combine(LogDirectory, "refactor.log"); + + public static void LogException(Exception ex, [CallerMemberName] string member = "") + { + try + { + lock (WriteLock) + { + Directory.CreateDirectory(LogDirectory); + RollIfNeeded(); + File.AppendAllText(LogFile, + $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{member}] {ex}{Environment.NewLine}"); + } + } + catch + { + // 日志设施自身失败时静默放弃,绝不影响主流程。 + } + } + + private static void RollIfNeeded() + { + if (!File.Exists(LogFile) || new FileInfo(LogFile).Length < MaxFileSizeBytes) return; + for (var i = RetainedFiles - 1; i >= 1; i--) + { + var older = $"{LogFile}.{i}"; + var newer = $"{LogFile}.{i + 1}"; + if (File.Exists(newer)) File.Delete(newer); + if (File.Exists(older)) File.Move(older, newer); + } + File.Move(LogFile, $"{LogFile}.1"); + } + } +} diff --git a/package.json b/package.json index be9accea6..bd9dcbbd0 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,8 @@ { + "scripts": { + "test": "dotnet test \"Ink Canvas.sln\" -c Debug --no-restore", + "build": "dotnet build \"Ink Canvas.sln\" -c Debug" + }, "dependencies": { "cors": "^2.8.6", "express": "^5.2.1"