diff --git a/Ink Canvas/Windows/MagnifierWindow.cs b/Ink Canvas/Windows/MagnifierWindow.cs index d8e035d3..c3612854 100644 --- a/Ink Canvas/Windows/MagnifierWindow.cs +++ b/Ink Canvas/Windows/MagnifierWindow.cs @@ -2,23 +2,23 @@ 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) 将窗口当前所在区域下方的屏幕内容放大显示。 + /// 使用 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; @@ -38,7 +38,7 @@ namespace Ink_Canvas.Windows 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 SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + [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(); @@ -50,8 +50,45 @@ namespace Ink_Canvas.Windows #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; @@ -69,116 +106,334 @@ namespace Ink_Canvas.Windows } } - 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; - _instance.Show(); - } - - public static void HideInstance() - { - _instance?.Close(); - } - - public static void SetZoom(float zoom) - { - if (_instance != null) _instance.Zoom = zoom; - } - private MagnifierWindow() { Title = "聚焦放大镜"; - Width = 420; - Height = 280; - MinWidth = 160; - MinHeight = 120; WindowStyle = WindowStyle.None; - ResizeMode = ResizeMode.CanResizeWithGrip; + ResizeMode = ResizeMode.NoResize; + AllowsTransparency = true; + Background = Brushes.Transparent; Topmost = true; ShowInTaskbar = false; - AllowsTransparency = false; - Background = new SolidColorBrush(Color.FromRgb(31, 31, 31)); - WindowStartupLocation = WindowStartupLocation.CenterScreen; + WindowState = WindowState.Maximized; - BuildChrome(); - } + _canvas = new System.Windows.Controls.Canvas { Background = Brushes.Transparent }; - private void BuildChrome() - { - var root = new DockPanel { LastChildFill = true }; - - // 标题栏 - var titleBar = new Border + // 半透明遮罩:固定浅色,挖空选择框 + _overlay = new Rectangle { - Height = 28, - Background = new SolidColorBrush(Color.FromArgb(0xEE, 0x2A, 0x2A, 0x2A)), - Cursor = Cursors.SizeAll + Fill = new SolidColorBrush(Color.FromArgb(102, 0, 0, 0)), + IsHitTestVisible = false }; - DockPanel.SetDock(titleBar, Dock.Top); - titleBar.MouseLeftButtonDown += (s, e) => { if (e.ButtonState == MouseButtonState.Pressed) DragMove(); }; + _canvas.Children.Add(_overlay); - var titleGrid = new Grid(); - titleGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - titleGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - - var titleText = new TextBlock + // 选择框 + _selectionBorder = new Border { - Text = "聚焦放大镜", - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(10, 0, 0, 0), - Foreground = Brushes.White, - FontSize = 12 + BorderBrush = Brushes.White, + BorderThickness = new Thickness(1.5), + Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)), // 几乎透明,可命中 + Cursor = Cursors.SizeAll, + SnapsToDevicePixels = true }; - Grid.SetColumn(titleText, 0); - titleGrid.Children.Add(titleText); - - var closeBtn = new Button - { - Content = "✕", - Width = 40, - Height = 28, - Foreground = Brushes.White, - Background = Brushes.Transparent, - BorderThickness = new Thickness(0), - FontSize = 12, - Cursor = Cursors.Arrow - }; - closeBtn.Click += (s, e) => Close(); - Grid.SetColumn(closeBtn, 1); - titleGrid.Children.Add(closeBtn); - - titleBar.Child = titleGrid; - root.Children.Add(titleBar); - - // 放大镜承载区(由 Win32 子窗口填充) + _selectionBorder.MouseLeftButtonDown += SelectionBorder_MouseDown; + _selectionBorder.MouseMove += SelectionBorder_MouseMove; + _selectionBorder.MouseLeftButtonUp += SelectionBorder_MouseUp; _magHost = new MagnifierHost(this); - root.Children.Add(_magHost); + _selectionBorder.Child = _magHost; + _canvas.Children.Add(_selectionBorder); - Content = root; + // 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 MagnifierHost _magHost; + 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 失败"); - } + if (!MagInitialize()) throw new InvalidOperationException("MagInitialize 失败"); _owner._magInitialized = true; MagHwnd = CreateWindowEx( @@ -186,17 +441,13 @@ namespace Ink_Canvas.Windows 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); - } + if (hwnd.Handle != IntPtr.Zero) DestroyWindow(hwnd.Handle); MagHwnd = IntPtr.Zero; } } @@ -205,14 +456,9 @@ namespace Ink_Canvas.Windows { 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; @@ -233,45 +479,34 @@ namespace Ink_Canvas.Windows private void OnTick(object sender, EventArgs e) { - if (_magHwnd == IntPtr.Zero || _hwndSource == null) return; + if (_magHwnd == IntPtr.Zero || _magHost == null) return; + if (_magHost.RenderSize.Width <= 0) return; - // 当前放大区域 = 放大镜承载控件在屏幕上的位置/尺寸 - var hostSize = _magHost.RenderSize; - if (hostSize.Width <= 0 || hostSize.Height <= 0) return; - - // 承载控件相对窗口的位置 + // 选择框在屏幕物理像素坐标 var hostOffset = _magHost.TransformToAncestor(this).Transform(new Point(0, 0)); - var screenTopLeft = PointToScreen(hostOffset); - var screenBottomRight = PointToScreen(new Point(hostOffset.X + hostSize.Width, hostOffset.Y + hostSize.Height)); - - int viewW = (int)(screenBottomRight.X - screenTopLeft.X); - int viewH = (int)(screenBottomRight.Y - screenTopLeft.Y); + 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 srcCx = (int)((screenTopLeft.X + screenBottomRight.X) / 2); - int srcCy = (int)((screenTopLeft.Y + screenBottomRight.Y) / 2); - - var src = new RECT(srcCx - srcW / 2, srcCy - srcH / 2, - srcCx - srcW / 2 + srcW, srcCy - srcH / 2 + srcH); + 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; - } + if (_timer != null) { _timer.Stop(); _timer.Tick -= OnTick; _timer = null; } + if (_magInitialized) { MagUninitialize(); _magInitialized = false; } base.OnClosed(e); } } diff --git a/Ink Canvas/Windows/PPTQuickPanel.xaml.cs b/Ink Canvas/Windows/PPTQuickPanel.xaml.cs index 0d958594..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(); } }