@@ -1 +1 @@
|
||||
1.7.17.0
|
||||
1.7.18.0
|
||||
|
||||
+55
-9
@@ -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());
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
+12
-14
@@ -1,22 +1,19 @@
|
||||
<Window x:Class="Ink_Canvas.QuickDrawFloatingButton"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Topmost="True" Background="Transparent"
|
||||
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True"
|
||||
Loaded="FloatingButton_Loaded" WindowStartupLocation="Manual"
|
||||
ShowInTaskbar="False" Focusable="False"
|
||||
Title="快抽悬浮按钮" Height="45" Width="65">
|
||||
|
||||
<Window.Resources>
|
||||
<UserControl x:Class="Ink_Canvas.Controls.QuickDrawFloatingButtonControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Width="65" Height="45">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<!-- 悬浮按钮资源 -->
|
||||
<SolidColorBrush x:Key="QuickDrawFloatingButtonBackground" Color="#80000000"/>
|
||||
<SolidColorBrush x:Key="QuickDrawFloatingButtonBorderBrush" Color="#40000000"/>
|
||||
<SolidColorBrush x:Key="QuickDrawFloatingButtonIconForeground" Color="White"/>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Background="{DynamicResource QuickDrawFloatingButtonBackground}"
|
||||
CornerRadius="8"
|
||||
@@ -85,4 +82,5 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
</UserControl>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 快抽悬浮按钮控件
|
||||
/// </summary>
|
||||
public partial class QuickDrawFloatingButtonControl : UserControl
|
||||
{
|
||||
private bool _isDragging = false;
|
||||
private Point _dragStartPoint;
|
||||
private Point _controlStartPoint;
|
||||
|
||||
public QuickDrawFloatingButtonControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快抽按钮点击事件
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拖动区域鼠标按下事件
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拖动区域鼠标移动事件
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拖动区域鼠标释放事件
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调整图像大小
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调整图像大小
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
/// <summary>
|
||||
/// 上传队列项
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传队列项
|
||||
@@ -39,6 +55,205 @@ namespace Ink_Canvas.Helpers
|
||||
/// </summary>
|
||||
private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// 队列保存锁,防止并发保存
|
||||
/// </summary>
|
||||
private static readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// 是否已初始化队列
|
||||
/// </summary>
|
||||
private static bool _isQueueInitialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取队列文件路径
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化上传队列
|
||||
/// </summary>
|
||||
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<List<UploadQueueItemData>>(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; // 即使出错也标记为已初始化,避免重复尝试
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存队列到文件
|
||||
/// </summary>
|
||||
private static async Task SaveQueueToFileAsync()
|
||||
{
|
||||
if (!await _queueSaveLock.WaitAsync(1000)) // 最多等待1秒
|
||||
{
|
||||
return; // 如果无法获取锁,跳过保存(避免阻塞)
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var queueData = new List<UploadQueueItemData>();
|
||||
|
||||
// 将队列转换为可序列化的格式
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空队列文件
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传笔记响应模型
|
||||
/// </summary>
|
||||
@@ -100,9 +315,9 @@ namespace Ink_Canvas.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步上传笔记文件到Dlass(支持PNG和ICSTK格式)
|
||||
/// 异步上传笔记文件到Dlass(支持PNG、ICSTK和ZIP格式)
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径(支持PNG和ICSTK)</param>
|
||||
/// <param name="filePath">文件路径(支持PNG、ICSTK和ZIP)</param>
|
||||
/// <returns>是否成功加入队列(不等待实际上传完成)</returns>
|
||||
public static async Task<bool> 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 多PPT墨迹管理器 - 支持多个PPT窗口分别管理墨迹
|
||||
/// </summary>
|
||||
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<string, PPTInkManager> _presentationManagers;
|
||||
private readonly Dictionary<string, PresentationInfo> _presentationInfos;
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _disposed;
|
||||
private string _currentActivePresentationId = "";
|
||||
|
||||
// 墨迹备份机制
|
||||
private readonly Dictionary<string, Dictionary<int, StrokeCollection>> _strokeBackups;
|
||||
private DateTime _lastBackupTime = DateTime.MinValue;
|
||||
private const int BackupIntervalMinutes = 2; // 每2分钟备份一次
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public MultiPPTInkManager()
|
||||
{
|
||||
_presentationManagers = new Dictionary<string, PPTInkManager>();
|
||||
_presentationInfos = new Dictionary<string, PresentationInfo>();
|
||||
_strokeBackups = new Dictionary<string, Dictionary<int, StrokeCollection>>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// 初始化新的演示文稿
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定的演示文稿
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前页面的墨迹
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制保存指定页面的墨迹(忽略锁定状态)
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载指定页面的墨迹
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换到指定页面并加载墨迹
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存所有墨迹到文件
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载已保存的墨迹
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除指定演示文稿的所有墨迹
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有演示文稿的墨迹
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻页后锁定墨迹写入
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以写入墨迹
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置当前演示文稿的墨迹锁定状态
|
||||
/// </summary>
|
||||
public void ResetCurrentPresentationLockState()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manager = GetCurrentManager();
|
||||
if (manager != null)
|
||||
{
|
||||
manager.ResetLockState();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"重置墨迹锁定状态失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除演示文稿管理器
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前管理的演示文稿数量
|
||||
/// </summary>
|
||||
public int GetPresentationCount()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _presentationManagers.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有演示文稿信息
|
||||
/// </summary>
|
||||
public List<PresentationInfo> GetAllPresentationInfos()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _presentationInfos.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理长时间未访问的演示文稿管理器
|
||||
/// </summary>
|
||||
public void CleanupInactivePresentations(TimeSpan inactiveThreshold)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var inactiveIds = new List<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建墨迹备份
|
||||
/// </summary>
|
||||
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<int, StrokeCollection>();
|
||||
}
|
||||
|
||||
// 释放旧的备份
|
||||
if (_strokeBackups[presentationId].ContainsKey(slideIndex))
|
||||
{
|
||||
_strokeBackups[presentationId][slideIndex] = null;
|
||||
}
|
||||
|
||||
// 创建新的备份
|
||||
_strokeBackups[presentationId][slideIndex] = strokes.Clone();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"创建墨迹备份失败: {ex}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从备份恢复墨迹
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并执行定期备份
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示文稿信息
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
<Slider x:Name="MLAvoidanceHistorySlider" Minimum="5"
|
||||
Maximum="50" Width="168" FontFamily="Microsoft YaHei UI"
|
||||
ValueChanged="MLAvoidanceHistorySlider_ValueChanged"
|
||||
FontSize="20" IsSnapToTickEnabled="True" Value="20" TickFrequency="5"
|
||||
FontSize="20" IsSnapToTickEnabled="True" Value="50" TickFrequency="5"
|
||||
TickPlacement="None" AutoToolTipPlacement="None" />
|
||||
<TextBlock VerticalAlignment="Center" Margin="12,0,16,0" FontSize="14"
|
||||
FontFamily="Consolas"
|
||||
@@ -3383,7 +3385,7 @@
|
||||
<Slider x:Name="MLAvoidanceWeightSlider" Minimum="0.1"
|
||||
Maximum="1.0" Width="168" FontFamily="Microsoft YaHei UI"
|
||||
ValueChanged="MLAvoidanceWeightSlider_ValueChanged"
|
||||
FontSize="20" IsSnapToTickEnabled="True" Value="0.8" TickFrequency="0.1"
|
||||
FontSize="20" IsSnapToTickEnabled="True" Value="1.0" TickFrequency="0.1"
|
||||
TickPlacement="None" AutoToolTipPlacement="None" />
|
||||
<TextBlock VerticalAlignment="Center" Margin="12,0,16,0" FontSize="14"
|
||||
FontFamily="Consolas"
|
||||
@@ -3956,6 +3958,14 @@
|
||||
</Image.RenderTransform>
|
||||
</Image>
|
||||
</Canvas>
|
||||
|
||||
<!-- 快抽悬浮按钮 -->
|
||||
<controls:QuickDrawFloatingButtonControl x:Name="QuickDrawFloatingButton"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,200"
|
||||
Visibility="Collapsed"
|
||||
Panel.ZIndex="1001"/>
|
||||
</Grid>
|
||||
|
||||
<Canvas IsHitTestVisible="False">
|
||||
@@ -9941,6 +9951,27 @@
|
||||
</ui:SimpleStackPanel>
|
||||
</Border>
|
||||
</Viewbox>
|
||||
|
||||
<Border x:Name="TimerContainer"
|
||||
Visibility="Collapsed"
|
||||
Panel.ZIndex="998"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Width="900"
|
||||
Height="500">
|
||||
<Windows:TimerControl x:Name="TimerControl"/>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="MinimizedTimerContainer"
|
||||
Visibility="Collapsed"
|
||||
Panel.ZIndex="997"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Margin="20,20,0,0"
|
||||
Width="600"
|
||||
Height="200">
|
||||
<Windows:MinimizedTimerControl x:Name="MinimizedTimerControl"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
|
||||
+232
-71
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 置顶维护定时器事件
|
||||
/// </summary>
|
||||
@@ -2349,47 +2563,6 @@ namespace Ink_Canvas
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PPT翻页直接传递
|
||||
/// <summary>
|
||||
/// 直接发送翻页请求到PPT放映软件,让PPT软件处理翻页
|
||||
/// </summary>
|
||||
/// <param name="isPrevious">是否为上一页</param>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 初始化文件关联状态显示
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -536,6 +536,17 @@ namespace Ink_Canvas
|
||||
operatingGuideWindow.RefreshTheme();
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新计时器控件
|
||||
if (TimerControl != null)
|
||||
{
|
||||
TimerControl.RefreshTheme();
|
||||
}
|
||||
|
||||
if (MinimizedTimerControl != null)
|
||||
{
|
||||
MinimizedTimerControl.RefreshTheme();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查墨迹是否为复杂形状(如一团墨迹、涂鸦等)
|
||||
/// 检查墨迹是否为复杂形状
|
||||
/// </summary>
|
||||
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<Point> filteredPoints = new List<Point>();
|
||||
|
||||
// 收集过滤后的点(跳过前 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
<Border Background="{DynamicResource TimerWindowBackground}" CornerRadius="10" BorderThickness="1" BorderBrush="{DynamicResource TimerWindowBorderBrush}" Margin="60">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Window x:Class="Ink_Canvas.FullscreenTimerWindow"
|
||||
<Window x:Class="Ink_Canvas.Windows.FullscreenTimerWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Ink_Canvas"
|
||||
xmlns:local="clr-namespace:Ink_Canvas.Windows"
|
||||
Topmost="True" Background="Black"
|
||||
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="False"
|
||||
WindowState="Maximized" WindowStartupLocation="Manual"
|
||||
|
||||
@@ -1,43 +1,115 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using System.Timers;
|
||||
|
||||
namespace Ink_Canvas
|
||||
namespace Ink_Canvas.Windows
|
||||
{
|
||||
/// <summary>
|
||||
/// 全屏计时器窗口
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 应用全屏窗口置顶
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
|
||||
+14
-17
@@ -1,23 +1,17 @@
|
||||
<Window x:Class="Ink_Canvas.MinimizedTimerWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Ink_Canvas"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
Topmost="True" Background="Transparent"
|
||||
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True"
|
||||
WindowStartupLocation="Manual" Title="计时器" Height="200" Width="600"
|
||||
MouseLeftButtonDown="Window_MouseLeftButtonDown" MouseLeftButtonUp="Window_MouseLeftButtonUp"
|
||||
MouseEnter="Window_MouseEnter" MouseLeave="Window_MouseLeave" MouseMove="Window_MouseMove">
|
||||
|
||||
<Window.Resources>
|
||||
<UserControl x:Class="Ink_Canvas.Windows.MinimizedTimerControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="200" d:DesignWidth="600">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="DigitResources.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border x:Name="MainBorder" Background="{DynamicResource NewTimerWindowBackground}"
|
||||
CornerRadius="15"
|
||||
@@ -25,7 +19,9 @@
|
||||
BorderBrush="{DynamicResource NewTimerWindowBorderBrush}"
|
||||
Margin="0"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True">
|
||||
SnapsToDevicePixels="True"
|
||||
MouseLeftButtonDown="MainBorder_MouseLeftButtonDown"
|
||||
Cursor="Hand">
|
||||
<Grid>
|
||||
<!-- 时间显示 -->
|
||||
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20,20,20,20">
|
||||
@@ -122,4 +118,5 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
</UserControl>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 最小化计时器窗口
|
||||
/// </summary>
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新主题
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 最小化计时器窗口
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据屏幕分辨率和 DPI 缩放窗口大小(保持原始尺寸,使用Transform缩放)
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置最小化窗口冒号显示颜色
|
||||
/// </summary>
|
||||
/// <param name="isRed">是否显示为红色</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace Ink_Canvas
|
||||
{
|
||||
public List<string> History { get; set; } = new List<string>();
|
||||
public Dictionary<string, int> NameFrequency { get; set; } = new Dictionary<string, int>();
|
||||
public Dictionary<string, double> NameProbabilities { get; set; } = new Dictionary<string, double>();
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用机器学习算法选择单个人员
|
||||
/// 使用概率算法选择单个人员
|
||||
/// </summary>
|
||||
private static string SelectSingleNameWithML(List<string> availableNames, List<string> alreadySelected, Random random)
|
||||
{
|
||||
if (availableNames.Count == 0) return null;
|
||||
if (availableNames.Count == 1) return availableNames[0];
|
||||
|
||||
// 计算每个人员的权重
|
||||
var nameWeights = new Dictionary<string, double>();
|
||||
// 确保历史数据已初始化
|
||||
if (historyData == null)
|
||||
{
|
||||
LoadRollCallHistory();
|
||||
}
|
||||
|
||||
// 初始化概率字典
|
||||
if (historyData.NameProbabilities == null)
|
||||
{
|
||||
historyData.NameProbabilities = new Dictionary<string, double>();
|
||||
}
|
||||
|
||||
// 获取每个人员的概率
|
||||
var nameProbabilities = new Dictionary<string, double>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取人员的概率
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据最近历史记录调整概率
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据频率统计更新保存的概率
|
||||
/// </summary>
|
||||
private static void UpdateProbabilitiesByFrequency()
|
||||
{
|
||||
if (historyData == null || historyData.NameFrequency == null || historyData.NameFrequency.Count == 0)
|
||||
return;
|
||||
|
||||
if (historyData.NameProbabilities == null)
|
||||
historyData.NameProbabilities = new Dictionary<string, double>();
|
||||
|
||||
// 计算总选中次数
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于概率的随机选择
|
||||
/// </summary>
|
||||
private static string ProbabilityBasedRandomSelection(Dictionary<string, double> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -600,7 +840,7 @@ namespace Ink_Canvas
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加权随机选择
|
||||
/// 加权随机选择(保留用于兼容,实际已改用概率选择)
|
||||
/// </summary>
|
||||
private static string WeightedRandomSelection(Dictionary<string, double> nameWeights, Random random)
|
||||
{
|
||||
@@ -639,29 +879,96 @@ namespace Ink_Canvas
|
||||
|
||||
lock (historyLock)
|
||||
{
|
||||
// 初始化概率字典
|
||||
if (historyData.NameProbabilities == null)
|
||||
{
|
||||
historyData.NameProbabilities = new Dictionary<string, double>();
|
||||
}
|
||||
|
||||
// 更新历史记录
|
||||
if (historyData.History == null)
|
||||
historyData.History = new List<string>();
|
||||
|
||||
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<string, int>();
|
||||
// 更新频率统计
|
||||
if (historyData.NameFrequency == null)
|
||||
historyData.NameFrequency = new Dictionary<string, int>();
|
||||
|
||||
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<string>(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<string, double>();
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 快抽悬浮按钮
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 应用无焦点模式
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用悬浮按钮置顶
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动置顶维护定时器
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止置顶维护定时器
|
||||
/// </summary>
|
||||
private void StopTopmostMaintenance()
|
||||
{
|
||||
if (topmostMaintenanceTimer != null && isTopmostMaintenanceEnabled)
|
||||
{
|
||||
topmostMaintenanceTimer.Stop();
|
||||
isTopmostMaintenanceEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 置顶维护定时器事件
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口关闭时停止置顶维护定时器
|
||||
/// </summary>
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
StopTopmostMaintenance();
|
||||
base.OnClosed(e);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,19 +48,74 @@ namespace Ink_Canvas
|
||||
return;
|
||||
}
|
||||
|
||||
// 按时间倒序显示(最新的在上方)
|
||||
// 由于历史记录是按时间顺序添加的,所以直接反转即可
|
||||
var reversedHistory = historyData.History.ToList();
|
||||
reversedHistory.Reverse();
|
||||
// 计算每个名字的总累计抽选次数(用于统计信息)
|
||||
var nameCountDict = new System.Collections.Generic.Dictionary<string, int>();
|
||||
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<System.Tuple<string, int>>();
|
||||
var runningCount = new System.Collections.Generic.Dictionary<string, int>();
|
||||
foreach (var name in historyData.History)
|
||||
{
|
||||
if (runningCount.ContainsKey(name))
|
||||
runningCount[name]++;
|
||||
else
|
||||
runningCount[name] = 1;
|
||||
historyWithCount.Add(new System.Tuple<string, int>(name, runningCount[name]));
|
||||
}
|
||||
|
||||
// 按时间倒序显示(最新的在上方)
|
||||
historyWithCount.Reverse();
|
||||
|
||||
// 显示历史记录,每行显示:名字 (累计X次)
|
||||
var historyLines = new System.Collections.Generic.List<string>();
|
||||
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<string>();
|
||||
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;
|
||||
|
||||
+483
-303
@@ -1,31 +1,35 @@
|
||||
<Window x:Class="Ink_Canvas.NewStyleTimerWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Ink_Canvas"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:controls="clr-namespace:Ink_Canvas.Windows.Controls"
|
||||
Topmost="True" Background="Transparent"
|
||||
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True"
|
||||
Loaded="Window_Loaded" Closing="Window_Closing" WindowStartupLocation="CenterScreen"
|
||||
MouseMove="Window_MouseMove" MouseEnter="Window_MouseEnter"
|
||||
Title="Ink Canvas 画板 - 计时器" Height="450" Width="900">
|
||||
|
||||
<Window.Resources>
|
||||
<UserControl x:Class="Ink_Canvas.Windows.TimerControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:controls="clr-namespace:Ink_Canvas.Windows.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="900">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Controls/WinUI3CloseButton.xaml" />
|
||||
<ResourceDictionary Source="DigitResources.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Background="{DynamicResource NewTimerWindowBackground}" CornerRadius="15" BorderThickness="1" BorderBrush="{DynamicResource NewTimerWindowBorderBrush}" Margin="10" x:Name="MainBorder" MouseLeftButtonDown="WindowDragMove">
|
||||
<Border Background="{DynamicResource NewTimerWindowBackground}"
|
||||
CornerRadius="15"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource NewTimerWindowBorderBrush}"
|
||||
Margin="10"
|
||||
x:Name="MainBorder"
|
||||
MouseLeftButtonDown="MainBorder_MouseLeftButtonDown">
|
||||
<Grid>
|
||||
<controls:WinUI3CloseButton x:Name="CloseButton"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Top"
|
||||
Margin="0,0,0,0" Cursor="Hand" Click="CloseButton_Click"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,0,0,0"
|
||||
Cursor="Hand"
|
||||
Click="CloseButton_Click"
|
||||
Content="✕"/>
|
||||
<!-- 主要内容区域 -->
|
||||
<Grid>
|
||||
@@ -33,8 +37,16 @@
|
||||
<Viewbox x:Name="MainViewController" Margin="20,20,20,20">
|
||||
<Grid Height="400" Width="900">
|
||||
<!-- 顶部标题栏 -->
|
||||
<Grid Height="50" Background="{DynamicResource NewTimerWindowBackground}" x:Name="TitleBar" VerticalAlignment="Top" MouseLeftButtonDown="WindowDragMove" Margin="0,0,450,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="22,0,0,0">
|
||||
<Grid Height="50"
|
||||
Background="{DynamicResource NewTimerWindowBackground}"
|
||||
x:Name="TitleBar"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,0,450,0"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Margin="22,0,0,0">
|
||||
<!-- 计时器图标 -->
|
||||
<Path Data="M5 13a7 7 0 1 0 14 0a7 7 0 0 0 -14 0z M14.5 10.5l-2.5 2.5 M17 8l1 -1 M14 3h-4"
|
||||
Stroke="{DynamicResource NewTimerWindowTitleForeground}"
|
||||
@@ -45,35 +57,56 @@
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,8,0"/>
|
||||
<!-- 计时文字 -->
|
||||
<TextBlock Text="计时" FontSize="28" FontWeight="Bold"
|
||||
Foreground="{DynamicResource NewTimerWindowTitleForeground}" x:Name="TitleText"/>
|
||||
<TextBlock Text="计时"
|
||||
FontSize="28"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource NewTimerWindowTitleForeground}"
|
||||
x:Name="TitleText"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<!-- 主要内容区域 - 分为左右两部分 -->
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Center"
|
||||
x:Name="MainContentGrid" Margin="0,-25,0,25">
|
||||
<Grid HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
x:Name="MainContentGrid"
|
||||
Margin="0,-25,0,25">
|
||||
<!-- 左侧:6位数字显示区域 -->
|
||||
<Grid HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
x:Name="MainDisplayGrid" Width="500" Margin="-10,0,0,0">
|
||||
<!-- 6位数字水平排列 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
|
||||
x:Name="DigitsPanel">
|
||||
<!-- 小时组(十位和个位) -->
|
||||
<Grid Margin="0,0,20,0" HorizontalAlignment="Center" Width="120">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<!-- 第1位数字(小时十位) -->
|
||||
<Grid Margin="0,0,15,0">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit1PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit1Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Grid HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
x:Name="MainDisplayGrid"
|
||||
Width="500"
|
||||
Margin="-10,0,0,0">
|
||||
<!-- 6位数字水平排列 -->
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
x:Name="DigitsPanel">
|
||||
<!-- 小时组(十位和个位) -->
|
||||
<Grid Margin="0,0,20,0"
|
||||
HorizontalAlignment="Center"
|
||||
Width="120">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 第1位数字(小时十位) -->
|
||||
<Grid Margin="0,0,15,0">
|
||||
<StackPanel Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit1PlusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit1Plus_Click"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,0,15"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M12 5l0 14 M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -83,25 +116,33 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit1Display" Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit1MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit1Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit1Display"
|
||||
Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit1MinusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit1Minus_Click"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -111,23 +152,31 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 第2位数字(小时个位) -->
|
||||
<Grid>
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit2PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit2Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<!-- 第2位数字(小时个位) -->
|
||||
<Grid>
|
||||
<StackPanel Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit2PlusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit2Plus_Click"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,0,15"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M12 5l0 14 M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -137,25 +186,33 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit2Display" Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit2MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit2Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit2Display"
|
||||
Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit2MinusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit2Minus_Click"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -165,34 +222,48 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<!-- 冒号分隔符 -->
|
||||
<TextBlock x:Name="Colon1Display" Text=":" FontSize="48" FontWeight="Bold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
<TextBlock x:Name="Colon1Display"
|
||||
Text=":"
|
||||
FontSize="48"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Margin="-20,0,0,0"/>
|
||||
|
||||
<!-- 分钟组(十位和个位) -->
|
||||
<Grid Margin="0,0,20,0" HorizontalAlignment="Center" Width="120">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<!-- 第3位数字(分钟十位) -->
|
||||
<Grid Margin="0,0,15,0">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit3PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit3Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<!-- 分钟组(十位和个位) -->
|
||||
<Grid Margin="0,0,20,0"
|
||||
HorizontalAlignment="Center"
|
||||
Width="120">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 第3位数字(分钟十位) -->
|
||||
<Grid Margin="0,0,15,0">
|
||||
<StackPanel Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit3PlusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit3Plus_Click"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,0,15"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M12 5l0 14 M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -202,25 +273,33 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit3Display" Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit3MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit3Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit3Display"
|
||||
Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit3MinusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit3Minus_Click"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -230,23 +309,31 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 第4位数字(分钟个位) -->
|
||||
<Grid>
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit4PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit4Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<!-- 第4位数字(分钟个位) -->
|
||||
<Grid>
|
||||
<StackPanel Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit4PlusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit4Plus_Click"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,0,15"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M12 5l0 14 M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -256,25 +343,33 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit4Display" Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit4MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit4Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit4Display"
|
||||
Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit4MinusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit4Minus_Click"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -284,34 +379,46 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<!-- 冒号分隔符 -->
|
||||
<TextBlock x:Name="Colon2Display" Text=":" FontSize="48" FontWeight="Bold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
<TextBlock x:Name="Colon2Display"
|
||||
Text=":"
|
||||
FontSize="48"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Margin="-20,0,0,0"/>
|
||||
|
||||
<!-- 秒组(十位和个位) -->
|
||||
<Grid Width="120">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<!-- 第5位数字(秒十位) -->
|
||||
<Grid Margin="0,0,15,0">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit5PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit5Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<!-- 秒组(十位和个位) -->
|
||||
<Grid Width="120">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 第5位数字(秒十位) -->
|
||||
<Grid Margin="0,0,15,0">
|
||||
<StackPanel Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit5PlusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit5Plus_Click"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,0,15"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M12 5l0 14 M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -321,25 +428,33 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit5Display" Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit5MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit5Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit5Display"
|
||||
Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit5MinusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit5Minus_Click"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -349,23 +464,31 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 第6位数字(秒个位) -->
|
||||
<Grid>
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit6PlusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit6Plus_Click" Cursor="Hand" Margin="0,0,0,15" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<!-- 第6位数字(秒个位) -->
|
||||
<Grid>
|
||||
<StackPanel Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 上方+按钮 -->
|
||||
<Button x:Name="Digit6PlusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit6Plus_Click"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,0,15"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M12 5l0 14 M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -375,25 +498,33 @@
|
||||
Fill="Transparent"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"/>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit6Display" Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit6MinusBtn" Width="40" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Digit6Minus_Click" Cursor="Hand" HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<!-- 数字显示 -->
|
||||
<Path x:Name="Digit6Display"
|
||||
Data="{StaticResource Digit0}"
|
||||
Fill="{DynamicResource NewTimerWindowDigitForeground}"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,0,15"/>
|
||||
<!-- 下方-按钮 -->
|
||||
<Button x:Name="Digit6MinusBtn"
|
||||
Width="40" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Digit6Minus_Click"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Center">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="15">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path Data="M5 12l14 0"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -410,16 +541,22 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<Border Width="2" Background="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
Opacity="0.3" HorizontalAlignment="Left" VerticalAlignment="Stretch"
|
||||
<Border Width="2"
|
||||
Background="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
Opacity="0.3"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch"
|
||||
Margin="465,0,0,0"/>
|
||||
|
||||
<!-- 右侧:快捷选项区域 -->
|
||||
<Grid HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
x:Name="QuickOptionsGrid" Width="400" Margin="470,0,0,0" Height="200">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<Grid HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
x:Name="QuickOptionsGrid"
|
||||
Width="400"
|
||||
Margin="470,0,0,0"
|
||||
Height="200">
|
||||
<StackPanel Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- Segmented Control -->
|
||||
<Grid Margin="0,0,0,20">
|
||||
<Border Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
@@ -436,7 +573,6 @@
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,0,0"/>
|
||||
|
||||
<!-- 按钮容器 -->
|
||||
<Grid>
|
||||
<Button x:Name="CommonTabBtn"
|
||||
@@ -446,7 +582,10 @@
|
||||
Click="CommonTab_Click"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Left">
|
||||
<TextBlock x:Name="CommonTabText" Text="常用" FontSize="16" FontWeight="Bold"
|
||||
<TextBlock x:Name="CommonTabText"
|
||||
Text="常用"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
|
||||
</Button>
|
||||
<Button x:Name="RecentTabBtn"
|
||||
@@ -456,14 +595,17 @@
|
||||
Click="RecentTab_Click"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Right">
|
||||
<TextBlock x:Name="RecentTabText" Text="最近" FontSize="16" FontWeight="Normal"
|
||||
Foreground="{DynamicResource NewTimerWindowButtonForeground}" Opacity="0.6"/>
|
||||
<TextBlock x:Name="RecentTabText"
|
||||
Text="最近"
|
||||
FontSize="16"
|
||||
FontWeight="Normal"
|
||||
Foreground="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
Opacity="0.6"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- 常用计时区域 -->
|
||||
<Grid x:Name="CommonTimersGrid" Visibility="Visible">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
@@ -618,9 +760,8 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<!-- 最近计时区域 -->
|
||||
<Grid x:Name="RecentTimersGrid" Visibility="Collapsed">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
@@ -662,7 +803,7 @@
|
||||
<TextBlock x:Name="RecentTimer3Text" Text="--:--" FontSize="16" FontWeight="Bold"
|
||||
Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 第二排:最近3个 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
@@ -708,21 +849,39 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- 底部控制按钮区域 -->
|
||||
<Grid HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,20">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Grid HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,20">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center">
|
||||
<!-- 全屏按钮 -->
|
||||
<Button x:Name="FullscreenBtn" Width="80" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Fullscreen_Click" Cursor="Hand" Margin="0,0,30,0">
|
||||
<Button x:Name="FullscreenBtn"
|
||||
Width="80" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Fullscreen_Click"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,30,0"
|
||||
IsEnabled="False">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="8">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="8"
|
||||
Opacity="1">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="border" Property="Opacity" Value="0.5"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center">
|
||||
<Path Data="M4 8v-2a2 2 0 0 1 2 -2h2 M4 16v2a2 2 0 0 0 2 2h2 M16 4h2a2 2 0 0 1 2 2v2 M16 20h2a2 2 0 0 0 2 -2v-2"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -731,37 +890,55 @@
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,5,0"/>
|
||||
<TextBlock Text="全屏" FontSize="16" Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
|
||||
<TextBlock Text="全屏"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 开始/暂停按钮 -->
|
||||
<Button x:Name="StartPauseBtn" Width="80" Height="80" Background="{DynamicResource NewTimerWindowPrimaryButtonBackground}"
|
||||
BorderThickness="0" Click="StartPause_Click" Cursor="Hand" Margin="0,0,30,0">
|
||||
<Button x:Name="StartPauseBtn"
|
||||
Width="80" Height="80"
|
||||
Background="{DynamicResource NewTimerWindowPrimaryButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="StartPause_Click"
|
||||
Cursor="Hand"
|
||||
Margin="0,0,30,0">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="40">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="40">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<Path x:Name="StartPauseIcon" Data="M6.5 4.00004V20C6.49995 20.178 6.54737 20.3527 6.63738 20.5062C6.72739 20.6597 6.85672 20.7864 7.01202 20.8732C7.16733 20.96 7.34299 21.0038 7.52088 21.0001C7.69878 20.9964 7.87245 20.9453 8.024 20.852L21.024 12.852C21.1696 12.7626 21.2898 12.6373 21.3733 12.4881C21.4567 12.339 21.5005 12.1709 21.5005 12C21.5005 11.8291 21.4567 11.6611 21.3733 11.512C21.2898 11.3628 21.1696 11.2375 21.024 11.148L8.024 3.14804C7.87245 3.0548 7.69878 3.00369 7.52088 2.99997C7.34299 2.99626 7.16733 3.04007 7.01202 3.1269C6.85672 3.21372 6.72739 3.34042 6.63738 3.4939C6.54737 3.64739 6.49995 3.82211 6.5 4.00004Z"
|
||||
Fill="{DynamicResource NewTimerWindowPrimaryButtonForeground}"
|
||||
Width="24" Height="24" Margin="2,0,0,0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="Fill"/>
|
||||
<Path x:Name="StartPauseIcon"
|
||||
Data="M6.5 4.00004V20C6.49995 20.178 6.54737 20.3527 6.63738 20.5062C6.72739 20.6597 6.85672 20.7864 7.01202 20.8732C7.16733 20.96 7.34299 21.0038 7.52088 21.0001C7.69878 20.9964 7.87245 20.9453 8.024 20.852L21.024 12.852C21.1696 12.7626 21.2898 12.6373 21.3733 12.4881C21.4567 12.339 21.5005 12.1709 21.5005 12C21.5005 11.8291 21.4567 11.6611 21.3733 11.512C21.2898 11.3628 21.1696 11.2375 21.024 11.148L8.024 3.14804C7.87245 3.0548 7.69878 3.00369 7.52088 2.99997C7.34299 2.99626 7.16733 3.04007 7.01202 3.1269C6.85672 3.21372 6.72739 3.34042 6.63738 3.4939C6.54737 3.64739 6.49995 3.82211 6.5 4.00004Z"
|
||||
Fill="{DynamicResource NewTimerWindowPrimaryButtonForeground}"
|
||||
Width="24" Height="24"
|
||||
Margin="2,0,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Fill"/>
|
||||
</Button>
|
||||
|
||||
<!-- 重置按钮 -->
|
||||
<Button x:Name="ResetBtn" Width="80" Height="40" Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0" Click="Reset_Click" Cursor="Hand">
|
||||
<Button x:Name="ResetBtn"
|
||||
Width="80" Height="40"
|
||||
Background="{DynamicResource NewTimerWindowButtonBackground}"
|
||||
BorderThickness="0"
|
||||
Click="Reset_Click"
|
||||
Cursor="Hand">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="8">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="8">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center">
|
||||
<Path Data="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4 M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
|
||||
Stroke="{DynamicResource NewTimerWindowButtonForeground}"
|
||||
StrokeThickness="2"
|
||||
@@ -770,7 +947,9 @@
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,5,0"/>
|
||||
<TextBlock Text="重置" FontSize="16" Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
|
||||
<TextBlock Text="重置"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource NewTimerWindowButtonForeground}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
@@ -780,4 +959,5 @@
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
</UserControl>
|
||||
|
||||
+346
-216
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 最近计时记录数据模型
|
||||
@@ -31,12 +29,11 @@ namespace Ink_Canvas
|
||||
/// <summary>
|
||||
/// 新计时器UI风格的倒计时器窗口
|
||||
/// </summary>
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新主题(供外部调用)
|
||||
/// </summary>
|
||||
public void RefreshTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 重新应用主题
|
||||
ApplyTheme();
|
||||
|
||||
// 强制刷新UI
|
||||
InvalidateVisual();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.WriteLogToFile($"刷新计时器窗口主题出错: {ex.Message}", LogHelper.LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#region 事件定义
|
||||
/// <summary>
|
||||
/// 计时器完成事件
|
||||
/// </summary>
|
||||
public event EventHandler TimerCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 关闭事件 - 通知主窗口隐藏容器
|
||||
/// </summary>
|
||||
public event EventHandler CloseRequested;
|
||||
|
||||
/// <summary>
|
||||
/// 显示最小化视图事件
|
||||
/// </summary>
|
||||
public event EventHandler ShowMinimizedRequested;
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏最小化视图事件
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据数字值设置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;
|
||||
|
||||
/// <summary>
|
||||
/// 应用计时器窗口置顶
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口加载事件处理,确保置顶
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user