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"> - + + + + + + + - + - - + - + + - - - + + + + + + + + + + + - + - + - - - + - + @@ -73,72 +131,93 @@ - - - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + - + + + + + + + + + + + + + + +