From 9edb58ee27f176bce6ed0fa517f954190688e837 Mon Sep 17 00:00:00 2001 From: unknown <2564608840@qq.com> Date: Tue, 15 Jul 2025 14:36:54 +0800 Subject: [PATCH 01/20] =?UTF-8?q?improve:=E8=87=AA=E5=8A=A8=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Helpers/AutoUpdateHelper.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Ink Canvas/Helpers/AutoUpdateHelper.cs b/Ink Canvas/Helpers/AutoUpdateHelper.cs index 6df97809..094e2445 100644 --- a/Ink Canvas/Helpers/AutoUpdateHelper.cs +++ b/Ink Canvas/Helpers/AutoUpdateHelper.cs @@ -34,7 +34,7 @@ namespace Ink_Canvas.Helpers if (channel == UpdateChannel.Release) { // Release通道版本信息地址 - primaryUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/beta/AutomaticUpdateVersionControl.txt"; + primaryUrl = "https://github.com/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt"; fallbackUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt"; } else @@ -622,6 +622,13 @@ namespace Ink_Canvas.Helpers VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/AutomaticUpdateVersionControl.txt", DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip", LogUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md" + }, + new UpdateLineGroup + { + GroupName = "kkgithub线路", + VersionUrl = "https://kkgithub.com/InkCanvasForClass/community/raw/refs/heads/beta/AutomaticUpdateVersionControl.txt", + DownloadUrlFormat = "https://kkgithub.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip", + LogUrl = "https://kkgithub.com/InkCanvasForClass/community/raw/refs/heads/beta/UpdateLog.md" } } }, @@ -640,6 +647,13 @@ namespace Ink_Canvas.Helpers VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt", DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip", LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md" + }, + new UpdateLineGroup + { + GroupName = "kkgithub线路", + VersionUrl = "https://kkgithub.com/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt", + DownloadUrlFormat = "https://kkgithub.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip", + LogUrl = "https://kkgithub.com/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md" } } } From a0d159da9d603b1ec94b4f2caf55d3fe02ba9543 Mon Sep 17 00:00:00 2001 From: unknown <2564608840@qq.com> Date: Tue, 15 Jul 2025 16:06:29 +0800 Subject: [PATCH 02/20] =?UTF-8?q?improve:=E8=BF=9B=E7=A8=8B=E6=A3=80?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/App.xaml.cs | 288 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 286 insertions(+), 2 deletions(-) diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index eca96c36..6c378e01 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -5,7 +5,7 @@ using System; using System.Diagnostics; using System.Threading; using Microsoft.Win32; -using System.Security; // 添加SecurityException所需命名空间 +using System.Security; using System.IO; using System.Linq; using System.Reflection; @@ -15,6 +15,7 @@ using static System.Windows.Forms.VisualStyles.VisualStyleElement; using MessageBox = System.Windows.MessageBox; using Window = System.Windows.Window; using System.Collections.Generic; +using System.Runtime.InteropServices; namespace Ink_Canvas { @@ -34,6 +35,16 @@ namespace Ink_Canvas public static bool IsAppExitByUser = false; // 新增:退出信号文件路径 private static string watchdogExitSignalFile = Path.Combine(Path.GetTempPath(), "icc_watchdog_exit_" + System.Diagnostics.Process.GetCurrentProcess().Id + ".flag"); + // 新增:崩溃日志文件路径 + private static string crashLogFile = Path.Combine(Environment.GetEnvironmentVariable("APPDATA"), "Ink Canvas", "crash_logs"); + // 新增:进程ID + private static int currentProcessId = Process.GetCurrentProcess().Id; + // 新增:应用启动时间 + private static DateTime appStartTime = DateTime.Now; + // 新增:最后一次错误信息 + private static string lastErrorMessage = string.Empty; + // 新增:是否已初始化崩溃监听器 + private static bool crashListenersInitialized = false; public App() { @@ -53,6 +64,9 @@ namespace Ink_Canvas this.DispatcherUnhandledException += App_DispatcherUnhandledException; StartHeartbeatMonitor(); + // 新增:初始化全局异常和进程结束处理 + InitializeCrashListeners(); + // 仅在崩溃后操作为静默重启时才启动看门狗 if (CrashAction == CrashActionType.SilentRestart) { @@ -61,6 +75,259 @@ namespace Ink_Canvas this.Exit += App_Exit; // 注册退出事件 } + // 新增:初始化崩溃监听器 + private void InitializeCrashListeners() + { + if (crashListenersInitialized) return; + + try + { + // 确保崩溃日志目录存在 + if (!Directory.Exists(crashLogFile)) + { + Directory.CreateDirectory(crashLogFile); + } + + // 注册非UI线程未处理异常处理程序 + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + + // 注册控制台Ctrl+C等终止信号处理 + Console.CancelKeyPress += Console_CancelKeyPress; + + // 注册系统会话结束事件(关机、注销等) + SystemEvents.SessionEnding += SystemEvents_SessionEnding; + + // 注册进程退出处理程序 + AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; + + // 尝试注册Windows关闭消息监听 + SetConsoleCtrlHandler(ConsoleCtrlHandler, true); + + // 如果系统支持,添加Windows Management Instrumentation监听器 + try + { + // 使用反射动态加载和调用WMI + TrySetupWmiMonitoring(); + } + catch (Exception wmiEx) + { + LogHelper.WriteLogToFile($"设置WMI进程监控失败: {wmiEx.Message}", LogHelper.LogType.Warning); + } + + crashListenersInitialized = true; + LogHelper.WriteLogToFile("已初始化崩溃监听器", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"初始化崩溃监听器失败: {ex.Message}", LogHelper.LogType.Error); + } + } + + // 新增:动态加载WMI监控(避免直接引用System.Management) + private void TrySetupWmiMonitoring() + { + try + { + // 检查System.Management程序集是否可用 + var assemblyName = "System.Management, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"; + var assembly = Assembly.Load(assemblyName); + if (assembly == null) + { + LogHelper.WriteLogToFile("未找到System.Management程序集,跳过WMI监控", LogHelper.LogType.Warning); + return; + } + + // 使用反射创建WMI查询 + var watcherType = assembly.GetType("System.Management.ManagementEventWatcher"); + if (watcherType == null) + { + LogHelper.WriteLogToFile("未找到ManagementEventWatcher类型,跳过WMI监控", LogHelper.LogType.Warning); + return; + } + + // 构建WMI查询字符串 + string queryString = $"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.ProcessId = {currentProcessId}"; + + // 创建ManagementEventWatcher实例 + object watcher = Activator.CreateInstance(watcherType, queryString); + + // 获取EventArrived事件信息 + var eventInfo = watcherType.GetEvent("EventArrived"); + if (eventInfo == null) + { + LogHelper.WriteLogToFile("未找到EventArrived事件,跳过WMI监控", LogHelper.LogType.Warning); + return; + } + + // 创建委托并订阅事件 + Type delegateType = eventInfo.EventHandlerType; + var handler = Delegate.CreateDelegate(delegateType, this, GetType().GetMethod("WmiEventHandler", BindingFlags.NonPublic | BindingFlags.Instance)); + eventInfo.AddEventHandler(watcher, handler); + + // 启动监听 + var startMethod = watcherType.GetMethod("Start"); + startMethod.Invoke(watcher, null); + + LogHelper.WriteLogToFile("已成功启动WMI进程监控", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"动态加载WMI监控失败: {ex.Message}", LogHelper.LogType.Warning); + } + } + + // WMI事件处理方法(通过反射调用) + private void WmiEventHandler(object sender, EventArgs e) + { + try + { + // 尝试从事件参数中提取信息 + dynamic eventArgs = e; + dynamic newEvent = eventArgs.NewEvent; + if (newEvent != null) + { + dynamic targetInstance = newEvent["TargetInstance"]; + if (targetInstance != null) + { + string processName = targetInstance["Name"]?.ToString() ?? "未知进程"; + WriteCrashLog($"WMI检测到进程{processName}(ID:{currentProcessId})已终止"); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"处理WMI事件时出错: {ex.Message}", LogHelper.LogType.Warning); + } + } + + // 新增:Windows控制台控制处理程序 + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate handler, bool add); + + private delegate bool ConsoleCtrlDelegate(int ctrlType); + + private static bool ConsoleCtrlHandler(int ctrlType) + { + string eventType = "未知控制类型"; + + // 使用传统switch语句替代switch表达式 + switch (ctrlType) + { + case 0: + eventType = "CTRL_C_EVENT"; + break; + case 1: + eventType = "CTRL_BREAK_EVENT"; + break; + case 2: + eventType = "CTRL_CLOSE_EVENT"; + break; + case 5: + eventType = "CTRL_LOGOFF_EVENT"; + break; + case 6: + eventType = "CTRL_SHUTDOWN_EVENT"; + break; + default: + eventType = $"未知控制类型({ctrlType})"; + break; + } + + WriteCrashLog($"接收到系统控制信号: {eventType}"); + + // 返回true表示已处理该事件 + return false; + } + + // 新增:系统会话结束事件处理 + private void SystemEvents_SessionEnding(object sender, SessionEndingEventArgs e) + { + string reason = e.Reason == SessionEndReasons.Logoff ? "用户注销" : "系统关机"; + WriteCrashLog($"系统会话即将结束: {reason}"); + } + + // 新增:控制台取消事件处理 + private void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + WriteCrashLog($"接收到控制台中断信号: {e.SpecialKey}"); + e.Cancel = true; // 取消默认处理 + } + + // 新增:处理非UI线程的未处理异常 + private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + try + { + var exception = e.ExceptionObject as Exception; + string errorMessage = exception?.ToString() ?? "未知异常"; + lastErrorMessage = errorMessage; + + WriteCrashLog($"捕获到未处理的异常: {errorMessage}"); + + if (e.IsTerminating) + { + WriteCrashLog("应用程序即将终止"); + } + } + catch (Exception ex) + { + // 尝试在最后时刻记录错误 + try + { + File.AppendAllText( + Path.Combine(crashLogFile, $"critical_error_{DateTime.Now:yyyyMMdd_HHmmss}.log"), + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 记录未处理异常时发生错误: {ex.Message}\r\n" + ); + } + catch { } + } + } + + // 新增:处理进程退出事件 + private void CurrentDomain_ProcessExit(object sender, EventArgs e) + { + TimeSpan runDuration = DateTime.Now - appStartTime; + WriteCrashLog($"应用程序退出,运行时长: {runDuration}"); + + // 如果有最后错误消息,记录到日志 + if (!string.IsNullOrEmpty(lastErrorMessage)) + { + WriteCrashLog($"最后错误信息: {lastErrorMessage}"); + } + } + + // 新增:记录崩溃日志 + private static void WriteCrashLog(string message) + { + try + { + // 确保目录存在 + if (!Directory.Exists(crashLogFile)) + { + Directory.CreateDirectory(crashLogFile); + } + + string logFileName = Path.Combine(crashLogFile, $"crash_{DateTime.Now:yyyyMMdd}.log"); + + // 收集系统状态信息 + string memoryUsage = (Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024)).ToString() + " MB"; + string cpuTime = Process.GetCurrentProcess().TotalProcessorTime.ToString(); + string processUptime = (DateTime.Now - Process.GetCurrentProcess().StartTime).ToString(); + + string statusInfo = $"[内存: {memoryUsage}, CPU时间: {cpuTime}, 运行时长: {processUptime}]"; + + // 写入日志 + File.AppendAllText( + logFileName, + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [PID:{currentProcessId}] {message}\r\n{statusInfo}\r\n\r\n" + ); + + // 同时记录到主日志 + LogHelper.WriteLogToFile(message, LogHelper.LogType.Error); + } + catch { } + } + // 增加字段保存崩溃后操作设置 public static CrashActionType CrashAction = CrashActionType.SilentRestart; @@ -92,6 +359,11 @@ namespace Ink_Canvas { Ink_Canvas.MainWindow.ShowNewMessage("抱歉,出现未预期的异常,可能导致 InkCanvasForClass 运行不稳定。\n建议保存墨迹后重启应用。", true); LogHelper.NewLog(e.Exception.ToString()); + + // 新增:记录到崩溃日志 + lastErrorMessage = e.Exception.ToString(); + WriteCrashLog($"UI线程未处理异常: {e.Exception}"); + e.Handled = true; SyncCrashActionFromSettings(); // 新增:崩溃时同步最新设置 @@ -398,6 +670,10 @@ namespace Ink_Canvas // 仅在软件内主动退出时关闭看门狗,并写入退出信号 try { + // 新增:记录应用退出状态 + string exitType = IsAppExitByUser ? "用户主动退出" : "应用程序退出"; + WriteCrashLog($"{exitType},退出代码: {e.ApplicationExitCode}"); + if (IsAppExitByUser) { // 写入退出信号文件,通知看门狗正常退出 @@ -409,7 +685,15 @@ namespace Ink_Canvas } } } - catch { } + catch (Exception ex) + { + // 尝试记录最后的错误 + try + { + LogHelper.WriteLogToFile($"退出处理时发生错误: {ex.Message}", LogHelper.LogType.Error); + } + catch { } + } } /// From 51f3d410c9b704454010da015405c51afcb2a0b6 Mon Sep 17 00:00:00 2001 From: unknown <2564608840@qq.com> Date: Tue, 15 Jul 2025 17:55:22 +0800 Subject: [PATCH 03/20] fix:issue #47 --- Ink Canvas/AssemblyInfo.cs | 4 +- Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs | 74 ++------------------- Ink Canvas/Properties/AssemblyInfo.cs | 4 +- 3 files changed, 8 insertions(+), 74 deletions(-) diff --git a/Ink Canvas/AssemblyInfo.cs b/Ink Canvas/AssemblyInfo.cs index 05a1aabd..72c0c87d 100644 --- a/Ink Canvas/AssemblyInfo.cs +++ b/Ink Canvas/AssemblyInfo.cs @@ -49,5 +49,5 @@ using System.Windows; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.7.0.4")] -[assembly: AssemblyFileVersion("1.7.0.4")] +[assembly: AssemblyVersion("1.7.0.5")] +[assembly: AssemblyFileVersion("1.7.0.5")] diff --git a/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs b/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs index 656eb16e..196c9913 100644 --- a/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs +++ b/Ink Canvas/MainWindow_cs/MW_ShapeDrawing.cs @@ -493,77 +493,11 @@ namespace Ink_Canvas { } // 触摸移动时保持自定义光标显示 - if (Settings.Canvas.IsShowCursor) { - inkCanvas.ForceCursor = true; - inkCanvas.UseCustomCursor = true; // 确保使用自定义光标 - System.Windows.Forms.Cursor.Show(); - } + if (inkCanvas.EditingMode != InkCanvasEditingMode.None) + inkCanvas.EditingMode = InkCanvasEditingMode.None; + - if (NeedUpdateIniP()) iniP = e.GetTouchPoint(inkCanvas).Position; - if (drawingShapeMode == 9 && isFirstTouchCuboid == false) MouseTouchMove(iniP); - inkCanvas.Opacity = 1; - double boundsWidth = GetTouchBoundWidth(e), eraserMultiplier = 1.0; - if (!Settings.Advanced.EraserBindTouchMultiplier && Settings.Advanced.IsSpecialScreen) - eraserMultiplier = 1 / Settings.Advanced.TouchMultiplier; - if (boundsWidth > BoundsWidth) { - isLastTouchEraser = true; - if (drawingShapeMode == 0 && forceEraser) return; - if (boundsWidth > BoundsWidth * 2.5) { - double k = 1; - switch (Settings.Canvas.EraserSize) { - case 0: - k = 0.5; - break; - case 1: - k = 0.8; - break; - case 3: - k = 1.25; - break; - case 4: - k = 1.5; - break; - } - - inkCanvas.EraserShape = new EllipseStylusShape(boundsWidth * k * eraserMultiplier, - boundsWidth * k * eraserMultiplier); - inkCanvas.EditingMode = InkCanvasEditingMode.EraseByPoint; - - // 立即应用光标设置 - if (Settings.Canvas.IsShowCursor) { - inkCanvas.Cursor = Cursors.Cross; - System.Windows.Forms.Cursor.Show(); - } - } - else { - if (StackPanelPPTControls.Visibility == Visibility.Visible && inkCanvas.Strokes.Count == 0 && - Settings.PowerPointSettings.IsEnableFingerGestureSlideShowControl) { - isLastTouchEraser = false; - inkCanvas.EditingMode = InkCanvasEditingMode.GestureOnly; - inkCanvas.Opacity = 0.1; - } - else { - inkCanvas.EraserShape = new EllipseStylusShape(5, 5); - inkCanvas.EditingMode = InkCanvasEditingMode.EraseByStroke; - } - } - } - else { - isLastTouchEraser = false; - // 修复面积擦时不显示橡皮形状:无论 forcePointEraser 状态,均显示 50x50 橡皮 - inkCanvas.EraserShape = new EllipseStylusShape(50, 50); - if (forceEraser) return; - inkCanvas.EditingMode = InkCanvasEditingMode.Ink; - } - - if (inkCanvas.EditingMode != InkCanvasEditingMode.None) - return; - - if (e.TouchDevice == null) { - System.Windows.Forms.Cursor.Show(); - } else { - System.Windows.Forms.Cursor.Hide(); - } + MouseTouchMove(e.GetTouchPoint(inkCanvas).Position); } private int drawMultiStepShapeCurrentStep = 0; //多笔完成的图形 当前所处在的笔画 diff --git a/Ink Canvas/Properties/AssemblyInfo.cs b/Ink Canvas/Properties/AssemblyInfo.cs index 2a6082a7..c823116b 100644 --- a/Ink Canvas/Properties/AssemblyInfo.cs +++ b/Ink Canvas/Properties/AssemblyInfo.cs @@ -49,5 +49,5 @@ using System.Windows; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.7.0.4")] -[assembly: AssemblyFileVersion("1.7.0.4")] +[assembly: AssemblyVersion("1.7.0.5")] +[assembly: AssemblyFileVersion("1.7.0.5")] From 29fa565258866568a6283ef63474e5e0c4b60fbb Mon Sep 17 00:00:00 2001 From: unknown <2564608840@qq.com> Date: Tue, 15 Jul 2025 18:09:38 +0800 Subject: [PATCH 04/20] fix:issue #75 --- Ink Canvas/MainWindow.xaml.cs | 3 ++ Ink Canvas/MainWindow_cs/MW_PPT.cs | 85 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs index e81dcb55..661ad4cf 100644 --- a/Ink Canvas/MainWindow.xaml.cs +++ b/Ink Canvas/MainWindow.xaml.cs @@ -219,6 +219,9 @@ namespace Ink_Canvas { RadioCrashSilentRestart.IsChecked = true; else RadioCrashNoAction.IsChecked = true; + + // 注册系统关机事件处理 + RegisterShutdownHandler(); } private void SystemEventsOnDisplaySettingsChanged(object sender, EventArgs e) { diff --git a/Ink Canvas/MainWindow_cs/MW_PPT.cs b/Ink Canvas/MainWindow_cs/MW_PPT.cs index d31ef87e..b94529ac 100644 --- a/Ink Canvas/MainWindow_cs/MW_PPT.cs +++ b/Ink Canvas/MainWindow_cs/MW_PPT.cs @@ -1226,5 +1226,90 @@ namespace Ink_Canvas { private void ImagePPTControlEnd_MouseUp(object sender, MouseButtonEventArgs e) { BtnPPTSlideShowEnd_Click(BtnPPTSlideShowEnd, null); } + + // 添加关机事件注册方法 + private void RegisterShutdownHandler() + { + try + { + SystemEvents.SessionEnding += SystemEvents_SessionEnding; + LogHelper.WriteLogToFile("已注册系统关机事件处理", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"注册系统关机事件处理失败: {ex.Message}", LogHelper.LogType.Error); + } + } + + // 系统关机事件处理 + private void SystemEvents_SessionEnding(object sender, SessionEndingEventArgs e) + { + LogHelper.WriteLogToFile("检测到系统关机事件,正在清理PowerPoint进程", LogHelper.LogType.Info); + + // 终止PowerPoint进程守护 + try + { + // 停止计时器以终止进程守护 + timerCheckPPT.Stop(); + + // 清理COM对象 + ResetPresentationObjects(); + + // 强制结束所有PowerPoint进程 + foreach (var process in Process.GetProcessesByName("POWERPNT")) + { + try + { + process.Kill(); + LogHelper.WriteLogToFile($"已终止PowerPoint进程: {process.Id}", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"终止PowerPoint进程失败: {ex.Message}", LogHelper.LogType.Error); + } + } + + // 强制结束所有WPS进程 + foreach (var processName in GetPossibleWPSProcessNames()) + { + foreach (var process in Process.GetProcessesByName(processName)) + { + try + { + process.Kill(); + LogHelper.WriteLogToFile($"已终止WPS进程: {process.ProcessName}({process.Id})", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"终止WPS进程失败: {ex.Message}", LogHelper.LogType.Error); + } + } + } + + // 强制GC回收 + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"系统关机清理过程中出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + // 在主窗口初始化方法中添加以下调用 + // 在适当的初始化方法中调用 RegisterShutdownHandler(); + + // 在主窗口关闭时取消注册关机事件 + protected override void OnClosed(EventArgs e) + { + try + { + // 取消注册系统关机事件 + SystemEvents.SessionEnding -= SystemEvents_SessionEnding; + } + catch { } + + base.OnClosed(e); + } } } From 2c2f46a0d88792e105b23270930e39d74efe7bd4 Mon Sep 17 00:00:00 2001 From: unknown <2564608840@qq.com> Date: Tue, 15 Jul 2025 18:30:25 +0800 Subject: [PATCH 05/20] =?UTF-8?q?improve:=E7=AB=AF=E7=82=B9=E5=90=B8?= =?UTF-8?q?=E9=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MW_SimulatePressure&InkToShape.cs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs index bb67682a..f66ed808 100644 --- a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs +++ b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs @@ -125,37 +125,31 @@ namespace Ink_Canvas { Point startPoint = e.Stroke.StylusPoints[0].ToPoint(); Point endPoint = e.Stroke.StylusPoints[e.Stroke.StylusPoints.Count - 1].ToPoint(); - // 记录是否需要拉直线条,默认不拉直 - bool shouldStraighten = false; - bool snapped = false; - - // 首先检查是否应该拉直线条(使用灵敏度设置),这是主要判断条件 + // 先完成所有直线判定,再考虑端点吸附 // 读取实际的灵敏度设置值 double sensitivity = Settings.InkToShape.LineStraightenSensitivity; System.Diagnostics.Debug.WriteLine($"当前灵敏度值: {sensitivity}"); - // 将灵敏度值传递给判断函数 - shouldStraighten = ShouldStraightenLine(e.Stroke); + // 判断是否应该拉直线条 + bool shouldStraighten = ShouldStraightenLine(e.Stroke); // 输出一些调试信息,帮助理解灵敏度设置的效果 System.Diagnostics.Debug.WriteLine($"LineStraightenSensitivity: {Settings.InkToShape.LineStraightenSensitivity}, ShouldStraighten: {shouldStraighten}"); - // 再检查端点吸附功能,这是独立的可选功能 - if (Settings.Canvas.LineEndpointSnapping) { + // 只有当确定要拉直线条时,才检查端点吸附 + if (shouldStraighten && Settings.Canvas.LineEndpointSnapping) { // 只有在启用了形状识别(矩形或三角形)时才执行端点吸附 if (Settings.InkToShape.IsInkToShapeRectangle || Settings.InkToShape.IsInkToShapeTriangle) { Point[] snappedPoints = GetSnappedEndpoints(startPoint, endPoint); if (snappedPoints != null) { startPoint = snappedPoints[0]; endPoint = snappedPoints[1]; - snapped = true; } } } - // 如果满足任一条件(需要拉直或成功吸附),则创建直线 - // 这确保灵敏度设置独立于端点吸附功能发挥作用 - if (shouldStraighten || snapped) { + // 如果确定要拉直,则创建直线 + if (shouldStraighten) { StylusPointCollection straightLinePoints = CreateStraightLine(startPoint, endPoint); Stroke straightStroke = new Stroke(straightLinePoints) { DrawingAttributes = inkCanvas.DefaultDrawingAttributes.Clone() From e687c78ba821a992259bc9a2ba13edafff8a694a Mon Sep 17 00:00:00 2001 From: unknown <2564608840@qq.com> Date: Tue, 15 Jul 2025 20:30:10 +0800 Subject: [PATCH 06/20] =?UTF-8?q?add:=E8=87=AA=E5=AE=9A=E4=B9=89=E6=B5=AE?= =?UTF-8?q?=E5=8A=A8=E6=A0=8F=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/MainWindow.xaml | 9 ++ Ink Canvas/MainWindow.xaml.cs | 2 +- Ink Canvas/MainWindow_cs/MW_Settings.cs | 74 ++++++++++-- Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs | 47 ++------ Ink Canvas/Resources/Settings.cs | 21 ++++ Ink Canvas/Windows/AddCustomIconWindow.xaml | 44 +++++++ .../Windows/AddCustomIconWindow.xaml.cs | 113 ++++++++++++++++++ Ink Canvas/Windows/CustomIconWindow.xaml | 45 +++++++ Ink Canvas/Windows/CustomIconWindow.xaml.cs | 63 ++++++++++ 9 files changed, 374 insertions(+), 44 deletions(-) create mode 100644 Ink Canvas/Windows/AddCustomIconWindow.xaml create mode 100644 Ink Canvas/Windows/AddCustomIconWindow.xaml.cs create mode 100644 Ink Canvas/Windows/CustomIconWindow.xaml create mode 100644 Ink Canvas/Windows/CustomIconWindow.xaml.cs diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 78234815..3e6b94d7 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -1023,6 +1023,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs new file mode 100644 index 00000000..4045a5fb --- /dev/null +++ b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs @@ -0,0 +1,460 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher +{ + /// + /// LauncherWindow.xaml 的交互逻辑 + /// + public partial class LauncherWindow : Window + { + /// + /// 父插件 + /// + private readonly SuperLauncherPlugin _plugin; + + /// + /// 是否处于固定模式 + /// + private bool _isFixMode = false; + + /// + /// 应用项按钮列表 + /// + private readonly Dictionary _appButtons = new Dictionary(); + + /// + /// 拖拽中的按钮 + /// + private Button _draggingButton; + + /// + /// 拖拽开始位置 + /// + private Point _dragStartPoint; + + /// + /// 构造函数 + /// + public LauncherWindow(SuperLauncherPlugin plugin) + { + InitializeComponent(); + + _plugin = plugin; + + // 加载应用项 + LoadLauncherItems(); + + // 添加鼠标按下事件(用于拖动窗口) + this.MouseDown += (s, e) => + { + if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed) + { + this.DragMove(); + } + }; + + // 根据应用数量调整窗口大小 + AdjustWindowSize(); + } + + /// + /// 加载启动台应用项 + /// + private void LoadLauncherItems() + { + // 清空现有应用项 + AppPanel.Children.Clear(); + _appButtons.Clear(); + + // 获取显示的应用项 + var visibleItems = _plugin.LauncherItems + .Where(item => item.IsVisible) + .OrderBy(item => item.Position) + .ToList(); + + foreach (var item in visibleItems) + { + // 创建应用按钮 + Button appButton = new Button + { + Style = (Style)FindResource("LauncherItemStyle"), + DataContext = item, + Tag = item.Position + }; + + // 添加点击事件 + appButton.Click += AppButton_Click; + + // 在固定模式下,添加拖拽事件 + appButton.PreviewMouseDown += AppButton_PreviewMouseDown; + appButton.PreviewMouseMove += AppButton_PreviewMouseMove; + appButton.PreviewMouseUp += AppButton_PreviewMouseUp; + + // 记录按钮和项目的对应关系 + _appButtons.Add(appButton, item); + + // 添加到面板 + AppPanel.Children.Add(appButton); + } + } + + /// + /// 根据应用数量调整窗口大小 + /// + private void AdjustWindowSize() + { + try + { + // 每行最多显示4个应用 + const int appsPerRow = 4; + + // 计算行数 + int visibleCount = _appButtons.Count; + int rowCount = (int)Math.Ceiling(visibleCount / (double)appsPerRow); + + // 设置窗口宽度(每个应用90像素宽 = 80 + 5*2) + Width = Math.Min(appsPerRow * 90 + 40, 400); // 最大宽度400 + + // 设置窗口高度(每个应用90像素高 = 80 + 5*2) + Height = Math.Min(rowCount * 90 + 60, 600); // 最大高度600,标题栏40 + 边距20 + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"调整启动台窗口大小时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 应用按钮点击事件 + /// + private void AppButton_Click(object sender, RoutedEventArgs e) + { + try + { + if (_isFixMode) return; // 在固定模式下,不响应点击事件 + + if (sender is Button button && _appButtons.TryGetValue(button, out LauncherItem item)) + { + // 获取应用路径和名称,用于后续启动 + string appPath = item.Path; + string appName = item.Name; + + LogHelper.WriteLogToFile($"点击启动应用: {appName}, 路径: {appPath}", LogHelper.LogType.Info); + + // 首先标记窗口正在关闭 + IsClosing = true; + + // 创建一个应用启动任务 + var launchTask = new System.Threading.Tasks.Task(() => + { + try + { + // 等待一段时间,确保窗口关闭流程已经开始 + System.Threading.Thread.Sleep(200); + + // 使用UI线程启动应用 + Application.Current.Dispatcher.Invoke(() => + { + try + { + // 检查应用路径是否存在 + if (System.IO.File.Exists(appPath) || !appPath.Contains(":\\")) + { + // 创建进程启动信息 + var psi = new System.Diagnostics.ProcessStartInfo + { + FileName = appPath, + UseShellExecute = true, + }; + + // 启动应用程序 + var process = System.Diagnostics.Process.Start(psi); + LogHelper.WriteLogToFile($"应用程序 {appName} 已启动", LogHelper.LogType.Info); + } + else + { + LogHelper.WriteLogToFile($"应用路径不存在: {appPath}", LogHelper.LogType.Error); + MessageBox.Show($"找不到应用程序: {appPath}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"启动应用程序失败: {ex.Message}", LogHelper.LogType.Error); + MessageBox.Show($"启动应用程序失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + }); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"应用启动任务出错: {ex.Message}", LogHelper.LogType.Error); + } + }); + + // 关闭窗口 + try + { + Dispatcher.BeginInvoke(new Action(() => + { + try { Close(); } catch { } + + // 启动应用程序任务 + launchTask.Start(); + }), System.Windows.Threading.DispatcherPriority.Background); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"关闭窗口或启动任务时出错: {ex.Message}", LogHelper.LogType.Error); + // 如果无法通过UI关闭窗口,直接启动任务 + launchTask.Start(); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"应用按钮点击事件出错: {ex.Message}", LogHelper.LogType.Error); + try { IsClosing = true; Close(); } catch { } + } + } + + #region 固定模式拖拽事件 + + /// + /// 应用按钮鼠标按下事件 + /// + private void AppButton_PreviewMouseDown(object sender, MouseButtonEventArgs e) + { + if (!_isFixMode) return; + + if (e.ChangedButton == MouseButton.Left && sender is Button button) + { + _draggingButton = button; + _dragStartPoint = e.GetPosition(AppPanel); + button.CaptureMouse(); + button.Opacity = 0.7; + + // 阻止事件冒泡,以避免触发按钮点击 + e.Handled = true; + } + } + + /// + /// 应用按钮鼠标移动事件 + /// + private void AppButton_PreviewMouseMove(object sender, MouseEventArgs e) + { + if (!_isFixMode || _draggingButton == null) return; + + if (e.LeftButton == MouseButtonState.Pressed) + { + Point currentPosition = e.GetPosition(AppPanel); + + // 移动按钮 + System.Windows.Controls.Canvas.SetLeft(_draggingButton, currentPosition.X - _draggingButton.ActualWidth / 2); + System.Windows.Controls.Canvas.SetTop(_draggingButton, currentPosition.Y - _draggingButton.ActualHeight / 2); + + // 将按钮移到最上层 + Panel.SetZIndex(_draggingButton, 100); + + // 阻止事件冒泡 + e.Handled = true; + } + } + + /// + /// 应用按钮鼠标释放事件 + /// + private void AppButton_PreviewMouseUp(object sender, MouseButtonEventArgs e) + { + if (!_isFixMode || _draggingButton == null) return; + + // 释放鼠标捕获 + _draggingButton.ReleaseMouseCapture(); + + // 计算新位置 + Point releasePoint = e.GetPosition(AppPanel); + int newPosition = CalculateGridPosition(releasePoint); + + // 获取当前项目 + LauncherItem currentItem = _appButtons[_draggingButton]; + + // 重新排序 + ReorderItems(currentItem, newPosition); + + // 重新加载应用项 + LoadLauncherItems(); + + // 保存配置 + _plugin.SaveConfig(); + + // 清除拖拽状态 + _draggingButton.Opacity = 1; + Panel.SetZIndex(_draggingButton, 0); + _draggingButton = null; + + // 阻止事件冒泡 + e.Handled = true; + } + + /// + /// 计算网格位置 + /// + private int CalculateGridPosition(Point point) + { + // 计算行和列 + int columnCount = 4; // 每行最多4个应用 + int columnWidth = 90; // 应用宽度(包括边距) + int rowHeight = 90; // 应用高度(包括边距) + + int column = (int)(point.X / columnWidth); + int row = (int)(point.Y / rowHeight); + + // 确保在有效范围内 + column = Math.Max(0, Math.Min(column, columnCount - 1)); + row = Math.Max(0, row); + + // 计算位置索引 + return row * columnCount + column; + } + + /// + /// 重新排序应用项 + /// + private void ReorderItems(LauncherItem item, int newPosition) + { + try + { + // 设置项目为固定位置 + item.IsPositionFixed = true; + + // 如果位置相同,无需调整 + if (item.Position == newPosition) + { + return; + } + + // 获取所有可见项目 + var visibleItems = _plugin.LauncherItems + .Where(i => i.IsVisible) + .OrderBy(i => i.Position) + .ToList(); + + // 移除当前项目 + visibleItems.Remove(item); + + // 查找插入位置 + int insertIndex = 0; + for (int i = 0; i < visibleItems.Count; i++) + { + if (visibleItems[i].Position >= newPosition) + { + insertIndex = i; + break; + } + insertIndex = i + 1; + } + + // 插入项目 + visibleItems.Insert(insertIndex, item); + + // 重新分配位置 + for (int i = 0; i < visibleItems.Count; i++) + { + visibleItems[i].Position = i; + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"重新排序应用项时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + #endregion + + #region 窗口事件处理 + + /// + /// 窗口失去焦点事件 + /// + private void Window_Deactivated(object sender, EventArgs e) + { + try + { + // 只有在非固定模式、窗口已加载、未处于关闭状态且IsLoaded=true时关闭窗口 + if (!_isFixMode && IsLoaded && !IsClosing) + { + // 标记为正在关闭 + IsClosing = true; + + // 使用Dispatcher.BeginInvoke而不是直接调用Close,避免冲突 + Dispatcher.BeginInvoke(new Action(() => + { + try + { + // 再次检查窗口状态 + if (IsLoaded && !IsClosing) + { + Close(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"延迟关闭窗口时出错: {ex.Message}", LogHelper.LogType.Error); + } + }), System.Windows.Threading.DispatcherPriority.Background); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"窗口失去焦点关闭时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 窗口是否正在关闭 + /// + private bool IsClosing { get; set; } = false; + + /// + /// 重写OnClosing方法,标记窗口正在关闭 + /// + protected override void OnClosing(System.ComponentModel.CancelEventArgs e) + { + IsClosing = true; + base.OnClosing(e); + } + + /// + /// 关闭按钮点击事件 + /// + private void BtnClose_Click(object sender, RoutedEventArgs e) + { + Close(); + } + + /// + /// 固定模式按钮点击事件 + /// + private void BtnFixMode_Click(object sender, RoutedEventArgs e) + { + // 切换固定模式 + _isFixMode = !_isFixMode; + + // 更新固定模式按钮图标颜色 + FixModeIcon.Fill = _isFixMode ? Brushes.Yellow : Brushes.White; + + // 显示提示 + if (_isFixMode) + { + MessageBox.Show("已进入固定模式,您可以拖动应用图标调整位置。", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs new file mode 100644 index 00000000..20cc2972 --- /dev/null +++ b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs @@ -0,0 +1,589 @@ +using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace Ink_Canvas.Helpers.Plugins.BuiltIn +{ + /// + /// 超级启动台插件 + /// + public class SuperLauncherPlugin : PluginBase + { + #region 插件基本信息 + + public override string Name => "超级启动台"; + + public override string Description => "在浮动栏添加一个启动台按钮,可快速启动常用应用程序。"; + + public override Version Version => new Version(1, 0, 0); + + public override string Author => "ICC CE 团队"; + + public override bool IsBuiltIn => true; + + #endregion + + #region 插件属性和字段 + + /// + /// 启动台配置 + /// + public LauncherConfig Config { get; private set; } + + /// + /// 启动台应用程序列表 + /// + public ObservableCollection LauncherItems { get; private set; } + + /// + /// 启动台按钮 + /// + private LauncherButton _launcherButton; + + /// + /// 启动台窗口 + /// + private LauncherWindow _launcherWindow; + + /// + /// 配置文件路径 + /// + private readonly string _configPath = Path.Combine(App.RootPath, "PluginConfigs", "SuperLauncher.json"); + + /// + /// 标记是否已添加到浮动栏 + /// + private bool _isAddedToFloatingBar = false; + + #endregion + + #region 插件生命周期 + + public override void Initialize() + { + try + { + base.Initialize(); + + // 创建配置目录 + string configDir = Path.Combine(App.RootPath, "PluginConfigs"); + if (!Directory.Exists(configDir)) + { + Directory.CreateDirectory(configDir); + } + + // 加载配置 + LoadConfig(); + + LogHelper.WriteLogToFile("超级启动台插件已初始化", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"初始化超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error); + LogHelper.NewLog(ex); + } + } + + public override void Enable() + { + try + { + if (IsEnabled) return; // 防止重复启用 + + // 创建启动台按钮 + if (_launcherButton == null) + { + _launcherButton = new LauncherButton(this); + LogHelper.WriteLogToFile("超级启动台按钮已创建", LogHelper.LogType.Info); + } + + // 添加启动台按钮到浮动栏 + AddLauncherButtonToFloatingBar(); + + // 设置启用状态 + base.Enable(); + + // 保存插件配置 + SavePluginSettings(); + + LogHelper.WriteLogToFile("超级启动台插件已启用", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"启用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error); + LogHelper.NewLog(ex); + } + } + + public override void Disable() + { + try + { + if (!IsEnabled) return; // 防止重复禁用 + + // 从浮动栏移除启动台按钮 + RemoveLauncherButtonFromFloatingBar(); + + // 如果启动台窗口打开,则关闭 + if (_launcherWindow != null && _launcherWindow.IsVisible) + { + _launcherWindow.Close(); + _launcherWindow = null; + } + + // 设置禁用状态 + base.Disable(); + + // 保存插件配置 + SavePluginSettings(); + + LogHelper.WriteLogToFile("超级启动台插件已禁用", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"禁用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error); + LogHelper.NewLog(ex); + } + } + + public override UserControl GetSettingsView() + { + return new LauncherSettingsControl(this); + } + + public override void Cleanup() + { + // 保存配置 + SaveConfig(); + + // 从浮动栏移除启动台按钮 + RemoveLauncherButtonFromFloatingBar(); + + // 如果启动台窗口打开,则关闭 + if (_launcherWindow != null && _launcherWindow.IsVisible) + { + _launcherWindow.Close(); + _launcherWindow = null; + } + + base.Cleanup(); + } + + /// + /// 保存插件设置 + /// + public override void SavePluginSettings() + { + try + { + // 确保配置已加载 + if (Config == null) + { + LoadConfig(); + } + + // 更新其他设置,但不更改插件启用状态 + + // 保存配置 + SaveConfig(); + + LogHelper.WriteLogToFile($"超级启动台插件设置已保存", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存超级启动台插件设置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + #endregion + + #region 配置管理 + + /// + /// 加载配置 + /// + private void LoadConfig() + { + try + { + if (File.Exists(_configPath)) + { + string json = File.ReadAllText(_configPath); + Config = JsonConvert.DeserializeObject(json) ?? CreateDefaultConfig(); + LauncherItems = new ObservableCollection(Config.Items ?? new List()); + + // 注意:不再根据配置更改插件启用状态 + // 插件状态由PluginManager统一管理 + } + else + { + Config = CreateDefaultConfig(); + LauncherItems = new ObservableCollection(Config.Items); + SaveConfig(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error); + Config = CreateDefaultConfig(); + LauncherItems = new ObservableCollection(Config.Items); + } + } + + /// + /// 保存配置 + /// + public void SaveConfig() + { + try + { + // 同步LauncherItems到Config + Config.Items = new List(LauncherItems); + + // 序列化并保存配置 + string json = JsonConvert.SerializeObject(Config, Formatting.Indented); + File.WriteAllText(_configPath, json); + + LogHelper.WriteLogToFile("超级启动台配置已保存", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 创建默认配置 + /// + private LauncherConfig CreateDefaultConfig() + { + var config = new LauncherConfig + { + ButtonPosition = LauncherButtonPosition.Right, + // 不再使用IsEnabled,插件状态由PluginManager管理 + Items = new List + { + new LauncherItem + { + Name = "资源管理器", + Path = "explorer.exe", + IsVisible = true, + Position = 0 + } + } + }; + + return config; + } + + #endregion + + #region 启动台按钮管理 + + /// + /// 将启动台按钮添加到浮动栏 + /// + private void AddLauncherButtonToFloatingBar() + { + try + { + // 如果已经添加,先移除 + if (_isAddedToFloatingBar) + { + RemoveLauncherButtonFromFloatingBar(); + _isAddedToFloatingBar = false; + } + + // 获取主窗口实例 + var mainWindow = Application.Current.MainWindow; + if (mainWindow == null) + { + LogHelper.WriteLogToFile("未找到主窗口实例,无法添加启动台按钮", LogHelper.LogType.Error); + return; + } + + // 创建启动台按钮 + _launcherButton = new LauncherButton(this); + var buttonElement = _launcherButton.Element; + + // 查找浮动栏 + var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel; + if (floatingBar == null) + { + // 如果直接查找失败,则尝试遍历可视树查找 + Panel floatingBarPanelFromTree = null; + FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree); + floatingBar = floatingBarPanelFromTree; + } + + if (floatingBar == null) + { + LogHelper.WriteLogToFile("未找到浮动栏,无法添加启动台按钮", LogHelper.LogType.Error); + return; + } + + // 添加启动台按钮到浮动栏 + if (Config.ButtonPosition == LauncherButtonPosition.Left) + { + floatingBar.Children.Insert(0, buttonElement); + LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏左侧", LogHelper.LogType.Info); + } + else + { + floatingBar.Children.Add(buttonElement); + LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏右侧", LogHelper.LogType.Info); + } + + _isAddedToFloatingBar = true; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"添加启动台按钮到浮动栏时出错: {ex.Message}", LogHelper.LogType.Error); + LogHelper.NewLog(ex); + } + } + + /// + /// 递归查找StackPanelFloatingBar + /// + private void FindStackPanelFloatingBar(DependencyObject parent, ref Panel result) + { + if (parent == null || result != null) return; + + try + { + // 检查当前对象是否为我们要找的面板 + if (parent is Panel panel && panel.Name == "StackPanelFloatingBar") + { + result = panel; + return; + } + + // 获取子元素数量 + int childCount = VisualTreeHelper.GetChildrenCount(parent); + + // 遍历所有子元素 + for (int i = 0; i < childCount; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(parent, i); + FindStackPanelFloatingBar(child, ref result); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"查找StackPanelFloatingBar时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 从浮动栏移除启动台按钮 + /// + private void RemoveLauncherButtonFromFloatingBar() + { + try + { + if (!_isAddedToFloatingBar || _launcherButton == null) + { + return; + } + + // 获取主窗口实例 + var mainWindow = Application.Current.MainWindow; + if (mainWindow == null) + { + LogHelper.WriteLogToFile("未找到主窗口实例,无法移除启动台按钮", LogHelper.LogType.Error); + return; + } + + // 获取按钮元素 + var buttonElement = _launcherButton.Element; + + // 查找浮动栏 + var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel; + if (floatingBar == null) + { + // 如果直接查找失败,则尝试遍历可视树查找 + Panel floatingBarPanelFromTree = null; + FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree); + floatingBar = floatingBarPanelFromTree; + } + + if (floatingBar == null) + { + LogHelper.WriteLogToFile("未找到浮动栏,无法移除启动台按钮", LogHelper.LogType.Error); + return; + } + + // 从浮动栏移除启动台按钮 + if (floatingBar.Children.Contains(buttonElement)) + { + floatingBar.Children.Remove(buttonElement); + LogHelper.WriteLogToFile("启动台按钮已从浮动栏移除", LogHelper.LogType.Info); + } + + _isAddedToFloatingBar = false; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"移除启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error); + LogHelper.NewLog(ex); + } + } + + /// + /// 更新启动台按钮位置 + /// + public void UpdateButtonPosition() + { + try + { + // 如果按钮已添加到浮动栏,重新添加以更新位置 + if (_isAddedToFloatingBar) + { + RemoveLauncherButtonFromFloatingBar(); + AddLauncherButtonToFloatingBar(); + LogHelper.WriteLogToFile($"启动台按钮位置已更新为: {Config.ButtonPosition}", LogHelper.LogType.Info); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error); + LogHelper.NewLog(ex); + } + } + + #endregion + + #region 启动台功能 + + /// + /// 显示启动台窗口 + /// + /// 按钮在屏幕上的位置 + public void ShowLauncherWindow(Point buttonPosition) + { + try + { + // 如果窗口已存在,关闭它 + if (_launcherWindow != null && _launcherWindow.IsVisible) + { + _launcherWindow.Close(); + _launcherWindow = null; + return; + } + + // 创建新的启动台窗口 + _launcherWindow = new LauncherWindow(this); + + // 计算窗口位置,使其位于按钮上方 + PositionLauncherWindow(_launcherWindow, buttonPosition); + + // 显示窗口 + _launcherWindow.Show(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"显示启动台窗口时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 设置启动台窗口位置 + /// + /// 启动台窗口 + /// 按钮在屏幕上的位置 + private void PositionLauncherWindow(LauncherWindow window, Point buttonPosition) + { + // 确保窗口已加载 + if (window.ActualWidth == 0 || window.ActualHeight == 0) + { + window.WindowStartupLocation = WindowStartupLocation.CenterScreen; + + // 设置窗口加载完成后的位置 + window.Loaded += (s, e) => + { + // 窗口位于按钮上方居中 + double left = buttonPosition.X - (window.ActualWidth / 2); + double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距 + + // 确保窗口在屏幕内 + left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth)); + top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight)); + + window.Left = left; + window.Top = top; + }; + } + else + { + // 窗口位于按钮上方居中 + double left = buttonPosition.X - (window.ActualWidth / 2); + double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距 + + // 确保窗口在屏幕内 + left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth)); + top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight)); + + window.Left = left; + window.Top = top; + } + } + + /// + /// 添加应用到启动台 + /// + /// 启动台项 + public void AddLauncherItem(LauncherItem item) + { + // 如果项目数量已达上限,则不添加 + if (LauncherItems.Count >= 40) + { + MessageBox.Show("启动台项目数量已达上限(40个)!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + // 寻找合适的位置 + if (item.Position < 0) + { + item.Position = FindNextAvailablePosition(); + } + + // 添加项目并保存配置 + LauncherItems.Add(item); + SaveConfig(); + } + + /// + /// 查找下一个可用位置 + /// + private int FindNextAvailablePosition() + { + // 获取已使用的位置列表 + var usedPositions = new HashSet(); + foreach (var item in LauncherItems) + { + usedPositions.Add(item.Position); + } + + // 查找第一个可用位置 + for (int i = 0; i < 40; i++) + { + if (!usedPositions.Contains(i)) + { + return i; + } + } + + // 如果所有位置都已使用,则返回0 + return 0; + } + + #endregion + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IPlugin.cs b/Ink Canvas/Helpers/Plugins/IPlugin.cs new file mode 100644 index 00000000..be55f8af --- /dev/null +++ b/Ink Canvas/Helpers/Plugins/IPlugin.cs @@ -0,0 +1,68 @@ +using System; +using System.Windows; +using System.Windows.Controls; + +namespace Ink_Canvas.Helpers.Plugins +{ + /// + /// 定义插件的基本接口 + /// + public interface IPlugin + { + /// + /// 插件名称 + /// + string Name { get; } + + /// + /// 插件描述 + /// + string Description { get; } + + /// + /// 插件版本 + /// + Version Version { get; } + + /// + /// 插件作者 + /// + string Author { get; } + + /// + /// 是否为内置插件 + /// + bool IsBuiltIn { get; } + + /// + /// 初始化插件 + /// 此方法在插件加载时被调用,用于执行一些初始化工作 + /// + void Initialize(); + + /// + /// 启用插件 + /// 此方法在插件被用户或系统启用时调用,激活插件功能 + /// + void Enable(); + + /// + /// 禁用插件 + /// 此方法在插件被用户或系统禁用时调用,停用插件功能 + /// + void Disable(); + + /// + /// 获取插件设置界面 + /// 此方法返回插件的设置界面控件,用于展示在设置窗口 + /// + /// 插件设置界面 + UserControl GetSettingsView(); + + /// + /// 插件卸载时的清理工作 + /// 此方法在插件被卸载前调用,用于释放资源和执行清理 + /// + void Cleanup(); + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginBase.cs b/Ink Canvas/Helpers/Plugins/PluginBase.cs new file mode 100644 index 00000000..6fee53ab --- /dev/null +++ b/Ink Canvas/Helpers/Plugins/PluginBase.cs @@ -0,0 +1,144 @@ +using System; +using System.Windows.Controls; + +namespace Ink_Canvas.Helpers.Plugins +{ + /// + /// 插件基类,提供基本实现 + /// + public abstract class PluginBase : IPlugin + { + /// + /// 插件状态(私有字段) + /// + private bool _isEnabled = false; + + /// + /// 插件状态(公共属性) + /// + public bool IsEnabled + { + get => _isEnabled; + protected set + { + if (_isEnabled != value) + { + _isEnabled = value; + OnEnabledStateChanged(value); + } + } + } + + /// + /// 插件ID + /// + public string Id { get; protected set; } + + /// + /// 插件路径 + /// + public string PluginPath { get; set; } + + /// + /// 插件名称 + /// + public abstract string Name { get; } + + /// + /// 插件描述 + /// + public abstract string Description { get; } + + /// + /// 插件版本 + /// + public abstract Version Version { get; } + + /// + /// 插件作者 + /// + public abstract string Author { get; } + + /// + /// 是否为内置插件 + /// + public virtual bool IsBuiltIn => false; + + /// + /// 状态变更事件 + /// + public event EventHandler EnabledStateChanged; + + /// + /// 初始化插件 + /// + public virtual void Initialize() + { + Id = GetType().FullName; + LogHelper.WriteLogToFile($"插件 {Name} 已初始化", LogHelper.LogType.Info); + } + + /// + /// 启用插件 + /// + public virtual void Enable() + { + if (!IsEnabled) + { + IsEnabled = true; + LogHelper.WriteLogToFile($"插件 {Name} 已启用", LogHelper.LogType.Info); + } + } + + /// + /// 禁用插件 + /// + public virtual void Disable() + { + if (IsEnabled) + { + IsEnabled = false; + LogHelper.WriteLogToFile($"插件 {Name} 已禁用", LogHelper.LogType.Info); + } + } + + /// + /// 获取插件设置界面 + /// + /// 插件设置界面 + public virtual UserControl GetSettingsView() + { + // 默认返回空设置页面 + return new UserControl(); + } + + /// + /// 插件卸载时的清理工作 + /// + public virtual void Cleanup() + { + LogHelper.WriteLogToFile($"插件 {Name} 已卸载", LogHelper.LogType.Info); + } + + /// + /// 保存插件自身的设置 + /// 注意:此方法仅用于保存插件的特定设置,不应影响插件启用/禁用状态 + /// 插件启用状态由PluginManager统一管理 + /// + public virtual void SavePluginSettings() + { + // 默认实现不做任何事情 + // 子类可以重写此方法,将自身设置保存到配置文件中 + LogHelper.WriteLogToFile($"插件 {Name} 设置已保存", LogHelper.LogType.Event); + } + + /// + /// 触发状态变更事件 + /// + /// 是否启用 + protected virtual void OnEnabledStateChanged(bool isEnabled) + { + EnabledStateChanged?.Invoke(this, isEnabled); + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginManager.cs b/Ink Canvas/Helpers/Plugins/PluginManager.cs new file mode 100644 index 00000000..e76322aa --- /dev/null +++ b/Ink Canvas/Helpers/Plugins/PluginManager.cs @@ -0,0 +1,1462 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; + +namespace Ink_Canvas.Helpers.Plugins +{ + /// + /// 插件管理器,负责插件的加载、卸载和管理 + /// + public class PluginManager + { + private static readonly string PluginsDirectory = Path.Combine(App.RootPath, "Plugins"); + private static readonly string PluginConfigFile = Path.Combine(App.RootPath, "PluginConfig.json"); + private static readonly string PluginConfigBackupFile = Path.Combine(App.RootPath, "PluginConfig.json.bak"); + + private static PluginManager _instance; + private static SemaphoreSlim _configLock = new SemaphoreSlim(1, 1); + + /// + /// 插件管理器单例 + /// + public static PluginManager Instance + { + get + { + if (_instance == null) + { + _instance = new PluginManager(); + } + return _instance; + } + } + + /// + /// 已加载的插件集合 + /// + public ObservableCollection Plugins { get; } = new ObservableCollection(); + + /// + /// 插件配置信息 + /// + public Dictionary PluginStates { get; private set; } = new Dictionary(); + + /// + /// 配置是否已更改但未保存 + /// + private bool _configDirty = false; + + /// + /// 配置自动保存计时器 + /// + private System.Timers.Timer _autoSaveTimer; + + /// + /// 加载的程序集缓存 + /// + private Dictionary _loadedAssemblies = new Dictionary(); + + /// + /// 插件文件哈希缓存,用于热重载检测 + /// + private Dictionary _pluginHashes = new Dictionary(); + + private PluginManager() + { + // 确保插件目录存在 + if (!Directory.Exists(PluginsDirectory)) + { + Directory.CreateDirectory(PluginsDirectory); + } + + // 加载插件配置 + LoadConfig(); + + // 初始化自动保存计时器(3秒) + _autoSaveTimer = new System.Timers.Timer(3000); + _autoSaveTimer.Elapsed += (s, e) => + { + if (_configDirty) + { + SaveConfigAsync().ConfigureAwait(false); + } + }; + _autoSaveTimer.AutoReset = false; + + // 注册插件状态变更事件处理 + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + // 应用退出时强制保存配置 + if (_configDirty) + { + SaveConfig(); + } + }; + } + + /// + /// 初始化插件系统 + /// + public void Initialize() + { + try + { + LogHelper.WriteLogToFile("开始初始化插件系统", LogHelper.LogType.Info); + + // 加载配置 + LoadConfig(); + LogHelper.WriteLogToFile($"已从配置文件加载 {PluginStates.Count} 个插件状态记录", LogHelper.LogType.Info); + + // 加载内置插件 + LogHelper.WriteLogToFile("正在加载内置插件...", LogHelper.LogType.Info); + LoadBuiltInPlugins(); + + // 加载外部插件 + LogHelper.WriteLogToFile("正在加载外部插件...", LogHelper.LogType.Info); + LoadExternalPlugins(); + + // 启用已配置为启用的插件 + LogHelper.WriteLogToFile("正在应用配置的插件状态...", LogHelper.LogType.Info); + EnableConfiguredPlugins(); + + // 设置定期检查热重载 + StartHotReloadWatcher(); + + // 保存初始化后的配置(可能有新插件) + SaveConfig(); + + LogHelper.WriteLogToFile($"插件系统初始化完成,共加载 {Plugins.Count} 个插件", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"初始化插件系统时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 加载内置插件 + /// + private void LoadBuiltInPlugins() + { + try + { + // 获取当前程序集 + Assembly currentAssembly = Assembly.GetExecutingAssembly(); + + // 查找实现了IPlugin接口的所有类型 + var pluginTypes = currentAssembly.GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass); + + foreach (var pluginType in pluginTypes) + { + try + { + // 创建插件实例 + IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType); + + // 只处理内置插件 + if (plugin.IsBuiltIn) + { + plugin.Initialize(); + Plugins.Add(plugin); + LogHelper.WriteLogToFile($"已加载内置插件: {plugin.Name} v{plugin.Version}", LogHelper.LogType.Info); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载内置插件 {pluginType.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载内置插件时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 加载外部插件 + /// + private void LoadExternalPlugins() + { + try + { + // 检查插件目录是否存在 + if (!Directory.Exists(PluginsDirectory)) + { + Directory.CreateDirectory(PluginsDirectory); + return; + } + + // 获取所有插件文件 + var pluginFiles = Directory.GetFiles(PluginsDirectory, "*.iccpp", SearchOption.TopDirectoryOnly); + + foreach (var pluginFile in pluginFiles) + { + LoadExternalPlugin(pluginFile); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载外部插件时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 加载单个外部插件 + /// + /// 插件文件路径 + /// 加载的插件实例,加载失败则返回null + public IPlugin LoadExternalPlugin(string pluginPath) + { + try + { + // 计算文件哈希 + string fileHash = CalculateFileHash(pluginPath); + _pluginHashes[pluginPath] = fileHash; + + // 加载插件程序集 + Assembly pluginAssembly = LoadPluginAssembly(pluginPath); + if (pluginAssembly == null) return null; + + // 查找实现了IPlugin接口的类型 + var pluginTypes = pluginAssembly.GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && t.IsClass); + + foreach (var pluginType in pluginTypes) + { + try + { + // 创建插件实例 + IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType); + + // 设置插件路径 + if (plugin is PluginBase pluginBase) + { + pluginBase.PluginPath = pluginPath; + } + + plugin.Initialize(); + Plugins.Add(plugin); + + LogHelper.WriteLogToFile($"已加载外部插件: {plugin.Name} v{plugin.Version} 来自 {Path.GetFileName(pluginPath)}", + LogHelper.LogType.Info); + + return plugin; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"实例化插件 {pluginType.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载插件 {Path.GetFileName(pluginPath)} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + + return null; + } + + /// + /// 加载插件程序集 + /// + /// 插件文件路径 + /// 加载的程序集 + private Assembly LoadPluginAssembly(string pluginPath) + { + try + { + // 检查是否已加载该程序集 + if (_loadedAssemblies.TryGetValue(pluginPath, out var loadedAssembly)) + { + return loadedAssembly; + } + + // 加载程序集 + Assembly pluginAssembly = Assembly.LoadFrom(pluginPath); + _loadedAssemblies[pluginPath] = pluginAssembly; + + return pluginAssembly; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载插件程序集 {Path.GetFileName(pluginPath)} 时出错: {ex.Message}", LogHelper.LogType.Error); + return null; + } + } + + /// + /// 启用已配置为启用的插件 + /// + private void EnableConfiguredPlugins() + { + int enabledCount = 0; + int disabledCount = 0; + int errorCount = 0; + + foreach (var plugin in Plugins) + { + try + { + string pluginTypeName = plugin.GetType().FullName; + + // 检查配置中的插件状态 + if (PluginStates.TryGetValue(pluginTypeName, out bool enabled)) + { + // 获取当前实际状态 + bool currentState = plugin is PluginBase pluginBase && pluginBase.IsEnabled; + + // 如果配置状态与当前状态不一致,则应用配置状态 + if (currentState != enabled) + { + // 注册插件状态变更事件 + if (plugin is PluginBase pb) + { + pb.EnabledStateChanged += Plugin_EnabledStateChanged; + } + + if (enabled) + { + plugin.Enable(); + enabledCount++; + LogHelper.WriteLogToFile($"根据配置启用插件: {plugin.Name}", LogHelper.LogType.Info); + } + else + { + plugin.Disable(); + disabledCount++; + LogHelper.WriteLogToFile($"根据配置禁用插件: {plugin.Name}", LogHelper.LogType.Info); + } + } + else + { + // 状态一致,只注册事件 + if (plugin is PluginBase pb) + { + pb.EnabledStateChanged += Plugin_EnabledStateChanged; + } + } + } + else + { + // 插件不在配置中,添加默认状态(禁用) + PluginStates[pluginTypeName] = false; + _configDirty = true; + + // 注册插件状态变更事件 + if (plugin is PluginBase pb) + { + pb.EnabledStateChanged += Plugin_EnabledStateChanged; + } + + // 如果当前是启用状态,则禁用 + if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) + { + plugin.Disable(); + disabledCount++; + LogHelper.WriteLogToFile($"插件不在配置中,默认禁用: {plugin.Name}", LogHelper.LogType.Info); + } + } + } + catch (Exception ex) + { + errorCount++; + LogHelper.WriteLogToFile($"应用插件 {plugin.Name} 配置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + // 如果有配置变更,启动自动保存 + if (_configDirty) + { + TriggerAutoSave(); + } + + LogHelper.WriteLogToFile($"已应用插件配置: 启用 {enabledCount} 个,禁用 {disabledCount} 个,错误 {errorCount} 个", LogHelper.LogType.Info); + } + + /// + /// 插件状态变更事件处理 + /// + private void Plugin_EnabledStateChanged(object sender, bool isEnabled) + { + try + { + if (sender is IPlugin plugin) + { + string pluginTypeName = plugin.GetType().FullName; + + // 更新配置状态 + if (!PluginStates.ContainsKey(pluginTypeName) || PluginStates[pluginTypeName] != isEnabled) + { + PluginStates[pluginTypeName] = isEnabled; + _configDirty = true; + + LogHelper.WriteLogToFile($"插件状态变更: {plugin.Name} = {(isEnabled ? "启用" : "禁用")}", LogHelper.LogType.Info); + + // 触发自动保存 + TriggerAutoSave(); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"处理插件状态变更事件时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 触发自动保存计时器 + /// + private void TriggerAutoSave() + { + // 重置并启动计时器 + _autoSaveTimer.Stop(); + _autoSaveTimer.Start(); + } + + /// + /// 启动热重载监视器 + /// + private void StartHotReloadWatcher() + { + // 创建定时检查任务 + Task.Run(async () => + { + while (true) + { + try + { + // 每5秒检查一次 + await Task.Delay(5000); + + // 获取所有外部插件 + var externalPlugins = Plugins.OfType() + .Where(p => !p.IsBuiltIn && !string.IsNullOrEmpty(p.PluginPath)) + .ToList(); + + foreach (var plugin in externalPlugins) + { + // 检查插件文件是否存在 + if (!File.Exists(plugin.PluginPath)) + { + continue; + } + + // 计算当前文件哈希 + string currentHash = CalculateFileHash(plugin.PluginPath); + + // 比较哈希值是否变化 + if (_pluginHashes.TryGetValue(plugin.PluginPath, out string oldHash) && + !string.IsNullOrEmpty(oldHash) && + oldHash != currentHash) + { + // 文件已变化,执行热重载 + Application.Current.Dispatcher.Invoke(() => + { + ReloadPlugin(plugin); + }); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"热重载检查出错: {ex.Message}", LogHelper.LogType.Error); + } + } + }); + } + + /// + /// 重新加载插件 + /// + /// 要重新加载的插件 + private void ReloadPlugin(PluginBase plugin) + { + try + { + LogHelper.WriteLogToFile($"开始重载插件: {plugin.Name}", LogHelper.LogType.Info); + + // 记录插件状态和信息 + bool wasEnabled = plugin.IsEnabled; + string pluginPath = plugin.PluginPath; + string pluginTypeName = plugin.GetType().FullName; + + // 记录日志,方便排查问题 + LogHelper.WriteLogToFile($"重载前插件状态 - 类型: {pluginTypeName}, 状态: {(wasEnabled ? "启用" : "禁用")}", LogHelper.LogType.Info); + + // 如果配置中有该插件的状态,记录配置中的状态 + if (PluginStates.TryGetValue(pluginTypeName, out bool currentConfigState)) + { + LogHelper.WriteLogToFile($"配置中插件状态: {(currentConfigState ? "启用" : "禁用")}", LogHelper.LogType.Info); + } + + // 卸载插件,但不从PluginStates中移除状态信息 + UnloadPlugin(plugin); + + // 清除程序集缓存,确保加载最新版本 + _loadedAssemblies.Remove(pluginPath); + + // 重新加载插件 + IPlugin newPlugin = LoadExternalPlugin(pluginPath); + + if (newPlugin != null) + { + // 更新配置中的插件状态 + string newPluginTypeName = newPlugin.GetType().FullName; + + // 如果插件类型名称变化,需要更新配置 + if (newPluginTypeName != pluginTypeName && PluginStates.ContainsKey(pluginTypeName)) + { + bool state = PluginStates[pluginTypeName]; + PluginStates.Remove(pluginTypeName); + PluginStates[newPluginTypeName] = state; + LogHelper.WriteLogToFile($"插件类型名称已变更: {pluginTypeName} -> {newPluginTypeName}, 已更新配置", LogHelper.LogType.Info); + } + + // 应用正确的状态 + bool shouldBeEnabled = false; + if (PluginStates.TryGetValue(newPluginTypeName, out bool storedConfigState)) + { + shouldBeEnabled = storedConfigState; + LogHelper.WriteLogToFile($"从配置获取插件状态: {(shouldBeEnabled ? "启用" : "禁用")}", LogHelper.LogType.Info); + } + else + { + shouldBeEnabled = wasEnabled; + PluginStates[newPluginTypeName] = shouldBeEnabled; + LogHelper.WriteLogToFile($"使用之前的状态: {(shouldBeEnabled ? "启用" : "禁用")}", LogHelper.LogType.Info); + } + + // 获取重载后的实际状态 + bool currentState = newPlugin is PluginBase pluginBaseState && pluginBaseState.IsEnabled; + LogHelper.WriteLogToFile($"重载后实际状态: {(currentState ? "启用" : "禁用")}", LogHelper.LogType.Info); + + // 根据应该启用的状态启用或禁用插件 + if (shouldBeEnabled != currentState) + { + if (shouldBeEnabled) + { + newPlugin.Enable(); + LogHelper.WriteLogToFile($"插件 {newPlugin.Name} 已重载并启用", LogHelper.LogType.Info); + } + else + { + newPlugin.Disable(); + LogHelper.WriteLogToFile($"插件 {newPlugin.Name} 已重载并禁用", LogHelper.LogType.Info); + } + + // 检查状态是否正确应用 + currentState = newPlugin is PluginBase reloadedBase && reloadedBase.IsEnabled; + LogHelper.WriteLogToFile($"应用状态后实际状态: {(currentState ? "启用" : "禁用")}", LogHelper.LogType.Info); + + if (currentState != shouldBeEnabled) + { + LogHelper.WriteLogToFile($"警告: 插件状态应用失败,目标状态: {(shouldBeEnabled ? "启用" : "禁用")}, 实际状态: {(currentState ? "启用" : "禁用")}", LogHelper.LogType.Warning); + } + } + else + { + LogHelper.WriteLogToFile($"插件 {newPlugin.Name} 已重载并保持{(shouldBeEnabled ? "启用" : "禁用")}状态", LogHelper.LogType.Info); + } + + // 保存插件设置 + if (newPlugin is PluginBase pluginBaseInstance) + { + try + { + // 保存插件设置(与启用状态无关) + pluginBaseInstance.SavePluginSettings(); + LogHelper.WriteLogToFile($"已保存插件 {newPlugin.Name} 设置", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存插件 {newPlugin.Name} 设置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + // 立即保存配置 + LogHelper.WriteLogToFile($"重载后保存插件配置...", LogHelper.LogType.Info); + SaveConfig(); + } + else + { + LogHelper.WriteLogToFile($"插件 {plugin.Name} 重载失败: 无法加载新插件", LogHelper.LogType.Error); + } + + // 更新UI + NotifyUIRefresh(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"重新加载插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 卸载插件 + /// + /// 要卸载的插件 + /// 是否从配置中移除插件状态(默认为false) + public void UnloadPlugin(IPlugin plugin, bool removeFromConfig = false) + { + try + { + // 如果插件已启用,先禁用它 + if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) + { + plugin.Disable(); + } + + // 执行插件清理 + plugin.Cleanup(); + + // 从插件集合中移除 + Plugins.Remove(plugin); + + // 从配置中移除(如果需要) + if (removeFromConfig && plugin.GetType() != null) + { + string pluginTypeName = plugin.GetType().FullName; + if (PluginStates.ContainsKey(pluginTypeName)) + { + PluginStates.Remove(pluginTypeName); + SaveConfig(); + } + } + + LogHelper.WriteLogToFile($"已卸载插件: {plugin.Name}", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"卸载插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 删除插件 + /// + /// 要删除的插件 + /// 删除是否成功 + public bool DeletePlugin(IPlugin plugin) + { + try + { + // 只能删除外部插件 + if (plugin.IsBuiltIn) + { + return false; + } + + // 获取插件路径 + string pluginPath = null; + if (plugin is PluginBase pluginBase) + { + pluginPath = pluginBase.PluginPath; + } + + if (string.IsNullOrEmpty(pluginPath) || !File.Exists(pluginPath)) + { + return false; + } + + // 卸载插件(并从配置中移除状态) + UnloadPlugin(plugin, true); + + // 删除插件文件 + File.Delete(pluginPath); + + // 清理缓存 + _loadedAssemblies.Remove(pluginPath); + _pluginHashes.Remove(pluginPath); + + // 保存配置 + SaveConfig(); + + LogHelper.WriteLogToFile($"已删除插件: {plugin.Name}", LogHelper.LogType.Info); + return true; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"删除插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + + /// + /// 切换插件启用状态 + /// + /// 目标插件 + /// 是否启用 + public void TogglePlugin(IPlugin plugin, bool enable) + { + try + { + // 检查当前状态是否已经是目标状态 + bool currentState = plugin is PluginBase pluginBase && pluginBase.IsEnabled; + if (currentState == enable) + { + // 已经是目标状态,无需操作 + LogHelper.WriteLogToFile($"插件 {plugin.Name} 已经是 {(enable ? "启用" : "禁用")} 状态,无需切换", LogHelper.LogType.Info); + return; + } + + // 记录插件信息,用于日志 + string pluginName = plugin.Name; + string pluginTypeName = plugin.GetType().FullName; + + LogHelper.WriteLogToFile($"开始切换插件 {pluginName} 状态为: {(enable ? "启用" : "禁用")}", LogHelper.LogType.Info); + + // 首先更新配置状态 + PluginStates[pluginTypeName] = enable; + _configDirty = true; + + // 更新插件状态 + try + { + // 注册事件(无需检查事件是否为null) + if (plugin is PluginBase pb) + { + // 先取消可能已有的订阅,避免重复订阅 + pb.EnabledStateChanged -= Plugin_EnabledStateChanged; + // 重新订阅 + pb.EnabledStateChanged += Plugin_EnabledStateChanged; + } + + // 更新插件状态 + if (enable) + { + plugin.Enable(); + LogHelper.WriteLogToFile($"插件 {pluginName} 已启用", LogHelper.LogType.Info); + } + else + { + // 禁用前先记录是否为内置插件 + bool isBuiltIn = plugin.IsBuiltIn; + LogHelper.WriteLogToFile($"尝试禁用{(isBuiltIn ? "内置" : "外部")}插件 {pluginName}", LogHelper.LogType.Info); + + // 禁用插件 + plugin.Disable(); + + // 禁用后立即检查状态,确保禁用成功 + bool actuallyDisabled = !(plugin is PluginBase pb2 && pb2.IsEnabled); + if (!actuallyDisabled) + { + LogHelper.WriteLogToFile($"警告: 插件 {pluginName} 禁用失败,再次尝试禁用", LogHelper.LogType.Warning); + plugin.Disable(); // 再次尝试禁用 + + // 再次检查 + actuallyDisabled = !(plugin is PluginBase pb3 && pb3.IsEnabled); + if (!actuallyDisabled) + { + LogHelper.WriteLogToFile($"错误: 插件 {pluginName} 禁用失败,强制设置禁用状态", LogHelper.LogType.Error); + // 强制设置状态 + if (plugin is PluginBase pb4) + { + // 使用反射强制设置禁用状态 + var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); + if (enabledProperty != null) + { + enabledProperty.SetValue(pb4, false); + LogHelper.WriteLogToFile($"已通过反射强制设置插件 {pluginName} 为禁用状态", LogHelper.LogType.Info); + } + } + } + } + + LogHelper.WriteLogToFile($"插件 {pluginName} 已禁用", LogHelper.LogType.Info); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"更改插件 {pluginName} 状态时出错: {ex.Message}", LogHelper.LogType.Error); + } + + // 立即保存配置 + SaveConfigAsync().ConfigureAwait(false); + + // 插件状态切换后,始终进行重载(无论是启用还是禁用) + if (plugin is PluginBase pluginInstance) + { + // 对于内置插件,执行专门的处理 + if (pluginInstance.IsBuiltIn) + { + LogHelper.WriteLogToFile($"处理内置插件 {pluginName} 状态变更", LogHelper.LogType.Info); + + // 对于内置插件,我们需要确保状态正确应用 + bool finalState = pluginInstance.IsEnabled; + bool expectedState = enable; + + if (finalState != expectedState) + { + LogHelper.WriteLogToFile($"内置插件状态不匹配: 当前={finalState}, 期望={expectedState},尝试纠正", LogHelper.LogType.Warning); + + // 再次尝试设置状态 + if (expectedState) + { + plugin.Enable(); + } + else + { + plugin.Disable(); + + // 最后一次检查,如果仍然不匹配,强制设置 + if (pluginInstance.IsEnabled != expectedState) + { + var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); + if (enabledProperty != null) + { + enabledProperty.SetValue(pluginInstance, expectedState); + LogHelper.WriteLogToFile($"已通过反射强制设置内置插件 {pluginName} 状态为 {(expectedState ? "启用" : "禁用")}", LogHelper.LogType.Info); + } + } + } + } + + // 通知UI刷新 + NotifyUIRefresh(); + } + else + { + // 外部插件,执行热重载 + try + { + if (!string.IsNullOrEmpty(pluginInstance.PluginPath) && File.Exists(pluginInstance.PluginPath)) + { + LogHelper.WriteLogToFile($"开始重载外部插件 {pluginName}", LogHelper.LogType.Info); + + // 使用调度器确保在UI线程执行热重载 + if (Application.Current != null && Application.Current.Dispatcher != null) + { + Application.Current.Dispatcher.BeginInvoke(new Action(() => + { + ReloadPlugin(pluginInstance); + LogHelper.WriteLogToFile($"插件 {pluginName} 已重载以应用{(enable ? "启用" : "禁用")}状态", LogHelper.LogType.Info); + })); + } + else + { + // 当前不在UI线程,直接重载 + ReloadPlugin(pluginInstance); + LogHelper.WriteLogToFile($"插件 {pluginName} 已重载以应用{(enable ? "启用" : "禁用")}状态", LogHelper.LogType.Info); + } + } + else + { + LogHelper.WriteLogToFile($"外部插件 {pluginName} 文件不存在,无法重载", LogHelper.LogType.Warning); + NotifyUIRefresh(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"重载插件 {pluginName} 时出错: {ex.Message}", LogHelper.LogType.Error); + // 出错时也要刷新UI + NotifyUIRefresh(); + } + } + } + else + { + // 通知UI刷新 + NotifyUIRefresh(); + } + + LogHelper.WriteLogToFile($"插件 {pluginName} 状态切换完成", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"切换插件状态时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 应用插件实时状态 + /// + /// 目标插件 + /// 是否启用 + private void ApplyPluginRealTimeState(IPlugin plugin, bool enable) + { + try + { + // 确保当前实例状态正确 + bool currentState = plugin is PluginBase pluginBase && pluginBase.IsEnabled; + if (currentState != enable) + { + if (enable) + { + plugin.Enable(); + LogHelper.WriteLogToFile($"实时应用: 已启用插件 {plugin.Name}", LogHelper.LogType.Info); + } + else + { + plugin.Disable(); + LogHelper.WriteLogToFile($"实时应用: 已禁用插件 {plugin.Name}", LogHelper.LogType.Info); + } + + // 同步状态到插件自身的配置 + if (plugin is PluginBase pluginSettings) + { + try + { + // 保存插件设置(与启用状态无关) + pluginSettings.SavePluginSettings(); + LogHelper.WriteLogToFile($"实时应用: 已保存插件 {plugin.Name} 设置", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"实时应用: 保存插件 {plugin.Name} 设置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + } + + // 对于外部插件,尝试执行热重载以确保状态立即生效 + if (plugin is PluginBase externalPlugin && !externalPlugin.IsBuiltIn) + { + string pluginPath = externalPlugin.PluginPath; + if (!string.IsNullOrEmpty(pluginPath) && File.Exists(pluginPath)) + { + // 记录插件类型名称,用于后续状态检查 + string pluginTypeName = plugin.GetType().FullName; + bool targetState = enable; + + // 使用调度器确保在UI线程执行热重载 + if (Application.Current != null && Application.Current.Dispatcher != null) + { + Application.Current.Dispatcher.BeginInvoke(new Action(() => + { + try + { + // 热重载前再次确认配置状态正确 + if (PluginStates.TryGetValue(pluginTypeName, out bool storedStateUi) && storedStateUi != targetState) + { + LogHelper.WriteLogToFile($"热重载前发现状态不一致,修正配置: {plugin.Name}, 配置={storedStateUi}, 目标={targetState}", LogHelper.LogType.Warning); + PluginStates[pluginTypeName] = targetState; + SaveConfig(); + } + + // 执行热重载 + ReloadPlugin(externalPlugin); + LogHelper.WriteLogToFile($"插件 {plugin.Name} 已成功热重载以应用实时状态", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"热重载插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + })); + } + else + { + // 当前不在UI线程,直接重载 + // 热重载前再次确认配置状态正确 + if (PluginStates.TryGetValue(pluginTypeName, out bool storedStateNonUi) && storedStateNonUi != targetState) + { + LogHelper.WriteLogToFile($"热重载前发现状态不一致,修正配置: {plugin.Name}, 配置={storedStateNonUi}, 目标={targetState}", LogHelper.LogType.Warning); + PluginStates[pluginTypeName] = targetState; + SaveConfig(); + } + + ReloadPlugin(externalPlugin); + } + } + } + + LogHelper.WriteLogToFile($"插件 {plugin.Name} 实时状态已应用: {(enable ? "启用" : "禁用")}", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"应用插件实时状态时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 通知UI刷新 + /// + private void NotifyUIRefresh() + { + try + { + // 通知UI刷新 + if (Application.Current != null && Application.Current.Dispatcher != null) + { + Application.Current.Dispatcher.BeginInvoke(new Action(() => { + // 通知任何可能打开的插件设置窗口刷新 + foreach (Window window in Application.Current.Windows) + { + if (window is Windows.PluginSettingsWindow pluginWindow) + { + pluginWindow.RefreshPluginList(); + break; + } + } + })); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"通知UI刷新时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 加载插件配置 + /// + private void LoadConfig() + { + const int maxRetries = 3; // 最大重试次数 + const int retryDelayMs = 300; // 重试延迟时间(毫秒) + + LogHelper.WriteLogToFile($"开始从配置文件加载插件状态: {PluginConfigFile}", LogHelper.LogType.Info); + + // 确保至少有一个默认配置 + Dictionary defaultConfig = new Dictionary(); + + // 尝试获取配置锁 + _configLock.Wait(); + + try + { + for (int attempt = 1; attempt <= maxRetries; attempt++) + { + try + { + if (File.Exists(PluginConfigFile)) + { + string json; + // 使用共享读取模式,允许其他进程同时读取但不允许写入 + using (FileStream fs = new FileStream(PluginConfigFile, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (StreamReader reader = new StreamReader(fs)) + { + json = reader.ReadToEnd(); + } + + var loadedStates = Newtonsoft.Json.JsonConvert.DeserializeObject>(json); + + if (loadedStates != null && loadedStates.Count > 0) + { + PluginStates = loadedStates; + _configDirty = false; // 重置脏标记 + LogHelper.WriteLogToFile($"成功从配置文件加载了 {PluginStates.Count} 个插件状态", LogHelper.LogType.Info); + return; // 成功加载,提前退出 + } + else + { + LogHelper.WriteLogToFile("配置文件解析为空,尝试使用备份", LogHelper.LogType.Warning); + // 尝试加载备份 + if (File.Exists(PluginConfigBackupFile)) + { + try + { + string backupJson = File.ReadAllText(PluginConfigBackupFile); + var backupStates = Newtonsoft.Json.JsonConvert.DeserializeObject>(backupJson); + + if (backupStates != null && backupStates.Count > 0) + { + PluginStates = backupStates; + _configDirty = true; // 从备份加载,需要重新保存主配置 + LogHelper.WriteLogToFile($"已从备份恢复 {PluginStates.Count} 个插件状态", LogHelper.LogType.Info); + return; // 成功从备份加载,提前退出 + } + } + catch (Exception backupEx) + { + LogHelper.WriteLogToFile($"从备份恢复配置失败: {backupEx.Message}", LogHelper.LogType.Error); + } + } + + // 备份也失败,使用默认配置 + PluginStates = defaultConfig; + _configDirty = true; + } + } + else + { + LogHelper.WriteLogToFile($"配置文件不存在,尝试使用备份: {PluginConfigFile}", LogHelper.LogType.Warning); + + // 尝试加载备份 + if (File.Exists(PluginConfigBackupFile)) + { + try + { + string backupJson = File.ReadAllText(PluginConfigBackupFile); + var backupStates = Newtonsoft.Json.JsonConvert.DeserializeObject>(backupJson); + + if (backupStates != null && backupStates.Count > 0) + { + PluginStates = backupStates; + _configDirty = true; // 从备份加载,需要重新保存主配置 + LogHelper.WriteLogToFile($"已从备份恢复 {PluginStates.Count} 个插件状态", LogHelper.LogType.Info); + return; // 成功从备份加载,提前退出 + } + } + catch (Exception backupEx) + { + LogHelper.WriteLogToFile($"从备份恢复配置失败: {backupEx.Message}", LogHelper.LogType.Error); + } + } + + PluginStates = defaultConfig; + _configDirty = true; + LogHelper.WriteLogToFile("使用默认空配置", LogHelper.LogType.Warning); + } + + // 没有成功加载或使用备份,使用默认配置 + break; + } + catch (Exception ex) + { + if (attempt < maxRetries) + { + LogHelper.WriteLogToFile($"加载配置失败 (尝试 {attempt}/{maxRetries}): {ex.Message},将在 {retryDelayMs}ms 后重试", LogHelper.LogType.Warning); + System.Threading.Thread.Sleep(retryDelayMs); + } + else + { + LogHelper.WriteLogToFile($"加载插件配置失败,已达最大重试次数 ({maxRetries}): {ex.Message}", LogHelper.LogType.Error); + + // 最终失败,使用默认配置 + PluginStates = defaultConfig; + _configDirty = true; + } + } + } + } + finally + { + // 释放配置锁 + _configLock.Release(); + } + } + + /// + /// 异步保存插件配置 + /// + public async Task SaveConfigAsync() + { + // 如果配置没有变化,无需保存 + if (!_configDirty) + { + return; + } + + // 尝试获取配置锁(异步) + if (!await _configLock.WaitAsync(0)) + { + // 已有保存操作在进行中,触发自动保存延迟 + TriggerAutoSave(); + return; + } + + try + { + // 创建配置任务 + await Task.Run(() => SaveConfig()); + } + finally + { + // 释放配置锁 + _configLock.Release(); + } + } + + /// + /// 保存插件配置 + /// + public void SaveConfig() + { + // 如果配置没有变化,无需保存 + if (!_configDirty) + { + return; + } + + const int maxRetries = 3; // 最大重试次数 + const int retryDelayMs = 500; // 重试延迟时间(毫秒) + + try + { + LogHelper.WriteLogToFile($"开始保存插件配置到: {PluginConfigFile}", LogHelper.LogType.Info); + + // 生成JSON数据 + string json = Newtonsoft.Json.JsonConvert.SerializeObject(PluginStates, Newtonsoft.Json.Formatting.Indented); + string tempFile = PluginConfigFile + ".temp"; // 临时文件路径 + + // 确保目录存在 + string configDir = Path.GetDirectoryName(PluginConfigFile); + if (!Directory.Exists(configDir)) + { + Directory.CreateDirectory(configDir); + LogHelper.WriteLogToFile($"创建配置目录: {configDir}", LogHelper.LogType.Info); + } + + // 先备份当前配置 + try + { + if (File.Exists(PluginConfigFile)) + { + File.Copy(PluginConfigFile, PluginConfigBackupFile, true); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"备份配置文件失败: {ex.Message}", LogHelper.LogType.Warning); + } + + for (int attempt = 1; attempt <= maxRetries; attempt++) + { + try + { + // 直接写入目标文件 + File.WriteAllText(PluginConfigFile, json); + + // 验证写入是否成功 + if (File.Exists(PluginConfigFile)) + { + // 重置脏标记 + _configDirty = false; + LogHelper.WriteLogToFile($"插件配置已成功保存到磁盘: {PluginConfigFile}, 共 {PluginStates.Count} 个插件状态", LogHelper.LogType.Info); + return; + } + } + catch (Exception ex) + { + if (attempt < maxRetries) + { + LogHelper.WriteLogToFile($"保存配置失败 (尝试 {attempt}/{maxRetries}): {ex.Message},将在 {retryDelayMs}ms 后重试", LogHelper.LogType.Warning); + System.Threading.Thread.Sleep(retryDelayMs); + } + else + { + LogHelper.WriteLogToFile($"保存插件配置失败,已达最大重试次数 ({maxRetries}): {ex.Message}", LogHelper.LogType.Error); + + // 尝试使用临时文件方式 + try + { + // 删除可能存在的旧临时文件 + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + // 写入临时文件 + File.WriteAllText(tempFile, json); + + // 如果目标文件存在,先删除 + if (File.Exists(PluginConfigFile)) + { + File.Delete(PluginConfigFile); + } + + // 重命名临时文件 + File.Move(tempFile, PluginConfigFile); + + // 重置脏标记 + _configDirty = false; + LogHelper.WriteLogToFile($"使用临时文件方式成功保存配置: {PluginConfigFile}", LogHelper.LogType.Info); + return; + } + catch (Exception fallbackEx) + { + LogHelper.WriteLogToFile($"临时文件保存方式也失败: {fallbackEx.Message}", LogHelper.LogType.Error); + } + } + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存插件配置时发生未处理异常: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 计算文件哈希 + /// + /// 文件路径 + /// 文件哈希值 + private string CalculateFileHash(string filePath) + { + try + { + using (var md5 = MD5.Create()) + using (var stream = File.OpenRead(filePath)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"计算文件哈希值时出错: {ex.Message}", LogHelper.LogType.Error); + return string.Empty; + } + } + + /// + /// 从配置文件重新加载所有插件状态并应用 + /// + public void ReloadPluginsFromConfig() + { + try + { + LogHelper.WriteLogToFile("开始从配置文件重新加载插件状态", LogHelper.LogType.Info); + + // 保存当前配置状态,以便在加载失败时回滚 + Dictionary previousStates = new Dictionary(PluginStates); + + // 重新加载配置文件 + LoadConfig(); + + // 如果配置文件加载失败,PluginStates可能为空,这时使用之前的状态 + if (PluginStates == null || PluginStates.Count == 0) + { + LogHelper.WriteLogToFile("加载的配置为空,恢复到之前的状态", LogHelper.LogType.Warning); + PluginStates = previousStates; + return; + } + + LogHelper.WriteLogToFile($"已加载 {PluginStates.Count} 个插件状态,开始应用...", LogHelper.LogType.Info); + + // 对比配置,查找变更的插件 + foreach (var plugin in Plugins.ToList()) // 创建副本进行遍历,避免集合修改异常 + { + string pluginTypeName = plugin.GetType().FullName; + + // 检查插件在配置中是否存在 + if (PluginStates.TryGetValue(pluginTypeName, out bool shouldBeEnabled)) + { + bool currentlyEnabled = plugin is PluginBase pluginBase && pluginBase.IsEnabled; + + // 如果状态需要变更 + if (currentlyEnabled != shouldBeEnabled) + { + LogHelper.WriteLogToFile($"应用插件 {plugin.Name} 的配置状态: {(shouldBeEnabled ? "启用" : "禁用")}", LogHelper.LogType.Info); + + if (shouldBeEnabled) + { + try + { + plugin.Enable(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"启用插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + else + { + try + { + // 记录禁用信息,特别是内置插件 + bool isBuiltIn = plugin.IsBuiltIn; + LogHelper.WriteLogToFile($"尝试禁用{(isBuiltIn ? "内置" : "外部")}插件 {plugin.Name}", LogHelper.LogType.Info); + + // 禁用插件 + plugin.Disable(); + + // 对于内置插件,特别检查禁用状态 + if (isBuiltIn && plugin is PluginBase builtInPluginBase) + { + if (builtInPluginBase.IsEnabled) + { + LogHelper.WriteLogToFile($"内置插件 {plugin.Name} 禁用失败,尝试强制禁用", LogHelper.LogType.Warning); + // 强制设置禁用状态 + var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); + if (enabledProperty != null) + { + enabledProperty.SetValue(builtInPluginBase, false); + LogHelper.WriteLogToFile($"已通过反射强制禁用内置插件 {plugin.Name}", LogHelper.LogType.Info); + } + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"禁用插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + // 如果是外部插件,执行重载 + if (!plugin.IsBuiltIn && plugin is PluginBase externalPlugin && !string.IsNullOrEmpty(externalPlugin.PluginPath)) + { + try + { + ReloadPlugin(externalPlugin); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"重载外部插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + } + } + else + { + // 插件不在配置中,将其添加为禁用状态 + PluginStates[pluginTypeName] = false; + LogHelper.WriteLogToFile($"插件 {plugin.Name} 不在配置中,默认设置为禁用状态", LogHelper.LogType.Info); + + // 如果当前是启用状态,则禁用它 + if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) + { + try + { + bool isBuiltIn = plugin.IsBuiltIn; + LogHelper.WriteLogToFile($"尝试禁用未配置的{(isBuiltIn ? "内置" : "外部")}插件 {plugin.Name}", LogHelper.LogType.Info); + + plugin.Disable(); + + // 对于内置插件,特别检查禁用状态 + if (isBuiltIn && pluginBase.IsEnabled) + { + LogHelper.WriteLogToFile($"未配置的内置插件 {plugin.Name} 禁用失败,尝试强制禁用", LogHelper.LogType.Warning); + // 强制设置禁用状态 + var enabledProperty = typeof(PluginBase).GetProperty("IsEnabled"); + if (enabledProperty != null) + { + enabledProperty.SetValue(pluginBase, false); + LogHelper.WriteLogToFile($"已通过反射强制禁用未配置的内置插件 {plugin.Name}", LogHelper.LogType.Info); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"禁用未配置插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + } + } + + // 保存更新后的配置 + SaveConfig(); + + // 通知UI更新 + if (Application.Current != null && Application.Current.Dispatcher != null) + { + Application.Current.Dispatcher.Invoke(() => { + // 通知任何可能打开的插件设置窗口刷新 + foreach (Window window in Application.Current.Windows) + { + if (window is Windows.PluginSettingsWindow pluginWindow) + { + pluginWindow.RefreshPluginList(); + } + } + }); + } + + LogHelper.WriteLogToFile("插件状态已从配置文件重新加载完成", LogHelper.LogType.Info); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"从配置文件重新加载插件状态时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + } +} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginTemplate.cs b/Ink Canvas/Helpers/Plugins/PluginTemplate.cs new file mode 100644 index 00000000..72f4c846 --- /dev/null +++ b/Ink Canvas/Helpers/Plugins/PluginTemplate.cs @@ -0,0 +1,276 @@ +using System; +using System.Windows; +using System.Windows.Controls; + +namespace Ink_Canvas.Helpers.Plugins +{ + /// + /// 插件模板,用于开发者参考 + /// 注意:实际开发时,请将此类移到单独的程序集中 + /// + public class PluginTemplate : PluginBase + { + #region 插件基本信息 + + /// + /// 插件名称 + /// + public override string Name => "插件模板"; + + /// + /// 插件描述 + /// + public override string Description => "这是一个插件开发模板,用于开发者参考。"; + + /// + /// 插件版本 + /// + public override Version Version => new Version(1, 0, 0); + + /// + /// 插件作者 + /// + public override string Author => "Your Name"; + + /// + /// 是否为内置插件(外部插件请返回false) + /// + public override bool IsBuiltIn => false; + + #endregion + + #region 插件生命周期 + + /// + /// 插件初始化 + /// 在这里进行插件的初始化工作,如加载配置、注册事件等 + /// + public override void Initialize() + { + // 先调用基类方法,这样会设置插件ID和记录日志 + base.Initialize(); + + // TODO: 在这里进行插件初始化工作 + + // 示例:记录初始化信息 + LogHelper.WriteLogToFile($"插件 {Name} 开始初始化", LogHelper.LogType.Info); + + // 示例:加载配置 + LoadConfig(); + + // 示例:注册自定义事件 + // MainWindow.Instance.SomeEvent += OnSomeEvent; + + LogHelper.WriteLogToFile($"插件 {Name} 初始化完成", LogHelper.LogType.Info); + } + + /// + /// 启用插件 + /// 在这里激活插件功能 + /// + public override void Enable() + { + // 先调用基类方法,这样会设置插件状态和记录日志 + base.Enable(); + + // TODO: 在这里启用插件功能 + + LogHelper.WriteLogToFile($"插件 {Name} 已启用", LogHelper.LogType.Info); + } + + /// + /// 禁用插件 + /// 在这里停用插件功能 + /// + public override void Disable() + { + // 先调用基类方法,这样会设置插件状态和记录日志 + base.Disable(); + + // TODO: 在这里禁用插件功能 + + LogHelper.WriteLogToFile($"插件 {Name} 已禁用", LogHelper.LogType.Info); + } + + /// + /// 清理资源 + /// 在插件卸载时调用,清理资源 + /// + public override void Cleanup() + { + // TODO: 在这里清理插件资源 + + // 示例:取消注册事件 + // MainWindow.Instance.SomeEvent -= OnSomeEvent; + + // 示例:保存配置 + SaveConfig(); + + // 最后调用基类方法 + base.Cleanup(); + } + + #endregion + + #region 插件配置 + + /// + /// 加载插件配置 + /// + private void LoadConfig() + { + try + { + // TODO: 从文件或其他位置加载配置 + // 示例: + // string configPath = Path.Combine(App.RootPath, "PluginConfigs", "YourPluginName.json"); + // if (File.Exists(configPath)) + // { + // string json = File.ReadAllText(configPath); + // YourConfig = Newtonsoft.Json.JsonConvert.DeserializeObject(json); + // } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载插件配置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 保存插件配置 + /// + private void SaveConfig() + { + try + { + // TODO: 保存配置到文件或其他位置 + // 示例: + // string configDir = Path.Combine(App.RootPath, "PluginConfigs"); + // if (!Directory.Exists(configDir)) + // { + // Directory.CreateDirectory(configDir); + // } + // string configPath = Path.Combine(configDir, "YourPluginName.json"); + // string json = Newtonsoft.Json.JsonConvert.SerializeObject(YourConfig, Newtonsoft.Json.Formatting.Indented); + // File.WriteAllText(configPath, json); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存插件配置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + #endregion + + #region 插件设置界面 + + /// + /// 获取插件设置界面 + /// + /// 插件设置界面 + public override UserControl GetSettingsView() + { + // 创建插件设置界面 + return new PluginTemplateSettingsControl(); + } + + #endregion + + #region 插件功能方法 + + // TODO: 在这里添加插件的具体功能方法 + + /// + /// 示例方法:执行一些功能 + /// + public void DoSomething() + { + if (!IsEnabled) return; + + try + { + // TODO: 实现你的功能 + MessageBox.Show("插件功能执行示例", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"执行插件功能时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + #endregion + } + + /// + /// 插件设置控件 + /// + public class PluginTemplateSettingsControl : UserControl + { + public PluginTemplateSettingsControl() + { + // 创建设置界面布局 + var panel = new StackPanel + { + Margin = new Thickness(10) + }; + + // 添加标题 + panel.Children.Add(new TextBlock + { + Text = "插件模板设置", + FontSize = 16, + FontWeight = FontWeights.Bold, + Margin = new Thickness(0, 0, 0, 10) + }); + + // 添加说明文字 + panel.Children.Add(new TextBlock + { + Text = "这是一个示例设置界面,你可以在这里添加自己的设置控件。", + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 0, 0, 15) + }); + + // 添加示例设置选项 + var checkBox = new CheckBox + { + Content = "启用某项功能", + Margin = new Thickness(0, 0, 0, 10) + }; + panel.Children.Add(checkBox); + + // 添加文本输入框 + panel.Children.Add(new TextBlock + { + Text = "设置项:", + Margin = new Thickness(0, 5, 0, 5) + }); + + panel.Children.Add(new TextBox + { + Margin = new Thickness(0, 0, 0, 10), + Width = 200, + HorizontalAlignment = HorizontalAlignment.Left + }); + + // 添加按钮 + var button = new Button + { + Content = "保存设置", + Padding = new Thickness(10, 5, 10, 5), + Margin = new Thickness(0, 10, 0, 0), + HorizontalAlignment = HorizontalAlignment.Left + }; + + button.Click += (sender, e) => + { + MessageBox.Show("设置已保存!", "插件模板", MessageBoxButton.OK, MessageBoxImage.Information); + }; + + panel.Children.Add(button); + + // 设置控件内容 + this.Content = panel; + } + } +} \ No newline at end of file diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 7f9a583c..9f43e127 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -113,6 +113,9 @@ + + + @@ -197,6 +200,11 @@ + + - - - + + + + + + + 通过插件扩展InkCanvas的功能。您可以启用或禁用插件,或加载自定义插件。 + +