diff --git a/Ink Canvas.sln b/Ink Canvas.sln
index 7907e2c2..91d90a3a 100644
--- a/Ink Canvas.sln
+++ b/Ink Canvas.sln
@@ -9,7 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.PluginSdk", "InkC
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.Controls", "InkCanvas.Controls\InkCanvas.Controls.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvasForClass.IACoreHelper", "InkCanvasForClass.IACoreHelper\InkCanvasForClass.IACoreHelper.csproj", "{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.IACoreHelper", "InkCanvas.IACoreHelper\InkCanvas.IACoreHelper.csproj", "{B1A2C3D4-E5F6-7890-ABCD-EF1234567891}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/Ink Canvas/Controls/ImageSelectionOverlay.xaml b/Ink Canvas/Controls/ImageSelectionOverlay.xaml
new file mode 100644
index 00000000..f3fe739c
--- /dev/null
+++ b/Ink Canvas/Controls/ImageSelectionOverlay.xaml
@@ -0,0 +1,60 @@
+
+
+
\ No newline at end of file
diff --git a/Ink Canvas/Controls/ImageSelectionOverlay.xaml.cs b/Ink Canvas/Controls/ImageSelectionOverlay.xaml.cs
new file mode 100644
index 00000000..ed5e2292
--- /dev/null
+++ b/Ink Canvas/Controls/ImageSelectionOverlay.xaml.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Shapes;
+
+namespace Ink_Canvas.Controls
+{
+ public enum ImageResizeCorner
+ {
+ TopLeft,
+ TopRight,
+ BottomLeft,
+ BottomRight
+ }
+
+ public class ImageResizeDeltaEventArgs : EventArgs
+ {
+ public ImageResizeCorner Corner { get; }
+ public Vector CanvasDelta { get; }
+ public bool LockAspectRatio { get; }
+
+ public ImageResizeDeltaEventArgs(ImageResizeCorner corner, Vector canvasDelta, bool lockAspect)
+ {
+ Corner = corner;
+ CanvasDelta = canvasDelta;
+ LockAspectRatio = lockAspect;
+ }
+ }
+
+ public class ImageMoveDeltaEventArgs : EventArgs
+ {
+ public Vector CanvasDelta { get; }
+ public ImageMoveDeltaEventArgs(Vector delta) { CanvasDelta = delta; }
+ }
+
+ public class ImageRotateDeltaEventArgs : EventArgs
+ {
+ public double AngleDelta { get; }
+ public ImageRotateDeltaEventArgs(double angleDelta) { AngleDelta = angleDelta; }
+ }
+
+ public partial class ImageSelectionOverlay : UserControl
+ {
+ private const double HandleSize = 12;
+ private const double RotationHandleSize = 14;
+ private const double RotationHandleOffset = 28;
+
+ public event EventHandler ResizeDelta;
+ public event EventHandler MoveDelta;
+ public event EventHandler RotateDelta;
+ public event EventHandler InteractionStarted;
+ public event EventHandler InteractionEnded;
+
+ public IInputElement CoordinateSource { get; set; }
+
+ private Point _rotationCenterCanvas;
+ private readonly RotateTransform _overlayRotation = new RotateTransform(0);
+
+ private bool _isResizing;
+ private bool _isRotating;
+ private bool _isMoving;
+ private ImageResizeCorner _activeCorner;
+ private Point _lastPoint;
+ private double _lastRotationAngle;
+
+ public ImageSelectionOverlay()
+ {
+ InitializeComponent();
+ RenderTransform = _overlayRotation;
+
+ TopLeftHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopLeft, e, TopLeftHandle);
+ TopRightHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopRight, e, TopRightHandle);
+ BottomLeftHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.BottomLeft, e, BottomLeftHandle);
+ BottomRightHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.BottomRight, e, BottomRightHandle);
+
+ TopLeftHandle.MouseMove += ResizeMove;
+ TopRightHandle.MouseMove += ResizeMove;
+ BottomLeftHandle.MouseMove += ResizeMove;
+ BottomRightHandle.MouseMove += ResizeMove;
+
+ TopLeftHandle.MouseLeftButtonUp += EndResize;
+ TopRightHandle.MouseLeftButtonUp += EndResize;
+ BottomLeftHandle.MouseLeftButtonUp += EndResize;
+ BottomRightHandle.MouseLeftButtonUp += EndResize;
+
+ RotationHandle.MouseLeftButtonDown += BeginRotate;
+ RotationHandle.MouseMove += RotateMove;
+ RotationHandle.MouseLeftButtonUp += EndRotate;
+
+ MoveSurface.MouseLeftButtonDown += BeginMove;
+ MoveSurface.MouseMove += MoveMove;
+ MoveSurface.MouseLeftButtonUp += EndMove;
+ }
+
+ ///
+ /// Position overlay so its logical rect (width × height) is centered at centerCanvas,
+ /// then rotated by rotationAngleDegrees around that center to match the target element.
+ ///
+ public void UpdateFrame(Point centerCanvas, double width, double height, double rotationAngleDegrees)
+ {
+ if (width <= 0 || height <= 0) return;
+
+ _rotationCenterCanvas = centerCanvas;
+
+ double left = centerCanvas.X - width / 2;
+ double top = centerCanvas.Y - height / 2;
+ Margin = new Thickness(left, top, 0, 0);
+ Width = width;
+ Height = height;
+
+ RenderTransformOrigin = new Point(0, 0);
+ _overlayRotation.Angle = rotationAngleDegrees;
+ _overlayRotation.CenterX = width / 2;
+ _overlayRotation.CenterY = height / 2;
+
+ FrameBorder.Width = width;
+ FrameBorder.Height = height;
+ System.Windows.Controls.Canvas.SetLeft(FrameBorder, 0);
+ System.Windows.Controls.Canvas.SetTop(FrameBorder, 0);
+
+ MoveSurface.Width = width;
+ MoveSurface.Height = height;
+ System.Windows.Controls.Canvas.SetLeft(MoveSurface, 0);
+ System.Windows.Controls.Canvas.SetTop(MoveSurface, 0);
+
+ double h = HandleSize / 2;
+ System.Windows.Controls.Canvas.SetLeft(TopLeftHandle, -h);
+ System.Windows.Controls.Canvas.SetTop(TopLeftHandle, -h);
+ System.Windows.Controls.Canvas.SetLeft(TopRightHandle, width - h);
+ System.Windows.Controls.Canvas.SetTop(TopRightHandle, -h);
+ System.Windows.Controls.Canvas.SetLeft(BottomLeftHandle, -h);
+ System.Windows.Controls.Canvas.SetTop(BottomLeftHandle, height - h);
+ System.Windows.Controls.Canvas.SetLeft(BottomRightHandle, width - h);
+ System.Windows.Controls.Canvas.SetTop(BottomRightHandle, height - h);
+
+ double rh = RotationHandleSize / 2;
+ double midX = width / 2;
+ System.Windows.Controls.Canvas.SetLeft(RotationHandle, midX - rh);
+ System.Windows.Controls.Canvas.SetTop(RotationHandle, -RotationHandleOffset - rh);
+
+ RotationLine.X1 = midX;
+ RotationLine.Y1 = 0;
+ RotationLine.X2 = midX;
+ RotationLine.Y2 = -RotationHandleOffset;
+ }
+
+ private IInputElement GetSource() => CoordinateSource ?? (IInputElement)Parent;
+
+ private void BeginResize(ImageResizeCorner corner, MouseButtonEventArgs e, Ellipse handle)
+ {
+ var source = GetSource();
+ if (source == null) return;
+ _isResizing = true;
+ _activeCorner = corner;
+ _lastPoint = e.GetPosition(source);
+ handle.CaptureMouse();
+ InteractionStarted?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ }
+
+ private void ResizeMove(object sender, MouseEventArgs e)
+ {
+ if (!_isResizing || !(sender is Ellipse handle) || !handle.IsMouseCaptured) return;
+ var source = GetSource();
+ if (source == null) return;
+ var current = e.GetPosition(source);
+ var delta = current - _lastPoint;
+ bool lockAspect = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
+ ResizeDelta?.Invoke(this, new ImageResizeDeltaEventArgs(_activeCorner, delta, lockAspect));
+ _lastPoint = current;
+ e.Handled = true;
+ }
+
+ private void EndResize(object sender, MouseButtonEventArgs e)
+ {
+ if (!_isResizing) return;
+ if (sender is Ellipse handle) handle.ReleaseMouseCapture();
+ _isResizing = false;
+ InteractionEnded?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ }
+
+ private void BeginRotate(object sender, MouseButtonEventArgs e)
+ {
+ var source = GetSource();
+ if (source == null) return;
+ _isRotating = true;
+ var p = e.GetPosition(source);
+ _lastRotationAngle = AngleFromCenter(p);
+ RotationHandle.CaptureMouse();
+ InteractionStarted?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ }
+
+ private void RotateMove(object sender, MouseEventArgs e)
+ {
+ if (!_isRotating || !RotationHandle.IsMouseCaptured) return;
+ var source = GetSource();
+ if (source == null) return;
+ var p = e.GetPosition(source);
+ double angle = AngleFromCenter(p);
+ double delta = angle - _lastRotationAngle;
+ if (delta > 180) delta -= 360;
+ else if (delta < -180) delta += 360;
+ _lastRotationAngle = angle;
+ RotateDelta?.Invoke(this, new ImageRotateDeltaEventArgs(delta));
+ e.Handled = true;
+ }
+
+ private void EndRotate(object sender, MouseButtonEventArgs e)
+ {
+ if (!_isRotating) return;
+ RotationHandle.ReleaseMouseCapture();
+ _isRotating = false;
+ InteractionEnded?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ }
+
+ private void BeginMove(object sender, MouseButtonEventArgs e)
+ {
+ var source = GetSource();
+ if (source == null) return;
+ _isMoving = true;
+ _lastPoint = e.GetPosition(source);
+ MoveSurface.CaptureMouse();
+ InteractionStarted?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ }
+
+ private void MoveMove(object sender, MouseEventArgs e)
+ {
+ if (!_isMoving || !MoveSurface.IsMouseCaptured) return;
+ var source = GetSource();
+ if (source == null) return;
+ var current = e.GetPosition(source);
+ var delta = current - _lastPoint;
+ _lastPoint = current;
+ MoveDelta?.Invoke(this, new ImageMoveDeltaEventArgs(delta));
+ e.Handled = true;
+ }
+
+ private void EndMove(object sender, MouseButtonEventArgs e)
+ {
+ if (!_isMoving) return;
+ MoveSurface.ReleaseMouseCapture();
+ _isMoving = false;
+ InteractionEnded?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ }
+
+ private double AngleFromCenter(Point p)
+ {
+ double dx = p.X - _rotationCenterCanvas.X;
+ double dy = p.Y - _rotationCenterCanvas.Y;
+ return Math.Atan2(dy, dx) * 180.0 / Math.PI;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Helpers/AnimationsHelper.cs b/Ink Canvas/Helpers/AnimationsHelper.cs
index c7a36a8c..00e8ba46 100644
--- a/Ink Canvas/Helpers/AnimationsHelper.cs
+++ b/Ink Canvas/Helpers/AnimationsHelper.cs
@@ -7,7 +7,10 @@ namespace Ink_Canvas.Helpers
{
internal class AnimationsHelper
{
- private static UIElement ResolveAnimationTarget(UIElement element) => element;
+ private static UIElement ResolveAnimationTarget(UIElement element)
+ {
+ return element;
+ }
public static void ShowWithFadeIn(UIElement element, double duration = 0.15)
{
diff --git a/Ink Canvas/Helpers/DeviceIdentifier.cs b/Ink Canvas/Helpers/DeviceIdentifier.cs
index 85b839b0..210954b4 100644
--- a/Ink Canvas/Helpers/DeviceIdentifier.cs
+++ b/Ink Canvas/Helpers/DeviceIdentifier.cs
@@ -1509,32 +1509,22 @@ namespace Ink_Canvas.Helpers
int.TryParse(remoteParts[2], out int remoteBuild) &&
int.TryParse(remoteParts[3], out int remoteRevision))
{
- // 计算代数差异:主版本号差异 * 1000 + 次版本号差异 * 100 + 构建号差异 * 10 + 修订号差异
- int majorDiff = remoteMajor - localMajor;
- int minorDiff = remoteMinor - localMinor;
- int buildDiff = remoteBuild - localBuild;
- int revisionDiff = remoteRevision - localRevision;
+ var localSemver = new Version(localMajor, localMinor, localBuild, localRevision);
+ var remoteSemver = new Version(remoteMajor, remoteMinor, remoteBuild, remoteRevision);
+ int direction = remoteSemver.CompareTo(localSemver);
+ if (direction == 0) return 0;
+ int sign = direction > 0 ? 1 : -1;
- // 如果主版本号不同,则代数差异很大
- if (majorDiff != 0)
- {
- return majorDiff * 1000 + minorDiff * 100 + buildDiff * 10 + revisionDiff;
- }
+ int majorDiff = Math.Abs(remoteMajor - localMajor);
+ if (majorDiff != 0) return sign * (majorDiff * 1000);
- // 如果次版本号不同,则代数差异中等
- if (minorDiff != 0)
- {
- return minorDiff * 100 + buildDiff * 10 + revisionDiff;
- }
+ int minorDiff = Math.Abs(remoteMinor - localMinor);
+ if (minorDiff != 0) return sign * (minorDiff * 100);
- // 如果构建号不同,则代数差异较小
- if (buildDiff != 0)
- {
- return buildDiff * 10 + revisionDiff;
- }
+ int buildDiff = Math.Abs(remoteBuild - localBuild);
+ if (buildDiff != 0) return sign * (buildDiff * 10);
- // 只有修订号不同,代数差异最小
- return revisionDiff;
+ return sign * Math.Abs(remoteRevision - localRevision);
}
return 0;
diff --git a/Ink Canvas/Helpers/IpcIACoreClient.cs b/Ink Canvas/Helpers/IpcIACoreClient.cs
index e320b80a..84a7e27f 100644
--- a/Ink Canvas/Helpers/IpcIACoreClient.cs
+++ b/Ink Canvas/Helpers/IpcIACoreClient.cs
@@ -32,7 +32,7 @@ namespace Ink_Canvas.Helpers
private bool _available;
private static string HelperExePath =>
- Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "InkCanvasForClass.IACoreHelper.exe");
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "InkCanvas.IACoreHelper.exe");
private string PipeName =>
string.Format("ICC_IACoreHelper_{0}", Process.GetCurrentProcess().Id);
diff --git a/Ink Canvas/Helpers/ROTPPTManager.cs b/Ink Canvas/Helpers/ROTPPTManager.cs
index 034c0d5b..575d4795 100644
--- a/Ink Canvas/Helpers/ROTPPTManager.cs
+++ b/Ink Canvas/Helpers/ROTPPTManager.cs
@@ -718,7 +718,7 @@ namespace Ink_Canvas.Helpers
try
{
- if (!IsConnected || !IsInSlideShow || PPTApplication == null)
+ if (!IsConnected || PPTApplication == null)
return;
if (!Marshal.IsComObject(PPTApplication))
@@ -733,7 +733,7 @@ namespace Ink_Canvas.Helpers
if (slideShowWindow == null)
return;
- SlideShowBegin?.Invoke(slideShowWindow);
+ OnSlideShowBegin(slideShowWindow);
}
catch (COMException comEx)
{
@@ -818,17 +818,10 @@ namespace Ink_Canvas.Helpers
{
UnbindEvents();
- if (activePresentation != null)
- {
- try
- {
- PresentationClose?.Invoke(activePresentation);
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"触发PresentationClose事件失败: {ex.Message}", LogHelper.LogType.Warning);
- }
- }
+ // 注意:PresentationClose 只应在真正的关闭(OnPresentationBeforeClose)中抛出。
+ // DisconnectFromPPT 会被重绑、热重载、瞬时 COM 失效恢复等内部路径调用,
+ // 此时演示文稿仍处于打开状态,抛 PresentationClose 会让上层错误地清空缓存,
+ // 而后续重连到同一个演示文稿也不会补发 PresentationOpen。
SafeReleaseComObject(slideShowWindow, "_pptSlideShowWindow");
SafeReleaseComObject(activePresentation, "_pptActivePresentation");
@@ -1217,6 +1210,15 @@ namespace Ink_Canvas.Helpers
{
try
{
+ try
+ {
+ PresentationClose?.Invoke(pres ?? _pptActivePresentation);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"触发PresentationClose事件失败: {ex.Message}", LogHelper.LogType.Warning);
+ }
+
if (_bindingEvents && PPTApplication != null)
{
try
diff --git a/Ink Canvas/Helpers/SaveFileNameHelper.cs b/Ink Canvas/Helpers/SaveFileNameHelper.cs
index f8ef2a2f..7dc36ee3 100644
--- a/Ink Canvas/Helpers/SaveFileNameHelper.cs
+++ b/Ink Canvas/Helpers/SaveFileNameHelper.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
@@ -12,6 +13,14 @@ namespace Ink_Canvas.Helpers
{
private const string DefaultDateTime = "yyyy-MM-dd HH-mm-ss-fff";
+ // Windows 保留设备名(不区分大小写)。这些名称无论是否带扩展名,CreateFile 都会失败。
+ private static readonly HashSet ReservedNames = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "CON", "PRN", "AUX", "NUL",
+ "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
+ "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
+ };
+
public static string Render(string template, SaveFileNameContext ctx)
{
if (ctx == null) ctx = new SaveFileNameContext();
@@ -51,7 +60,20 @@ namespace Ink_Canvas.Helpers
{
name = name.Replace(c, '_');
}
- return name.Trim();
+
+ // Windows 禁止文件名以点号或空格结尾(会被静默截断甚至创建失败)。
+ name = name.Trim().TrimEnd('.', ' ');
+
+ if (string.IsNullOrEmpty(name)) return name;
+
+ // 保留设备名:比较时忽略扩展名,命中则加下划线前缀以规避。
+ var stem = Path.GetFileNameWithoutExtension(name);
+ if (!string.IsNullOrEmpty(stem) && ReservedNames.Contains(stem))
+ {
+ name = "_" + name;
+ }
+
+ return name;
}
}
diff --git a/Ink Canvas/Helpers/UIAccessHelper.cs b/Ink Canvas/Helpers/UIAccessHelper.cs
index dfae8688..00c0e032 100644
--- a/Ink Canvas/Helpers/UIAccessHelper.cs
+++ b/Ink Canvas/Helpers/UIAccessHelper.cs
@@ -294,7 +294,13 @@ namespace Ink_Canvas.Helpers
{
try
{
- if (!GetUserPrimaryToken(out IntPtr userToken))
+ if (!GetCurrentProcessSessionId(out uint sessionId))
+ {
+ LogHelper.WriteLogToFile($"UIAccess | 获取当前会话 ID 失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
+ return false;
+ }
+
+ if (!GetUserPrimaryToken(sessionId, out IntPtr userToken))
{
LogHelper.WriteLogToFile($"UIAccess | 获取用户令牌失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
return false;
@@ -506,11 +512,31 @@ namespace Ink_Canvas.Helpers
finally { CloseHandle(hProc); }
}
+ private static bool GetCurrentProcessSessionId(out uint sessionId)
+ {
+ sessionId = 0;
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hSelfQuery))
+ return false;
+ try
+ {
+ IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
+ try
+ {
+ if (!GetTokenInformation(hSelfQuery, TokenSessionId, sesBuf, sizeof(uint), out _))
+ return false;
+ sessionId = (uint)Marshal.ReadInt32(sesBuf);
+ return true;
+ }
+ finally { Marshal.FreeHGlobal(sesBuf); }
+ }
+ finally { CloseHandle(hSelfQuery); }
+ }
+
///
/// 从 explorer.exe / ctfmon.exe 取得普通用户(非提升)令牌的主令牌副本,用于降权启动。
/// 仅当当前进程为管理员时才能成功。
///
- private static bool GetUserPrimaryToken(out IntPtr userToken)
+ private static bool GetUserPrimaryToken(uint sessionId, out IntPtr userToken)
{
userToken = IntPtr.Zero;
@@ -528,9 +554,9 @@ namespace Ink_Canvas.Helpers
{
if (string.Equals(pe.szExeFile, name, StringComparison.OrdinalIgnoreCase))
{
- if (TryDuplicateUserPrimaryToken(pe.th32ProcessID, out userToken))
+ if (TryDuplicateUserPrimaryToken(pe.th32ProcessID, sessionId, out userToken))
{
- LogHelper.WriteLogToFile($"UIAccess | 已从 {name} (PID={pe.th32ProcessID}) 取得用户令牌");
+ LogHelper.WriteLogToFile($"UIAccess | 已从 {name} (PID={pe.th32ProcessID}, Session={sessionId}) 取得用户令牌");
return true;
}
}
@@ -543,7 +569,7 @@ namespace Ink_Canvas.Helpers
return false;
}
- private static bool TryDuplicateUserPrimaryToken(uint pid, out IntPtr dupToken)
+ private static bool TryDuplicateUserPrimaryToken(uint pid, uint sessionId, out IntPtr dupToken)
{
dupToken = IntPtr.Zero;
@@ -557,6 +583,18 @@ namespace Ink_Canvas.Helpers
try
{
+ // 会话隔离:拒绝来自其他登录会话(RDP / 终端服务 / 快速用户切换)的令牌,
+ // 否则降权后进程会落到错误用户的桌面上下文中。
+ IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
+ try
+ {
+ if (!GetTokenInformation(hToken, TokenSessionId, sesBuf, sizeof(uint), out _))
+ return false;
+ if ((uint)Marshal.ReadInt32(sesBuf) != sessionId)
+ return false;
+ }
+ finally { Marshal.FreeHGlobal(sesBuf); }
+
// 仅接受非提升令牌(否则降权失败)
IntPtr elevBuf = Marshal.AllocHGlobal(sizeof(int));
try
diff --git a/Ink Canvas/InkCanvasForClass.csproj b/Ink Canvas/InkCanvasForClass.csproj
index 91c7e492..83a74383 100644
--- a/Ink Canvas/InkCanvasForClass.csproj
+++ b/Ink Canvas/InkCanvasForClass.csproj
@@ -139,7 +139,7 @@
-
+
false
false
diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml
index 02aa2dac..0f2ce824 100644
--- a/Ink Canvas/MainWindow.xaml
+++ b/Ink Canvas/MainWindow.xaml
@@ -666,86 +666,10 @@
-
-
+
+
@@ -2460,7 +2384,7 @@
@@ -3066,21 +2990,20 @@
CornerRadius="5" Background="{DynamicResource FloatBarBackground}" Opacity="1" BorderThickness="1"
BorderBrush="#2563eb">
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -3116,7 +3039,7 @@
-
-
-
diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs
index 890763fd..fcde88b2 100644
--- a/Ink Canvas/MainWindow.xaml.cs
+++ b/Ink Canvas/MainWindow.xaml.cs
@@ -1814,6 +1814,7 @@ namespace Ink_Canvas
// 清除之前的更新状态,确保使用新通道重新检查
AvailableLatestVersion = null;
AvailableLatestLineGroup = null;
+ AvailableLatestReleaseNotes = null;
// 使用当前选择的更新通道检查更新
var (remoteVersion, lineGroup, apiReleaseNotes) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel);
diff --git a/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs b/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs
index fa7edd3b..6177154a 100644
--- a/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs
+++ b/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs
@@ -247,7 +247,9 @@ namespace Ink_Canvas
SetTheme(ThemeDark);
break;
case 2:
- SetTheme(ThemeHelper.IsSystemThemeLightLegacy() ? ThemeLight : ThemeDark);
+ // 与 IsCurrentThemeDark / GetEffectiveTheme / 浮动栏一致,统一读 AppsUseLightTheme,
+ // 否则 SystemUsesLightTheme 与 AppsUseLightTheme 可独立取值时主题会混搭
+ SetTheme(ThemeHelper.IsSystemThemeLight() ? ThemeLight : ThemeDark);
break;
}
}
diff --git a/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs b/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs
index e91bcb73..25c67c69 100644
--- a/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs
+++ b/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs
@@ -289,7 +289,7 @@ namespace Ink_Canvas
}
// 如果是图片元素,更新选择点位置
- if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
+ if (IsBitmapLikeCanvasElement(element) && ImageSelectionOverlay?.Visibility == Visibility.Visible)
{
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
}
@@ -325,7 +325,7 @@ namespace Ink_Canvas
}
// 如果是图片元素,更新选择点位置
- if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
+ if (IsBitmapLikeCanvasElement(element) && ImageSelectionOverlay?.Visibility == Visibility.Visible)
{
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
}
@@ -415,7 +415,7 @@ namespace Ink_Canvas
}
// 如果是图片元素,更新选择点位置
- if (IsBitmapLikeCanvasElement(element) && ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
+ if (IsBitmapLikeCanvasElement(element) && ImageSelectionOverlay?.Visibility == Visibility.Visible)
{
UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
}
@@ -510,11 +510,22 @@ namespace Ink_Canvas
{
if (element.RenderTransform is TransformGroup transformGroup)
{
+ var scaleTransform = transformGroup.Children.OfType().FirstOrDefault();
+ var translateTransform = transformGroup.Children.OfType().FirstOrDefault();
var rotateTransform = transformGroup.Children.OfType().FirstOrDefault();
- if (rotateTransform != null)
- {
- rotateTransform.Angle += angle;
- }
+ if (rotateTransform == null) return;
+
+ var (ox, oy, visW, visH) = GetElementVisualBox(element);
+ double sX = scaleTransform?.ScaleX ?? 1;
+ double sY = scaleTransform?.ScaleY ?? 1;
+ double tx = translateTransform?.X ?? 0;
+ double ty = translateTransform?.Y ?? 0;
+
+ // Rotate runs last in the group, so its Center is in post-scale/translate space —
+ // i.e. the current visual center of the element.
+ rotateTransform.CenterX = tx + (ox + visW / 2) * sX;
+ rotateTransform.CenterY = ty + (oy + visH / 2) * sY;
+ rotateTransform.Angle += angle;
}
}
@@ -2176,7 +2187,7 @@ namespace Ink_Canvas
if (currentSelectedElement != null && IsBitmapLikeCanvasElement(currentSelectedElement))
{
UpdateImageSelectionToolbarPosition(currentSelectedElement);
- if (ImageResizeHandlesCanvas?.Visibility == Visibility.Visible)
+ if (ImageSelectionOverlay?.Visibility == Visibility.Visible)
UpdateImageResizeHandlesPosition(GetElementActualBounds(currentSelectedElement));
}
}), DispatcherPriority.Loaded);
@@ -2379,240 +2390,361 @@ namespace Ink_Canvas
#endregion
- #region Image Resize Handles
+ #region Image Selection Overlay
- // 图片缩放选择点相关变量
- private bool isResizingImage = false;
- private Point imageResizeStartPoint;
- private string activeResizeHandle = "";
+ private bool _imageOverlayHooked;
+ private FrameworkElement _overlayTrackedElement;
+
+ private void EnsureImageOverlayHooks()
+ {
+ if (_imageOverlayHooked || ImageSelectionOverlay == null) return;
+ ImageSelectionOverlay.CoordinateSource = inkCanvas;
+ ImageSelectionOverlay.ResizeDelta += ImageSelectionOverlay_ResizeDelta;
+ ImageSelectionOverlay.MoveDelta += ImageSelectionOverlay_MoveDelta;
+ ImageSelectionOverlay.RotateDelta += ImageSelectionOverlay_RotateDelta;
+ _imageOverlayHooked = true;
+ }
+
+ private void AttachOverlayTracking(FrameworkElement element)
+ {
+ if (_overlayTrackedElement == element) return;
+ DetachOverlayTracking();
+ _overlayTrackedElement = element;
+ if (element != null) element.LayoutUpdated += OverlayTrackedElement_LayoutUpdated;
+ }
+
+ private void DetachOverlayTracking()
+ {
+ if (_overlayTrackedElement != null)
+ {
+ _overlayTrackedElement.LayoutUpdated -= OverlayTrackedElement_LayoutUpdated;
+ _overlayTrackedElement = null;
+ }
+ }
+
+ private void OverlayTrackedElement_LayoutUpdated(object sender, EventArgs e)
+ {
+ if (ImageSelectionOverlay?.Visibility != Visibility.Visible) return;
+ if (currentSelectedElement == null || _overlayTrackedElement == null) return;
+ if (!ReferenceEquals(currentSelectedElement, _overlayTrackedElement)) return;
+ UpdateImageResizeHandlesPosition(default);
+ }
- // 显示图片缩放选择点
private void ShowImageResizeHandles(FrameworkElement element)
{
try
{
- if (ImageResizeHandlesCanvas == null || element == null) return;
-
- // 获取元素的实际边界
- Rect elementBounds = GetElementActualBounds(element);
-
- // 设置选择点位置
- UpdateImageResizeHandlesPosition(elementBounds);
-
- // 显示选择点
- ImageResizeHandlesCanvas.Visibility = Visibility.Visible;
+ if (ImageSelectionOverlay == null || element == null) return;
+ EnsureImageOverlayHooks();
+ AttachOverlayTracking(element);
+ UpdateImageResizeHandlesPosition(default);
+ ImageSelectionOverlay.Visibility = Visibility.Visible;
}
catch (Exception ex)
{
- LogHelper.WriteLogToFile($"显示图片缩放选择点失败: {ex.Message}", LogHelper.LogType.Error);
+ LogHelper.WriteLogToFile($"显示图片选中框失败: {ex.Message}", LogHelper.LogType.Error);
}
}
- // 隐藏图片缩放选择点
private void HideImageResizeHandles()
{
try
{
- if (ImageResizeHandlesCanvas != null)
- {
- ImageResizeHandlesCanvas.Visibility = Visibility.Collapsed;
- }
+ DetachOverlayTracking();
+ if (ImageSelectionOverlay != null)
+ ImageSelectionOverlay.Visibility = Visibility.Collapsed;
}
catch (Exception ex)
{
- LogHelper.WriteLogToFile($"隐藏图片缩放选择点失败: {ex.Message}", LogHelper.LogType.Error);
+ LogHelper.WriteLogToFile($"隐藏图片选中框失败: {ex.Message}", LogHelper.LogType.Error);
}
}
- // 更新图片缩放选择点位置
+ // elementBounds parameter is ignored — overlay needs the UNROTATED bounds of the element,
+ // computed directly from the element's own state so rotation never distorts the frame.
private void UpdateImageResizeHandlesPosition(Rect elementBounds)
{
try
{
- if (ImageResizeHandlesCanvas == null) return;
+ if (ImageSelectionOverlay == null || currentSelectedElement == null) return;
- ImageResizeHandlesCanvas.Margin = new Thickness(elementBounds.Left, elementBounds.Top, 0, 0);
+ var element = currentSelectedElement;
+ var (ox, oy, visW, visH) = GetElementVisualBox(element);
+ if (visW <= 0 || visH <= 0) return;
- // 四个角控制点
- System.Windows.Controls.Canvas.SetLeft(ImageTopLeftHandle, -4);
- System.Windows.Controls.Canvas.SetTop(ImageTopLeftHandle, -4);
-
- System.Windows.Controls.Canvas.SetLeft(ImageTopRightHandle, elementBounds.Width - 4);
- System.Windows.Controls.Canvas.SetTop(ImageTopRightHandle, -4);
-
- System.Windows.Controls.Canvas.SetLeft(ImageBottomLeftHandle, -4);
- System.Windows.Controls.Canvas.SetTop(ImageBottomLeftHandle, elementBounds.Height - 4);
-
- System.Windows.Controls.Canvas.SetLeft(ImageBottomRightHandle, elementBounds.Width - 4);
- System.Windows.Controls.Canvas.SetTop(ImageBottomRightHandle, elementBounds.Height - 4);
-
- // 四个边控制点
- System.Windows.Controls.Canvas.SetLeft(ImageTopHandle, elementBounds.Width / 2 - 4);
- System.Windows.Controls.Canvas.SetTop(ImageTopHandle, -4);
-
- System.Windows.Controls.Canvas.SetLeft(ImageBottomHandle, elementBounds.Width / 2 - 4);
- System.Windows.Controls.Canvas.SetTop(ImageBottomHandle, elementBounds.Height - 4);
-
- System.Windows.Controls.Canvas.SetLeft(ImageLeftHandle, -4);
- System.Windows.Controls.Canvas.SetTop(ImageLeftHandle, elementBounds.Height / 2 - 4);
-
- System.Windows.Controls.Canvas.SetLeft(ImageRightHandle, elementBounds.Width - 4);
- System.Windows.Controls.Canvas.SetTop(ImageRightHandle, elementBounds.Height / 2 - 4);
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"更新图片缩放选择点位置失败: {ex.Message}", LogHelper.LogType.Error);
- }
- }
-
- // 图片缩放选择点鼠标按下事件
- private void ImageResizeHandle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- try
- {
- if (IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse)
+ double scaleX = 1, scaleY = 1, angle = 0;
+ if (element.RenderTransform is TransformGroup tg)
{
- isResizingImage = true;
- imageResizeStartPoint = e.GetPosition(inkCanvas);
-
- // 确定是哪个控制点
- activeResizeHandle = ellipse.Name;
-
- // 捕获鼠标
- ellipse.CaptureMouse();
- e.Handled = true;
+ var st = tg.Children.OfType().FirstOrDefault();
+ var rt = tg.Children.OfType().FirstOrDefault();
+ if (st != null) { scaleX = st.ScaleX; scaleY = st.ScaleY; }
+ if (rt != null) angle = rt.Angle;
}
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"图片缩放选择点鼠标按下事件失败: {ex.Message}", LogHelper.LogType.Error);
- }
- }
- // 图片缩放选择点鼠标释放事件
- private void ImageResizeHandle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
- {
- try
- {
- if (isResizingImage && sender is Ellipse ellipse)
+ // Compute the visual center directly from the element's actual transform,
+ // so we don't have to model the transform chain ourselves.
+ Point visualCenterLocal = new Point(ox + visW / 2, oy + visH / 2);
+ Point centerCanvas;
+ try
{
- isResizingImage = false;
- ellipse.ReleaseMouseCapture();
- activeResizeHandle = "";
- e.Handled = true;
+ var t = element.TransformToAncestor(inkCanvas);
+ centerCanvas = t.Transform(visualCenterLocal);
+ // Cancel out rotation: TransformToAncestor includes Rotate; we need pre-rotation
+ // center in canvas coords for the overlay (overlay applies rotation itself).
+ // The visual center is invariant under rotation around itself, so this is the
+ // same point in canvas coords either way.
}
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"图片缩放选择点鼠标释放事件失败: {ex.Message}", LogHelper.LogType.Error);
- }
- }
-
- // 图片缩放选择点鼠标移动事件
- private void ImageResizeHandle_MouseMove(object sender, MouseEventArgs e)
- {
- try
- {
- if (isResizingImage && IsBitmapLikeCanvasElement(currentSelectedElement) && sender is Ellipse ellipse)
+ catch
{
- var currentPoint = e.GetPosition(inkCanvas);
- ResizeImageByHandle(currentSelectedElement, imageResizeStartPoint, currentPoint, activeResizeHandle);
- imageResizeStartPoint = currentPoint;
- e.Handled = true;
- }
- }
- catch (Exception ex)
- {
- LogHelper.WriteLogToFile($"图片缩放选择点鼠标移动事件失败: {ex.Message}", LogHelper.LogType.Error);
- }
- }
-
- // 根据控制点缩放图片
- private void ResizeImageByHandle(FrameworkElement element, Point startPoint, Point currentPoint, string handleName)
- {
- try
- {
- if (element.RenderTransform is TransformGroup transformGroup)
- {
- var scaleTransform = transformGroup.Children.OfType().FirstOrDefault();
- var translateTransform = transformGroup.Children.OfType().FirstOrDefault();
-
- if (scaleTransform == null || translateTransform == null) return;
-
- // 获取图片的当前边界
- Rect currentBounds = GetElementActualBounds(element);
- double deltaX = currentPoint.X - startPoint.X;
- double deltaY = currentPoint.Y - startPoint.Y;
-
- // 计算缩放比例
- double scaleX = 1.0;
- double scaleY = 1.0;
- double translateX = 0;
- double translateY = 0;
-
- switch (handleName)
+ double left = InkCanvas.GetLeft(element); if (double.IsNaN(left)) left = 0;
+ double top = InkCanvas.GetTop(element); if (double.IsNaN(top)) top = 0;
+ double tx = 0, ty = 0;
+ if (element.RenderTransform is TransformGroup tg2)
{
- case "ImageTopLeftHandle":
- scaleX = (currentBounds.Width - deltaX) / currentBounds.Width;
- scaleY = (currentBounds.Height - deltaY) / currentBounds.Height;
- translateX = deltaX;
- translateY = deltaY;
- break;
- case "ImageTopRightHandle":
- scaleX = (currentBounds.Width + deltaX) / currentBounds.Width;
- scaleY = (currentBounds.Height - deltaY) / currentBounds.Height;
- translateY = deltaY;
- break;
- case "ImageBottomLeftHandle":
- scaleX = (currentBounds.Width - deltaX) / currentBounds.Width;
- scaleY = (currentBounds.Height + deltaY) / currentBounds.Height;
- translateX = deltaX;
- break;
- case "ImageBottomRightHandle":
- scaleX = (currentBounds.Width + deltaX) / currentBounds.Width;
- scaleY = (currentBounds.Height + deltaY) / currentBounds.Height;
- break;
- case "ImageTopHandle":
- scaleY = (currentBounds.Height - deltaY) / currentBounds.Height;
- translateY = deltaY;
- break;
- case "ImageBottomHandle":
- scaleY = (currentBounds.Height + deltaY) / currentBounds.Height;
- break;
- case "ImageLeftHandle":
- scaleX = (currentBounds.Width - deltaX) / currentBounds.Width;
- translateX = deltaX;
- break;
- case "ImageRightHandle":
- scaleX = (currentBounds.Width + deltaX) / currentBounds.Width;
- break;
+ var tt = tg2.Children.OfType().FirstOrDefault();
+ if (tt != null) { tx = tt.X; ty = tt.Y; }
}
-
- // 限制缩放范围
- scaleX = Math.Max(0.1, Math.Min(scaleX, 5.0));
- scaleY = Math.Max(0.1, Math.Min(scaleY, 5.0));
-
- // 应用缩放
- scaleTransform.ScaleX *= scaleX;
- scaleTransform.ScaleY *= scaleY;
-
- // 应用平移
- translateTransform.X += translateX;
- translateTransform.Y += translateY;
-
- // 更新选择点位置
- UpdateImageResizeHandlesPosition(GetElementActualBounds(element));
-
- if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
- UpdateImageSelectionToolbarPosition(element);
+ centerCanvas = new Point(left + tx + (ox + visW / 2) * scaleX,
+ top + ty + (oy + visH / 2) * scaleY);
}
+
+ double scaledW = visW * scaleX;
+ double scaledH = visH * scaleY;
+
+ ImageSelectionOverlay.UpdateFrame(centerCanvas, scaledW, scaledH, angle);
}
catch (Exception ex)
{
- LogHelper.WriteLogToFile($"根据控制点缩放图片失败: {ex.Message}", LogHelper.LogType.Error);
+ LogHelper.WriteLogToFile($"更新图片选中框位置失败: {ex.Message}", LogHelper.LogType.Error);
}
}
+ private double GetElementRotationAngle(FrameworkElement element)
+ {
+ if (element?.RenderTransform is TransformGroup tg)
+ {
+ var rt = tg.Children.OfType().FirstOrDefault();
+ if (rt != null) return rt.Angle;
+ }
+ return 0;
+ }
+
+ // Visual box of the rendered content inside element.ActualWidth/Height,
+ // accounting for Stretch=Uniform letterboxing on Image elements.
+ // Returns (offsetX, offsetY, visibleW, visibleH) in base (unscaled) coords.
+ private static (double ox, double oy, double w, double h) GetElementVisualBox(FrameworkElement element)
+ {
+ double boxW = element.ActualWidth > 0 ? element.ActualWidth : element.Width;
+ double boxH = element.ActualHeight > 0 ? element.ActualHeight : element.Height;
+ if (double.IsNaN(boxW) || double.IsNaN(boxH) || boxW <= 0 || boxH <= 0)
+ return (0, 0, 0, 0);
+
+ if (element is Image img && img.Stretch == Stretch.Uniform && img.Source is BitmapSource bs
+ && bs.PixelWidth > 0 && bs.PixelHeight > 0)
+ {
+ double srcAspect = (double)bs.PixelWidth / bs.PixelHeight;
+ double boxAspect = boxW / boxH;
+ double vW, vH;
+ if (srcAspect > boxAspect) { vW = boxW; vH = boxW / srcAspect; }
+ else { vH = boxH; vW = boxH * srcAspect; }
+ return ((boxW - vW) / 2, (boxH - vH) / 2, vW, vH);
+ }
+
+ return (0, 0, boxW, boxH);
+ }
+
+ // Rotate a canvas-space vector back into the element's local (unrotated) space.
+ private Vector CanvasVectorToLocal(Vector canvasDelta, double angleDegrees)
+ {
+ double rad = -angleDegrees * Math.PI / 180.0;
+ double cos = Math.Cos(rad);
+ double sin = Math.Sin(rad);
+ return new Vector(canvasDelta.X * cos - canvasDelta.Y * sin,
+ canvasDelta.X * sin + canvasDelta.Y * cos);
+ }
+
+ private void ImageSelectionOverlay_ResizeDelta(object sender, ImageResizeDeltaEventArgs e)
+ {
+ try
+ {
+ if (!IsBitmapLikeCanvasElement(currentSelectedElement)) return;
+ ResizeImageByCorner(currentSelectedElement, e.CanvasDelta, e.Corner, e.LockAspectRatio);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"图片缩放失败: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ private void ImageSelectionOverlay_MoveDelta(object sender, ImageMoveDeltaEventArgs e)
+ {
+ try
+ {
+ if (currentSelectedElement == null) return;
+ if (currentSelectedElement.RenderTransform is TransformGroup tg)
+ {
+ var tt = tg.Children.OfType().FirstOrDefault();
+ var rt = tg.Children.OfType().FirstOrDefault();
+ if (tt != null)
+ {
+ tt.X += e.CanvasDelta.X;
+ tt.Y += e.CanvasDelta.Y;
+ // Keep rotation center locked to the visual center so the element
+ // translates rigidly instead of swinging around an old pivot.
+ if (rt != null)
+ {
+ rt.CenterX += e.CanvasDelta.X;
+ rt.CenterY += e.CanvasDelta.Y;
+ }
+ }
+ }
+ UpdateImageResizeHandlesPosition(default);
+ if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
+ UpdateImageSelectionToolbarPosition(currentSelectedElement);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"图片拖动失败: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ private void ImageSelectionOverlay_RotateDelta(object sender, ImageRotateDeltaEventArgs e)
+ {
+ try
+ {
+ if (currentSelectedElement == null) return;
+ ApplyRotateTransform(currentSelectedElement, e.AngleDelta);
+ UpdateImageResizeHandlesPosition(default);
+ if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
+ UpdateImageSelectionToolbarPosition(currentSelectedElement);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"图片旋转失败: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ private void ResizeImageByCorner(FrameworkElement element, Vector canvasDelta,
+ ImageResizeCorner corner, bool lockAspect)
+ {
+ if (!(element.RenderTransform is TransformGroup transformGroup)) return;
+ var scaleTransform = transformGroup.Children.OfType().FirstOrDefault();
+ var translateTransform = transformGroup.Children.OfType().FirstOrDefault();
+ var rotateTransform = transformGroup.Children.OfType().FirstOrDefault();
+ if (scaleTransform == null || translateTransform == null) return;
+
+ double angle = rotateTransform?.Angle ?? 0;
+
+ var (ox, oy, visW, visH) = GetElementVisualBox(element);
+ if (visW <= 0 || visH <= 0) return;
+
+ double left = InkCanvas.GetLeft(element); if (double.IsNaN(left)) left = 0;
+ double top = InkCanvas.GetTop(element); if (double.IsNaN(top)) top = 0;
+
+ double curW = visW * scaleTransform.ScaleX;
+ double curH = visH * scaleTransform.ScaleY;
+
+ // Drag delta → local (unrotated) space so corner tracks cursor under any rotation.
+ Vector local = CanvasVectorToLocal(canvasDelta, angle);
+
+ double newW = curW, newH = curH;
+ double pivotFracX = 0, pivotFracY = 0; // opposite visual corner
+ switch (corner)
+ {
+ case ImageResizeCorner.TopLeft:
+ newW = curW - local.X; newH = curH - local.Y;
+ pivotFracX = 1; pivotFracY = 1;
+ break;
+ case ImageResizeCorner.TopRight:
+ newW = curW + local.X; newH = curH - local.Y;
+ pivotFracX = 0; pivotFracY = 1;
+ break;
+ case ImageResizeCorner.BottomLeft:
+ newW = curW - local.X; newH = curH + local.Y;
+ pivotFracX = 1; pivotFracY = 0;
+ break;
+ case ImageResizeCorner.BottomRight:
+ newW = curW + local.X; newH = curH + local.Y;
+ pivotFracX = 0; pivotFracY = 0;
+ break;
+ }
+
+ if (lockAspect && curW > 0 && curH > 0)
+ {
+ double uniform = Math.Min(newW / curW, newH / curH);
+ newW = curW * uniform;
+ newH = curH * uniform;
+ }
+
+ double newScaleX = newW / visW;
+ double newScaleY = newH / visH;
+ newScaleX = Math.Max(0.1, Math.Min(newScaleX, 10.0));
+ newScaleY = Math.Max(0.1, Math.Min(newScaleY, 10.0));
+ newW = visW * newScaleX;
+ newH = visH * newScaleY;
+
+ double tx = translateTransform.X;
+ double ty = translateTransform.Y;
+
+ // Visual pivot canvas position BEFORE the change.
+ // Visual box origin (pre-scale) is (ox, oy); after scale its top-left in post-scale
+ // space (before rotation/translate) is (ox*sx, oy*sy), size (visW*sx, visH*sy).
+ // Pivot pre-rotation = (tx + (ox + pivotFrac*visW) * sx, ty + (oy + pivotFrac*visH) * sy).
+ // Rotation center is the element visual center, which is the SAME pre-rotation anchor
+ // we derive below — so pre-rotation coord == canvas coord relative to (left, top) up
+ // to a rotation around that center. Because we rotate around the center and both
+ // before/after share the same center (we'll update it to newCenter below after shift),
+ // we must match canvas positions WITH rotation applied.
+ Point pivotBefore = ApplyCanvasTransform(ox + pivotFracX * visW, oy + pivotFracY * visH,
+ ox + visW / 2, oy + visH / 2,
+ scaleTransform.ScaleX, scaleTransform.ScaleY,
+ tx, ty, angle, left, top);
+
+ Point pivotAfter = ApplyCanvasTransform(ox + pivotFracX * visW, oy + pivotFracY * visH,
+ ox + visW / 2, oy + visH / 2,
+ newScaleX, newScaleY,
+ tx, ty, angle, left, top);
+
+ Vector canvasDrift = pivotBefore - pivotAfter;
+ translateTransform.X += canvasDrift.X;
+ translateTransform.Y += canvasDrift.Y;
+
+ scaleTransform.ScaleX = newScaleX;
+ scaleTransform.ScaleY = newScaleY;
+
+ // Update rotation center to the new visual center so future rotations behave.
+ if (rotateTransform != null)
+ {
+ rotateTransform.CenterX = translateTransform.X + (ox + visW / 2) * newScaleX;
+ rotateTransform.CenterY = translateTransform.Y + (oy + visH / 2) * newScaleY;
+ }
+
+ UpdateImageResizeHandlesPosition(default);
+ if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
+ UpdateImageSelectionToolbarPosition(element);
+ }
+
+ // Canvas position of element-local point (lx, ly) under the given transform.
+ // Model: P_canvas = (left, top) + Rotate_center(S * (lx,ly) + T)
+ // where center = S * (cx, cy) + T, and rotation angle is angleDeg.
+ private static Point ApplyCanvasTransform(double lx, double ly, double cx, double cy,
+ double sx, double sy, double tx, double ty,
+ double angleDeg, double left, double top)
+ {
+ double preX = sx * lx + tx;
+ double preY = sy * ly + ty;
+ double centerX = sx * cx + tx;
+ double centerY = sy * cy + ty;
+ double rad = angleDeg * Math.PI / 180.0;
+ double cos = Math.Cos(rad);
+ double sin = Math.Sin(rad);
+ double relX = preX - centerX;
+ double relY = preY - centerY;
+ double rotX = relX * cos - relY * sin + centerX;
+ double rotY = relX * sin + relY * cos + centerY;
+ return new Point(left + rotX, top + rotY);
+ }
+
#endregion
}
}
diff --git a/Ink Canvas/MainWindow_cs/MW_PPT.cs b/Ink Canvas/MainWindow_cs/MW_PPT.cs
index 0a354f6e..e2e9e1a3 100644
--- a/Ink Canvas/MainWindow_cs/MW_PPT.cs
+++ b/Ink Canvas/MainWindow_cs/MW_PPT.cs
@@ -2275,7 +2275,7 @@ namespace Ink_Canvas
}
else
{
- var slides = await Task.Run(BuildPptPreviewItems);
+ var slides = await RunOnStaAsync(BuildPptPreviewItems);
if (slides == null || slides.Count == 0)
{
LogHelper.WriteLogToFile("PPT增强预览未生成可用缩略图,改用默认导航", LogHelper.LogType.Warning);
@@ -2360,8 +2360,14 @@ namespace Ink_Canvas
}
/// 在 MainWindow 加载完成后调用,把 4 个 PptNavBar 的事件接到本类。
+ private bool _pptNavBarsWired;
+
private void WirePptNavBars()
{
+ // InitializePPTManagers 可能被多次调用(切换 COM/ROT、设置变更等)。
+ // PptNavBar 事件若在同一控件上重复订阅,会导致翻页、长按、预览展开等逻辑成倍触发。
+ if (_pptNavBarsWired) return;
+
var bars = new[]
{
LeftBottomPanelForPPTNavigation,
@@ -2390,6 +2396,8 @@ namespace Ink_Canvas
bar.SlideSelected += (s, slideNumber) => OnPptNavBarSlideSelected(captured, slideNumber);
bar.PreviewExpandedChanged += (s, expanded) => OnPptNavBarPreviewExpandedChanged(captured, expanded);
}
+
+ _pptNavBarsWired = true;
}
private bool _suppressPreviewExpandedSync;
@@ -2460,6 +2468,22 @@ namespace Ink_Canvas
}
}
+ private static Task RunOnStaAsync(Func func)
+ {
+ // Office interop 要求 STA + COM 单元;Task.Run 跑到 MTA 线程池里会触发 RPC_E_WRONG_THREAD
+ // 等随机 COM 失败,表现为增强预览空白或崩溃。显式创建 STA worker 在其中执行导出。
+ var tcs = new TaskCompletionSource();
+ var thread = new Thread(() =>
+ {
+ try { tcs.SetResult(func()); }
+ catch (Exception ex) { tcs.SetException(ex); }
+ });
+ thread.IsBackground = true;
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ return tcs.Task;
+ }
+
private List BuildPptPreviewItems()
{
var result = new List();
diff --git a/Ink Canvas/MainWindow_cs/MW_Timer.cs b/Ink Canvas/MainWindow_cs/MW_Timer.cs
index e70c1499..1a9d3c7b 100644
--- a/Ink Canvas/MainWindow_cs/MW_Timer.cs
+++ b/Ink Canvas/MainWindow_cs/MW_Timer.cs
@@ -1291,11 +1291,13 @@ namespace Ink_Canvas
// 清除之前的更新状态
AvailableLatestVersion = null;
AvailableLatestLineGroup = null;
+ AvailableLatestReleaseNotes = null;
// 使用当前选择的更新通道检查更新
var (remoteVersion, lineGroup, apiReleaseNotes) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel);
AvailableLatestVersion = remoteVersion;
AvailableLatestLineGroup = lineGroup;
+ AvailableLatestReleaseNotes = apiReleaseNotes;
if (AvailableLatestVersion != null)
{
diff --git a/Ink Canvas/Properties/Strings.en-US.resx b/Ink Canvas/Properties/Strings.en-US.resx
index 8e6cd975..ad3535a9 100644
--- a/Ink Canvas/Properties/Strings.en-US.resx
+++ b/Ink Canvas/Properties/Strings.en-US.resx
@@ -2175,6 +2175,9 @@ Hide
Gesture
+
+ Exit
+
Gesture options
@@ -2734,7 +2737,22 @@ Hide
Logs
- .txt logs and crash reports under Logs / Crashs. Cleanable.
+ .txt logs and crash reports under Logs / Crashes. Cleanable.
+
+
+ Not set
+
+
+ Security
+
+
+ Security password and process protection
+
+
+ Hotkeys
+
+
+ Hotkey settings
Ink
@@ -2796,4 +2814,46 @@ Hide
View and clean storage used by ICC CE
+
+ Show debug console
+
+
+ Show a separate console window for live log output (takes effect immediately; if "Enable logging" is off in settings, no content will be emitted).
+
+
+ Use custom save file name
+
+
+ When enabled, choose how saved files are named
+
+
+ File name format
+
+
+ Timestamp (default)
+
+
+ Date
+
+
+ Date + Time
+
+
+ Date + Mode
+
+
+ Date + Mode + Page
+
+
+ Date + Mode + Page + Stroke count
+
+
+ Custom...
+
+
+ Custom template
+
+
+ Available placeholders: {date} {time} {datetime} {mode} {page} {count} {type}
+
\ No newline at end of file
diff --git a/Ink Canvas/Properties/Strings.resx b/Ink Canvas/Properties/Strings.resx
index 03da96e4..f814cae4 100644
--- a/Ink Canvas/Properties/Strings.resx
+++ b/Ink Canvas/Properties/Strings.resx
@@ -2218,6 +2218,9 @@
手势
+
+ 退出
+
手势选项
@@ -2779,6 +2782,21 @@
位于 Logs / Crashs 目录下的 .txt 日志与崩溃报告,可清理
+
+ 未设置
+
+
+ 安全
+
+
+ 安全密码与进程保护
+
+
+ 快捷键
+
+
+ 快捷键设置
+
墨迹
@@ -2839,4 +2857,46 @@
查看与清理 ICC CE 占用的存储空间
+
+ 显示调试窗口
+
+
+ 显示一个独立的控制台窗口,用于实时输出日志(开启后立即生效;关闭设置中的“启用日志记录”将不会输出内容)。
+
+
+ 使用自定义保存文件名
+
+
+ 开启后可选择保存文件的命名方式
+
+
+ 文件名格式
+
+
+ 时间戳(默认)
+
+
+ 日期
+
+
+ 日期 + 时间
+
+
+ 日期 + 模式
+
+
+ 日期 + 模式 + 页码
+
+
+ 日期 + 模式 + 页码 + 笔画数
+
+
+ 自定义...
+
+
+ 自定义模板
+
+
+ 可用占位符:{date} {time} {datetime} {mode} {page} {count} {type}
+
\ No newline at end of file
diff --git a/Ink Canvas/Windows/HotkeyItem.xaml b/Ink Canvas/Windows/HotkeyItem.xaml
index 89d8d54e..6e6b46c2 100644
--- a/Ink Canvas/Windows/HotkeyItem.xaml
+++ b/Ink Canvas/Windows/HotkeyItem.xaml
@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
+ xmlns:i18n="clr-namespace:Ink_Canvas.MarkupExtensions"
mc:Ignorable="d"
d:DesignHeight="68"
d:DesignWidth="600">
@@ -20,7 +21,7 @@
FontFamily="Consolas"
FontWeight="SemiBold"
Click="BtnSetHotkey_Click">
-
+
\ No newline at end of file
diff --git a/Ink Canvas/Windows/HotkeyItem.xaml.cs b/Ink Canvas/Windows/HotkeyItem.xaml.cs
index 63cda413..96f7056c 100644
--- a/Ink Canvas/Windows/HotkeyItem.xaml.cs
+++ b/Ink Canvas/Windows/HotkeyItem.xaml.cs
@@ -1,3 +1,4 @@
+using Ink_Canvas.Helpers;
using System;
using System.Windows;
using System.Windows.Controls;
@@ -68,7 +69,7 @@ namespace Ink_Canvas.Windows
{
if (_currentKey == Key.None)
{
- CurrentHotkeyTextBlock.Text = "未设置";
+ CurrentHotkeyTextBlock.Text = LocalizationHelper.GetString("Hotkey_NotSet");
}
else
{
diff --git a/Ink Canvas/Windows/MagnifierWindow.cs b/Ink Canvas/Windows/MagnifierWindow.cs
new file mode 100644
index 00000000..c3612854
--- /dev/null
+++ b/Ink Canvas/Windows/MagnifierWindow.cs
@@ -0,0 +1,513 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Interop;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using System.Windows.Threading;
+
+namespace Ink_Canvas.Windows
+{
+ ///
+ /// 使用 Win32 Magnification API (Magnification.dll)。
+ ///
+ internal class MagnifierWindow : Window
+ {
+ #region P/Invoke
+
+ private const string MagnifierClassName = "Magnifier";
+ private const int WS_CHILD = 0x40000000;
+ private const int WS_VISIBLE = 0x10000000;
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RECT { public int left, top, right, bottom; public RECT(int l, int t, int r, int b) { left = l; top = t; right = r; bottom = b; } }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct MAGTRANSFORM
+ {
+ public float m00, m01, m02;
+ public float m10, m11, m12;
+ public float m20, m21, m22;
+ }
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName, int dwStyle,
+ int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
+
+ [DllImport("user32.dll")] private static extern bool DestroyWindow(IntPtr hWnd);
+ [DllImport("user32.dll")] private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int w, int h, bool repaint);
+ [DllImport("user32.dll")] private static extern bool InvalidateRect(IntPtr hWnd, IntPtr lpRect, bool bErase);
+
+ [DllImport("Magnification.dll")] private static extern bool MagInitialize();
+ [DllImport("Magnification.dll")] private static extern bool MagUninitialize();
+ [DllImport("Magnification.dll")] private static extern bool MagSetWindowSource(IntPtr hwnd, RECT rect);
+ [DllImport("Magnification.dll")] private static extern bool MagSetWindowTransform(IntPtr hwnd, ref MAGTRANSFORM pTransform);
+ [DllImport("Magnification.dll")] private static extern bool MagSetWindowFilterList(IntPtr hwnd, int dwFilterMode, int count, IntPtr[] pHWND);
+ private const int MW_FILTERMODE_EXCLUDE = 0;
+
+ #endregion
+
+ #region 单例
+
+ private static MagnifierWindow _instance;
+ public static bool HasInstance => _instance != null;
+ public static event EventHandler Closed2;
+
+ public static void Show(float zoom)
+ {
+ if (_instance != null) { _instance.Activate(); _instance.Zoom = zoom; return; }
+ _instance = new MagnifierWindow { Zoom = zoom };
+ _instance.Closed += (s, e) =>
+ {
+ _instance = null;
+ Closed2?.Invoke(null, EventArgs.Empty);
+ };
+ _instance.Show();
+ }
+
+ public static void HideInstance() => _instance?.Close();
+ public static void SetZoom(float zoom) { if (_instance != null) _instance.Zoom = zoom; }
+
+ #endregion
+
+ // 选择框(放大区域)在 Canvas 中的几何属性 (DIP)
+ private double _boxLeft = 200;
+ private double _boxTop = 150;
+ private double _boxWidth = 520;
+ private double _boxHeight = 360;
+ private const double MinBoxW = 200;
+ private const double MinBoxH = 140;
+
+ private System.Windows.Controls.Canvas _canvas;
+ private Rectangle _overlay; // 半透明遮罩
+ private Border _selectionBorder; // 白色边框选择框
+ private MagnifierHost _magHost;
+ private System.Windows.Controls.Border _toolbar;
+ private Ellipse[] _handles; // 8 个控点
+ private ToggleButton _blackoutButton;
+ private bool _blackoutOn;
+
+ private IntPtr _magHwnd;
+ private HwndSource _hwndSource;
+ private DispatcherTimer _timer;
+ private bool _magInitialized;
+
+ private float _zoom = 2.0f;
+ public float Zoom
+ {
+ get => _zoom;
+ set
+ {
+ _zoom = Math.Max(1.1f, Math.Min(8.0f, value));
+ if (_magHwnd != IntPtr.Zero) ApplyTransform();
+ }
+ }
+
+ private MagnifierWindow()
+ {
+ Title = "聚焦放大镜";
+ WindowStyle = WindowStyle.None;
+ ResizeMode = ResizeMode.NoResize;
+ AllowsTransparency = true;
+ Background = Brushes.Transparent;
+ Topmost = true;
+ ShowInTaskbar = false;
+ WindowState = WindowState.Maximized;
+
+ _canvas = new System.Windows.Controls.Canvas { Background = Brushes.Transparent };
+
+ // 半透明遮罩:固定浅色,挖空选择框
+ _overlay = new Rectangle
+ {
+ Fill = new SolidColorBrush(Color.FromArgb(102, 0, 0, 0)),
+ IsHitTestVisible = false
+ };
+ _canvas.Children.Add(_overlay);
+
+ // 选择框
+ _selectionBorder = new Border
+ {
+ BorderBrush = Brushes.White,
+ BorderThickness = new Thickness(1.5),
+ Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)), // 几乎透明,可命中
+ Cursor = Cursors.SizeAll,
+ SnapsToDevicePixels = true
+ };
+ _selectionBorder.MouseLeftButtonDown += SelectionBorder_MouseDown;
+ _selectionBorder.MouseMove += SelectionBorder_MouseMove;
+ _selectionBorder.MouseLeftButtonUp += SelectionBorder_MouseUp;
+ _magHost = new MagnifierHost(this);
+ _selectionBorder.Child = _magHost;
+ _canvas.Children.Add(_selectionBorder);
+
+ // 8 个控点
+ _handles = new Ellipse[8];
+ string[] tags = { "NW", "N", "NE", "E", "SE", "S", "SW", "W" };
+ Cursor[] cursors = { Cursors.SizeNWSE, Cursors.SizeNS, Cursors.SizeNESW, Cursors.SizeWE,
+ Cursors.SizeNWSE, Cursors.SizeNS, Cursors.SizeNESW, Cursors.SizeWE };
+ for (int i = 0; i < 8; i++)
+ {
+ var h = new Ellipse
+ {
+ Width = 12,
+ Height = 12,
+ Fill = Brushes.White,
+ Stroke = new SolidColorBrush(Color.FromArgb(220, 0, 0, 0)),
+ StrokeThickness = 1,
+ Cursor = cursors[i],
+ Tag = tags[i]
+ };
+ h.MouseLeftButtonDown += Handle_MouseDown;
+ h.MouseMove += Handle_MouseMove;
+ h.MouseLeftButtonUp += Handle_MouseUp;
+ _handles[i] = h;
+ _canvas.Children.Add(h);
+ }
+
+ _toolbar = BuildToolbar();
+ _canvas.Children.Add(_toolbar);
+
+ Content = _canvas;
+
+ Loaded += (s, e) =>
+ {
+ LayoutAll();
+ };
+ SizeChanged += (s, e) => LayoutAll();
+ KeyDown += (s, e) => { if (e.Key == Key.Escape) Close(); };
+ }
+
+ private System.Windows.Controls.Border BuildToolbar()
+ {
+ var bar = new System.Windows.Controls.Border
+ {
+ Background = new SolidColorBrush(Color.FromArgb(245, 26, 26, 26)),
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(8, 4, 8, 4),
+ SnapsToDevicePixels = true
+ };
+ var sp = new StackPanel { Orientation = Orientation.Horizontal };
+
+ _blackoutButton = new ToggleButton { Content = MakeBtnContent("💡", "关灯") };
+ StyleToolButton(_blackoutButton);
+ _blackoutButton.Checked += (s, e) => SetBlackout(true);
+ _blackoutButton.Unchecked += (s, e) => SetBlackout(false);
+
+ var closeBtn = new System.Windows.Controls.Button { Content = MakeBtnContent("✕", "关闭") };
+ StyleToolButton(closeBtn);
+ closeBtn.Click += (s, e) => Close();
+
+ sp.Children.Add(_blackoutButton);
+ sp.Children.Add(closeBtn);
+ bar.Child = sp;
+ return bar;
+ }
+
+ private static FrameworkElement MakeBtnContent(string icon, string text)
+ {
+ var sp = new StackPanel { Orientation = Orientation.Vertical, HorizontalAlignment = HorizontalAlignment.Center };
+ sp.Children.Add(new TextBlock { Text = icon, FontSize = 16, Foreground = Brushes.White, HorizontalAlignment = HorizontalAlignment.Center });
+ sp.Children.Add(new TextBlock { Text = text, FontSize = 11, Foreground = Brushes.White, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 1, 0, 0) });
+ return sp;
+ }
+
+ private static void StyleToolButton(ButtonBase b)
+ {
+ b.Width = 56;
+ b.Height = 46;
+ b.Margin = new Thickness(3, 0, 3, 0);
+ b.Background = Brushes.Transparent;
+ b.BorderThickness = new Thickness(0);
+ b.Foreground = Brushes.White;
+ b.Cursor = Cursors.Hand;
+ b.Padding = new Thickness(0);
+ b.Template = BuildToolButtonTemplate();
+ }
+
+ private static ControlTemplate BuildToolButtonTemplate()
+ {
+ var t = new ControlTemplate(typeof(ButtonBase));
+ var border = new FrameworkElementFactory(typeof(System.Windows.Controls.Border));
+ border.Name = "Bd";
+ border.SetValue(System.Windows.Controls.Border.BackgroundProperty, new TemplateBindingExtension(System.Windows.Controls.Control.BackgroundProperty));
+ border.SetValue(System.Windows.Controls.Border.CornerRadiusProperty, new CornerRadius(6));
+ var cp = new FrameworkElementFactory(typeof(ContentPresenter));
+ cp.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
+ cp.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
+ border.AppendChild(cp);
+ t.VisualTree = border;
+
+ var hover = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
+ hover.Setters.Add(new Setter(System.Windows.Controls.Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(60, 255, 255, 255)), "Bd"));
+ t.Triggers.Add(hover);
+
+ var checkedT = new Trigger { Property = ToggleButton.IsCheckedProperty, Value = true };
+ checkedT.Setters.Add(new Setter(System.Windows.Controls.Border.BackgroundProperty, new SolidColorBrush(Color.FromRgb(0x05, 0x96, 0x69)), "Bd"));
+ t.Triggers.Add(checkedT);
+ return t;
+ }
+
+ #region 布局
+
+ private void LayoutAll()
+ {
+ double w = _canvas.ActualWidth;
+ double h = _canvas.ActualHeight;
+ if (w <= 0 || h <= 0) return;
+
+ // 首次居中
+ if (_boxLeft + _boxWidth > w) _boxLeft = Math.Max(0, w - _boxWidth);
+ if (_boxTop + _boxHeight > h) _boxTop = Math.Max(0, h - _boxHeight);
+
+ // 遮罩占满
+ _overlay.Width = w;
+ _overlay.Height = h;
+ System.Windows.Controls.Canvas.SetLeft(_overlay, 0);
+ System.Windows.Controls.Canvas.SetTop(_overlay, 0);
+ UpdateOverlayClip();
+
+ // 选择框
+ _selectionBorder.Width = _boxWidth;
+ _selectionBorder.Height = _boxHeight;
+ System.Windows.Controls.Canvas.SetLeft(_selectionBorder, _boxLeft);
+ System.Windows.Controls.Canvas.SetTop(_selectionBorder, _boxTop);
+
+ // 8 个控点(中心定位)
+ PositionHandle(0, _boxLeft, _boxTop); // NW
+ PositionHandle(1, _boxLeft + _boxWidth / 2, _boxTop); // N
+ PositionHandle(2, _boxLeft + _boxWidth, _boxTop); // NE
+ PositionHandle(3, _boxLeft + _boxWidth, _boxTop + _boxHeight / 2); // E
+ PositionHandle(4, _boxLeft + _boxWidth, _boxTop + _boxHeight); // SE
+ PositionHandle(5, _boxLeft + _boxWidth / 2, _boxTop + _boxHeight); // S
+ PositionHandle(6, _boxLeft, _boxTop + _boxHeight); // SW
+ PositionHandle(7, _boxLeft, _boxTop + _boxHeight / 2); // W
+
+ // 工具栏:选择框正下方居中;若超出则放上方
+ _toolbar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ double tw = _toolbar.DesiredSize.Width, th = _toolbar.DesiredSize.Height;
+ double tx = _boxLeft + _boxWidth / 2 - tw / 2;
+ double ty = _boxTop + _boxHeight + 12;
+ if (ty + th > h) ty = _boxTop - th - 12;
+ tx = Math.Max(8, Math.Min(w - tw - 8, tx));
+ System.Windows.Controls.Canvas.SetLeft(_toolbar, tx);
+ System.Windows.Controls.Canvas.SetTop(_toolbar, ty);
+ }
+
+ private void PositionHandle(int i, double cx, double cy)
+ {
+ var dot = _handles[i];
+ System.Windows.Controls.Canvas.SetLeft(dot, cx - dot.Width / 2);
+ System.Windows.Controls.Canvas.SetTop(dot, cy - dot.Height / 2);
+ }
+
+ private void UpdateOverlayClip()
+ {
+ // 用 EvenOdd Geometry 在 overlay 上挖空选择框区域
+ double w = _canvas.ActualWidth;
+ double h = _canvas.ActualHeight;
+ if (w <= 0 || h <= 0) { _overlay.Clip = null; return; }
+
+ var outer = new RectangleGeometry(new Rect(0, 0, w, h));
+ var inner = new RectangleGeometry(new Rect(_boxLeft, _boxTop, _boxWidth, _boxHeight));
+ var combined = new CombinedGeometry(GeometryCombineMode.Exclude, outer, inner);
+ _overlay.Clip = combined;
+ }
+
+ #endregion
+
+ #region 拖动
+
+ private bool _draggingBox;
+ private Point _dragStart;
+ private double _dragBoxLeft, _dragBoxTop;
+
+ private void SelectionBorder_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ _draggingBox = true;
+ _dragStart = e.GetPosition(_canvas);
+ _dragBoxLeft = _boxLeft;
+ _dragBoxTop = _boxTop;
+ _selectionBorder.CaptureMouse();
+ e.Handled = true;
+ }
+
+ private void SelectionBorder_MouseMove(object sender, MouseEventArgs e)
+ {
+ if (!_draggingBox) return;
+ var p = e.GetPosition(_canvas);
+ _boxLeft = Math.Max(0, Math.Min(_canvas.ActualWidth - _boxWidth, _dragBoxLeft + p.X - _dragStart.X));
+ _boxTop = Math.Max(0, Math.Min(_canvas.ActualHeight - _boxHeight, _dragBoxTop + p.Y - _dragStart.Y));
+ LayoutAll();
+ }
+
+ private void SelectionBorder_MouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (!_draggingBox) return;
+ _draggingBox = false;
+ _selectionBorder.ReleaseMouseCapture();
+ }
+
+ #endregion
+
+ #region 缩放
+
+ private bool _resizing;
+ private string _resizeTag;
+ private Point _resizeStart;
+ private double _rL, _rT, _rW, _rH;
+
+ private void Handle_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ var el = (FrameworkElement)sender;
+ _resizing = true;
+ _resizeTag = (string)el.Tag;
+ _resizeStart = e.GetPosition(_canvas);
+ _rL = _boxLeft; _rT = _boxTop; _rW = _boxWidth; _rH = _boxHeight;
+ el.CaptureMouse();
+ e.Handled = true;
+ }
+
+ private void Handle_MouseMove(object sender, MouseEventArgs e)
+ {
+ if (!_resizing) return;
+ var p = e.GetPosition(_canvas);
+ double dx = p.X - _resizeStart.X;
+ double dy = p.Y - _resizeStart.Y;
+
+ double nL = _rL, nT = _rT, nW = _rW, nH = _rH;
+ switch (_resizeTag)
+ {
+ case "NW": nL = _rL + dx; nT = _rT + dy; nW = _rW - dx; nH = _rH - dy; break;
+ case "N": nT = _rT + dy; nH = _rH - dy; break;
+ case "NE": nT = _rT + dy; nW = _rW + dx; nH = _rH - dy; break;
+ case "E": nW = _rW + dx; break;
+ case "SE": nW = _rW + dx; nH = _rH + dy; break;
+ case "S": nH = _rH + dy; break;
+ case "SW": nL = _rL + dx; nW = _rW - dx; nH = _rH + dy; break;
+ case "W": nL = _rL + dx; nW = _rW - dx; break;
+ }
+ if (nW < MinBoxW) { if (_resizeTag.Contains("W")) nL -= MinBoxW - nW; nW = MinBoxW; }
+ if (nH < MinBoxH) { if (_resizeTag.Contains("N")) nT -= MinBoxH - nH; nH = MinBoxH; }
+
+ // 限制在窗口内
+ nL = Math.Max(0, nL);
+ nT = Math.Max(0, nT);
+ if (nL + nW > _canvas.ActualWidth) nW = _canvas.ActualWidth - nL;
+ if (nT + nH > _canvas.ActualHeight) nH = _canvas.ActualHeight - nT;
+
+ _boxLeft = nL; _boxTop = nT; _boxWidth = nW; _boxHeight = nH;
+ LayoutAll();
+ }
+
+ private void Handle_MouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (!_resizing) return;
+ _resizing = false;
+ ((FrameworkElement)sender).ReleaseMouseCapture();
+ }
+
+ #endregion
+
+ #region 关灯
+
+ private void SetBlackout(bool on)
+ {
+ _blackoutOn = on;
+ _overlay.Fill = new SolidColorBrush(on
+ ? Color.FromArgb(245, 0, 0, 0)
+ : Color.FromArgb(102, 0, 0, 0));
+ }
+
+ #endregion
+
+ #region Magnifier 子窗口
+
+ private class MagnifierHost : HwndHost
+ {
+ private readonly MagnifierWindow _owner;
+ public IntPtr MagHwnd { get; private set; }
+ public MagnifierHost(MagnifierWindow owner) { _owner = owner; }
+
+ protected override HandleRef BuildWindowCore(HandleRef hwndParent)
+ {
+ if (!MagInitialize()) throw new InvalidOperationException("MagInitialize 失败");
+ _owner._magInitialized = true;
+
+ MagHwnd = CreateWindowEx(
+ 0, MagnifierClassName, "ICCMagnifier",
+ WS_CHILD | WS_VISIBLE,
+ 0, 0, 100, 100,
+ hwndParent.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
+ _owner._magHwnd = MagHwnd;
+ return new HandleRef(this, MagHwnd);
+ }
+
+ protected override void DestroyWindowCore(HandleRef hwnd)
+ {
+ if (hwnd.Handle != IntPtr.Zero) DestroyWindow(hwnd.Handle);
+ MagHwnd = IntPtr.Zero;
+ }
+ }
+
+ protected override void OnSourceInitialized(EventArgs e)
+ {
+ base.OnSourceInitialized(e);
+ _hwndSource = (HwndSource)PresentationSource.FromVisual(this);
+ ApplyTransform();
+ if (_hwndSource != null && _magHwnd != IntPtr.Zero)
+ MagSetWindowFilterList(_magHwnd, MW_FILTERMODE_EXCLUDE, 1, new[] { _hwndSource.Handle });
+
+ _timer = new DispatcherTimer(DispatcherPriority.Render) { Interval = TimeSpan.FromMilliseconds(33) };
+ _timer.Tick += OnTick;
+ _timer.Start();
+ }
+
+ private void ApplyTransform()
+ {
+ if (_magHwnd == IntPtr.Zero) return;
+ var m = new MAGTRANSFORM
+ {
+ m00 = _zoom, m01 = 0, m02 = 0,
+ m10 = 0, m11 = _zoom, m12 = 0,
+ m20 = 0, m21 = 0, m22 = 1.0f
+ };
+ MagSetWindowTransform(_magHwnd, ref m);
+ }
+
+ private void OnTick(object sender, EventArgs e)
+ {
+ if (_magHwnd == IntPtr.Zero || _magHost == null) return;
+ if (_magHost.RenderSize.Width <= 0) return;
+
+ // 选择框在屏幕物理像素坐标
+ var hostOffset = _magHost.TransformToAncestor(this).Transform(new Point(0, 0));
+ var tl = PointToScreen(hostOffset);
+ var br = PointToScreen(new Point(hostOffset.X + _magHost.RenderSize.Width,
+ hostOffset.Y + _magHost.RenderSize.Height));
+ int viewW = (int)(br.X - tl.X);
+ int viewH = (int)(br.Y - tl.Y);
+ if (viewW <= 0 || viewH <= 0) return;
+
+ int srcW = Math.Max(1, (int)(viewW / _zoom));
+ int srcH = Math.Max(1, (int)(viewH / _zoom));
+ int cx = (int)((tl.X + br.X) / 2);
+ int cy = (int)((tl.Y + br.Y) / 2);
+ var src = new RECT(cx - srcW / 2, cy - srcH / 2,
+ cx - srcW / 2 + srcW, cy - srcH / 2 + srcH);
+ MagSetWindowSource(_magHwnd, src);
+ InvalidateRect(_magHwnd, IntPtr.Zero, false);
+ }
+
+ #endregion
+
+ protected override void OnClosed(EventArgs e)
+ {
+ if (_timer != null) { _timer.Stop(); _timer.Tick -= OnTick; _timer = null; }
+ if (_magInitialized) { MagUninitialize(); _magInitialized = false; }
+ base.OnClosed(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ink Canvas/Windows/PPTQuickPanel.xaml b/Ink Canvas/Windows/PPTQuickPanel.xaml
index 6a7262ea..55967f42 100644
--- a/Ink Canvas/Windows/PPTQuickPanel.xaml
+++ b/Ink Canvas/Windows/PPTQuickPanel.xaml
@@ -3,69 +3,127 @@
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"
mc:Ignorable="d"
- d:DesignHeight="400" d:DesignWidth="300">
+ d:DesignHeight="420" d:DesignWidth="260">
-
+
+ Storyboard.TargetProperty="X"
+ Duration="0:0:0.28">
-
-
+ Storyboard.TargetProperty="X"
+ Duration="0:0:0.28">
-
+
+
+
+
+
+
+
-
+
-
-
-
+
\ No newline at end of file
diff --git a/Ink Canvas/Windows/PPTQuickPanel.xaml.cs b/Ink Canvas/Windows/PPTQuickPanel.xaml.cs
index 2ea42ea9..02699271 100644
--- a/Ink Canvas/Windows/PPTQuickPanel.xaml.cs
+++ b/Ink Canvas/Windows/PPTQuickPanel.xaml.cs
@@ -118,6 +118,19 @@ namespace Ink_Canvas.Windows
Loaded += PPTQuickPanel_Loaded;
Unloaded += PPTQuickPanel_Unloaded;
IsVisibleChanged += PPTQuickPanel_IsVisibleChanged;
+
+ MagnifierWindow.Closed2 += OnMagnifierClosed;
+ }
+
+ private void OnMagnifierClosed(object sender, EventArgs e)
+ {
+ Dispatcher.BeginInvoke(new Action(SyncMagnifierButtonState));
+ }
+
+ private void SyncMagnifierButtonState()
+ {
+ if (MagnifierToggleButton == null) return;
+ MagnifierToggleButton.Content = MagnifierWindow.HasInstance ? "关闭放大镜" : "开启放大镜";
}
private void PPTQuickPanel_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
@@ -125,6 +138,7 @@ namespace Ink_Canvas.Windows
if (Visibility == Visibility.Visible)
{
ApplyTheme();
+ SyncMagnifierButtonState();
}
}
@@ -747,31 +761,34 @@ namespace Ink_Canvas.Windows
private void ArrowButton_MouseEnter(object sender, MouseEventArgs e)
{
- // 根据当前主题设置悬停颜色
- bool isDark = ArrowButtonBackgroundBrush.Color.R < 128;
- if (isDark)
- {
- ArrowButtonBackgroundBrush.Color = Color.FromArgb(230, 32, 32, 32);
- }
- else
- {
- ArrowButtonBackgroundBrush.Color = Color.FromArgb(230, 255, 255, 255);
- }
+ ArrowButtonBackgroundBrush.Color = Color.FromArgb(220, 55, 55, 55);
}
private void ArrowButton_MouseLeave(object sender, MouseEventArgs e)
{
- // 恢复主题颜色
- ApplyTheme();
+ ArrowButtonBackgroundBrush.Color = Color.FromArgb(204, 31, 31, 31);
}
#endregion
#region 拖动手势
+ private static bool IsWithinSlider(object source)
+ {
+ var d = source as DependencyObject;
+ while (d != null)
+ {
+ if (d is Slider) return true;
+ d = (d is System.Windows.Media.Visual || d is System.Windows.Media.Media3D.Visual3D)
+ ? System.Windows.Media.VisualTreeHelper.GetParent(d)
+ : LogicalTreeHelper.GetParent(d);
+ }
+ return false;
+ }
+
private void ContentBorder_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
- if (e.OriginalSource is Slider) return; // 如果点击的是滑块,不处理拖动
+ if (IsWithinSlider(e.OriginalSource)) return;
_isDragging = true;
_dragStartPoint = e.GetPosition(MainCanvas);
@@ -816,7 +833,7 @@ namespace Ink_Canvas.Windows
private void ContentBorder_TouchDown(object sender, TouchEventArgs e)
{
- if (e.OriginalSource is Slider) return;
+ if (IsWithinSlider(e.OriginalSource)) return;
_isDragging = true;
_dragStartPoint = e.GetTouchPoint(MainCanvas).Position;
@@ -1835,35 +1852,16 @@ namespace Ink_Canvas.Windows
{
try
{
- bool isDarkTheme = settings.Appearance.Theme == 1 ||
- (settings.Appearance.Theme == 2 && !ThemeHelper.IsSystemThemeLight());
-
- if (isDarkTheme)
- {
- // 深色主题:使用80%不透明度的深色背景
- ArrowButtonBackgroundBrush.Color = Color.FromArgb(204, 32, 32, 32); // #CC202020
- ContentBackgroundBrush.Color = Color.FromArgb(204, 32, 32, 32); // #CC202020
- ArrowPathFillBrush.Color = Colors.White;
- VolumeIconFillBrush.Color = Colors.White;
- VolumeIconFillBrush2.Color = Colors.White;
- VolumeValueForegroundBrush.Color = Color.FromArgb(200, 255, 255, 255);
- MagnifierTitleForegroundBrush.Color = Colors.White;
- MagnifierDescForegroundBrush.Color = Color.FromArgb(200, 255, 255, 255);
- Separator1BackgroundBrush.Color = Color.FromArgb(128, 255, 255, 255);
- }
- else
- {
- // 浅色主题:使用80%不透明度的白色背景
- ArrowButtonBackgroundBrush.Color = Color.FromArgb(204, 255, 255, 255); // #CCFFFFFF
- ContentBackgroundBrush.Color = Color.FromArgb(204, 255, 255, 255); // #CCFFFFFF
- ArrowPathFillBrush.Color = Colors.Black;
- VolumeIconFillBrush.Color = Colors.Black;
- VolumeIconFillBrush2.Color = Colors.Black;
- VolumeValueForegroundBrush.Color = Color.FromArgb(128, 0, 0, 0);
- MagnifierTitleForegroundBrush.Color = Colors.Black;
- MagnifierDescForegroundBrush.Color = Color.FromArgb(128, 0, 0, 0);
- Separator1BackgroundBrush.Color = Color.FromArgb(255, 224, 224, 224);
- }
+ // Game Bar 风格:始终使用深色半透明外壳,不随系统主题翻转
+ ArrowButtonBackgroundBrush.Color = Color.FromArgb(204, 31, 31, 31);
+ ContentBackgroundBrush.Color = Color.FromArgb(204, 31, 31, 31); // #CC1F1F1F
+ ArrowPathFillBrush.Color = Colors.White;
+ VolumeIconFillBrush.Color = Colors.White;
+ VolumeIconFillBrush2.Color = Colors.White;
+ VolumeValueForegroundBrush.Color = Color.FromArgb(230, 255, 255, 255);
+ MagnifierTitleForegroundBrush.Color = Colors.White;
+ MagnifierDescForegroundBrush.Color = Color.FromArgb(200, 255, 255, 255);
+ Separator1BackgroundBrush.Color = Color.FromArgb(60, 255, 255, 255);
}
catch (Exception ex)
{
@@ -1873,6 +1871,46 @@ namespace Ink_Canvas.Windows
#endregion
+ #region 聚焦放大镜
+
+ private void MagnifierToggleButton_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (MagnifierWindow.HasInstance)
+ {
+ MagnifierWindow.HideInstance();
+ MagnifierToggleButton.Content = "开启放大镜";
+ }
+ else
+ {
+ MagnifierWindow.Show((float)MagnifierZoomSlider.Value);
+ if (MagnifierWindow.HasInstance)
+ {
+ MagnifierToggleButton.Content = "关闭放大镜";
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.WriteLogToFile($"切换聚焦放大镜失败: {ex.Message}", LogHelper.LogType.Error);
+ }
+ }
+
+ private void MagnifierZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ if (MagnifierZoomValueText != null)
+ {
+ MagnifierZoomValueText.Text = $"{e.NewValue:0.0}x";
+ }
+ if (MagnifierWindow.HasInstance)
+ {
+ MagnifierWindow.SetZoom((float)e.NewValue);
+ }
+ }
+
+ #endregion
+
#region 公开方法
///
@@ -1881,6 +1919,12 @@ namespace Ink_Canvas.Windows
public void UpdateVisibility(bool isInPPTMode)
{
Visibility = isInPPTMode ? Visibility.Visible : Visibility.Collapsed;
+ if (!isInPPTMode && MagnifierWindow.HasInstance)
+ {
+ MagnifierWindow.HideInstance();
+ if (MagnifierToggleButton != null)
+ MagnifierToggleButton.Content = "开启放大镜";
+ }
}
#endregion
diff --git a/Ink Canvas/Windows/SettingsViews/Helpers/MainWindowSettingsHelper.cs b/Ink Canvas/Windows/SettingsViews/Helpers/MainWindowSettingsHelper.cs
index a8aacd5d..c0df8820 100644
--- a/Ink Canvas/Windows/SettingsViews/Helpers/MainWindowSettingsHelper.cs
+++ b/Ink Canvas/Windows/SettingsViews/Helpers/MainWindowSettingsHelper.cs
@@ -339,14 +339,12 @@ namespace Ink_Canvas.Windows.SettingsViews.Helpers
};
bool isThemeRelated = false;
- string controlNameLower = controlName.ToLower();
foreach (var themeControl in themeRelatedControls)
{
- string themeControlLower = themeControl.ToLower();
- if (controlNameLower.Contains(themeControlLower) ||
- themeControlLower.Contains(controlNameLower) ||
- controlNameLower == themeControlLower)
+ // OrdinalIgnoreCase 避免在循环里反复 ToLower() 生成中间字符串。
+ if (controlName.IndexOf(themeControl, StringComparison.OrdinalIgnoreCase) >= 0 ||
+ themeControl.IndexOf(controlName, StringComparison.OrdinalIgnoreCase) >= 0)
{
isThemeRelated = true;
break;
diff --git a/Ink Canvas/Windows/SettingsViews/Pages/AppearancePage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/AppearancePage.xaml.cs
index befe7f63..a3b317f0 100644
--- a/Ink Canvas/Windows/SettingsViews/Pages/AppearancePage.xaml.cs
+++ b/Ink Canvas/Windows/SettingsViews/Pages/AppearancePage.xaml.cs
@@ -228,8 +228,14 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
private void ViewboxFloatingBarScaleTransformValueSlider_ValueChanged(object sender, RoutedEventArgs e)
{
if (!_isLoaded) return;
- var val = Math.Round(ViewboxFloatingBarScaleTransformValueSlider.Value, 2);
- ViewboxFloatingBarScaleTransformValueSlider.Value = val;
+ var slider = ViewboxFloatingBarScaleTransformValueSlider;
+ var val = Math.Round(slider.Value, 2);
+ // 仅当四舍五入纠正了显示值时才回写;那次 set 会重入 ValueChanged 完成保存。
+ if (slider.Value != val)
+ {
+ slider.Value = val;
+ return;
+ }
SettingsManager.Settings.Appearance.ViewboxFloatingBarScaleTransformValue = val;
SettingsManager.SaveSettingsToFile();
var mw = GetMainWindow();
@@ -247,8 +253,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
private void ViewboxFloatingBarOpacityValueSlider_ValueChanged(object sender, RoutedEventArgs e)
{
if (!_isLoaded) return;
- var val = Math.Round(ViewboxFloatingBarOpacityValueSlider.Value, 2);
- ViewboxFloatingBarOpacityValueSlider.Value = val;
+ var slider = ViewboxFloatingBarOpacityValueSlider;
+ var val = Math.Round(slider.Value, 2);
+ if (slider.Value != val)
+ {
+ slider.Value = val;
+ return;
+ }
SettingsManager.Settings.Appearance.ViewboxFloatingBarOpacityValue = val;
SettingsManager.SaveSettingsToFile();
var mw = GetMainWindow();
@@ -258,8 +269,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
private void ViewboxFloatingBarOpacityInPPTValueSlider_ValueChanged(object sender, RoutedEventArgs e)
{
if (!_isLoaded) return;
- var val = Math.Round(ViewboxFloatingBarOpacityInPPTValueSlider.Value, 2);
- ViewboxFloatingBarOpacityInPPTValueSlider.Value = val;
+ var slider = ViewboxFloatingBarOpacityInPPTValueSlider;
+ var val = Math.Round(slider.Value, 2);
+ if (slider.Value != val)
+ {
+ slider.Value = val;
+ return;
+ }
SettingsManager.Settings.Appearance.ViewboxFloatingBarOpacityInPPTValue = val;
SettingsManager.SaveSettingsToFile();
var mw = GetMainWindow();
@@ -290,8 +306,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
private void ViewboxBlackBoardScaleTransformValueSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
{
if (!_isLoaded) return;
- var val = Math.Round(ViewboxBlackBoardScaleTransformValueSlider.Value, 2);
- ViewboxBlackBoardScaleTransformValueSlider.Value = val;
+ var slider = ViewboxBlackBoardScaleTransformValueSlider;
+ var val = Math.Round(slider.Value, 2);
+ if (slider.Value != val)
+ {
+ slider.Value = val;
+ return;
+ }
SettingsManager.Settings.Appearance.ViewboxBlackBoardScaleTransformValue = val;
SettingsManager.SaveSettingsToFile();
var mw = GetMainWindow();
diff --git a/Ink Canvas/Windows/SettingsViews/Pages/AutomationPage.xaml b/Ink Canvas/Windows/SettingsViews/Pages/AutomationPage.xaml
index 349fe365..a536648c 100644
--- a/Ink Canvas/Windows/SettingsViews/Pages/AutomationPage.xaml
+++ b/Ink Canvas/Windows/SettingsViews/Pages/AutomationPage.xaml
@@ -373,30 +373,30 @@
Toggled="ToggleSwitchSaveFullPageStrokes_Toggled"/>
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
e)
{
if (!_isLoaded) return;
- var val = Math.Round(BrushAutoRestoreWidthSlider.Value, 2);
- BrushAutoRestoreWidthSlider.Value = val;
+ var slider = BrushAutoRestoreWidthSlider;
+ var val = Math.Round(slider.Value, 2);
+ // 仅在四舍五入纠正了显示值时回写;那次 set 会重入 ValueChanged 完成保存。
+ if (slider.Value != val)
+ {
+ slider.Value = val;
+ return;
+ }
SettingsManager.Settings.Canvas.BrushAutoRestoreWidth = val;
SettingsManager.SaveSettingsToFile();
}
diff --git a/Ink Canvas/Windows/SettingsViews/Pages/DebugPage.xaml b/Ink Canvas/Windows/SettingsViews/Pages/DebugPage.xaml
index 50f5540b..50842bda 100644
--- a/Ink Canvas/Windows/SettingsViews/Pages/DebugPage.xaml
+++ b/Ink Canvas/Windows/SettingsViews/Pages/DebugPage.xaml
@@ -8,6 +8,7 @@
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:controls="clr-namespace:Ink_Canvas.Controls;assembly=InkCanvas.Controls"
+ xmlns:i18n="clr-namespace:Ink_Canvas.MarkupExtensions"
mc:Ignorable="d"
Title="Debug">
@@ -32,8 +33,8 @@
Text="Debug" />
diff --git a/Ink Canvas/Windows/SettingsViews/Pages/RandomDrawPage.xaml.cs b/Ink Canvas/Windows/SettingsViews/Pages/RandomDrawPage.xaml.cs
index 5229cd89..b9e93862 100644
--- a/Ink Canvas/Windows/SettingsViews/Pages/RandomDrawPage.xaml.cs
+++ b/Ink Canvas/Windows/SettingsViews/Pages/RandomDrawPage.xaml.cs
@@ -216,8 +216,14 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
private void MLAvoidanceWeightSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
{
if (!_isLoaded) return;
- var val = Math.Round(MLAvoidanceWeightSlider.Value, 2);
- MLAvoidanceWeightSlider.Value = val;
+ var slider = MLAvoidanceWeightSlider;
+ var val = Math.Round(slider.Value, 2);
+ // 仅当四舍五入纠正了显示值时才回写;那次 set 会重入 ValueChanged 完成保存。
+ if (slider.Value != val)
+ {
+ slider.Value = val;
+ return;
+ }
SettingsManager.Settings.RandSettings.MLAvoidanceWeight = val;
SettingsManager.SaveSettingsToFile();
}
@@ -281,8 +287,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
private void TimerVolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
{
if (!_isLoaded) return;
- var val = Math.Round(TimerVolumeSlider.Value, 2);
- TimerVolumeSlider.Value = val;
+ var slider = TimerVolumeSlider;
+ var val = Math.Round(slider.Value, 2);
+ if (slider.Value != val)
+ {
+ slider.Value = val;
+ return;
+ }
SettingsManager.Settings.RandSettings.TimerVolume = val;
SettingsManager.SaveSettingsToFile();
}
@@ -321,8 +332,13 @@ namespace Ink_Canvas.Windows.SettingsViews.Pages
private void ProgressiveReminderVolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
{
if (!_isLoaded) return;
- var val = Math.Round(ProgressiveReminderVolumeSlider.Value, 2);
- ProgressiveReminderVolumeSlider.Value = val;
+ var slider = ProgressiveReminderVolumeSlider;
+ var val = Math.Round(slider.Value, 2);
+ if (slider.Value != val)
+ {
+ slider.Value = val;
+ return;
+ }
SettingsManager.Settings.RandSettings.ProgressiveReminderVolume = val;
SettingsManager.SaveSettingsToFile();
}
diff --git a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml
index 9444c0ad..9ba11761 100644
--- a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml
+++ b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml
@@ -155,9 +155,9 @@
+ ToolTipService.ToolTip="{i18n:I18n Key=Settings_Nav_Security_Tooltip}">
@@ -195,9 +195,9 @@
+ ToolTipService.ToolTip="{i18n:I18n Key=Settings_Nav_Hotkey_Tooltip}">
diff --git a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs
index 93e9b628..828cfce0 100644
--- a/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs
+++ b/Ink Canvas/Windows/SettingsViews/SettingsWindow.xaml.cs
@@ -587,10 +587,9 @@ namespace Ink_Canvas.Windows.SettingsViews
if (string.IsNullOrWhiteSpace(raw)) return;
string query = raw.Trim();
- string queryLower = query.ToLower();
var entry = _searchIndex.FirstOrDefault(e => e.Text.Equals(query, StringComparison.OrdinalIgnoreCase))
- ?? _searchIndex.FirstOrDefault(e => e.Text.ToLower().Contains(queryLower));
+ ?? _searchIndex.FirstOrDefault(e => e.Text.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0);
NavigateToSearchEntry(entry);
}
@@ -608,9 +607,8 @@ namespace Ink_Canvas.Windows.SettingsViews
return;
}
- string queryLower = query.ToLower();
var suggestions = _searchIndex
- .Where(e => e.Text.ToLower().Contains(queryLower))
+ .Where(e => e.Text.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0)
.Select(e => e.Text)
.Distinct()
.Take(50)
diff --git a/Ink Canvas/Windows/TimerControl.xaml.cs b/Ink Canvas/Windows/TimerControl.xaml.cs
index 97e0c934..8a962d87 100644
--- a/Ink Canvas/Windows/TimerControl.xaml.cs
+++ b/Ink Canvas/Windows/TimerControl.xaml.cs
@@ -349,10 +349,68 @@ namespace Ink_Canvas.Windows
ThemeHelper.ApplyTheme(this, settings, theme =>
{
if (theme == "Dark") SetDarkThemeBorder();
- UpdateDigitDisplays();
+ // 复用当前状态下的显示逻辑,避免把暂停/超时/运行中的读数重置为初始设定时间。
+ RefreshDigitDisplayForCurrentState();
});
}
+ ///
+ /// 依据当前计时器状态刷新数字显示:
+ /// 运行中按 DateTime.Now、暂停中按 pauseTime 推算;处于超时模式时按超时值渲染;未启动时回退到初始设定值。
+ /// 供主题切换等场景直接复用 Timer_Elapsed 的渲染分支。
+ ///
+ private void RefreshDigitDisplayForCurrentState()
+ {
+ if (!isTimerRunning)
+ {
+ UpdateDigitDisplays();
+ return;
+ }
+
+ DateTime referenceTime = isPaused ? pauseTime : DateTime.Now;
+ TimeSpan timeSpan = referenceTime - startTime;
+ TimeSpan totalTimeSpan = new TimeSpan(hour, minute, second);
+
+ if (!isOvertimeMode)
+ {
+ TimeSpan leftTimeSpan = totalTimeSpan - timeSpan;
+ if (leftTimeSpan.Milliseconds > 0) leftTimeSpan += new TimeSpan(0, 0, 1);
+ if (leftTimeSpan < TimeSpan.Zero) leftTimeSpan = TimeSpan.Zero;
+
+ int displayHours = Math.Min(99, (int)leftTimeSpan.TotalHours);
+ SetDigitDisplay("Digit1Display", displayHours / 10);
+ SetDigitDisplay("Digit2Display", displayHours % 10);
+ SetDigitDisplay("Digit3Display", leftTimeSpan.Minutes / 10);
+ SetDigitDisplay("Digit4Display", leftTimeSpan.Minutes % 10);
+ SetDigitDisplay("Digit5Display", leftTimeSpan.Seconds / 10);
+ SetDigitDisplay("Digit6Display", leftTimeSpan.Seconds % 10);
+ SetColonDisplay(false);
+ }
+ else
+ {
+ TimeSpan overtimeSpan = timeSpan - totalTimeSpan;
+ if (overtimeSpan < TimeSpan.Zero) overtimeSpan = TimeSpan.Zero;
+
+ int displayHours = Math.Max(0, Math.Min(99, (int)overtimeSpan.TotalHours));
+ bool shouldShowRed = MainWindow.Settings?.RandSettings?.EnableOvertimeRedText == true;
+
+ 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);
+ }
+ }
+
private void UpdateDigitDisplays()
{
SetDigitDisplay("Digit1Display", hour / 10);
diff --git a/InkCanvasForClass.IACoreHelper/App.config b/InkCanvas.IACoreHelper/App.config
similarity index 100%
rename from InkCanvasForClass.IACoreHelper/App.config
rename to InkCanvas.IACoreHelper/App.config
diff --git a/InkCanvasForClass.IACoreHelper/InkCanvasForClass.IACoreHelper.csproj b/InkCanvas.IACoreHelper/InkCanvas.IACoreHelper.csproj
similarity index 99%
rename from InkCanvasForClass.IACoreHelper/InkCanvasForClass.IACoreHelper.csproj
rename to InkCanvas.IACoreHelper/InkCanvas.IACoreHelper.csproj
index 2bd4ccd1..7a621447 100644
--- a/InkCanvasForClass.IACoreHelper/InkCanvasForClass.IACoreHelper.csproj
+++ b/InkCanvas.IACoreHelper/InkCanvas.IACoreHelper.csproj
@@ -11,7 +11,6 @@
x86
-
diff --git a/InkCanvasForClass.IACoreHelper/IpcProtocol.cs b/InkCanvas.IACoreHelper/IpcProtocol.cs
similarity index 99%
rename from InkCanvasForClass.IACoreHelper/IpcProtocol.cs
rename to InkCanvas.IACoreHelper/IpcProtocol.cs
index ed93f411..765062e7 100644
--- a/InkCanvasForClass.IACoreHelper/IpcProtocol.cs
+++ b/InkCanvas.IACoreHelper/IpcProtocol.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Text;
-namespace InkCanvasForClass.IACoreHelper
+namespace InkCanvas.IACoreHelper
{
// Named Pipe 名称,主进程和辅助进程共用
internal static class IpcConstants
diff --git a/InkCanvasForClass.IACoreHelper/Program.cs b/InkCanvas.IACoreHelper/Program.cs
similarity index 99%
rename from InkCanvasForClass.IACoreHelper/Program.cs
rename to InkCanvas.IACoreHelper/Program.cs
index 640606f5..cddd1ddf 100644
--- a/InkCanvasForClass.IACoreHelper/Program.cs
+++ b/InkCanvas.IACoreHelper/Program.cs
@@ -5,7 +5,7 @@ using System.Linq;
using System.Windows.Ink;
using System.Windows.Input;
-namespace InkCanvasForClass.IACoreHelper
+namespace InkCanvas.IACoreHelper
{
internal static class Program
{
diff --git a/InkCanvasForClass.IACoreHelper/Properties/AssemblyInfo.cs b/InkCanvas.IACoreHelper/Properties/AssemblyInfo.cs
similarity index 74%
rename from InkCanvasForClass.IACoreHelper/Properties/AssemblyInfo.cs
rename to InkCanvas.IACoreHelper/Properties/AssemblyInfo.cs
index 51bf9aa3..996eff24 100644
--- a/InkCanvasForClass.IACoreHelper/Properties/AssemblyInfo.cs
+++ b/InkCanvas.IACoreHelper/Properties/AssemblyInfo.cs
@@ -1,10 +1,10 @@
using System.Reflection;
using System.Runtime.InteropServices;
-[assembly: AssemblyTitle("InkCanvasForClass.IACoreHelper")]
+[assembly: AssemblyTitle("InkCanvas.IACoreHelper")]
[assembly: AssemblyDescription("IACore 32-bit ink shape recognition helper process")]
[assembly: AssemblyCompany("ICC CE")]
-[assembly: AssemblyProduct("InkCanvasForClass.IACoreHelper")]
+[assembly: AssemblyProduct("InkCanvas.IACoreHelper")]
[assembly: AssemblyCopyright("Copyright © ICC CE")]
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("1.0.0.0")]