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/LauncherSettingsControl.xaml.cs b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherSettingsControl.xaml.cs
deleted file mode 100644
index 6666f10d..00000000
--- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherSettingsControl.xaml.cs
+++ /dev/null
@@ -1,396 +0,0 @@
-using Ink_Canvas.Windows;
-using Microsoft.Win32;
-using System;
-using System.ComponentModel;
-using System.IO;
-using System.Windows;
-using System.Windows.Controls;
-
-namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
-{
- ///
- /// LauncherSettingsControl.xaml 的交互逻辑
- ///
- public partial class LauncherSettingsControl : UserControl
- {
- ///
- /// 父插件
- ///
- private readonly SuperLauncherPlugin _plugin;
-
- ///
- /// 构造函数
- ///
- /// 父插件
- public LauncherSettingsControl(SuperLauncherPlugin plugin)
- {
- InitializeComponent();
-
- _plugin = plugin;
-
- // 设置按钮位置
- RbtnLeft.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left;
- RbtnRight.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Right;
-
- // 绑定应用列表
- DgApps.ItemsSource = _plugin.LauncherItems;
-
- // 初始化按钮状态
- UpdateButtonStates();
- }
-
- ///
- /// 更新按钮状态
- ///
- private void UpdateButtonStates()
- {
- bool hasSelection = DgApps.SelectedItem != null;
- BtnEdit.IsEnabled = hasSelection;
- BtnDelete.IsEnabled = hasSelection;
- }
-
- ///
- /// 位置单选按钮选择事件
- ///
- private void RbtnPosition_Checked(object sender, RoutedEventArgs e)
- {
- if (!IsLoaded) return;
-
- LauncherButtonPosition oldPosition = _plugin.Config.ButtonPosition;
-
- if (sender == RbtnLeft)
- {
- _plugin.Config.ButtonPosition = LauncherButtonPosition.Left;
- }
- else if (sender == RbtnRight)
- {
- _plugin.Config.ButtonPosition = LauncherButtonPosition.Right;
- }
-
- // 如果位置发生变化,更新按钮位置
- if (oldPosition != _plugin.Config.ButtonPosition)
- {
- try
- {
- // 更新按钮位置
- _plugin.UpdateButtonPosition();
-
- // 保存配置
- _plugin.SaveConfig();
-
- LogHelper.WriteLogToFile($"启动台按钮位置已更改为: {_plugin.Config.ButtonPosition}");
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error);
- MessageBox.Show($"更新启动台按钮位置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
- }
-
- ///
- /// 添加按钮点击事件
- ///
- private void BtnAdd_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- // 创建新的启动项
- LauncherItem item = new LauncherItem
- {
- Name = "",
- Path = "",
- IsVisible = true,
- Position = -1 // 让插件管理器分配位置
- };
-
- // 直接显示编辑对话框
- EditLauncherItem(item, true);
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"添加启动项时出错: {ex.Message}", LogHelper.LogType.Error);
- MessageBox.Show($"添加启动项时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- ///
- /// 编辑应用按钮点击事件
- ///
- private void BtnEdit_Click(object sender, RoutedEventArgs e)
- {
- if (DgApps.SelectedItem is LauncherItem item)
- {
- EditLauncherItem(item, false);
- }
- }
-
- ///
- /// 删除应用按钮点击事件
- ///
- private void BtnDelete_Click(object sender, RoutedEventArgs e)
- {
- if (DgApps.SelectedItem is LauncherItem item)
- {
- // 确认删除
- MessageBoxResult result = MessageBox.Show(
- $"确定要删除 {item.Name} 吗?",
- "删除确认",
- MessageBoxButton.YesNo,
- MessageBoxImage.Question);
-
- if (result == MessageBoxResult.Yes)
- {
- // 从集合中移除
- _plugin.LauncherItems.Remove(item);
-
- // 保存配置
- _plugin.SaveConfig();
- }
- }
- }
-
- ///
- /// 保存设置按钮点击事件
- ///
- private void BtnSave_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- // 保存配置
- _plugin.SaveConfig();
-
- // 如果插件已启用,重新加载启动台按钮
- if (_plugin.IsEnabled)
- {
- _plugin.Disable();
- _plugin.Enable();
- }
- else
- {
- // 如果插件未启用,则启用它
- _plugin.Enable();
-
- // 通知PluginSettingsWindow刷新插件列表
- var window = Window.GetWindow(this);
- if (window is PluginSettingsWindow pluginSettingsWindow)
- {
- // 触发刷新
- pluginSettingsWindow.RefreshPluginList();
- }
- }
-
- MessageBox.Show("设置已保存并应用!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"保存设置时出错: {ex.Message}", LogHelper.LogType.Error);
- MessageBox.Show($"保存设置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- ///
- /// 应用项选择变更事件
- ///
- private void DgApps_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- UpdateButtonStates();
- }
-
- ///
- /// 编辑启动项
- ///
- /// 启动项
- /// 是否为新建
- private void EditLauncherItem(LauncherItem item, bool isNew)
- {
- // 创建简单的编辑窗口
- Window editWindow = new Window
- {
- Title = isNew ? "添加" : "编辑应用",
- Width = 400,
- Height = 200,
- WindowStartupLocation = WindowStartupLocation.CenterScreen,
- ResizeMode = ResizeMode.NoResize
- };
-
- // 创建编辑表单
- Grid grid = new Grid
- {
- Margin = new Thickness(20)
- };
-
- grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
- grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
- grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
-
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
-
- // 名称输入框
- TextBlock nameLabel = new TextBlock
- {
- Text = "名称:",
- VerticalAlignment = VerticalAlignment.Center
- };
- TextBox nameTextBox = new TextBox
- {
- Text = item.Name,
- Margin = new Thickness(0, 5, 0, 5)
- };
-
- Grid.SetRow(nameLabel, 0);
- Grid.SetColumn(nameLabel, 0);
- Grid.SetRow(nameTextBox, 0);
- Grid.SetColumn(nameTextBox, 1);
-
- grid.Children.Add(nameLabel);
- grid.Children.Add(nameTextBox);
-
- // 路径输入框
- TextBlock pathLabel = new TextBlock
- {
- Text = "路径:",
- VerticalAlignment = VerticalAlignment.Center
- };
- Grid pathGrid = new Grid();
- pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
- pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength() });
-
- TextBox pathTextBox = new TextBox
- {
- Text = item.Path,
- Margin = new Thickness(0, 5, 5, 5)
- };
- Button browseButton = new Button
- {
- Content = "浏览",
- Padding = new Thickness(5, 0, 5, 0),
- Margin = new Thickness(0, 5, 0, 5)
- };
-
- browseButton.Click += (s, e) =>
- {
- OpenFileDialog dialog = new OpenFileDialog
- {
- Title = "选择应用程序",
- Filter = "应用程序 (*.exe)|*.exe|所有文件 (*.*)|*.*",
- Multiselect = false,
- FileName = pathTextBox.Text
- };
-
- if (dialog.ShowDialog() == true)
- {
- pathTextBox.Text = dialog.FileName;
-
- // 如果选择的是.exe文件,自动获取文件名填入名称字段
- if (Path.GetExtension(dialog.FileName).ToLower() == ".exe")
- {
- string fileName = Path.GetFileNameWithoutExtension(dialog.FileName);
- // 只有在名称字段为空或者是新建项目时才自动填入
- if (string.IsNullOrWhiteSpace(nameTextBox.Text) || isNew)
- {
- nameTextBox.Text = fileName;
- }
- }
- }
- };
-
- Grid.SetColumn(pathTextBox, 0);
- Grid.SetColumn(browseButton, 1);
- pathGrid.Children.Add(pathTextBox);
- pathGrid.Children.Add(browseButton);
-
- Grid.SetRow(pathLabel, 1);
- Grid.SetColumn(pathLabel, 0);
- Grid.SetRow(pathGrid, 1);
- Grid.SetColumn(pathGrid, 1);
-
- grid.Children.Add(pathLabel);
- grid.Children.Add(pathGrid);
-
- // 确认和取消按钮
- StackPanel buttonPanel = new StackPanel
- {
- Orientation = Orientation.Horizontal,
- HorizontalAlignment = HorizontalAlignment.Right,
- Margin = new Thickness(0, 10, 0, 0)
- };
-
- Button okButton = new Button
- {
- Content = "确定",
- Padding = new Thickness(15, 5, 15, 5),
- Margin = new Thickness(0, 0, 10, 0),
- IsDefault = true
- };
-
- Button cancelButton = new Button
- {
- Content = "取消",
- Padding = new Thickness(15, 5, 15, 5),
- IsCancel = true
- };
-
- okButton.Click += (s, e) =>
- {
- // 验证输入
- if (string.IsNullOrWhiteSpace(nameTextBox.Text))
- {
- MessageBox.Show("请输入应用名称!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
- return;
- }
-
- if (string.IsNullOrWhiteSpace(pathTextBox.Text))
- {
- MessageBox.Show("请输入应用路径!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
- return;
- }
-
- // 更新项目
- item.Name = nameTextBox.Text;
- item.Path = pathTextBox.Text;
-
- // 如果是新建,添加到集合
- if (isNew)
- {
- _plugin.AddLauncherItem(item);
- }
- else
- {
- // 触发属性变更通知,刷新DataGrid
- if (DgApps.ItemsSource is ICollectionView view)
- {
- view.Refresh();
- }
-
- // 保存配置
- _plugin.SaveConfig();
- }
-
- editWindow.DialogResult = true;
- editWindow.Close();
- };
-
- cancelButton.Click += (s, e) =>
- {
- editWindow.DialogResult = false;
- editWindow.Close();
- };
-
- buttonPanel.Children.Add(okButton);
- buttonPanel.Children.Add(cancelButton);
-
- Grid.SetRow(buttonPanel, 2);
- Grid.SetColumnSpan(buttonPanel, 2);
-
- grid.Children.Add(buttonPanel);
-
- // 设置窗口内容
- editWindow.Content = grid;
-
- // 显示窗口
- editWindow.ShowDialog();
- }
- }
-}
\ No newline at end of file
diff --git a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml b/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml
deleted file mode 100644
index b6463afe..00000000
--- a/Ink Canvas/Helpers/Plugins/BuiltIn/SuperLauncher/LauncherWindow.xaml
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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
-
-
-
-
-
-
-
-
-
-
_boothResolutionWidth;
public int BoothResolutionHeight => _boothResolutionHeight;
- /// 供插件系统访问的白板页面列表(只读)。
- public IList WhiteboardPages => whiteboardPages;
-
- /// 供插件系统访问的当前页索引。
- public int CurrentPageIndex => currentPageIndex;
-
private static Cursor _cachedPenCursor = null;
private static readonly object _cursorLock = new object();
@@ -1372,8 +1365,6 @@ namespace Ink_Canvas
}), DispatcherPriority.Loaded);
}
- // 初始化插件系统
- InitializePluginSystem();
// 确保开关和设置同步
ToggleSwitchNoFocusMode.IsOn = Settings.Advanced.IsNoFocusMode;
ApplyNoFocusMode();
@@ -2584,9 +2575,6 @@ namespace Ink_Canvas
case "about":
targetGroupBox = GroupBoxAbout;
break;
- case "plugins":
- targetGroupBox = GroupBoxPlugins;
- break;
default:
// 默认滚动到顶部
SettingsPanelScrollViewer.ScrollToTop();
@@ -2734,9 +2722,6 @@ namespace Ink_Canvas
case "about":
SetNavButtonTag("about");
break;
- case "plugins":
- SetNavButtonTag("plugins");
- break;
}
}
@@ -2823,64 +2808,6 @@ namespace Ink_Canvas
#endregion Navigation Sidebar Methods
- #region 插件???
-
- // 添加插件系统初始化方法
- private void InitializePluginSystem()
- {
- try
- {
- PluginRuntime.Initialize(this);
- PluginManager.Instance.Initialize();
- LogHelper.WriteLogToFile("插件系统已初始化");
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"初始化插件系统时出错: {ex.Message}", LogHelper.LogType.Error);
- }
- }
-
- // 添加插件管理导航点击事件处理
- private void NavPlugins_Click(object sender, RoutedEventArgs e)
- {
- ShowSettingsSection("plugins");
- }
-
- // 添加打开插件管理器按钮点击事件
- private void BtnOpenPluginManager_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- // 暂时隐藏设置面板
- BorderSettings.Visibility = Visibility.Hidden;
- BorderSettingsMask.Visibility = Visibility.Hidden;
-
- // 创建并显示插件设置窗口
- PluginSettingsWindow pluginSettingsWindow = new PluginSettingsWindow();
-
- // 设置窗口关闭事件,用于在插件管理窗口关闭后恢复设置面板
- pluginSettingsWindow.Closed += (s, args) =>
- {
- // 恢复设置面板显示
- BorderSettings.Visibility = Visibility.Visible;
- BorderSettingsMask.Visibility = Visibility.Visible;
- };
-
- // 显示插件设置窗口
- pluginSettingsWindow.ShowDialog();
- }
- catch (Exception ex)
- {
- // 确保在发生错误时也恢复设置面板显示
- BorderSettings.Visibility = Visibility.Visible;
- BorderSettingsMask.Visibility = Visibility.Visible;
-
- LogHelper.WriteLogToFile($"打开插件管理器时出错: {ex.Message}", LogHelper.LogType.Error);
- MessageBox.Show($"打开插件管理器时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
- #endregion 插件???
-
#region 新设置窗口
///
diff --git a/Ink Canvas/MainWindow_cs/MainWindow_PluginHostApi.cs b/Ink Canvas/MainWindow_cs/MainWindow_PluginHostApi.cs
deleted file mode 100644
index b94ad59a..00000000
--- a/Ink Canvas/MainWindow_cs/MainWindow_PluginHostApi.cs
+++ /dev/null
@@ -1,152 +0,0 @@
-using System;
-using System.Windows;
-using System.Windows.Ink;
-using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
-
-namespace Ink_Canvas
-{
- ///
- /// 供 调用的宿主 API,封装 UI 线程与内部墨迹逻辑。
- ///
- public partial class MainWindow : Window
- {
- internal void PluginHost_RunOnUiThread(Action action)
- {
- if (action == null)
- {
- return;
- }
-
- if (Dispatcher.CheckAccess())
- {
- action();
- }
- else
- {
- Dispatcher.Invoke(action);
- }
- }
-
- internal void PluginHost_Undo()
- {
- PluginHost_RunOnUiThread(() =>
- {
- if (inkCanvas.GetSelectedStrokes().Count != 0)
- {
- GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
- inkCanvas.Select(new StrokeCollection());
- }
-
- var item = timeMachine.Undo();
- ApplyHistoryToCanvas(item);
- });
- }
-
- internal void PluginHost_Redo()
- {
- PluginHost_RunOnUiThread(() =>
- {
- if (inkCanvas.GetSelectedStrokes().Count != 0)
- {
- GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
- inkCanvas.Select(new StrokeCollection());
- }
-
- var item = timeMachine.Redo();
- ApplyHistoryToCanvas(item);
- });
- }
-
- internal void PluginHost_ClearInk(bool erasedByCode)
- {
- PluginHost_RunOnUiThread(() => ClearStrokes(erasedByCode));
- }
-
- internal bool PluginHost_CanUndo()
- {
- if (Dispatcher.CheckAccess())
- {
- return timeMachine != null && timeMachine.CanUndo;
- }
-
- return Dispatcher.Invoke(() => timeMachine != null && timeMachine.CanUndo);
- }
-
- internal bool PluginHost_CanRedo()
- {
- if (Dispatcher.CheckAccess())
- {
- return timeMachine != null && timeMachine.CanRedo;
- }
-
- return Dispatcher.Invoke(() => timeMachine != null && timeMachine.CanRedo);
- }
-
- internal void PluginHost_ShowInfo(string title, string message)
- {
- PluginHost_RunOnUiThread(() =>
- {
- try
- {
- MessageBox.Show(message ?? string.Empty, title ?? string.Empty);
- }
- catch
- {
- // 忽略对话框失败,避免插件拖垮宿主
- }
- });
- }
-
- internal bool PluginHost_ShowConfirm(string title, string message)
- {
- if (Dispatcher.CheckAccess())
- {
- try
- {
- return MessageBox.Show(message ?? string.Empty, title ?? string.Empty, MessageBoxButton.YesNo) ==
- MessageBoxResult.Yes;
- }
- catch
- {
- return false;
- }
- }
-
- return Dispatcher.Invoke(() =>
- {
- try
- {
- return MessageBox.Show(message ?? string.Empty, title ?? string.Empty, MessageBoxButton.YesNo) ==
- MessageBoxResult.Yes;
- }
- catch
- {
- return false;
- }
- });
- }
-
- internal string PluginHost_ShowInput(string title, string message, string defaultValue)
- {
- string Show()
- {
- try
- {
- return Microsoft.VisualBasic.Interaction.InputBox(message ?? string.Empty, title ?? string.Empty,
- defaultValue ?? string.Empty);
- }
- catch
- {
- return defaultValue ?? string.Empty;
- }
- }
-
- if (Dispatcher.CheckAccess())
- {
- return Show();
- }
-
- return Dispatcher.Invoke(Show);
- }
- }
-}
diff --git a/Ink Canvas/Windows/PluginSettingsWindow.xaml b/Ink Canvas/Windows/PluginSettingsWindow.xaml
deleted file mode 100644
index 65633802..00000000
--- a/Ink Canvas/Windows/PluginSettingsWindow.xaml
+++ /dev/null
@@ -1,179 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Ink Canvas/Windows/PluginSettingsWindow.xaml.cs b/Ink Canvas/Windows/PluginSettingsWindow.xaml.cs
deleted file mode 100644
index 43575d21..00000000
--- a/Ink Canvas/Windows/PluginSettingsWindow.xaml.cs
+++ /dev/null
@@ -1,727 +0,0 @@
-using Ink_Canvas.Helpers;
-using Ink_Canvas.Helpers.Plugins;
-using Ink_Canvas.Helpers.Plugins.BuiltIn;
-using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher;
-using iNKORE.UI.WPF.Modern.Controls;
-using Microsoft.Win32;
-using System;
-using System.Collections.ObjectModel;
-using System.ComponentModel;
-using System.IO;
-using System.Linq;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Threading;
-using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
-
-namespace Ink_Canvas.Windows
-{
- ///
- /// PluginSettingsWindow.xaml 的交互逻辑
- ///
- public partial class PluginSettingsWindow : Window, INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler PropertyChanged;
-
- ///
- /// 刷新插件列表
- ///
- public void RefreshPluginList()
- {
- LoadPlugins();
-
- // 如果当前选中的插件仍然存在,保持其选中状态
- if (SelectedPlugin != null)
- {
- var matchingPlugin = Plugins.FirstOrDefault(p => p.Plugin.GetType().FullName == SelectedPlugin.GetType().FullName);
- if (matchingPlugin != null)
- {
- PluginListView.SelectedItem = matchingPlugin;
- }
- }
-
- OnPropertyChanged(nameof(SelectedPlugin));
- LogHelper.WriteLogToFile("插件列表已刷新");
- }
-
- private IPlugin _selectedPlugin;
-
- ///
- /// 当前选中的插件
- ///
- public IPlugin SelectedPlugin
- {
- get => _selectedPlugin;
- set
- {
- if (_selectedPlugin != value)
- {
- _selectedPlugin = value;
- OnPropertyChanged(nameof(SelectedPlugin));
- OnPropertyChanged(nameof(Name));
- OnPropertyChanged(nameof(Version));
- OnPropertyChanged(nameof(Author));
- OnPropertyChanged(nameof(Description));
- OnPropertyChanged(nameof(IsEnabled));
- OnPropertyChanged(nameof(IsBuiltIn));
- }
- }
- }
-
- public new string Name => SelectedPlugin?.Name ?? string.Empty;
- public string Version => SelectedPlugin?.Version?.ToString() ?? string.Empty;
- public string Author => SelectedPlugin?.Author ?? string.Empty;
- public string Description => SelectedPlugin?.Description ?? string.Empty;
- public new bool IsEnabled => SelectedPlugin is PluginBase plugin && plugin.IsEnabled;
- public bool IsBuiltIn => SelectedPlugin?.IsBuiltIn ?? false;
-
- ///
- /// 插件列表
- ///
- public ObservableCollection Plugins { get; } = new ObservableCollection();
-
- public PluginSettingsWindow()
- {
- InitializeComponent();
-
- // 设置数据上下文
- PluginDetailGrid.DataContext = this;
-
- // 设置导出按钮初始状态
- BtnExportPlugin.IsEnabled = false;
- BtnExportPlugin.ToolTip = "请先选择要导出的插件";
-
- // 加载插件列表
- LoadPlugins();
-
- // 注册窗口关闭事件
- Closing += PluginSettingsWindow_Closing;
- }
-
- ///
- /// 窗口关闭事件处理
- ///
- private void PluginSettingsWindow_Closing(object sender, CancelEventArgs e)
- {
- try
- {
- // 保存插件配置
- LogHelper.WriteLogToFile("插件管理窗口关闭,保存插件配置...");
- PluginManager.Instance.SaveConfig();
- LogHelper.WriteLogToFile("插件配置已保存");
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"关闭窗口时保存插件配置出错: {ex.Message}", LogHelper.LogType.Error);
- }
- }
-
- ///
- /// 加载插件列表
- ///
- private void LoadPlugins()
- {
- Plugins.Clear();
-
- LogHelper.WriteLogToFile($"开始加载插件列表到UI,插件总数: {PluginManager.Instance.Plugins.Count}");
-
- // 添加所有已加载的插件
- foreach (var plugin in PluginManager.Instance.Plugins)
- {
- bool isEnabled = false;
-
- // 从插件实例获取启用状态
- if (plugin is PluginBase pluginBase)
- {
- isEnabled = pluginBase.IsEnabled;
- }
-
- // 记录插件详细信息
- LogHelper.WriteLogToFile($"正在加载插件到UI: 类型={plugin.GetType().FullName}, 名称={plugin.Name ?? "未命名"}, 状态={isEnabled}");
-
- // 创建视图模型并添加到集合
- var viewModel = new PluginViewModel(plugin)
- {
- IsEnabled = isEnabled
- };
- Plugins.Add(viewModel);
-
- LogHelper.WriteLogToFile($"已加载插件到UI列表: {plugin.Name},状态: {(isEnabled ? "启用" : "禁用")}");
- }
-
- // 绑定到ListView
- LogHelper.WriteLogToFile($"绑定 {Plugins.Count} 个插件到ListView");
- PluginListView.ItemsSource = Plugins;
-
- // 如果有插件,选择第一个
- if (Plugins.Count > 0)
- {
- LogHelper.WriteLogToFile($"选择第一个插件: {Plugins[0].Name}");
- PluginListView.SelectedIndex = 0;
- }
- else
- {
- LogHelper.WriteLogToFile("没有找到任何插件", LogHelper.LogType.Warning);
- }
- }
-
- ///
- /// 更新属性变更通知
- ///
- /// 属性名称
- protected void OnPropertyChanged(string propertyName)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
-
- ///
- /// 插件列表选择变更事件
- ///
- private void PluginListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- if (PluginListView.SelectedItem is PluginViewModel viewModel)
- {
- // 设置当前选中的插件
- SelectedPlugin = viewModel.Plugin;
-
- // 加载插件设置界面
- PluginSettingsContainer.Content = SelectedPlugin.GetSettingsView();
-
- // 设置删除按钮的可见性
- BtnDeletePlugin.Visibility = !SelectedPlugin.IsBuiltIn ? Visibility.Visible : Visibility.Collapsed;
-
- // 设置导出按钮的可用状态
- BtnExportPlugin.IsEnabled = !SelectedPlugin.IsBuiltIn;
- if (SelectedPlugin.IsBuiltIn)
- {
- BtnExportPlugin.ToolTip = "内置插件无法导出";
- }
- else
- {
- BtnExportPlugin.ToolTip = "将插件导出为.iccpp文件";
- }
- }
- else
- {
- SelectedPlugin = null;
- PluginSettingsContainer.Content = null;
- BtnDeletePlugin.Visibility = Visibility.Collapsed;
- BtnExportPlugin.IsEnabled = false;
- BtnExportPlugin.ToolTip = "请先选择要导出的插件";
- }
- }
-
- ///
- /// 加载本地插件按钮点击事件
- ///
- private void BtnLoadPlugin_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- // 创建文件对话框
- OpenFileDialog dialog = new OpenFileDialog
- {
- Filter = "ICC插件文件(*.iccpp)|*.iccpp",
- Title = "选择要加载的插件文件"
- };
-
- // 显示对话框
- if (dialog.ShowDialog() == true)
- {
- // 获取插件文件路径
- string pluginPath = dialog.FileName;
-
- // 检查是否在Plugins目录下
- string pluginsDirectory = Path.Combine(App.RootPath, "Plugins");
- string targetPath = Path.Combine(pluginsDirectory, Path.GetFileName(pluginPath));
-
- // 确保Plugins目录存在
- if (!Directory.Exists(pluginsDirectory))
- {
- Directory.CreateDirectory(pluginsDirectory);
- }
-
- // 如果插件不在Plugins目录下,复制过去
- if (!string.Equals(pluginPath, targetPath, StringComparison.OrdinalIgnoreCase))
- {
- File.Copy(pluginPath, targetPath, true);
- pluginPath = targetPath;
- }
-
- // 加载插件
- IPlugin plugin = PluginManager.Instance.LoadExternalPlugin(pluginPath);
-
- if (plugin != null)
- {
- // 刷新插件列表
- LoadPlugins();
-
- // 选择新加载的插件
- foreach (var item in Plugins)
- {
- if (item.Plugin == plugin)
- {
- PluginListView.SelectedItem = item;
- break;
- }
- }
-
- MessageBox.Show($"插件 {plugin.Name} v{plugin.Version} 已成功加载!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
- }
- else
- {
- MessageBox.Show("插件加载失败,请检查文件是否有效。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
- }
- catch (Exception ex)
- {
- MessageBox.Show($"加载插件时发生错误:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- ///
- /// 删除插件按钮点击事件
- ///
- private void BtnDeletePlugin_Click(object sender, RoutedEventArgs e)
- {
- if (SelectedPlugin == null) return;
-
- // 不能删除内置插件
- if (SelectedPlugin.IsBuiltIn)
- {
- MessageBox.Show("内置插件无法删除。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
- return;
- }
-
- // 保存插件名称,以便在删除后使用
- string pluginName = SelectedPlugin.Name;
-
- // 确认删除
- MessageBoxResult result = MessageBox.Show(
- $"确定要删除插件 {pluginName} 吗?\n此操作将永久删除插件文件,无法恢复。",
- "删除确认",
- MessageBoxButton.YesNo,
- MessageBoxImage.Warning);
-
- if (result == MessageBoxResult.Yes)
- {
- // 删除插件
- bool success = PluginManager.Instance.DeletePlugin(SelectedPlugin);
-
- if (success)
- {
- // 刷新插件列表
- LoadPlugins();
-
- // 如果还有插件,选择第一个
- if (Plugins.Count > 0)
- {
- PluginListView.SelectedIndex = 0;
- }
-
- MessageBox.Show($"插件 {pluginName} 已成功删除。", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
- }
- else
- {
- MessageBox.Show($"删除插件 {pluginName} 失败,请稍后重试。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
- }
-
- ///
- /// 导出插件按钮点击事件
- ///
- private void BtnExportPlugin_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- // 检查是否有选中的插件
- if (SelectedPlugin == null)
- {
- MessageBox.Show("请先选择要导出的插件", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
- return;
- }
-
- // 检查是否为内置插件
- if (SelectedPlugin.IsBuiltIn)
- {
- MessageBox.Show("内置插件无法导出", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
- return;
- }
-
- // 检查插件文件是否存在
- string pluginPath = null;
- if (SelectedPlugin is PluginBase pluginBase)
- {
- pluginPath = pluginBase.PluginPath;
- }
-
- if (string.IsNullOrEmpty(pluginPath) || !File.Exists(pluginPath))
- {
- MessageBox.Show("插件文件不存在或无法访问", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- return;
- }
-
- // 创建保存文件对话框
- SaveFileDialog dialog = new SaveFileDialog
- {
- Filter = "ICC插件文件(*.iccpp)|*.iccpp",
- Title = "导出插件",
- FileName = Path.GetFileName(pluginPath)
- };
-
- // 显示对话框
- if (dialog.ShowDialog() == true)
- {
- // 获取目标路径
- string targetPath = dialog.FileName;
-
- // 如果目标文件已存在,询问是否覆盖
- if (File.Exists(targetPath) && !string.Equals(pluginPath, targetPath, StringComparison.OrdinalIgnoreCase))
- {
- MessageBoxResult result = MessageBox.Show("目标文件已存在,是否覆盖?", "确认", MessageBoxButton.YesNo, MessageBoxImage.Question);
- if (result != MessageBoxResult.Yes)
- {
- return;
- }
- }
-
- // 复制插件文件到目标路径
- if (!string.Equals(pluginPath, targetPath, StringComparison.OrdinalIgnoreCase))
- {
- File.Copy(pluginPath, targetPath, true);
- }
-
- LogHelper.WriteLogToFile($"插件 {SelectedPlugin.Name} 已成功导出到: {targetPath}");
- MessageBox.Show($"插件 {SelectedPlugin.Name} 已成功导出!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
- }
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"导出插件时出错: {ex.Message}", LogHelper.LogType.Error);
- MessageBox.Show($"导出插件时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- ///
- /// 插件开关切换事件
- ///
- private void PluginToggleSwitch_Toggled(object sender, RoutedEventArgs e)
- {
- try
- {
- if (sender is ToggleSwitch toggleSwitch &&
- toggleSwitch.Tag is IPlugin plugin)
- {
- // 记录当前开关状态
- bool targetState = toggleSwitch.IsOn;
-
- // 记录插件类型名称和名称,用于稍后查找重载后的插件
- string pluginTypeName = plugin.GetType().FullName;
- string pluginName = plugin.Name;
- bool wasBuiltIn = plugin.IsBuiltIn;
-
- LogHelper.WriteLogToFile($"UI开关切换: {pluginName}, 目标状态: {(targetState ? "启用" : "禁用")}");
-
- // 切换插件状态
- PluginManager.Instance.TogglePlugin(plugin, targetState);
-
- // 立即同步保存配置到文件(确保状态被立即持久化)
- PluginManager.Instance.SaveConfig();
- LogHelper.WriteLogToFile("插件状态已立即保存到配置文件");
-
- // 延迟一下再检查状态,确保变更已应用
- Dispatcher.BeginInvoke(new Action(() =>
- {
- try
- {
- // 查找最新的插件实例
- IPlugin currentPlugin = null;
- foreach (var p in PluginManager.Instance.Plugins)
- {
- if (p.GetType().FullName == pluginTypeName || p.Name == pluginName)
- {
- currentPlugin = p;
- break;
- }
- }
-
- if (currentPlugin == null)
- {
- LogHelper.WriteLogToFile($"无法找到插件: {pluginName},UI状态可能不准确", LogHelper.LogType.Warning);
- return;
- }
-
- // 检查实际状态
- bool actualState = currentPlugin is PluginBase pb && pb.IsEnabled;
- LogHelper.WriteLogToFile($"插件 {pluginName} 实际状态: {(actualState ? "启用" : "禁用")}");
-
- // 更新视图模型
- PluginViewModel viewModel = null;
- if (toggleSwitch.DataContext is PluginViewModel vm)
- {
- viewModel = vm;
- }
- else
- {
- viewModel = Plugins.FirstOrDefault(p => p.Plugin == currentPlugin);
- }
-
- if (viewModel != null)
- {
- // 确保视图模型状态与实际状态一致
- if (viewModel.IsEnabled != actualState)
- {
- LogHelper.WriteLogToFile($"同步视图模型状态: {(actualState ? "启用" : "禁用")}");
- viewModel.IsEnabled = actualState;
- }
-
- // 确保UI开关状态与实际状态一致
- if (toggleSwitch.IsOn != actualState)
- {
- LogHelper.WriteLogToFile($"同步UI开关状态: {(actualState ? "启用" : "禁用")}");
- toggleSwitch.IsOn = actualState;
- }
- }
-
- // 如果是当前选中的插件,更新属性
- if (currentPlugin == SelectedPlugin)
- {
- OnPropertyChanged(nameof(IsEnabled));
- }
-
- // 对于内置插件,特别处理
- if (wasBuiltIn)
- {
- // 特殊插件刷新逻辑,如果是超级启动台插件,立即刷新UI
- if (currentPlugin is SuperLauncherPlugin &&
- PluginSettingsContainer.Content is LauncherSettingsControl)
- {
- // 重新获取设置界面
- PluginSettingsContainer.Content = currentPlugin.GetSettingsView();
- }
- }
-
- LogHelper.WriteLogToFile($"插件 {pluginName} UI状态同步完成");
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"同步UI状态时出错: {ex.Message}", LogHelper.LogType.Error);
- }
- }), DispatcherPriority.Background);
- }
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"切换插件状态时出错: {ex.Message}", LogHelper.LogType.Error);
- MessageBox.Show($"切换插件状态时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- ///
- /// 刷新插件列表按钮点击事件
- ///
- private void Button_Refresh_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- // 刷新插件列表
- RefreshPluginList();
- LogHelper.WriteLogToFile("用户点击刷新按钮,刷新插件列表");
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"刷新插件列表时出错: {ex.Message}", LogHelper.LogType.Error);
- MessageBox.Show($"刷新插件列表时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- ///
- /// 保存插件状态按钮点击事件
- ///
- private void BtnSaveConfig_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- int syncCount = 0;
-
- // 遍历界面上所有插件视图模型,获取开关状态
- foreach (var viewModel in Plugins)
- {
- try
- {
- if (viewModel.Plugin != null)
- {
- // 获取UI中开关的当前状态(从界面控件读取)
- bool uiState = viewModel.IsEnabled;
-
- // 获取插件类型名,用于查找配置
- string pluginTypeName = viewModel.Plugin.GetType().FullName;
-
- // 查找实际的插件实例(可能与viewModel.Plugin不同,因为可能已经重新加载)
- IPlugin actualPlugin = null;
- foreach (var p in PluginManager.Instance.Plugins)
- {
- if (p.GetType().FullName == pluginTypeName)
- {
- actualPlugin = p;
- break;
- }
- }
-
- // 如果找不到对应的实际插件实例,跳过
- if (actualPlugin == null)
- {
- LogHelper.WriteLogToFile($"手动保存:无法找到与UI对应的插件实例:{viewModel.Name}", LogHelper.LogType.Warning);
- continue;
- }
-
- // 获取插件实际状态
- bool pluginState = false;
- if (actualPlugin is PluginBase pluginBase)
- {
- pluginState = pluginBase.IsEnabled;
- }
-
- // 如果界面状态与插件实际状态不一致,应用界面状态
- if (uiState != pluginState)
- {
- // 应用界面的状态到插件
- PluginManager.Instance.TogglePlugin(actualPlugin, uiState);
- LogHelper.WriteLogToFile($"手动保存:同步插件 {actualPlugin.Name} 状态 {pluginState} -> {uiState}");
- syncCount++;
- }
-
- // 确保配置中的状态也与界面一致
- if (PluginManager.Instance.PluginStates.TryGetValue(pluginTypeName, out bool configState) && configState != uiState)
- {
- PluginManager.Instance.PluginStates[pluginTypeName] = uiState;
- LogHelper.WriteLogToFile($"手动保存:更新配置中插件 {actualPlugin.Name} 状态 {configState} -> {uiState}");
- syncCount++;
- }
- }
- }
- catch (Exception pluginEx)
- {
- // 单个插件处理失败不应该影响其他插件
- LogHelper.WriteLogToFile($"手动保存:处理插件 {viewModel.Name} 时出错: {pluginEx.Message}", LogHelper.LogType.Error);
- }
- }
-
- // 保存插件状态配置
- PluginManager.Instance.SaveConfig();
-
- // 记录日志
- LogHelper.WriteLogToFile($"用户手动保存插件状态配置,同步了 {syncCount} 个状态变更");
-
- // 刷新插件列表,确保UI与最新状态同步
- RefreshPluginList();
-
- // 显示保存成功提示
- string message = syncCount > 0
- ? $"插件状态已成功保存,同步了 {syncCount} 个状态变更"
- : "插件状态已成功保存,所有插件状态已是最新";
- MessageBox.Show(message, "保存成功", MessageBoxButton.OK, MessageBoxImage.Information);
- }
- catch (Exception ex)
- {
- // 记录错误日志
- LogHelper.WriteLogToFile($"手动保存插件状态时出错: {ex.Message}", LogHelper.LogType.Error);
-
- // 显示错误信息
- MessageBox.Show($"保存插件状态时发生错误: {ex.Message}", "保存失败", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
-
- }
-
- ///
- /// 插件视图模型
- ///
- public class PluginViewModel : INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler PropertyChanged;
-
- ///
- /// 插件实例
- ///
- public IPlugin Plugin { get; }
-
- ///
- /// 插件名称
- ///
- public string Name
- {
- get
- {
- string name = Plugin?.Name ?? "未命名插件";
- LogHelper.WriteLogToFile($"获取插件名称: {name},类型: {Plugin?.GetType().FullName ?? "未知"}");
- return name;
- }
- }
-
- ///
- /// 插件是否启用
- ///
- private bool _isEnabled;
- public bool IsEnabled
- {
- get => _isEnabled;
- set
- {
- if (_isEnabled != value)
- {
- _isEnabled = value;
- OnPropertyChanged(nameof(IsEnabled));
- }
- }
- }
-
- public PluginViewModel(IPlugin plugin)
- {
- Plugin = plugin;
-
- // 获取实际状态
- _isEnabled = plugin is PluginBase pluginBase && pluginBase.IsEnabled;
-
- // 记录日志
- LogHelper.WriteLogToFile($"创建插件视图模型: {plugin?.GetType().FullName ?? "未知"}, 名称: {plugin?.Name ?? "未命名"}");
-
- // 注册插件状态变更事件
- if (plugin is PluginBase pb)
- {
- pb.EnabledStateChanged += Plugin_EnabledStateChanged;
- }
- }
-
- ///
- /// 处理插件状态变更事件
- ///
- private void Plugin_EnabledStateChanged(object sender, bool isEnabled)
- {
- // 在UI线程上更新状态
- Application.Current.Dispatcher.BeginInvoke(new Action(() =>
- {
- IsEnabled = isEnabled;
-
- // 确保配置立即保存
- if (sender is IPlugin plugin)
- {
- LogHelper.WriteLogToFile($"视图模型捕获到插件 {plugin.Name} 状态变更: {(isEnabled ? "启用" : "禁用")}");
- PluginManager.Instance.SaveConfig();
- LogHelper.WriteLogToFile("视图模型已触发配置保存");
- }
- }));
- }
-
- ///
- /// 属性变更通知
- ///
- protected void OnPropertyChanged(string propertyName)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
-}
\ No newline at end of file
diff --git a/Ink Canvas/Windows/SettingsViews/SettingsViews/StartupPanel.xaml b/Ink Canvas/Windows/SettingsViews/SettingsViews/StartupPanel.xaml
index b983cafc..cd88f2b3 100644
--- a/Ink Canvas/Windows/SettingsViews/SettingsViews/StartupPanel.xaml
+++ b/Ink Canvas/Windows/SettingsViews/SettingsViews/StartupPanel.xaml
@@ -142,15 +142,6 @@
-
-
-
-
-
-
-
diff --git a/Plugins/Host/CollectingPluginRegistry.cs b/Plugins/Host/CollectingPluginRegistry.cs
deleted file mode 100644
index 0da5142c..00000000
--- a/Plugins/Host/CollectingPluginRegistry.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Windows.Controls;
-using InkCanvasForClass.PluginSdk;
-
-namespace InkCanvasForClass.PluginHost
-{
- ///
- /// 收集插件登记的菜单 / 工具栏 / 设置页,供宿主窗口在启动后统一挂载。
- ///
- public sealed class CollectingPluginRegistry : IPluginRegistry
- {
- private string _currentPluginId = "";
-
- public string CurrentPluginId => _currentPluginId;
-
- public ObservableCollection MenuItems { get; } =
- new ObservableCollection();
-
- public ObservableCollection ToolbarButtons { get; } =
- new ObservableCollection();
-
- public ObservableCollection SettingsPages { get; } =
- new ObservableCollection();
-
- public void SetCurrentPluginId(string pluginId)
- {
- _currentPluginId = pluginId ?? "";
- }
-
- public void RegisterMenuItem(string groupKey, MenuItem item)
- {
- if (item == null) return;
- MenuItems.Add(new MenuItemRegistration(_currentPluginId, groupKey ?? "", item));
- }
-
- public void RegisterToolbarButton(Button button)
- {
- if (button == null) return;
- ToolbarButtons.Add(new ToolbarButtonRegistration(_currentPluginId, button));
- }
-
- public void RegisterSettingsPage(string pageId, string displayName, Func createView)
- {
- if (string.IsNullOrWhiteSpace(pageId) || createView == null) return;
- SettingsPages.Add(new SettingsPageRegistration(
- _currentPluginId,
- pageId,
- displayName ?? pageId,
- createView));
- }
-
- public void Clear()
- {
- MenuItems.Clear();
- ToolbarButtons.Clear();
- SettingsPages.Clear();
- _currentPluginId = "";
- }
- }
-
- public sealed class MenuItemRegistration
- {
- public MenuItemRegistration(string pluginId, string groupKey, MenuItem item)
- {
- PluginId = pluginId ?? "";
- GroupKey = groupKey ?? "";
- Item = item ?? throw new ArgumentNullException(nameof(item));
- }
-
- public string PluginId { get; }
- public string GroupKey { get; }
- public MenuItem Item { get; }
- }
-
- public sealed class ToolbarButtonRegistration
- {
- public ToolbarButtonRegistration(string pluginId, Button button)
- {
- PluginId = pluginId ?? "";
- Button = button ?? throw new ArgumentNullException(nameof(button));
- }
-
- public string PluginId { get; }
- public Button Button { get; }
- }
-
- public sealed class SettingsPageRegistration
- {
- public SettingsPageRegistration(string pluginId, string pageId, string displayName, Func createView)
- {
- PluginId = pluginId ?? "";
- PageId = pageId ?? "";
- DisplayName = displayName ?? "";
- CreateView = createView ?? throw new ArgumentNullException(nameof(createView));
- }
-
- public string PluginId { get; }
- public string PageId { get; }
- public string DisplayName { get; }
- public Func CreateView { get; }
- }
-}
diff --git a/Plugins/Host/InkCanvas.PluginHost.csproj b/Plugins/Host/InkCanvas.PluginHost.csproj
deleted file mode 100644
index 6fe1ef7f..00000000
--- a/Plugins/Host/InkCanvas.PluginHost.csproj
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
- net472
- true
- InkCanvasForClass.PluginHost
- InkCanvas.PluginHost
- 7.3
- disable
-
-
-
-
-
-
-
diff --git a/Plugins/SDK/IInkCanvasPlugin.cs b/Plugins/SDK/IInkCanvasPlugin.cs
deleted file mode 100644
index 1b36f434..00000000
--- a/Plugins/SDK/IInkCanvasPlugin.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Windows.Controls;
-using System.Windows.Media;
-
-namespace InkCanvasForClass.PluginSdk
-{
- ///
- /// Ink Canvas 插件接口
- ///
- public interface IInkCanvasPlugin
- {
- ///
- /// 插件唯一标识符
- ///
- string Id { get; }
-
- ///
- /// 插件名称
- ///
- string Name { get; }
-
- ///
- /// 插件描述
- ///
- string Description { get; }
-
- ///
- /// 插件版本
- ///
- Version Version { get; }
-
- ///
- /// 插件作者
- ///
- string Author { get; }
-
- ///
- /// 插件主页URL
- ///
- string Homepage { get; }
-
- ///
- /// 插件图标
- ///
- ImageSource Icon { get; }
-
- ///
- /// 插件初始化
- ///
- /// 插件上下文
- void Initialize(IPluginContext context);
-
- ///
- /// 插件启动
- ///
- void Start();
-
- ///
- /// 插件停止
- ///
- void Stop();
-
- ///
- /// 插件清理
- ///
- void Cleanup();
-
- ///
- /// 获取插件设置界面
- ///
- /// 设置界面控件
- UserControl GetSettingsView();
-
- ///
- /// 获取插件菜单项
- ///
- /// 菜单项列表
- IEnumerable
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)