From 18b737b22b740756348327c10a33713e8552e985 Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sun, 29 Mar 2026 12:24:13 +0800 Subject: [PATCH 01/27] =?UTF-8?q?add:=E6=89=8B=E5=86=99=E4=BD=93=E8=AF=86?= =?UTF-8?q?=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Helpers/InkRecognitionManager.cs | 93 ++- Ink Canvas/Helpers/InkRecognizeHelper.cs | 31 + .../Helpers/WinRtHandwritingRecognizer.cs | 599 ++++++++++++++++++ Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs | 3 +- Ink Canvas/MainWindow.xaml | 10 + Ink Canvas/MainWindow_cs/MW_Settings.cs | 12 +- Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs | 4 + .../MW_SimulatePressure&InkToShape.cs | 174 +++++ Ink Canvas/Properties/Strings.enUS.xml | 2 + Ink Canvas/Properties/Strings.resx | 2 + Ink Canvas/Resources/Settings.cs | 8 +- 11 files changed, 925 insertions(+), 13 deletions(-) create mode 100644 Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs diff --git a/Ink Canvas/Helpers/InkRecognitionManager.cs b/Ink Canvas/Helpers/InkRecognitionManager.cs index 19be366f..69c86b32 100644 --- a/Ink Canvas/Helpers/InkRecognitionManager.cs +++ b/Ink Canvas/Helpers/InkRecognitionManager.cs @@ -100,19 +100,68 @@ namespace Ink_Canvas.Helpers return await WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes).ConfigureAwait(true); } + /// 为 true 且走 WinRT 时,将识别成功的词替换为手写风格字体的轮廓墨迹(见设置中的字体列表)。 + /// 逗号分隔的字体回退列表(WPF FontFamily);null 时使用内置默认。 public Task CorrectInkAsync( StrokeCollection strokes, - ShapeRecognitionEngineMode mode) + ShapeRecognitionEngineMode mode, + bool applyHandwritingBeautify = false, + string handwritingFontFamilyList = null) { - if (!_isInitialized || strokes == null || strokes.Count == 0) + if (!_isInitialized) + { + LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info); return Task.FromResult(strokes); + } + + if (strokes == null || strokes.Count == 0) + { + LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:无笔画。", LogHelper.LogType.Info); + return Task.FromResult(strokes); + } try { - if (ShapeRecognitionRouter.ResolveUseWinRt(mode) && _modernAnalyzer != null) - return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes); + var useWinRt = ShapeRecognitionRouter.ResolveUseWinRt(mode); + if (!applyHandwritingBeautify) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:未开启「识别转手写体字形」(applyHandwritingBeautify=false)。笔画数=" + + strokes.Count, + LogHelper.LogType.Info); + return Task.FromResult(strokes); + } - return Task.FromResult(strokes); + if (!useWinRt) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:当前引擎非 WinRT(模式=" + mode + ")。笔画数=" + strokes.Count, + LogHelper.LogType.Info); + return Task.FromResult(strokes); + } + + if (!Environment.Is64BitProcess) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:非 64 位进程,WinRT 手写体替换不可用。笔画数=" + strokes.Count, + LogHelper.LogType.Info); + return Task.FromResult(strokes); + } + + if (_modernAnalyzer == null) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:ModernInkAnalyzer 未就绪(WinRT 初始化失败?)。笔画数=" + + strokes.Count, + LogHelper.LogType.Warning); + return Task.FromResult(strokes); + } + + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count + + ",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()), + LogHelper.LogType.Info); + return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes, handwritingFontFamilyList); } catch (Exception ex) { @@ -121,6 +170,32 @@ namespace Ink_Canvas.Helpers } } + /// + /// WinRT 手写体识别(需 64 位进程、Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。 + /// + public Task RecognizeHandwritingAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode) + { + if (!_isInitialized || strokes == null || strokes.Count == 0) + return Task.FromResult(HandwritingRecognitionResult.Empty); + + try + { + if (!Environment.Is64BitProcess + || !ShapeRecognitionRouter.ResolveUseWinRt(mode) + || !WinRtHandwritingRecognizer.IsApiAvailable) + return Task.FromResult(HandwritingRecognitionResult.Empty); + + return WinRtHandwritingRecognizer.RecognizeHandwritingAsync(strokes); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("手写识别失败: " + ex.Message, LogHelper.LogType.Error); + return Task.FromResult(HandwritingRecognitionResult.Empty); + } + } + public bool IsValidShapeType(string shapeName) { return !string.IsNullOrEmpty(shapeName) @@ -166,9 +241,13 @@ namespace Ink_Canvas.Helpers internal sealed class ModernInkAnalyzer : IDisposable { - public Task AnalyzeAndCorrectAsync(StrokeCollection strokes) + public Task AnalyzeAndCorrectAsync( + StrokeCollection strokes, + string handwritingFontFamilyList) { - return Task.FromResult(strokes); + return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync( + strokes, + handwritingFontFamilyList); } public void Dispose() diff --git a/Ink Canvas/Helpers/InkRecognizeHelper.cs b/Ink Canvas/Helpers/InkRecognizeHelper.cs index 34f5cfe6..7ec48cf3 100644 --- a/Ink Canvas/Helpers/InkRecognizeHelper.cs +++ b/Ink Canvas/Helpers/InkRecognizeHelper.cs @@ -1,3 +1,4 @@ +using Ink_Canvas; using System.Linq; using System.Threading.Tasks; using System.Windows; @@ -101,7 +102,10 @@ namespace Ink_Canvas.Helpers { _ = InkRecognitionManager.Instance; if (ShapeRecognitionRouter.ResolveUseWinRt(mode)) + { WinRtInkShapeRecognizer.Warmup(); + WinRtHandwritingRecognizer.Warmup(); + } else RecognizeShapeIACore(new StrokeCollection()); } @@ -111,6 +115,33 @@ namespace Ink_Canvas.Helpers } } + /// WinRT 手写识别(64 位 + Windows 10+)。 + public static Task RecognizeHandwritingUnifiedAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode) => + InkRecognitionManager.Instance.RecognizeHandwritingAsync(strokes, mode); + + /// WinRT 下将识别成功的词替换为手写体字形墨迹;是否应用由设置「WinRT 识别转手写体字形」控制。 + public static Task CorrectHandwritingStrokesUnifiedAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode) => + InkRecognitionManager.Instance.CorrectInkAsync( + strokes, + mode, + MainWindow.Settings?.InkToShape?.EnableWinRtHandwritingStrokeBeautify ?? false, + MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily); + + /// 显式指定是否应用手写体字形替换(忽略开关);字体仍从设置读取。 + public static Task CorrectHandwritingStrokesUnifiedAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode, + bool applyHandwritingBeautify) => + InkRecognitionManager.Instance.CorrectInkAsync( + strokes, + mode, + applyHandwritingBeautify, + MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily); + internal static InkShapeRecognitionResult FromIACoreOrEmpty(ShapeRecognizeResult legacy) { if (legacy?.InkDrawingNode == null) diff --git a/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs new file mode 100644 index 00000000..94a17dcf --- /dev/null +++ b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs @@ -0,0 +1,599 @@ +using OSVersionExtension; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Ink; +using System.Windows.Input; +using System.Windows.Media; +using WinAnalysis = global::Windows.UI.Input.Inking.Analysis; +using WinRtInk = global::Windows.UI.Input.Inking; + +namespace Ink_Canvas.Helpers +{ + /// + /// WinRT 手写体识别,以及将识别结果用手写风格字体轮廓转为墨迹笔画(「识别转手写体字形」)。 + /// + internal static class WinRtHandwritingRecognizer + { + private static void LogHandwriting(string message, LogHelper.LogType logType = LogHelper.LogType.Info) + { + LogHelper.WriteLogToFile("[手写体] " + message, logType); + } + + public static bool IsApiAvailable => + OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10; + + public static void Warmup() + { + if (!IsApiAvailable || !Environment.Is64BitProcess) return; + try + { + var d = Application.Current?.Dispatcher; + if (d == null) return; + d.BeginInvoke(new Action(async () => + { + try + { + await RecognizeHandwritingAsync(new StrokeCollection()).ConfigureAwait(true); + } + catch + { + // ignore + } + })); + } + catch + { + // ignore + } + } + + /// + /// 将当前笔画集合识别为文字片段(含候选):先用墨迹分析得到分词与 , + /// 再对每一分词用 GetTextCandidates(与当前 SDK 中部分版本的 + /// 未暴露笔画映射的局限兼容)。 + /// + public static async Task RecognizeHandwritingAsync(StrokeCollection strokes) + { + if (!IsApiAvailable || strokes == null || strokes.Count == 0) + return HandwritingRecognitionResult.Empty; + + var traceRecognition = strokes.Count > 0; + + try + { + var analyzer = new WinAnalysis.InkAnalyzer(); + var idToWpf = new Dictionary(); + + foreach (Stroke s in strokes) + { + var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s); + if (ink == null) continue; + analyzer.AddDataForStroke(ink); + analyzer.SetStrokeDataKind(ink.Id, WinAnalysis.InkAnalysisStrokeKind.Writing); + idToWpf[ink.Id] = s; + } + + if (idToWpf.Count == 0) + { + if (traceRecognition) + LogHandwriting("识别:无有效 WinRT 笔画(全部转换失败),输入笔画数=" + strokes.Count); + return HandwritingRecognitionResult.Empty; + } + + var analysisResult = await analyzer.AnalyzeAsync().AsTask().ConfigureAwait(true); + if (analysisResult == null || analysisResult.Status != WinAnalysis.InkAnalysisStatus.Updated) + { + if (traceRecognition) + LogHandwriting( + "识别:AnalyzeAsync 未得到 Updated,Status=" + + (analysisResult == null ? "null" : analysisResult.Status.ToString()) + + ",有效笔画数=" + idToWpf.Count); + return HandwritingRecognitionResult.Empty; + } + + var wordNodes = analyzer.AnalysisRoot?.FindNodes(WinAnalysis.InkAnalysisNodeKind.InkWord); + if (wordNodes == null || wordNodes.Count == 0) + { + if (traceRecognition) + LogHandwriting("识别:未找到 InkWord 节点(可能被判为绘图或非书写),有效笔画数=" + idToWpf.Count); + return HandwritingRecognitionResult.Empty; + } + + var recognizer = new WinRtInk.InkRecognizerContainer(); + var segments = new List(); + + foreach (var node in wordNodes) + { + if (!(node is WinAnalysis.InkAnalysisInkWord word)) + continue; + + var ids = word.GetStrokeIds(); + if (ids == null || ids.Count == 0) + continue; + + var group = new List(); + foreach (var sid in ids) + { + if (idToWpf.TryGetValue(sid, out var st)) + group.Add(st); + } + + if (group.Count == 0) + continue; + + var wbr = word.BoundingRect; + var wpfRect = new Rect(wbr.X, wbr.Y, wbr.Width, wbr.Height); + var analysisText = word.RecognizedText ?? string.Empty; + + IReadOnlyList candList = Array.Empty(); + try + { + if (recognizer != null) + { + var mini = new WinRtInk.InkStrokeContainer(); + foreach (var st in group) + { + var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(st); + if (ink != null) + mini.AddStroke(ink); + } + + var miniStrokes = mini.GetStrokes(); + if (miniStrokes != null && miniStrokes.Count > 0) + { + var rr = await recognizer + .RecognizeAsync(mini, WinRtInk.InkRecognitionTarget.All) + .AsTask() + .ConfigureAwait(true); + if (rr != null && rr.Count > 0 && rr[0] != null) + { + var cands = rr[0].GetTextCandidates(); + if (cands != null && cands.Count > 0) + candList = cands.ToList(); + } + } + } + } + catch + { + candList = Array.Empty(); + } + + var primary = candList.Count > 0 ? candList[0] : analysisText; + var mergedCandidates = new List(); + if (candList.Count > 0) + { + foreach (var c in candList) + { + if (!string.IsNullOrEmpty(c) && !mergedCandidates.Contains(c)) + mergedCandidates.Add(c); + } + } + + if (!string.IsNullOrEmpty(analysisText) && !mergedCandidates.Contains(analysisText)) + mergedCandidates.Insert(0, analysisText); + + if (mergedCandidates.Count == 0 && !string.IsNullOrEmpty(primary)) + mergedCandidates.Add(primary); + + segments.Add(new HandwritingWordSegment( + primary, + mergedCandidates, + wpfRect, + group)); + } + + if (segments.Count == 0) + { + if (traceRecognition) + LogHandwriting("识别:分词列表为空(InkWord 无有效笔画映射)。"); + return HandwritingRecognitionResult.Empty; + } + + var hr = new HandwritingRecognitionResult(segments); + if (traceRecognition) + { + var preview = hr.CombinedText; + if (preview.Length > 120) + preview = preview.Substring(0, 117) + "..."; + LogHandwriting( + "识别成功:词数=" + segments.Count + + ",合并文本=\"" + preview + "\"" + + ",进程位数=" + (Environment.Is64BitProcess ? "x64" : "x86")); + for (var i = 0; i < segments.Count; i++) + { + var seg = segments[i]; + var t = seg.Text ?? ""; + if (t.Length > 40) + t = t.Substring(0, 37) + "..."; + LogHandwriting( + " 词[" + i + "] 文本=\"" + t + "\",笔画数=" + seg.Strokes.Count + + ",候选数=" + (seg.TextCandidates?.Count ?? 0) + + ",框=(" + Math.Round(seg.BoundingRectangle.X, 1) + "," + + Math.Round(seg.BoundingRectangle.Y, 1) + "," + + Math.Round(seg.BoundingRectangle.Width, 1) + "×" + + Math.Round(seg.BoundingRectangle.Height, 1) + ")"); + } + } + + return hr; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("WinRT 手写识别失败: " + ex.Message, LogHelper.LogType.Warning); + if (strokes != null && strokes.Count > 0) + LogHandwriting("识别异常:" + ex.Message, LogHelper.LogType.Warning); + return HandwritingRecognitionResult.Empty; + } + } + + private const string DefaultHandwritingFontFamilyList = "Ink Free,KaiTi,Segoe Script"; + + /// + /// 识别手写词后,将「有识别文本」的分词替换为指定手写风格字体的字形轮廓墨迹;未识别或空文本的词保留原笔画。 + /// + public static async Task ConvertRecognizedTextToHandwritingInkAsync( + StrokeCollection strokes, + string handwritingFontFamilyList) + { + if (!IsApiAvailable || strokes == null || strokes.Count == 0) + { + if (strokes != null && strokes.Count > 0 && !IsApiAvailable) + LogHandwriting("字形替换:跳过,IsApiAvailable=false。"); + return strokes; + } + + var fontList = string.IsNullOrWhiteSpace(handwritingFontFamilyList) + ? DefaultHandwritingFontFamilyList + : handwritingFontFamilyList.Trim(); + LogHandwriting( + "字形替换开始:输入笔画数=" + strokes.Count + + ",字体链=\"" + fontList + "\"" + + ",PixelsPerDip=" + Math.Round(GetPixelsPerDipSafe(), 3)); + + try + { + var reco = await RecognizeHandwritingAsync(strokes).ConfigureAwait(true); + if (!reco.IsSuccess || reco.Words == null || reco.Words.Count == 0) + { + LogHandwriting( + "字形替换中止:识别未成功(IsSuccess=" + reco.IsSuccess + + ",词数=" + (reco.Words?.Count ?? 0) + "),原样返回笔画。"); + return strokes; + } + + var firstStrokeToSegment = new Dictionary(); + foreach (var w in reco.Words) + { + if (w?.Strokes == null || w.Strokes.Count == 0) + continue; + var ordered = w.Strokes.OrderBy(st => IndexOfStrokeInCollection(strokes, st)).ToList(); + var first = ordered[0]; + if (!firstStrokeToSegment.ContainsKey(first)) + firstStrokeToSegment[first] = w; + } + + if (firstStrokeToSegment.Count == 0) + { + LogHandwriting("字形替换中止:无法建立「首笔画→分词」映射,原样返回。"); + return strokes; + } + + var consumed = new HashSet(); + var result = new StrokeCollection(); + var pixelsPerDip = GetPixelsPerDipSafe(); + var replacedWordCount = 0; + var keptOriginalWordCount = 0; + var glyphStrokeTotal = 0; + + foreach (Stroke s in strokes) + { + if (consumed.Contains(s)) + continue; + + if (!firstStrokeToSegment.TryGetValue(s, out var seg)) + { + result.Add(s); + continue; + } + + if (string.IsNullOrWhiteSpace(seg.Text)) + { + LogHandwriting( + " 分词:文本为空,保留原笔画,笔画数=" + seg.Strokes.Count); + keptOriginalWordCount++; + foreach (var z in seg.Strokes) + { + if (!consumed.Contains(z)) + { + result.Add(z); + consumed.Add(z); + } + } + + continue; + } + + var templateDa = seg.Strokes[0]?.DrawingAttributes?.Clone() ?? new DrawingAttributes(); + OutlineAttributesForGlyphInk(templateDa); + + var glyphStrokes = CreateHandwritingGlyphStrokes( + seg.Text.Trim(), + seg.BoundingRectangle, + templateDa, + fontList, + pixelsPerDip); + + if (glyphStrokes == null || glyphStrokes.Count == 0) + { + LogHandwriting( + " 分词:字形轮廓生成失败,保留原笔画。文本=\"" + + (seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) + "\""); + keptOriginalWordCount++; + foreach (var z in seg.Strokes) + { + if (!consumed.Contains(z)) + { + result.Add(z); + consumed.Add(z); + } + } + + continue; + } + + foreach (var nk in glyphStrokes) + result.Add(nk); + glyphStrokeTotal += glyphStrokes.Count; + replacedWordCount++; + LogHandwriting( + " 分词:已替换为手写体字形墨迹,文本=\"" + + (seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) + + "\",生成笔画数=" + glyphStrokes.Count + ",移除原笔画数=" + seg.Strokes.Count); + + foreach (var z in seg.Strokes) + consumed.Add(z); + } + + LogHandwriting( + "字形替换结束:输出笔画数=" + result.Count + + "(输入=" + strokes.Count + "),替换词数=" + replacedWordCount + + ",保留原迹词数=" + keptOriginalWordCount + + ",字形子笔画合计=" + glyphStrokeTotal); + return result; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("WinRT 手写体字形替换失败: " + ex.Message, LogHelper.LogType.Warning); + LogHandwriting("字形替换异常:" + ex, LogHelper.LogType.Warning); + return strokes; + } + } + + private static int IndexOfStrokeInCollection(StrokeCollection collection, Stroke stroke) + { + if (collection == null || stroke == null) + return int.MaxValue; + for (var i = 0; i < collection.Count; i++) + { + if (ReferenceEquals(collection[i], stroke)) + return i; + } + + return int.MaxValue; + } + + private static void OutlineAttributesForGlyphInk(DrawingAttributes da) + { + if (da == null) return; + var w = Math.Max(0.8, Math.Min(da.Width, da.Height) * 0.2); + da.Width = w; + da.Height = w; + da.StylusTip = StylusTip.Ellipse; + da.IsHighlighter = false; + } + + private static double GetPixelsPerDipSafe() + { + try + { + if (Application.Current?.MainWindow is Visual v) + return VisualTreeHelper.GetDpi(v).PixelsPerDip; + } + catch + { + // ignore + } + + return 1.0; + } + + private static Typeface ResolveHandwritingTypeface(string fontFamilyList) + { + try + { + var ff = new FontFamily(fontFamilyList ?? DefaultHandwritingFontFamilyList); + return new Typeface(ff, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); + } + catch + { + return new Typeface( + SystemFonts.MessageFontFamily, + SystemFonts.MessageFontStyle, + SystemFonts.MessageFontWeight, + FontStretches.Normal); + } + } + + private static List CreateHandwritingGlyphStrokes( + string text, + Rect placeRect, + DrawingAttributes templateDa, + string fontFamilyList, + double pixelsPerDip) + { + var list = new List(); + if (string.IsNullOrEmpty(text) || placeRect.Width < 1 || placeRect.Height < 1) + return list; + + var typeface = ResolveHandwritingTypeface(fontFamilyList); + var culture = CultureInfo.CurrentCulture; + var em = Math.Max(6.0, placeRect.Height * 0.72); + FormattedText ft = null; + + for (var i = 0; i < 14; i++) + { + ft = new FormattedText( + text, + culture, + FlowDirection.LeftToRight, + typeface, + em, + Brushes.Black, + new NumberSubstitution(NumberCultureSource.Text, culture, NumberSubstitutionMethod.Context), + TextFormattingMode.Display, + pixelsPerDip); + + if (ft.Width <= placeRect.Width * 0.96 && ft.Height <= placeRect.Height * 0.96) + break; + + em *= 0.9; + if (em < 4.5) + break; + } + + if (ft == null || ft.Width < 0.5 || ft.Height < 0.5) + return list; + + var scale = Math.Min( + placeRect.Width * 0.94 / Math.Max(1e-6, ft.Width), + placeRect.Height * 0.94 / Math.Max(1e-6, ft.Height)); + var tx = placeRect.Left + (placeRect.Width - ft.Width * scale) / 2.0; + var ty = placeRect.Top + (placeRect.Height - ft.Height * scale) / 2.0; + + Geometry geom; + try + { + geom = ft.BuildGeometry(new Point(0, 0)); + } + catch + { + return list; + } + + if (geom == null || geom.IsEmpty()) + return list; + + var m = new Matrix(scale, 0, 0, scale, tx, ty); + geom.Transform = new MatrixTransform(m); + return StrokesFromOutlinedGeometry(geom, templateDa, 0.35); + } + + private static List StrokesFromOutlinedGeometry(Geometry geometry, DrawingAttributes da, double tolerance) + { + var list = new List(); + if (geometry == null || geometry.IsEmpty() || da == null) + return list; + + Geometry outlined; + try + { + outlined = geometry.GetOutlinedPathGeometry(tolerance, ToleranceType.Absolute); + } + catch + { + return list; + } + + if (outlined == null || outlined.IsEmpty()) + return list; + + Geometry flat; + try + { + flat = outlined.GetFlattenedPathGeometry(tolerance, ToleranceType.Absolute); + } + catch + { + return list; + } + + if (!(flat is PathGeometry pg)) + return list; + + foreach (var fig in pg.Figures) + { + var pts = new StylusPointCollection(); + pts.Add(new StylusPoint(fig.StartPoint.X, fig.StartPoint.Y, 0.5f)); + foreach (var seg in fig.Segments) + { + switch (seg) + { + case LineSegment ls: + pts.Add(new StylusPoint(ls.Point.X, ls.Point.Y, 0.5f)); + break; + case PolyLineSegment pls: + foreach (var p in pls.Points) + pts.Add(new StylusPoint(p.X, p.Y, 0.5f)); + break; + } + } + + if (pts.Count >= 2) + list.Add(new Stroke(pts) { DrawingAttributes = da.Clone() }); + } + + return list; + } + } + + /// 单个手写词片段的识别结果。 + public sealed class HandwritingWordSegment + { + public HandwritingWordSegment( + string text, + IReadOnlyList textCandidates, + Rect boundingRectangle, + IReadOnlyList strokes) + { + Text = text ?? string.Empty; + TextCandidates = textCandidates ?? Array.Empty(); + BoundingRectangle = boundingRectangle; + Strokes = strokes ?? Array.Empty(); + } + + public string Text { get; } + public IReadOnlyList TextCandidates { get; } + public Rect BoundingRectangle { get; } + public IReadOnlyList Strokes { get; } + } + + /// 一次手写识别批次的汇总结果。 + public sealed class HandwritingRecognitionResult + { + public static readonly HandwritingRecognitionResult Empty = new HandwritingRecognitionResult(); + + private HandwritingRecognitionResult() + { + Words = Array.Empty(); + IsSuccess = false; + CombinedText = string.Empty; + } + + public HandwritingRecognitionResult(IReadOnlyList words) + { + Words = words ?? Array.Empty(); + IsSuccess = Words.Count > 0; + CombinedText = string.Join("", Words.Select(w => w.Text ?? string.Empty)); + } + + public bool IsSuccess { get; } + public IReadOnlyList Words { get; } + public string CombinedText { get; } + } +} diff --git a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs index 2257ad0d..bfd0618f 100644 --- a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs +++ b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs @@ -99,7 +99,8 @@ namespace Ink_Canvas.Helpers } } - private static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke) + /// 供 WinRT 手写等模块复用:将 WPF 转为 WinRT + internal static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke) { if (stroke?.StylusPoints == null || stroke.StylusPoints.Count == 0) return null; diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 9af46a13..035591b9 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -1137,6 +1137,16 @@ + + + + + diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index 9fb01303..0749adcd 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -3859,7 +3859,8 @@ namespace Ink_Canvas 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.Startup.IsEnableNibMode = false; Settings.Startup.IsAutoUpdate = true; @@ -3942,6 +3943,15 @@ namespace Ink_Canvas SaveSettingsToFile(); } + private void ToggleSwitchEnableWinRtHandwritingStrokeBeautify_Toggled(object sender, RoutedEventArgs e) + { + if (!isLoaded) return; + Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify = + ToggleSwitchEnableWinRtHandwritingStrokeBeautify != null && + ToggleSwitchEnableWinRtHandwritingStrokeBeautify.IsOn; + SaveSettingsToFile(); + } + private void ToggleSwitchEnableInkToShapeNoFakePressureTriangle_Toggled(object sender, RoutedEventArgs e) { if (!isLoaded) return; diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index a525e3ae..c9616ec1 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -1060,6 +1060,10 @@ namespace Ink_Canvas ComboBoxShapeRecognitionEngine.SelectedIndex = eng; } + if (ToggleSwitchEnableWinRtHandwritingStrokeBeautify != null) + ToggleSwitchEnableWinRtHandwritingStrokeBeautify.IsOn = + Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify; + ToggleSwitchEnableInkToShapeNoFakePressureRectangle.IsOn = Settings.InkToShape.IsInkToShapeNoFakePressureRectangle; diff --git a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs index 9dceb38a..0b1935dc 100644 --- a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs +++ b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs @@ -43,7 +43,20 @@ namespace Ink_Canvas /// 串行执行墨迹转形状(尤其 WinRT 异步延后),避免多笔 BeginInvoke 交错修改 newStrokes。 private static readonly SemaphoreSlim InkToShapeSerial = new SemaphoreSlim(1, 1); + /// + /// 收笔后待参与「手写体字形替换」的最近笔迹(多笔一字/一词时由 WinRT InkAnalyzer 合并为 InkWord)。 + /// + private readonly StrokeCollection _handwritingRecentStrokesForBeautify = new StrokeCollection(); + private const int HandwritingBeautifyMaxRecentStrokes = 20; + + /// 手写体矫正:停笔后延迟执行(毫秒),多笔一字时等用户写完再识别。 + private const int HandwritingBeautifyDebounceMs = 1000; + + private DispatcherTimer _handwritingBeautifyDebounceTimer; + + /// 每次收笔并入批次时递增;防抖 Tick 携带快照,识别过程中若又有新笔则放弃本轮替换。 + private ulong _handwritingBeautifyScheduleRevision; /// /// 矩形参考线的列表 @@ -388,6 +401,10 @@ namespace Ink_Canvas } } + Stroke strokeForHandwritingBeautify = e.Stroke; + if (wasStraightened && inkCanvas.Strokes.Count > 0) + strokeForHandwritingBeautify = inkCanvas.Strokes[inkCanvas.Strokes.Count - 1]; + if (ShapeRecognitionRouter.ShouldRunShapeRecognition( Settings.InkToShape.IsInkToShapeEnabled, ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine))) @@ -734,6 +751,8 @@ namespace Ink_Canvas try { await InkToShapeProcessCoreAsync(); + if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke); } catch (Exception ex) { @@ -744,10 +763,16 @@ namespace Ink_Canvas } InkToShapeProcessCoreAsync().GetAwaiter().GetResult(); + if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke); } InkToShapeProcess(); } + else if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + { + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke); + } foreach (var stylusPoint in e.Stroke.StylusPoints) //LogHelper.WriteLogToFile(stylusPoint.PressureFactor.ToString(), LogHelper.LogType.Info); @@ -2668,6 +2693,155 @@ namespace Ink_Canvas return corners.OrderBy(p => Math.Atan2(p.Y - center.Y, p.X - center.X)).ToList(); } + /// + /// 收笔后:在墨迹转形状(若启用)完成之后,将笔画并入批次并启动/重置停笔防抖计时器,再于延迟后多笔合并矫正。 + /// + private void PruneHandwritingBeautifyBatch() + { + for (var i = _handwritingRecentStrokesForBeautify.Count - 1; i >= 0; i--) + { + if (!inkCanvas.Strokes.Contains(_handwritingRecentStrokesForBeautify[i])) + _handwritingRecentStrokesForBeautify.RemoveAt(i); + } + } + + private void EnsureHandwritingBeautifyDebounceTimer() + { + if (_handwritingBeautifyDebounceTimer != null) + return; + _handwritingBeautifyDebounceTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher) + { + Interval = TimeSpan.FromMilliseconds(HandwritingBeautifyDebounceMs) + }; + _handwritingBeautifyDebounceTimer.Tick += HandwritingBeautifyDebounceTimer_Tick; + } + + /// 并入批次并重置 1s 计时器(多笔需停笔满延迟后才矫正)。 + private void ScheduleHandwritingGlyphReplaceAfterStrokeCollected(Stroke strokeForBeautify, bool isBoardBrushStroke) + { + if (!Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + return; + if (isBoardBrushStroke) + return; + if (strokeForBeautify == null || strokeForBeautify.DrawingAttributes?.IsHighlighter == true) + return; + if (!inkCanvas.Strokes.Contains(strokeForBeautify)) + return; + + _handwritingBeautifyScheduleRevision++; + + var alreadyInBatch = false; + foreach (Stroke x in _handwritingRecentStrokesForBeautify) + { + if (ReferenceEquals(x, strokeForBeautify)) + { + alreadyInBatch = true; + break; + } + } + + if (!alreadyInBatch) + _handwritingRecentStrokesForBeautify.Add(strokeForBeautify); + + PruneHandwritingBeautifyBatch(); + while (_handwritingRecentStrokesForBeautify.Count > HandwritingBeautifyMaxRecentStrokes) + _handwritingRecentStrokesForBeautify.RemoveAt(0); + + EnsureHandwritingBeautifyDebounceTimer(); + _handwritingBeautifyDebounceTimer.Stop(); + _handwritingBeautifyDebounceTimer.Interval = TimeSpan.FromMilliseconds(HandwritingBeautifyDebounceMs); + _handwritingBeautifyDebounceTimer.Start(); + } + + private async void HandwritingBeautifyDebounceTimer_Tick(object sender, EventArgs e) + { + if (_handwritingBeautifyDebounceTimer != null) + _handwritingBeautifyDebounceTimer.Stop(); + + var revisionWhenIdle = _handwritingBeautifyScheduleRevision; + try + { + await HandwritingGlyphReplaceFromBatchCoreAsync(revisionWhenIdle).ConfigureAwait(true); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("[手写体] 停笔防抖矫正异常: " + ex.Message, LogHelper.LogType.Warning); + Debug.WriteLine(ex); + } + } + + private async Task HandwritingGlyphReplaceFromBatchCoreAsync(ulong revisionWhenIdle) + { + await InkToShapeSerial.WaitAsync().ConfigureAwait(true); + try + { + if (_handwritingBeautifyScheduleRevision != revisionWhenIdle) + return; + + PruneHandwritingBeautifyBatch(); + + var input = new StrokeCollection(); + foreach (Stroke s in _handwritingRecentStrokesForBeautify) + { + if (inkCanvas.Strokes.Contains(s)) + input.Add(s); + } + + if (input.Count == 0) + return; + if (_handwritingBeautifyScheduleRevision != revisionWhenIdle) + return; + + var shapeMode = ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine); + var result = await InkRecognizeHelper.CorrectHandwritingStrokesUnifiedAsync(input, shapeMode).ConfigureAwait(true); + + if (_handwritingBeautifyScheduleRevision != revisionWhenIdle) + return; + if (result == null || result.Count == 0) + return; + if (ReferenceEquals(result, input)) + return; + + var anyInputStillPresent = false; + foreach (Stroke s in input) + { + if (inkCanvas.Strokes.Contains(s)) + { + anyInputStillPresent = true; + break; + } + } + + if (!anyInputStillPresent) + return; + + SetNewBackupOfStroke(); + _currentCommitType = CommitReason.ShapeRecognition; + foreach (Stroke s in input) + { + if (inkCanvas.Strokes.Contains(s)) + inkCanvas.Strokes.Remove(s); + } + + foreach (Stroke s in result) + inkCanvas.Strokes.Add(s); + _currentCommitType = CommitReason.UserInput; + + foreach (Stroke s in input) + _handwritingRecentStrokesForBeautify.Remove(s); + + PruneHandwritingBeautifyBatch(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("[手写体] 收笔替换异常: " + ex.Message, LogHelper.LogType.Warning); + } + finally + { + InkToShapeSerial.Release(); + } + } + #endregion } } \ No newline at end of file diff --git a/Ink Canvas/Properties/Strings.enUS.xml b/Ink Canvas/Properties/Strings.enUS.xml index 7b469ec7..8b85772e 100644 --- a/Ink Canvas/Properties/Strings.enUS.xml +++ b/Ink Canvas/Properties/Strings.enUS.xml @@ -229,6 +229,8 @@ Auto IACore WinRT + WinRT: recognized text to handwriting glyphs + # When the ink correction API runs: WinRT recognizes ink words, then replaces recognized text with outline strokes from a handwriting-style font (default Ink Free / KaiTi / Segoe Script; set handwritingCorrectionFontFamily in Settings JSON). 64-bit + WinRT. Block fake pressure on corrected rectangles Block fake pressure on corrected triangles Correct freehand triangles diff --git a/Ink Canvas/Properties/Strings.resx b/Ink Canvas/Properties/Strings.resx index 44ea35ee..3f00ba51 100644 --- a/Ink Canvas/Properties/Strings.resx +++ b/Ink Canvas/Properties/Strings.resx @@ -244,6 +244,8 @@ 自动 IACore WinRT + WinRT 识别转手写体字形 + # 开启后,调用墨迹纠正 API 时:先 WinRT 识别手写词,再将识别成功的文字用手写风格字体(默认 Ink Free / 楷体 等,可在设置 JSON 的 handwritingCorrectionFontFamily 调整)转成字形轮廓墨迹替换原笔画。需 64 位与 WinRT。 阻止矫正后的矩形带有模拟压感值 阻止矫正后的三角形带有模拟压感值 矫正手绘三角形 diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index e4023769..88eef9c6 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -747,12 +747,12 @@ namespace Ink_Canvas public double LineStraightenSensitivity { get; set; } = 0.20; [JsonProperty("lineNormalizationThreshold")] public double LineNormalizationThreshold { get; set; } = 0.5; - - /// - /// 形状识别后端:0=自动(x64 用 WinRT,x86 用 IACore),1=IACore,2=WinRT。 - /// [JsonProperty("shapeRecognitionEngine")] public int ShapeRecognitionEngine { get; set; } + [JsonProperty("enableWinRtHandwritingStrokeBeautify")] + public bool EnableWinRtHandwritingStrokeBeautify { get; set; } + [JsonProperty("handwritingCorrectionFontFamily")] + public string HandwritingCorrectionFontFamily { get; set; } = "Ink Free,KaiTi,Segoe Script"; } public class RandSettings From aef860a8cc1063d587a384102a2f8cf10a0d6ae1 Mon Sep 17 00:00:00 2001 From: doudou0720 <98651603+doudou0720@users.noreply.github.com> Date: Sat, 4 Apr 2026 11:22:54 +0800 Subject: [PATCH 02/27] =?UTF-8?q?ci:=20=E4=BF=AE=E5=A4=8D=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=B8=AD=E7=9A=84=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=BC=95=E7=94=A8=E6=A0=BC=E5=BC=8F=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8Dchangelog=E5=B8=A6=E6=9C=89=E5=8F=8C=E5=BC=95=E5=8F=B7?= =?UTF-8?q?=E8=80=8C=E8=A2=ABShell=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 神秘问题 Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com> --- .github/workflows/prerelease.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index e891e6b1..c8ee4798 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -528,13 +528,13 @@ jobs: - name: Create enhanced changelog with file table id: enhanced_changelog run: | - version="${{ needs.prepare.outputs.version }}" + version='${{ needs.prepare.outputs.version }}' # 读取git-cliff生成的changelog内容 - originalChangelog="${{ needs.prepare.outputs.changelog }}" + originalChangelog='${{ needs.prepare.outputs.changelog }}' # 检查是否为预发布版本,如果是则添加警告提示 - if [ "${{ needs.prepare.outputs.is_prerelease }}" = "true" ]; then + if [ '${{ needs.prepare.outputs.is_prerelease }}' = "true" ]; then warningText=$'\n> [!CAUTION]\n' warningText+=$'> **注意:此版本为预览或测试版**\n' warningText+=$'> \n' From af19ffb7368c999b97395ce4266ace0f7a6aede3 Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sat, 4 Apr 2026 22:16:32 +0800 Subject: [PATCH 03/27] =?UTF-8?q?fix:=E4=B8=80=E8=A8=80API=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=87=8D=E5=A4=8D=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/MainWindow_cs/MW_Settings.cs | 176 ++++++++++++++++-- Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs | 29 ++- 2 files changed, 183 insertions(+), 22 deletions(-) diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index 0749adcd..02cf401e 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -1,9 +1,11 @@ using Hardcodet.Wpf.TaskbarNotification; using Ink_Canvas.Helpers; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OSVersionExtension; using System; using System.Collections.Generic; +using System.Globalization; using System.Diagnostics; using System.IO; using System.Linq; @@ -1209,17 +1211,14 @@ namespace Ink_Canvas return; } - // 构建API URL,包含选中的分类参数 - var categories = Settings.Appearance.HitokotoCategories; - if (categories == null || categories.Count == 0) - { - // 如果没有选中任何分类,默认全选 - categories = new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; - Settings.Appearance.HitokotoCategories = categories; - } + // 构建 API URL:仅用于本次请求;null/空列表时本地采用默认全部分类,不写回 Settings,避免启动或拉取一言时触发无意义的配置持久化 + var stored = Settings.Appearance.HitokotoCategories; + var categoriesForRequest = (stored == null || stored.Count == 0) + ? new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" } + : stored; var urlBuilder = new StringBuilder("https://v1.hitokoto.cn/?encode=text"); - foreach (var category in categories) + foreach (var category in categoriesForRequest) { urlBuilder.Append($"&c={category}"); } @@ -1267,7 +1266,11 @@ namespace Ink_Canvas private async void ComboBoxChickenSoupSource_SelectionChanged(object sender, RoutedEventArgs e) { if (!isLoaded) return; - Settings.Appearance.ChickenSoupSource = ComboBoxChickenSoupSource.SelectedIndex; + int idx = ComboBoxChickenSoupSource.SelectedIndex; + if (Settings.Appearance.ChickenSoupSource == idx) + return; + + Settings.Appearance.ChickenSoupSource = idx; if (BtnHitokotoCustomize != null) { @@ -1317,6 +1320,9 @@ namespace Ink_Canvas // 存储各个分类的复选框 var categoryCheckBoxes = new Dictionary(); + var savedHitokoto = Settings.Appearance.HitokotoCategories; + bool implicitAllCategories = savedHitokoto == null || savedHitokoto.Count == 0; + // 创建分类复选框 foreach (var category in categories) { @@ -1326,7 +1332,7 @@ namespace Ink_Canvas Tag = category.Key, FontSize = 13, FontFamily = new FontFamily("Microsoft YaHei UI"), - IsChecked = Settings.Appearance.HitokotoCategories.Contains(category.Key), + IsChecked = implicitAllCategories || savedHitokoto.Contains(category.Key), Margin = new Thickness(0, 0, 0, 8) }; categoryCheckBoxes[category.Key] = checkBox; @@ -1335,7 +1341,7 @@ namespace Ink_Canvas // 全选复选框逻辑 bool isUpdatingSelectAll = false; - selectAllCheckBox.IsChecked = Settings.Appearance.HitokotoCategories.Count == categories.Count; + selectAllCheckBox.IsChecked = implicitAllCategories || savedHitokoto.Count == categories.Count; selectAllCheckBox.Checked += (s, args) => { @@ -5137,14 +5143,142 @@ namespace Ink_Canvas #endregion + /// + /// 将 JSON 树归一化后再比较:统一数值为 double、对象属性按名称排序、纯字符串数组按字典序排序(如 hitokotoCategories 顺序与文件不一致时仍视为相同)。 + /// + private static JToken NormalizeJsonForSettingsCompare(JToken token) + { + if (token == null || token.Type == JTokenType.Null) + return JValue.CreateNull(); + + switch (token.Type) + { + case JTokenType.Integer: + case JTokenType.Float: + return new JValue(token.Value()); + case JTokenType.Date: + return new JValue(token.Value().ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)); + case JTokenType.Object: + var o = (JObject)token; + var sorted = new JObject(); + foreach (var p in o.Properties().OrderBy(x => x.Name, StringComparer.Ordinal)) + sorted[p.Name] = NormalizeJsonForSettingsCompare(p.Value); + return sorted; + case JTokenType.Array: + var arr = (JArray)token; + var items = arr.Select(NormalizeJsonForSettingsCompare).ToList(); + if (items.Count > 0) + { + if (items.TrueForAll(x => x.Type == JTokenType.String)) + return new JArray(items.Cast().OrderBy(x => x.Value(), StringComparer.Ordinal)); + if (items.TrueForAll(x => x.Type == JTokenType.Integer || x.Type == JTokenType.Float)) + return new JArray(items.OrderBy(x => x.Value())); + } + return new JArray(items); + default: + return token.DeepClone(); + } + } + + private static bool SettingsFileContentSemanticallyEquals(string newJson, string existingJson) + { + if (string.IsNullOrWhiteSpace(existingJson)) + return false; + var a = NormalizeJsonForSettingsCompare(JToken.Parse(newJson)); + var b = NormalizeJsonForSettingsCompare(JToken.Parse(existingJson)); + return JToken.DeepEquals(a, b); + } + + /// 一言 API 官方分类字母,顺序固定,与自定义对话框一致。 + private static readonly string[] HitokotoCategoryCanonicalOrder = + { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; + + private static readonly HashSet HitokotoCategoryKnownKeys = + new HashSet(HitokotoCategoryCanonicalOrder, StringComparer.Ordinal); + + /// + /// 将 规范为固定顺序并去重,避免 JSON 仅因数组顺序/重复项变化而反复重写。 + /// null 或空列表表示「未持久化、运行时按全部分类」语义,不改为非空列表。 + /// + private static void StabilizeAppearanceHitokotoCategories() + { + var appearance = Settings?.Appearance; + if (appearance == null) + return; + + var list = appearance.HitokotoCategories; + if (list == null || list.Count == 0) + return; + + var normalizedTokens = list + .Select(x => x?.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + var canonical = new List(); + foreach (var key in HitokotoCategoryCanonicalOrder) + { + if (normalizedTokens.Any(x => string.Equals(x, key, StringComparison.Ordinal))) + canonical.Add(key); + } + + foreach (var key in normalizedTokens + .Where(x => !HitokotoCategoryKnownKeys.Contains(x)) + .Distinct(StringComparer.Ordinal) + .OrderBy(x => x, StringComparer.Ordinal)) + { + canonical.Add(key); + } + + if (canonical.Count == 0) + return; + + if (list.Count != canonical.Count || !list.SequenceEqual(canonical, StringComparer.Ordinal)) + appearance.HitokotoCategories = canonical; + } + /// /// 将当前内存中的 Settings 序列化为格式化的 JSON 并写入应用程序配置文件(位于 App.RootPath 下的 Configs 目录或根设置文件)。 /// /// - /// 在写入前会确保目标目录/文件具有写入权限(使用 ProcessProtectionManager)。任何写入失败或异常都会被吞掉,调用方不会收到异常抛出。 + /// 在写入前会确保目标目录/文件具有写入权限(使用 ProcessProtectionManager)。若与磁盘已有内容语义一致则跳过写入,避免启动或其它路径多次调用时重复刷盘。 + /// 在 LoadSettings 执行期间会延迟到加载结束再统一写盘,避免启动流程中多次保存。 + /// 任何写入失败或异常都会被吞掉,调用方不会收到异常抛出。 /// + private static int _settingsLoadReentrancyDepth; + private static bool _settingsSavePendingDuringLoad; + + /// 配对,在 LoadSettings 的 try/finally 中使用。 + private static void BeginDeferredSettingsSaveDuringLoad() + { + _settingsLoadReentrancyDepth++; + } + + private static void EndDeferredSettingsSaveDuringLoad() + { + if (_settingsLoadReentrancyDepth > 0) + _settingsLoadReentrancyDepth--; + if (_settingsLoadReentrancyDepth == 0 && _settingsSavePendingDuringLoad) + { + _settingsSavePendingDuringLoad = false; + SaveSettingsToFileCore(); + } + } + public static void SaveSettingsToFile() { + if (_settingsLoadReentrancyDepth > 0) + { + _settingsSavePendingDuringLoad = true; + return; + } + + SaveSettingsToFileCore(); + } + + private static void SaveSettingsToFileCore() + { + StabilizeAppearanceHitokotoCategories(); var text = JsonConvert.SerializeObject(Settings, Formatting.Indented); try { @@ -5155,6 +5289,22 @@ namespace Ink_Canvas } var path = App.RootPath + settingsFileName; + if (File.Exists(path)) + { + try + { + string existing = File.ReadAllText(path); + if (existing.Length > 0 && existing[0] == '\uFEFF') + existing = existing.TrimStart('\uFEFF'); + if (!string.IsNullOrWhiteSpace(existing) && SettingsFileContentSemanticallyEquals(text, existing)) + return; + } + catch + { + // 无法比较或解析失败时仍写入,避免丢失修复机会 + } + } + ProcessProtectionManager.WithWriteAccess(path, () => File.WriteAllText(path, text)); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index c9616ec1..069920e3 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -37,6 +37,9 @@ namespace Ink_Canvas /// 指示是否跳过自动更新检查;为 true 时不会在加载设置后执行自动更新检测。 private void LoadSettings(bool isStartup = false, bool skipAutoUpdateCheck = false) { + BeginDeferredSettingsSaveDuringLoad(); + try + { AppVersionTextBlock.Text = Assembly.GetExecutingAssembly().GetName().Version.ToString(); try { @@ -450,12 +453,6 @@ namespace Ink_Canvas : Visibility.Collapsed; } - // 初始化HitokotoCategories,如果为空则默认全选 - if (Settings.Appearance.HitokotoCategories == null || Settings.Appearance.HitokotoCategories.Count == 0) - { - Settings.Appearance.HitokotoCategories = new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; - } - ToggleSwitchEnableQuickPanel.IsOn = Settings.Appearance.IsShowQuickPanel; ToggleSwitchEnableSplashScreen.IsOn = Settings.Appearance.EnableSplashScreen; @@ -636,12 +633,14 @@ namespace Ink_Canvas } else { + int prev = Settings.PowerPointSettings.PPTButtonsDisplayOption; Settings.PowerPointSettings.PPTButtonsDisplayOption = 2222; CheckboxEnableLBPPTButton.IsChecked = true; CheckboxEnableRBPPTButton.IsChecked = true; CheckboxEnableLSPPTButton.IsChecked = true; CheckboxEnableRSPPTButton.IsChecked = true; - SaveSettingsToFile(); + if (prev != 2222) + SaveSettingsToFile(); } var sops = Settings.PowerPointSettings.PPTSButtonsOption.ToString(); @@ -655,11 +654,13 @@ namespace Ink_Canvas } else { + int prev = Settings.PowerPointSettings.PPTSButtonsOption; Settings.PowerPointSettings.PPTSButtonsOption = 221; CheckboxSPPTDisplayPage.IsChecked = true; CheckboxSPPTHalfOpacity.IsChecked = true; CheckboxSPPTBlackBackground.IsChecked = false; - SaveSettingsToFile(); + if (prev != 221) + SaveSettingsToFile(); } var bops = Settings.PowerPointSettings.PPTBButtonsOption.ToString(); @@ -673,11 +674,13 @@ namespace Ink_Canvas } else { + int prev = Settings.PowerPointSettings.PPTBButtonsOption; Settings.PowerPointSettings.PPTBButtonsOption = 121; CheckboxBPPTDisplayPage.IsChecked = false; CheckboxBPPTHalfOpacity.IsChecked = true; CheckboxBPPTBlackBackground.IsChecked = false; - SaveSettingsToFile(); + if (prev != 121) + SaveSettingsToFile(); } PPTButtonLeftPositionValueSlider.Value = Settings.PowerPointSettings.PPTLSButtonPosition; @@ -1322,6 +1325,14 @@ namespace Ink_Canvas // 刷新配置文件列表 try { RefreshConfigProfileList(); } catch (Exception ex) { LogHelper.WriteLogToFile($"刷新配置文件列表失败: {ex.Message}", LogHelper.LogType.Warning); } + + // 一言分类数组固定为 a–l 顺序并去重,避免仅顺序/重复导致配置反复变化 + StabilizeAppearanceHitokotoCategories(); + } + finally + { + EndDeferredSettingsSaveDuringLoad(); + } } /// From 277e46030d9d00940323f5562dca0bd3a119ce55 Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sat, 4 Apr 2026 22:16:37 +0800 Subject: [PATCH 04/27] =?UTF-8?q?improve:WinRT=E5=A2=A8=E8=BF=B9=E8=AF=86?= =?UTF-8?q?=E5=88=AB=E5=8F=8A=E7=AC=94=E9=94=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MW_SimulatePressure&InkToShape.cs | 426 ++++++++++-------- 1 file changed, 245 insertions(+), 181 deletions(-) diff --git a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs index 0b1935dc..f0220b3c 100644 --- a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs +++ b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs @@ -40,7 +40,7 @@ namespace Ink_Canvas /// private List circles = new List(); - /// 串行执行墨迹转形状(尤其 WinRT 异步延后),避免多笔 BeginInvoke 交错修改 newStrokes。 + /// 串行执行墨迹转形状。WinRT 必须在 Dispatcher 上延后执行:若在 StrokeCollected(UI 线程)内对 WinRT 路径同步 GetResult(),AnalyzeAsync 延续再贴回 UI 时会死锁。 private static readonly SemaphoreSlim InkToShapeSerial = new SemaphoreSlim(1, 1); /// @@ -58,6 +58,10 @@ namespace Ink_Canvas /// 每次收笔并入批次时递增;防抖 Tick 携带快照,识别过程中若又有新笔则放弃本轮替换。 private ulong _handwritingBeautifyScheduleRevision; + /// 画布笔画 → 手写纠正的识别输入(收笔时、笔锋/首段压感合成前的点集副本;替换墨迹时仍移除画布上的原笔画)。 + private readonly Dictionary _handwritingBeautifyInkInputByCanvasStroke = + new Dictionary(); + /// /// 矩形参考线的列表 /// @@ -138,6 +142,163 @@ namespace Ink_Canvas } } + private void RunStrokeCollectedPostShapeRecognitionTail(InkCanvasStrokeCollectedEventArgs e, bool wasStraightened) + { + try + { + foreach (var stylusPoint in e.Stroke.StylusPoints) + if ((stylusPoint.PressureFactor > 0.501 || stylusPoint.PressureFactor < 0.5) && + stylusPoint.PressureFactor != 0) + return; + + try + { + if (e.Stroke.StylusPoints.Count > 3) + { + var random = new Random(); + var _speed = GetPointSpeed( + e.Stroke.StylusPoints[random.Next(0, e.Stroke.StylusPoints.Count - 1)].ToPoint(), + e.Stroke.StylusPoints[random.Next(0, e.Stroke.StylusPoints.Count - 1)].ToPoint(), + e.Stroke.StylusPoints[random.Next(0, e.Stroke.StylusPoints.Count - 1)].ToPoint()); + + RandWindow.randSeed = (int)(_speed * 100000 * 1000); + } + } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } + + switch (Settings.Canvas.InkStyle) + { + case 1: + if (penType == 0) + try + { + var stylusPoints = new StylusPointCollection(); + var n = e.Stroke.StylusPoints.Count - 1; + + for (var i = 0; i <= n; i++) + { + var speed = GetPointSpeed(e.Stroke.StylusPoints[Math.Max(i - 1, 0)].ToPoint(), + e.Stroke.StylusPoints[i].ToPoint(), + e.Stroke.StylusPoints[Math.Min(i + 1, n)].ToPoint()); + var point = new StylusPoint(); + if (speed >= 0.25) + point.PressureFactor = (float)(0.5 - 0.3 * (Math.Min(speed, 1.5) - 0.3) / 1.2); + else if (speed >= 0.05) + point.PressureFactor = (float)0.5; + else + point.PressureFactor = (float)(0.5 + 0.4 * (0.05 - speed) / 0.05); + + point.X = e.Stroke.StylusPoints[i].X; + point.Y = e.Stroke.StylusPoints[i].Y; + stylusPoints.Add(point); + } + + e.Stroke.StylusPoints = stylusPoints; + } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } + + break; + case 0: + if (penType == 0) + try + { + var stylusPoints = new StylusPointCollection(); + var n = e.Stroke.StylusPoints.Count - 1; + var pressure = 0.1; + var x = 10; + if (n == 1) return; + if (n >= x) + { + for (var i = 0; i < n - x; i++) + { + var point = new StylusPoint(); + + point.PressureFactor = (float)0.5; + point.X = e.Stroke.StylusPoints[i].X; + point.Y = e.Stroke.StylusPoints[i].Y; + stylusPoints.Add(point); + } + + for (var i = n - x; i <= n; i++) + { + var point = new StylusPoint(); + + point.PressureFactor = (float)((0.5 - pressure) * (n - i) / x + pressure); + point.X = e.Stroke.StylusPoints[i].X; + point.Y = e.Stroke.StylusPoints[i].Y; + stylusPoints.Add(point); + } + } + else + { + for (var i = 0; i <= n; i++) + { + var point = new StylusPoint(); + + point.PressureFactor = (float)(0.4 * (n - i) / n + pressure); + point.X = e.Stroke.StylusPoints[i].X; + point.Y = e.Stroke.StylusPoints[i].Y; + stylusPoints.Add(point); + } + } + + e.Stroke.StylusPoints = stylusPoints; + } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } + + break; + case 3: + break; + } + } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } + + Debug.WriteLine($"墨迹平滑检查: UseAdvancedBezierSmoothing={Settings.Canvas.UseAdvancedBezierSmoothing}, wasStraightened={wasStraightened}"); + Debug.WriteLine($"异步平滑设置: UseAsyncInkSmoothing={Settings.Canvas.UseAsyncInkSmoothing}, _inkSmoothingManager={_inkSmoothingManager != null}"); + + if (Settings.Canvas.UseAdvancedBezierSmoothing && !wasStraightened) + { + try + { + Debug.WriteLine($"开始墨迹平滑处理: 原始点数={e.Stroke.StylusPoints.Count}, 直线拉直={wasStraightened}"); + + if (inkCanvas.Strokes.Contains(e.Stroke)) + { + if (Settings.Canvas.UseAsyncInkSmoothing && _inkSmoothingManager != null) + { + Debug.WriteLine("使用异步墨迹平滑"); + _ = ProcessStrokeAsync(e.Stroke); + } + else + { + var smoothedStroke = _inkSmoothingManager?.SmoothStroke(e.Stroke) ?? e.Stroke; + + if (smoothedStroke != e.Stroke) + { + SetNewBackupOfStroke(); + _currentCommitType = CommitReason.ShapeRecognition; + inkCanvas.Strokes.Remove(e.Stroke); + inkCanvas.Strokes.Add(smoothedStroke); + _currentCommitType = CommitReason.UserInput; + } + } + } + else + { + Debug.WriteLine("原始笔画不在画布中,跳过平滑处理"); + } + } + catch (Exception ex) + { + Debug.WriteLine($"高级贝塞尔曲线平滑失败: {ex.Message}"); + } + } + else if (Settings.Canvas.FitToCurve && !wasStraightened) + { + drawingAttributes.FitToCurve = true; + } + } + /// /// 处理墨水画布的笔画收集事件 /// @@ -222,6 +383,7 @@ namespace Ink_Canvas { inkCanvas.Opacity = 1; var touchPressureSimulationApplied = false; + var preBrushHandwritingPoints = CloneStylusPointCollectionForHandwritingInput(e.Stroke?.StylusPoints); if (Settings.Canvas.DisablePressure) { @@ -332,13 +494,14 @@ namespace Ink_Canvas } } + // 实时笔锋:勿依赖 DrawingAttributes.IgnorePressure。无压感触摸/鼠标等设备上,运行时仍可能为 true, + // 会导致不进入逻辑或进入后渲染仍忽略 PressureFactor;具体在 ApplyVelocityBrushTipFromSpeed 内关闭。 if (Settings.Canvas.InkStyle == 3 && !touchPressureSimulationApplied && !Settings.Canvas.DisablePressure && penType != 1 && e.Stroke?.DrawingAttributes != null && !e.Stroke.DrawingAttributes.IsHighlighter - && !e.Stroke.DrawingAttributes.IgnorePressure && e.Stroke.StylusPoints.Count >= 3) { ApplyVelocityBrushTipFromSpeed(e.Stroke); @@ -405,6 +568,9 @@ namespace Ink_Canvas if (wasStraightened && inkCanvas.Strokes.Count > 0) strokeForHandwritingBeautify = inkCanvas.Strokes[inkCanvas.Strokes.Count - 1]; + if (wasStraightened && strokeForHandwritingBeautify != null) + preBrushHandwritingPoints = CloneStylusPointCollectionForHandwritingInput(strokeForHandwritingBeautify.StylusPoints); + if (ShapeRecognitionRouter.ShouldRunShapeRecognition( Settings.InkToShape.IsInkToShapeEnabled, ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine))) @@ -741,202 +907,56 @@ namespace Ink_Canvas } } - void InkToShapeProcess() + bool InkToShapeProcess() { var engineMode = ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine); if (ShapeRecognitionRouter.ResolveUseWinRt(engineMode)) { + var strokeHw = strokeForHandwritingBeautify; + var preBrushHwPts = preBrushHandwritingPoints; + var wsTail = wasStraightened; Dispatcher.BeginInvoke(new Action(async () => { try { await InkToShapeProcessCoreAsync(); if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) - ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke); + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeHw, isBoardBrushStroke, preBrushHwPts); + RunStrokeCollectedPostShapeRecognitionTail(e, wsTail); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } }), DispatcherPriority.Normal); - return; + return true; + } + + try + { + InkToShapeProcessCoreAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); } - InkToShapeProcessCoreAsync().GetAwaiter().GetResult(); if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) - ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke); + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke, preBrushHandwritingPoints); + return false; } - InkToShapeProcess(); + if (InkToShapeProcess()) + return; } else if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) { - ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke); - } - - foreach (var stylusPoint in e.Stroke.StylusPoints) - //LogHelper.WriteLogToFile(stylusPoint.PressureFactor.ToString(), LogHelper.LogType.Info); - // 检查是否是压感笔书写 - //if (stylusPoint.PressureFactor != 0.5 && stylusPoint.PressureFactor != 0) - if ((stylusPoint.PressureFactor > 0.501 || stylusPoint.PressureFactor < 0.5) && - stylusPoint.PressureFactor != 0) - return; - - try - { - if (e.Stroke.StylusPoints.Count > 3) - { - var random = new Random(); - var _speed = GetPointSpeed( - e.Stroke.StylusPoints[random.Next(0, e.Stroke.StylusPoints.Count - 1)].ToPoint(), - e.Stroke.StylusPoints[random.Next(0, e.Stroke.StylusPoints.Count - 1)].ToPoint(), - e.Stroke.StylusPoints[random.Next(0, e.Stroke.StylusPoints.Count - 1)].ToPoint()); - - RandWindow.randSeed = (int)(_speed * 100000 * 1000); - } - } - catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - - switch (Settings.Canvas.InkStyle) - { - case 1: - if (penType == 0) - try - { - var stylusPoints = new StylusPointCollection(); - var n = e.Stroke.StylusPoints.Count - 1; - var s = ""; - - for (var i = 0; i <= n; i++) - { - var speed = GetPointSpeed(e.Stroke.StylusPoints[Math.Max(i - 1, 0)].ToPoint(), - e.Stroke.StylusPoints[i].ToPoint(), - e.Stroke.StylusPoints[Math.Min(i + 1, n)].ToPoint()); - s += speed + "\t"; - var point = new StylusPoint(); - if (speed >= 0.25) - point.PressureFactor = (float)(0.5 - 0.3 * (Math.Min(speed, 1.5) - 0.3) / 1.2); - else if (speed >= 0.05) - point.PressureFactor = (float)0.5; - else - point.PressureFactor = (float)(0.5 + 0.4 * (0.05 - speed) / 0.05); - - point.X = e.Stroke.StylusPoints[i].X; - point.Y = e.Stroke.StylusPoints[i].Y; - stylusPoints.Add(point); - } - - e.Stroke.StylusPoints = stylusPoints; - } - catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - - break; - case 0: - if (penType == 0) - try - { - var stylusPoints = new StylusPointCollection(); - var n = e.Stroke.StylusPoints.Count - 1; - var pressure = 0.1; - var x = 10; - if (n == 1) return; - if (n >= x) - { - for (var i = 0; i < n - x; i++) - { - var point = new StylusPoint(); - - point.PressureFactor = (float)0.5; - point.X = e.Stroke.StylusPoints[i].X; - point.Y = e.Stroke.StylusPoints[i].Y; - stylusPoints.Add(point); - } - - for (var i = n - x; i <= n; i++) - { - var point = new StylusPoint(); - - point.PressureFactor = (float)((0.5 - pressure) * (n - i) / x + pressure); - point.X = e.Stroke.StylusPoints[i].X; - point.Y = e.Stroke.StylusPoints[i].Y; - stylusPoints.Add(point); - } - } - else - { - for (var i = 0; i <= n; i++) - { - var point = new StylusPoint(); - - point.PressureFactor = (float)(0.4 * (n - i) / n + pressure); - point.X = e.Stroke.StylusPoints[i].X; - point.Y = e.Stroke.StylusPoints[i].Y; - stylusPoints.Add(point); - } - } - - e.Stroke.StylusPoints = stylusPoints; - } - catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - - break; - case 3: - break; + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke, preBrushHandwritingPoints); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - // 应用高级贝塞尔曲线平滑(仅在未进行直线拉直时) - Debug.WriteLine($"墨迹平滑检查: UseAdvancedBezierSmoothing={Settings.Canvas.UseAdvancedBezierSmoothing}, wasStraightened={wasStraightened}"); - Debug.WriteLine($"异步平滑设置: UseAsyncInkSmoothing={Settings.Canvas.UseAsyncInkSmoothing}, _inkSmoothingManager={_inkSmoothingManager != null}"); - - if (Settings.Canvas.UseAdvancedBezierSmoothing && !wasStraightened) - { - try - { - Debug.WriteLine($"开始墨迹平滑处理: 原始点数={e.Stroke.StylusPoints.Count}, 直线拉直={wasStraightened}"); - - // 检查原始笔画是否仍然存在于画布中 - if (inkCanvas.Strokes.Contains(e.Stroke)) - { - // 使用新的异步墨迹平滑管理器 - if (Settings.Canvas.UseAsyncInkSmoothing && _inkSmoothingManager != null) - { - Debug.WriteLine("使用异步墨迹平滑"); - // 异步处理 - _ = ProcessStrokeAsync(e.Stroke); - } - else - { - // 同步处理(向后兼容) - var smoothedStroke = _inkSmoothingManager?.SmoothStroke(e.Stroke) ?? e.Stroke; - - if (smoothedStroke != e.Stroke) - { - // 替换原始笔画 - SetNewBackupOfStroke(); - _currentCommitType = CommitReason.ShapeRecognition; - inkCanvas.Strokes.Remove(e.Stroke); - inkCanvas.Strokes.Add(smoothedStroke); - _currentCommitType = CommitReason.UserInput; - } - } - } - else - { - Debug.WriteLine("原始笔画不在画布中,跳过平滑处理"); - } - } - catch (Exception ex) - { - // 如果高级平滑失败,回退到原始笔画 - Debug.WriteLine($"高级贝塞尔曲线平滑失败: {ex.Message}"); - } - } - else if (Settings.Canvas.FitToCurve && !wasStraightened) - { - drawingAttributes.FitToCurve = true; - } + RunStrokeCollectedPostShapeRecognitionTail(e, wasStraightened); } /// @@ -2106,6 +2126,7 @@ namespace Ink_Canvas /// /// 将沿线速度映射为压感并与硬件压感混合,快写略细、慢写略粗;与 Inkeys 中 RTSSpeed 驱动的笔锋类似,在落笔后统一施加。 + /// 无压感设备上系统可能将 置为 true,此处强制关闭以便粗细随合成压感变化(与「屏蔽压感」无关:调用方已保证未屏蔽)。 /// private void ApplyVelocityBrushTipFromSpeed(Stroke stroke) { @@ -2115,6 +2136,9 @@ namespace Ink_Canvas if (mix <= 0 || stroke == null) return; if (mix > 1) mix = 1; + if (stroke.DrawingAttributes != null) + stroke.DrawingAttributes.IgnorePressure = false; + var pts = stroke.StylusPoints; if (pts.Count < 3) return; @@ -2700,8 +2724,12 @@ namespace Ink_Canvas { for (var i = _handwritingRecentStrokesForBeautify.Count - 1; i >= 0; i--) { - if (!inkCanvas.Strokes.Contains(_handwritingRecentStrokesForBeautify[i])) + var s = _handwritingRecentStrokesForBeautify[i]; + if (!inkCanvas.Strokes.Contains(s)) + { + _handwritingBeautifyInkInputByCanvasStroke.Remove(s); _handwritingRecentStrokesForBeautify.RemoveAt(i); + } } } @@ -2716,8 +2744,23 @@ namespace Ink_Canvas _handwritingBeautifyDebounceTimer.Tick += HandwritingBeautifyDebounceTimer_Tick; } + /// 深拷贝点集,供手写纠正识别输入(与笔锋/二次压感合成前的画布数据一致)。 + private static StylusPointCollection CloneStylusPointCollectionForHandwritingInput(StylusPointCollection source) + { + if (source == null || source.Count == 0) + return null; + var copy = new StylusPointCollection(); + foreach (StylusPoint p in source) + copy.Add(new StylusPoint(p.X, p.Y, p.PressureFactor)); + return copy; + } + /// 并入批次并重置 1s 计时器(多笔需停笔满延迟后才矫正)。 - private void ScheduleHandwritingGlyphReplaceAfterStrokeCollected(Stroke strokeForBeautify, bool isBoardBrushStroke) + /// 笔锋与后续 InkStyle 压感合成前的点集;为 null 时识别输入与画布笔画一致(兼容旧行为)。 + private void ScheduleHandwritingGlyphReplaceAfterStrokeCollected( + Stroke strokeForBeautify, + bool isBoardBrushStroke, + StylusPointCollection preBrushHandwritingPoints = null) { if (!Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) return; @@ -2730,6 +2773,18 @@ namespace Ink_Canvas _handwritingBeautifyScheduleRevision++; + if (preBrushHandwritingPoints != null && preBrushHandwritingPoints.Count > 0) + { + _handwritingBeautifyInkInputByCanvasStroke[strokeForBeautify] = new Stroke(preBrushHandwritingPoints) + { + DrawingAttributes = strokeForBeautify.DrawingAttributes.Clone() + }; + } + else + { + _handwritingBeautifyInkInputByCanvasStroke.Remove(strokeForBeautify); + } + var alreadyInBatch = false; foreach (Stroke x in _handwritingRecentStrokesForBeautify) { @@ -2780,30 +2835,36 @@ namespace Ink_Canvas PruneHandwritingBeautifyBatch(); - var input = new StrokeCollection(); + var canvasStrokes = new StrokeCollection(); + var recognitionInput = new StrokeCollection(); foreach (Stroke s in _handwritingRecentStrokesForBeautify) { - if (inkCanvas.Strokes.Contains(s)) - input.Add(s); + if (!inkCanvas.Strokes.Contains(s)) + continue; + canvasStrokes.Add(s); + if (_handwritingBeautifyInkInputByCanvasStroke.TryGetValue(s, out var inkInput) && inkInput != null) + recognitionInput.Add(inkInput); + else + recognitionInput.Add(s); } - if (input.Count == 0) + if (canvasStrokes.Count == 0) return; if (_handwritingBeautifyScheduleRevision != revisionWhenIdle) return; var shapeMode = ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine); - var result = await InkRecognizeHelper.CorrectHandwritingStrokesUnifiedAsync(input, shapeMode).ConfigureAwait(true); + var result = await InkRecognizeHelper.CorrectHandwritingStrokesUnifiedAsync(recognitionInput, shapeMode).ConfigureAwait(true); if (_handwritingBeautifyScheduleRevision != revisionWhenIdle) return; if (result == null || result.Count == 0) return; - if (ReferenceEquals(result, input)) + if (ReferenceEquals(result, recognitionInput)) return; var anyInputStillPresent = false; - foreach (Stroke s in input) + foreach (Stroke s in canvasStrokes) { if (inkCanvas.Strokes.Contains(s)) { @@ -2817,7 +2878,7 @@ namespace Ink_Canvas SetNewBackupOfStroke(); _currentCommitType = CommitReason.ShapeRecognition; - foreach (Stroke s in input) + foreach (Stroke s in canvasStrokes) { if (inkCanvas.Strokes.Contains(s)) inkCanvas.Strokes.Remove(s); @@ -2827,8 +2888,11 @@ namespace Ink_Canvas inkCanvas.Strokes.Add(s); _currentCommitType = CommitReason.UserInput; - foreach (Stroke s in input) + foreach (Stroke s in canvasStrokes) + { _handwritingRecentStrokesForBeautify.Remove(s); + _handwritingBeautifyInkInputByCanvasStroke.Remove(s); + } PruneHandwritingBeautifyBatch(); } From 3cf1ea438b155c4e22d5cc59fb3ada43f33e030e Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sat, 4 Apr 2026 22:21:34 +0800 Subject: [PATCH 05/27] =?UTF-8?q?fix:=E4=B8=80=E8=A8=80API=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=87=8D=E5=A4=8D=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index 069920e3..e4227ac1 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -633,14 +633,12 @@ namespace Ink_Canvas } else { - int prev = Settings.PowerPointSettings.PPTButtonsDisplayOption; Settings.PowerPointSettings.PPTButtonsDisplayOption = 2222; CheckboxEnableLBPPTButton.IsChecked = true; CheckboxEnableRBPPTButton.IsChecked = true; CheckboxEnableLSPPTButton.IsChecked = true; CheckboxEnableRSPPTButton.IsChecked = true; - if (prev != 2222) - SaveSettingsToFile(); + SaveSettingsToFile(); } var sops = Settings.PowerPointSettings.PPTSButtonsOption.ToString(); @@ -654,13 +652,11 @@ namespace Ink_Canvas } else { - int prev = Settings.PowerPointSettings.PPTSButtonsOption; Settings.PowerPointSettings.PPTSButtonsOption = 221; CheckboxSPPTDisplayPage.IsChecked = true; CheckboxSPPTHalfOpacity.IsChecked = true; CheckboxSPPTBlackBackground.IsChecked = false; - if (prev != 221) - SaveSettingsToFile(); + SaveSettingsToFile(); } var bops = Settings.PowerPointSettings.PPTBButtonsOption.ToString(); @@ -674,13 +670,11 @@ namespace Ink_Canvas } else { - int prev = Settings.PowerPointSettings.PPTBButtonsOption; Settings.PowerPointSettings.PPTBButtonsOption = 121; CheckboxBPPTDisplayPage.IsChecked = false; CheckboxBPPTHalfOpacity.IsChecked = true; CheckboxBPPTBlackBackground.IsChecked = false; - if (prev != 121) - SaveSettingsToFile(); + SaveSettingsToFile(); } PPTButtonLeftPositionValueSlider.Value = Settings.PowerPointSettings.PPTLSButtonPosition; @@ -1325,9 +1319,6 @@ namespace Ink_Canvas // 刷新配置文件列表 try { RefreshConfigProfileList(); } catch (Exception ex) { LogHelper.WriteLogToFile($"刷新配置文件列表失败: {ex.Message}", LogHelper.LogType.Warning); } - - // 一言分类数组固定为 a–l 顺序并去重,避免仅顺序/重复导致配置反复变化 - StabilizeAppearanceHitokotoCategories(); } finally { From 66afe271c57e5dcf8ee17e1e6c302c72460353f3 Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sat, 4 Apr 2026 22:56:34 +0800 Subject: [PATCH 06/27] =?UTF-8?q?improve:=E5=AE=9E=E6=97=B6=E7=AC=94?= =?UTF-8?q?=E9=94=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Helpers/MultiTouchInput.cs | 26 ++++-- .../MW_SimulatePressure&InkToShape.cs | 92 ++++++++++++------- Ink Canvas/MainWindow_cs/MW_TouchEvents.cs | 18 +++- Ink Canvas/Resources/Settings.cs | 2 +- 4 files changed, 98 insertions(+), 40 deletions(-) diff --git a/Ink Canvas/Helpers/MultiTouchInput.cs b/Ink Canvas/Helpers/MultiTouchInput.cs index 40391fc0..f1a745b1 100644 --- a/Ink Canvas/Helpers/MultiTouchInput.cs +++ b/Ink Canvas/Helpers/MultiTouchInput.cs @@ -111,6 +111,14 @@ namespace Ink_Canvas.Helpers /// /// 绘制点段到新的DrawingVisual /// + private static double PressureToVisualScale(float pressureFactor, bool ignorePressure) + { + if (ignorePressure) + return 1.0; + // 与 WPF 墨迹观感接近:0.5 为标称,压低变细、抬高变粗(预览此前固定 Pen 宽,等同忽略压感) + return Math.Max(0.22, Math.Min(2.1, 0.42 + 1.16 * pressureFactor)); + } + private void DrawSegmentToNewVisual(int startIndex, int endIndex) { if (Stroke == null || Stroke.StylusPoints.Count == 0 || _visualCanvas == null) return; @@ -118,6 +126,7 @@ namespace Ink_Canvas.Helpers var points = Stroke.StylusPoints; var drawingAttributes = Stroke.DrawingAttributes; + var ignorePressure = drawingAttributes.IgnorePressure; // 创建新的DrawingVisual用于绘制这个点段 var segmentVisual = new DrawingVisual(); @@ -128,11 +137,6 @@ namespace Ink_Canvas.Helpers using (var dc = segmentVisual.RenderOpen()) { - var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), drawingAttributes.Width); - pen.StartLineCap = PenLineCap.Round; - pen.EndLineCap = PenLineCap.Round; - pen.LineJoin = PenLineJoin.Round; - // 绘制指定范围内的点段 if (endIndex - startIndex >= 2) { @@ -141,6 +145,15 @@ namespace Ink_Canvas.Helpers { var startPoint = new Point(points[i].X, points[i].Y); var endPoint = new Point(points[i + 1].X, points[i + 1].Y); + var s0 = PressureToVisualScale(points[i].PressureFactor, ignorePressure); + var s1 = PressureToVisualScale(points[i + 1].PressureFactor, ignorePressure); + var thickness = Math.Max(0.35, (drawingAttributes.Width * s0 + drawingAttributes.Width * s1) / 2.0); + var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), thickness) + { + StartLineCap = PenLineCap.Round, + EndLineCap = PenLineCap.Round, + LineJoin = PenLineJoin.Round + }; dc.DrawLine(pen, startPoint, endPoint); } } @@ -149,8 +162,9 @@ namespace Ink_Canvas.Helpers // 只有一个点,绘制圆点 var brush = new SolidColorBrush(drawingAttributes.Color); var point = points[startIndex]; + var s = PressureToVisualScale(point.PressureFactor, ignorePressure); dc.DrawEllipse(brush, null, new Point(point.X, point.Y), - drawingAttributes.Width / 2, drawingAttributes.Height / 2); + drawingAttributes.Width * s / 2, drawingAttributes.Height * s / 2); } } diff --git a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs index f0220b3c..48155dcf 100644 --- a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs +++ b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs @@ -180,16 +180,12 @@ namespace Ink_Canvas var speed = GetPointSpeed(e.Stroke.StylusPoints[Math.Max(i - 1, 0)].ToPoint(), e.Stroke.StylusPoints[i].ToPoint(), e.Stroke.StylusPoints[Math.Min(i + 1, n)].ToPoint()); - var point = new StylusPoint(); - if (speed >= 0.25) - point.PressureFactor = (float)(0.5 - 0.3 * (Math.Min(speed, 1.5) - 0.3) / 1.2); - else if (speed >= 0.05) - point.PressureFactor = (float)0.5; - else - point.PressureFactor = (float)(0.5 + 0.4 * (0.05 - speed) / 0.05); - - point.X = e.Stroke.StylusPoints[i].X; - point.Y = e.Stroke.StylusPoints[i].Y; + var point = new StylusPoint + { + PressureFactor = RateBasedPressureFactorFromPointSpeed(speed), + X = e.Stroke.StylusPoints[i].X, + Y = e.Stroke.StylusPoints[i].Y + }; stylusPoints.Add(point); } @@ -423,16 +419,12 @@ namespace Ink_Canvas var speed = GetPointSpeed(e.Stroke.StylusPoints[Math.Max(i - 1, 0)].ToPoint(), e.Stroke.StylusPoints[i].ToPoint(), e.Stroke.StylusPoints[Math.Min(i + 1, n)].ToPoint()); - var point = new StylusPoint(); - if (speed >= 0.25) - point.PressureFactor = (float)(0.5 - 0.3 * (Math.Min(speed, 1.5) - 0.3) / 1.2); - else if (speed >= 0.05) - point.PressureFactor = (float)0.5; - else - point.PressureFactor = (float)(0.5 + 0.4 * (0.05 - speed) / 0.05); - - point.X = e.Stroke.StylusPoints[i].X; - point.Y = e.Stroke.StylusPoints[i].Y; + var point = new StylusPoint + { + PressureFactor = RateBasedPressureFactorFromPointSpeed(speed), + X = e.Stroke.StylusPoints[i].X, + Y = e.Stroke.StylusPoints[i].Y + }; stylusPoints.Add(point); } @@ -498,7 +490,6 @@ namespace Ink_Canvas // 会导致不进入逻辑或进入后渲染仍忽略 PressureFactor;具体在 ApplyVelocityBrushTipFromSpeed 内关闭。 if (Settings.Canvas.InkStyle == 3 && !touchPressureSimulationApplied - && !Settings.Canvas.DisablePressure && penType != 1 && e.Stroke?.DrawingAttributes != null && !e.Stroke.DrawingAttributes.IsHighlighter @@ -2124,8 +2115,49 @@ namespace Ink_Canvas / 20; } + private static float RateBasedPressureFactorFromPointSpeed(double speed) + { + if (speed >= 0.25) + return (float)(0.5 - 0.3 * (Math.Min(speed, 1.5) - 0.3) / 1.2); + if (speed >= 0.05) + return 0.5f; + return (float)(0.5 + 0.4 * (0.05 - speed) / 0.05); + } + + private static float RealtimeBrushTipMixRatePressureFromSpeed(double speed) + { + if (speed < 0) speed = 0; + const double slowRef = 0.012; + const double fastRef = 0.5; + var t = (speed - slowRef) / (fastRef - slowRef); + if (t < 0) t = 0; + else if (t > 1) t = 1; + t = t * t * (3.0 - 2.0 * t); + const double pThick = 0.9; + const double pThin = 0.22; + var p = pThick + (pThin - pThick) * t; + return (float)Math.Max(0.08, Math.Min(1.0, p)); + } + + private static bool IsStrokePressureApproximatelyConstant(StylusPointCollection pts, out float meanPf) + { + meanPf = 0.5f; + if (pts == null || pts.Count == 0) return true; + double sum = 0; + foreach (StylusPoint p in pts) sum += p.PressureFactor; + meanPf = (float)(sum / pts.Count); + const float tol = 0.04f; + foreach (StylusPoint p in pts) + { + if (Math.Abs(p.PressureFactor - meanPf) > tol) + return false; + } + + return true; + } + /// - /// 将沿线速度映射为压感并与硬件压感混合,快写略细、慢写略粗;与 Inkeys 中 RTSSpeed 驱动的笔锋类似,在落笔后统一施加。 + /// 将沿线速度映射为压感并与硬件压感混合,快写略细、慢写略粗;在落笔时(及手写笔移动时由调用方)统一施加。 /// 无压感设备上系统可能将 置为 true,此处强制关闭以便粗细随合成压感变化(与「屏蔽压感」无关:调用方已保证未屏蔽)。 /// private void ApplyVelocityBrushTipFromSpeed(Stroke stroke) @@ -2142,6 +2174,10 @@ namespace Ink_Canvas var pts = stroke.StylusPoints; if (pts.Count < 3) return; + var effectiveMix = (float)mix; + if (IsStrokePressureApproximatelyConstant(pts, out _)) + effectiveMix = Math.Max(effectiveMix, 0.78f); + var n = pts.Count - 1; var stylusPoints = new StylusPointCollection(); @@ -2152,18 +2188,10 @@ namespace Ink_Canvas pts[i].ToPoint(), pts[Math.Min(i + 1, n)].ToPoint()); - float speedPressure; - if (speed >= 0.25) - speedPressure = (float)(0.5 - 0.3 * (Math.Min(speed, 1.5) - 0.3) / 1.2); - else if (speed >= 0.05) - speedPressure = 0.5f; - else - speedPressure = (float)(0.5 + 0.4 * (0.05 - speed) / 0.05); - - speedPressure = (float)Math.Max(0.08, Math.Min(1.0, speedPressure)); + var speedPressure = RealtimeBrushTipMixRatePressureFromSpeed(speed); var basePf = pts[i].PressureFactor; - var blended = (float)((1.0 - mix) * basePf + mix * speedPressure); + var blended = (1.0f - effectiveMix) * basePf + effectiveMix * speedPressure; blended = (float)Math.Max(0.08, Math.Min(1.0, blended)); var p = new StylusPoint(pts[i].X, pts[i].Y, blended); diff --git a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs index 48f46d7e..7f44331e 100644 --- a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs +++ b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs @@ -542,7 +542,23 @@ namespace Ink_Canvas var stylusPointCollection = e.GetStylusPoints(this); foreach (var stylusPoint in stylusPointCollection) strokeVisual.Add(new StylusPoint(stylusPoint.X, stylusPoint.Y, stylusPoint.PressureFactor)); - strokeVisual.Redraw(); + + // 实时笔锋:在绘制过程中更新压感并整笔重绘预览;否则预览层固定线宽,收笔后改点集也看不到笔锋变化。 + var committedStroke = strokeVisual.Stroke; + if (committedStroke != null + && Settings.Canvas.InkStyle == 3 + && penType == 0 + && committedStroke.DrawingAttributes != null + && !committedStroke.DrawingAttributes.IsHighlighter + && committedStroke.StylusPoints.Count >= 3) + { + ApplyVelocityBrushTipFromSpeed(committedStroke); + strokeVisual.ForceRedraw(); + } + else + { + strokeVisual.Redraw(); + } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } } diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index 88eef9c6..303d5102 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -147,7 +147,7 @@ namespace Ink_Canvas [JsonProperty("eraserAutoSwitchBackDelaySeconds")] public int EraserAutoSwitchBackDelaySeconds { get; set; } = 10; // 默认10秒 [JsonProperty("velocityBrushTipMix")] - public double VelocityBrushTipMix { get; set; } = 0.22; + public double VelocityBrushTipMix { get; set; } = 0.45; [JsonProperty("enableVelocityBrushTip")] public bool EnableVelocityBrushTip { get; set; } From c33ac03255357d09912ed097045ad60171d07d11 Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sat, 4 Apr 2026 23:06:16 +0800 Subject: [PATCH 07/27] =?UTF-8?q?improve:=E5=A2=A8=E8=BF=B9=E6=B8=B2?= =?UTF-8?q?=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MW_SimulatePressure&InkToShape.cs | 53 +++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs index 48155dcf..fc304e72 100644 --- a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs +++ b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs @@ -900,41 +900,28 @@ namespace Ink_Canvas bool InkToShapeProcess() { - var engineMode = ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine); - if (ShapeRecognitionRouter.ResolveUseWinRt(engineMode)) + // WinRT 与非 WinRT 均延后到 Dispatcher 上异步执行:在 StrokeCollected 内对 + // InkToShapeProcessCoreAsync 同步 GetResult() 会长时间阻塞 UI 线程(抬笔卡顿); + // WinRT 路径若同步等待还可能与贴回 UI 的延续死锁(见类注释 InkToShapeSerial)。 + var strokeHw = strokeForHandwritingBeautify; + var preBrushHwPts = preBrushHandwritingPoints; + var wsTail = wasStraightened; + // ApplicationIdle:在更高优先级(布局/输入/本帧渲染)之后执行,减轻抬笔瞬间主线程“顶死”感。 + Dispatcher.BeginInvoke(new Action(async () => { - var strokeHw = strokeForHandwritingBeautify; - var preBrushHwPts = preBrushHandwritingPoints; - var wsTail = wasStraightened; - Dispatcher.BeginInvoke(new Action(async () => + try { - try - { - await InkToShapeProcessCoreAsync(); - if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) - ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeHw, isBoardBrushStroke, preBrushHwPts); - RunStrokeCollectedPostShapeRecognitionTail(e, wsTail); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine(ex); - } - }), DispatcherPriority.Normal); - return true; - } - - try - { - InkToShapeProcessCoreAsync().GetAwaiter().GetResult(); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine(ex); - } - - if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) - ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke, preBrushHandwritingPoints); - return false; + await InkToShapeProcessCoreAsync(); + if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeHw, isBoardBrushStroke, preBrushHwPts); + RunStrokeCollectedPostShapeRecognitionTail(e, wsTail); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); + } + }), DispatcherPriority.ApplicationIdle); + return true; } if (InkToShapeProcess()) From fc4a3a1194303cc7421a102267efe04f8e04a5a1 Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sat, 4 Apr 2026 23:34:26 +0800 Subject: [PATCH 08/27] improve:UI --- Ink Canvas/MainWindow.xaml | 49 ++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 035591b9..7a2fb819 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -134,6 +134,7 @@ + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 deleted file mode 100644 index 6ca80e90..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml.cs +++ /dev/null @@ -1,466 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Threading; - -namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher -{ - /// - /// LauncherWindow.xaml 的交互逻辑 - /// - public partial class LauncherWindow : Window - { - /// - /// 父插件 - /// - private readonly SuperLauncherPlugin _plugin; - - /// - /// 是否处于固定模式 - /// - private bool _isFixMode; - - /// - /// 应用项按钮列表 - /// - private readonly Dictionary _appButtons = new Dictionary(); - - /// - /// 拖拽中的按钮 - /// - private Button _draggingButton; - - /// - /// 拖拽开始位置 - /// - private Point _dragStartPoint; - - /// - /// 构造函数 - /// - public LauncherWindow(SuperLauncherPlugin plugin) - { - InitializeComponent(); - - _plugin = plugin; - - // 加载应用项 - LoadLauncherItems(); - - // 添加鼠标按下事件(用于拖动窗口) - MouseDown += (s, e) => - { - if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed) - { - 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}"); - - // 首先标记窗口正在关闭 - IsClosing = true; - - // 创建一个应用启动任务 - var launchTask = new Task(() => - { - try - { - // 等待一段时间,确保窗口关闭流程已经开始 - Thread.Sleep(200); - - // 使用UI线程启动应用 - Application.Current.Dispatcher.Invoke(() => - { - try - { - // 检查应用路径是否存在 - if (File.Exists(appPath) || !appPath.Contains(":\\")) - { - // 创建进程启动信息 - var psi = new ProcessStartInfo - { - FileName = appPath, - UseShellExecute = true, - }; - - // 启动应用程序 - var process = Process.Start(psi); - LogHelper.WriteLogToFile($"应用程序 {appName} 已启动"); - } - 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 (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - - // 启动应用程序任务 - launchTask.Start(); - }), 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 (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); } - } - } - - #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); - } - }), DispatcherPriority.Background); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"窗口失去焦点关闭时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 窗口是否正在关闭 - /// - private bool IsClosing { get; set; } - - /// - /// 重写OnClosing方法,标记窗口正在关闭 - /// - protected override void OnClosing(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 deleted file mode 100644 index 555c7bf9..00000000 --- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncherPlugin.cs +++ /dev/null @@ -1,589 +0,0 @@ -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, 1); - - 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; - - #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("超级启动台插件已初始化"); - } - 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("超级启动台按钮已创建"); - } - - // 添加启动台按钮到浮动栏 - AddLauncherButtonToFloatingBar(); - - // 设置启用状态 - base.Enable(); - - // 保存插件配置 - SavePluginSettings(); - - LogHelper.WriteLogToFile("超级启动台插件已启用"); - } - 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("超级启动台插件已禁用"); - } - 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("超级启动台插件设置已保存"); - } - 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("超级启动台配置已保存"); - } - 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 = @"C:\Windows\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("启动台按钮已添加到浮动栏左侧"); - } - else - { - floatingBar.Children.Add(buttonElement); - LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏右侧"); - } - - _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("启动台按钮已从浮动栏移除"); - } - - _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}"); - } - } - 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/IActionService.cs b/Ink Canvas/Helpers/Plugins/IActionService.cs deleted file mode 100644 index 99c61473..00000000 --- a/Ink Canvas/Helpers/Plugins/IActionService.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 操作服务接口,统一所有执行操作相关的方法 - /// - public interface IActionService - { - #region 画布操作 - - /// - /// 清除当前画布 - /// - void ClearCanvas(); - - /// - /// 清除所有画布 - /// - void ClearAllCanvases(); - - /// - /// 添加新页面 - /// - void AddNewPage(); - - /// - /// 删除当前页面 - /// - void DeleteCurrentPage(); - - /// - /// 切换到指定页面 - /// - /// 页面索引 - void SwitchToPage(int pageIndex); - - /// - /// 切换到下一页 - /// - void NextPage(); - - /// - /// 切换到上一页 - /// - void PreviousPage(); - - #endregion - - #region 绘制操作 - - /// - /// 设置绘制模式 - /// - /// 绘制模式 - void SetDrawingMode(int mode); - - /// - /// 设置笔触宽度 - /// - /// 宽度 - void SetInkWidth(double width); - - /// - /// 设置笔触颜色 - /// - /// 颜色 - void SetInkColor(Color color); - - /// - /// 设置高亮笔宽度 - /// - /// 宽度 - void SetHighlighterWidth(double width); - - /// - /// 设置橡皮擦大小 - /// - /// 大小 - void SetEraserSize(int size); - - /// - /// 设置橡皮擦类型 - /// - /// 类型 - void SetEraserType(int type); - - /// - /// 设置橡皮擦形状 - /// - /// 形状 - void SetEraserShape(int shape); - - /// - /// 设置笔触透明度 - /// - /// 透明度 - void SetInkAlpha(double alpha); - - /// - /// 设置笔触样式 - /// - /// 样式 - void SetInkStyle(int style); - - /// - /// 设置背景颜色 - /// - /// 颜色 - void SetBackgroundColor(string color); - - #endregion - - #region 文件操作 - - /// - /// 保存画布内容 - /// - /// 文件路径 - void SaveCanvas(string filePath); - - /// - /// 加载画布内容 - /// - /// 文件路径 - void LoadCanvas(string filePath); - - /// - /// 导出为图片 - /// - /// 文件路径 - /// 图片格式 - void ExportAsImage(string filePath, string format); - - /// - /// 导出为PDF - /// - /// 文件路径 - void ExportAsPDF(string filePath); - - #endregion - - #region 撤销重做操作 - - /// - /// 撤销操作 - /// - void Undo(); - - /// - /// 重做操作 - /// - void Redo(); - - #endregion - - #region 选择操作 - - /// - /// 全选 - /// - void SelectAll(); - - /// - /// 取消选择 - /// - void DeselectAll(); - - /// - /// 删除选中内容 - /// - void DeleteSelected(); - - /// - /// 复制选中内容 - /// - void CopySelected(); - - /// - /// 剪切选中内容 - /// - void CutSelected(); - - /// - /// 粘贴内容 - /// - void Paste(); - - #endregion - - #region 系统设置操作 - - /// - /// 设置系统设置 - /// - /// 设置类型 - /// 设置键 - /// 设置值 - void SetSetting(string key, T value); - - /// - /// 保存设置到文件 - /// - void SaveSettings(); - - /// - /// 从文件加载设置 - /// - void LoadSettings(); - - /// - /// 重置设置为默认值 - /// - void ResetSettings(); - - #endregion - - #region 插件管理操作 - - /// - /// 启用插件 - /// - /// 插件名称 - void EnablePlugin(string pluginName); - - /// - /// 禁用插件 - /// - /// 插件名称 - void DisablePlugin(string pluginName); - - /// - /// 卸载插件 - /// - /// 插件名称 - void UnloadPlugin(string pluginName); - - #endregion - - #region 事件系统操作 - - /// - /// 注册事件处理器 - /// - /// 事件名称 - /// 事件处理器 - void RegisterEventHandler(string eventName, EventHandler handler); - - /// - /// 注销事件处理器 - /// - /// 事件名称 - /// 事件处理器 - void UnregisterEventHandler(string eventName, EventHandler handler); - - /// - /// 触发事件 - /// - /// 事件名称 - /// 事件发送者 - /// 事件参数 - void TriggerEvent(string eventName, object sender, EventArgs args); - - #endregion - - #region 应用程序操作 - - /// - /// 重启应用程序 - /// - void RestartApplication(); - - /// - /// 退出应用程序 - /// - void ExitApplication(); - - /// - /// 检查更新 - /// - void CheckForUpdates(); - - /// - /// 打开帮助文档 - /// - void OpenHelpDocument(); - - /// - /// 打开关于页面 - /// - void OpenAboutPage(); - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/ICCPPPluginAdapter.cs b/Ink Canvas/Helpers/Plugins/ICCPPPluginAdapter.cs deleted file mode 100644 index 4287225d..00000000 --- a/Ink Canvas/Helpers/Plugins/ICCPPPluginAdapter.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System; -using System.IO; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// ICCPP 插件适配器,用于加载和管理 .iccpp 格式的插件 - /// - public class ICCPPPluginAdapter : PluginBase - { - private readonly byte[] _pluginData; - private readonly string _pluginPath; - private readonly string _pluginName; - private readonly Version _pluginVersion; - private bool _isInitialized; - - public override string PluginStateKey => "ICCPP:" + (_pluginPath ?? string.Empty); - - /// - /// 创建 ICCPP 插件适配器 - /// - /// 插件文件路径 - /// 插件文件数据 - public ICCPPPluginAdapter(string pluginPath, byte[] pluginData) - { - _pluginPath = pluginPath; - _pluginData = pluginData; - PluginPath = pluginPath; - - // 从文件名获取插件名称 - _pluginName = Path.GetFileNameWithoutExtension(pluginPath); - _pluginVersion = new Version(1, 0, 0); // 默认版本 - - // 尝试从插件数据中读取更多信息 - TryReadPluginMetadata(); - } - - public ICCPPPluginAdapter() - { - _pluginPath = string.Empty; - _pluginData = new byte[0]; - PluginPath = string.Empty; - _pluginName = "ICCPPPlugin"; - _pluginVersion = new Version(1, 0, 0); - // 可选:初始化其他字段 - } - - /// - /// 尝试从插件数据中读取元数据 - /// - private void TryReadPluginMetadata() - { - try - { - // 这里可以根据 .iccpp 文件的实际格式解析元数据 - // 例如,如果文件有特定的头部结构,可以在这里解析 - - // 示例:如果前100字节包含元数据 - if (_pluginData.Length > 100) - { - // 解析元数据的代码... - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"解析插件 {_pluginName} 元数据时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #region IPlugin 接口实现 - - /// - /// 插件名称 - /// - public override string Name => _pluginName; - - /// - /// 插件描述 - /// - public override string Description => $"{_pluginName} (ICCPP 格式插件)"; - - /// - /// 插件版本 - /// - public override Version Version => _pluginVersion; - - /// - /// 插件作者 - /// - public override string Author => "未知"; - - /// - /// 是否为内置插件 - /// - public override bool IsBuiltIn => false; - - /// - /// 初始化插件 - /// - public override void Initialize() - { - if (_isInitialized) return; - - try - { - // 这里可以添加 .iccpp 插件的初始化逻辑 - // 例如,根据文件格式加载特定资源 - - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已初始化"); - _isInitialized = true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"初始化 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 启用插件 - /// - public override void Enable() - { - if (IsEnabled) return; - - try - { - // 这里可以添加 .iccpp 插件的启用逻辑 - // 例如,加载动态库、注册事件等 - - base.Enable(); // 设置启用状态并触发事件 - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已启用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"启用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 禁用插件 - /// - public override void Disable() - { - if (!IsEnabled) return; - - try - { - // 这里可以添加 .iccpp 插件的禁用逻辑 - // 例如,卸载动态库、注销事件等 - - base.Disable(); // 设置禁用状态并触发事件 - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已禁用"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"禁用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 清理插件资源 - /// - public override void Cleanup() - { - try - { - // 这里可以添加 .iccpp 插件的清理逻辑 - // 例如,释放资源等 - - LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已清理资源"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清理 ICCPP 插件 {Name} 资源时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IGetService.cs b/Ink Canvas/Helpers/Plugins/IGetService.cs deleted file mode 100644 index 60f299ba..00000000 --- a/Ink Canvas/Helpers/Plugins/IGetService.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System.Collections.Generic; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 获取服务接口,统一所有获取类的方法 - /// - public interface IGetService - { - #region 窗口和UI获取 - - /// - /// 获取主窗口引用 - /// - Window MainWindow { get; } - - /// - /// 获取当前画布 - /// - global::System.Windows.Controls.InkCanvas CurrentCanvas { get; } - - /// - /// 获取所有画布页面 - /// - List AllCanvasPages { get; } - - /// - /// 获取当前页面索引 - /// - int CurrentPageIndex { get; } - - /// - /// 获取当前页面数量 - /// - int TotalPageCount { get; } - - /// - /// 获取浮动工具栏 - /// - FrameworkElement FloatingToolBar { get; } - - /// - /// 获取左侧面板 - /// - FrameworkElement LeftPanel { get; } - - /// - /// 获取右侧面板 - /// - FrameworkElement RightPanel { get; } - - /// - /// 获取顶部面板 - /// - FrameworkElement TopPanel { get; } - - /// - /// 获取底部面板 - /// - FrameworkElement BottomPanel { get; } - - #endregion - - #region 绘制工具状态获取 - - /// - /// 获取当前绘制模式 - /// - int CurrentDrawingMode { get; } - - /// - /// 获取当前笔触宽度 - /// - double CurrentInkWidth { get; } - - /// - /// 获取当前笔触颜色 - /// - Color CurrentInkColor { get; } - - /// - /// 获取当前高亮笔宽度 - /// - double CurrentHighlighterWidth { get; } - - /// - /// 获取当前橡皮擦大小 - /// - int CurrentEraserSize { get; } - - /// - /// 获取当前橡皮擦类型 - /// - int CurrentEraserType { get; } - - /// - /// 获取当前橡皮擦形状 - /// - int CurrentEraserShape { get; } - - /// - /// 获取当前笔触透明度 - /// - double CurrentInkAlpha { get; } - - /// - /// 获取当前笔触样式 - /// - int CurrentInkStyle { get; } - - /// - /// 获取当前背景颜色 - /// - string CurrentBackgroundColor { get; } - - #endregion - - #region 应用状态获取 - - /// - /// 获取当前主题模式 - /// - bool IsDarkTheme { get; } - - /// - /// 获取当前是否为白板模式 - /// - bool IsWhiteboardMode { get; } - - /// - /// 获取当前是否为PPT模式 - /// - bool IsPPTMode { get; } - - /// - /// 获取当前是否为全屏模式 - /// - bool IsFullScreenMode { get; } - - /// - /// 获取当前是否为画板模式 - /// - bool IsCanvasMode { get; } - - /// - /// 获取当前是否为选择模式 - /// - bool IsSelectionMode { get; } - - /// - /// 获取当前是否为擦除模式 - /// - bool IsEraserMode { get; } - - /// - /// 获取当前是否为形状绘制模式 - /// - bool IsShapeDrawingMode { get; } - - /// - /// 获取当前是否为高亮模式 - /// - bool IsHighlighterMode { get; } - - #endregion - - #region 撤销重做状态获取 - - /// - /// 获取是否可以撤销 - /// - bool CanUndo { get; } - - /// - /// 获取是否可以重做 - /// - bool CanRedo { get; } - - #endregion - - #region 系统设置获取 - - /// - /// 获取系统设置 - /// - /// 设置类型 - /// 设置键 - /// 默认值 - /// 设置值 - T GetSetting(string key, T defaultValue = default(T)); - - #endregion - - #region 插件信息获取 - - /// - /// 获取所有已加载的插件 - /// - /// 插件列表 - List GetAllPlugins(); - - /// - /// 获取指定插件 - /// - /// 插件名称 - /// 插件实例 - IPlugin GetPlugin(string pluginName); - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IPlugin.cs b/Ink Canvas/Helpers/Plugins/IPlugin.cs deleted file mode 100644 index 0a43ab15..00000000 --- a/Ink Canvas/Helpers/Plugins/IPlugin.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -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/IPluginService.cs b/Ink Canvas/Helpers/Plugins/IPluginService.cs deleted file mode 100644 index 9559a992..00000000 --- a/Ink Canvas/Helpers/Plugins/IPluginService.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件服务接口,提供对软件内部功能的访问 - /// 继承自三个专门的服务接口:获取服务、窗口服务、操作服务 - /// - public interface IPluginService : IGetService, IWindowService, IActionService - { - // 这个接口现在继承自三个专门的服务接口 - // 所有方法都在子接口中定义,这里不需要重复定义 - } - - /// - /// 通知类型枚举 - /// - public enum NotificationType - { - /// - /// 信息 - /// - Info, - - /// - /// 成功 - /// - Success, - - /// - /// 警告 - /// - Warning, - - /// - /// 错误 - /// - Error - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/IWindowService.cs b/Ink Canvas/Helpers/Plugins/IWindowService.cs deleted file mode 100644 index eb6fb863..00000000 --- a/Ink Canvas/Helpers/Plugins/IWindowService.cs +++ /dev/null @@ -1,152 +0,0 @@ -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 窗口服务接口,统一所有窗口操作相关的方法 - /// - public interface IWindowService - { - #region 窗口显示和隐藏 - - /// - /// 显示设置窗口 - /// - void ShowSettingsWindow(); - - /// - /// 隐藏设置窗口 - /// - void HideSettingsWindow(); - - /// - /// 显示插件设置窗口 - /// - void ShowPluginSettingsWindow(); - - /// - /// 隐藏插件设置窗口 - /// - void HidePluginSettingsWindow(); - - /// - /// 显示帮助窗口 - /// - void ShowHelpWindow(); - - /// - /// 隐藏帮助窗口 - /// - void HideHelpWindow(); - - /// - /// 显示关于窗口 - /// - void ShowAboutWindow(); - - /// - /// 隐藏关于窗口 - /// - void HideAboutWindow(); - - #endregion - - #region 对话框和通知 - - /// - /// 显示通知消息 - /// - /// 消息内容 - /// 消息类型 - void ShowNotification(string message, NotificationType type = NotificationType.Info); - - /// - /// 显示确认对话框 - /// - /// 消息内容 - /// 标题 - /// 用户选择结果 - bool ShowConfirmDialog(string message, string title = "确认"); - - /// - /// 显示输入对话框 - /// - /// 提示消息 - /// 标题 - /// 默认值 - /// 用户输入内容 - string ShowInputDialog(string message, string title = "输入", string defaultValue = ""); - - #endregion - - #region 窗口状态控制 - - /// - /// 设置窗口全屏状态 - /// - /// 是否全屏 - void SetFullScreen(bool isFullScreen); - - /// - /// 设置窗口置顶状态 - /// - /// 是否置顶 - void SetTopMost(bool isTopMost); - - /// - /// 设置窗口可见性 - /// - /// 是否可见 - void SetWindowVisibility(bool isVisible); - - /// - /// 最小化窗口 - /// - void MinimizeWindow(); - - /// - /// 最大化窗口 - /// - void MaximizeWindow(); - - /// - /// 恢复窗口 - /// - void RestoreWindow(); - - /// - /// 关闭窗口 - /// - void CloseWindow(); - - #endregion - - #region 窗口位置和大小 - - /// - /// 设置窗口位置 - /// - /// X坐标 - /// Y坐标 - void SetWindowPosition(double x, double y); - - /// - /// 设置窗口大小 - /// - /// 宽度 - /// 高度 - void SetWindowSize(double width, double height); - - /// - /// 获取窗口位置 - /// - /// 窗口位置 - (double x, double y) GetWindowPosition(); - - /// - /// 获取窗口大小 - /// - /// 窗口大小 - (double width, double height) GetWindowSize(); - - #endregion - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginBase.cs b/Ink Canvas/Helpers/Plugins/PluginBase.cs deleted file mode 100644 index 0c960100..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginBase.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Windows.Controls; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 插件基类,提供基本实现 - /// - public abstract class PluginBase : IPlugin - { - /// - /// 插件状态(私有字段) - /// - private bool _isEnabled; - - /// - /// 插件状态(公共属性) - /// - public bool IsEnabled - { - get => _isEnabled; - protected set - { - if (_isEnabled != value) - { - _isEnabled = value; - OnEnabledStateChanged(value); - } - } - } - - /// - /// 插件ID - /// - public string Id { get; protected set; } - - /// - /// 写入 配置时使用的稳定键(默认同类型全名;多实例类型如 SDK 目录插件应重写)。 - /// - public virtual string PluginStateKey => GetType().FullName; - - /// - /// 插件路径 - /// - 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; - - // 添加日志,记录插件名称 - try - { - string name = Name; - LogHelper.WriteLogToFile($"初始化插件: ID={Id}, 名称={name ?? "未命名"}"); - - if (string.IsNullOrEmpty(name)) - { - LogHelper.WriteLogToFile($"警告: 插件 {Id} 的名称为空", LogHelper.LogType.Warning); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取插件名称时出错: {ex.Message}", LogHelper.LogType.Error); - } - - LogHelper.WriteLogToFile($"插件 {Name} 已初始化"); - } - - /// - /// 启用插件 - /// - public virtual void Enable() - { - if (!IsEnabled) - { - IsEnabled = true; - LogHelper.WriteLogToFile($"插件 {Name} 已启用"); - } - } - - /// - /// 禁用插件 - /// - public virtual void Disable() - { - if (IsEnabled) - { - IsEnabled = false; - LogHelper.WriteLogToFile($"插件 {Name} 已禁用"); - } - } - - /// - /// 获取插件设置界面 - /// - /// 插件设置界面 - public virtual UserControl GetSettingsView() - { - // 默认返回空设置页面 - return new UserControl(); - } - - /// - /// 插件卸载时的清理工作 - /// - public virtual void Cleanup() - { - LogHelper.WriteLogToFile($"插件 {Name} 已卸载"); - } - - /// - /// 保存插件自身的设置 - /// 注意:此方法仅用于保存插件的特定设置,不应影响插件启用/禁用状态 - /// 插件启用状态由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 deleted file mode 100644 index 82c4c7d0..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginManager.cs +++ /dev/null @@ -1,1622 +0,0 @@ -using Ink_Canvas.Windows; -using InkCanvasForClass.PluginHost; -using InkCanvasForClass.PluginSdk; -using Newtonsoft.Json; -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; -using Timer = System.Timers.Timer; - -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, "Configs", "PluginConfig.json"); - private static readonly string PluginConfigBackupFile = Path.Combine(App.RootPath, "Configs", "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; - - /// - /// 配置自动保存计时器 - /// - private Timer _autoSaveTimer; - - /// - /// 加载的程序集缓存 - /// - private Dictionary _loadedAssemblies = new Dictionary(); - - /// - /// 插件文件哈希缓存,用于热重载检测 - /// - private Dictionary _pluginHashes = new Dictionary(); - - /// - /// SDK 插件程序集(按主 DLL 路径缓存) - /// - private readonly Dictionary _sdkAssembliesByPath = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// 已加载的 SDK 插件核心实例(键为插件目录名) - /// - private readonly Dictionary _sdkCoreByFolderId = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// SDK 插件登记的菜单 / 工具栏 / 设置页,供主窗口挂载。 - /// - public CollectingPluginRegistry ExtensionRegistry { get; } = new CollectingPluginRegistry(); - - private PluginManager() - { - // 确保插件目录存在 - if (!Directory.Exists(PluginsDirectory)) - { - Directory.CreateDirectory(PluginsDirectory); - } - - - // 初始化自动保存计时器(3秒) - _autoSaveTimer = new Timer(3000); - _autoSaveTimer.Elapsed += (s, e) => - { - if (_configDirty) - { - SaveConfigAsync().ConfigureAwait(false); - } - }; - _autoSaveTimer.AutoReset = false; - - // 注册插件状态变更事件处理 - AppDomain.CurrentDomain.ProcessExit += (s, e) => - { - // 应用退出时强制保存配置 - if (_configDirty) - { - SaveConfig(); - } - }; - } - - private static string GetPluginStateKey(IPlugin plugin) - { - if (plugin is PluginBase pluginBase) - { - return pluginBase.PluginStateKey; - } - - return plugin.GetType().FullName; - } - - public IInkCanvasPlugin GetSdkPluginInstance(string folderId) - { - if (string.IsNullOrEmpty(folderId)) - { - return null; - } - - return _sdkCoreByFolderId.TryGetValue(folderId, out var core) ? core : null; - } - - public IReadOnlyList GetAllSdkPluginInstances() - { - return _sdkCoreByFolderId.Values.ToList(); - } - - public IInkCanvasPlugin GetSdkPluginByName(string name) - { - return _sdkCoreByFolderId.Values.FirstOrDefault(p => p.Name == name); - } - - public void SetSdkPluginEnabledByName(string name, bool enable) - { - var adapter = Plugins.OfType().FirstOrDefault(a => - a.Name == name || string.Equals(a.FolderId, name, StringComparison.OrdinalIgnoreCase)); - if (adapter == null) - { - return; - } - - TogglePlugin(adapter, enable); - } - - public void UnloadSdkPluginByName(string name) - { - var adapter = Plugins.OfType().FirstOrDefault(a => - a.Name == name || string.Equals(a.FolderId, name, StringComparison.OrdinalIgnoreCase)); - if (adapter == null) - { - return; - } - - UnloadPlugin(adapter, true); - } - - /// - /// 初始化插件系统 - /// - public void Initialize() - { - try - { - LogHelper.WriteLogToFile("开始初始化插件系统"); - - // 加载配置 - LoadConfig(); - LogHelper.WriteLogToFile($"已从配置文件加载 {PluginStates.Count} 个插件状态记录"); - - // 加载内置插件 - LogHelper.WriteLogToFile("正在加载内置插件..."); - LoadBuiltInPlugins(); - - // 加载外部插件 - LogHelper.WriteLogToFile("正在加载外部插件..."); - LoadExternalPlugins(); - - // 加载 Plugins 子目录中的 SDK 插件(IInkCanvasPlugin) - LogHelper.WriteLogToFile("正在加载 SDK 插件(子目录)..."); - LoadSdkPluginsFromSubfolders(); - - // 启用已配置为启用的插件 - LogHelper.WriteLogToFile("正在应用配置的插件状态..."); - EnableConfiguredPlugins(); - - // 设置定期检查热重载 - StartHotReloadWatcher(); - - // 保存初始化后的配置(可能有新插件) - SaveConfig(); - - LogHelper.WriteLogToFile($"插件系统初始化完成,共加载 {Plugins.Count} 个插件"); - } - 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}"); - } - } - 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; - } - - // 获取所有插件文件(支持 .iccpp 和 .dll 格式) - var pluginFiles = Directory.GetFiles(PluginsDirectory, "*.iccpp", SearchOption.TopDirectoryOnly) - .Concat(Directory.GetFiles(PluginsDirectory, "*.dll", SearchOption.TopDirectoryOnly)) - .ToArray(); - - LogHelper.WriteLogToFile($"发现 {pluginFiles.Length} 个外部插件文件"); - - foreach (var pluginFile in pluginFiles) - { - LoadExternalPlugin(pluginFile); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载外部插件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 扫描 下一层子目录,加载实现 的 SDK 插件。 - /// - private void LoadSdkPluginsFromSubfolders() - { - try - { - if (!Directory.Exists(PluginsDirectory)) - { - return; - } - - foreach (var pluginDir in Directory.GetDirectories(PluginsDirectory)) - { - var folderId = Path.GetFileName(pluginDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); - if (string.IsNullOrEmpty(folderId)) - { - continue; - } - - var dllFiles = Directory.GetFiles(pluginDir, "*.dll", SearchOption.AllDirectories); - var mainDll = dllFiles.FirstOrDefault(f => - f.IndexOf($"{Path.DirectorySeparatorChar}lib{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) < 0 && - f.IndexOf($"{Path.AltDirectorySeparatorChar}lib{Path.AltDirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) < 0 && - f.IndexOf($"{Path.DirectorySeparatorChar}runtimes{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) < 0 && - f.IndexOf($"{Path.AltDirectorySeparatorChar}runtimes{Path.AltDirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) < 0); - - if (mainDll == null) - { - continue; - } - - try - { - if (!_sdkAssembliesByPath.TryGetValue(mainDll, out var assembly)) - { - assembly = Assembly.LoadFrom(mainDll); - _sdkAssembliesByPath[mainDll] = assembly; - } - - Type pluginType = null; - try - { - pluginType = assembly.GetTypes() - .FirstOrDefault(t => - typeof(IInkCanvasPlugin).IsAssignableFrom(t) && - !t.IsAbstract && - !t.IsInterface && - t.IsClass); - } - catch (ReflectionTypeLoadException ex) - { - LogHelper.WriteLogToFile($"枚举 SDK 插件类型失败 {folderId}: {ex.Message}", LogHelper.LogType.Error); - } - - if (pluginType == null) - { - continue; - } - - var core = (IInkCanvasPlugin)Activator.CreateInstance(pluginType); - _sdkCoreByFolderId[folderId] = core; - - var adapter = new SdkPluginAdapter(folderId, core, mainDll); - adapter.Initialize(); - Plugins.Add(adapter); - - LogHelper.WriteLogToFile($"已加载 SDK 插件(子目录): {core.Name} — {folderId}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载 SDK 插件目录 {folderId} 失败: {ex.Message}", LogHelper.LogType.Error); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"扫描 SDK 插件子目录失败: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 加载单个外部插件 - /// - /// 插件文件路径 - /// 加载的插件实例,加载失败则返回null - public IPlugin LoadExternalPlugin(string pluginPath) - { - try - { - // 计算文件哈希 - string fileHash = CalculateFileHash(pluginPath); - _pluginHashes[pluginPath] = fileHash; - - // 检查文件扩展名 - string extension = Path.GetExtension(pluginPath).ToLowerInvariant(); - if (extension == ".iccpp") - { - // 创建 ICCPP 插件适配器 - return CreateICCPPPluginAdapter(pluginPath); - } - - // 加载插件程序集 - 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)}"); - - return plugin; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"实例化插件类型 {pluginType.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - LogHelper.WriteLogToFile($"在程序集 {Path.GetFileName(pluginPath)} 中未找到有效的插件类型", LogHelper.LogType.Warning); - return null; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载外部插件 {Path.GetFileName(pluginPath)} 时出错: {ex.Message}", LogHelper.LogType.Error); - return null; - } - } - - /// - /// 创建 ICCPP 插件适配器 - /// - /// 插件文件路径 - /// 适配的插件实例 - private IPlugin CreateICCPPPluginAdapter(string pluginPath) - { - try - { - // 读取插件文件内容 - byte[] pluginData = File.ReadAllBytes(pluginPath); - - // 创建适配器插件实例 - var pluginAdapter = new ICCPPPluginAdapter(pluginPath, pluginData); - - // 添加到插件列表 - Plugins.Add(pluginAdapter); - - LogHelper.WriteLogToFile($"已创建 ICCPP 插件适配器: {pluginAdapter.Name} 来自 {Path.GetFileName(pluginPath)}"); - - return pluginAdapter; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建 ICCPP 插件适配器时出错: {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 = GetPluginStateKey(plugin); - - // 检查配置中的插件状态 - 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}"); - } - else - { - plugin.Disable(); - disabledCount++; - LogHelper.WriteLogToFile($"根据配置禁用插件: {plugin.Name}"); - } - } - 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}"); - } - } - } - catch (Exception ex) - { - errorCount++; - LogHelper.WriteLogToFile($"应用插件 {plugin.Name} 配置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - // 如果有配置变更,启动自动保存 - if (_configDirty) - { - TriggerAutoSave(); - } - - LogHelper.WriteLogToFile($"已应用插件配置: 启用 {enabledCount} 个,禁用 {disabledCount} 个,错误 {errorCount} 个"); - } - - /// - /// 插件状态变更事件处理 - /// - private void Plugin_EnabledStateChanged(object sender, bool isEnabled) - { - try - { - if (sender is IPlugin plugin) - { - string pluginTypeName = GetPluginStateKey(plugin); - - // 更新配置状态 - if (!PluginStates.ContainsKey(pluginTypeName) || PluginStates[pluginTypeName] != isEnabled) - { - PluginStates[pluginTypeName] = isEnabled; - _configDirty = true; - - LogHelper.WriteLogToFile($"插件状态变更: {plugin.Name} = {(isEnabled ? "启用" : "禁用")}"); - - // 立即同步保存配置(不再使用延迟自动保存) - SaveConfig(); - LogHelper.WriteLogToFile($"插件 {plugin.Name} 状态已立即保存到配置文件"); - } - } - } - 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 - { - string pluginPath = plugin.PluginPath; - if (string.IsNullOrEmpty(pluginPath) || !File.Exists(pluginPath)) - { - LogHelper.WriteLogToFile($"无法重新加载插件 {plugin.Name}: 插件文件不存在", LogHelper.LogType.Error); - return; - } - - LogHelper.WriteLogToFile($"开始热重载插件: {plugin.Name} ({Path.GetFileName(pluginPath)})"); - - // 保存插件的当前状态 - bool wasEnabled = plugin.IsEnabled; - string pluginTypeName = GetPluginStateKey(plugin); - - // 卸载插件 - UnloadPlugin(plugin); - - // 从加载缓存中移除 - if (_loadedAssemblies.ContainsKey(pluginPath)) - { - _loadedAssemblies.Remove(pluginPath); - } - - // 计算新的文件哈希 - string newHash = CalculateFileHash(pluginPath); - _pluginHashes[pluginPath] = newHash; - - // 重新加载插件 - IPlugin newPlugin = LoadExternalPlugin(pluginPath); - - if (newPlugin != null) - { - // 恢复插件状态 - if (wasEnabled) - { - newPlugin.Enable(); - } - - // 更新配置(如果类型名称变化) - string newPluginTypeName = GetPluginStateKey(newPlugin); - if (pluginTypeName != newPluginTypeName && PluginStates.ContainsKey(pluginTypeName)) - { - bool state = PluginStates[pluginTypeName]; - PluginStates.Remove(pluginTypeName); - PluginStates[newPluginTypeName] = state; - _configDirty = true; - SaveConfig(); - } - - LogHelper.WriteLogToFile($"插件 {newPlugin.Name} v{newPlugin.Version} 热重载成功"); - - // 通知UI刷新 - NotifyUIRefresh(); - } - else - { - LogHelper.WriteLogToFile($"插件 {plugin.Name} 热重载失败", LogHelper.LogType.Error); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重新加载插件 {plugin.Name} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 卸载插件 - /// - /// 要卸载的插件 - /// 是否从配置中移除 - public void UnloadPlugin(IPlugin plugin, bool removeFromConfig = false) - { - try - { - // 保存插件名称,以便在卸载后使用 - string pluginName = plugin.Name; - - // 如果插件已启用,先禁用它 - if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) - { - plugin.Disable(); - } - - // 执行插件清理 - plugin.Cleanup(); - - // 从插件集合中移除 - Plugins.Remove(plugin); - - if (plugin is SdkPluginAdapter sdkAdapter) - { - _sdkCoreByFolderId.Remove(sdkAdapter.FolderId); - } - - // 从配置中移除(如果需要) - if (removeFromConfig && plugin.GetType() != null) - { - string pluginTypeName = GetPluginStateKey(plugin); - if (PluginStates.ContainsKey(pluginTypeName)) - { - PluginStates.Remove(pluginTypeName); - SaveConfig(); - } - } - - LogHelper.WriteLogToFile($"已卸载插件: {pluginName}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"卸载插件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 删除插件 - /// - /// 要删除的插件 - /// 删除是否成功 - public bool DeletePlugin(IPlugin plugin) - { - try - { - // 只能删除外部插件 - if (plugin.IsBuiltIn) - { - return false; - } - - // 保存插件名称,以便在删除后使用 - string pluginName = plugin.Name; - - // 获取插件路径 - 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($"已删除插件: {pluginName}"); - return true; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"删除插件时出错: {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 ? "启用" : "禁用")} 状态,无需切换"); - return; - } - - // 记录插件信息,用于日志 - string pluginName = plugin.Name; - string pluginTypeName = GetPluginStateKey(plugin); - - LogHelper.WriteLogToFile($"开始切换插件 {pluginName} 状态为: {(enable ? "启用" : "禁用")}"); - - // 首先更新配置状态 - 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} 已启用"); - } - else - { - // 禁用前先记录是否为内置插件 - bool isBuiltIn = plugin.IsBuiltIn; - LogHelper.WriteLogToFile($"尝试禁用{(isBuiltIn ? "内置" : "外部")}插件 {pluginName}"); - - // 禁用插件 - 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.WriteLogToFile($"插件 {pluginName} 已禁用"); - } - } - 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} 状态变更"); - - // 对于内置插件,我们需要确保状态正确应用 - 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 ? "启用" : "禁用")}"); - } - } - } - } - - // 通知UI刷新 - NotifyUIRefresh(); - } - else - { - // 外部插件,执行热重载 - try - { - if (!string.IsNullOrEmpty(pluginInstance.PluginPath) && File.Exists(pluginInstance.PluginPath)) - { - LogHelper.WriteLogToFile($"开始重载外部插件 {pluginName}"); - - // 使用调度器确保在UI线程执行热重载 - if (Application.Current != null && Application.Current.Dispatcher != null) - { - Application.Current.Dispatcher.BeginInvoke(new Action(() => - { - ReloadPlugin(pluginInstance); - LogHelper.WriteLogToFile($"插件 {pluginName} 已重载以应用{(enable ? "启用" : "禁用")}状态"); - })); - } - else - { - // 当前不在UI线程,直接重载 - ReloadPlugin(pluginInstance); - LogHelper.WriteLogToFile($"插件 {pluginName} 已重载以应用{(enable ? "启用" : "禁用")}状态"); - } - } - 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} 状态切换完成"); - } - 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}"); - } - else - { - plugin.Disable(); - LogHelper.WriteLogToFile($"实时应用: 已禁用插件 {plugin.Name}"); - } - - // 同步状态到插件自身的配置 - if (plugin is PluginBase pluginSettings) - { - try - { - // 保存插件设置(与启用状态无关) - pluginSettings.SavePluginSettings(); - LogHelper.WriteLogToFile($"实时应用: 已保存插件 {plugin.Name} 设置"); - } - 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 = GetPluginStateKey(plugin); - 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} 已成功热重载以应用实时状态"); - } - 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 ? "启用" : "禁用")}"); - } - 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 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}"); - - // 确保至少有一个默认配置 - 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 = JsonConvert.DeserializeObject>(json); - - if (loadedStates != null && loadedStates.Count > 0) - { - PluginStates = loadedStates; - _configDirty = false; // 重置脏标记 - LogHelper.WriteLogToFile($"成功从配置文件加载了 {PluginStates.Count} 个插件状态"); - } - else - { - LogHelper.WriteLogToFile("配置文件解析为空,尝试使用备份", LogHelper.LogType.Warning); - // 尝试加载备份 - if (File.Exists(PluginConfigBackupFile)) - { - try - { - string backupJson = File.ReadAllText(PluginConfigBackupFile); - var backupStates = JsonConvert.DeserializeObject>(backupJson); - - if (backupStates != null && backupStates.Count > 0) - { - PluginStates = backupStates; - _configDirty = true; // 从备份加载,需要重新保存主配置 - LogHelper.WriteLogToFile($"已从备份恢复 {PluginStates.Count} 个插件状态"); - 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 = JsonConvert.DeserializeObject>(backupJson); - - if (backupStates != null && backupStates.Count > 0) - { - PluginStates = backupStates; - _configDirty = true; // 从备份加载,需要重新保存主配置 - LogHelper.WriteLogToFile($"已从备份恢复 {PluginStates.Count} 个插件状态"); - 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); - 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}"); - - // 生成JSON数据 - string json = JsonConvert.SerializeObject(PluginStates, Formatting.Indented); - string tempFile = PluginConfigFile + ".temp"; // 临时文件路径 - - // 确保目录存在 - string configDir = Path.GetDirectoryName(PluginConfigFile); - if (!Directory.Exists(configDir)) - { - Directory.CreateDirectory(configDir); - LogHelper.WriteLogToFile($"创建配置目录: {configDir}"); - } - - // 先备份当前配置 - 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} 个插件状态"); - return; - } - } - catch (Exception ex) - { - if (attempt < maxRetries) - { - LogHelper.WriteLogToFile($"保存配置失败 (尝试 {attempt}/{maxRetries}): {ex.Message},将在 {retryDelayMs}ms 后重试", LogHelper.LogType.Warning); - 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}"); - 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("开始从配置文件重新加载插件状态"); - - // 保存当前配置状态,以便在加载失败时回滚 - 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} 个插件状态,开始应用..."); - - // 对比配置,查找变更的插件 - foreach (var plugin in Plugins.ToList()) // 创建副本进行遍历,避免集合修改异常 - { - string pluginTypeName = GetPluginStateKey(plugin); - - // 检查插件在配置中是否存在 - if (PluginStates.TryGetValue(pluginTypeName, out bool shouldBeEnabled)) - { - bool currentlyEnabled = plugin is PluginBase pluginBase && pluginBase.IsEnabled; - - // 如果状态需要变更 - if (currentlyEnabled != shouldBeEnabled) - { - LogHelper.WriteLogToFile($"应用插件 {plugin.Name} 的配置状态: {(shouldBeEnabled ? "启用" : "禁用")}"); - - 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}"); - - // 禁用插件 - 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}"); - } - } - } - } - 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} 不在配置中,默认设置为禁用状态"); - - // 如果当前是启用状态,则禁用它 - if (plugin is PluginBase pluginBase && pluginBase.IsEnabled) - { - try - { - bool isBuiltIn = plugin.IsBuiltIn; - LogHelper.WriteLogToFile($"尝试禁用未配置的{(isBuiltIn ? "内置" : "外部")}插件 {plugin.Name}"); - - 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}"); - } - } - } - 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 PluginSettingsWindow pluginWindow) - { - pluginWindow.RefreshPluginList(); - } - } - }); - } - - LogHelper.WriteLogToFile("插件状态已从配置文件重新加载完成"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"从配置文件重新加载插件状态时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - } -} \ No newline at end of file diff --git a/Ink Canvas/Helpers/Plugins/PluginRuntime.cs b/Ink Canvas/Helpers/Plugins/PluginRuntime.cs deleted file mode 100644 index a3ea7ec2..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginRuntime.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 在加载任何 SDK 插件之前初始化宿主上下文(实现 )。 - /// - public static class PluginRuntime - { - private static PluginSdkHostContext _context; - - public static PluginSdkHostContext SdkContext => _context; - - /// 相同实例,便于旧代码通过 访问。 - public static IPluginService Services => SdkContext != null ? (IPluginService)SdkContext : null; - - public static void Initialize(MainWindow mainWindow) - { - if (_context == null) - { - _context = new PluginSdkHostContext(); - } - - _context.SetMainWindow(mainWindow); - } - } -} diff --git a/Ink Canvas/Helpers/Plugins/PluginSdkHostContext.cs b/Ink Canvas/Helpers/Plugins/PluginSdkHostContext.cs deleted file mode 100644 index e5b9db97..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginSdkHostContext.cs +++ /dev/null @@ -1,1289 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Ink; -using Ink_Canvas.Windows; -using InkCanvasForClass.PluginSdk; -using LegacyNotificationType = Ink_Canvas.Helpers.Plugins.NotificationType; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 统一宿主上下文:同时实现 SDK 的 与旧版 。 - /// - public class PluginSdkHostContext : IPluginContext, IPluginService - { - private MainWindow _mainWindow; - private Dictionary _eventHandlers = new Dictionary(); - - /// - /// 设置主窗口引用 - /// - /// 主窗口实例 - public void SetMainWindow(MainWindow mainWindow) - { - _mainWindow = mainWindow; - } - - #region 窗口和UI访问 - - public Window MainWindow => _mainWindow; - - public System.Windows.Controls.InkCanvas CurrentCanvas => _mainWindow?.inkCanvas; - - public IList AllCanvasPages => - _mainWindow?.WhiteboardPages ?? new List(); - - public int CurrentPageIndex => _mainWindow?.CurrentPageIndex ?? 0; - - public int TotalPageCount => _mainWindow?.WhiteboardPages?.Count ?? 0; - - public FrameworkElement FloatingToolBar => _mainWindow?.ViewboxFloatingBar; - - public FrameworkElement LeftPanel => _mainWindow?.BlackboardLeftSide; - - public FrameworkElement RightPanel => _mainWindow?.BlackboardRightSide; - - public FrameworkElement TopPanel => _mainWindow?.BorderTools; - - public FrameworkElement BottomPanel => _mainWindow?.BorderSettings; - - #endregion - - #region 绘制工具状态 - - public int CurrentDrawingMode => GetCurrentDrawingMode(); - - public double CurrentInkWidth => GetCurrentInkWidth(); - - public Color CurrentInkColor => GetCurrentInkColor(); - - public double CurrentHighlighterWidth => GetCurrentHighlighterWidth(); - - public int CurrentEraserSize => GetCurrentEraserSize(); - - public int CurrentEraserType => GetCurrentEraserType(); - - public int CurrentEraserShape => GetCurrentEraserShape(); - - public double CurrentInkAlpha => GetCurrentInkAlpha(); - - public int CurrentInkStyle => GetCurrentInkStyle(); - - public string CurrentBackgroundColor => GetCurrentBackgroundColor(); - - #endregion - - #region 应用状态 - - public bool IsDarkTheme => GetIsDarkTheme(); - - public bool IsWhiteboardMode => GetIsWhiteboardMode(); - - public bool IsPPTMode => GetIsPPTMode(); - - public bool IsFullScreenMode => GetIsFullScreenMode(); - - public bool IsCanvasMode => GetIsCanvasMode(); - - public bool IsSelectionMode => GetIsSelectionMode(); - - public bool IsEraserMode => GetIsEraserMode(); - - public bool IsShapeDrawingMode => GetIsShapeDrawingMode(); - - public bool IsHighlighterMode => GetIsHighlighterMode(); - - #endregion - - #region 操作状态 - - public bool CanUndo => GetCanUndo(); - - public bool CanRedo => GetCanRedo(); - - #endregion - - #region 设置管理 - - public T GetSetting(string key, T defaultValue = default(T)) - { - try - { - // 这里需要根据实际的设置系统来实现 - // 暂时返回默认值 - return defaultValue; - } - catch - { - return defaultValue; - } - } - - public void SetSetting(string key, T value) - { - try - { - // 这里需要根据实际的设置系统来实现 - LogHelper.WriteLogToFile($"设置 {key} = {value}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置 {key} 时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SaveSettings() - { - try - { - // 这里需要根据实际的设置系统来实现 - LogHelper.WriteLogToFile("设置已保存"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存设置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void LoadSettings() - { - try - { - // 这里需要根据实际的设置系统来实现 - LogHelper.WriteLogToFile("设置已加载"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载设置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void ResetSettings() - { - try - { - // 这里需要根据实际的设置系统来实现 - LogHelper.WriteLogToFile("设置已重置"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重置设置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 插件管理 - - public IList GetAllPlugins() - { - return PluginManager.Instance.GetAllSdkPluginInstances().ToList(); - } - - public IInkCanvasPlugin GetPlugin(string pluginName) - { - return PluginManager.Instance.GetSdkPluginByName(pluginName); - } - - public void EnablePlugin(string pluginName) - { - if (PluginManager.Instance.GetSdkPluginByName(pluginName) != null) - { - PluginManager.Instance.SetSdkPluginEnabledByName(pluginName, true); - return; - } - - var plugin = PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName); - if (plugin != null) - { - PluginManager.Instance.TogglePlugin(plugin, true); - } - } - - public void DisablePlugin(string pluginName) - { - if (PluginManager.Instance.GetSdkPluginByName(pluginName) != null) - { - PluginManager.Instance.SetSdkPluginEnabledByName(pluginName, false); - return; - } - - var plugin = PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName); - if (plugin != null) - { - PluginManager.Instance.TogglePlugin(plugin, false); - } - } - - public void UnloadPlugin(string pluginName) - { - if (PluginManager.Instance.GetSdkPluginByName(pluginName) != null) - { - PluginManager.Instance.UnloadSdkPluginByName(pluginName); - return; - } - - var plugin = PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName); - if (plugin != null) - { - PluginManager.Instance.UnloadPlugin(plugin, true); - } - } - - #endregion - - #region 窗口操作 - - public void ShowSettingsWindow() - { - try - { - if (_mainWindow == null) - { - return; - } - - _mainWindow.Dispatcher.BeginInvoke(new Action(() => _mainWindow.BtnSettings_Click(null, null))); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"显示设置窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void HideSettingsWindow() - { - try - { - if (_mainWindow == null) - { - return; - } - - _mainWindow.Dispatcher.BeginInvoke(new Action(() => _mainWindow.HideSubPanels())); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"隐藏设置窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void ShowPluginSettingsWindow() - { - try - { - if (_mainWindow == null) - { - return; - } - - _mainWindow.Dispatcher.BeginInvoke(new Action(() => - { - var w = new PluginSettingsWindow { Owner = _mainWindow }; - w.Show(); - })); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"显示插件设置窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void HidePluginSettingsWindow() - { - try - { - var app = Application.Current; - if (app?.Dispatcher == null) - { - return; - } - - app.Dispatcher.BeginInvoke(new Action(() => - { - foreach (Window w in app.Windows) - { - if (w is PluginSettingsWindow psw) - { - psw.Close(); - } - } - })); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"隐藏插件设置窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void ShowHelpWindow() - { - try - { - // 这里需要调用帮助窗口显示方法 - LogHelper.WriteLogToFile("显示帮助窗口"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"显示帮助窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void HideHelpWindow() - { - try - { - // 这里需要调用帮助窗口隐藏方法 - LogHelper.WriteLogToFile("隐藏帮助窗口"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"隐藏帮助窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void ShowAboutWindow() - { - try - { - // 这里需要调用关于窗口显示方法 - LogHelper.WriteLogToFile("显示关于窗口"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"显示关于窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void HideAboutWindow() - { - try - { - // 这里需要调用关于窗口隐藏方法 - LogHelper.WriteLogToFile("隐藏关于窗口"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"隐藏关于窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void ShowNotification(string message, InkCanvasForClass.PluginSdk.NotificationType type = InkCanvasForClass.PluginSdk.NotificationType.Info) - { - try - { - LogHelper.WriteLogToFile($"通知: {message} ({type})"); - var title = type.ToString(); - _mainWindow?.PluginHost_ShowInfo(title, message); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"显示通知时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public bool ShowConfirmDialog(string message, string title = "确认") - { - try - { - return _mainWindow != null && _mainWindow.PluginHost_ShowConfirm(title, message); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"显示确认对话框时出错: {ex.Message}", LogHelper.LogType.Error); - return false; - } - } - - public string ShowInputDialog(string message, string title = "输入", string defaultValue = "") - { - try - { - return _mainWindow != null - ? _mainWindow.PluginHost_ShowInput(title, message, defaultValue) - : defaultValue; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"显示输入对话框时出错: {ex.Message}", LogHelper.LogType.Error); - return defaultValue; - } - } - - public void SetFullScreen(bool isFullScreen) - { - try - { - if (_mainWindow == null) - { - return; - } - - _mainWindow.Dispatcher.Invoke(() => - { - _mainWindow.WindowState = isFullScreen ? WindowState.Maximized : WindowState.Normal; - }); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置全屏时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetTopMost(bool isTopMost) - { - try - { - if (_mainWindow == null) - { - return; - } - - _mainWindow.Dispatcher.Invoke(() => _mainWindow.Topmost = isTopMost); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置置顶时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetWindowVisibility(bool isVisible) - { - try - { - if (_mainWindow == null) - { - return; - } - - _mainWindow.Dispatcher.Invoke(() => - _mainWindow.Visibility = isVisible ? Visibility.Visible : Visibility.Hidden); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置窗口可见性时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void MinimizeWindow() - { - try - { - if (_mainWindow != null) - { - _mainWindow.WindowState = WindowState.Minimized; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"最小化窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void MaximizeWindow() - { - try - { - if (_mainWindow != null) - { - _mainWindow.WindowState = WindowState.Maximized; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"最大化窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void RestoreWindow() - { - try - { - if (_mainWindow != null) - { - _mainWindow.WindowState = WindowState.Normal; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"还原窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void CloseWindow() - { - try - { - _mainWindow?.Close(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"关闭窗口时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetWindowPosition(double x, double y) - { - try - { - if (_mainWindow != null) - { - _mainWindow.Left = x; - _mainWindow.Top = y; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置窗口位置时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetWindowSize(double width, double height) - { - try - { - if (_mainWindow != null) - { - _mainWindow.Width = width; - _mainWindow.Height = height; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置窗口大小时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public (double x, double y) GetWindowPosition() - { - try - { - if (_mainWindow != null) - { - return (_mainWindow.Left, _mainWindow.Top); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取窗口位置时出错: {ex.Message}", LogHelper.LogType.Error); - } - return (0, 0); - } - - public (double width, double height) GetWindowSize() - { - try - { - if (_mainWindow != null) - { - return (_mainWindow.Width, _mainWindow.Height); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取窗口大小时出错: {ex.Message}", LogHelper.LogType.Error); - } - return (800, 600); - } - - #endregion - - #region 画布操作 - - public void ClearCanvas() - { - try - { - _mainWindow?.PluginHost_ClearInk(false); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清除画布时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void ClearAllCanvases() - { - try - { - // 这里需要调用清除所有画布的方法 - LogHelper.WriteLogToFile("清除所有画布"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清除所有画布时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void AddNewPage() - { - try - { - // 这里需要调用添加新页面的方法 - LogHelper.WriteLogToFile("添加新页面"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"添加新页面时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void DeleteCurrentPage() - { - try - { - // 这里需要调用删除当前页面的方法 - LogHelper.WriteLogToFile("删除当前页面"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"删除当前页面时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SwitchToPage(int pageIndex) - { - try - { - // 这里需要调用切换页面的方法 - LogHelper.WriteLogToFile($"切换到页面: {pageIndex}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"切换页面时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void NextPage() - { - try - { - // 这里需要调用下一页的方法 - LogHelper.WriteLogToFile("下一页"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"下一页时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void PreviousPage() - { - try - { - // 这里需要调用上一页的方法 - LogHelper.WriteLogToFile("上一页"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"上一页时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 绘制设置 - - public void SetDrawingMode(int mode) - { - try - { - // 这里需要调用设置绘制模式的方法 - LogHelper.WriteLogToFile($"设置绘制模式: {mode}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置绘制模式时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetInkWidth(double width) - { - try - { - // 这里需要调用设置墨迹宽度的方法 - LogHelper.WriteLogToFile($"设置墨迹宽度: {width}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置墨迹宽度时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetInkColor(Color color) - { - try - { - // 这里需要调用设置墨迹颜色的方法 - LogHelper.WriteLogToFile($"设置墨迹颜色: {color}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置墨迹颜色时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetHighlighterWidth(double width) - { - try - { - // 这里需要调用设置高亮笔宽度的方法 - LogHelper.WriteLogToFile($"设置高亮笔宽度: {width}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置高亮笔宽度时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetEraserSize(int size) - { - try - { - // 这里需要调用设置橡皮擦大小的方法 - LogHelper.WriteLogToFile($"设置橡皮擦大小: {size}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置橡皮擦大小时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetEraserType(int type) - { - try - { - // 这里需要调用设置橡皮擦类型的方法 - LogHelper.WriteLogToFile($"设置橡皮擦类型: {type}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置橡皮擦类型时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetEraserShape(int shape) - { - try - { - // 这里需要调用设置橡皮擦形状的方法 - LogHelper.WriteLogToFile($"设置橡皮擦形状: {shape}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置橡皮擦形状时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetInkAlpha(double alpha) - { - try - { - // 这里需要调用设置墨迹透明度的方法 - LogHelper.WriteLogToFile($"设置墨迹透明度: {alpha}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置墨迹透明度时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetInkStyle(int style) - { - try - { - // 这里需要调用设置墨迹样式的方法 - LogHelper.WriteLogToFile($"设置墨迹样式: {style}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置墨迹样式时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SetBackgroundColor(string color) - { - try - { - // 这里需要调用设置背景颜色的方法 - LogHelper.WriteLogToFile($"设置背景颜色: {color}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置背景颜色时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 文件操作 - - public void SaveCanvas(string filePath) - { - try - { - // 这里需要调用保存画布的方法 - LogHelper.WriteLogToFile($"保存画布到: {filePath}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存画布时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void LoadCanvas(string filePath) - { - try - { - // 这里需要调用加载画布的方法 - LogHelper.WriteLogToFile($"加载画布从: {filePath}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载画布时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void ExportAsImage(string filePath, string format) - { - try - { - // 这里需要调用导出为图片的方法 - LogHelper.WriteLogToFile($"导出为图片: {filePath} ({format})"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"导出为图片时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void ExportAsPDF(string filePath) - { - try - { - // 这里需要调用导出为PDF的方法 - LogHelper.WriteLogToFile($"导出为PDF: {filePath}"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"导出为PDF时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 编辑操作 - - public void Undo() - { - try - { - _mainWindow?.PluginHost_Undo(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"撤销操作时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void Redo() - { - try - { - _mainWindow?.PluginHost_Redo(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重做操作时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void SelectAll() - { - try - { - // 这里需要调用全选的方法 - LogHelper.WriteLogToFile("全选"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"全选时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void DeselectAll() - { - try - { - // 这里需要调用取消选择的方法 - LogHelper.WriteLogToFile("取消选择"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"取消选择时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void DeleteSelected() - { - try - { - // 这里需要调用删除选中项的方法 - LogHelper.WriteLogToFile("删除选中项"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"删除选中项时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void CopySelected() - { - try - { - // 这里需要调用复制选中项的方法 - LogHelper.WriteLogToFile("复制选中项"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"复制选中项时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void CutSelected() - { - try - { - // 这里需要调用剪切选中项的方法 - LogHelper.WriteLogToFile("剪切选中项"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"剪切选中项时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void Paste() - { - try - { - // 这里需要调用粘贴的方法 - LogHelper.WriteLogToFile("粘贴"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"粘贴时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 事件系统 - - public void RegisterEventHandler(string eventName, EventHandler handler) - { - try - { - if (!_eventHandlers.ContainsKey(eventName)) - { - _eventHandlers[eventName] = handler; - } - else - { - _eventHandlers[eventName] += handler; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"注册事件处理器时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void UnregisterEventHandler(string eventName, EventHandler handler) - { - try - { - if (_eventHandlers.ContainsKey(eventName)) - { - _eventHandlers[eventName] -= handler; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"注销事件处理器时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void TriggerEvent(string eventName, object sender, EventArgs args) - { - try - { - if (_eventHandlers.ContainsKey(eventName)) - { - _eventHandlers[eventName]?.Invoke(sender, args); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"触发事件时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 应用程序操作 - - public void RestartApplication() - { - try - { - var path = Process.GetCurrentProcess().MainModule?.FileName; - if (!string.IsNullOrEmpty(path)) - { - Process.Start(new ProcessStartInfo - { - FileName = path, - UseShellExecute = true - }); - } - - Application.Current?.Shutdown(0); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重启应用程序时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void ExitApplication() - { - try - { - Application.Current?.Shutdown(0); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"退出应用程序时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void CheckForUpdates() - { - try - { - // 这里需要调用检查更新的方法 - LogHelper.WriteLogToFile("检查更新"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"检查更新时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void OpenHelpDocument() - { - try - { - // 这里需要调用打开帮助文档的方法 - LogHelper.WriteLogToFile("打开帮助文档"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"打开帮助文档时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - public void OpenAboutPage() - { - try - { - // 这里需要调用打开关于页面的方法 - LogHelper.WriteLogToFile("打开关于页面"); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"打开关于页面时出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - #endregion - - #region 私有方法 - 获取当前状态 - - private int GetCurrentDrawingMode() - { - // 这里需要根据实际的绘制模式状态来实现 - return 0; - } - - private double GetCurrentInkWidth() - { - // 这里需要根据实际的墨迹宽度状态来实现 - return 2.5; - } - - private Color GetCurrentInkColor() - { - // 这里需要根据实际的墨迹颜色状态来实现 - return Colors.Black; - } - - private double GetCurrentHighlighterWidth() - { - // 这里需要根据实际的高亮笔宽度状态来实现 - return 20.0; - } - - private int GetCurrentEraserSize() - { - // 这里需要根据实际的橡皮擦大小状态来实现 - return 2; - } - - private int GetCurrentEraserType() - { - // 这里需要根据实际的橡皮擦类型状态来实现 - return 0; - } - - private int GetCurrentEraserShape() - { - // 这里需要根据实际的橡皮擦形状状态来实现 - return 0; - } - - private double GetCurrentInkAlpha() - { - // 这里需要根据实际的墨迹透明度状态来实现 - return 255.0; - } - - private int GetCurrentInkStyle() - { - // 这里需要根据实际的墨迹样式状态来实现 - return 0; - } - - private string GetCurrentBackgroundColor() - { - // 这里需要根据实际的背景颜色状态来实现 - return "#162924"; - } - - private bool GetIsDarkTheme() - { - // 这里需要根据实际的主题状态来实现 - return false; - } - - private bool GetIsWhiteboardMode() - { - // 这里需要根据实际的白板模式状态来实现 - return false; - } - - private bool GetIsPPTMode() - { - // 这里需要根据实际的PPT模式状态来实现 - return false; - } - - private bool GetIsFullScreenMode() - { - // 这里需要根据实际的全屏模式状态来实现 - return false; - } - - private bool GetIsCanvasMode() - { - // 这里需要根据实际的画布模式状态来实现 - return true; - } - - private bool GetIsSelectionMode() - { - // 这里需要根据实际的选择模式状态来实现 - return false; - } - - private bool GetIsEraserMode() - { - // 这里需要根据实际的橡皮擦模式状态来实现 - return false; - } - - private bool GetIsShapeDrawingMode() - { - // 这里需要根据实际的形状绘制模式状态来实现 - return false; - } - - private bool GetIsHighlighterMode() - { - // 这里需要根据实际的高亮笔模式状态来实现 - return false; - } - - private bool GetCanUndo() - { - return _mainWindow?.PluginHost_CanUndo() ?? false; - } - - private bool GetCanRedo() - { - return _mainWindow?.PluginHost_CanRedo() ?? false; - } - - #endregion - - #region IPluginService / IGetService 显式实现 - - List IGetService.GetAllPlugins() - { - return PluginManager.Instance.Plugins.ToList(); - } - - IPlugin IGetService.GetPlugin(string pluginName) - { - return PluginManager.Instance.Plugins.FirstOrDefault(p => p.Name == pluginName); - } - - List IGetService.AllCanvasPages - { - get - { - var pages = AllCanvasPages; - return pages == null ? new List() : pages.ToList(); - } - } - - global::System.Windows.Controls.InkCanvas IGetService.CurrentCanvas => CurrentCanvas; - - void IWindowService.ShowNotification(string message, LegacyNotificationType type) - { - ShowNotification(message, MapLegacyNotification(type)); - } - - private static InkCanvasForClass.PluginSdk.NotificationType MapLegacyNotification(LegacyNotificationType type) - { - switch (type) - { - case LegacyNotificationType.Success: - return InkCanvasForClass.PluginSdk.NotificationType.Success; - case LegacyNotificationType.Warning: - return InkCanvasForClass.PluginSdk.NotificationType.Warning; - case LegacyNotificationType.Error: - return InkCanvasForClass.PluginSdk.NotificationType.Error; - default: - return InkCanvasForClass.PluginSdk.NotificationType.Info; - } - } - - #endregion - } -} diff --git a/Ink Canvas/Helpers/Plugins/PluginServiceManager.cs b/Ink Canvas/Helpers/Plugins/PluginServiceManager.cs deleted file mode 100644 index cb2cb2ef..00000000 --- a/Ink Canvas/Helpers/Plugins/PluginServiceManager.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 兼容旧代码的薄门面:与 为同一实例,实现 。 - /// - public static class PluginServiceManager - { - public static IPluginService Instance - { - get - { - var ctx = PluginRuntime.SdkContext; - if (ctx == null) - { - throw new InvalidOperationException("插件宿主尚未初始化:请先调用 PluginRuntime.Initialize(MainWindow)。"); - } - - return (IPluginService)ctx; - } - } - } -} diff --git a/Ink Canvas/Helpers/Plugins/SdkPluginAdapter.cs b/Ink Canvas/Helpers/Plugins/SdkPluginAdapter.cs deleted file mode 100644 index 11e20961..00000000 --- a/Ink Canvas/Helpers/Plugins/SdkPluginAdapter.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Windows.Controls; -using InkCanvasForClass.PluginHost; -using InkCanvasForClass.PluginSdk; - -namespace Ink_Canvas.Helpers.Plugins -{ - /// - /// 将基于 的外部插件适配为宿主统一的 。 - /// - public sealed class SdkPluginAdapter : PluginBase - { - private readonly IInkCanvasPlugin _core; - private readonly string _folderId; - - public SdkPluginAdapter(string folderId, IInkCanvasPlugin core, string mainAssemblyPath) - { - _folderId = folderId ?? throw new ArgumentNullException(nameof(folderId)); - _core = core ?? throw new ArgumentNullException(nameof(core)); - PluginPath = mainAssemblyPath ?? string.Empty; - } - - public string FolderId => _folderId; - - public IInkCanvasPlugin Core => _core; - - public override string PluginStateKey => "SdkFolder:" + _folderId; - - public override string Name => _core.Name; - - public override string Description => _core.Description; - - public override Version Version => _core.Version; - - public override string Author => _core.Author; - - public override bool IsBuiltIn => false; - - public override void Initialize() - { - base.Initialize(); - - var ctx = PluginRuntime.SdkContext; - if (ctx == null) - { - LogHelper.WriteLogToFile($"SDK 插件 {_core.Name} 初始化失败:宿主上下文未就绪", LogHelper.LogType.Error); - return; - } - - _core.Initialize(ctx); - - var registry = PluginManager.Instance.ExtensionRegistry; - registry.SetCurrentPluginId(_folderId); - try - { - if (_core is InkCanvasPluginBase pluginBase) - { - pluginBase.RegisterExtensions(registry); - } - } - finally - { - registry.SetCurrentPluginId(string.Empty); - } - } - - public override void Enable() - { - if (IsEnabled) - { - return; - } - - if (_core is InkCanvasPluginBase b) - { - b.IsEnabled = true; - } - else - { - _core.Start(); - } - - base.Enable(); - } - - public override void Disable() - { - if (!IsEnabled) - { - return; - } - - if (_core is InkCanvasPluginBase b) - { - b.IsEnabled = false; - } - else - { - _core.Stop(); - } - - base.Disable(); - } - - public override UserControl GetSettingsView() - { - return _core.GetSettingsView(); - } - - public override void Cleanup() - { - try - { - if (_core is InkCanvasPluginBase b) - { - b.IsEnabled = false; - } - else - { - _core.Stop(); - } - - _core.Cleanup(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"SDK 插件 {_core.Name} Cleanup 出错: {ex.Message}", LogHelper.LogType.Error); - } - - base.Cleanup(); - } - } -} diff --git a/Ink Canvas/InkCanvasForClass.csproj b/Ink Canvas/InkCanvasForClass.csproj index 962ca671..47bdc078 100644 --- a/Ink Canvas/InkCanvasForClass.csproj +++ b/Ink Canvas/InkCanvasForClass.csproj @@ -157,10 +157,6 @@ - - - - {F935DC20-1CF0-11D0-ADB9-00C04FD58A0B} diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 698fd6da..070af88d 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -247,21 +247,6 @@ - - - private const int ExitPPTModeAfterDisconnectDelayMs = 1200; + + /// + /// 仅PPT模式下周期性探测放映界面(COM 失效时依赖 Win32),间隔不宜过小以免多余开销。 + /// + private DispatcherTimer _pptOnlyVisibilityProbeTimer; + + private const int PptOnlyVisibilityProbeIntervalMs = 800; + + /// + /// PowerPoint 全屏放映顶层窗口类名(与编辑态 PPTFrameClass 区分)。 + /// + private const string PowerPointSlideShowWindowClassName = "screenClass"; #endregion #region PPT Managers @@ -638,6 +651,8 @@ namespace Ink_Canvas ClosePowerPointApplication(); ClearStaticInteropState(); + StopPptOnlyVisibilityProbeTimer(); + LogHelper.WriteLogToFile("PPT管理器已释放", LogHelper.LogType.Event); } catch (Exception ex) @@ -700,6 +715,104 @@ namespace Ink_Canvas } #endregion + #region 仅PPT模式可见性(COM + Win32 兜底) + + /// + /// 在启用「仅PPT模式」时启动轻量探测,COM 事件延迟或失效时仍可根据全屏放映窗口显示主窗口。 + /// + internal void EnsurePptOnlyVisibilityProbeTimer() + { + try + { + if (!Settings.ModeSettings.IsPPTOnlyMode) + { + StopPptOnlyVisibilityProbeTimer(); + return; + } + + if (_pptOnlyVisibilityProbeTimer == null) + { + _pptOnlyVisibilityProbeTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(PptOnlyVisibilityProbeIntervalMs) + }; + _pptOnlyVisibilityProbeTimer.Tick += (_, __) => CheckMainWindowVisibility(); + } + + if (!_pptOnlyVisibilityProbeTimer.IsEnabled) + _pptOnlyVisibilityProbeTimer.Start(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"仅PPT可见性探测计时器启动失败: {ex.Message}", LogHelper.LogType.Warning); + } + } + + internal void StopPptOnlyVisibilityProbeTimer() + { + try + { + _pptOnlyVisibilityProbeTimer?.Stop(); + } + catch + { + } + } + + /// + /// 检测是否存在 PowerPoint 全屏放映顶层窗口(类名 screenClass,进程 powerpnt),用于 COM 不可用时的兜底。 + /// + internal bool IsPowerPointSlideshowSurfacePresentWin32() + { + if (!Settings.ModeSettings.IsPPTOnlyMode) + return false; + + try + { + bool found = false; + EnumWindows((hWnd, _) => + { + if (!IsWindow(hWnd) || !IsWindowVisible(hWnd)) + return true; + + var cls = new StringBuilder(256); + if (GetClassName(hWnd, cls, cls.Capacity) == 0) + return true; + + if (!string.Equals(cls.ToString(), PowerPointSlideShowWindowClassName, StringComparison.OrdinalIgnoreCase)) + return true; + + try + { + GetWindowThreadProcessId(hWnd, out uint pid); + using (var proc = Process.GetProcessById((int)pid)) + { + var name = proc.ProcessName; + if (string.Equals(name, "POWERPNT", StringComparison.OrdinalIgnoreCase)) + { + found = true; + return false; + } + } + } + catch + { + } + + return true; + }, IntPtr.Zero); + + return found; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"Win32 检测 PPT 放映窗口失败: {ex.Message}", LogHelper.LogType.Trace); + return false; + } + } + + #endregion + #region New PPT Event Handlers /// /// 处理 PowerPoint 连接状态的变更:更新界面连接/放映状态,并在断开时启动一个短延迟以安全退出 PPT 模式。 @@ -731,6 +844,8 @@ namespace Ink_Canvas _ = HandleManualSlideShowEnd(); if (Settings.PowerPointSettings.UseRotPptLink) _pptManager?.ReloadConnection(); + + CheckMainWindowVisibility(); } }); } @@ -1114,6 +1229,9 @@ namespace Ink_Canvas // 加载当前页墨迹 LoadCurrentSlideInk(currentSlide); + + // 仅PPT模式:放映开始立即同步主窗口可见性(勿仅依赖 SlideShowStateChanged 定时器) + CheckMainWindowVisibility(); }); if (!isFloatingBarFolded) @@ -1435,6 +1553,8 @@ namespace Ink_Canvas UpdateCurrentToolMode("cursor"); SetFloatingBarHighlightPosition("cursor"); + + CheckMainWindowVisibility(); } catch (Exception ex) { @@ -2291,6 +2411,7 @@ namespace Ink_Canvas _pptUIManager?.UpdateSlideShowStatus(false); _pptUIManager?.UpdateSidebarExitButtons(false); LogHelper.WriteLogToFile("手动更新放映结束UI状态", LogHelper.LogType.Trace); + CheckMainWindowVisibility(); }); // 手动处理自动收纳,因为OnPPTSlideShowEnd事件可能未触发 @@ -2323,6 +2444,7 @@ namespace Ink_Canvas { _pptUIManager?.UpdateSlideShowStatus(false); _pptUIManager?.UpdateSidebarExitButtons(false); + CheckMainWindowVisibility(); }); // 异常情况下也手动处理自动收纳 From 24c6ca60a37337fe394005a5648788b6b2bdd326 Mon Sep 17 00:00:00 2001 From: doudou0720 <98651603+doudou0720@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:04:37 +0800 Subject: [PATCH 26/27] =?UTF-8?q?refactor:=20=E6=9B=BF=E6=8D=A2=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=A0=8F=E5=9B=BE=E6=A0=87=E5=BA=93=E4=B8=BA=20H.Noti?= =?UTF-8?q?fyIcon=20(#425)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新所有相关文件和引用,从 Hardcodet.Wpf.TaskbarNotification 迁移到 H.NotifyIcon 调整通知显示方式并添加 ForceCreate 调用确保图标正确显示 Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com> --- Ink Canvas/App.xaml | 2 +- Ink Canvas/App.xaml.cs | 3 ++- .../Helpers/WindowsNotificationHelper.cs | 7 +++--- Ink Canvas/InkCanvasForClass.csproj | 2 +- Ink Canvas/MainWindow_cs/MW_Settings.cs | 2 +- Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs | 2 +- Ink Canvas/MainWindow_cs/MW_TrayIcon.cs | 2 +- .../SettingsViews/ThemePanel.xaml.cs | 2 +- Ink Canvas/packages.lock.json | 25 ++++++++++++++++--- 9 files changed, 32 insertions(+), 15 deletions(-) diff --git a/Ink Canvas/App.xaml b/Ink Canvas/App.xaml index 39bb6117..aa4fc279 100644 --- a/Ink Canvas/App.xaml +++ b/Ink Canvas/App.xaml @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Ink_Canvas" - xmlns:tb="http://www.hardcodet.net/taskbar" + xmlns:tb="clr-namespace:H.NotifyIcon;assembly=H.NotifyIcon.Wpf" xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf" > diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index 787cbc0b..2238c408 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Ink_Canvas.Helpers; using Ink_Canvas.Properties; using iNKORE.UI.WPF.Modern.Controls; @@ -1061,6 +1061,7 @@ namespace Ink_Canvas } _taskbar = (TaskbarIcon)FindResource("TaskbarTrayIcon"); + _taskbar.ForceCreate(); StartArgs = e.Args; diff --git a/Ink Canvas/Helpers/WindowsNotificationHelper.cs b/Ink Canvas/Helpers/WindowsNotificationHelper.cs index 13f15c82..652732eb 100644 --- a/Ink Canvas/Helpers/WindowsNotificationHelper.cs +++ b/Ink Canvas/Helpers/WindowsNotificationHelper.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Microsoft.Toolkit.Uwp.Notifications; using System; using System.Windows; @@ -40,10 +40,9 @@ namespace Ink_Canvas.Helpers taskbar.Visibility = Visibility.Visible; - taskbar.ShowBalloonTip( + taskbar.ShowNotification( "InkCanvasForClass CE", - $"发现新版本!:{version}", - BalloonIcon.Info); + $"发现新版本!:{version}"); } catch { diff --git a/Ink Canvas/InkCanvasForClass.csproj b/Ink Canvas/InkCanvasForClass.csproj index 47bdc078..969d8176 100644 --- a/Ink Canvas/InkCanvasForClass.csproj +++ b/Ink Canvas/InkCanvasForClass.csproj @@ -135,7 +135,7 @@ all - + diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index cedea1b4..7fe730bf 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Ink_Canvas.Helpers; using Newtonsoft.Json; using OSVersionExtension; diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index 49c8ac24..9fbca2bc 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Ink_Canvas.Helpers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs b/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs index 302dfac7..1a537013 100644 --- a/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs +++ b/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using Ink_Canvas.Helpers; using iNKORE.UI.WPF.Controls; using System; diff --git a/Ink Canvas/Windows/SettingsViews/SettingsViews/ThemePanel.xaml.cs b/Ink Canvas/Windows/SettingsViews/SettingsViews/ThemePanel.xaml.cs index 10fc0f34..6f059470 100644 --- a/Ink Canvas/Windows/SettingsViews/SettingsViews/ThemePanel.xaml.cs +++ b/Ink Canvas/Windows/SettingsViews/SettingsViews/ThemePanel.xaml.cs @@ -1,4 +1,4 @@ -using Hardcodet.Wpf.TaskbarNotification; +using H.NotifyIcon; using iNKORE.UI.WPF.Helpers; using System; using System.Collections.Generic; diff --git a/Ink Canvas/packages.lock.json b/Ink Canvas/packages.lock.json index f1e58c3c..cf179f10 100644 --- a/Ink Canvas/packages.lock.json +++ b/Ink Canvas/packages.lock.json @@ -48,11 +48,15 @@ "Fody": "6.8.2" } }, - "Hardcodet.NotifyIcon.Wpf": { + "H.NotifyIcon.Wpf": { "type": "Direct", - "requested": "[2.0.1, )", - "resolved": "2.0.1", - "contentHash": "dtxmeZXzV2GzSm91aZ3hqzgoeVoARSkDPVCYfhVUNyyKBWYxMgNC0EcLiSYxD4Uc4alq/2qb3SmV8DgAENLRLQ==" + "requested": "[2.0.131, )", + "resolved": "2.0.131", + "contentHash": "f71kXNl6PjCqipJ7DQytg1QUBMQ+7j8rF1UyL8UPegymG1G57EYsskdIcf/VmF6JDuts6Dk6F8Hd4ziiz4/3Dw==", + "dependencies": { + "H.NotifyIcon": "2.0.131", + "System.ValueTuple": "4.5.0" + } }, "iNKORE.UI.WPF": { "type": "Direct", @@ -185,6 +189,19 @@ "resolved": "6.8.2", "contentHash": "sjGHrtGS1+kcrv99WXCvujOFBTQp4zCH3ZC9wo2LAtVaJkuLpHghQx3y4k1Q8ZKuDAbEw+HE6ZjPUJQK3ejepQ==" }, + "H.GeneratedIcons.System.Drawing": { + "type": "Transitive", + "resolved": "2.0.131", + "contentHash": "QoNGQrhxzG+dQufa4xRjSqihMy5aVVVZqQUt0fLJbwhs7rcM4hpN1qVkZpZEkHsRgrHfFBC/Ursjh8STY/sg7A==" + }, + "H.NotifyIcon": { + "type": "Transitive", + "resolved": "2.0.131", + "contentHash": "mdznQAfcJFehblFoDUvtmdm1Y9+u1eMN1ffORbdYv5EwreMxkCwvdj8qQn3qnUo9EIJ6h5Xdgqey9Nj4us8w7w==", + "dependencies": { + "H.GeneratedIcons.System.Drawing": "2.0.131" + } + }, "MdXaml.Plugins": { "type": "Transitive", "resolved": "1.27.0", From 1165e5bbf23d32a7530b6f1ea6167b3c6b33c4ef Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sun, 5 Apr 2026 15:49:06 +0800 Subject: [PATCH 27/27] =?UTF-8?q?add:=E4=B8=B4=E6=97=B6=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/App.xaml | 22 +++++- Ink Canvas/MainWindow.xaml.cs | 11 ++- Ink Canvas/MainWindow_cs/MW_TrayIcon.cs | 89 +++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) diff --git a/Ink Canvas/App.xaml b/Ink Canvas/App.xaml index aa4fc279..3d4723fd 100644 --- a/Ink Canvas/App.xaml +++ b/Ink Canvas/App.xaml @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + + + diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs index 1a3d2729..97402f0e 100644 --- a/Ink Canvas/MainWindow.xaml.cs +++ b/Ink Canvas/MainWindow.xaml.cs @@ -82,6 +82,8 @@ namespace Ink_Canvas private static Cursor _cachedPenCursor = null; private static readonly object _cursorLock = new object(); + internal static DateTime? TrayTemporaryShowUntilUtc; + #region Window Initialization /// @@ -4398,12 +4400,19 @@ namespace Ink_Canvas /// /// 检查是否应该显示主窗口(基于PPT模式和PPT放映状态) /// - private void CheckMainWindowVisibility() + internal void CheckMainWindowVisibility() { try { if (Settings.ModeSettings.IsPPTOnlyMode) { + if (TrayTemporaryShowUntilUtc.HasValue && DateTime.UtcNow < TrayTemporaryShowUntilUtc.Value) + { + if (!IsVisible) + Show(); + return; + } + // 仅PPT模式:以 COM/UI 状态为主,Win32 检测全屏放映窗口(screenClass)作兜底,避免 COM 异常时无法唤出 bool comUiSlideShow = BtnPPTSlideShowEnd.Visibility == Visibility.Visible; bool win32SlideShow = IsPowerPointSlideshowSurfacePresentWin32(); diff --git a/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs b/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs index 1a537013..76a573c8 100644 --- a/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs +++ b/Ink Canvas/MainWindow_cs/MW_TrayIcon.cs @@ -9,6 +9,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Forms; using System.Windows.Interop; +using System.Windows.Threading; using Application = System.Windows.Application; using ContextMenu = System.Windows.Controls.ContextMenu; using MenuItem = System.Windows.Controls.MenuItem; @@ -17,6 +18,11 @@ namespace Ink_Canvas { public partial class App : Application { + private const int TrayTemporaryShowMinutes = 2; + + private DispatcherTimer _trayTemporaryShowTimer; + + private bool _trayTemporaryShowRestoreHideChecked; /// /// 系统托盘菜单打开时的事件处理方法 @@ -131,6 +137,85 @@ namespace Ink_Canvas } } + 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 + { + } + + _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; @@ -326,6 +411,10 @@ namespace Ink_Canvas /// 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)