Merge branch 'net6' into net462

This commit is contained in:
doudou0720
2026-05-01 23:15:07 +08:00
228 changed files with 23683 additions and 26236 deletions
+27 -15
View File
@@ -1,3 +1,4 @@
using Ink_Canvas.Controls;
using System;
using System.Windows;
using System.Windows.Media;
@@ -7,6 +8,16 @@ namespace Ink_Canvas.Helpers
{
internal class AnimationsHelper
{
private static UIElement ResolveAnimationTarget(UIElement element)
{
if (element is BoardMenuFrame frame)
{
frame.ApplyTemplate();
return frame.AnimationTarget ?? element;
}
return element;
}
public static void ShowWithFadeIn(UIElement element, double duration = 0.15)
{
if (element.Visibility == Visibility.Visible) return;
@@ -36,14 +47,17 @@ namespace Ink_Canvas.Helpers
{
try
{
if (element.Visibility == Visibility.Visible) return;
if (element == null)
throw new ArgumentNullException(nameof(element));
if (element.Visibility == Visibility.Visible) return;
element.Visibility = Visibility.Visible;
var target = ResolveAnimationTarget(element);
var sb = new Storyboard();
// 渐变动画
var fadeInAnimation = new DoubleAnimation
{
From = 0.5,
@@ -54,10 +68,9 @@ namespace Ink_Canvas.Helpers
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(UIElement.OpacityProperty));
// 滑动动画
var slideAnimation = new DoubleAnimation
{
From = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
From = 10,
To = 0,
Duration = TimeSpan.FromSeconds(duration)
};
@@ -68,10 +81,9 @@ namespace Ink_Canvas.Helpers
sb.Children.Add(fadeInAnimation);
sb.Children.Add(slideAnimation);
element.Visibility = Visibility.Visible;
element.RenderTransform = new TranslateTransform();
target.RenderTransform = new TranslateTransform();
sb.Begin((FrameworkElement)element);
sb.Begin((FrameworkElement)target);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
@@ -207,14 +219,15 @@ namespace Ink_Canvas.Helpers
{
try
{
if (element.Visibility == Visibility.Collapsed) return;
if (element == null)
throw new ArgumentNullException(nameof(element));
if (element.Visibility == Visibility.Collapsed) return;
var target = ResolveAnimationTarget(element);
var sb = new Storyboard();
// 渐变动画
var fadeOutAnimation = new DoubleAnimation
{
From = 1,
@@ -224,11 +237,10 @@ namespace Ink_Canvas.Helpers
fadeOutAnimation.EasingFunction = new CubicEase();
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(UIElement.OpacityProperty));
// 滑动动画
var slideAnimation = new DoubleAnimation
{
From = 0,
To = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
To = 10,
Duration = TimeSpan.FromSeconds(duration)
};
slideAnimation.EasingFunction = new CubicEase();
@@ -243,8 +255,8 @@ namespace Ink_Canvas.Helpers
element.Visibility = Visibility.Collapsed;
};
element.RenderTransform = new TranslateTransform();
sb.Begin((FrameworkElement)element);
target.RenderTransform = new TranslateTransform();
sb.Begin((FrameworkElement)target);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
+113
View File
@@ -0,0 +1,113 @@
using Ink_Canvas.Windows.SettingsViews.Helpers;
using System;
using System.Diagnostics;
using System.Security.Principal;
using System.Windows;
namespace Ink_Canvas.Helpers
{
public static class AppRestartHelper
{
public static bool IsRunningAsAdmin()
{
try
{
var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
catch
{
return false;
}
}
public static void RestartApp(bool asAdmin)
{
try
{
App.IsAppExitByUser = true;
(Application.Current as App)?.ReleaseMutexForRestart();
string exePath = Process.GetCurrentProcess().MainModule.FileName;
if (asAdmin)
{
var psi = new ProcessStartInfo(exePath) { UseShellExecute = true, Verb = "runas" };
Process.Start(psi);
}
else
{
// 当前已是管理员时,直接通过用户令牌降权启动,避免经由 explorer 中转的延迟
if (IsRunningAsAdmin() && UIAccessHelper.RestartAsNormalUser())
{
Application.Current.Shutdown();
return;
}
Process.Start("explorer.exe", "\"" + exePath + "\"");
}
Application.Current.Shutdown();
}
catch (Exception ex)
{
Debug.WriteLine($"重启应用时出错: {ex.Message}");
}
}
public static void RestartWithCurrentPrivileges()
{
RestartApp(IsRunningAsAdmin());
}
public static void RestartAsAdmin()
{
RestartApp(true);
}
public static void RestartAsNormal()
{
RestartApp(false);
}
public static void SwitchToUIATopMostAndRestart()
{
try
{
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = true;
if (!SettingsManager.Settings.Advanced.IsAlwaysOnTop)
{
SettingsManager.Settings.Advanced.IsAlwaysOnTop = true;
}
SettingsManager.SaveSettingsToFile();
App.IsUIAccessTopMostEnabled = true;
RestartApp(true);
}
catch (Exception ex)
{
Debug.WriteLine($"切换到UIA置顶模式时出错: {ex.Message}");
}
}
public static void SwitchToNormalTopMostAndRestart()
{
try
{
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = false;
SettingsManager.SaveSettingsToFile();
App.IsUIAccessTopMostEnabled = false;
RestartApp(IsRunningAsAdmin());
}
catch (Exception ex)
{
Debug.WriteLine($"切换到普通置顶模式时出错: {ex.Message}");
}
}
}
}
+155 -10
View File
@@ -27,6 +27,37 @@ namespace Ink_Canvas.Helpers
private static readonly string updatesFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "AutoUpdate");
private static string statusFilePath;
// 全局下载取消令牌;UI 通过 RequestCancelDownload 取消当前下载
private static CancellationTokenSource _activeDownloadCts;
private static readonly object _activeDownloadLock = new object();
public static void RequestCancelDownload()
{
lock (_activeDownloadLock)
{
try { _activeDownloadCts?.Cancel(); } catch { }
}
}
private static CancellationTokenSource BeginDownloadSession()
{
lock (_activeDownloadLock)
{
try { _activeDownloadCts?.Cancel(); } catch { }
_activeDownloadCts = new CancellationTokenSource();
return _activeDownloadCts;
}
}
private static void EndDownloadSession(CancellationTokenSource cts)
{
lock (_activeDownloadLock)
{
if (ReferenceEquals(_activeDownloadCts, cts)) _activeDownloadCts = null;
}
try { cts?.Dispose(); } catch { }
}
public static bool IsX64UpdatePackageSelected()
{
try
@@ -383,6 +414,8 @@ namespace Ink_Canvas.Helpers
// 获取所有可用线路组,按延迟排序
public static async Task<List<UpdateLineGroup>> GetAvailableLineGroupsOrdered(UpdateChannel channel)
{
var cached = TryGetCachedOrderedGroups(channel);
if (cached != null) return cached;
var groups = ChannelLineGroups[channel];
var availableGroups = new List<(UpdateLineGroup group, long delay)>();
@@ -468,9 +501,46 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error);
}
CacheOrderedGroups(channel, orderedGroups);
return orderedGroups;
}
// 缓存按延迟排序后的线路组,避免短时间内重复测速
private static readonly Dictionary<UpdateChannel, (List<UpdateLineGroup> groups, DateTime cachedAt)> _orderedGroupsCache
= new Dictionary<UpdateChannel, (List<UpdateLineGroup>, DateTime)>();
private static readonly TimeSpan _orderedGroupsCacheTtl = TimeSpan.FromMinutes(15);
private static List<UpdateLineGroup> TryGetCachedOrderedGroups(UpdateChannel channel)
{
lock (_orderedGroupsCache)
{
if (_orderedGroupsCache.TryGetValue(channel, out var entry) &&
entry.groups != null && entry.groups.Count > 0 &&
DateTime.UtcNow - entry.cachedAt < _orderedGroupsCacheTtl)
{
LogHelper.WriteLogToFile($"AutoUpdate | 复用线路组延迟检测缓存({entry.groups.Count} 个)");
return new List<UpdateLineGroup>(entry.groups);
}
return null;
}
}
private static void CacheOrderedGroups(UpdateChannel channel, List<UpdateLineGroup> groups)
{
lock (_orderedGroupsCache)
{
_orderedGroupsCache[channel] = (new List<UpdateLineGroup>(groups), DateTime.UtcNow);
}
}
public static void InvalidateOrderedGroupsCache()
{
lock (_orderedGroupsCache)
{
_orderedGroupsCache.Clear();
}
}
private static async Task<long> GetDownloadUrlDelay(string url)
{
try
@@ -945,6 +1015,7 @@ namespace Ink_Canvas.Helpers
// 使用多线路组下载新版(支持自动切换)
public static async Task<bool> DownloadSetupFileWithFallback(string version, List<UpdateLineGroup> groups, Action<double, string> progressCallback = null)
{
var session = BeginDownloadSession();
try
{
version = NormalizeVersionForUpdate(version);
@@ -979,8 +1050,19 @@ namespace Ink_Canvas.Helpers
}
// 依次尝试每个线路组
CancellationToken groupLoopToken;
lock (_activeDownloadLock)
{
groupLoopToken = _activeDownloadCts?.Token ?? CancellationToken.None;
}
foreach (var group in groups)
{
if (groupLoopToken.IsCancellationRequested)
{
LogHelper.WriteLogToFile("AutoUpdate | 用户已取消,停止尝试后续线路组");
break;
}
string url = string.Format(group.DownloadUrlFormat, version);
url = AppendX64SuffixBeforeZipExtension(url);
// 智教联盟需要先获取真实下载地址
@@ -1006,6 +1088,12 @@ namespace Ink_Canvas.Helpers
bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback);
if (groupLoopToken.IsCancellationRequested)
{
LogHelper.WriteLogToFile("AutoUpdate | 用户已取消,停止尝试后续线路组");
break;
}
if (downloadSuccess)
{
SaveDownloadStatus(true);
@@ -1021,6 +1109,13 @@ namespace Ink_Canvas.Helpers
progressCallback?.Invoke(0, "所有线路组下载均失败");
return false;
}
catch (OperationCanceledException)
{
LogHelper.WriteLogToFile("AutoUpdate | 下载已被用户取消", LogHelper.LogType.Warning);
SaveDownloadStatus(false);
progressCallback?.Invoke(0, "下载已取消");
return false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载更新时出错: {ex.Message}", LogHelper.LogType.Error);
@@ -1033,6 +1128,10 @@ namespace Ink_Canvas.Helpers
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
return false;
}
finally
{
EndDownloadSession(session);
}
}
// 下载文件的具体实现
@@ -1043,6 +1142,12 @@ namespace Ink_Canvas.Helpers
// 降低并发数,减少网络压力
int[] threadOptions = { 32, 16, 8, 4, 1 };
CancellationToken externalToken;
lock (_activeDownloadLock)
{
externalToken = _activeDownloadCts?.Token ?? CancellationToken.None;
}
// 检查服务器是否支持Range分块下载
bool supportRange = false;
long totalSize = -1;
@@ -1146,7 +1251,7 @@ namespace Ink_Canvas.Helpers
// 增加连接超时设置
client.Timeout = TimeSpan.FromSeconds(30);
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, externalToken);
var lastReadTime = DateTime.UtcNow;
bool dataReceived = false;
@@ -1206,8 +1311,20 @@ namespace Ink_Canvas.Helpers
success = true;
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载成功");
}
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException)
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException || ex is OperationCanceledException)
{
// 用户主动取消:不再重试
if (externalToken.IsCancellationRequested)
{
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载已被用户取消", LogHelper.LogType.Warning);
if (File.Exists(tempPath))
{
try { File.Delete(tempPath); } catch { }
}
cts.Cancel();
return;
}
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}");
@@ -1218,7 +1335,8 @@ namespace Ink_Canvas.Helpers
}
// 增加重试间隔,避免频繁重试
await Task.Delay(2000 * (retry + 1));
try { await Task.Delay(2000 * (retry + 1), externalToken); }
catch (OperationCanceledException) { cts.Cancel(); return; }
}
}
if (success)
@@ -1339,12 +1457,18 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 开始单线程下载: {fileUrl}");
progressCallback?.Invoke(0, "开始单线程下载");
CancellationToken token;
lock (_activeDownloadLock)
{
token = _activeDownloadCts?.Token ?? CancellationToken.None;
}
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
client.Timeout = TimeSpan.FromMinutes(10); // 单线程下载设置更长的超时时间
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead))
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token))
{
resp.EnsureSuccessStatusCode();
using (var fs = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
@@ -1355,9 +1479,9 @@ namespace Ink_Canvas.Helpers
long downloaded = 0;
var lastProgressUpdate = DateTime.UtcNow;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
{
await fs.WriteAsync(buffer, 0, read);
await fs.WriteAsync(buffer, 0, read, token);
downloaded += read;
// 限制进度更新频率,避免UI卡顿
@@ -1379,6 +1503,13 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile("AutoUpdate | 单线程下载完成");
return true;
}
catch (OperationCanceledException)
{
LogHelper.WriteLogToFile("AutoUpdate | 单线程下载已被取消", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, "下载已取消");
try { if (File.Exists(destinationPath)) File.Delete(destinationPath); } catch { }
return false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 单线程下载失败: {ex.Message}", LogHelper.LogType.Error);
@@ -2201,9 +2332,9 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile($"AutoUpdate | 开始修复版本,通道: {channel}");
// 获取远程版本号(自动选择最快线路组,始终下载远程版本,版本修复模式)
var (remoteVersion, group, _) = await CheckForUpdates(channel, true, true);
if (string.IsNullOrEmpty(remoteVersion) || group == null)
// 获取远程版本号(始终下载远程版本,版本修复模式)
var (remoteVersion, preferredGroup, _) = await CheckForUpdates(channel, true, true);
if (string.IsNullOrEmpty(remoteVersion))
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时获取远程版本失败", LogHelper.LogType.Error);
return false;
@@ -2211,8 +2342,22 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 修复版本远程版本: {remoteVersion}");
var availableGroups = await GetAvailableLineGroupsOrdered(channel);
if (availableGroups.Count == 0)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时无可用线路组", LogHelper.LogType.Error);
return false;
}
if (preferredGroup != null)
{
availableGroups.RemoveAll(g => g.GroupName == preferredGroup.GroupName);
availableGroups.Insert(0, preferredGroup);
LogHelper.WriteLogToFile($"AutoUpdate | 修复版本下载优先使用线路组: {preferredGroup.GroupName}");
}
// 无论版本是否为最新,都下载远程版本
bool downloadResult = await DownloadSetupFile(remoteVersion, group);
bool downloadResult = await DownloadSetupFileWithFallback(remoteVersion, availableGroups);
if (!downloadResult)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时下载更新失败", LogHelper.LogType.Error);
+6 -6
View File
@@ -519,7 +519,7 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 异步上传文件
/// </summary>
public async Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
public Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
{
try
{
@@ -528,19 +528,19 @@ namespace Ink_Canvas.Helpers
// 检查是否启用
if (!IsUploadEnabled())
{
return false;
return Task.FromResult(false);
}
// 基本验证
if (!File.Exists(filePath))
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
return false;
return Task.FromResult(false);
}
if (!IsValidFile(filePath))
{
return false;
return Task.FromResult(false);
}
// 确保队列已初始化
@@ -552,7 +552,7 @@ namespace Ink_Canvas.Helpers
// 加入队列
EnqueueFile(filePath, 0, cancellationToken);
return true;
return Task.FromResult(true);
}
catch (OperationCanceledException)
{
@@ -562,7 +562,7 @@ namespace Ink_Canvas.Helpers
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
return Task.FromResult(false);
}
}
+24
View File
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace Ink_Canvas.Converter
{
@@ -152,4 +153,27 @@ namespace Ink_Canvas.Converter
return null;
}
}
public class StringToGeometryConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
if (value is string geometryString && !string.IsNullOrEmpty(geometryString))
{
return Geometry.Parse(geometryString);
}
}
catch (Exception)
{
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
+96
View File
@@ -0,0 +1,96 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace Ink_Canvas.Helpers
{
public static class DebugConsoleManager
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32.dll")]
private static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
[DllImport("kernel32.dll")]
private static extern bool SetConsoleTitle(string lpConsoleTitle);
private const int SW_HIDE = 0;
private const int SW_SHOW = 5;
private const uint SC_CLOSE = 0xF060;
private const uint MF_BYCOMMAND = 0x00000000;
private static bool _allocated;
public static bool IsVisible { get; private set; }
public static void Show()
{
try
{
if (!_allocated)
{
if (GetConsoleWindow() == IntPtr.Zero)
{
if (!AllocConsole()) return;
}
_allocated = true;
Console.OutputEncoding = Encoding.UTF8;
SetConsoleTitle("InkCanvasForClass - Debug Console");
// 移除关闭菜单,避免用户点 X 时直接结束进程
var hWnd = GetConsoleWindow();
if (hWnd != IntPtr.Zero)
{
var hMenu = GetSystemMenu(hWnd, false);
if (hMenu != IntPtr.Zero) DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
}
}
else
{
var hWnd = GetConsoleWindow();
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_SHOW);
}
IsVisible = true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Show failed: {ex.Message}");
}
}
public static void Hide()
{
try
{
var hWnd = GetConsoleWindow();
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_HIDE);
IsVisible = false;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Hide failed: {ex.Message}");
}
}
public static void WriteLine(string line)
{
if (!IsVisible) return;
try { Console.WriteLine(line); }
catch { }
}
}
}
+156 -113
View File
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
@@ -22,6 +23,9 @@ namespace Ink_Canvas.Helpers
private static readonly string DeviceId;
private static readonly object fileLock = new object();
private static UsageStats usageStatsCache;
private static DateTime usageStatsCacheTime;
private static readonly TimeSpan UsageStatsCacheDuration = TimeSpan.FromMinutes(2);
static DeviceIdentifier()
{
@@ -116,114 +120,26 @@ namespace Ink_Canvas.Helpers
/// </summary>
private static string GenerateHardwareFingerprint()
{
// 收集硬件信息
var hardwareInfo = new StringBuilder();
AppendFingerprintPart(hardwareInfo, "CPU",
GetWmiProperty("SELECT ProcessorId FROM Win32_Processor", "ProcessorId"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0", "ProcessorNameString"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0", "Identifier"));
try
{
var assembly = Assembly.Load("System.Management");
if (assembly != null)
{
// CPU信息
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT ProcessorId FROM Win32_Processor");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
AppendFingerprintPart(hardwareInfo, "BOARD",
GetWmiProperty("SELECT SerialNumber FROM Win32_BaseBoard", "SerialNumber"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BaseBoardSerialNumber"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BaseBoardProduct"));
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
AppendFingerprintPart(hardwareInfo, "BIOS",
GetWmiProperty("SELECT SerialNumber FROM Win32_BIOS", "SerialNumber"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BIOSVersion"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BIOSVendor"));
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var processorId = indexer.GetValue(obj, new object[] { "ProcessorId" });
hardwareInfo.Append(processorId?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
// 主板序列号
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BaseBoard");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
hardwareInfo.Append(serialNumber?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
// BIOS序列号
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BIOS");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
hardwareInfo.Append(serialNumber?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
// 主硬盘序列号
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
hardwareInfo.Append(serialNumber?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
}
catch
{
}
AppendFingerprintPart(hardwareInfo, "DISK",
GetWmiProperty("SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'", "SerialNumber"),
GetSystemDriveVolumeSerial(),
GetRegistryValue(@"SOFTWARE\Microsoft\Cryptography", "MachineGuid"));
if (hardwareInfo.Length < 10)
{
@@ -235,6 +151,108 @@ namespace Ink_Canvas.Helpers
return hardwareInfo.ToString();
}
private static void AppendFingerprintPart(StringBuilder hardwareInfo, string key, params string[] candidates)
{
foreach (var candidate in candidates)
{
if (!string.IsNullOrWhiteSpace(candidate))
{
hardwareInfo.Append(key).Append(':').Append(candidate.Trim()).Append(';');
return;
}
}
}
private static string GetWmiProperty(string query, string propertyName)
{
try
{
var assembly = Assembly.Load("System.Management");
var searcherType = assembly?.GetType("System.Management.ManagementObjectSearcher");
if (searcherType == null)
{
return null;
}
var searcher = Activator.CreateInstance(searcherType, query);
var getMethod = searcherType.GetMethod("Get");
var resultCollection = getMethod?.Invoke(searcher, null);
if (resultCollection == null)
{
return null;
}
var enumerator = resultCollection.GetType().GetMethod("GetEnumerator")?.Invoke(resultCollection, null);
var moveNextMethod = enumerator?.GetType().GetMethod("MoveNext");
var currentProperty = enumerator?.GetType().GetProperty("Current");
if (enumerator == null || moveNextMethod == null || currentProperty == null)
{
return null;
}
if (!(bool)moveNextMethod.Invoke(enumerator, null))
{
return null;
}
var currentObject = currentProperty.GetValue(enumerator);
var indexer = currentObject?.GetType().GetProperty("Item", new[] { typeof(string) });
var result = indexer?.GetValue(currentObject, new object[] { propertyName })?.ToString();
searcher?.GetType().GetMethod("Dispose")?.Invoke(searcher, null);
return result;
}
catch
{
return null;
}
}
private static string GetRegistryValue(string subKey, string valueName)
{
try
{
return Microsoft.Win32.Registry.GetValue($@"HKEY_LOCAL_MACHINE\{subKey}", valueName, null)?.ToString();
}
catch
{
return null;
}
}
private static string GetSystemDriveVolumeSerial()
{
try
{
var rootPath = Path.GetPathRoot(Environment.SystemDirectory);
if (string.IsNullOrWhiteSpace(rootPath))
{
return null;
}
if (GetVolumeInformation(rootPath, null, 0, out uint serialNumber, out _, out _, null, 0))
{
return serialNumber.ToString("X8");
}
}
catch
{
}
return null;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool GetVolumeInformation(
string rootPathName,
StringBuilder volumeNameBuffer,
uint volumeNameSize,
out uint volumeSerialNumber,
out uint maximumComponentLength,
out uint fileSystemFlags,
StringBuilder fileSystemNameBuffer,
uint nFileSystemNameSize);
/// <summary>
/// 基于硬件指纹生成25字符的设备ID
/// </summary>
@@ -654,7 +672,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var stats = LoadUsageStats();
var stats = GetUsageStatsCached();
return stats.SystemVersion;
}
catch (Exception ex)
@@ -773,7 +791,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var stats = LoadUsageStats();
var stats = GetUsageStatsCached();
return stats.UpdatePriority;
}
catch (Exception ex)
@@ -790,7 +808,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var stats = LoadUsageStats();
var stats = GetUsageStatsCached();
return stats.UsageFrequency;
}
catch (Exception ex)
@@ -892,6 +910,23 @@ namespace Ink_Canvas.Helpers
}
}
private static UsageStats GetUsageStatsCached(bool forceRefresh = false)
{
lock (fileLock)
{
if (!forceRefresh
&& usageStatsCache != null
&& (DateTime.Now - usageStatsCacheTime) < UsageStatsCacheDuration)
{
return usageStatsCache;
}
usageStatsCache = LoadUsageStats();
usageStatsCacheTime = DateTime.Now;
return usageStatsCache;
}
}
/// <summary>
/// 保存使用统计
/// </summary>
@@ -902,6 +937,9 @@ namespace Ink_Canvas.Helpers
// 保存到备份文件
SaveUsageStatsToFile(UsageStatsBackupPath, stats);
usageStatsCache = stats;
usageStatsCacheTime = DateTime.Now;
}
@@ -1242,15 +1280,20 @@ namespace Ink_Canvas.Helpers
int versionDiff = CalculateVersionGenerationDifference(localVersion, updateVersion);
LogHelper.WriteLogToFile($"DeviceIdentifier | 无法获取版本发布时间,使用版本号差异判断 - 本地版本: {localVersion}, 远程版本: {updateVersion}, 代数差异: {versionDiff}");
if (versionDiff >= 1)
if (versionDiff <= 0)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=1,允许更新");
}
else
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})<1,可能是相同版本或降级,暂不更新");
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})<=0,可能是相同版本或降级,暂不更新");
return false;
}
// 当代数差异较大(>=3)时直接放行,避免被分级策略卡住
if (versionDiff >= 3)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=3,跳过分级策略直接推送");
return true;
}
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=1,进入分级策略判断");
}
// 计算最近活跃度(最后一次使用距今的天数)
+6 -1
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
@@ -54,6 +54,11 @@ namespace Ink_Canvas.Helpers
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromRect(ref RECT lprc, uint dwFlags);
public static IntPtr GetForegroundWindowHandle()
{
return GetForegroundWindow();
}
public static string WindowTitle()
{
IntPtr foregroundWindowHandle = GetForegroundWindow();
-3
View File
@@ -1,5 +1,4 @@
using System;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
@@ -189,9 +188,7 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 确保窗口全屏的Hook
/// 使用HandleProcessCorruptedStateExceptions,防止访问内存过程中因为一些致命异常导致程序崩溃
/// </summary>
[HandleProcessCorruptedStateExceptions]
private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
//处理WM_WINDOWPOSCHANGING消息
+48 -6
View File
@@ -567,6 +567,36 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 刷新多屏相关设置(开关和跟随鼠标策略)。
/// </summary>
public void RefreshMultiScreenSettings()
{
try
{
var advanced = MainWindow.Settings.Advanced;
_isMultiScreenMode = advanced.EnableMultiScreenSupport && ScreenDetectionHelper.HasMultipleScreens();
_enableScreenSpecificHotkeys = _isMultiScreenMode;
if (_isMultiScreenMode)
{
_currentScreen = advanced.FollowMouseForScreenSelection
? Screen.FromPoint(Control.MousePosition)
: ScreenDetectionHelper.GetWindowScreen(_mainWindow);
}
else
{
_currentScreen = ScreenDetectionHelper.GetPrimaryScreen();
}
RefreshHotkeysForCurrentScreen();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"刷新多屏设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 获取当前屏幕信息
/// </summary>
@@ -624,13 +654,15 @@ namespace Ink_Canvas.Helpers
{
try
{
// 检测是否有多个屏幕
_isMultiScreenMode = ScreenDetectionHelper.HasMultipleScreens();
var advanced = MainWindow.Settings.Advanced;
_isMultiScreenMode = advanced.EnableMultiScreenSupport && ScreenDetectionHelper.HasMultipleScreens();
_enableScreenSpecificHotkeys = _isMultiScreenMode;
if (_isMultiScreenMode)
{
// 获取当前窗口所在的屏幕
_currentScreen = ScreenDetectionHelper.GetWindowScreen(_mainWindow);
_currentScreen = advanced.FollowMouseForScreenSelection
? Screen.FromPoint(Control.MousePosition)
: ScreenDetectionHelper.GetWindowScreen(_mainWindow);
// 监听窗口位置变化事件
_mainWindow.LocationChanged += OnWindowLocationChanged;
@@ -688,6 +720,9 @@ namespace Ink_Canvas.Helpers
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
return;
if (MainWindow.Settings.Advanced.FollowMouseForScreenSelection)
return;
var newScreen = ScreenDetectionHelper.GetWindowScreen(_mainWindow);
if (newScreen != null && newScreen != _currentScreen)
{
@@ -800,9 +835,16 @@ namespace Ink_Canvas.Helpers
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
return;
// 检查鼠标是否在当前窗口所在的屏幕上
var mousePosition = Control.MousePosition;
var currentScreen = Screen.FromPoint(mousePosition);
var mouseScreen = Screen.FromPoint(mousePosition);
if (MainWindow.Settings.Advanced.FollowMouseForScreenSelection &&
mouseScreen != null &&
mouseScreen != _currentScreen)
{
_currentScreen = mouseScreen;
RefreshHotkeysForCurrentScreen();
}
// 无论屏幕是否变化,都检查热键状态
// 这样可以确保热键状态始终与当前上下文保持一致
+46 -57
View File
@@ -8,9 +8,9 @@ namespace Ink_Canvas.Helpers
{
private static InkRecognitionManager _instance;
private static readonly object _lock = new object();
private readonly object _initSync = new object();
private ModernInkProcessor _modernProcessor;
private ModernInkAnalyzer _modernAnalyzer;
private bool _isModernSystemAvailable;
private bool _isInitialized;
@@ -31,35 +31,16 @@ namespace Ink_Canvas.Helpers
}
}
private InkRecognitionManager()
{
Initialize();
}
private InkRecognitionManager() { }
private void Initialize()
{
if (_isInitialized) return;
try
{
var tryModern = WinRtInkShapeRecognizer.IsApiAvailable && Environment.Is64BitProcess;
_isModernSystemAvailable = false;
if (tryModern)
{
try
{
_modernProcessor = new ModernInkProcessor();
_modernAnalyzer = new ModernInkAnalyzer();
_isModernSystemAvailable = true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("WinRT 墨迹初始化失败: " + ex.Message, LogHelper.LogType.Warning);
_isModernSystemAvailable = false;
_modernProcessor = null;
_modernAnalyzer = null;
}
}
// 启动阶段只做能力探测,不做 WinRT 组件实例化(避免冷启动延迟)
_isModernSystemAvailable = WinRtInkShapeRecognizer.IsApiAvailable;
_isInitialized = true;
}
catch (Exception ex)
@@ -69,10 +50,41 @@ namespace Ink_Canvas.Helpers
}
}
private void EnsureInitialized()
{
if (_isInitialized) return;
lock (_initSync)
{
if (_isInitialized) return;
Initialize();
}
}
private void EnsureModernAnalyzerInitialized()
{
if (_modernProcessor != null || !_isModernSystemAvailable) return;
lock (_initSync)
{
if (_modernProcessor != null || !_isModernSystemAvailable) return;
try
{
_modernProcessor ??= new ModernInkProcessor();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("WinRT 墨迹模块懒加载失败: " + ex.Message, LogHelper.LogType.Warning);
_isModernSystemAvailable = false;
_modernProcessor = null;
}
}
}
public Task<InkShapeRecognitionResult> RecognizeShapeAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
EnsureInitialized();
if (!_isInitialized || strokes == null || strokes.Count == 0)
return Task.FromResult(InkShapeRecognitionResult.Empty);
@@ -108,6 +120,7 @@ namespace Ink_Canvas.Helpers
bool applyHandwritingBeautify = false,
string handwritingFontFamilyList = null)
{
EnsureInitialized();
if (!_isInitialized)
{
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info);
@@ -140,18 +153,11 @@ namespace Ink_Canvas.Helpers
return Task.FromResult(strokes);
}
if (!Environment.Is64BitProcess)
EnsureModernAnalyzerInitialized();
if (_modernProcessor == null)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:非 64 位进程,WinRT 手写体替换不可用。笔画数=" + strokes.Count,
LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
if (_modernAnalyzer == null)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:ModernInkAnalyzer 未就绪(WinRT 初始化失败?)。笔画数=" +
"[手写体] CorrectInkAsync 跳过:ModernInkProcessor 未就绪(WinRT 初始化失败?)。笔画数=" +
strokes.Count,
LogHelper.LogType.Warning);
return Task.FromResult(strokes);
@@ -161,7 +167,7 @@ namespace Ink_Canvas.Helpers
"[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count +
",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()),
LogHelper.LogType.Info);
return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes, handwritingFontFamilyList);
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(strokes, handwritingFontFamilyList);
}
catch (Exception ex)
{
@@ -171,19 +177,19 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// WinRT 手写体识别(需 64 位进程、Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。
/// WinRT 手写体识别(需 Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。
/// </summary>
public Task<HandwritingRecognitionResult> RecognizeHandwritingAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
EnsureInitialized();
if (!_isInitialized || strokes == null || strokes.Count == 0)
return Task.FromResult(HandwritingRecognitionResult.Empty);
try
{
if (!Environment.Is64BitProcess
|| !ShapeRecognitionRouter.ResolveUseWinRt(mode)
if (!ShapeRecognitionRouter.ResolveUseWinRt(mode)
|| !WinRtHandwritingRecognizer.IsApiAvailable)
return Task.FromResult(HandwritingRecognitionResult.Empty);
@@ -209,14 +215,13 @@ namespace Ink_Canvas.Helpers
public string GetSystemInfo()
{
return _isModernSystemAvailable
? $"现代化64位墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}"
? $"现代化墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}"
: $"传统墨迹识别系统 (IACore) - 进程架构: {Environment.Is64BitProcess}";
}
public void Dispose()
{
_modernProcessor?.Dispose();
_modernAnalyzer?.Dispose();
_isInitialized = false;
}
}
@@ -238,20 +243,4 @@ namespace Ink_Canvas.Helpers
{
}
}
internal sealed class ModernInkAnalyzer : IDisposable
{
public Task<StrokeCollection> AnalyzeAndCorrectAsync(
StrokeCollection strokes,
string handwritingFontFamilyList)
{
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(
strokes,
handwritingFontFamilyList);
}
public void Dispose()
{
}
}
}
+7 -2
View File
@@ -103,7 +103,6 @@ namespace Ink_Canvas.Helpers
{
try
{
_ = InkRecognitionManager.Instance;
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
{
WinRtInkShapeRecognizer.Warmup();
@@ -118,7 +117,7 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>WinRT 手写识别(64 位 + Windows 10+)。</summary>
/// <summary>WinRT 手写识别(Windows 10+)。</summary>
public static Task<HandwritingRecognitionResult> RecognizeHandwritingUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode) =>
@@ -152,6 +151,9 @@ namespace Ink_Canvas.Helpers
var node = legacy.InkDrawingNode;
var shape = node.GetShape();
if (shape == null)
return InkShapeRecognitionResult.Empty;
var hot = ClonePointCollection(node.HotPoints);
return new InkShapeRecognitionResult(
node.GetShapeName(),
@@ -173,6 +175,9 @@ namespace Ink_Canvas.Helpers
public static bool IsContainShapeType(string name)
{
if (string.IsNullOrEmpty(name))
return false;
if (name.Contains("Triangle") || name.Contains("Circle") ||
name.Contains("Rectangle") || name.Contains("Diamond") ||
name.Contains("Parallelogram") || name.Contains("Square")
+3 -4
View File
@@ -1,5 +1,4 @@
using OSVersionExtension;
using System;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Media;
@@ -17,13 +16,13 @@ namespace Ink_Canvas.Helpers
public static class ShapeRecognitionRouter
{
/// <summary>
/// 自动模式:按当前进程位数选择——<c>64</c> 位进程用 WinRT<c>32</c> 位进程(含 x86 目标在 WOW64 下运行)用 IACore。
/// 自动模式:在 Windows 10 及以上系统默认使用 WinRT,否则使用 IACore。
/// </summary>
public static bool ResolveUseWinRt(ShapeRecognitionEngineMode mode)
{
if (mode == ShapeRecognitionEngineMode.WinRT) return true;
if (mode == ShapeRecognitionEngineMode.IACore) return false;
return Environment.Is64BitProcess;
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
}
public static bool ShouldRunShapeRecognition(bool inkToShapeEnabled, ShapeRecognitionEngineMode mode)
@@ -31,7 +30,7 @@ namespace Ink_Canvas.Helpers
if (!inkToShapeEnabled) return false;
if (ResolveUseWinRt(mode))
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
return !Environment.Is64BitProcess;
return true;
}
public static ShapeRecognitionEngineMode FromSettingsInt(int value)
+1
View File
@@ -83,6 +83,7 @@ namespace Ink_Canvas.Helpers
}
}
string logLine = string.Format("{0} [T{1}] [{2}] [{3}] {4}", DateTime.Now.ToString("O"), threadId, strLogType, callerInfo, str);
DebugConsoleManager.WriteLine(logLine);
ProcessProtectionManager.WithWriteAccess(file, () =>
{
using (StreamWriter sw = new StreamWriter(file, true))
+26
View File
@@ -0,0 +1,26 @@
using System;
using System.Runtime.InteropServices;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// .NET Core / 5+ 未提供 <see cref="Marshal.GetActiveObject"/>,通过 OLE 实现等效行为。
/// </summary>
internal static class OleActiveObject
{
[DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern int CLSIDFromProgID(string lpszProgId, out Guid lpclsid);
[DllImport("oleaut32.dll", PreserveSig = true)]
private static extern int GetActiveObject(ref Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
public static object GetActiveObject(string progId)
{
int hr = CLSIDFromProgID(progId, out Guid clsid);
Marshal.ThrowExceptionForHR(hr);
hr = GetActiveObject(ref clsid, IntPtr.Zero, out object obj);
Marshal.ThrowExceptionForHR(hr);
return obj;
}
}
}
-1
View File
@@ -460,7 +460,6 @@ namespace Ink_Canvas.Helpers
_memoryStreams = new MemoryStream[_maxSlides + 2];
}
CurrentStrokes?.Clear();
LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace);
}
/// <summary>
+11 -4
View File
@@ -271,7 +271,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("PowerPoint.Application");
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("PowerPoint.Application");
if (pptApp != null && Marshal.IsComObject(pptApp))
{
@@ -298,7 +298,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("kwpp.Application");
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("kwpp.Application");
if (wpsApp != null && Marshal.IsComObject(wpsApp))
{
@@ -410,6 +410,15 @@ namespace Ink_Canvas.Helpers
// COM对象类型转换失败,通常是因为对象已经被释放
LogHelper.WriteLogToFile("PPT COM对象已被释放,跳过事件注册取消", LogHelper.LogType.Trace);
}
catch (System.Reflection.TargetInvocationException tie) when (tie.InnerException is InvalidComObjectException)
{
// RCW 已分离:Office Interop 内部通过反射创建 EventProvider 时抛出,是正常情况
LogHelper.WriteLogToFile("PPT COM对象RCW已分离,跳过事件注册取消", LogHelper.LogType.Trace);
}
catch (InvalidComObjectException)
{
LogHelper.WriteLogToFile("PPT COM对象RCW已分离,跳过事件注册取消", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"取消PPT事件注册时发生异常: {ex}", LogHelper.LogType.Warning);
@@ -1255,7 +1264,6 @@ namespace Ink_Canvas.Helpers
object slideNavigation = null;
try
{
LogHelper.WriteLogToFile($"尝试显示幻灯片导航 - 连接状态: {IsConnected}, 放映状态: {IsInSlideShow}", LogHelper.LogType.Trace);
if (!IsConnected || !IsInSlideShow || PPTApplication == null)
{
@@ -1288,7 +1296,6 @@ namespace Ink_Canvas.Helpers
{
dynamic sn = slideNavigation;
sn.Visible = true;
LogHelper.WriteLogToFile("成功显示幻灯片导航(PowerPoint模式)", LogHelper.LogType.Event);
return true;
}
+2 -2
View File
@@ -104,7 +104,7 @@ namespace Ink_Canvas.Helpers
try
{
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("PowerPoint.Application");
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("PowerPoint.Application");
if (pptApp != null && Marshal.IsComObject(pptApp))
{
try
@@ -124,7 +124,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)Marshal.GetActiveObject("kwpp.Application");
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("kwpp.Application");
if (wpsApp != null && Marshal.IsComObject(wpsApp))
{
try
+50 -129
View File
@@ -1,8 +1,6 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers
@@ -86,18 +84,8 @@ namespace Ink_Canvas.Helpers
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Collapsed;
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Visible;
// 只有在页数有效时才更新页码显示
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
}
else
{
// 页数无效时清空页码显示
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
}
// 同步页码到所有翻页条 + 兼容旧绑定的隐藏 placeholder
SetPageNumberOnAllBars(currentSlide, totalSlides);
UpdateNavigationPanelsVisibility();
UpdateNavigationButtonStyles();
@@ -112,6 +100,11 @@ namespace Ink_Canvas.Helpers
MainWindow.MoveWindow(new WindowInteropHelper(_mainWindow).Handle, 0, 0,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height, true);
// MoveWindow 触发的 WM_WINDOWPOSCHANGING + 重绘会打断面板的 ShowWithFadeIn 动画,
// 在窗口尺寸最终确定后重新评估一次翻页面板的可见性。
UpdateNavigationPanelsVisibility();
UpdateNavigationButtonStyles();
}), DispatcherPriority.ApplicationIdle);
_mainWindow.isFullScreenApplied = true; // 标记已应用全屏处理
@@ -158,18 +151,7 @@ namespace Ink_Canvas.Helpers
{
try
{
// 只有在页数有效时才更新页码显示
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
}
else
{
// 页数无效时清空页码显示
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
}
SetPageNumberOnAllBars(currentSlide, totalSlides);
}
catch (Exception ex)
{
@@ -178,6 +160,34 @@ namespace Ink_Canvas.Helpers
});
}
private void SetPageNumberOnAllBars(int currentSlide, int totalSlides)
{
var bars = new[]
{
_mainWindow.LeftBottomPanelForPPTNavigation,
_mainWindow.RightBottomPanelForPPTNavigation,
_mainWindow.LeftSidePanelForPPTNavigation,
_mainWindow.RightSidePanelForPPTNavigation,
};
foreach (var bar in bars)
{
if (bar == null) continue;
bar.CurrentSlide = currentSlide;
bar.TotalSlides = totalSlides;
}
// 兼容旧绑定(其它界面通过 ElementName 引用 PPTBtnPageNow / PPTBtnPageTotal
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
}
else
{
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
}
}
/// <summary>
/// 处理PPT放映状态变化
/// </summary>
@@ -386,16 +396,17 @@ namespace Ink_Canvas.Helpers
// 页码按钮显示
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
_mainWindow.PPTLSPageButton.Visibility = pageButtonVisibility;
_mainWindow.PPTRSPageButton.Visibility = pageButtonVisibility;
_mainWindow.LeftSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
_mainWindow.RightSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
// 透明度设置 - 直接使用用户设置的透明度值
_mainWindow.PPTBtnLSBorder.Opacity = PPTLSButtonOpacity;
_mainWindow.PPTBtnRSBorder.Opacity = PPTRSButtonOpacity;
// 透明度
_mainWindow.LeftSidePanelForPPTNavigation.SetBarOpacity(PPTLSButtonOpacity);
_mainWindow.RightSidePanelForPPTNavigation.SetBarOpacity(PPTRSButtonOpacity);
// 颜色主题
bool isDarkTheme = options[2] == '2';
ApplyButtonTheme(_mainWindow.PPTBtnLSBorder, _mainWindow.PPTBtnRSBorder, isDarkTheme, true);
_mainWindow.LeftSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
_mainWindow.RightSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
}
catch (Exception ex)
{
@@ -414,113 +425,23 @@ namespace Ink_Canvas.Helpers
// 页码按钮显示
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
_mainWindow.PPTLBPageButton.Visibility = pageButtonVisibility;
_mainWindow.PPTRBPageButton.Visibility = pageButtonVisibility;
_mainWindow.LeftBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
_mainWindow.RightBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
// 透明度设置 - 直接使用用户设置的透明度值
_mainWindow.PPTBtnLBBorder.Opacity = PPTLBButtonOpacity;
_mainWindow.PPTBtnRBBorder.Opacity = PPTRBButtonOpacity;
// 透明度
_mainWindow.LeftBottomPanelForPPTNavigation.SetBarOpacity(PPTLBButtonOpacity);
_mainWindow.RightBottomPanelForPPTNavigation.SetBarOpacity(PPTRBButtonOpacity);
// 颜色主题
bool isDarkTheme = options[2] == '2';
ApplyButtonTheme(_mainWindow.PPTBtnLBBorder, _mainWindow.PPTBtnRBBorder, isDarkTheme, false);
_mainWindow.LeftBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
_mainWindow.RightBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新底部按钮样式失败: {ex}", LogHelper.LogType.Error);
}
}
private void ApplyButtonTheme(Border leftBorder, Border rightBorder, bool isDarkTheme, bool isSideButton)
{
try
{
Color backgroundColor, borderColor, foregroundColor, feedbackColor;
if (isDarkTheme)
{
backgroundColor = Color.FromRgb(39, 39, 42);
borderColor = Color.FromRgb(82, 82, 91);
foregroundColor = Colors.White;
feedbackColor = Colors.White;
}
else
{
backgroundColor = Color.FromRgb(244, 244, 245);
borderColor = Color.FromRgb(161, 161, 170);
foregroundColor = Color.FromRgb(39, 39, 42);
feedbackColor = Color.FromRgb(24, 24, 27);
}
// 应用背景和边框颜色
var backgroundBrush = new SolidColorBrush(backgroundColor);
var borderBrush = new SolidColorBrush(borderColor);
leftBorder.Background = backgroundBrush;
leftBorder.BorderBrush = borderBrush;
rightBorder.Background = backgroundBrush;
rightBorder.BorderBrush = borderBrush;
// 应用图标和文字颜色
var foregroundBrush = new SolidColorBrush(foregroundColor);
var feedbackBrush = new SolidColorBrush(feedbackColor);
if (isSideButton)
{
ApplySideButtonColors(foregroundBrush, feedbackBrush);
}
else
{
ApplyBottomButtonColors(foregroundBrush, feedbackBrush);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用按钮主题失败: {ex}", LogHelper.LogType.Error);
}
}
private void ApplySideButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
{
// 图标颜色
_mainWindow.PPTLSPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRSPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTLSNextButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRSNextButtonGeometry.Brush = foregroundBrush;
// 反馈背景颜色
_mainWindow.PPTLSPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRSPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLSPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRSPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLSNextButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRSNextButtonFeedbackBorder.Background = feedbackBrush;
// 文字颜色
TextBlock.SetForeground(_mainWindow.PPTLSPageButton, foregroundBrush);
TextBlock.SetForeground(_mainWindow.PPTRSPageButton, foregroundBrush);
}
private void ApplyBottomButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
{
// 图标颜色
_mainWindow.PPTLBPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRBPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTLBNextButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRBNextButtonGeometry.Brush = foregroundBrush;
// 反馈背景颜色
_mainWindow.PPTLBPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRBPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLBPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRBPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLBNextButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRBNextButtonFeedbackBorder.Background = feedbackBrush;
// 文字颜色
TextBlock.SetForeground(_mainWindow.PPTLBPageButton, foregroundBrush);
TextBlock.SetForeground(_mainWindow.PPTRBPageButton, foregroundBrush);
}
#endregion
}
}
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 渲染保存文件名模板。支持占位符: {date} {time} {datetime} {mode} {page} {count} {type}。
/// 当模板为空、渲染结果非法或仅含分隔符时,回退到默认时间戳命名。
/// </summary>
public static class SaveFileNameHelper
{
private const string DefaultDateTime = "yyyy-MM-dd HH-mm-ss-fff";
public static string Render(string template, SaveFileNameContext ctx)
{
if (ctx == null) ctx = new SaveFileNameContext();
var now = ctx.Time ?? DateTime.Now;
if (string.IsNullOrWhiteSpace(template))
return now.ToString(DefaultDateTime);
try
{
string result = template
.Replace("{date}", now.ToString("yyyy-MM-dd"))
.Replace("{time}", now.ToString("HH-mm-ss"))
.Replace("{datetime}", now.ToString(DefaultDateTime))
.Replace("{mode}", ctx.Mode ?? "")
.Replace("{page}", ctx.Page?.ToString() ?? "")
.Replace("{count}", ctx.Count?.ToString() ?? "")
.Replace("{type}", ctx.Type ?? "");
result = SanitizeFileName(result);
if (string.IsNullOrWhiteSpace(result) || Regex.IsMatch(result, @"^[\s\-_]+$"))
return now.ToString(DefaultDateTime);
return result;
}
catch
{
return now.ToString(DefaultDateTime);
}
}
private static string SanitizeFileName(string name)
{
if (string.IsNullOrEmpty(name)) return name;
foreach (var c in Path.GetInvalidFileNameChars())
{
name = name.Replace(c, '_');
}
return name.Trim();
}
}
public class SaveFileNameContext
{
public DateTime? Time { get; set; }
/// <summary>"Annotation" or "BlackBoard" or "Screenshot" etc.</summary>
public string Mode { get; set; }
/// <summary>"User" or "Auto"</summary>
public string Type { get; set; }
public int? Page { get; set; }
public int? Count { get; set; }
}
}
+92
View File
@@ -0,0 +1,92 @@
using iNKORE.UI.WPF.Modern;
using Microsoft.Win32;
using System;
using System.Windows;
namespace Ink_Canvas.Helpers
{
public static class ThemeHelper
{
public static bool IsSystemThemeLight()
{
try
{
var registryKey = Registry.CurrentUser;
var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (themeKey != null)
{
var value = themeKey.GetValue("AppsUseLightTheme");
if (value != null)
{
bool result = (int)value == 1;
themeKey.Close();
return result;
}
themeKey.Close();
}
}
catch
{
}
return true;
}
public static bool IsSystemThemeLightLegacy()
{
try
{
var registryKey = Registry.CurrentUser;
var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (themeKey != null)
{
int keyValue = (int)themeKey.GetValue("SystemUsesLightTheme");
themeKey.Close();
return keyValue == 1;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
return false;
}
public static ElementTheme GetEffectiveTheme(Settings settings)
{
if (settings.Appearance.Theme == 0)
return ElementTheme.Light;
if (settings.Appearance.Theme == 1)
return ElementTheme.Dark;
return IsSystemThemeLight() ? ElementTheme.Light : ElementTheme.Dark;
}
public static void ApplyTheme(FrameworkElement element, Settings settings)
{
if (element == null || settings == null) return;
try
{
ThemeManager.SetRequestedTheme(element, GetEffectiveTheme(settings));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用主题失败: {ex.Message}", LogHelper.LogType.Error);
}
}
public static void ApplyTheme(FrameworkElement element, Settings settings, Action<string> onThemeApplied)
{
if (element == null || settings == null) return;
try
{
var theme = GetEffectiveTheme(settings);
ThemeManager.SetRequestedTheme(element, theme);
onThemeApplied?.Invoke(theme == ElementTheme.Dark ? "Dark" : "Light");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用主题失败: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
-165
View File
@@ -1,165 +0,0 @@
using System;
using System.IO;
using System.Reflection;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// UIAccess DLL释放器
/// </summary>
public static class UIAccessDllExtractor
{
private static readonly string[] RequiredDlls = {
"UIAccessDLL_x64.dll",
"UIAccessDLL_x86.dll"
};
/// <summary>
/// 在应用启动时释放UIAccess相关DLL
/// </summary>
public static void ExtractUIAccessDlls()
{
try
{
string appDirectory = AppDomain.CurrentDomain.BaseDirectory;
LogHelper.WriteLogToFile("开始检查并释放UIAccess相关DLL文件");
foreach (string dllName in RequiredDlls)
{
string targetPath = Path.Combine(appDirectory, dllName);
// 检查文件是否已存在且有效
if (File.Exists(targetPath) && IsValidDll(targetPath))
{
LogHelper.WriteLogToFile($"{dllName} 已存在且有效,跳过释放");
continue;
}
// 从嵌入资源中释放DLL
if (ExtractDllFromResource(dllName, targetPath))
{
LogHelper.WriteLogToFile($"成功释放 {dllName} 到 {targetPath}");
}
else
{
LogHelper.WriteLogToFile($"警告:无法释放 {dllName},可能影响UIA置顶功能", LogHelper.LogType.Warning);
}
}
LogHelper.WriteLogToFile("UIAccess DLL释放检查完成");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 从嵌入资源中提取DLL文件
/// </summary>
private static bool ExtractDllFromResource(string dllName, string targetPath)
{
try
{
Assembly assembly = Assembly.GetExecutingAssembly();
string resourceName = $"Ink_Canvas.{dllName}";
using (Stream resourceStream = assembly.GetManifestResourceStream(resourceName))
{
if (resourceStream == null)
{
LogHelper.WriteLogToFile($"未找到嵌入资源: {resourceName}", LogHelper.LogType.Warning);
return false;
}
// 确保目标目录存在
string targetDirectory = Path.GetDirectoryName(targetPath);
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}
// 写入文件
using (FileStream fileStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write))
{
resourceStream.CopyTo(fileStream);
}
return true;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从资源提取 {dllName} 失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 检查DLL文件是否有效
/// </summary>
private static bool IsValidDll(string filePath)
{
try
{
if (!File.Exists(filePath))
return false;
FileInfo fileInfo = new FileInfo(filePath);
// 检查文件大小(空文件或过小的文件可能无效)
if (fileInfo.Length < 1024) // 小于1KB可能无效
return false;
// 简单检查PE头(DLL文件应该以MZ开头)
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[2];
if (fs.Read(buffer, 0, 2) == 2)
{
return buffer[0] == 0x4D && buffer[1] == 0x5A; // "MZ"
}
}
return false;
}
catch
{
return false;
}
}
/// <summary>
/// 清理释放的DLL文件(可选,在应用退出时调用)
/// </summary>
public static void CleanupExtractedDlls()
{
try
{
string appDirectory = AppDomain.CurrentDomain.BaseDirectory;
foreach (string dllName in RequiredDlls)
{
string filePath = Path.Combine(appDirectory, dllName);
if (File.Exists(filePath))
{
try
{
File.Delete(filePath);
LogHelper.WriteLogToFile($"已清理 {dllName}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理 {dllName} 失败: {ex.Message}", LogHelper.LogType.Warning);
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理UIAccess DLL时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
+672
View File
@@ -0,0 +1,672 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 通过 Winlogon 令牌模拟实现 UIAccess 提权重启。
/// 1. 找到当前会话中 winlogon.exe 的令牌,复制为模拟令牌;
/// 2. SetThreadToken 暂时模拟 winlogon(拥有 TCB 权限);
/// 3. 在自身令牌副本上 SetTokenInformation(TokenUIAccess, TRUE)
/// 4. RevertToSelf 后用 CreateProcessWithTokenW 启动新进程;
/// 5. 新进程具有 UIAccess 权限,可置顶于 UAC 提示之上。
/// </summary>
public static class UIAccessHelper
{
#region Constants
private const uint TOKEN_QUERY = 0x0008;
private const uint TOKEN_DUPLICATE = 0x0002;
private const uint TOKEN_IMPERSONATE = 0x0004;
private const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
private const uint TOKEN_ADJUST_DEFAULT = 0x0080;
private const uint TOKEN_ADJUST_SESSIONID = 0x0100;
private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
private const int SecurityAnonymous = 0;
private const int SecurityImpersonation = 2;
private const int TokenPrimary = 1;
private const int TokenImpersonation = 2;
// TOKEN_INFORMATION_CLASS
private const int TokenSessionId = 12;
private const int TokenElevationType = 18;
private const int TokenUIAccess = 26;
// TOKEN_ELEVATION_TYPE
private const int TokenElevationTypeDefault = 1;
private const int TokenElevationTypeFull = 2;
private const int TokenElevationTypeLimited = 3;
private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
private const uint TH32CS_SNAPPROCESS = 0x00000002;
private const uint LOGON_WITH_PROFILE = 0x00000001;
private const uint CREATE_NEW_CONSOLE = 0x00000010;
private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
private const uint SE_PRIVILEGE_ENABLED = 0x00000002;
private const string SE_ASSIGNPRIMARYTOKEN_NAME = "SeAssignPrimaryTokenPrivilege";
private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
#endregion
#region Structs
[StructLayout(LayoutKind.Sequential)]
private struct LUID
{
public uint LowPart;
public int HighPart;
}
[StructLayout(LayoutKind.Sequential)]
private struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public uint Attributes;
}
[StructLayout(LayoutKind.Sequential)]
private struct TOKEN_PRIVILEGES
{
public uint PrivilegeCount;
public LUID_AND_ATTRIBUTES Privilege;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct PROCESSENTRY32W
{
public uint dwSize;
public uint cntUsage;
public uint th32ProcessID;
public IntPtr th32DefaultHeapID;
public uint th32ModuleID;
public uint cntThreads;
public uint th32ParentProcessID;
public int pcPriClassBase;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szExeFile;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFOW
{
public uint cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public uint dwX, dwY, dwXSize, dwYSize;
public uint dwXCountChars, dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public ushort wShowWindow;
public ushort cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput, hStdOutput, hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
#endregion
#region P/Invoke
[DllImport("kernel32.dll")]
private static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DuplicateTokenEx(
IntPtr hExistingToken,
uint dwDesiredAccess,
IntPtr lpTokenAttributes,
int ImpersonationLevel,
int TokenType,
out IntPtr phNewToken);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetTokenInformation(
IntPtr TokenHandle,
int TokenInformationClass,
IntPtr TokenInformation,
uint TokenInformationLength,
out uint ReturnLength);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetTokenInformation(
IntPtr TokenHandle,
int TokenInformationClass,
IntPtr TokenInformation,
uint TokenInformationLength);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetThreadToken(IntPtr Thread, IntPtr Token);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool RevertToSelf();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Process32FirstW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Process32NextW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool LookupPrivilegeValueW(string lpSystemName, string lpName, out LUID lpLuid);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AdjustTokenPrivileges(
IntPtr TokenHandle,
[MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
uint BufferLength,
IntPtr PreviousState,
IntPtr ReturnLength);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateProcessWithTokenW(
IntPtr hToken,
uint dwLogonFlags,
string lpApplicationName,
StringBuilder lpCommandLine,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFOW lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern void GetStartupInfoW(ref STARTUPINFOW lpStartupInfo);
#endregion
#region Public API
/// <summary>
/// 检查当前进程是否已具有 UIAccess 标志。
/// </summary>
public static bool HasUIAccess()
{
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hToken))
return false;
try
{
IntPtr buf = Marshal.AllocHGlobal(sizeof(uint));
try
{
Marshal.WriteInt32(buf, 0);
if (!GetTokenInformation(hToken, TokenUIAccess, buf, sizeof(uint), out _))
return false;
return Marshal.ReadInt32(buf) != 0;
}
finally
{
Marshal.FreeHGlobal(buf);
}
}
finally
{
CloseHandle(hToken);
}
}
/// <summary>
/// 以 UIAccess 令牌重启自身。当前进程必须已经以管理员身份运行。
/// 成功时新进程已启动,调用方应立即退出当前进程。
/// </summary>
/// <param name="extraArgs">追加到新进程的额外命令行参数(例如 --skip-mutex-check)。</param>
public static bool RestartWithUIAccess(string extraArgs = null)
{
try
{
if (HasUIAccess())
{
LogHelper.WriteLogToFile("UIAccess | 当前进程已具有 UIAccess,跳过重启");
return true;
}
if (!CreateUIAccessToken(out IntPtr uiaToken))
{
LogHelper.WriteLogToFile($"UIAccess | 创建 UIAccess 令牌失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
return false;
}
try
{
return LaunchWithToken(uiaToken, extraArgs);
}
finally
{
CloseHandle(uiaToken);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"UIAccess | RestartWithUIAccess 异常: {ex}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 以普通用户权限(非提升)重启自身。
/// 通过获取 explorer.exe / ctfmon.exe 的非特权令牌,再用 CreateProcessWithTokenW 启动新进程,
/// 避免经由 explorer.exe 中转可能产生的 UAC 提示或丢失参数问题。
/// 成功时调用方应立即退出当前进程。
/// </summary>
/// <param name="extraArgs">追加到新进程的额外命令行参数。</param>
public static bool RestartAsNormalUser(string extraArgs = null)
{
try
{
if (!GetUserPrimaryToken(out IntPtr userToken))
{
LogHelper.WriteLogToFile($"UIAccess | 获取用户令牌失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
return false;
}
try
{
return LaunchWithToken(userToken, extraArgs);
}
finally
{
CloseHandle(userToken);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"UIAccess | RestartAsNormalUser 异常: {ex}", LogHelper.LogType.Error);
return false;
}
}
#endregion
#region Token Manipulation
private static bool CreateUIAccessToken(out IntPtr uiaToken)
{
uiaToken = IntPtr.Zero;
// 1. 获取当前进程的 session id
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hSelfQuery))
{
LogHelper.WriteLogToFile($"UIAccess | OpenProcessToken(query) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
uint sessionId;
try
{
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
if (!GetTokenInformation(hSelfQuery, TokenSessionId, sesBuf, sizeof(uint), out _))
{
LogHelper.WriteLogToFile($"UIAccess | GetTokenInformation(SessionId) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
sessionId = (uint)Marshal.ReadInt32(sesBuf);
}
finally { Marshal.FreeHGlobal(sesBuf); }
}
finally { CloseHandle(hSelfQuery); }
// 2. 找到同一会话的 winlogon 模拟令牌
if (!GetWinlogonImpersonationToken(sessionId, out IntPtr winlogonToken))
{
LogHelper.WriteLogToFile("UIAccess | 未能获取 winlogon 模拟令牌(需要管理员权限)", LogHelper.LogType.Error);
return false;
}
try
{
// 3. 模拟 winlogon
if (!SetThreadToken(IntPtr.Zero, winlogonToken))
{
LogHelper.WriteLogToFile($"UIAccess | SetThreadToken(winlogon) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
try
{
// 4. 复制自身令牌为主令牌
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, out IntPtr hSelfDup))
{
LogHelper.WriteLogToFile($"UIAccess | OpenProcessToken(dup) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
IntPtr dupToken;
try
{
bool ok = DuplicateTokenEx(
hSelfDup,
TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
IntPtr.Zero,
SecurityAnonymous,
TokenPrimary,
out dupToken);
if (!ok)
{
LogHelper.WriteLogToFile($"UIAccess | DuplicateTokenEx 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
}
finally { CloseHandle(hSelfDup); }
// 5. 在副本上设置 UIAccess = TRUE
IntPtr uiBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
Marshal.WriteInt32(uiBuf, 1);
if (!SetTokenInformation(dupToken, TokenUIAccess, uiBuf, sizeof(uint)))
{
int err = Marshal.GetLastWin32Error();
LogHelper.WriteLogToFile($"UIAccess | SetTokenInformation(UIAccess) 失败: {err}", LogHelper.LogType.Error);
CloseHandle(dupToken);
return false;
}
}
finally { Marshal.FreeHGlobal(uiBuf); }
uiaToken = dupToken;
return true;
}
finally
{
RevertToSelf();
}
}
finally
{
CloseHandle(winlogonToken);
}
}
private static bool GetWinlogonImpersonationToken(uint sessionId, out IntPtr winlogonToken)
{
winlogonToken = IntPtr.Zero;
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE || snapshot == IntPtr.Zero)
{
LogHelper.WriteLogToFile($"UIAccess | CreateToolhelp32Snapshot 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
try
{
var pe = new PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32W)) };
bool more = Process32FirstW(snapshot, ref pe);
while (more)
{
if (string.Equals(pe.szExeFile, "winlogon.exe", StringComparison.OrdinalIgnoreCase))
{
if (TryDuplicateWinlogonToken(pe.th32ProcessID, sessionId, out winlogonToken))
return true;
}
more = Process32NextW(snapshot, ref pe);
}
}
finally { CloseHandle(snapshot); }
return false;
}
private static bool TryDuplicateWinlogonToken(uint pid, uint sessionId, out IntPtr dupToken)
{
dupToken = IntPtr.Zero;
IntPtr hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (hProc == IntPtr.Zero) return false;
try
{
if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE, out IntPtr hToken))
return false;
try
{
// 检查 session id 匹配
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
if (!GetTokenInformation(hToken, TokenSessionId, sesBuf, sizeof(uint), out _))
return false;
if ((uint)Marshal.ReadInt32(sesBuf) != sessionId)
return false;
}
finally { Marshal.FreeHGlobal(sesBuf); }
if (!DuplicateTokenEx(
hToken,
TOKEN_IMPERSONATE | TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | TOKEN_DUPLICATE,
IntPtr.Zero,
SecurityImpersonation,
TokenImpersonation,
out dupToken))
{
return false;
}
// 启用 SeAssignPrimaryTokenPrivilegeInkeys 行为)
var tkp = new TOKEN_PRIVILEGES
{
PrivilegeCount = 1,
Privilege = new LUID_AND_ATTRIBUTES { Attributes = SE_PRIVILEGE_ENABLED }
};
if (LookupPrivilegeValueW(null, SE_ASSIGNPRIMARYTOKEN_NAME, out tkp.Privilege.Luid))
{
AdjustTokenPrivileges(dupToken, false, ref tkp, (uint)Marshal.SizeOf(tkp), IntPtr.Zero, IntPtr.Zero);
}
return true;
}
finally { CloseHandle(hToken); }
}
finally { CloseHandle(hProc); }
}
/// <summary>
/// 从 explorer.exe / ctfmon.exe 取得普通用户(非提升)令牌的主令牌副本,用于降权启动。
/// 仅当当前进程为管理员时才能成功。
/// </summary>
private static bool GetUserPrimaryToken(out IntPtr userToken)
{
userToken = IntPtr.Zero;
string[] candidates = { "explorer.exe", "ctfmon.exe" };
foreach (var name in candidates)
{
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE || snapshot == IntPtr.Zero) continue;
try
{
var pe = new PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32W)) };
bool more = Process32FirstW(snapshot, ref pe);
while (more)
{
if (string.Equals(pe.szExeFile, name, StringComparison.OrdinalIgnoreCase))
{
if (TryDuplicateUserPrimaryToken(pe.th32ProcessID, out userToken))
{
LogHelper.WriteLogToFile($"UIAccess | 已从 {name} (PID={pe.th32ProcessID}) 取得用户令牌");
return true;
}
}
more = Process32NextW(snapshot, ref pe);
}
}
finally { CloseHandle(snapshot); }
}
return false;
}
private static bool TryDuplicateUserPrimaryToken(uint pid, out IntPtr dupToken)
{
dupToken = IntPtr.Zero;
IntPtr hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (hProc == IntPtr.Zero) return false;
try
{
if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE, out IntPtr hToken))
return false;
try
{
// 仅接受非提升令牌(否则降权失败)
IntPtr elevBuf = Marshal.AllocHGlobal(sizeof(int));
try
{
if (!GetTokenInformation(hToken, TokenElevationType, elevBuf, sizeof(int), out _))
return false;
int elev = Marshal.ReadInt32(elevBuf);
if (elev == TokenElevationTypeFull)
return false; // 该进程是提升令牌,跳过
}
finally { Marshal.FreeHGlobal(elevBuf); }
return DuplicateTokenEx(
hToken,
TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
IntPtr.Zero,
SecurityAnonymous,
TokenPrimary,
out dupToken);
}
finally { CloseHandle(hToken); }
}
finally { CloseHandle(hProc); }
}
#endregion
#region Process Launch
private static bool LaunchWithToken(IntPtr token, string extraArgs)
{
string exePath = Process.GetCurrentProcess().MainModule.FileName;
string workDir = System.IO.Path.GetDirectoryName(exePath);
// 重建命令行:保留原始参数,追加 --skip-mutex-check 防止单实例阻塞
var cmdBuilder = new StringBuilder(32768);
cmdBuilder.Append('"').Append(exePath).Append('"');
string[] args = Environment.GetCommandLineArgs();
for (int i = 1; i < args.Length; i++)
{
cmdBuilder.Append(' ');
AppendQuoted(cmdBuilder, args[i]);
}
if (!string.IsNullOrEmpty(extraArgs))
cmdBuilder.Append(' ').Append(extraArgs);
// 防止单实例 Mutex 阻塞新进程
if (Array.IndexOf(args, "--skip-mutex-check") < 0
&& (extraArgs == null || extraArgs.IndexOf("--skip-mutex-check", StringComparison.Ordinal) < 0))
{
cmdBuilder.Append(" --skip-mutex-check");
}
var si = new STARTUPINFOW { cb = (uint)Marshal.SizeOf(typeof(STARTUPINFOW)) };
GetStartupInfoW(ref si);
bool ok = CreateProcessWithTokenW(
token,
LOGON_WITH_PROFILE,
null,
cmdBuilder,
CREATE_UNICODE_ENVIRONMENT,
IntPtr.Zero,
workDir,
ref si,
out PROCESS_INFORMATION pi);
if (!ok)
{
int err = Marshal.GetLastWin32Error();
LogHelper.WriteLogToFile($"UIAccess | CreateProcessWithTokenW 失败: {err}", LogHelper.LogType.Error);
return false;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
LogHelper.WriteLogToFile($"UIAccess | 已使用 UIAccess 令牌启动新进程 (PID={pi.dwProcessId})");
return true;
}
private static void AppendQuoted(StringBuilder sb, string arg)
{
if (arg == null) { sb.Append("\"\""); return; }
bool needQuote = arg.Length == 0 || arg.IndexOfAny(new[] { ' ', '\t', '"' }) >= 0;
if (!needQuote) { sb.Append(arg); return; }
sb.Append('"');
int backslashes = 0;
foreach (char c in arg)
{
if (c == '\\') { backslashes++; continue; }
if (c == '"')
{
sb.Append('\\', backslashes * 2 + 1);
sb.Append('"');
}
else
{
sb.Append('\\', backslashes);
sb.Append(c);
}
backslashes = 0;
}
sb.Append('\\', backslashes * 2);
sb.Append('"');
}
#endregion
}
}
@@ -29,31 +29,13 @@ namespace Ink_Canvas.Helpers
public static bool IsApiAvailable =>
OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
/// <summary>
/// 启动阶段不再预热线程内 WinRT 手写管线。历史上曾用 <see cref="WinRtInkShapeRecognizer.CreateMinimalWarmupStrokeCollection"/> 跑全链路,
/// 会显著拖慢启动;与更早的「空 <see cref="StrokeCollection"/>」一样,此处不再在 Idle 上做任何工作。
/// 首次真正需要手写识别时由 <see cref="RecognizeHandwritingAsync"/> 承担冷启动成本。
/// </summary>
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(
WinRtInkShapeRecognizer.CreateMinimalWarmupStrokeCollection(),
verboseTrace: false).ConfigureAwait(true);
}
catch
{
// ignore
}
}));
}
catch
{
// ignore
}
}
/// <summary>
+132 -6
View File
@@ -1,6 +1,7 @@
using OSVersionExtension;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
@@ -11,6 +12,128 @@ using WinRtInkAnalyzer = global::Windows.UI.Input.Inking.Analysis.InkAnalyzer;
namespace Ink_Canvas.Helpers
{
internal class ModernInkAnalyzer : IDisposable
{
public static readonly Guid ShapeStrokePropertyGuid = new Guid("11111111-2222-3333-4444-555555555555");
private global::Windows.UI.Input.Inking.Analysis.InkAnalyzer _internalAnalyzer;
private readonly Dictionary<Stroke, uint> _strokeIdMap = new Dictionary<Stroke, uint>();
private readonly Dictionary<uint, Stroke> _reverseIdMap = new Dictionary<uint, Stroke>();
private readonly object _syncLock = new object();
public ModernInkAnalyzer()
{
if (!WinRtInkShapeRecognizer.IsApiAvailable)
return;
_internalAnalyzer = new global::Windows.UI.Input.Inking.Analysis.InkAnalyzer();
}
private void AddStrokeInternal(Stroke stroke)
{
if (stroke.ContainsPropertyData(ShapeStrokePropertyGuid))
return;
var inkStroke = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(stroke);
if (inkStroke == null) return;
_internalAnalyzer.AddDataForStroke(inkStroke);
_internalAnalyzer.SetStrokeDataKind(
inkStroke.Id,
global::Windows.UI.Input.Inking.Analysis.InkAnalysisStrokeKind.Drawing);
_strokeIdMap[stroke] = inkStroke.Id;
_reverseIdMap[inkStroke.Id] = stroke;
}
private CancellationTokenSource _cts;
public async Task<InkShapeRecognitionResult> AnalyzeAsync(StrokeCollection strokes)
{
if (_internalAnalyzer == null || strokes == null || strokes.Count == 0)
return InkShapeRecognitionResult.Empty;
_cts?.Cancel();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
lock (_syncLock)
{
_internalAnalyzer.ClearDataForAllStrokes();
_strokeIdMap.Clear();
_reverseIdMap.Clear();
foreach (var stroke in strokes)
{
AddStrokeInternal(stroke);
}
}
if (_strokeIdMap.Count == 0)
return InkShapeRecognitionResult.Empty;
var result = await _internalAnalyzer.AnalyzeAsync().AsTask(token).ConfigureAwait(true);
if (token.IsCancellationRequested) return InkShapeRecognitionResult.Empty;
// Use the internal method from WinRtInkShapeRecognizer to find the primary drawing
var drawing = WinRtInkShapeRecognizer.FindPrimaryDrawing(_internalAnalyzer);
if (drawing == null)
return InkShapeRecognitionResult.Empty;
if (drawing.DrawingKind == global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind.Drawing)
return InkShapeRecognitionResult.Empty;
var name = WinRtInkShapeRecognizer.MapDrawingKindToShapeName(drawing.DrawingKind);
if (string.IsNullOrEmpty(name) || name == "Drawing")
return InkShapeRecognitionResult.Empty;
var winPts = WinRtInkShapeRecognizer.CopyWinRtPoints(drawing);
var hot = WinRtInkShapeRecognizer.ToWpfPointCollection(winPts);
var c = drawing.Center;
var centroid = new SysPoint(c.X, c.Y);
WinRtInkShapeRecognizer.BoundsFromPoints(winPts, out double w, out double h);
var toRemove = new StrokeCollection();
lock (_syncLock)
{
foreach (var id in drawing.GetStrokeIds())
{
if (_reverseIdMap.TryGetValue(id, out var stroke))
{
toRemove.Add(stroke);
}
}
}
if (toRemove.Count == 0)
return InkShapeRecognitionResult.Empty;
return new InkShapeRecognitionResult(name, centroid, hot, w, h, toRemove);
}
catch (Exception)
{
return InkShapeRecognitionResult.Empty;
}
}
public Task<StrokeCollection> AnalyzeAndCorrectAsync(
StrokeCollection strokes,
string handwritingFontFamilyList)
{
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(
strokes,
handwritingFontFamilyList);
}
public void Dispose()
{
_internalAnalyzer = null;
}
}
/// <summary>基于 Windows.UI.Input.Inking.Analysis 的形状识别(适用于 64 位进程等场景)。</summary>
internal static class WinRtInkShapeRecognizer
{
@@ -124,6 +247,9 @@ namespace Ink_Canvas.Helpers
return null;
var da = stroke.DrawingAttributes;
if (da == null)
return null;
var wda = new global::Windows.UI.Input.Inking.InkDrawingAttributes
{
PenTip = global::Windows.UI.Input.Inking.PenTipShape.Circle,
@@ -147,8 +273,8 @@ namespace Ink_Canvas.Helpers
return builder.CreateStroke(points);
}
private static global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing FindPrimaryDrawing(
WinRtInkAnalyzer analyzer)
internal static global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing FindPrimaryDrawing(
global::Windows.UI.Input.Inking.Analysis.InkAnalyzer analyzer)
{
global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing best = null;
double bestArea = -1;
@@ -187,7 +313,7 @@ namespace Ink_Canvas.Helpers
return w * h;
}
private static global::Windows.Foundation.Point[] CopyWinRtPoints(
internal static global::Windows.Foundation.Point[] CopyWinRtPoints(
global::Windows.UI.Input.Inking.Analysis.InkAnalysisInkDrawing drawing)
{
var src = drawing?.Points;
@@ -204,7 +330,7 @@ namespace Ink_Canvas.Helpers
return arr;
}
private static void BoundsFromPoints(
internal static void BoundsFromPoints(
System.Collections.Generic.IReadOnlyList<global::Windows.Foundation.Point> points,
out double w,
out double h)
@@ -229,7 +355,7 @@ namespace Ink_Canvas.Helpers
h = Math.Max(0, maxY - minY);
}
private static PointCollection ToWpfPointCollection(
internal static PointCollection ToWpfPointCollection(
System.Collections.Generic.IReadOnlyList<global::Windows.Foundation.Point> points)
{
var hot = new PointCollection();
@@ -243,7 +369,7 @@ namespace Ink_Canvas.Helpers
return hot;
}
private static string MapDrawingKindToShapeName(
internal static string MapDrawingKindToShapeName(
global::Windows.UI.Input.Inking.Analysis.InkAnalysisDrawingKind kind)
{
switch (kind)