Merge branch 'net6' into net462
This commit is contained in:
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,进入分级策略判断");
|
||||
}
|
||||
|
||||
// 计算最近活跃度(最后一次使用距今的天数)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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消息
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
// 无论屏幕是否变化,都检查热键状态
|
||||
// 这样可以确保热键状态始终与当前上下文保持一致
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,7 +460,6 @@ namespace Ink_Canvas.Helpers
|
||||
_memoryStreams = new MemoryStream[_maxSlides + 2];
|
||||
}
|
||||
CurrentStrokes?.Clear();
|
||||
LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+194
-758
File diff suppressed because it is too large
Load Diff
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 启用 SeAssignPrimaryTokenPrivilege(Inkeys 行为)
|
||||
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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user