diff --git a/AutomaticUpdateVersionControl.txt b/AutomaticUpdateVersionControl.txt index cca5748a..2756467a 100644 --- a/AutomaticUpdateVersionControl.txt +++ b/AutomaticUpdateVersionControl.txt @@ -1 +1 @@ -1.7.17.0 +1.7.18.0 diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index 51ea0265..54c1d425 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -31,7 +31,7 @@ namespace Ink_Canvas Mutex mutex; public static string[] StartArgs; - public static string RootPath = Environment.GetEnvironmentVariable("APPDATA") + "\\Ink Canvas\\"; + public static string RootPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; // 新增:标记是否通过--board参数启动 public static bool StartWithBoardMode = false; @@ -46,7 +46,7 @@ namespace Ink_Canvas // 新增:退出信号文件路径 private static string watchdogExitSignalFile = Path.Combine(Path.GetTempPath(), "icc_watchdog_exit_" + Process.GetCurrentProcess().Id + ".flag"); // 新增:崩溃日志文件路径 - private static string crashLogFile = Path.Combine(Environment.GetEnvironmentVariable("APPDATA"), "Ink Canvas", "crash_logs"); + private static string crashLogFile = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "Crashes"); // 新增:进程ID private static int currentProcessId = Process.GetCurrentProcess().Id; // 新增:应用启动时间 @@ -107,7 +107,6 @@ namespace Ink_Canvas if (isWindows7) { - LogHelper.WriteLogToFile("检测到Windows 7系统,配置TLS协议支持"); // 启用所有TLS版本以支持Windows 7 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; @@ -117,17 +116,14 @@ namespace Ink_Canvas ServicePointManager.Expect100Continue = false; ServicePointManager.UseNagleAlgorithm = false; - LogHelper.WriteLogToFile("TLS协议配置完成,已启用TLS 1.2/1.1/1.0支持"); } else { // 对于更新的Windows版本,不进行任何TLS配置,使用系统默认设置 - LogHelper.WriteLogToFile($"检测到Windows版本: {osVersion.VersionString},使用系统默认TLS配置"); } } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"配置TLS协议时出错: {ex.Message}", LogHelper.LogType.Error); } } @@ -346,6 +342,25 @@ namespace Ink_Canvas try { var exception = e.ExceptionObject as Exception; + + if (exception is InvalidOperationException invalidOpEx) + { + string exceptionMessage = invalidOpEx.Message ?? ""; + string exceptionStackTrace = invalidOpEx.StackTrace ?? ""; + + if (exceptionMessage.Contains("调用线程无法访问此对象") || + exceptionMessage.Contains("because another thread owns it") || + exceptionStackTrace.Contains("DynamicRenderer") || + exceptionStackTrace.Contains("CompositionTarget.get_RootVisual")) + { + LogHelper.WriteLogToFile( + $"检测到DynamicRenderer线程访问异常: {invalidOpEx.Message}", + LogHelper.LogType.Warning + ); + return; + } + } + string errorMessage = exception?.ToString() ?? "未知异常"; lastErrorMessage = errorMessage; @@ -361,8 +376,11 @@ namespace Ink_Canvas // 尝试在最后时刻记录错误 try { + string timeStr = (appStartTime != default(DateTime) && appStartTime != DateTime.MinValue) + ? appStartTime.ToString("yyyy-MM-dd-HH-mm-ss") + : DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss"); File.AppendAllText( - Path.Combine(crashLogFile, $"critical_error_{DateTime.Now:yyyyMMdd_HHmmss}.log"), + Path.Combine(crashLogFile, $"Crash_{timeStr}.txt"), $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 记录未处理异常时发生错误: {ex.Message}\r\n" ); } @@ -500,7 +518,10 @@ namespace Ink_Canvas Directory.CreateDirectory(crashLogFile); } - string logFileName = Path.Combine(crashLogFile, $"crash_{DateTime.Now:yyyyMMdd}.log"); + string appStartTimeStr = (appStartTime != default(DateTime) && appStartTime != DateTime.MinValue) + ? appStartTime.ToString("yyyy-MM-dd-HH-mm-ss") + : DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss"); + string logFileName = Path.Combine(crashLogFile, $"Crash_{appStartTimeStr}.txt"); // 收集系统状态信息 string memoryUsage = (Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024)) + " MB"; @@ -549,6 +570,31 @@ namespace Ink_Canvas private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { + // 检查是否是DynamicRenderer线程访问UI对象的已知问题 + if (e.Exception is InvalidOperationException invalidOpEx) + { + string exceptionMessage = invalidOpEx.Message ?? ""; + string exceptionStackTrace = invalidOpEx.StackTrace ?? ""; + + // 检查是否是DynamicRenderer相关的线程访问问题 + if (exceptionMessage.Contains("调用线程无法访问此对象") || + exceptionMessage.Contains("because another thread owns it") || + exceptionStackTrace.Contains("DynamicRenderer") || + exceptionStackTrace.Contains("CompositionTarget.get_RootVisual")) + { + // 这是WPF InkCanvas的已知问题,DynamicRenderer的后台线程尝试访问UI对象 + // 这个异常不会影响应用程序功能,可以安全地忽略 + LogHelper.WriteLogToFile( + $"检测到DynamicRenderer线程访问异常(已安全处理): {invalidOpEx.Message}", + LogHelper.LogType.Warning + ); + + // 标记为已处理,不显示错误消息,不触发重启 + e.Handled = true; + return; + } + } + Ink_Canvas.MainWindow.ShowNewMessage("抱歉,出现未预期的异常,可能导致 InkCanvasForClass 运行不稳定。\n建议保存墨迹后重启应用。"); LogHelper.NewLog(e.Exception.ToString()); diff --git a/Ink Canvas/AssemblyInfo.cs b/Ink Canvas/AssemblyInfo.cs index e30061e1..87f44273 100644 --- a/Ink Canvas/AssemblyInfo.cs +++ b/Ink Canvas/AssemblyInfo.cs @@ -49,5 +49,5 @@ using System.Windows; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.7.17.2")] -[assembly: AssemblyFileVersion("1.7.17.2")] +[assembly: AssemblyVersion("1.7.18.0")] +[assembly: AssemblyFileVersion("1.7.18.0")] diff --git a/Ink Canvas/Windows/QuickDrawFloatingButton.xaml b/Ink Canvas/Controls/QuickDrawFloatingButtonControl.xaml similarity index 86% rename from Ink Canvas/Windows/QuickDrawFloatingButton.xaml rename to Ink Canvas/Controls/QuickDrawFloatingButtonControl.xaml index 07786911..37a17947 100644 --- a/Ink Canvas/Windows/QuickDrawFloatingButton.xaml +++ b/Ink Canvas/Controls/QuickDrawFloatingButtonControl.xaml @@ -1,22 +1,19 @@ - - - + + + - + - + + diff --git a/Ink Canvas/Controls/QuickDrawFloatingButtonControl.xaml.cs b/Ink Canvas/Controls/QuickDrawFloatingButtonControl.xaml.cs new file mode 100644 index 00000000..c09e3f3b --- /dev/null +++ b/Ink Canvas/Controls/QuickDrawFloatingButtonControl.xaml.cs @@ -0,0 +1,149 @@ +using Ink_Canvas.Windows; +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; +using MouseEventArgs = System.Windows.Input.MouseEventArgs; +using HorizontalAlignment = System.Windows.HorizontalAlignment; +using VerticalAlignment = System.Windows.VerticalAlignment; + +namespace Ink_Canvas.Controls +{ + /// + /// 快抽悬浮按钮控件 + /// + public partial class QuickDrawFloatingButtonControl : UserControl + { + private bool _isDragging = false; + private Point _dragStartPoint; + private Point _controlStartPoint; + + public QuickDrawFloatingButtonControl() + { + InitializeComponent(); + } + + /// + /// 快抽按钮点击事件 + /// + private void FloatingButton_Click(object sender, MouseButtonEventArgs e) + { + try + { + // 如果正在拖动,不触发点击事件 + if (_isDragging) return; + + // 打开快抽窗口 + var quickDrawWindow = new QuickDrawWindow(); + quickDrawWindow.ShowDialog(); + } + catch (Exception ex) + { + Helpers.LogHelper.WriteLogToFile($"打开快抽窗口失败: {ex.Message}", Helpers.LogHelper.LogType.Error); + } + } + + /// + /// 拖动区域鼠标按下事件 + /// + private void DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + _isDragging = false; + + // 记录鼠标在屏幕上的初始位置 + _dragStartPoint = this.PointToScreen(e.GetPosition(this)); + + // 记录控件的初始位置 + var parent = this.Parent as FrameworkElement; + if (parent != null) + { + var transform = this.TransformToVisual(parent); + var currentPos = transform.Transform(new Point(0, 0)); + _controlStartPoint = currentPos; + } + else + { + var currentMargin = this.Margin; + _controlStartPoint = new Point( + double.IsNaN(currentMargin.Left) ? 0 : currentMargin.Left, + double.IsNaN(currentMargin.Top) ? 0 : currentMargin.Top); + } + + ((UIElement)sender).CaptureMouse(); + e.Handled = true; + } + + /// + /// 拖动区域鼠标移动事件 + /// + private void DragArea_MouseMove(object sender, MouseEventArgs e) + { + if (e.LeftButton == MouseButtonState.Pressed && ((UIElement)sender).IsMouseCaptured) + { + // 获取鼠标在屏幕上的当前位置 + Point currentScreenPoint = this.PointToScreen(e.GetPosition(this)); + Vector diff = currentScreenPoint - _dragStartPoint; + + if (!_isDragging && (Math.Abs(diff.X) > 3 || Math.Abs(diff.Y) > 3)) + { + _isDragging = true; + // 切换到绝对定位模式 + this.HorizontalAlignment = HorizontalAlignment.Left; + this.VerticalAlignment = VerticalAlignment.Top; + } + + if (_isDragging) + { + // 计算新位置 + var parent = this.Parent as FrameworkElement; + if (parent != null) + { + // 计算屏幕坐标相对于父容器的位置 + var parentPoint = parent.PointFromScreen(currentScreenPoint); + var startParentPoint = parent.PointFromScreen(_dragStartPoint); + + // 计算相对于初始位置的偏移 + double offsetX = parentPoint.X - startParentPoint.X; + double offsetY = parentPoint.Y - startParentPoint.Y; + + // 新位置 = 初始位置 + 偏移 + double newLeft = _controlStartPoint.X + offsetX; + double newTop = _controlStartPoint.Y + offsetY; + + // 限制在父容器范围内 + newLeft = Math.Max(0, Math.Min(newLeft, parent.ActualWidth - this.ActualWidth)); + newTop = Math.Max(0, Math.Min(newTop, parent.ActualHeight - this.ActualHeight)); + + // 更新Margin + this.Margin = new Thickness(newLeft, newTop, 0, 0); + } + } + } + } + + /// + /// 拖动区域鼠标释放事件 + /// + private void DragArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (((UIElement)sender).IsMouseCaptured) + { + ((UIElement)sender).ReleaseMouseCapture(); + } + + if (_isDragging) + { + Dispatcher.BeginInvoke(new Action(() => { _isDragging = false; }), + DispatcherPriority.Background); + } + else + { + _isDragging = false; + } + + e.Handled = true; + } + } +} + diff --git a/Ink Canvas/Helpers/CameraService.cs b/Ink Canvas/Helpers/CameraService.cs index 491c9fef..a8868e29 100644 --- a/Ink Canvas/Helpers/CameraService.cs +++ b/Ink Canvas/Helpers/CameraService.cs @@ -281,8 +281,16 @@ namespace Ink_Canvas.Helpers // 应用旋转 Bitmap rotatedFrame = ApplyRotation(sourceFrame); - // 应用分辨率调整 - _currentFrame = ResizeImage(rotatedFrame, _resolutionWidth, _resolutionHeight); + int targetWidth = _resolutionWidth; + int targetHeight = _resolutionHeight; + + if (_rotationAngle == 1 || _rotationAngle == 3) + { + targetWidth = _resolutionHeight; + targetHeight = _resolutionWidth; + } + + _currentFrame = ResizeImageWithAspectRatio(rotatedFrame, targetWidth, targetHeight); rotatedFrame?.Dispose(); } @@ -357,6 +365,33 @@ namespace Ink_Canvas.Helpers return rotated; } + /// + /// 调整图像大小 + /// + private Bitmap ResizeImageWithAspectRatio(Bitmap source, int targetWidth, int targetHeight) + { + if (source.Width == targetWidth && source.Height == targetHeight) + return new Bitmap(source); + + double scaleX = (double)targetWidth / source.Width; + double scaleY = (double)targetHeight / source.Height; + double scale = Math.Min(scaleX, scaleY); + + // 计算实际尺寸 + int actualWidth = (int)(source.Width * scale); + int actualHeight = (int)(source.Height * scale); + + var resized = new Bitmap(actualWidth, actualHeight, PixelFormat.Format24bppRgb); + using (var graphics = Graphics.FromImage(resized)) + { + graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; + graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality; + graphics.DrawImage(source, 0, 0, actualWidth, actualHeight); + } + return resized; + } + /// /// 调整图像大小 /// diff --git a/Ink Canvas/Helpers/DlassApiClient.cs b/Ink Canvas/Helpers/DlassApiClient.cs index d77bed80..94146ef1 100644 --- a/Ink Canvas/Helpers/DlassApiClient.cs +++ b/Ink Canvas/Helpers/DlassApiClient.cs @@ -331,15 +331,15 @@ namespace Ink_Canvas.Helpers return false; } } - catch (HttpRequestException httpEx) + catch (HttpRequestException) { return false; } - catch (TaskCanceledException timeoutEx) + catch (TaskCanceledException) { return false; } - catch (Exception ex) + catch (Exception) { return false; } diff --git a/Ink Canvas/Helpers/DlassNoteUploader.cs b/Ink Canvas/Helpers/DlassNoteUploader.cs index 4265c2bc..f1374eaa 100644 --- a/Ink Canvas/Helpers/DlassNoteUploader.cs +++ b/Ink Canvas/Helpers/DlassNoteUploader.cs @@ -19,6 +19,22 @@ namespace Ink_Canvas.Helpers private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA"; private const int BATCH_SIZE = 10; // 批量上传大小 private const int MAX_RETRY_COUNT = 3; // 最大重试次数 + private const string QUEUE_FILE_NAME = "DlassUploadQueue.json"; + + /// + /// 上传队列项 + /// + private class UploadQueueItemData + { + [JsonProperty("file_path")] + public string FilePath { get; set; } + + [JsonProperty("retry_count")] + public int RetryCount { get; set; } + + [JsonProperty("added_time")] + public DateTime AddedTime { get; set; } + } /// /// 上传队列项 @@ -39,6 +55,205 @@ namespace Ink_Canvas.Helpers /// private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1); + /// + /// 队列保存锁,防止并发保存 + /// + private static readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1); + + /// + /// 是否已初始化队列 + /// + private static bool _isQueueInitialized = false; + + /// + /// 获取队列文件路径 + /// + private static string GetQueueFilePath() + { + var configsDir = Path.Combine(App.RootPath, "Configs"); + if (!Directory.Exists(configsDir)) + { + Directory.CreateDirectory(configsDir); + } + return Path.Combine(configsDir, QUEUE_FILE_NAME); + } + + /// + /// 初始化上传队列 + /// + public static void InitializeQueue() + { + if (_isQueueInitialized) + { + return; + } + + try + { + var queueFilePath = GetQueueFilePath(); + if (!File.Exists(queueFilePath)) + { + _isQueueInitialized = true; + return; + } + + var jsonContent = File.ReadAllText(queueFilePath); + if (string.IsNullOrWhiteSpace(jsonContent)) + { + _isQueueInitialized = true; + return; + } + + var queueData = JsonConvert.DeserializeObject>(jsonContent); + if (queueData == null || queueData.Count == 0) + { + _isQueueInitialized = true; + return; + } + + int restoredCount = 0; + int skippedCount = 0; + + foreach (var item in queueData) + { + // 验证文件是否存在 + if (!File.Exists(item.FilePath)) + { + skippedCount++; + continue; + } + + // 验证文件格式和大小 + var fileExtension = Path.GetExtension(item.FilePath).ToLower(); + if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip") + { + skippedCount++; + continue; + } + + try + { + var fileInfo = new FileInfo(item.FilePath); + long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024; + if (fileInfo.Length > maxSize) + { + skippedCount++; + continue; + } + } + catch + { + skippedCount++; + continue; + } + + // 恢复队列项 + _uploadQueue.Enqueue(new UploadQueueItem + { + FilePath = item.FilePath, + RetryCount = item.RetryCount + }); + restoredCount++; + } + + _isQueueInitialized = true; + + if (restoredCount > 0) + { + LogHelper.WriteLogToFile($"已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event); + // 如果恢复了队列,触发处理 + _ = ProcessUploadQueueAsync(); + } + else if (skippedCount > 0) + { + LogHelper.WriteLogToFile($"队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error); + _isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试 + } + } + + /// + /// 保存队列到文件 + /// + private static async Task SaveQueueToFileAsync() + { + if (!await _queueSaveLock.WaitAsync(1000)) // 最多等待1秒 + { + return; // 如果无法获取锁,跳过保存(避免阻塞) + } + + try + { + var queueData = new List(); + + // 将队列转换为可序列化的格式 + foreach (var item in _uploadQueue) + { + queueData.Add(new UploadQueueItemData + { + FilePath = item.FilePath, + RetryCount = item.RetryCount, + AddedTime = DateTime.Now + }); + } + + var queueFilePath = GetQueueFilePath(); + + // 如果队列为空,清空文件 + if (queueData.Count == 0) + { + ClearQueueFile(); + return; + } + + var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented); + + // 使用临时文件写入,然后替换,确保原子性 + var tempFilePath = queueFilePath + ".tmp"; + File.WriteAllText(tempFilePath, jsonContent); + + // 如果原文件存在,先删除 + if (File.Exists(queueFilePath)) + { + File.Delete(queueFilePath); + } + + // 重命名临时文件 + File.Move(tempFilePath, queueFilePath); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error); + } + finally + { + _queueSaveLock.Release(); + } + } + + /// + /// 清空队列文件 + /// + private static void ClearQueueFile() + { + try + { + var queueFilePath = GetQueueFilePath(); + if (File.Exists(queueFilePath)) + { + File.WriteAllText(queueFilePath, "[]"); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + /// /// 上传笔记响应模型 /// @@ -100,9 +315,9 @@ namespace Ink_Canvas.Helpers } /// - /// 异步上传笔记文件到Dlass(支持PNG和ICSTK格式) + /// 异步上传笔记文件到Dlass(支持PNG、ICSTK和ZIP格式) /// - /// 文件路径(支持PNG和ICSTK) + /// 文件路径(支持PNG、ICSTK和ZIP) /// 是否成功加入队列(不等待实际上传完成) public static async Task UploadNoteFileAsync(string filePath) { @@ -122,15 +337,16 @@ namespace Ink_Canvas.Helpers } var fileExtension = Path.GetExtension(filePath).ToLower(); - if (fileExtension != ".png" && fileExtension != ".icstk") + if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip") { return false; } var fileInfo = new FileInfo(filePath); - if (fileInfo.Length > 10 * 1024 * 1024) + long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024; + if (fileInfo.Length > maxSize) { - LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过10MB限制", LogHelper.LogType.Error); + LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error); return false; } @@ -171,6 +387,9 @@ namespace Ink_Canvas.Helpers RetryCount = retryCount }); + // 异步保存队列到文件 + _ = Task.Run(async () => await SaveQueueToFileAsync()); + // 如果队列达到批量大小,触发批量上传 if (_uploadQueue.Count >= BATCH_SIZE) { @@ -219,6 +438,11 @@ namespace Ink_Canvas.Helpers if (string.IsNullOrEmpty(selectedClassName)) { LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error); + // 将文件重新加入队列 + foreach (var item in filesToUpload) + { + EnqueueFile(item.FilePath, item.RetryCount); + } return; } @@ -226,6 +450,11 @@ namespace Ink_Canvas.Helpers if (string.IsNullOrEmpty(userToken)) { LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error); + // 将文件重新加入队列 + foreach (var item in filesToUpload) + { + EnqueueFile(item.FilePath, item.RetryCount); + } return; } @@ -246,6 +475,11 @@ namespace Ink_Canvas.Helpers if (authResult == null || !authResult.Success || authResult.Whiteboards == null) { LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error); + // 将文件重新加入队列 + foreach (var item in filesToUpload) + { + EnqueueFile(item.FilePath, item.RetryCount); + } return; } @@ -255,6 +489,11 @@ namespace Ink_Canvas.Helpers if (sharedWhiteboard == null || string.IsNullOrEmpty(sharedWhiteboard.BoardId) || string.IsNullOrEmpty(sharedWhiteboard.SecretKey)) { LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error); + // 将文件重新加入队列 + foreach (var item in filesToUpload) + { + EnqueueFile(item.FilePath, item.RetryCount); + } return; } } @@ -262,6 +501,11 @@ namespace Ink_Canvas.Helpers catch (Exception ex) { LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error); + // 将文件重新加入队列 + foreach (var item in filesToUpload) + { + EnqueueFile(item.FilePath, item.RetryCount); + } return; } @@ -317,6 +561,9 @@ namespace Ink_Canvas.Helpers }); await Task.WhenAll(uploadTasks); + // 上传完成后保存队列状态 + await SaveQueueToFileAsync(); + // 如果队列达到批量大小,继续处理 if (_uploadQueue.Count >= BATCH_SIZE) { @@ -348,16 +595,17 @@ namespace Ink_Canvas.Helpers // 检查文件扩展名 var fileExtension = Path.GetExtension(filePath).ToLower(); - if (fileExtension != ".png" && fileExtension != ".icstk") + if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip") { return false; } - // 检查文件大小(最大10MB) + // 检查文件大小(最大10MB,ZIP文件可能更大,允许50MB) var fileInfo = new FileInfo(filePath); - if (fileInfo.Length > 10 * 1024 * 1024) + long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024; + if (fileInfo.Length > maxSize) { - LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过10MB限制", LogHelper.LogType.Error); + LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error); return false; } @@ -417,9 +665,24 @@ namespace Ink_Canvas.Helpers // 准备上传参数 var fileName = Path.GetFileNameWithoutExtension(filePath); var title = fileName; - var fileType = fileExtension == ".icstk" ? "墨迹文件" : "笔记"; + string fileType; + string tags; + if (fileExtension == ".zip") + { + fileType = "多页面墨迹压缩包"; + tags = "自动上传,多页面,zip,压缩包"; + } + else if (fileExtension == ".icstk") + { + fileType = "墨迹文件"; + tags = "自动上传,墨迹,icstk"; + } + else + { + fileType = "笔记"; + tags = "自动上传,笔记,png"; + } var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; - var tags = fileExtension == ".icstk" ? "自动上传,墨迹,icstk" : "自动上传,笔记,png"; // 创建API客户端并上传文件 using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken)) @@ -466,7 +729,7 @@ namespace Ink_Canvas.Helpers // 检查文件扩展名 var fileExtension = Path.GetExtension(filePath).ToLower(); - if (fileExtension != ".png" && fileExtension != ".icstk") + if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".zip") { return false; // 文件格式错误,不可重试 } @@ -475,7 +738,8 @@ namespace Ink_Canvas.Helpers try { var fileInfo = new FileInfo(filePath); - if (fileInfo.Length > 10 * 1024 * 1024) + long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024; + if (fileInfo.Length > maxSize) { return false; // 文件过大,不可重试 } @@ -491,3 +755,4 @@ namespace Ink_Canvas.Helpers } } + diff --git a/Ink Canvas/Helpers/GlobalHotkeyManager.cs b/Ink Canvas/Helpers/GlobalHotkeyManager.cs index 11fea5a5..cd974d6e 100644 --- a/Ink Canvas/Helpers/GlobalHotkeyManager.cs +++ b/Ink Canvas/Helpers/GlobalHotkeyManager.cs @@ -78,6 +78,16 @@ namespace Ink_Canvas.Helpers { UnregisterHotkey(hotkeyName); } + else + { + try + { + HotkeyManager.Current.Remove(hotkeyName); + } + catch + { + } + } // 创建快捷键信息 var hotkeyInfo = new HotkeyInfo @@ -112,9 +122,8 @@ namespace Ink_Canvas.Helpers return true; } - catch (Exception ex) + catch (Exception) { - LogHelper.WriteLogToFile($"注册全局快捷键 {hotkeyName} 失败: {ex.Message}", LogHelper.LogType.Error); return false; } } diff --git a/Ink Canvas/Helpers/MultiPPTInkManager.cs b/Ink Canvas/Helpers/MultiPPTInkManager.cs deleted file mode 100644 index 02d70da2..00000000 --- a/Ink Canvas/Helpers/MultiPPTInkManager.cs +++ /dev/null @@ -1,813 +0,0 @@ -using Microsoft.Office.Interop.PowerPoint; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Security.Cryptography; -using System.Text; -using System.Windows.Ink; - -namespace Ink_Canvas.Helpers -{ - /// - /// 多PPT墨迹管理器 - 支持多个PPT窗口分别管理墨迹 - /// - public class MultiPPTInkManager : IDisposable - { - #region Properties - public bool IsAutoSaveEnabled { get; set; } = true; - public string AutoSaveLocation { get; set; } = ""; - public PPTManager PPTManager { get; set; } - #endregion - - #region Private Fields - private readonly Dictionary _presentationManagers; - private readonly Dictionary _presentationInfos; - private readonly object _lockObject = new object(); - private bool _disposed; - private string _currentActivePresentationId = ""; - - // 墨迹备份机制 - private readonly Dictionary> _strokeBackups; - private DateTime _lastBackupTime = DateTime.MinValue; - private const int BackupIntervalMinutes = 2; // 每2分钟备份一次 - #endregion - - #region Constructor - public MultiPPTInkManager() - { - _presentationManagers = new Dictionary(); - _presentationInfos = new Dictionary(); - _strokeBackups = new Dictionary>(); - } - #endregion - - #region Public Methods - /// - /// 初始化新的演示文稿 - /// - public void InitializePresentation(Presentation presentation) - { - if (presentation == null) return; - - lock (_lockObject) - { - try - { - var presentationId = GeneratePresentationId(presentation); - - // 如果已存在该演示文稿的管理器,先清理 - if (_presentationManagers.ContainsKey(presentationId)) - { - _presentationManagers[presentationId].Dispose(); - _presentationManagers.Remove(presentationId); - } - - // 创建新的墨迹管理器 - var inkManager = new PPTInkManager(); - inkManager.IsAutoSaveEnabled = IsAutoSaveEnabled; - inkManager.AutoSaveLocation = AutoSaveLocation; - inkManager.InitializePresentation(presentation); - - // 保存管理器和演示文稿信息 - _presentationManagers[presentationId] = inkManager; - _presentationInfos[presentationId] = new PresentationInfo - { - Id = presentationId, - Name = presentation.Name, - FullName = presentation.FullName, - SlideCount = presentation.Slides.Count, - CreatedTime = DateTime.Now, - LastAccessTime = DateTime.Now - }; - - // 设置为当前活跃的演示文稿 - _currentActivePresentationId = presentationId; - - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"初始化多PPT墨迹管理失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 切换到指定的演示文稿 - /// - public bool SwitchToPresentation(Presentation presentation) - { - if (presentation == null) return false; - - lock (_lockObject) - { - try - { - var presentationId = GeneratePresentationId(presentation); - - if (_presentationManagers.ContainsKey(presentationId)) - { - // 如果切换的是不同的演示文稿,先保存当前活跃演示文稿的墨迹 - if (!string.IsNullOrEmpty(_currentActivePresentationId) && - _currentActivePresentationId != presentationId) - { - var currentManager = GetCurrentManager(); - if (currentManager != null) - { - // 获取当前活跃的演示文稿并保存墨迹 - var currentPresentation = GetCurrentActivePresentation(); - if (currentPresentation != null) - { - try - { - currentManager.SaveAllStrokesToFile(currentPresentation); - LogHelper.WriteLogToFile($"已保存当前演示文稿墨迹: {currentPresentation.Name}", LogHelper.LogType.Trace); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存当前演示文稿墨迹失败: {ex}", LogHelper.LogType.Error); - } - } - } - } - - _currentActivePresentationId = presentationId; - - // 更新最后访问时间 - if (_presentationInfos.ContainsKey(presentationId)) - { - _presentationInfos[presentationId].LastAccessTime = DateTime.Now; - } - - if (_currentActivePresentationId != presentationId) - { - LogHelper.WriteLogToFile($"已切换到演示文稿: {presentation.Name}", LogHelper.LogType.Trace); - } - return true; - } - else - { - // 如果不存在,尝试初始化 - InitializePresentation(presentation); - return true; - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"切换到演示文稿失败: {ex}", LogHelper.LogType.Error); - return false; - } - } - } - - /// - /// 保存当前页面的墨迹 - /// - public void SaveCurrentSlideStrokes(int slideIndex, StrokeCollection strokes) - { - if (slideIndex <= 0 || strokes == null) return; - - lock (_lockObject) - { - try - { - var manager = GetCurrentManager(); - if (manager != null) - { - // 保存到管理器 - manager.SaveCurrentSlideStrokes(slideIndex, strokes); - - // 只有在保存成功后才创建备份 - if (!string.IsNullOrEmpty(_currentActivePresentationId)) - { - CreateStrokeBackup(_currentActivePresentationId, slideIndex, strokes); - } - - // 检查是否需要执行定期备份 - CheckAndPerformBackup(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存当前页面墨迹失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 强制保存指定页面的墨迹(忽略锁定状态) - /// - public void ForceSaveSlideStrokes(int slideIndex, StrokeCollection strokes) - { - if (slideIndex <= 0 || strokes == null) return; - - lock (_lockObject) - { - try - { - var manager = GetCurrentManager(); - if (manager != null) - { - manager.ForceSaveSlideStrokes(slideIndex, strokes); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"强制保存页面墨迹失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 加载指定页面的墨迹 - /// - public StrokeCollection LoadSlideStrokes(int slideIndex) - { - if (slideIndex <= 0) return new StrokeCollection(); - - lock (_lockObject) - { - try - { - var manager = GetCurrentManager(); - if (manager != null) - { - var strokes = manager.LoadSlideStrokes(slideIndex); - - // 如果从管理器加载失败,尝试从备份恢复 - if (strokes == null || strokes.Count == 0) - { - if (!string.IsNullOrEmpty(_currentActivePresentationId)) - { - strokes = RestoreStrokeFromBackup(_currentActivePresentationId, slideIndex); - } - } - - return strokes ?? new StrokeCollection(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"加载页面墨迹失败: {ex}", LogHelper.LogType.Error); - - // 尝试从备份恢复 - if (!string.IsNullOrEmpty(_currentActivePresentationId)) - { - return RestoreStrokeFromBackup(_currentActivePresentationId, slideIndex); - } - } - } - - return new StrokeCollection(); - } - - /// - /// 切换到指定页面并加载墨迹 - /// - public StrokeCollection SwitchToSlide(int slideIndex, StrokeCollection currentStrokes = null) - { - lock (_lockObject) - { - try - { - var manager = GetCurrentManager(); - if (manager != null) - { - return manager.SwitchToSlide(slideIndex, currentStrokes); - } - else - { - LogHelper.WriteLogToFile($"无法获取当前墨迹管理器,页面切换失败: {slideIndex}", LogHelper.LogType.Warning); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"切换页面墨迹失败: {ex}", LogHelper.LogType.Error); - } - } - - return new StrokeCollection(); - } - - /// - /// 保存所有墨迹到文件 - /// - public void SaveAllStrokesToFile(Presentation presentation) - { - if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return; - - lock (_lockObject) - { - try - { - var presentationId = GeneratePresentationId(presentation); - if (_presentationManagers.ContainsKey(presentationId)) - { - _presentationManagers[presentationId].SaveAllStrokesToFile(presentation); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"保存所有墨迹到文件失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 从文件加载已保存的墨迹 - /// - public void LoadSavedStrokes(Presentation presentation) - { - if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return; - - lock (_lockObject) - { - try - { - var presentationId = GeneratePresentationId(presentation); - if (_presentationManagers.ContainsKey(presentationId)) - { - _presentationManagers[presentationId].LoadSavedStrokes(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"从文件加载墨迹失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 清除指定演示文稿的所有墨迹 - /// - public void ClearPresentationStrokes(Presentation presentation) - { - if (presentation == null) return; - - lock (_lockObject) - { - try - { - var presentationId = GeneratePresentationId(presentation); - if (_presentationManagers.ContainsKey(presentationId)) - { - _presentationManagers[presentationId].ClearAllStrokes(); - LogHelper.WriteLogToFile($"已清除演示文稿墨迹: {presentation.Name}", LogHelper.LogType.Trace); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清除演示文稿墨迹失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 清除所有演示文稿的墨迹 - /// - public void ClearAllStrokes() - { - lock (_lockObject) - { - try - { - foreach (var manager in _presentationManagers.Values) - { - manager?.ClearAllStrokes(); - } - LogHelper.WriteLogToFile("已清除所有演示文稿墨迹", LogHelper.LogType.Trace); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清除所有墨迹失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 翻页后锁定墨迹写入 - /// - public void LockInkForSlide(int slideIndex) - { - lock (_lockObject) - { - try - { - var manager = GetCurrentManager(); - if (manager != null) - { - manager.LockInkForSlide(slideIndex); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"锁定墨迹写入失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 检查是否可以写入墨迹 - /// - public bool CanWriteInk(int currentSlideIndex) - { - lock (_lockObject) - { - try - { - var manager = GetCurrentManager(); - if (manager != null) - { - return manager.CanWriteInk(currentSlideIndex); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"检查墨迹写入权限失败: {ex}", LogHelper.LogType.Error); - } - } - - return false; - } - - /// - /// 重置当前演示文稿的墨迹锁定状态 - /// - public void ResetCurrentPresentationLockState() - { - lock (_lockObject) - { - try - { - var manager = GetCurrentManager(); - if (manager != null) - { - manager.ResetLockState(); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"重置墨迹锁定状态失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 移除演示文稿管理器 - /// - public void RemovePresentation(Presentation presentation) - { - if (presentation == null) return; - - lock (_lockObject) - { - try - { - var presentationId = GeneratePresentationId(presentation); - - if (_presentationManagers.ContainsKey(presentationId)) - { - // 保存墨迹到文件 - _presentationManagers[presentationId].SaveAllStrokesToFile(presentation); - - // 释放资源 - _presentationManagers[presentationId].Dispose(); - _presentationManagers.Remove(presentationId); - } - - if (_presentationInfos.ContainsKey(presentationId)) - { - _presentationInfos.Remove(presentationId); - } - - // 如果移除的是当前活跃的演示文稿,重置活跃ID - if (_currentActivePresentationId == presentationId) - { - _currentActivePresentationId = ""; - } - - } - catch (COMException comEx) - { - var hr = (uint)comEx.HResult; - if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706BA || hr == 0x800706BE || hr == 0x80048010) - { - } - } - catch (Exception) - { - } - } - } - - /// - /// 获取当前管理的演示文稿数量 - /// - public int GetPresentationCount() - { - lock (_lockObject) - { - return _presentationManagers.Count; - } - } - - /// - /// 获取所有演示文稿信息 - /// - public List GetAllPresentationInfos() - { - lock (_lockObject) - { - return _presentationInfos.Values.ToList(); - } - } - - /// - /// 清理长时间未访问的演示文稿管理器 - /// - public void CleanupInactivePresentations(TimeSpan inactiveThreshold) - { - lock (_lockObject) - { - try - { - var inactiveIds = new List(); - var cutoffTime = DateTime.Now - inactiveThreshold; - - foreach (var info in _presentationInfos.Values) - { - if (info.LastAccessTime < cutoffTime && info.Id != _currentActivePresentationId) - { - inactiveIds.Add(info.Id); - } - } - - foreach (var id in inactiveIds) - { - if (_presentationManagers.ContainsKey(id)) - { - _presentationManagers[id].Dispose(); - _presentationManagers.Remove(id); - } - _presentationInfos.Remove(id); - - // 清理备份数据 - if (_strokeBackups.ContainsKey(id)) - { - _strokeBackups.Remove(id); - } - - LogHelper.WriteLogToFile($"已清理非活跃演示文稿: {id}", LogHelper.LogType.Trace); - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"清理非活跃演示文稿失败: {ex}", LogHelper.LogType.Error); - } - } - } - - /// - /// 创建墨迹备份 - /// - private void CreateStrokeBackup(string presentationId, int slideIndex, StrokeCollection strokes) - { - try - { - if (strokes == null || strokes.Count == 0) return; - - if (!_strokeBackups.ContainsKey(presentationId)) - { - _strokeBackups[presentationId] = new Dictionary(); - } - - // 释放旧的备份 - if (_strokeBackups[presentationId].ContainsKey(slideIndex)) - { - _strokeBackups[presentationId][slideIndex] = null; - } - - // 创建新的备份 - _strokeBackups[presentationId][slideIndex] = strokes.Clone(); - - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"创建墨迹备份失败: {ex}", LogHelper.LogType.Error); - } - } - - /// - /// 从备份恢复墨迹 - /// - private StrokeCollection RestoreStrokeFromBackup(string presentationId, int slideIndex) - { - try - { - if (_strokeBackups.ContainsKey(presentationId) && - _strokeBackups[presentationId].ContainsKey(slideIndex)) - { - var backup = _strokeBackups[presentationId][slideIndex]; - if (backup != null) - { - LogHelper.WriteLogToFile($"从备份恢复第{slideIndex}页墨迹", LogHelper.LogType.Trace); - return backup.Clone(); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"从备份恢复墨迹失败: {ex}", LogHelper.LogType.Error); - } - - return new StrokeCollection(); - } - - /// - /// 检查并执行定期备份 - /// - private void CheckAndPerformBackup() - { - try - { - var now = DateTime.Now; - - // 检查是否需要执行备份 - if (now - _lastBackupTime < TimeSpan.FromMinutes(BackupIntervalMinutes)) - { - return; - } - - // 备份当前活跃演示文稿的所有墨迹 - if (!string.IsNullOrEmpty(_currentActivePresentationId) && - _presentationManagers.ContainsKey(_currentActivePresentationId)) - { - var manager = _presentationManagers[_currentActivePresentationId]; - if (manager != null) - { - // 这里可以添加更详细的备份逻辑 - } - } - - _lastBackupTime = now; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"定期备份检查失败: {ex}", LogHelper.LogType.Error); - } - } - #endregion - - #region Private Methods - private PPTInkManager GetCurrentManager() - { - if (string.IsNullOrEmpty(_currentActivePresentationId) || - !_presentationManagers.ContainsKey(_currentActivePresentationId)) - { - return null; - } - - return _presentationManagers[_currentActivePresentationId]; - } - - private Presentation GetCurrentActivePresentation() - { - try - { - // 通过PPTManager获取当前活跃的演示文稿 - return PPTManager?.GetCurrentActivePresentation(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"获取当前活跃演示文稿失败: {ex}", LogHelper.LogType.Error); - return null; - } - } - - private string GeneratePresentationId(Presentation presentation) - { - try - { - // 检查COM对象是否仍然有效 - if (presentation == null) - { - return $"invalid_{DateTime.Now.Ticks}"; - } - - var presentationPath = presentation.FullName; - var fileHash = GetFileHash(presentationPath); - var processId = GetProcessId(presentation); - return $"{presentation.Name}_{presentation.Slides.Count}_{fileHash}_{processId}"; - } - catch (COMException comEx) - { - var hr = (uint)comEx.HResult; - if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706BA || hr == 0x800706BE || hr == 0x80048010) - { - return $"disconnected_{DateTime.Now.Ticks}"; - } - return $"error_{DateTime.Now.Ticks}"; - } - catch (Exception) - { - return $"unknown_{DateTime.Now.Ticks}"; - } - } - - private string GetFileHash(string filePath) - { - try - { - if (string.IsNullOrEmpty(filePath)) return "unknown"; - - using (var md5 = MD5.Create()) - { - byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(filePath)); - return BitConverter.ToString(hashBytes).Replace("-", "").Substring(0, 8); - } - } - catch (Exception) - { - // 所有异常都静默处理,避免日志噪音 - return "error"; - } - } - - private string GetProcessId(Presentation presentation) - { - try - { - // 尝试获取PowerPoint应用程序的进程ID - if (presentation.Application != null) - { - // 通过COM对象获取进程信息 - var hwnd = presentation.Application.HWND; - if (hwnd != 0) - { - return hwnd.ToString(); - } - } - return "unknown"; - } - catch (COMException comEx) - { - // COM对象已失效,这是正常情况,完全静默处理 - var hr = (uint)comEx.HResult; - if (hr == 0x8001010E || hr == 0x80004005 || hr == 0x800706BA || hr == 0x800706BE || hr == 0x80048010) - { - return "disconnected"; - } - return "error"; - } - catch (Exception) - { - return "error"; - } - } - #endregion - - #region Dispose - public void Dispose() - { - if (!_disposed) - { - lock (_lockObject) - { - // 释放所有管理器 - foreach (var manager in _presentationManagers.Values) - { - manager?.Dispose(); - } - _presentationManagers.Clear(); - _presentationInfos.Clear(); - - // 清理备份数据 - foreach (var backupDict in _strokeBackups.Values) - { - foreach (var backup in backupDict.Values) - { - backup?.Clear(); - } - backupDict.Clear(); - } - _strokeBackups.Clear(); - } - _disposed = true; - } - } - #endregion - } - - /// - /// 演示文稿信息 - /// - public class PresentationInfo - { - public string Id { get; set; } - public string Name { get; set; } - public string FullName { get; set; } - public int SlideCount { get; set; } - public DateTime CreatedTime { get; set; } - public DateTime LastAccessTime { get; set; } - } -} diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index beaa21df..3a6ecac9 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -6,6 +6,8 @@ xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" xmlns:c="clr-namespace:Ink_Canvas.Converter" xmlns:Controls="http://schemas.microsoft.com/netfx/2009/xaml/presentation" + xmlns:controls="clr-namespace:Ink_Canvas.Controls" + xmlns:Windows="clr-namespace:Ink_Canvas.Windows" mc:Ignorable="d" AllowsTransparency="True" WindowStyle="None" @@ -3370,7 +3372,7 @@ + + + @@ -9941,6 +9951,27 @@ + + + + + + + + diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs index 5e9b8d14..13e4676d 100644 --- a/Ink Canvas/MainWindow.xaml.cs +++ b/Ink Canvas/MainWindow.xaml.cs @@ -28,6 +28,9 @@ using Application = System.Windows.Application; using Brushes = System.Windows.Media.Brushes; using Button = System.Windows.Controls.Button; using Cursor = System.Windows.Input.Cursor; +using MouseEventArgs = System.Windows.Input.MouseEventArgs; +using HorizontalAlignment = System.Windows.HorizontalAlignment; +using VerticalAlignment = System.Windows.VerticalAlignment; using Cursors = System.Windows.Input.Cursors; using DpiChangedEventArgs = System.Windows.DpiChangedEventArgs; using File = System.IO.File; @@ -60,8 +63,6 @@ namespace Ink_Canvas // 悬浮窗拦截管理器 private FloatingWindowInterceptorManager _floatingWindowInterceptorManager; - // 快抽悬浮按钮 - private QuickDrawFloatingButton _quickDrawFloatingButton; // 设置面板相关状态 private bool userChangedNoFocusModeInSettings; @@ -267,6 +268,74 @@ namespace Ink_Canvas // 为滑块控件添加触摸事件支持 AddTouchSupportToSliders(); + + // 初始化计时器控件事件 + Dispatcher.BeginInvoke(new Action(() => + { + if (TimerControl != null) + { + TimerControl.ShowMinimizedRequested += TimerControl_ShowMinimizedRequested; + TimerControl.HideMinimizedRequested += TimerControl_HideMinimizedRequested; + } + + if (MinimizedTimerControl != null && TimerControl != null) + { + MinimizedTimerControl.SetParentControl(TimerControl); + } + }), DispatcherPriority.Loaded); + } + + private void TimerControl_ShowMinimizedRequested(object sender, EventArgs e) + { + var timerContainer = FindName("TimerContainer") as FrameworkElement; + var minimizedContainer = FindName("MinimizedTimerContainer") as FrameworkElement; + + if (timerContainer != null && minimizedContainer != null) + { + double x = 0, y = 0; + + if (timerContainer.HorizontalAlignment == HorizontalAlignment.Center && + timerContainer.VerticalAlignment == VerticalAlignment.Center) + { + var timerPoint = timerContainer.TransformToAncestor(this).Transform(new Point(0, 0)); + x = timerPoint.X; + y = timerPoint.Y; + } + else + { + var timerMargin = timerContainer.Margin; + x = double.IsNaN(timerMargin.Left) ? 0 : timerMargin.Left; + y = double.IsNaN(timerMargin.Top) ? 0 : timerMargin.Top; + } + + minimizedContainer.Margin = new Thickness(x, y, 0, 0); + minimizedContainer.HorizontalAlignment = HorizontalAlignment.Left; + minimizedContainer.VerticalAlignment = VerticalAlignment.Top; + + timerContainer.Margin = new Thickness(x, y, 0, 0); + timerContainer.HorizontalAlignment = HorizontalAlignment.Left; + timerContainer.VerticalAlignment = VerticalAlignment.Top; + + timerContainer.Visibility = Visibility.Collapsed; + minimizedContainer.Visibility = Visibility.Visible; + } + } + + private void TimerControl_HideMinimizedRequested(object sender, EventArgs e) + { + var timerContainer = FindName("TimerContainer") as FrameworkElement; + var minimizedContainer = FindName("MinimizedTimerContainer") as FrameworkElement; + + if (timerContainer != null && minimizedContainer != null) + { + minimizedContainer.Visibility = Visibility.Collapsed; + timerContainer.Visibility = Visibility.Visible; + + if (TimerControl != null) + { + TimerControl.UpdateActivityTime(); + } + } } @@ -324,13 +393,11 @@ namespace Ink_Canvas { if (gest.ApplicationGesture == ApplicationGesture.Left) { - // 直接发送翻页请求到PPT放映软件 - SendKeyToPPTSlideShow(false); // 下一页 + BtnPPTSlidesDown_Click(null, null); // 下一页 } if (gest.ApplicationGesture == ApplicationGesture.Right) { - // 直接发送翻页请求到PPT放映软件 - SendKeyToPPTSlideShow(true); // 上一页 + BtnPPTSlidesUp_Click(null, null); // 上一页 } } } @@ -403,6 +470,9 @@ namespace Ink_Canvas LoadSettings(true); AutoBackupManager.Initialize(Settings); + // 初始化Dlass上传队列(恢复上次的上传队列) + DlassNoteUploader.InitializeQueue(); + // 检查保存路径是否可用,不可用则修正 try { @@ -616,6 +686,34 @@ namespace Ink_Canvas } }), DispatcherPriority.Loaded); } + + // 初始化计时器控件关联 + Dispatcher.BeginInvoke(new Action(() => + { + if (TimerControl != null && MinimizedTimerControl != null) + { + MinimizedTimerControl.SetParentControl(TimerControl); + + TimerControl.ShowMinimizedRequested += (s, args) => + { + if (TimerContainer != null && MinimizedTimerContainer != null && MinimizedTimerControl != null) + { + TimerContainer.Visibility = Visibility.Collapsed; + MinimizedTimerContainer.Visibility = Visibility.Visible; + MinimizedTimerControl.Visibility = Visibility.Visible; + } + }; + + TimerControl.HideMinimizedRequested += (s, args) => + { + if (MinimizedTimerContainer != null && MinimizedTimerControl != null) + { + MinimizedTimerContainer.Visibility = Visibility.Collapsed; + MinimizedTimerControl.Visibility = Visibility.Collapsed; + } + }; + } + }), DispatcherPriority.Loaded); } private void SystemEventsOnDisplaySettingsChanged(object sender, EventArgs e) @@ -676,11 +774,7 @@ namespace Ink_Canvas LogHelper.WriteLogToFile("Ink Canvas closing", LogHelper.LogType.Event); try { - if (_quickDrawFloatingButton != null) - { - _quickDrawFloatingButton.Close(); - _quickDrawFloatingButton = null; - } + // 快抽按钮现在集成在主窗口中,不需要单独关闭 } catch (Exception ex) { @@ -782,6 +876,8 @@ namespace Ink_Canvas // 停止置顶维护定时器 StopTopmostMaintenance(); + UninstallKeyboardHook(); + // 从Z-Order管理器中移除主窗口 WindowZOrderManager.UnregisterWindow(this); @@ -1838,7 +1934,40 @@ namespace Ink_Canvas private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll")] private static extern uint GetCurrentProcessId(); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern IntPtr GetModuleHandle(string lpModuleName); + + private const int WH_KEYBOARD_LL = 13; + private const int WM_KEYDOWN = 0x0100; + private const int WM_KEYUP = 0x0101; + private const int WM_SYSKEYDOWN = 0x0104; + private const int WM_SYSKEYUP = 0x0105; + + private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + private struct KBDLLHOOKSTRUCT + { + public uint vkCode; + public uint scanCode; + public uint flags; + public uint time; + public IntPtr dwExtraInfo; + } + + private LowLevelKeyboardProc _keyboardProc; + private IntPtr _keyboardHookId = IntPtr.Zero; private const int GWL_EXSTYLE = -20; private const int WS_EX_NOACTIVATE = 0x08000000; @@ -1856,6 +1985,67 @@ namespace Ink_Canvas private DispatcherTimer autoSaveStrokesTimer; private bool isTopmostMaintenanceEnabled; + private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam) + { + if (nCode >= 0) + { + if (Settings.Advanced.IsNoFocusMode && + BtnPPTSlideShowEnd.Visibility == Visibility.Visible && + currentMode == 0) + { + KBDLLHOOKSTRUCT hookStruct = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); + uint vkCode = hookStruct.vkCode; + + if (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN) + { + if (vkCode == 0x22 || vkCode == 0x28 || vkCode == 0x27 || + vkCode == 0x4E || vkCode == 0x20) + { + Dispatcher.BeginInvoke(new Action(() => + { + BtnPPTSlidesDown_Click(null, null); + }), DispatcherPriority.Normal); + return (IntPtr)1; + } + else if (vkCode == 0x21 || vkCode == 0x26 || vkCode == 0x25 || + vkCode == 0x50) + { + Dispatcher.BeginInvoke(new Action(() => + { + BtnPPTSlidesUp_Click(null, null); + }), DispatcherPriority.Normal); + return (IntPtr)1; + } + } + } + } + return CallNextHookEx(_keyboardHookId, nCode, wParam, lParam); + } + + private void InstallKeyboardHook() + { + if (_keyboardHookId == IntPtr.Zero) + { + _keyboardProc = KeyboardHookProc; + _keyboardHookId = SetWindowsHookEx(WH_KEYBOARD_LL, _keyboardProc, + GetModuleHandle(null), 0); + if (_keyboardHookId == IntPtr.Zero) + { + LogHelper.WriteLogToFile("安装低级键盘钩子失败", LogHelper.LogType.Error); + } + } + } + + private void UninstallKeyboardHook() + { + if (_keyboardHookId != IntPtr.Zero) + { + UnhookWindowsHookEx(_keyboardHookId); + _keyboardHookId = IntPtr.Zero; + _keyboardProc = null; + } + } + private void ApplyNoFocusMode() { var hwnd = new WindowInteropHelper(this).Handle; @@ -1867,10 +2057,12 @@ namespace Ink_Canvas if (shouldBeNoFocus) { SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE); + InstallKeyboardHook(); } else { SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_NOACTIVATE); + UninstallKeyboardHook(); } } @@ -1961,6 +2153,28 @@ namespace Ink_Canvas } } + public void PauseTopmostMaintenance() + { + if (topmostMaintenanceTimer != null && isTopmostMaintenanceEnabled) + { + topmostMaintenanceTimer.Stop(); + } + } + + public void ResumeTopmostMaintenance() + { + if (Settings.Advanced.IsAlwaysOnTop && + Settings.Advanced.IsNoFocusMode && + !Settings.Advanced.EnableUIAccessTopMost) + { + if (topmostMaintenanceTimer != null && !isTopmostMaintenanceEnabled) + { + topmostMaintenanceTimer.Start(); + isTopmostMaintenanceEnabled = true; + } + } + } + /// /// 置顶维护定时器事件 /// @@ -2349,47 +2563,6 @@ namespace Ink_Canvas } #endregion - #region PPT翻页直接传递 - /// - /// 直接发送翻页请求到PPT放映软件,让PPT软件处理翻页 - /// - /// 是否为上一页 - private void SendKeyToPPTSlideShow(bool isPrevious) - { - try - { - // 查找PPT放映窗口并发送按键 - var pptWindows = Process.GetProcessesByName("POWERPNT"); - var wpsWindows = Process.GetProcessesByName("wpp"); - - foreach (var process in pptWindows.Concat(wpsWindows)) - { - if (process.MainWindowHandle != IntPtr.Zero) - { - // 激活PPT窗口 - SetForegroundWindow(process.MainWindowHandle); - - // 发送翻页按键消息 - int keyCode = isPrevious ? 0x21 : 0x22; // VK_PRIOR : VK_NEXT - - // 发送按键按下和释放消息 - PostMessage(process.MainWindowHandle, 0x0100, (IntPtr)keyCode, IntPtr.Zero); // WM_KEYDOWN - PostMessage(process.MainWindowHandle, 0x0101, (IntPtr)keyCode, IntPtr.Zero); // WM_KEYUP - - break; - } - } - } - catch (Exception) - { - // 如果直接发送失败,回退到原来的方法 - if (isPrevious) - BtnPPTSlidesUp_Click(BtnPPTSlidesUp, null); - else - BtnPPTSlidesDown_Click(BtnPPTSlidesDown, null); - } - } - #endregion /// /// 初始化文件关联状态显示 @@ -3082,30 +3255,18 @@ namespace Ink_Canvas { try { + var quickDrawButton = FindName("QuickDrawFloatingButton") as Controls.QuickDrawFloatingButtonControl; + if (quickDrawButton == null) return; + // 检查设置是否启用快抽功能 - if (Settings?.RandSettings?.EnableQuickDraw != true) + if (Settings?.RandSettings?.EnableQuickDraw == true) { - // 如果设置未启用,确保悬浮按钮被关闭 - if (_quickDrawFloatingButton != null) - { - _quickDrawFloatingButton.Close(); - _quickDrawFloatingButton = null; - } - return; + quickDrawButton.Visibility = Visibility.Visible; } - - // 如果已经存在悬浮按钮,先关闭它 - if (_quickDrawFloatingButton != null) + else { - _quickDrawFloatingButton.Close(); - _quickDrawFloatingButton = null; + quickDrawButton.Visibility = Visibility.Collapsed; } - - // 创建并显示悬浮按钮 - _quickDrawFloatingButton = new QuickDrawFloatingButton(); - _quickDrawFloatingButton.Show(); - - LogHelper.WriteLogToFile("快抽悬浮按钮已显示", LogHelper.LogType.Trace); } catch (Exception ex) { diff --git a/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs b/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs index 005f548e..9b636ef2 100644 --- a/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs +++ b/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs @@ -536,6 +536,17 @@ namespace Ink_Canvas operatingGuideWindow.RefreshTheme(); } } + + // 刷新计时器控件 + if (TimerControl != null) + { + TimerControl.RefreshTheme(); + } + + if (MinimizedTimerControl != null) + { + MinimizedTimerControl.RefreshTheme(); + } } catch (Exception) { diff --git a/Ink Canvas/MainWindow_cs/MW_BoardControls.cs b/Ink Canvas/MainWindow_cs/MW_BoardControls.cs index a1b2319b..ab338002 100644 --- a/Ink Canvas/MainWindow_cs/MW_BoardControls.cs +++ b/Ink Canvas/MainWindow_cs/MW_BoardControls.cs @@ -424,8 +424,14 @@ namespace Ink_Canvas BtnLeftWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页"; BtnRightWhiteBoardSwitchNextLabel.Text = isLastPage ? "新页面" : "下一页"; - // 始终允许点击"下一页/新页面"按钮(除非已达最大页数) - BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage; + if (isLastPage) + { + BtnWhiteBoardSwitchNext.IsEnabled = !isMaxPage; + } + else + { + BtnWhiteBoardSwitchNext.IsEnabled = true; + } // 获取主题颜色资源 var iconForegroundBrush = Application.Current.FindResource("IconForeground") as SolidColorBrush; diff --git a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs index e9334f88..1aec6e11 100644 --- a/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs +++ b/Ink Canvas/MainWindow_cs/MW_FloatingBarIcons.cs @@ -742,7 +742,6 @@ namespace Ink_Canvas ICCWaterMarkDark.Visibility = Visibility.Collapsed; } - // 新增:确保在白板模式下基础浮动栏被隐藏 ViewboxFloatingBar.Visibility = Visibility.Collapsed; } else @@ -1052,17 +1051,38 @@ namespace Ink_Canvas AnimationsHelper.HideWithSlideAndFade(BoardBorderTools); AnimationsHelper.HideWithSlideAndFade(BoardImageOptionsPanel); - // 参考老计时器的窗口置顶功能:在白板模式下停止窗口置顶 - if (currentMode == 1) // 白板模式 + if (Settings.RandSettings?.UseNewStyleUI == true) { - Topmost = false; + if (TimerContainer != null && TimerControl != null) + { + TimerContainer.Visibility = Visibility.Visible; + if (MinimizedTimerContainer != null) + { + MinimizedTimerContainer.Visibility = Visibility.Collapsed; + } + TimerControl.CloseRequested += (s, args) => + { + TimerContainer.Visibility = Visibility.Collapsed; + if (MinimizedTimerContainer != null) + { + MinimizedTimerContainer.Visibility = Visibility.Collapsed; + } + }; + } } - - var timerWindow = CountdownTimerWindow.CreateTimerWindow(); - timerWindow.Show(); - if (currentMode == 1) // 白板模式 + else { - timerWindow.Topmost = true; + if (currentMode == 1) + { + Topmost = false; + } + + var timerWindow = CountdownTimerWindow.CreateTimerWindow(); + timerWindow.Show(); + if (currentMode == 1) + { + timerWindow.Topmost = true; + } } } @@ -3048,11 +3068,8 @@ namespace Ink_Canvas SaveStrokes(true); ClearStrokes(true); - // 总是恢复备份墨迹,不管是否在PPT模式 - // PPT墨迹和白板墨迹应该分别管理,不应该互相影响 RestoreStrokes(); - // 新增:在白板模式下隐藏基础浮动栏 ViewboxFloatingBar.Visibility = Visibility.Collapsed; BtnSwitch.Content = "屏幕"; @@ -3203,7 +3220,6 @@ namespace Ink_Canvas CheckEnableTwoFingerGestureBtnVisibility(false); HideSubPanels("cursor"); - // 新增:在屏幕模式下显示基础浮动栏 if (currentMode == 0) { ViewboxFloatingBar.Visibility = Visibility.Visible; @@ -3214,7 +3230,6 @@ namespace Ink_Canvas AnimationsHelper.ShowWithSlideFromLeftAndFade(StackPanelCanvasControls); CheckEnableTwoFingerGestureBtnVisibility(true); - // 新增:在批注模式下显示基础浮动栏 if (currentMode == 0) { ViewboxFloatingBar.Visibility = Visibility.Visible; @@ -3358,7 +3373,7 @@ namespace Ink_Canvas } } - // 新增:插入图片方法 + // 插入图片方法 private async void InsertImage_MouseUp_New(object sender, MouseButtonEventArgs e) { var dialog = new OpenFileDialog @@ -3590,8 +3605,6 @@ namespace Ink_Canvas // 检查浮动栏是否处于收起状态 if (isFloatingBarFolded || (BorderFloatingBarMainControls != null && BorderFloatingBarMainControls.Visibility == Visibility.Collapsed)) { - // 在收起状态下,仍然需要设置高光位置,但可能需要调整计算方式 - // 这里先隐藏高光,等浮动栏展开时再显示 FloatingbarSelectionBG.Visibility = Visibility.Hidden; return; } diff --git a/Ink Canvas/MainWindow_cs/MW_Hotkeys.cs b/Ink Canvas/MainWindow_cs/MW_Hotkeys.cs index 0d585128..e14f5e92 100644 --- a/Ink Canvas/MainWindow_cs/MW_Hotkeys.cs +++ b/Ink Canvas/MainWindow_cs/MW_Hotkeys.cs @@ -7,49 +7,31 @@ namespace Ink_Canvas { private void Window_MouseWheel(object sender, MouseWheelEventArgs e) { - // 只有在PPT放映模式下才响应鼠标滚轮翻页 - if (StackPanelPPTControls.Visibility != Visibility.Visible || - currentMode != 0 || - BtnPPTSlideShowEnd.Visibility != Visibility.Visible || - PPTManager?.IsInSlideShow != true) return; - - // 直接发送翻页请求到PPT放映软件,不通过软件处理 + if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return; if (e.Delta >= 120) { - // 上一页 - 发送PageUp键到PPT放映窗口 - SendKeyToPPTSlideShow(true); + BtnPPTSlidesUp_Click(null, null); } else if (e.Delta <= -120) { - // 下一页 - 发送PageDown键到PPT放映窗口 - SendKeyToPPTSlideShow(false); + BtnPPTSlidesDown_Click(null, null); } } private void Main_Grid_PreviewKeyDown(object sender, KeyEventArgs e) { - // 只有在PPT放映模式下才响应键盘翻页快捷键 - if (StackPanelPPTControls.Visibility != Visibility.Visible || - currentMode != 0 || - BtnPPTSlideShowEnd.Visibility != Visibility.Visible || - PPTManager?.IsInSlideShow != true) return; + if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || currentMode != 0) return; - // 直接发送翻页请求到PPT放映软件,不通过软件处理 - if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N || - e.Key == Key.Space) + if (e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Right || e.Key == Key.N || e.Key == Key.Space) { - e.Handled = true; // 阻止事件继续传播 - SendKeyToPPTSlideShow(false); // 下一页 + BtnPPTSlidesDown_Click(null, null); } - else if (e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Left || e.Key == Key.P) + if (e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Left || e.Key == Key.P) { - e.Handled = true; // 阻止事件继续传播 - SendKeyToPPTSlideShow(true); // 上一页 + BtnPPTSlidesUp_Click(null, null); } } - // 保留PPT翻页快捷键处理 - // 以下方法保留供全局快捷键调用 private void HotKey_Undo(object sender, ExecutedRoutedEventArgs e) { diff --git a/Ink Canvas/MainWindow_cs/MW_PPT.cs b/Ink Canvas/MainWindow_cs/MW_PPT.cs index d542b616..2906d5b1 100644 --- a/Ink Canvas/MainWindow_cs/MW_PPT.cs +++ b/Ink Canvas/MainWindow_cs/MW_PPT.cs @@ -104,7 +104,6 @@ namespace Ink_Canvas #region PPT Managers private PPTManager _pptManager; - private MultiPPTInkManager _multiPPTInkManager; private PPTInkManager _singlePPTInkManager; private PPTUIManager _pptUIManager; @@ -135,19 +134,9 @@ namespace Ink_Canvas _pptManager.PresentationClose += OnPPTPresentationClose; _pptManager.SlideShowStateChanged += OnPPTSlideShowStateChanged; - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager = new PPTInkManager(); - _singlePPTInkManager.IsAutoSaveEnabled = Settings.PowerPointSettings.IsAutoSaveStrokesInPowerPoint; - _singlePPTInkManager.AutoSaveLocation = Settings.Automation.AutoSavedStrokesLocation; - } - else - { - _multiPPTInkManager = new MultiPPTInkManager(); - _multiPPTInkManager.IsAutoSaveEnabled = Settings.PowerPointSettings.IsAutoSaveStrokesInPowerPoint; - _multiPPTInkManager.AutoSaveLocation = Settings.Automation.AutoSavedStrokesLocation; - _multiPPTInkManager.PPTManager = _pptManager; - } + _singlePPTInkManager = new PPTInkManager(); + _singlePPTInkManager.IsAutoSaveEnabled = Settings.PowerPointSettings.IsAutoSaveStrokesInPowerPoint; + _singlePPTInkManager.AutoSaveLocation = Settings.Automation.AutoSavedStrokesLocation; // 初始化UI管理器 _pptUIManager = new PPTUIManager(this); @@ -430,12 +419,10 @@ namespace Ink_Canvas try { _pptManager?.Dispose(); - _multiPPTInkManager?.Dispose(); _singlePPTInkManager?.Dispose(); _longPressTimer?.Stop(); _longPressTimer = null; _pptManager = null; - _multiPPTInkManager = null; _singlePPTInkManager = null; _pptUIManager = null; @@ -521,14 +508,7 @@ namespace Ink_Canvas else { LogHelper.WriteLogToFile("PPT连接已断开", LogHelper.LogType.Event); - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager?.ClearAllStrokes(); - } - else - { - _multiPPTInkManager?.ClearAllStrokes(); - } + _singlePPTInkManager?.ClearAllStrokes(); } }); } @@ -553,14 +533,7 @@ namespace Ink_Canvas TimeMachineHistories[0] = null; } - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager?.InitializePresentation(pres); - } - else - { - _multiPPTInkManager?.InitializePresentation(pres); - } + _singlePPTInkManager?.InitializePresentation(pres); // 处理跳转到首页或上次播放页的逻辑 HandlePresentationOpenNavigation(pres); @@ -594,15 +567,7 @@ namespace Ink_Canvas { Application.Current.Dispatcher.InvokeAsync(() => { - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager?.SaveAllStrokesToFile(pres); - } - else - { - _multiPPTInkManager?.SaveAllStrokesToFile(pres); - _multiPPTInkManager?.RemovePresentation(pres); - } + _singlePPTInkManager?.SaveAllStrokesToFile(pres); _pptUIManager?.UpdateConnectionStatus(false); }); @@ -665,7 +630,7 @@ namespace Ink_Canvas isStopInkReplay = true; - await Application.Current.Dispatcher.InvokeAsync(() => + await Application.Current.Dispatcher.InvokeAsync(async () => { Presentation activePresentation = null; int currentSlide = 0; @@ -686,12 +651,15 @@ namespace Ink_Canvas if (activePresentation != null) { - if (Settings.PowerPointSettings.IsSupportWPS) + if (_singlePPTInkManager != null) { - } - else - { - _multiPPTInkManager?.SwitchToPresentation(activePresentation); + try + { + _singlePPTInkManager.InitializePresentation(activePresentation); + } + catch (Exception) + { + } } } @@ -754,6 +722,7 @@ namespace Ink_Canvas if (Settings.PowerPointSettings.IsShowCanvasAtNewSlideShow && !Settings.Automation.IsAutoFoldInPPTSlideShow) { + await Task.Delay(300); // 先进入批注模式,这会显示调色盘 PenIcon_Click(null, null); // 然后设置颜色 @@ -827,11 +796,6 @@ namespace Ink_Canvas var activePresentation = wn.Presentation; var totalSlides = activePresentation.Slides.Count; - if (!Settings.PowerPointSettings.IsSupportWPS) - { - _multiPPTInkManager?.SwitchToPresentation(activePresentation); - } - // 使用防抖机制处理页面切换 HandleSlideSwitchWithDebounce(currentSlide, totalSlides); @@ -879,14 +843,7 @@ namespace Ink_Canvas if (isEnteredSlideShowEndEvent) return; isEnteredSlideShowEndEvent = true; - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager?.SaveAllStrokesToFile(pres); - } - else - { - _multiPPTInkManager?.SaveAllStrokesToFile(pres); - } + _singlePPTInkManager?.SaveAllStrokesToFile(pres); await Application.Current.Dispatcher.InvokeAsync(() => { @@ -1148,15 +1105,7 @@ namespace Ink_Canvas ClearStrokes(true); timeMachine.ClearStrokeHistory(); - StrokeCollection strokes = null; - if (Settings.PowerPointSettings.IsSupportWPS) - { - strokes = _singlePPTInkManager?.LoadSlideStrokes(slideIndex); - } - else - { - strokes = _multiPPTInkManager?.LoadSlideStrokes(slideIndex); - } + StrokeCollection strokes = _singlePPTInkManager?.LoadSlideStrokes(slideIndex); if (strokes != null && strokes.Count > 0) { @@ -1176,19 +1125,7 @@ namespace Ink_Canvas { try { - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager?.ResetLockState(); - } - else - { - var activePresentation = _pptManager?.GetCurrentActivePresentation(); - if (activePresentation != null) - { - _multiPPTInkManager?.SwitchToPresentation(activePresentation); - _multiPPTInkManager?.ResetCurrentPresentationLockState(); - } - } + _singlePPTInkManager?.ResetLockState(); } catch (Exception ex) { @@ -1300,47 +1237,23 @@ namespace Ink_Canvas // 如果有当前墨迹且不是第一次切换,先保存到当前页面 if (inkCanvas.Strokes.Count > 0 && currentSlideIndex > 0 && currentSlideIndex != newSlideIndex) { - bool canWrite = false; - if (Settings.PowerPointSettings.IsSupportWPS) - { - canWrite = _singlePPTInkManager?.CanWriteInk(currentSlideIndex) == true; - } - else - { - canWrite = _multiPPTInkManager?.CanWriteInk(currentSlideIndex) == true; - } + bool canWrite = _singlePPTInkManager?.CanWriteInk(currentSlideIndex) == true; if (canWrite) { - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlideIndex, inkCanvas.Strokes); - } - else - { - _multiPPTInkManager?.SaveCurrentSlideStrokes(currentSlideIndex, inkCanvas.Strokes); - } + _singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlideIndex, inkCanvas.Strokes); } } ClearStrokes(true); timeMachine.ClearStrokeHistory(); - StrokeCollection newStrokes = null; - if (Settings.PowerPointSettings.IsSupportWPS) - { - newStrokes = _singlePPTInkManager?.SwitchToSlide(newSlideIndex, null); - } - else - { - newStrokes = _multiPPTInkManager?.SwitchToSlide(newSlideIndex, null); - } + StrokeCollection newStrokes = _singlePPTInkManager?.SwitchToSlide(newSlideIndex, null); if (newStrokes != null && newStrokes.Count > 0) { inkCanvas.Strokes.Add(newStrokes); } - // 注意:LockInkForSlide已经在SwitchToSlide中调用,这里不需要重复调用 } catch (Exception ex) { @@ -1474,14 +1387,7 @@ namespace Ink_Canvas var currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0; if (currentSlide > 0) { - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes); - } - else - { - _multiPPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes); - } + _singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes); } // 保存截图(如果启用) @@ -1521,14 +1427,7 @@ namespace Ink_Canvas var currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0; if (currentSlide > 0) { - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes); - } - else - { - _multiPPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes); - } + _singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes); } // 保存截图(如果启用) @@ -1688,14 +1587,7 @@ namespace Ink_Canvas { Application.Current.Dispatcher.Invoke(() => { - if (Settings.PowerPointSettings.IsSupportWPS) - { - _singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes); - } - else - { - _multiPPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes); - } + _singlePPTInkManager?.SaveCurrentSlideStrokes(currentSlide, inkCanvas.Strokes); timeMachine.ClearStrokeHistory(); }); } diff --git a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs index 0e3b6f12..84621012 100644 --- a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs +++ b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs @@ -79,7 +79,7 @@ namespace Ink_Canvas for (int i = 1; i <= totalSlides; i++) { - var slideStrokes = _multiPPTInkManager?.LoadSlideStrokes(i); + var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i); if (slideStrokes != null && slideStrokes.Count > 0) { allPageStrokes.Add(slideStrokes); @@ -246,6 +246,24 @@ namespace Ink_Canvas // 使用System.IO.Compression.FileSystem来创建ZIP ZipFile.CreateFromDirectory(tempDir, zipFileName); + // 异步上传ZIP文件到Dlass + _ = Task.Run(async () => + { + try + { + var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0; + if (delayMinutes > 0) + { + await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); + } + + await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName); + } + catch (Exception) + { + } + }); + if (newNotice) ShowNotification($"多页面墨迹成功保存至压缩包 {zipFileName}"); } finally @@ -563,7 +581,7 @@ namespace Ink_Canvas timeMachine.ClearStrokeHistory(); // 重置PPT墨迹存储 - _multiPPTInkManager?.ClearAllStrokes(); + _singlePPTInkManager?.ClearAllStrokes(); // 读取所有页面的墨迹文件 var files = Directory.GetFiles(tempDir, "page_*.icstk"); @@ -577,7 +595,7 @@ namespace Ink_Canvas var strokes = new StrokeCollection(fs); if (strokes.Count > 0) { - _multiPPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes); + _singlePPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes); } } } @@ -587,7 +605,7 @@ namespace Ink_Canvas if (_pptManager?.IsInSlideShow == true) { int currentSlide = _pptManager.GetCurrentSlideNumber(); - var currentStrokes = _multiPPTInkManager?.LoadSlideStrokes(currentSlide); + var currentStrokes = _singlePPTInkManager?.LoadSlideStrokes(currentSlide); if (currentStrokes != null && currentStrokes.Count > 0) { inkCanvas.Strokes.Add(currentStrokes); diff --git a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs index 058fc518..e56b1ac3 100644 --- a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs +++ b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs @@ -236,39 +236,30 @@ namespace Ink_Canvas // 检查是否启用了直线自动拉直功能 if (Settings.Canvas.AutoStraightenLine && IsPotentialStraightLine(e.Stroke)) { - // Get start and end points of the stroke - Point startPoint = e.Stroke.StylusPoints[0].ToPoint(); - Point endPoint = e.Stroke.StylusPoints[e.Stroke.StylusPoints.Count - 1].ToPoint(); + Point endpoint1, endpoint2; + bool shouldStraighten = TryGetStraightLineEndpoints(e.Stroke, out endpoint1, out endpoint2); - // 先完成所有直线判定,再考虑端点吸附 - // 读取实际的灵敏度设置值 - double sensitivity = Settings.InkToShape.LineStraightenSensitivity; - Debug.WriteLine($"当前灵敏度值: {sensitivity}"); - - // 判断是否应该拉直线条 - bool shouldStraighten = ShouldStraightenLine(e.Stroke); - - // 输出一些调试信息,帮助理解灵敏度设置的效果 - Debug.WriteLine($"LineStraightenSensitivity: {Settings.InkToShape.LineStraightenSensitivity}, ShouldStraighten: {shouldStraighten}"); - - // 只有当确定要拉直线条时,才检查端点吸附 - if (shouldStraighten && Settings.Canvas.LineEndpointSnapping) - { - // 只有在启用了形状识别(矩形或三角形)时才执行端点吸附 - if (Settings.InkToShape.IsInkToShapeRectangle || Settings.InkToShape.IsInkToShapeTriangle) - { - Point[] snappedPoints = GetSnappedEndpoints(startPoint, endPoint); - if (snappedPoints != null) - { - startPoint = snappedPoints[0]; - endPoint = snappedPoints[1]; - } - } - } - - // 如果确定要拉直,则创建直线 if (shouldStraighten) { + Point startPoint = endpoint1; + Point endPoint = endpoint2; + + // 只有当确定要拉直线条时,才检查端点吸附 + if (Settings.Canvas.LineEndpointSnapping) + { + // 只有在启用了形状识别(矩形或三角形)时才执行端点吸附 + if (Settings.InkToShape.IsInkToShapeRectangle || Settings.InkToShape.IsInkToShapeTriangle) + { + Point[] snappedPoints = GetSnappedEndpoints(startPoint, endPoint); + if (snappedPoints != null) + { + startPoint = snappedPoints[0]; + endPoint = snappedPoints[1]; + } + } + } + + // 创建直线 StylusPointCollection straightLinePoints = CreateStraightLine(startPoint, endPoint); Stroke straightStroke = new Stroke(straightLinePoints) { @@ -857,7 +848,7 @@ namespace Ink_Canvas // 获取用户设置的灵敏度值,确保使用正确的设置 double sensitivity = Settings.InkToShape.LineStraightenSensitivity; - // 输出当前灵敏度值(调试用) + // 输出当前灵敏度值 Debug.WriteLine($"IsPotentialStraightLine - sensitivity: {sensitivity}, length: {lineLength}"); // 将灵敏度转换为阈值:灵敏度0.05-2.0映射到阈值0.01-0.4 @@ -884,11 +875,9 @@ namespace Ink_Canvas // 使用相对偏差:偏差与线长的比例,并使用灵敏度进行调整 double quickRelativeThreshold = lineLength * quickThreshold; - // 记录检测到的偏差(调试用) + // 记录检测到的偏差 Debug.WriteLine($"Deviations: q={quarterDeviation}, m={midDeviation}, tq={threeQuarterDeviation}, threshold={quickRelativeThreshold}"); - // 修复后的逻辑:灵敏度越大,容许的偏差越大 - // 如果任一点偏离太大,直接排除(使用统一的判断标准) if (quarterDeviation > quickRelativeThreshold || midDeviation > quickRelativeThreshold || threeQuarterDeviation > quickRelativeThreshold) @@ -901,7 +890,7 @@ namespace Ink_Canvas } /// - /// 检查墨迹是否为复杂形状(如一团墨迹、涂鸦等) + /// 检查墨迹是否为复杂形状 /// private bool IsComplexShape(Stroke stroke) { @@ -1177,291 +1166,144 @@ namespace Ink_Canvas lineEnd.X * lineStart.Y - lineEnd.Y * lineStart.X) / lineLength; } - // New method: Determines if a stroke should be straightened into a line + private bool TryGetStraightLineEndpoints(Stroke stroke, out Point endpoint1, out Point endpoint2) + { + endpoint1 = new Point(); + endpoint2 = new Point(); + + var points = stroke.StylusPoints.Select(p => p.ToPoint()).ToList(); + if (points.Count < 10) + { + return false; + } + + // 使用总最小二乘法(TLS/PCA)进行直线拟合 + int n = points.Count - 8; + List filteredPoints = new List(); + + // 收集过滤后的点(跳过前 4 个和后 4 个点,用于计算直线方向) + for (int i = 4; i < n + 4; i++) + { + filteredPoints.Add(points[i]); + } + + // 计算中心点(使用过滤后的点) + double centerX = 0, centerY = 0; + foreach (Point p in filteredPoints) + { + centerX += p.X; + centerY += p.Y; + } + centerX /= filteredPoints.Count; + centerY /= filteredPoints.Count; + + // 计算协方差矩阵(使用过滤后的点) + double covXX = 0, covYY = 0, covXY = 0; + foreach (Point p in filteredPoints) + { + double dx = p.X - centerX; + double dy = p.Y - centerY; + covXX += dx * dx; + covYY += dy * dy; + covXY += dx * dy; + } + + // 计算特征值和特征向量 + double trace = covXX + covYY; + double determinant = covXX * covYY - covXY * covXY; + double discriminant = Math.Sqrt(trace * trace - 4 * determinant); + + double eigenvalue1 = (trace + discriminant) / 2; + double eigenvalue2 = (trace - discriminant) / 2; + + // 最大特征值对应的特征向量即为直线方向 + double directionX, directionY; + if (Math.Abs(covXY) > 1e-10) + { + directionX = covXY; + directionY = eigenvalue1 - covXX; + // 归一化 + double length = Math.Sqrt(directionX * directionX + directionY * directionY); + directionX /= length; + directionY /= length; + } + else + { + // 如果协方差为 0,则是水平或垂直直线 + directionX = (covXX >= covYY) ? 1 : 0; + directionY = (covXX >= covYY) ? 0 : 1; + } + + // 计算解释方差比例(拟合优度) + double totalVariance = eigenvalue1 + eigenvalue2; + double explainedVarianceRatio = (totalVariance > 1e-10) ? + Math.Max(eigenvalue1, eigenvalue2) / totalVariance : 1d; + + // 使用所有点计算端点 + double minProjection = double.MaxValue; + double maxProjection = double.MinValue; + + // 计算所有点在直线方向上的投影 + foreach (Point p in points) + { + // 相对于过滤点中心的投影 + double projection = (p.X - centerX) * directionX + (p.Y - centerY) * directionY; + minProjection = Math.Min(minProjection, projection); + maxProjection = Math.Max(maxProjection, projection); + } + + // 计算端点坐标 + endpoint1 = new Point( + centerX + minProjection * directionX, + centerY + minProjection * directionY + ); + + endpoint2 = new Point( + centerX + maxProjection * directionX, + centerY + maxProjection * directionY + ); + + // 使用解释方差比例作为判断条件 + double threshold = 0.998 + Settings.InkToShape.LineNormalizationThreshold / 500; + return explainedVarianceRatio > threshold; + } + + // New method: Determines if a stroke should be straightened into a line private bool ShouldStraightenLine(Stroke stroke) { + // 分辨率自适应阈值 Point start = stroke.StylusPoints.First().ToPoint(); Point end = stroke.StylusPoints.Last().ToPoint(); - double maxDeviation = 0; double lineLength = GetDistance(start, end); - // 分辨率自适应阈值 double adaptiveThreshold = Settings.Canvas.AutoStraightenLineThreshold * GetResolutionScale(); - // 如果线条太短,不进行拉直处理,使用自适应阈值 + + // 如果线条太短,不进行拉直处理 if (lineLength < adaptiveThreshold) { Debug.WriteLine($"线条太短: {lineLength} < {adaptiveThreshold}"); return false; } - // 新增:再次检查复杂度(双重保险) + // 检查复杂度 if (IsComplexShape(stroke)) { Debug.WriteLine("拒绝拉直:检测到复杂形状"); return false; } - // 新增:检查线条的直线度评分 - double straightnessScore = CalculateStraightnessScore(stroke); - double minStraightnessThreshold = 0.7; // 最低直线度要求 - - if (straightnessScore < minStraightnessThreshold) + Point endpoint1, endpoint2; + bool shouldStraighten = TryGetStraightLineEndpoints(stroke, out endpoint1, out endpoint2); + + if (shouldStraighten) { - Debug.WriteLine($"拒绝拉直:直线度评分过低 {straightnessScore:F3} < {minStraightnessThreshold}"); - return false; - } - - // 获取用户设置的灵敏度值,确保使用正确的值进行后续判断 - double sensitivity = Settings.InkToShape.LineStraightenSensitivity; - - // 输出详细的调试信息 - Debug.WriteLine($"ShouldStraightenLine - sensitivity: {sensitivity}, length: {lineLength}"); - - // 临时:显示调试消息框 - // MessageBox.Show($"灵敏度值: {sensitivity}", "调试信息"); - - // 计算点与直线的偏差 - double totalDeviation = 0; - int pointCount = 0; - - // 检查是否启用了高精度直线拉直 - bool useHighPrecision = Settings.Canvas.HighPrecisionLineStraighten; - - if (useHighPrecision) - { - Debug.WriteLine("使用高精度直线拉直模式"); - - // 高精度模式:每隔10像素取一个计数点 - double strokeLength = 0; - double sampleInterval = 10.0; // 10像素间隔 - - // 计算笔画的总长度,用于后续采样 - for (int i = 1; i < stroke.StylusPoints.Count; i++) - { - Point p1 = stroke.StylusPoints[i - 1].ToPoint(); - Point p2 = stroke.StylusPoints[i].ToPoint(); - strokeLength += GetDistance(p1, p2); - } - - // 如果笔画太短,直接使用所有点 - if (strokeLength < sampleInterval * 5) - { - foreach (StylusPoint sp in stroke.StylusPoints) - { - Point p = sp.ToPoint(); - double deviation = DistanceFromLineToPoint(start, end, p); - maxDeviation = Math.Max(maxDeviation, deviation); - totalDeviation += deviation; - pointCount++; - } - } - else - { - // 使用等距采样点 - double currentLength = 0; - double nextSampleAt = 0; - - // 总是包含起点 - Point lastPoint = start; - double deviation = DistanceFromLineToPoint(start, end, lastPoint); - maxDeviation = Math.Max(maxDeviation, deviation); - totalDeviation += deviation; - pointCount++; - - // 采样中间点 - for (int i = 1; i < stroke.StylusPoints.Count; i++) - { - Point currentPoint = stroke.StylusPoints[i].ToPoint(); - double segmentLength = GetDistance(lastPoint, currentPoint); - - // 如果这段线段跨越了下一个采样点 - while (currentLength + segmentLength >= nextSampleAt) - { - // 计算采样点在线段上的位置 - double t = (nextSampleAt - currentLength) / segmentLength; - Point samplePoint = new Point( - lastPoint.X + t * (currentPoint.X - lastPoint.X), - lastPoint.Y + t * (currentPoint.Y - lastPoint.Y) - ); - - // 计算采样点的偏差 - deviation = DistanceFromLineToPoint(start, end, samplePoint); - maxDeviation = Math.Max(maxDeviation, deviation); - totalDeviation += deviation; - pointCount++; - - // 设置下一个采样点位置 - nextSampleAt += sampleInterval; - - // 防止无限循环 - if (nextSampleAt > strokeLength) break; - } - - currentLength += segmentLength; - lastPoint = currentPoint; - } - - // 总是包含终点 - deviation = DistanceFromLineToPoint(start, end, end); - maxDeviation = Math.Max(maxDeviation, deviation); - totalDeviation += deviation; - pointCount++; - } + Debug.WriteLine($"接受拉直:判断为直线,解释方差比例满足阈值"); } else { - // 原始模式:使用所有点 - foreach (StylusPoint sp in stroke.StylusPoints) - { - Point p = sp.ToPoint(); - double deviation = DistanceFromLineToPoint(start, end, p); - maxDeviation = Math.Max(maxDeviation, deviation); - totalDeviation += deviation; - pointCount++; - } + Debug.WriteLine($"拒绝拉直:判断不满足直线条件"); } - // 计算平均偏差 - double avgDeviation = totalDeviation / pointCount; - - // 更详细的调试信息 - Debug.WriteLine($"Max deviation: {maxDeviation}, Avg: {avgDeviation}, Threshold: {sensitivity * lineLength}, Points: {pointCount}"); - - // 支持更广泛的灵敏度范围 (0.05-2.0) - - // 移除特殊的高灵敏度模式,使用统一的阈值计算逻辑 - - // 检查点分布的一致性 - 如果有些点偏离很大而其他点很接近直线,表明线条有明显弯曲 - double deviationVariance = 0; - - // 使用相同的高精度/原始模式来计算方差 - if (useHighPrecision) - { - // 高精度模式:重新采样计算方差 - double strokeLength = 0; - double sampleInterval = 10.0; // 10像素间隔 - - // 计算笔画的总长度,用于后续采样 - for (int i = 1; i < stroke.StylusPoints.Count; i++) - { - Point p1 = stroke.StylusPoints[i - 1].ToPoint(); - Point p2 = stroke.StylusPoints[i].ToPoint(); - strokeLength += GetDistance(p1, p2); - } - - // 如果笔画太短,直接使用所有点 - if (strokeLength < sampleInterval * 5) - { - foreach (StylusPoint sp in stroke.StylusPoints) - { - Point p = sp.ToPoint(); - double deviation = DistanceFromLineToPoint(start, end, p); - deviationVariance += Math.Pow(deviation - avgDeviation, 2); - } - } - else - { - // 使用等距采样点 - double currentLength = 0; - double nextSampleAt = 0; - Point lastPoint = start; - - // 起点方差 - double deviation = DistanceFromLineToPoint(start, end, lastPoint); - deviationVariance += Math.Pow(deviation - avgDeviation, 2); - - // 采样中间点 - for (int i = 1; i < stroke.StylusPoints.Count; i++) - { - Point currentPoint = stroke.StylusPoints[i].ToPoint(); - double segmentLength = GetDistance(lastPoint, currentPoint); - - // 如果这段线段跨越了下一个采样点 - while (currentLength + segmentLength >= nextSampleAt) - { - // 计算采样点在线段上的位置 - double t = (nextSampleAt - currentLength) / segmentLength; - Point samplePoint = new Point( - lastPoint.X + t * (currentPoint.X - lastPoint.X), - lastPoint.Y + t * (currentPoint.Y - lastPoint.Y) - ); - - // 计算采样点的方差 - deviation = DistanceFromLineToPoint(start, end, samplePoint); - deviationVariance += Math.Pow(deviation - avgDeviation, 2); - - // 设置下一个采样点位置 - nextSampleAt += sampleInterval; - - // 防止无限循环 - if (nextSampleAt > strokeLength) break; - } - - currentLength += segmentLength; - lastPoint = currentPoint; - } - - // 终点方差 - deviation = DistanceFromLineToPoint(start, end, end); - deviationVariance += Math.Pow(deviation - avgDeviation, 2); - } - } - else - { - // 原始模式:使用所有点计算方差 - foreach (StylusPoint sp in stroke.StylusPoints) - { - Point p = sp.ToPoint(); - double deviation = DistanceFromLineToPoint(start, end, p); - deviationVariance += Math.Pow(deviation - avgDeviation, 2); - } - } - - deviationVariance /= pointCount; - - // 输出更多调试信息 - Debug.WriteLine($"Deviation variance: {deviationVariance}, Threshold: {sensitivity * lineLength * 0.05}"); - - // 修复灵敏度逻辑:灵敏度越大,容许的偏差越大,更容易将线条识别为直线 - // 将灵敏度转换为阈值:灵敏度0.05-1.0映射到阈值0.01-0.2 - double threshold = Math.Max(0.01, sensitivity * 0.2); // 确保最小阈值为0.01 - - if ((maxDeviation / lineLength) > threshold) - { - Debug.WriteLine($"拒绝拉直:最大偏差过大 {maxDeviation / lineLength:F3} > {threshold:F3}"); - return false; - } - - // 如果偏差方差大,说明线条弯曲不均匀 - // 灵敏度越大,容许的偏差方差越大 - double varianceThreshold = threshold * lineLength * 0.25; // 调整方差阈值比例 - if (deviationVariance > varianceThreshold) - { - Debug.WriteLine($"拒绝拉直:偏差方差过大 {deviationVariance:F3} > {varianceThreshold:F3}"); - return false; - } - - // 检查中点偏离情况 - 针对弧形线条特别有效 - if (stroke.StylusPoints.Count > 10) - { - int midIndex = stroke.StylusPoints.Count / 2; - Point midPoint = stroke.StylusPoints[midIndex].ToPoint(); - double midDeviation = DistanceFromLineToPoint(start, end, midPoint); - - // 输出中点偏差信息 - double midThreshold = lineLength * threshold * 0.8; - Debug.WriteLine($"Mid deviation: {midDeviation:F3}, Threshold: {midThreshold:F3}"); - - // 如果中点偏离过大,不拉直 - // 使用调整后的阈值,灵敏度越大,容许的中点偏离越大 - if (midDeviation > midThreshold) - { - Debug.WriteLine($"拒绝拉直:中点偏差过大 {midDeviation:F3} > {midThreshold:F3}"); - return false; - } - } - - Debug.WriteLine($"接受拉直:直线度评分 = {straightnessScore:F3}"); - return true; + return shouldStraighten; } /// diff --git a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs index e0878d90..31525760 100644 --- a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs +++ b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs @@ -391,12 +391,20 @@ namespace Ink_Canvas { var stroke = GetStrokeVisual(e.StylusDevice.Id).Stroke; - inkCanvas.Strokes.Add(stroke); - await Task.Delay(5); - inkCanvas.Children.Remove(GetVisualCanvas(e.StylusDevice.Id)); + if (stroke != null) + { + inkCanvas.Strokes.Add(stroke); + await Task.Delay(5); + inkCanvas.Children.Remove(GetVisualCanvas(e.StylusDevice.Id)); - inkCanvas_StrokeCollected(inkCanvas, - new InkCanvasStrokeCollectedEventArgs(stroke)); + inkCanvas_StrokeCollected(inkCanvas, + new InkCanvasStrokeCollectedEventArgs(stroke)); + } + else + { + await Task.Delay(5); + inkCanvas.Children.Remove(GetVisualCanvas(e.StylusDevice.Id)); + } } catch (Exception ex) { diff --git a/Ink Canvas/Properties/AssemblyInfo.cs b/Ink Canvas/Properties/AssemblyInfo.cs index e30061e1..87f44273 100644 --- a/Ink Canvas/Properties/AssemblyInfo.cs +++ b/Ink Canvas/Properties/AssemblyInfo.cs @@ -49,5 +49,5 @@ using System.Windows; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.7.17.2")] -[assembly: AssemblyFileVersion("1.7.17.2")] +[assembly: AssemblyVersion("1.7.18.0")] +[assembly: AssemblyFileVersion("1.7.18.0")] diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index de4c9f4d..5ef12f77 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -619,7 +619,9 @@ namespace Ink_Canvas [JsonProperty("isInkToShapeRounded")] public bool IsInkToShapeRounded { get; set; } = true; [JsonProperty("lineStraightenSensitivity")] - public double LineStraightenSensitivity { get; set; } = 0.20; // 直线检测灵敏度,值越小越严格(0.05-2.0) + public double LineStraightenSensitivity { get; set; } = 0.20; + [JsonProperty("lineNormalizationThreshold")] + public double LineNormalizationThreshold { get; set; } = 0.5; } public class RandSettings @@ -663,9 +665,9 @@ namespace Ink_Canvas [JsonProperty("enableMLAvoidance")] public bool EnableMLAvoidance { get; set; } = true; [JsonProperty("mlAvoidanceHistoryCount")] - public int MLAvoidanceHistoryCount { get; set; } = 20; + public int MLAvoidanceHistoryCount { get; set; } = 50; [JsonProperty("mlAvoidanceWeight")] - public double MLAvoidanceWeight { get; set; } = 0.8; + public double MLAvoidanceWeight { get; set; } = 1.0; [JsonProperty("enableQuickDraw")] public bool EnableQuickDraw { get; set; } = true; } diff --git a/Ink Canvas/Windows/CountdownTimerWindow.xaml b/Ink Canvas/Windows/CountdownTimerWindow.xaml index 093f416b..63008641 100644 --- a/Ink Canvas/Windows/CountdownTimerWindow.xaml +++ b/Ink Canvas/Windows/CountdownTimerWindow.xaml @@ -8,6 +8,7 @@ xmlns:processbars="clr-namespace:Ink_Canvas.ProcessBars" ui:ThemeManager.RequestedTheme="Light" Topmost="True" Background="Transparent" mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True" + ResizeMode="CanMinimize" Loaded="Window_Loaded" Closing="Window_Closing" WindowStartupLocation="CenterScreen" Title="Ink Canvas 画板 - 计时器" Height="700" Width="1100"> diff --git a/Ink Canvas/Windows/CountdownTimerWindow.xaml.cs b/Ink Canvas/Windows/CountdownTimerWindow.xaml.cs index a4d1cbae..1fd31fd5 100644 --- a/Ink Canvas/Windows/CountdownTimerWindow.xaml.cs +++ b/Ink Canvas/Windows/CountdownTimerWindow.xaml.cs @@ -29,14 +29,7 @@ namespace Ink_Canvas public static Window CreateTimerWindow() { - if (MainWindow.Settings.RandSettings?.UseNewStyleUI == true) - { - return new NewStyleTimerWindow(); - } - else - { - return new CountdownTimerWindow(); - } + return new CountdownTimerWindow(); } private void Timer_Elapsed(object sender, ElapsedEventArgs e) diff --git a/Ink Canvas/Windows/FullscreenTimerWindow.xaml b/Ink Canvas/Windows/FullscreenTimerWindow.xaml index 115730bf..fda1bb4d 100644 --- a/Ink Canvas/Windows/FullscreenTimerWindow.xaml +++ b/Ink Canvas/Windows/FullscreenTimerWindow.xaml @@ -1,9 +1,9 @@ - /// 全屏计时器窗口 /// public partial class FullscreenTimerWindow : Window { - private NewStyleTimerWindow parentWindow; + private TimerControl parentControl; private System.Timers.Timer updateTimer; + private Visibility previousTimerContainerVisibility = Visibility.Visible; - public FullscreenTimerWindow(NewStyleTimerWindow parent) + public FullscreenTimerWindow(TimerControl parent) { InitializeComponent(); - parentWindow = parent; + parentControl = parent; - // 设置窗口位置和大小 this.Left = 0; this.Top = 0; this.Width = SystemParameters.PrimaryScreenWidth; this.Height = SystemParameters.PrimaryScreenHeight; - // 启动更新定时器 updateTimer = new System.Timers.Timer(100); updateTimer.Elapsed += UpdateTimer_Elapsed; updateTimer.Start(); - parentWindow.TimerCompleted += ParentWindow_TimerCompleted; + parentControl.TimerCompleted += ParentWindow_TimerCompleted; + + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + mainWindow.PauseTopmostMaintenance(); + + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + if (timerContainer != null) + { + previousTimerContainerVisibility = timerContainer.Visibility; + timerContainer.Visibility = Visibility.Collapsed; + } + } + + // 确保窗口置顶 + Loaded += FullscreenTimerWindow_Loaded; } + + private void FullscreenTimerWindow_Loaded(object sender, RoutedEventArgs e) + { + // 使用延迟确保窗口完全加载后再应用置顶 + Dispatcher.BeginInvoke(new Action(() => + { + ApplyTopmost(); + }), System.Windows.Threading.DispatcherPriority.Loaded); + } + + #region Win32 API 声明和置顶管理 + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll")] + private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + private const int GWL_EXSTYLE = -20; + private const int WS_EX_TOPMOST = 0x00000008; + private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + private const uint SWP_NOMOVE = 0x0002; + private const uint SWP_NOSIZE = 0x0001; + private const uint SWP_NOACTIVATE = 0x0010; + private const uint SWP_SHOWWINDOW = 0x0040; + + /// + /// 应用全屏窗口置顶 + /// + private void ApplyTopmost() + { + try + { + var hwnd = new WindowInteropHelper(this).Handle; + if (hwnd == IntPtr.Zero) return; + + // 设置WPF的Topmost属性 + Topmost = true; + + // 使用Win32 API强制置顶 + int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST); + + // 使用SetWindowPos确保窗口在最顶层 + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"应用全屏窗口置顶失败: {ex.Message}"); + } + } + #endregion private void UpdateTimer_Elapsed(object sender, ElapsedEventArgs e) { - if (parentWindow != null) + if (parentControl != null) { Application.Current.Dispatcher.Invoke(() => { @@ -54,16 +126,16 @@ namespace Ink_Canvas private bool ShouldCloseWindow() { - if (parentWindow == null) return true; + if (parentControl == null) return true; if (MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true) { - if (parentWindow.IsTimerRunning) + if (parentControl.IsTimerRunning) { return false; } - var remainingTime = parentWindow.GetRemainingTime(); + var remainingTime = parentControl.GetRemainingTime(); if (remainingTime.HasValue && remainingTime.Value.TotalSeconds < 0) { return false; @@ -73,16 +145,15 @@ namespace Ink_Canvas } else { - return !parentWindow.IsTimerRunning; + return !parentControl.IsTimerRunning; } } private void UpdateTimeDisplay() { - if (parentWindow == null) return; + if (parentControl == null) return; - // 获取剩余时间 - var remainingTime = parentWindow.GetRemainingTime(); + var remainingTime = parentControl.GetRemainingTime(); if (remainingTime.HasValue) { var timeSpan = remainingTime.Value; @@ -93,10 +164,10 @@ namespace Ink_Canvas if (isOvertimeMode) { - var totalTimeSpan = parentWindow.GetTotalTimeSpan(); + var totalTimeSpan = parentControl.GetTotalTimeSpan(); if (totalTimeSpan.HasValue) { - var elapsedTime = parentWindow.GetElapsedTime(); + var elapsedTime = parentControl.GetElapsedTime(); if (elapsedTime.HasValue) { var overtimeSpan = elapsedTime.Value - totalTimeSpan.Value; @@ -128,11 +199,9 @@ namespace Ink_Canvas SetDigitDisplay("FullHour1Display", Math.Abs(hours / 10) % 10, shouldShowRed); SetDigitDisplay("FullHour2Display", (hours % 10 + 10) % 10, shouldShowRed); - // 更新分钟显示 SetDigitDisplay("FullMinute1Display", minutes / 10, shouldShowRed); SetDigitDisplay("FullMinute2Display", minutes % 10, shouldShowRed); - // 更新秒显示 SetDigitDisplay("FullSecond1Display", seconds / 10, shouldShowRed); SetDigitDisplay("FullSecond2Display", seconds % 10, shouldShowRed); @@ -223,23 +292,32 @@ namespace Ink_Canvas private void ExitFullscreen() { - // 恢复主窗口 - if (parentWindow != null) - { - // 清除全屏模式标志 - parentWindow.SetFullscreenMode(false); - parentWindow.Show(); - parentWindow.Activate(); - parentWindow.WindowState = WindowState.Normal; - } this.Close(); } protected override void OnClosed(EventArgs e) { - if (parentWindow != null) + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) { - parentWindow.TimerCompleted -= ParentWindow_TimerCompleted; + mainWindow.ResumeTopmostMaintenance(); + + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + if (timerContainer != null && previousTimerContainerVisibility == Visibility.Visible) + { + timerContainer.Visibility = Visibility.Visible; + + // 重置5秒最小化计时 + if (parentControl != null) + { + parentControl.UpdateActivityTime(); + } + } + } + + if (parentControl != null) + { + parentControl.TimerCompleted -= ParentWindow_TimerCompleted; } // 清理资源 diff --git a/Ink Canvas/Windows/MinimizedTimerWindow.xaml b/Ink Canvas/Windows/MinimizedTimerControl.xaml similarity index 85% rename from Ink Canvas/Windows/MinimizedTimerWindow.xaml rename to Ink Canvas/Windows/MinimizedTimerControl.xaml index 08b6f91c..d3b08ecf 100644 --- a/Ink Canvas/Windows/MinimizedTimerWindow.xaml +++ b/Ink Canvas/Windows/MinimizedTimerControl.xaml @@ -1,23 +1,17 @@ - - - + + - + + SnapsToDevicePixels="True" + MouseLeftButtonDown="MainBorder_MouseLeftButtonDown" + Cursor="Hand"> @@ -122,4 +118,5 @@ - + + diff --git a/Ink Canvas/Windows/MinimizedTimerControl.xaml.cs b/Ink Canvas/Windows/MinimizedTimerControl.xaml.cs new file mode 100644 index 00000000..603db416 --- /dev/null +++ b/Ink Canvas/Windows/MinimizedTimerControl.xaml.cs @@ -0,0 +1,579 @@ +using System; +using System.Timers; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.Win32; +using iNKORE.UI.WPF.Modern; + +namespace Ink_Canvas.Windows +{ + /// + /// 最小化计时器窗口 + /// + public partial class MinimizedTimerControl : UserControl + { + private TimerControl parentControl; + private System.Timers.Timer updateTimer; + + public MinimizedTimerControl() + { + InitializeComponent(); + + updateTimer = new System.Timers.Timer(100); + updateTimer.Elapsed += UpdateTimer_Elapsed; + updateTimer.Start(); + + ApplyTheme(); + + // 监听主题变化事件 + SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged; + + Unloaded += MinimizedTimerControl_Unloaded; + } + + private void MinimizedTimerControl_Unloaded(object sender, RoutedEventArgs e) + { + // 取消订阅主题变化事件 + SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged; + + if (parentControl != null) + { + parentControl.TimerCompleted -= ParentControl_TimerCompleted; + } + + if (updateTimer != null) + { + updateTimer.Stop(); + updateTimer.Dispose(); + } + } + + private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) + { + // 当主题变化时,重新应用主题 + Application.Current.Dispatcher.Invoke(() => + { + RefreshTheme(); + }); + } + + /// + /// 刷新主题 + /// + public void RefreshTheme() + { + try + { + // 重新应用主题 + ApplyTheme(); + + // 强制刷新UI + InvalidateVisual(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"刷新最小化计时器窗口主题出错: {ex.Message}"); + } + } + + public void SetParentControl(TimerControl parent) + { + if (parentControl != null) + { + parentControl.TimerCompleted -= ParentControl_TimerCompleted; + } + + parentControl = parent; + + if (parentControl != null) + { + parentControl.TimerCompleted += ParentControl_TimerCompleted; + UpdateTimeDisplay(); + } + } + + private void UpdateTimer_Elapsed(object sender, ElapsedEventArgs e) + { + if (parentControl != null) + { + Application.Current.Dispatcher.Invoke(() => + { + if (this.Visibility != Visibility.Visible) + { + return; + } + + if (ShouldHide()) + { + this.Visibility = Visibility.Collapsed; + var parent = this.Parent as FrameworkElement; + if (parent != null) + { + parent.Visibility = Visibility.Collapsed; + } + return; + } + + UpdateTimeDisplay(); + }); + } + } + + private bool ShouldHide() + { + if (parentControl == null) return true; + + if (parentControl.IsFullscreenWindowOpen) + { + return true; + } + + if (MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true) + { + if (parentControl.IsTimerRunning) + { + return false; + } + + var remainingTime = parentControl.GetRemainingTime(); + if (remainingTime.HasValue && remainingTime.Value.TotalSeconds < 0) + { + return false; + } + + return true; + } + else + { + return !parentControl.IsTimerRunning; + } + } + + private void UpdateTimeDisplay() + { + if (parentControl == null) return; + + var remainingTime = parentControl.GetRemainingTime(); + if (remainingTime.HasValue) + { + var timeSpan = remainingTime.Value; + bool isOvertimeMode = timeSpan.TotalSeconds < 0; + bool shouldShowRed = isOvertimeMode && MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true; + + int hours, minutes, seconds; + + if (isOvertimeMode) + { + var totalTimeSpan = parentControl.GetTotalTimeSpan(); + if (totalTimeSpan.HasValue) + { + var elapsedTime = parentControl.GetElapsedTime(); + if (elapsedTime.HasValue) + { + var overtimeSpan = elapsedTime.Value - totalTimeSpan.Value; + hours = (int)overtimeSpan.TotalHours; + minutes = overtimeSpan.Minutes; + seconds = overtimeSpan.Seconds; + } + else + { + hours = 0; + minutes = 0; + seconds = 0; + } + } + else + { + hours = 0; + minutes = 0; + seconds = 0; + } + } + else + { + hours = (int)timeSpan.TotalHours; + minutes = timeSpan.Minutes; + seconds = timeSpan.Seconds; + } + + SetDigitDisplay("MinHour1Display", Math.Abs(hours / 10) % 10, shouldShowRed); + SetDigitDisplay("MinHour2Display", (hours % 10 + 10) % 10, shouldShowRed); + + SetDigitDisplay("MinMinute1Display", minutes / 10, shouldShowRed); + SetDigitDisplay("MinMinute2Display", minutes % 10, shouldShowRed); + + SetDigitDisplay("MinSecond1Display", seconds / 10, shouldShowRed); + SetDigitDisplay("MinSecond2Display", seconds % 10, shouldShowRed); + + SetColonDisplay(shouldShowRed); + } + } + + private void ParentControl_TimerCompleted(object sender, EventArgs e) + { + Application.Current.Dispatcher.Invoke(() => + { + Visibility = Visibility.Collapsed; + }); + } + + private void SetDigitDisplay(string pathName, int digit, bool isRed = false) + { + var path = this.FindName(pathName) as Path; + if (path != null) + { + string resourceKey = $"Digit{digit}"; + var geometry = this.FindResource(resourceKey) as Geometry; + if (geometry != null) + { + path.Data = geometry; + } + + if (isRed) + { + path.Fill = Brushes.Red; + } + else + { + var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush; + if (defaultBrush != null) + { + path.Fill = defaultBrush; + } + else + { + bool isLightTheme = IsLightTheme(); + path.Fill = isLightTheme ? Brushes.Black : Brushes.White; + } + } + } + } + + private void SetColonDisplay(bool isRed = false) + { + var colon1 = this.FindName("MinColon1Display") as TextBlock; + var colon2 = this.FindName("MinColon2Display") as TextBlock; + + if (colon1 != null) + { + if (isRed) + { + colon1.Foreground = Brushes.Red; + } + else + { + var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush; + if (defaultBrush != null) + { + colon1.Foreground = defaultBrush; + } + else + { + bool isLightTheme = IsLightTheme(); + colon1.Foreground = isLightTheme ? Brushes.Black : Brushes.White; + } + } + } + + if (colon2 != null) + { + if (isRed) + { + colon2.Foreground = Brushes.Red; + } + else + { + var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush; + if (defaultBrush != null) + { + colon2.Foreground = defaultBrush; + } + else + { + bool isLightTheme = IsLightTheme(); + colon2.Foreground = isLightTheme ? Brushes.Black : Brushes.White; + } + } + } + } + + private void ApplyTheme() + { + try + { + if (MainWindow.Settings != null) + { + ApplyTheme(MainWindow.Settings); + } + else + { + bool isLightTheme = IsLightTheme(); + if (!isLightTheme) + { + SetDarkThemeBorder(); + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"应用主题时出错: {ex.Message}"); + } + } + + private void ApplyTheme(Settings settings) + { + try + { + if (settings.Appearance.Theme == 0) // 浅色主题 + { + ThemeManager.SetRequestedTheme(this, ElementTheme.Light); + } + else if (settings.Appearance.Theme == 1) // 深色主题 + { + ThemeManager.SetRequestedTheme(this, ElementTheme.Dark); + SetDarkThemeBorder(); + } + else // 跟随系统主题 + { + bool isSystemLight = IsSystemThemeLight(); + if (isSystemLight) + { + ThemeManager.SetRequestedTheme(this, ElementTheme.Light); + } + else + { + ThemeManager.SetRequestedTheme(this, ElementTheme.Dark); + SetDarkThemeBorder(); + } + } + + // 刷新数字和冒号显示的颜色 + if (parentControl != null) + { + UpdateTimeDisplay(); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"应用最小化计时器窗口主题出错: {ex.Message}"); + } + } + + private bool IsSystemThemeLight() + { + var light = false; + try + { + var registryKey = Microsoft.Win32.Registry.CurrentUser; + var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + if (themeKey != null) + { + var value = themeKey.GetValue("AppsUseLightTheme"); + if (value != null) + { + light = (int)value == 1; + } + themeKey.Close(); + } + } + catch + { + // 如果读取注册表失败,默认为浅色主题 + light = true; + } + return light; + } + + private bool IsLightTheme() + { + try + { + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + var currentModeField = mainWindow.GetType().GetField("currentMode", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (currentModeField != null) + { + var currentMode = currentModeField.GetValue(mainWindow); + return currentMode?.ToString() == "Light"; + } + } + } + catch + { + } + return true; + } + + private void SetDarkThemeBorder() + { + try + { + var border = this.FindName("MainBorder") as Border; + if (border != null) + { + border.BorderBrush = new SolidColorBrush(Color.FromRgb(64, 64, 64)); + } + } + catch + { + } + } + + private void CloseButton_Click(object sender, RoutedEventArgs e) + { + if (parentControl != null) + { + parentControl.StopTimer(); + } + Visibility = Visibility.Collapsed; + } + + private bool isDragging = false; + private bool isDragStarted = false; + private Point dragStartPoint; + private Point containerStartPosition; + private const double DragThreshold = 5.0; // 拖动阈值,像素 + + private void MainBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (e.ClickCount == 2) + { + // 双击:恢复主窗口 + if (parentControl != null) + { + parentControl.UpdateActivityTime(); + + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement; + + if (timerContainer != null && minimizedContainer != null) + { + timerContainer.Visibility = Visibility.Visible; + minimizedContainer.Visibility = Visibility.Collapsed; + } + } + } + e.Handled = true; + } + else if (e.ClickCount == 1) + { + // 单击:准备拖动或点击 + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement; + if (minimizedContainer != null) + { + var point = e.GetPosition(minimizedContainer); + var mainWindowPoint = minimizedContainer.TransformToAncestor(mainWindow).Transform(point); + + // 初始化拖动状态,但不立即开始拖动 + isDragging = false; + isDragStarted = false; + dragStartPoint = mainWindowPoint; + + var margin = minimizedContainer.Margin; + containerStartPosition = new Point(margin.Left, margin.Top); + + if (double.IsNaN(containerStartPosition.X) || containerStartPosition.X < 0) containerStartPosition.X = 0; + if (double.IsNaN(containerStartPosition.Y) || containerStartPosition.Y < 0) containerStartPosition.Y = 0; + + // 捕获鼠标并订阅事件,等待判断是拖动还是点击 + minimizedContainer.CaptureMouse(); + minimizedContainer.MouseMove += MinimizedContainer_MouseMove; + minimizedContainer.MouseLeftButtonUp += MinimizedContainer_MouseLeftButtonUp; + e.Handled = true; + } + } + } + } + + private void MinimizedContainer_MouseMove(object sender, MouseEventArgs e) + { + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow == null) return; + + var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement; + if (minimizedContainer == null) return; + + var currentPoint = e.GetPosition(mainWindow); + var deltaX = currentPoint.X - dragStartPoint.X; + var deltaY = currentPoint.Y - dragStartPoint.Y; + var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + + // 如果移动距离超过阈值,开始拖动 + if (!isDragStarted && distance > DragThreshold) + { + isDragStarted = true; + isDragging = true; + } + + // 如果已经开始拖动,更新位置 + if (isDragging) + { + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + + var newX = containerStartPosition.X + deltaX; + var newY = containerStartPosition.Y + deltaY; + + if (newX < 0) newX = 0; + if (newY < 0) newY = 0; + + minimizedContainer.Margin = new Thickness(newX, newY, 0, 0); + minimizedContainer.HorizontalAlignment = HorizontalAlignment.Left; + minimizedContainer.VerticalAlignment = VerticalAlignment.Top; + + if (timerContainer != null) + { + timerContainer.Margin = new Thickness(newX, newY, 0, 0); + timerContainer.HorizontalAlignment = HorizontalAlignment.Left; + timerContainer.VerticalAlignment = VerticalAlignment.Top; + } + } + } + + private void MinimizedContainer_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow == null) return; + + var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement; + if (minimizedContainer != null) + { + minimizedContainer.ReleaseMouseCapture(); + minimizedContainer.MouseMove -= MinimizedContainer_MouseMove; + minimizedContainer.MouseLeftButtonUp -= MinimizedContainer_MouseLeftButtonUp; + } + + // 如果没有开始拖动(移动距离小于阈值),则视为单击,恢复主窗口 + if (!isDragStarted) + { + if (parentControl != null) + { + parentControl.UpdateActivityTime(); + + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + if (timerContainer != null && minimizedContainer != null) + { + timerContainer.Visibility = Visibility.Visible; + minimizedContainer.Visibility = Visibility.Collapsed; + } + } + } + + isDragging = false; + isDragStarted = false; + } + + } +} + + diff --git a/Ink Canvas/Windows/MinimizedTimerWindow.xaml.cs b/Ink Canvas/Windows/MinimizedTimerWindow.xaml.cs deleted file mode 100644 index 10b8a3aa..00000000 --- a/Ink Canvas/Windows/MinimizedTimerWindow.xaml.cs +++ /dev/null @@ -1,431 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Shapes; -using System.Windows.Media.Animation; - -namespace Ink_Canvas -{ - /// - /// 最小化计时器窗口 - /// - public partial class MinimizedTimerWindow : Window - { - private NewStyleTimerWindow parentWindow; - private System.Timers.Timer updateTimer; - private bool isMouseOver = false; - private bool isDragging = false; - private Point lastMousePosition; - - public MinimizedTimerWindow(NewStyleTimerWindow parent) - { - InitializeComponent(); - parentWindow = parent; - - // 设置窗口位置 - this.Left = parent.Left; - this.Top = parent.Top; - - // 根据分辨率和DPI缩放窗口 - ScaleWindowForResolution(); - - // 启动更新定时器 - updateTimer = new System.Timers.Timer(100); // 100ms更新一次 - updateTimer.Elapsed += UpdateTimer_Elapsed; - updateTimer.Start(); - - parentWindow.TimerCompleted += ParentWindow_TimerCompleted; - - // 应用主题 - ApplyTheme(); - } - - /// - /// 根据屏幕分辨率和 DPI 缩放窗口大小(保持原始尺寸,使用Transform缩放) - /// - private void ScaleWindowForResolution() - { - try - { - // 获取屏幕尺寸(考虑 DPI 缩放) - double screenWidth = SystemParameters.PrimaryScreenWidth; - double screenHeight = SystemParameters.PrimaryScreenHeight; - - // 基准分辨率(1920x1080) - const double baseWidth = 1920.0; - const double baseHeight = 1080.0; - - // 计算缩放比例(使用较小的比例以保持比例) - double scaleX = screenWidth / baseWidth; - double scaleY = screenHeight / baseHeight; - double scale = Math.Min(scaleX, scaleY); - - // 限制最小和最大缩放,避免过小或过大 - scale = Math.Max(0.5, Math.Min(2.0, scale)); - - // 应用缩放变换到整个窗口内容 - var scaleTransform = this.FindName("WindowScaleTransform") as ScaleTransform; - if (scaleTransform != null) - { - scaleTransform.ScaleX = scale; - scaleTransform.ScaleY = scale; - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"缩放窗口大小时出错: {ex.Message}"); - } - } - - private void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) - { - if (parentWindow != null) - { - Application.Current.Dispatcher.Invoke(() => - { - if (ShouldCloseWindow()) - { - this.Close(); - return; - } - - UpdateTimeDisplay(); - }); - } - } - - private bool ShouldCloseWindow() - { - if (parentWindow == null) return true; - - if (MainWindow.Settings.RandSettings?.EnableOvertimeCountUp == true) - { - if (parentWindow.IsTimerRunning) - { - return false; - } - - var remainingTime = parentWindow.GetRemainingTime(); - if (remainingTime.HasValue && remainingTime.Value.TotalSeconds < 0) - { - return false; - } - - return true; - } - else - { - return !parentWindow.IsTimerRunning; - } - } - - private void UpdateTimeDisplay() - { - if (parentWindow == null) return; - - // 获取剩余时间 - var remainingTime = parentWindow.GetRemainingTime(); - if (remainingTime.HasValue) - { - var timeSpan = remainingTime.Value; - bool isOvertimeMode = timeSpan.TotalSeconds < 0; - bool shouldShowRed = isOvertimeMode && MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true; - - int hours, minutes, seconds; - - if (isOvertimeMode) - { - var totalTimeSpan = parentWindow.GetTotalTimeSpan(); - if (totalTimeSpan.HasValue) - { - var elapsedTime = parentWindow.GetElapsedTime(); - if (elapsedTime.HasValue) - { - var overtimeSpan = elapsedTime.Value - totalTimeSpan.Value; - hours = (int)overtimeSpan.TotalHours; - minutes = overtimeSpan.Minutes; - seconds = overtimeSpan.Seconds; - } - else - { - hours = 0; - minutes = 0; - seconds = 0; - } - } - else - { - hours = 0; - minutes = 0; - seconds = 0; - } - } - else - { - hours = (int)timeSpan.TotalHours; - minutes = timeSpan.Minutes; - seconds = timeSpan.Seconds; - } - - SetDigitDisplay("MinHour1Display", Math.Abs(hours / 10) % 10, shouldShowRed); - SetDigitDisplay("MinHour2Display", (hours % 10 + 10) % 10, shouldShowRed); - - // 更新分钟显示 - SetDigitDisplay("MinMinute1Display", minutes / 10, shouldShowRed); - SetDigitDisplay("MinMinute2Display", minutes % 10, shouldShowRed); - - // 更新秒显示 - SetDigitDisplay("MinSecond1Display", seconds / 10, shouldShowRed); - SetDigitDisplay("MinSecond2Display", seconds % 10, shouldShowRed); - - SetColonDisplay(shouldShowRed); - } - } - - private void ParentWindow_TimerCompleted(object sender, EventArgs e) - { - Application.Current.Dispatcher.Invoke(() => - { - this.Close(); - }); - } - - private void SetDigitDisplay(string pathName, int digit, bool isRed = false) - { - var path = this.FindName(pathName) as Path; - if (path != null) - { - string resourceKey = $"Digit{digit}"; - var geometry = this.FindResource(resourceKey) as Geometry; - if (geometry != null) - { - path.Data = geometry; - } - - // 设置颜色 - if (isRed) - { - path.Fill = Brushes.Red; - } - else - { - var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush; - if (defaultBrush != null) - { - path.Fill = defaultBrush; - } - else - { - bool isLightTheme = IsLightTheme(); - path.Fill = isLightTheme ? Brushes.Black : Brushes.White; - } - } - } - } - - /// - /// 设置最小化窗口冒号显示颜色 - /// - /// 是否显示为红色 - private void SetColonDisplay(bool isRed = false) - { - var colon1 = this.FindName("MinColon1Display") as TextBlock; - var colon2 = this.FindName("MinColon2Display") as TextBlock; - - if (colon1 != null) - { - if (isRed) - { - colon1.Foreground = Brushes.Red; - } - else - { - var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush; - if (defaultBrush != null) - { - colon1.Foreground = defaultBrush; - } - else - { - bool isLightTheme = IsLightTheme(); - colon1.Foreground = isLightTheme ? Brushes.Black : Brushes.White; - } - } - } - - if (colon2 != null) - { - if (isRed) - { - colon2.Foreground = Brushes.Red; - } - else - { - var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush; - if (defaultBrush != null) - { - colon2.Foreground = defaultBrush; - } - else - { - bool isLightTheme = IsLightTheme(); - colon2.Foreground = isLightTheme ? Brushes.Black : Brushes.White; - } - } - } - } - - private void ApplyTheme() - { - try - { - - bool isLightTheme = IsLightTheme(); - if (!isLightTheme) - { - SetDarkThemeBorder(); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"应用主题时出错: {ex.Message}"); - } - } - - private bool IsLightTheme() - { - try - { - var mainWindow = Application.Current.MainWindow as MainWindow; - if (mainWindow != null) - { - var currentModeField = mainWindow.GetType().GetField("currentMode", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - if (currentModeField != null) - { - var currentMode = currentModeField.GetValue(mainWindow); - return currentMode?.ToString() == "Light"; - } - } - } - catch - { - // 如果获取主题失败,默认使用浅色主题 - } - return true; - } - - // 设置深色主题下的灰色边框 - private void SetDarkThemeBorder() - { - try - { - // 找到Border元素并设置灰色边框 - var border = this.FindName("MainBorder") as Border; - if (border != null) - { - border.BorderBrush = new SolidColorBrush(Color.FromRgb(64, 64, 64)); - } - } - catch - { - } - } - - private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - // 记录点击时间 - lastClickTime = DateTime.Now; - // 开始拖动 - isDragging = true; - lastMousePosition = e.GetPosition(this); - this.CaptureMouse(); - } - - private void Window_MouseMove(object sender, MouseEventArgs e) - { - if (isDragging) - { - var currentPosition = e.GetPosition(this); - var deltaX = currentPosition.X - lastMousePosition.X; - var deltaY = currentPosition.Y - lastMousePosition.Y; - - this.Left += deltaX; - this.Top += deltaY; - } - } - - private void Window_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) - { - if (isDragging) - { - isDragging = false; - this.ReleaseMouseCapture(); - - // 如果点击时间很短,认为是单击,恢复主窗口 - var clickDuration = DateTime.Now - lastClickTime; - if (clickDuration.TotalMilliseconds < 200) // 200ms内认为是单击 - { - // 恢复主窗口 - if (parentWindow != null) - { - parentWindow.Show(); - parentWindow.Activate(); - parentWindow.WindowState = WindowState.Normal; - this.Close(); - } - } - } - } - - private DateTime lastClickTime = DateTime.Now; - - private void Window_MouseEnter(object sender, MouseEventArgs e) - { - isMouseOver = true; - // 鼠标进入时显示关闭按钮 - if (CloseButton != null) - { - CloseButton.Opacity = 1.0; - } - } - - private void Window_MouseLeave(object sender, MouseEventArgs e) - { - isMouseOver = false; - // 鼠标离开时隐藏关闭按钮 - if (CloseButton != null) - { - CloseButton.Opacity = 0.7; - } - } - - private void CloseButton_Click(object sender, RoutedEventArgs e) - { - // 停止计时器并关闭窗口 - if (parentWindow != null) - { - parentWindow.StopTimer(); - } - this.Close(); - } - - protected override void OnClosed(EventArgs e) - { - if (parentWindow != null) - { - parentWindow.TimerCompleted -= ParentWindow_TimerCompleted; - } - - // 清理资源 - if (updateTimer != null) - { - updateTimer.Stop(); - updateTimer.Dispose(); - } - base.OnClosed(e); - } - } -} diff --git a/Ink Canvas/Windows/NewStyleRollCallWindow.cs b/Ink Canvas/Windows/NewStyleRollCallWindow.cs index b80090d6..adc03650 100644 --- a/Ink Canvas/Windows/NewStyleRollCallWindow.cs +++ b/Ink Canvas/Windows/NewStyleRollCallWindow.cs @@ -32,6 +32,7 @@ namespace Ink_Canvas { public List History { get; set; } = new List(); public Dictionary NameFrequency { get; set; } = new Dictionary(); + public Dictionary NameProbabilities { get; set; } = new Dictionary(); public DateTime LastUpdate { get; set; } = DateTime.Now; } @@ -66,6 +67,25 @@ namespace Ink_Canvas // 初始化点名相关变量 InitializeRollCallData(); + if (isSingleDrawMode) + { + if (ControlOptionsGrid != null) + { + ControlOptionsGrid.Opacity = 0.4; + ControlOptionsGrid.IsHitTestVisible = false; + } + if (StartRollCallBtn != null) + { + StartRollCallBtn.Opacity = 0.4; + StartRollCallBtn.IsEnabled = false; + } + if (ResetBtn != null) + { + ResetBtn.Opacity = 0.4; + ResetBtn.IsEnabled = false; + } + } + // 单次抽模式:自动开始抽选 if (isSingleDrawMode) { @@ -101,6 +121,27 @@ namespace Ink_Canvas // 初始化点名相关变量 InitializeRollCallData(); + // 单次抽模式:禁用控制面板,阻止用户点击按钮 + if (isSingleDrawMode) + { + if (ControlOptionsGrid != null) + { + ControlOptionsGrid.Opacity = 0.4; + ControlOptionsGrid.IsHitTestVisible = false; + } + // 禁用开始点名和重置按钮 + if (StartRollCallBtn != null) + { + StartRollCallBtn.Opacity = 0.4; + StartRollCallBtn.IsEnabled = false; + } + if (ResetBtn != null) + { + ResetBtn.Opacity = 0.4; + ResetBtn.IsEnabled = false; + } + } + // 单次抽模式:自动开始抽选 if (isSingleDrawMode) { @@ -129,8 +170,15 @@ namespace Ink_Canvas private static RollCallHistoryData historyData = null; private static readonly object historyLock = new object(); private static int maxRecentHistory = 20; - private static double avoidanceWeight = 0.8; // 避免重复的权重 - private const double FREQUENCY_WEIGHT = 0.2; // 频率平衡的权重 + private static double avoidanceWeight = 0.8; + private const double FREQUENCY_WEIGHT = 0.2; + + // 概率相关 + private const double DEFAULT_PROBABILITY = 1.0; + private const double BASE_PROBABILITY_DECAY_FACTOR = 0.5; + private const double MIN_PROBABILITY = 0.01; + private const double PROBABILITY_RECOVERY_RATE = 0.2; + private const double FREQUENCY_BOOST_FACTOR = 2.0; // 单次抽相关 private bool isSingleDrawMode = false; @@ -531,38 +579,230 @@ namespace Ink_Canvas } /// - /// 使用机器学习算法选择单个人员 + /// 使用概率算法选择单个人员 /// private static string SelectSingleNameWithML(List availableNames, List alreadySelected, Random random) { if (availableNames.Count == 0) return null; if (availableNames.Count == 1) return availableNames[0]; - // 计算每个人员的权重 - var nameWeights = new Dictionary(); + // 确保历史数据已初始化 + if (historyData == null) + { + LoadRollCallHistory(); + } + + // 初始化概率字典 + if (historyData.NameProbabilities == null) + { + historyData.NameProbabilities = new Dictionary(); + } + + // 获取每个人员的概率 + var nameProbabilities = new Dictionary(); foreach (string name in availableNames) { if (alreadySelected.Contains(name)) continue; - double weight = 1.0; // 基础权重 - - // 1. 避免最近重复的权重计算 - double recentAvoidanceWeight = CalculateRecentAvoidanceWeight(name); - weight *= (1.0 - recentAvoidanceWeight * avoidanceWeight); - - // 2. 频率平衡权重计算 - double frequencyWeight = CalculateFrequencyWeight(name); - weight *= (1.0 + frequencyWeight * FREQUENCY_WEIGHT); - - // 3. 确保权重不为负数 - weight = Math.Max(weight, 0.1); - - nameWeights[name] = weight; + // 获取基础概率 + double baseProbability = GetNameProbability(name); + + // 根据最近历史记录调整概率 + double adjustedProbability = AdjustProbabilityByRecentHistory(name, baseProbability); + + double finalProbability = AdjustProbabilityByFrequency(name, adjustedProbability); + + nameProbabilities[name] = finalProbability; } - // 使用加权随机选择 - return WeightedRandomSelection(nameWeights, random); + // 使用概率进行加权随机选择 + return ProbabilityBasedRandomSelection(nameProbabilities, random); + } + + /// + /// 获取人员的概率 + /// + private static double GetNameProbability(string name) + { + if (historyData == null || historyData.NameProbabilities == null) + return DEFAULT_PROBABILITY; + + if (historyData.NameProbabilities.ContainsKey(name)) + { + return historyData.NameProbabilities[name]; + } + else + { + // 新人员,初始化默认概率 + historyData.NameProbabilities[name] = DEFAULT_PROBABILITY; + return DEFAULT_PROBABILITY; + } + } + + /// + /// 根据最近历史记录调整概率 + /// + private static double AdjustProbabilityByRecentHistory(string name, double baseProbability) + { + if (historyData == null || historyData.History == null || historyData.History.Count == 0) + return baseProbability; + + // 获取最近记录 + var recentHistory = historyData.History.Skip(Math.Max(0, historyData.History.Count - maxRecentHistory)).ToList(); + int recentCount = recentHistory.Count(n => n == name); + + if (recentCount == 0) + return baseProbability; + + double recentFrequency = (double)recentCount / Math.Min(recentHistory.Count, maxRecentHistory); + + double reductionFactor = 1.0 - (recentFrequency * avoidanceWeight); + reductionFactor = Math.Max(reductionFactor, MIN_PROBABILITY / DEFAULT_PROBABILITY); // 确保不会降得太低 + + return baseProbability * reductionFactor; + } + + private static double AdjustProbabilityByFrequency(string name, double baseProbability) + { + if (historyData == null || historyData.NameFrequency == null || historyData.NameFrequency.Count == 0) + return baseProbability; + + // 计算总选中次数 + int totalSelections = historyData.NameFrequency.Values.Sum(); + if (totalSelections == 0) + return baseProbability; + + // 获取该名字的选中次数 + int nameCount = historyData.NameFrequency.ContainsKey(name) ? historyData.NameFrequency[name] : 0; + + // 计算该名字的选中频率 + double nameFrequency = (double)nameCount / totalSelections; + + // 计算平均频率(假设有N个不同的人) + int uniqueNamesCount = historyData.NameFrequency.Keys.Count; + if (uniqueNamesCount == 0) + return baseProbability; + + double averageFrequency = 1.0 / uniqueNamesCount; + + // 如果该名字的频率低于平均频率,则增加概率 + if (nameFrequency < averageFrequency) + { + // 计算频率差异比例 + double frequencyRatio = nameFrequency / averageFrequency; + + double frequencyGap = 1.0 - frequencyRatio; + double boostFactor = FREQUENCY_BOOST_FACTOR * frequencyGap * frequencyGap; + + // 增加概率 + double boostedProbability = baseProbability * (1.0 + boostFactor); + + return Math.Min(boostedProbability, DEFAULT_PROBABILITY * 10.0); + } + else if (nameFrequency > averageFrequency) + { + double frequencyRatio = nameFrequency / averageFrequency; + + double reductionFactor = 1.0 - (frequencyRatio - 1.0) * 0.3; + reductionFactor = Math.Max(reductionFactor, MIN_PROBABILITY / DEFAULT_PROBABILITY); + + return baseProbability * reductionFactor; + } + + return baseProbability; + } + + /// + /// 根据频率统计更新保存的概率 + /// + private static void UpdateProbabilitiesByFrequency() + { + if (historyData == null || historyData.NameFrequency == null || historyData.NameFrequency.Count == 0) + return; + + if (historyData.NameProbabilities == null) + historyData.NameProbabilities = new Dictionary(); + + // 计算总选中次数 + int totalSelections = historyData.NameFrequency.Values.Sum(); + if (totalSelections == 0) + return; + + // 计算平均频率 + int uniqueNamesCount = historyData.NameFrequency.Keys.Count; + if (uniqueNamesCount == 0) + return; + + double averageFrequency = 1.0 / uniqueNamesCount; + + // 遍历所有在频率统计中的人员 + foreach (var kvp in historyData.NameFrequency) + { + string name = kvp.Key; + int nameCount = kvp.Value; + + // 获取当前保存的概率(如果不存在则使用默认值) + double currentProbability = historyData.NameProbabilities.ContainsKey(name) + ? historyData.NameProbabilities[name] + : DEFAULT_PROBABILITY; + + // 计算该名字的选中频率 + double nameFrequency = (double)nameCount / totalSelections; + + // 如果该名字的频率低于平均频率,则增加概率并保存 + if (nameFrequency < averageFrequency) + { + // 计算频率差异比例 + double frequencyRatio = nameFrequency / averageFrequency; + double frequencyGap = 1.0 - frequencyRatio; + double boostFactor = FREQUENCY_BOOST_FACTOR * frequencyGap * frequencyGap; + + // 增加概率 + double boostedProbability = currentProbability * (1.0 + boostFactor); + + // 限制最大概率,避免过高 + boostedProbability = Math.Min(boostedProbability, DEFAULT_PROBABILITY * 10.0); + + // 保存更新后的概率 + historyData.NameProbabilities[name] = boostedProbability; + } + else if (nameFrequency > averageFrequency) + { + double frequencyRatio = nameFrequency / averageFrequency; + + double reductionFactor = 1.0 - (frequencyRatio - 1.0) * 0.3; + reductionFactor = Math.Max(reductionFactor, MIN_PROBABILITY / DEFAULT_PROBABILITY); + + double reducedProbability = currentProbability * reductionFactor; + historyData.NameProbabilities[name] = reducedProbability; + } + } + } + + /// + /// 基于概率的随机选择 + /// + private static string ProbabilityBasedRandomSelection(Dictionary nameProbabilities, Random random) + { + if (nameProbabilities.Count == 0) return null; + + double totalProbability = nameProbabilities.Values.Sum(); + if (totalProbability <= 0) return nameProbabilities.Keys.First(); + + double randomValue = random.NextDouble() * totalProbability; + double currentProbability = 0; + + foreach (var kvp in nameProbabilities) + { + currentProbability += kvp.Value; + if (randomValue <= currentProbability) + { + return kvp.Key; + } + } + + return nameProbabilities.Keys.Last(); } /// @@ -600,7 +840,7 @@ namespace Ink_Canvas } /// - /// 加权随机选择 + /// 加权随机选择(保留用于兼容,实际已改用概率选择) /// private static string WeightedRandomSelection(Dictionary nameWeights, Random random) { @@ -639,29 +879,96 @@ namespace Ink_Canvas lock (historyLock) { + // 初始化概率字典 + if (historyData.NameProbabilities == null) + { + historyData.NameProbabilities = new Dictionary(); + } + // 更新历史记录 if (historyData.History == null) historyData.History = new List(); historyData.History.AddRange(selectedNames); - // 保持历史记录不超过100条 - if (historyData.History.Count > 100) - { - historyData.History = historyData.History.Skip(historyData.History.Count - 100).ToList(); - } + // 保持历史记录不超过100条 + if (historyData.History.Count > 100) + { + historyData.History = historyData.History.Skip(historyData.History.Count - 100).ToList(); + } - // 更新频率统计 - if (historyData.NameFrequency == null) - historyData.NameFrequency = new Dictionary(); + // 更新频率统计 + if (historyData.NameFrequency == null) + historyData.NameFrequency = new Dictionary(); - foreach (string name in selectedNames) - { - if (historyData.NameFrequency.ContainsKey(name)) - historyData.NameFrequency[name]++; - else - historyData.NameFrequency[name] = 1; - } + // 更新概率:降重机制 + foreach (string name in selectedNames) + { + // 更新频率统计 + if (historyData.NameFrequency.ContainsKey(name)) + historyData.NameFrequency[name]++; + else + historyData.NameFrequency[name] = 1; + + // 降重:被选中的人员概率降低 + double currentProbability = GetNameProbability(name); + + double frequencyBasedDecay = 1.0; + if (historyData.NameFrequency != null && historyData.NameFrequency.ContainsKey(name)) + { + int totalSelections = historyData.NameFrequency.Values.Sum(); + if (totalSelections > 0) + { + int uniqueNamesCount = historyData.NameFrequency.Keys.Count; + if (uniqueNamesCount > 0) + { + double nameFrequency = (double)historyData.NameFrequency[name] / totalSelections; + double averageFrequency = 1.0 / uniqueNamesCount; + + if (nameFrequency > averageFrequency) + { + double frequencyRatio = nameFrequency / averageFrequency; + frequencyBasedDecay = 1.0 - (frequencyRatio - 1.0) * 0.2; + } + } + } + } + + double decayFactor = BASE_PROBABILITY_DECAY_FACTOR * (1.0 + avoidanceWeight) * frequencyBasedDecay; + decayFactor = Math.Min(decayFactor, 0.85); + + double newProbability = currentProbability * decayFactor; + newProbability = Math.Max(newProbability, MIN_PROBABILITY); // 确保不低于最小概率 + historyData.NameProbabilities[name] = newProbability; + } + + if (historyData.History != null && historyData.History.Count > 0) + { + int historyCount = historyData.History.Count; + int skipCount = Math.Max(0, historyCount - maxRecentHistory); + var recentHistory = historyData.History.Skip(skipCount).ToList(); + var recentNames = new HashSet(recentHistory); + + var allNames = historyData.NameProbabilities.Keys.ToList(); + foreach (string name in allNames) + { + if (!recentNames.Contains(name)) + { + double currentProbability = historyData.NameProbabilities[name]; + if (currentProbability < DEFAULT_PROBABILITY) + { + double newProbability = Math.Min( + currentProbability + PROBABILITY_RECOVERY_RATE, + DEFAULT_PROBABILITY + ); + historyData.NameProbabilities[name] = newProbability; + } + } + } + } + + // 根据频率统计更新概率 + UpdateProbabilitiesByFrequency(); historyData.LastUpdate = DateTime.Now; @@ -697,6 +1004,11 @@ namespace Ink_Canvas if (data != null) { historyData = data; + // 确保概率字典已初始化 + if (historyData.NameProbabilities == null) + { + historyData.NameProbabilities = new Dictionary(); + } } else { @@ -1325,9 +1637,9 @@ namespace Ink_Canvas // 动画结束,显示最终结果 Application.Current.Dispatcher.Invoke(() => { - // 使用降重抽选方法选择数字 + // 根据选择的模式进行不同的抽选逻辑 var numberList = Enumerable.Range(1, 60).Select(n => n.ToString()).ToList(); - var selectedNumbers = SelectNamesWithML(numberList, currentCount, random); + var selectedNumbers = SelectNamesByMode(numberList, currentCount); // 更新历史记录 UpdateRollCallHistory(selectedNumbers); @@ -1418,8 +1730,8 @@ namespace Ink_Canvas // 动画结束,显示最终结果 Application.Current.Dispatcher.Invoke(() => { - // 使用降重抽选方法 - var selectedNames = SelectNamesWithML(nameList, currentCount, random); + // 根据选择的模式进行不同的抽选逻辑 + var selectedNames = SelectNamesByMode(nameList, currentCount); // 更新历史记录 UpdateRollCallHistory(selectedNames); @@ -1440,6 +1752,21 @@ namespace Ink_Canvas System.Threading.Thread.Sleep(autoCloseWaitTime); Application.Current.Dispatcher.Invoke(() => { + if (ControlOptionsGrid != null) + { + ControlOptionsGrid.Opacity = 1; + ControlOptionsGrid.IsHitTestVisible = true; + } + if (StartRollCallBtn != null) + { + StartRollCallBtn.Opacity = 1; + StartRollCallBtn.IsEnabled = true; + } + if (ResetBtn != null) + { + ResetBtn.Opacity = 1; + ResetBtn.IsEnabled = true; + } Close(); }); }).Start(); @@ -1476,9 +1803,9 @@ namespace Ink_Canvas // 动画结束,显示最终结果 Application.Current.Dispatcher.Invoke(() => { - // 使用降重抽选方法选择数字 + // 根据选择的模式进行不同的抽选逻辑 var numberList = Enumerable.Range(1, 60).Select(n => n.ToString()).ToList(); - var selectedNumbers = SelectNamesWithML(numberList, currentCount, random); + var selectedNumbers = SelectNamesByMode(numberList, currentCount); // 更新历史记录 UpdateRollCallHistory(selectedNumbers); @@ -1512,6 +1839,22 @@ namespace Ink_Canvas System.Threading.Thread.Sleep(autoCloseWaitTime); Application.Current.Dispatcher.Invoke(() => { + if (ControlOptionsGrid != null) + { + ControlOptionsGrid.Opacity = 1; + ControlOptionsGrid.IsHitTestVisible = true; + } + // 恢复开始点名和重置按钮 + if (StartRollCallBtn != null) + { + StartRollCallBtn.Opacity = 1; + StartRollCallBtn.IsEnabled = true; + } + if (ResetBtn != null) + { + ResetBtn.Opacity = 1; + ResetBtn.IsEnabled = true; + } Close(); }); }).Start(); diff --git a/Ink Canvas/Windows/QuickDrawFloatingButton.cs b/Ink Canvas/Windows/QuickDrawFloatingButton.cs deleted file mode 100644 index 062306bf..00000000 --- a/Ink Canvas/Windows/QuickDrawFloatingButton.cs +++ /dev/null @@ -1,355 +0,0 @@ -using Ink_Canvas.Helpers; -using System; -using System.Windows; -using System.Windows.Input; -using System.Windows.Interop; -using System.Windows.Media; -using System.Runtime.InteropServices; -using System.Windows.Threading; - -namespace Ink_Canvas -{ - /// - /// 快抽悬浮按钮 - /// - public partial class QuickDrawFloatingButton : Window - { - private bool isDragging = false; - private Point dragStartPoint; - private Point windowStartPoint; - - public QuickDrawFloatingButton() - { - InitializeComponent(); - - // 设置无焦点状态 - this.Focusable = false; - this.ShowInTaskbar = false; - - // 窗口句柄创建后应用无焦点模式 - this.SourceInitialized += QuickDrawFloatingButton_SourceInitialized; - } - - private void QuickDrawFloatingButton_SourceInitialized(object sender, EventArgs e) - { - ApplyNoFocusMode(); - } - - - private void FloatingButton_Loaded(object sender, RoutedEventArgs e) - { - // 设置位置到屏幕右下角稍微靠近中部 - SetPositionToBottomRight(); - - // 应用无焦点模式 - ApplyNoFocusMode(); - - // 应用置顶 - ApplyFloatingButtonTopmost(); - - if (MainWindow.Settings?.Advanced?.EnableUIAccessTopMost != true) - { - StartTopmostMaintenance(); - } - } - - private void SetPositionToBottomRight() - { - try - { - // 获取主屏幕的工作区域 - var workingArea = SystemParameters.WorkArea; - this.Left = workingArea.Right - this.Width - 0; - this.Top = workingArea.Bottom - this.Height - 200; - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"设置悬浮按钮位置失败: {ex.Message}", LogHelper.LogType.Error); - // 如果计算失败,使用默认位置 - this.Left = 720; - this.Top = 400; - } - } - - private void FloatingButton_Click(object sender, MouseButtonEventArgs e) - { - try - { - // 如果正在拖动,不触发点击事件 - if (isDragging) return; - - // 打开快抽窗口 - var quickDrawWindow = new QuickDrawWindow(); - quickDrawWindow.ShowDialog(); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"打开快抽窗口失败: {ex.Message}", LogHelper.LogType.Error); - } - } - - private void DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - isDragging = false; - // 记录鼠标在屏幕上的初始位置 - dragStartPoint = this.PointToScreen(e.GetPosition(this)); - // 记录窗口的初始位置 - windowStartPoint = new Point(this.Left, this.Top); - ((UIElement)sender).CaptureMouse(); - e.Handled = true; - } - - private void DragArea_MouseMove(object sender, MouseEventArgs e) - { - if (e.LeftButton == MouseButtonState.Pressed && ((UIElement)sender).IsMouseCaptured) - { - // 获取鼠标在屏幕上的当前位置 - Point currentScreenPoint = this.PointToScreen(e.GetPosition(this)); - Vector diff = currentScreenPoint - dragStartPoint; - - if (!isDragging && (Math.Abs(diff.X) > 3 || Math.Abs(diff.Y) > 3)) - { - isDragging = true; - } - - if (isDragging) - { - // 使用窗口初始位置加上鼠标移动的距离 - double newLeft = windowStartPoint.X + diff.X; - double newTop = windowStartPoint.Y + diff.Y; - - // 限制在屏幕范围内 - var workingArea = SystemParameters.WorkArea; - newLeft = Math.Max(workingArea.Left, Math.Min(newLeft, workingArea.Right - this.Width)); - newTop = Math.Max(workingArea.Top, Math.Min(newTop, workingArea.Bottom - this.Height)); - - this.Left = newLeft; - this.Top = newTop; - } - } - } - - private void DragArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) - { - if (((UIElement)sender).IsMouseCaptured) - { - ((UIElement)sender).ReleaseMouseCapture(); - } - - // 延迟重置拖动状态,避免触发点击事件 - if (isDragging) - { - Dispatcher.BeginInvoke(new Action(() => { isDragging = false; }), - DispatcherPriority.Background); - } - else - { - isDragging = false; - } - - e.Handled = true; - } - - - - - #region Win32 API 声明和置顶管理 - [DllImport("user32.dll")] - private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); - - [DllImport("user32.dll")] - private static extern int GetWindowLong(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll")] - private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); - - [DllImport("user32.dll")] - private static extern bool IsWindow(IntPtr hWnd); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern bool IsIconic(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - [DllImport("kernel32.dll")] - private static extern uint GetCurrentProcessId(); - - private const int GWL_EXSTYLE = -20; - private const int WS_EX_NOACTIVATE = 0x08000000; - private const int WS_EX_TOPMOST = 0x00000008; - private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); - private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); - private const uint SWP_NOMOVE = 0x0002; - private const uint SWP_NOSIZE = 0x0001; - private const uint SWP_NOACTIVATE = 0x0010; - private const uint SWP_SHOWWINDOW = 0x0040; - private const uint SWP_NOOWNERZORDER = 0x0200; - - // 添加定时器来维护置顶状态 - private DispatcherTimer topmostMaintenanceTimer; - private bool isTopmostMaintenanceEnabled; - - /// - /// 应用无焦点模式 - /// - private void ApplyNoFocusMode() - { - try - { - var hwnd = new WindowInteropHelper(this).Handle; - if (hwnd == IntPtr.Zero) return; - - int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); - - // 悬浮快抽窗口始终启用无焦点模式 - SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE); - } - catch (Exception) - { - } - } - - /// - /// 应用悬浮按钮置顶 - /// - private void ApplyFloatingButtonTopmost() - { - try - { - var hwnd = new WindowInteropHelper(this).Handle; - if (hwnd == IntPtr.Zero) return; - // 设置WPF的Topmost属性 - Topmost = true; - - // 使用Win32 API强制置顶 - // 1. 设置窗口样式为置顶 - int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); - SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST); - - // 2. 使用SetWindowPos确保窗口在最顶层 - SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER); - - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用快抽悬浮按钮置顶失败: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 启动置顶维护定时器 - /// - private void StartTopmostMaintenance() - { - if (MainWindow.Settings?.Advanced?.EnableUIAccessTopMost == true) - { - return; - } - - if (isTopmostMaintenanceEnabled) return; - - if (topmostMaintenanceTimer == null) - { - topmostMaintenanceTimer = new DispatcherTimer(); - topmostMaintenanceTimer.Interval = TimeSpan.FromMilliseconds(500); // 每500ms检查一次 - topmostMaintenanceTimer.Tick += TopmostMaintenanceTimer_Tick; - } - - topmostMaintenanceTimer.Start(); - isTopmostMaintenanceEnabled = true; - } - - /// - /// 停止置顶维护定时器 - /// - private void StopTopmostMaintenance() - { - if (topmostMaintenanceTimer != null && isTopmostMaintenanceEnabled) - { - topmostMaintenanceTimer.Stop(); - isTopmostMaintenanceEnabled = false; - } - } - - /// - /// 置顶维护定时器事件 - /// - private void TopmostMaintenanceTimer_Tick(object sender, EventArgs e) - { - try - { - if (MainWindow.Settings?.Advanced?.EnableUIAccessTopMost == true) - { - StopTopmostMaintenance(); - return; - } - - // 悬浮快抽窗口始终启用无焦点模式,不需要检查主窗口设置 - - var hwnd = new WindowInteropHelper(this).Handle; - if (hwnd == IntPtr.Zero) return; - - // 检查窗口是否仍然可见且不是最小化状态 - if (!IsWindow(hwnd) || !IsWindowVisible(hwnd) || IsIconic(hwnd)) - { - return; - } - - // 检查是否有子窗口在前景 - var foregroundWindow = GetForegroundWindow(); - if (foregroundWindow != hwnd) - { - // 检查前景窗口是否是当前应用程序的子窗口 - var foregroundWindowProcessId = GetWindowThreadProcessId(foregroundWindow, out uint processId); - var currentProcessId = GetCurrentProcessId(); - - if (processId == currentProcessId) - { - // 如果有子窗口在前景,暂停置顶维护 - return; - } - - // 如果窗口不在最顶层且没有子窗口,重新设置置顶 - SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER); - - // 确保窗口样式正确 - int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); - if ((exStyle & WS_EX_TOPMOST) == 0) - { - SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST); - } - - // 确保无焦点模式样式正确 - if ((exStyle & WS_EX_NOACTIVATE) == 0) - { - SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE); - } - } - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"快抽悬浮按钮置顶维护定时器出错: {ex.Message}", LogHelper.LogType.Error); - } - } - - /// - /// 窗口关闭时停止置顶维护定时器 - /// - protected override void OnClosed(EventArgs e) - { - StopTopmostMaintenance(); - base.OnClosed(e); - } - #endregion - } -} diff --git a/Ink Canvas/Windows/QuickDrawWindow.cs b/Ink Canvas/Windows/QuickDrawWindow.cs index e8bfec04..7504c8a1 100644 --- a/Ink Canvas/Windows/QuickDrawWindow.cs +++ b/Ink Canvas/Windows/QuickDrawWindow.cs @@ -215,10 +215,6 @@ namespace Ink_Canvas } - private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) - { - // 窗口关闭时的清理工作 - } private void WindowDragMove(object sender, MouseButtonEventArgs e) { @@ -281,12 +277,27 @@ namespace Ink_Canvas /// private void QuickDrawWindow_Loaded(object sender, RoutedEventArgs e) { + MainWindow mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + mainWindow.PauseTopmostMaintenance(); + } + // 使用延迟确保窗口完全加载后再应用置顶 Dispatcher.BeginInvoke(new Action(() => { ApplyQuickDrawWindowTopmost(); }), DispatcherPriority.Loaded); } + + private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + MainWindow mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + mainWindow.ResumeTopmostMaintenance(); + } + } #endregion } } diff --git a/Ink Canvas/Windows/RollCallHistoryWindow.xaml.cs b/Ink Canvas/Windows/RollCallHistoryWindow.xaml.cs index 16c7bc0f..67bb4654 100644 --- a/Ink Canvas/Windows/RollCallHistoryWindow.xaml.cs +++ b/Ink Canvas/Windows/RollCallHistoryWindow.xaml.cs @@ -48,19 +48,74 @@ namespace Ink_Canvas return; } - // 按时间倒序显示(最新的在上方) - // 由于历史记录是按时间顺序添加的,所以直接反转即可 - var reversedHistory = historyData.History.ToList(); - reversedHistory.Reverse(); + // 计算每个名字的总累计抽选次数(用于统计信息) + var nameCountDict = new System.Collections.Generic.Dictionary(); + if (historyData.NameFrequency != null && historyData.NameFrequency.Count > 0) + { + // 使用已保存的频率统计 + foreach (var kvp in historyData.NameFrequency) + { + nameCountDict[kvp.Key] = kvp.Value; + } + } + else + { + // 如果没有频率统计,从历史记录中计算 + foreach (var name in historyData.History) + { + if (nameCountDict.ContainsKey(name)) + nameCountDict[name]++; + else + nameCountDict[name] = 1; + } + } - // 显示历史记录,每行一个 - TextBoxHistory.Text = string.Join(Environment.NewLine, reversedHistory); + // 计算历史记录中每条记录出现时的累计次数(按时间正序) + var historyWithCount = new System.Collections.Generic.List>(); + var runningCount = new System.Collections.Generic.Dictionary(); + foreach (var name in historyData.History) + { + if (runningCount.ContainsKey(name)) + runningCount[name]++; + else + runningCount[name] = 1; + historyWithCount.Add(new System.Tuple(name, runningCount[name])); + } + + // 按时间倒序显示(最新的在上方) + historyWithCount.Reverse(); + + // 显示历史记录,每行显示:名字 (累计X次) + var historyLines = new System.Collections.Generic.List(); + foreach (var item in historyWithCount) + { + historyLines.Add($"{item.Item1} (最近累计{item.Item2}次)"); + } // 显示统计信息 int totalCount = historyData.History.Count; string lastUpdate = historyData.LastUpdate.ToString("yyyy-MM-dd HH:mm:ss"); - string header = $"共 {totalCount} 条记录,最后更新:{lastUpdate}\n\n"; - TextBoxHistory.Text = header + TextBoxHistory.Text; + + // 计算累计统计信息 + var statsLines = new System.Collections.Generic.List(); + statsLines.Add($""); + statsLines.Add($""); + statsLines.Add($"累计抽选次数统计:"); + + // 按累计次数降序排序显示 + var sortedStats = nameCountDict.OrderByDescending(kvp => kvp.Value).ToList(); + foreach (var kvp in sortedStats) + { + statsLines.Add($" {kvp.Key}: {kvp.Value}次"); + } + + statsLines.Add($""); + statsLines.Add($"共 {totalCount} 条记录,最后更新:{lastUpdate}"); + + // 组合历史记录和统计信息 + TextBoxHistory.Text = string.Join(Environment.NewLine, historyLines) + + Environment.NewLine + + string.Join(Environment.NewLine, statsLines); } catch (Exception ex) { @@ -174,7 +229,6 @@ namespace Ink_Canvas } catch { - // 如果无法读取注册表,默认使用浅色主题 light = true; } return light; diff --git a/Ink Canvas/Windows/NewStyleTimerWindow.xaml b/Ink Canvas/Windows/TimerControl.xaml similarity index 54% rename from Ink Canvas/Windows/NewStyleTimerWindow.xaml rename to Ink Canvas/Windows/TimerControl.xaml index 06b76a1a..02f931f3 100644 --- a/Ink Canvas/Windows/NewStyleTimerWindow.xaml +++ b/Ink Canvas/Windows/TimerControl.xaml @@ -1,31 +1,35 @@ - - - + + - + - + @@ -33,8 +37,16 @@ - - + + - + - + - - - - - - - - - - - - - - - + + + + - - - - - - - - + + + + + + + - - - - + + + + - - - - - + + + + + - - - - - - - - - - - - - - + + + + - - - - - - - - + + + + + + + - - - - + + + + - - - - - + + + + + - - - - - - - - - - - - - - + + + + - - - - - - - - + + + + + + + - - - - + + + + - @@ -618,9 +760,8 @@ - - - + + @@ -662,7 +803,7 @@ - + @@ -708,21 +849,39 @@ - - - + + - - - - - @@ -780,4 +959,5 @@ - \ No newline at end of file + + diff --git a/Ink Canvas/Windows/NewStyleTimerWindow.cs b/Ink Canvas/Windows/TimerControl.xaml.cs similarity index 75% rename from Ink Canvas/Windows/NewStyleTimerWindow.cs rename to Ink Canvas/Windows/TimerControl.xaml.cs index 34c7e928..4ea97948 100644 --- a/Ink Canvas/Windows/NewStyleTimerWindow.cs +++ b/Ink Canvas/Windows/TimerControl.xaml.cs @@ -6,14 +6,12 @@ using System.Timers; using System.Windows; using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Interop; using System.Windows.Media; using System.Windows.Shapes; using Newtonsoft.Json; -using System.Runtime.InteropServices; using System.Windows.Threading; - -namespace Ink_Canvas +using Microsoft.Win32; +namespace Ink_Canvas.Windows { /// /// 最近计时记录数据模型 @@ -31,12 +29,11 @@ namespace Ink_Canvas /// /// 新计时器UI风格的倒计时器窗口 /// - public partial class NewStyleTimerWindow : Window + public partial class TimerControl : UserControl { - public NewStyleTimerWindow() + public TimerControl() { InitializeComponent(); - AnimationsHelper.ShowWithSlideFromBottomAndFade(this, 0.25); timer.Elapsed += Timer_Elapsed; timer.Interval = 50; @@ -49,10 +46,69 @@ namespace Ink_Canvas hideTimer = new Timer(1000); // 每秒检查一次 hideTimer.Elapsed += HideTimer_Elapsed; lastActivityTime = DateTime.Now; - - // 添加窗口加载事件处理,确保置顶 - Loaded += TimerWindow_Loaded; + + // 监听主题变化事件 + SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged; + + // 监听卸载事件,清理资源 + Unloaded += TimerControl_Unloaded; } + + private void TimerControl_Unloaded(object sender, RoutedEventArgs e) + { + // 取消订阅主题变化事件 + SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged; + } + + private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) + { + // 当主题变化时,重新应用主题 + Application.Current.Dispatcher.Invoke(() => + { + RefreshTheme(); + }); + } + + /// + /// 刷新主题(供外部调用) + /// + public void RefreshTheme() + { + try + { + // 重新应用主题 + ApplyTheme(); + + // 强制刷新UI + InvalidateVisual(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"刷新计时器窗口主题出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + #region 事件定义 + /// + /// 计时器完成事件 + /// + public event EventHandler TimerCompleted; + + /// + /// 关闭事件 - 通知主窗口隐藏容器 + /// + public event EventHandler CloseRequested; + + /// + /// 显示最小化视图事件 + /// + public event EventHandler ShowMinimizedRequested; + + /// + /// 隐藏最小化视图事件 + /// + public event EventHandler HideMinimizedRequested; + #endregion private void Timer_Elapsed(object sender, ElapsedEventArgs e) @@ -116,6 +172,12 @@ namespace Ink_Canvas StartPauseIcon.Data = Geometry.Parse(PlayIconData); PlayTimerSound(); + // 禁用全屏按钮 + if (FullscreenBtn != null) + { + FullscreenBtn.IsEnabled = false; + } + TimerCompleted?.Invoke(this, EventArgs.Empty); HandleTimerCompletion(); } @@ -127,15 +189,23 @@ namespace Ink_Canvas int displayHours = totalHours; if (displayHours > 99) displayHours = 99; + if (displayHours < 0) displayHours = 0; bool shouldShowRed = MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true; - SetDigitDisplay("Digit1Display", Math.Abs(displayHours / 10) % 10, shouldShowRed); - SetDigitDisplay("Digit2Display", (displayHours % 10 + 10) % 10, shouldShowRed); - SetDigitDisplay("Digit3Display", overtimeSpan.Minutes / 10, shouldShowRed); - SetDigitDisplay("Digit4Display", overtimeSpan.Minutes % 10, shouldShowRed); - SetDigitDisplay("Digit5Display", overtimeSpan.Seconds / 10, shouldShowRed); - SetDigitDisplay("Digit6Display", overtimeSpan.Seconds % 10, shouldShowRed); + int hoursTens = Math.Max(0, Math.Min(9, Math.Abs(displayHours / 10) % 10)); + int hoursOnes = Math.Max(0, Math.Min(9, (displayHours % 10 + 10) % 10)); + int minutesTens = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Minutes) / 10)); + int minutesOnes = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Minutes) % 10)); + int secondsTens = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Seconds) / 10)); + int secondsOnes = Math.Max(0, Math.Min(9, Math.Abs(overtimeSpan.Seconds) % 10)); + + SetDigitDisplay("Digit1Display", hoursTens, shouldShowRed); + SetDigitDisplay("Digit2Display", hoursOnes, shouldShowRed); + SetDigitDisplay("Digit3Display", minutesTens, shouldShowRed); + SetDigitDisplay("Digit4Display", minutesOnes, shouldShowRed); + SetDigitDisplay("Digit5Display", secondsTens, shouldShowRed); + SetDigitDisplay("Digit6Display", secondsOnes, shouldShowRed); SetColonDisplay(shouldShowRed); } @@ -160,11 +230,7 @@ namespace Ink_Canvas Timer timer = new Timer(); private Timer hideTimer; - private MinimizedTimerWindow minimizedWindow; - private DateTime lastActivityTime; - private bool isFullscreenMode = false; - private FullscreenTimerWindow fullscreenWindow; - public event EventHandler TimerCompleted; + private DateTime lastActivityTime; public TimeSpan? GetTotalTimeSpan() { return new TimeSpan(hour, minute, second); @@ -270,6 +336,9 @@ namespace Ink_Canvas SetDarkThemeBorder(); } } + + // 刷新数字和冒号显示的颜色 + UpdateDigitDisplays(); } catch (Exception ex) { @@ -314,47 +383,6 @@ namespace Ink_Canvas SetColonDisplay(false); } - private void HideTimer_Elapsed(object sender, ElapsedEventArgs e) - { - Application.Current.Dispatcher.Invoke(() => - { - // 只有在计时器运行时且不在全屏模式下才检查自动隐藏 - if (isTimerRunning && !isPaused && !isFullscreenMode) - { - var timeSinceLastActivity = DateTime.Now - lastActivityTime; - if (timeSinceLastActivity.TotalSeconds >= 5) - { - ShowMinimizedWindow(); - } - } - }); - } - - private void ShowMinimizedWindow() - { - if (minimizedWindow == null || !minimizedWindow.IsVisible) - { - minimizedWindow = new MinimizedTimerWindow(this); - minimizedWindow.Show(); - - // 确保最小化窗口也置顶 - minimizedWindow.Topmost = true; - - // 隐藏主窗口 - this.Hide(); - } - } - - public void UpdateActivityTime() - { - lastActivityTime = DateTime.Now; - } - - public void SetFullscreenMode(bool isFullscreen) - { - isFullscreenMode = isFullscreen; - } - // 更新剩余时间 private void UpdateRemainingTime() { @@ -445,15 +473,6 @@ namespace Ink_Canvas StartPauseIcon.Data = Geometry.Parse(PlayIconData); } - private void Window_MouseMove(object sender, MouseEventArgs e) - { - UpdateActivityTime(); - } - - private void Window_MouseEnter(object sender, MouseEventArgs e) - { - UpdateActivityTime(); - } /// /// 根据数字值设置SVG数字显示 @@ -466,6 +485,8 @@ namespace Ink_Canvas var path = this.FindName(pathName) as System.Windows.Shapes.Path; if (path != null) { + digit = Math.Max(0, Math.Min(9, digit)); + string resourceKey = $"Digit{digit}"; var geometry = this.FindResource(resourceKey) as Geometry; if (geometry != null) @@ -479,7 +500,7 @@ namespace Ink_Canvas } else { - var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush; + var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush; if (defaultBrush != null) { path.Fill = defaultBrush; @@ -509,7 +530,7 @@ namespace Ink_Canvas } else { - var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush; + var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush; if (defaultBrush != null) { colon1.Foreground = defaultBrush; @@ -529,7 +550,7 @@ namespace Ink_Canvas } else { - var defaultBrush = this.FindResource("NewTimerWindowDigitForeground") as Brush; + var defaultBrush = this.TryFindResource("NewTimerWindowDigitForeground") as Brush; if (defaultBrush != null) { colon2.Foreground = defaultBrush; @@ -546,6 +567,7 @@ namespace Ink_Canvas private void Digit1Plus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentHour = hour; int hourTens = currentHour / 10; int hourOnes = currentHour % 10; @@ -560,6 +582,7 @@ namespace Ink_Canvas private void Digit1Minus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentHour = hour; int hourTens = currentHour / 10; int hourOnes = currentHour % 10; @@ -575,6 +598,7 @@ namespace Ink_Canvas private void Digit2Plus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentHour = hour; int hourTens = currentHour / 10; int hourOnes = currentHour % 10; @@ -594,6 +618,7 @@ namespace Ink_Canvas private void Digit2Minus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentHour = hour; int hourTens = currentHour / 10; int hourOnes = currentHour % 10; @@ -614,6 +639,7 @@ namespace Ink_Canvas private void Digit3Plus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentMinute = minute; int minuteTens = currentMinute / 10; int minuteOnes = currentMinute % 10; @@ -628,6 +654,7 @@ namespace Ink_Canvas private void Digit3Minus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentMinute = minute; int minuteTens = currentMinute / 10; int minuteOnes = currentMinute % 10; @@ -643,6 +670,7 @@ namespace Ink_Canvas private void Digit4Plus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentMinute = minute; int minuteTens = currentMinute / 10; int minuteOnes = currentMinute % 10; @@ -662,6 +690,7 @@ namespace Ink_Canvas private void Digit4Minus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentMinute = minute; int minuteTens = currentMinute / 10; int minuteOnes = currentMinute % 10; @@ -682,6 +711,7 @@ namespace Ink_Canvas private void Digit5Plus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentSecond = second; int secondTens = currentSecond / 10; int secondOnes = currentSecond % 10; @@ -696,6 +726,7 @@ namespace Ink_Canvas private void Digit5Minus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentSecond = second; int secondTens = currentSecond / 10; int secondOnes = currentSecond % 10; @@ -711,6 +742,7 @@ namespace Ink_Canvas private void Digit6Plus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentSecond = second; int secondTens = currentSecond / 10; int secondOnes = currentSecond % 10; @@ -730,6 +762,7 @@ namespace Ink_Canvas private void Digit6Minus_Click(object sender, RoutedEventArgs e) { if (isTimerRunning) return; + UpdateActivityTime(); int currentSecond = second; int secondTens = currentSecond / 10; int secondOnes = currentSecond % 10; @@ -752,6 +785,7 @@ namespace Ink_Canvas private void StartPause_Click(object sender, RoutedEventArgs e) { + UpdateActivityTime(); if (isPaused && isTimerRunning) { // 继续计时 @@ -788,26 +822,50 @@ namespace Ink_Canvas // 启动隐藏定时器 hideTimer.Start(); - // 确保计时器窗口置顶 - ApplyTimerWindowTopmost(); - // 保存到最近计时记录 SaveRecentTimer(); + + // 启用全屏按钮 + if (FullscreenBtn != null) + { + FullscreenBtn.IsEnabled = true; + } } } private void Reset_Click(object sender, RoutedEventArgs e) { + UpdateActivityTime(); + if (isTimerRunning) { + // 停止计时器 timer.Stop(); isTimerRunning = false; + isPaused = false; + + if (hideTimer != null) + { + hideTimer.Stop(); + } } - isPaused = false; - isOvertimeMode = false; UpdateDigitDisplays(); - StartPauseIcon.Data = Geometry.Parse(PlayIconData); + SetColonDisplay(false); + + if (StartPauseIcon != null) + { + StartPauseIcon.Data = Geometry.Parse(PlayIconData); + } + + isOvertimeMode = false; + hasPlayedProgressiveReminder = false; + + // 禁用全屏按钮 + if (FullscreenBtn != null) + { + FullscreenBtn.IsEnabled = false; + } } private void PlayTimerSound() @@ -876,29 +934,15 @@ namespace Ink_Canvas } } - private void Window_Loaded(object sender, RoutedEventArgs e) - { - // 窗口加载时的初始化 - } - - private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) - { - isTimerRunning = false; - } - private void CloseButton_Click(object sender, RoutedEventArgs e) { - Close(); - } - - private void WindowDragMove(object sender, MouseEventArgs e) - { - if (e.LeftButton == MouseButtonState.Pressed) - DragMove(); + StopTimer(); + CloseRequested?.Invoke(this, EventArgs.Empty); } private void CommonTab_Click(object sender, RoutedEventArgs e) { + UpdateActivityTime(); CommonTimersGrid.Visibility = Visibility.Visible; RecentTimersGrid.Visibility = Visibility.Collapsed; @@ -933,6 +977,7 @@ namespace Ink_Canvas private void RecentTab_Click(object sender, RoutedEventArgs e) { + UpdateActivityTime(); CommonTimersGrid.Visibility = Visibility.Collapsed; RecentTimersGrid.Visibility = Visibility.Visible; @@ -969,36 +1014,42 @@ namespace Ink_Canvas private void Common5Min_Click(object sender, RoutedEventArgs e) { if (isTimerRunning && !isPaused) return; + UpdateActivityTime(); SetQuickTime(0, 5, 0); } private void Common10Min_Click(object sender, RoutedEventArgs e) { if (isTimerRunning && !isPaused) return; + UpdateActivityTime(); SetQuickTime(0, 10, 0); } private void Common15Min_Click(object sender, RoutedEventArgs e) { if (isTimerRunning && !isPaused) return; + UpdateActivityTime(); SetQuickTime(0, 15, 0); } private void Common30Min_Click(object sender, RoutedEventArgs e) { if (isTimerRunning && !isPaused) return; + UpdateActivityTime(); SetQuickTime(0, 30, 0); } private void Common45Min_Click(object sender, RoutedEventArgs e) { if (isTimerRunning && !isPaused) return; + UpdateActivityTime(); SetQuickTime(0, 45, 0); } private void Common60Min_Click(object sender, RoutedEventArgs e) { if (isTimerRunning && !isPaused) return; + UpdateActivityTime(); SetQuickTime(1, 0, 0); } @@ -1006,36 +1057,42 @@ namespace Ink_Canvas private void RecentTimer1_Click(object sender, RoutedEventArgs e) { if ((isTimerRunning && !isPaused) || recentTimer1 == "--:--") return; + UpdateActivityTime(); ApplyRecentTimer(recentTimer1); } private void RecentTimer2_Click(object sender, RoutedEventArgs e) { if ((isTimerRunning && !isPaused) || recentTimer2 == "--:--") return; + UpdateActivityTime(); ApplyRecentTimer(recentTimer2); } private void RecentTimer3_Click(object sender, RoutedEventArgs e) { if ((isTimerRunning && !isPaused) || recentTimer3 == "--:--") return; + UpdateActivityTime(); ApplyRecentTimer(recentTimer3); } private void RecentTimer4_Click(object sender, RoutedEventArgs e) { if ((isTimerRunning && !isPaused) || recentTimer4 == "--:--") return; + UpdateActivityTime(); ApplyRecentTimer(recentTimer4); } private void RecentTimer5_Click(object sender, RoutedEventArgs e) { if ((isTimerRunning && !isPaused) || recentTimer5 == "--:--") return; + UpdateActivityTime(); ApplyRecentTimer(recentTimer5); } private void RecentTimer6_Click(object sender, RoutedEventArgs e) { if ((isTimerRunning && !isPaused) || recentTimer6 == "--:--") return; + UpdateActivityTime(); ApplyRecentTimer(recentTimer6); } @@ -1175,16 +1232,22 @@ namespace Ink_Canvas { try { - RecentTimer1Text.Text = recentTimer1; - RecentTimer2Text.Text = recentTimer2; - RecentTimer3Text.Text = recentTimer3; - RecentTimer4Text.Text = recentTimer4; - RecentTimer5Text.Text = recentTimer5; - RecentTimer6Text.Text = recentTimer6; + var timer1Text = this.FindName("RecentTimer1Text") as TextBlock; + var timer2Text = this.FindName("RecentTimer2Text") as TextBlock; + var timer3Text = this.FindName("RecentTimer3Text") as TextBlock; + var timer4Text = this.FindName("RecentTimer4Text") as TextBlock; + var timer5Text = this.FindName("RecentTimer5Text") as TextBlock; + var timer6Text = this.FindName("RecentTimer6Text") as TextBlock; + + if (timer1Text != null) timer1Text.Text = recentTimer1; + if (timer2Text != null) timer2Text.Text = recentTimer2; + if (timer3Text != null) timer3Text.Text = recentTimer3; + if (timer4Text != null) timer4Text.Text = recentTimer4; + if (timer5Text != null) timer5Text.Text = recentTimer5; + if (timer6Text != null) timer6Text.Text = recentTimer6; } catch { - // 如果UI元素还未初始化,忽略错误 } } @@ -1290,135 +1353,202 @@ namespace Ink_Canvas } } + private FullscreenTimerWindow fullscreenWindow; + + public bool IsFullscreenWindowOpen => fullscreenWindow != null && fullscreenWindow.IsVisible; + private void Fullscreen_Click(object sender, RoutedEventArgs e) { - ShowFullscreenTimer(); + if (fullscreenWindow != null && fullscreenWindow.IsVisible) + { + fullscreenWindow.Close(); + fullscreenWindow = null; + return; + } + + if (isTimerRunning && !isPaused) + { + fullscreenWindow = new FullscreenTimerWindow(this); + fullscreenWindow.Closed += (s, args) => { fullscreenWindow = null; }; + fullscreenWindow.Show(); + HideMinimizedRequested?.Invoke(this, EventArgs.Empty); + } } - - private void ShowFullscreenTimer() + + private void MainBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { - // 设置全屏模式标志 - isFullscreenMode = true; + UpdateActivityTime(); + if (e.ClickCount == 1) + { + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + if (timerContainer != null) + { + var point = e.GetPosition(timerContainer); + var mainWindowPoint = timerContainer.TransformToAncestor(mainWindow).Transform(point); + DragTimerContainer(mainWindow, mainWindowPoint, e); + } + } + } + } + + private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + UpdateActivityTime(); + if (e.ClickCount == 1) + { + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + if (timerContainer != null) + { + var point = e.GetPosition(timerContainer); + var mainWindowPoint = timerContainer.TransformToAncestor(mainWindow).Transform(point); + DragTimerContainer(mainWindow, mainWindowPoint, e); + } + } + } + } + + private bool isDragging = false; + private Point dragStartPoint; + private Point containerStartPosition; + + private void DragTimerContainer(MainWindow mainWindow, Point startPoint, MouseButtonEventArgs e) + { + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + if (timerContainer == null) return; - // 创建全屏计时器窗口 - fullscreenWindow = new FullscreenTimerWindow(this); - fullscreenWindow.Show(); + isDragging = true; + dragStartPoint = startPoint; - // 确保全屏窗口也置顶 - fullscreenWindow.Topmost = true; + if (timerContainer.HorizontalAlignment == HorizontalAlignment.Center || + timerContainer.VerticalAlignment == VerticalAlignment.Center) + { + var timerPoint = timerContainer.TransformToAncestor(mainWindow).Transform(new Point(0, 0)); + containerStartPosition = new Point(timerPoint.X, timerPoint.Y); + + timerContainer.Margin = new Thickness(containerStartPosition.X, containerStartPosition.Y, 0, 0); + timerContainer.HorizontalAlignment = HorizontalAlignment.Left; + timerContainer.VerticalAlignment = VerticalAlignment.Top; + } + else + { + var margin = timerContainer.Margin; + containerStartPosition = new Point(margin.Left, margin.Top); + + if (double.IsNaN(containerStartPosition.X) || containerStartPosition.X < 0) containerStartPosition.X = 0; + if (double.IsNaN(containerStartPosition.Y) || containerStartPosition.Y < 0) containerStartPosition.Y = 0; + } - // 隐藏主窗口 - this.Hide(); + timerContainer.CaptureMouse(); + timerContainer.MouseMove += TimerContainer_MouseMove; + timerContainer.MouseLeftButtonUp += TimerContainer_MouseLeftButtonUp; + e.Handled = true; + } + + private void TimerContainer_MouseMove(object sender, MouseEventArgs e) + { + if (!isDragging) return; + + UpdateActivityTime(); + + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow == null) return; + + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement; + if (timerContainer == null) return; + + var currentPoint = e.GetPosition(mainWindow); + var deltaX = currentPoint.X - dragStartPoint.X; + var deltaY = currentPoint.Y - dragStartPoint.Y; + + var newX = containerStartPosition.X + deltaX; + var newY = containerStartPosition.Y + deltaY; + + if (newX < 0) newX = 0; + if (newY < 0) newY = 0; + + timerContainer.Margin = new Thickness(newX, newY, 0, 0); + timerContainer.HorizontalAlignment = HorizontalAlignment.Left; + timerContainer.VerticalAlignment = VerticalAlignment.Top; + + if (minimizedContainer != null && minimizedContainer.Visibility == Visibility.Visible) + { + minimizedContainer.Margin = new Thickness(newX, newY, 0, 0); + minimizedContainer.HorizontalAlignment = HorizontalAlignment.Left; + minimizedContainer.VerticalAlignment = VerticalAlignment.Top; + } + } + + private void TimerContainer_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (!isDragging) return; + + isDragging = false; + + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow == null) return; + + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + if (timerContainer != null) + { + timerContainer.ReleaseMouseCapture(); + timerContainer.MouseMove -= TimerContainer_MouseMove; + timerContainer.MouseLeftButtonUp -= TimerContainer_MouseLeftButtonUp; + } } private void HandleTimerCompletion() { - if (minimizedWindow != null) - { - minimizedWindow.Close(); - minimizedWindow = null; - this.Show(); - this.Activate(); - this.WindowState = WindowState.Normal; - // 重新应用置顶 - ApplyTimerWindowTopmost(); - } - else if (fullscreenWindow != null) - { - fullscreenWindow.Close(); - fullscreenWindow = null; - isFullscreenMode = false; - this.Show(); - this.Activate(); - this.WindowState = WindowState.Normal; - // 重新应用置顶 - ApplyTimerWindowTopmost(); - } } - - #region Win32 API 声明和置顶管理 - [DllImport("user32.dll")] - private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); - - [DllImport("user32.dll")] - private static extern int GetWindowLong(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll")] - private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); - - [DllImport("user32.dll")] - private static extern bool IsWindow(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern bool IsIconic(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - [DllImport("kernel32.dll")] - private static extern uint GetCurrentProcessId(); - - private const int GWL_EXSTYLE = -20; - private const int WS_EX_TOPMOST = 0x00000008; - private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); - private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); - private const uint SWP_NOMOVE = 0x0002; - private const uint SWP_NOSIZE = 0x0001; - private const uint SWP_NOACTIVATE = 0x0010; - private const uint SWP_SHOWWINDOW = 0x0040; - private const uint SWP_NOOWNERZORDER = 0x0200; - - /// - /// 应用计时器窗口置顶 - /// - private void ApplyTimerWindowTopmost() + + private void HideTimer_Elapsed(object sender, ElapsedEventArgs e) { - try + if (!isTimerRunning || isPaused) return; + + Application.Current.Dispatcher.Invoke(() => { - var hwnd = new WindowInteropHelper(this).Handle; - if (hwnd == IntPtr.Zero) return; - - // 强制激活窗口 - Activate(); - Focus(); - - // 设置WPF的Topmost属性 - Topmost = true; - - // 使用Win32 API强制置顶 - // 1. 设置窗口样式为置顶 - int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); - SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST); - - // 2. 使用SetWindowPos确保窗口在最顶层 - SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOOWNERZORDER); - - LogHelper.WriteLogToFile("计时器窗口已应用置顶", LogHelper.LogType.Trace); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"应用计时器窗口置顶失败: {ex.Message}", LogHelper.LogType.Error); - } + var timeSinceLastActivity = DateTime.Now - lastActivityTime; + + if (timeSinceLastActivity.TotalSeconds >= 5) + { + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) + { + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + if (timerContainer != null && timerContainer.Visibility == Visibility.Visible) + { + ShowMinimizedRequested?.Invoke(this, EventArgs.Empty); + } + } + } + }); } - - /// - /// 窗口加载事件处理,确保置顶 - /// - private void TimerWindow_Loaded(object sender, RoutedEventArgs e) + + public void UpdateActivityTime() { - // 使用延迟确保窗口完全加载后再应用置顶 - Dispatcher.BeginInvoke(new Action(() => + lastActivityTime = DateTime.Now; + + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow != null) { - ApplyTimerWindowTopmost(); - }), DispatcherPriority.Loaded); + var timerContainer = mainWindow.FindName("TimerContainer") as FrameworkElement; + var minimizedContainer = mainWindow.FindName("MinimizedTimerContainer") as FrameworkElement; + + if (timerContainer != null && minimizedContainer != null) + { + if (timerContainer.Visibility == Visibility.Collapsed && minimizedContainer.Visibility == Visibility.Visible) + { + HideMinimizedRequested?.Invoke(this, EventArgs.Empty); + } + } + } } - #endregion + } }