WPF CaptureMouse 不捕获主 window 之外的鼠标事件

WPF CaptureMouse does not capture mouse events outside of the main window

我创建了一个简单的 WPF 应用程序,它试图捕获鼠标,但在鼠标离开 window 后它停止获取 mousemove 事件。奇怪的是,我确实在 window 之外收到了一个 mouseup 事件。

我已经尝试了几种mousecapture 变体,但没有任何效果。我也尝试过观察 MouseLost 事件,但当鼠标超出 window 时我没有看到它。当我松开鼠标按钮时它就会看到它。

这是我的主窗口 class。只要鼠标在 window 中,我就会收到鼠标移动事件,但如果我单击并将鼠标拖出 window,我将停止收到移动事件。

 public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            MouseDown += MainWindow_MouseDown;
            MouseUp += MainWindow_MouseUp;
            MouseMove += MainWindow_MouseMove;
            LostMouseCapture += MainWindow_LostMouseCapture;
        }

        private void MainWindow_LostMouseCapture(object sender, MouseEventArgs e)
        {
            Debug.WriteLine("Lost Mouse");
        }

        private void MainWindow_MouseMove(object sender, MouseEventArgs e)
        {
            Debug.WriteLine("P: " + Mouse.GetPosition(this));
        }

        private void MainWindow_MouseUp(object sender, MouseButtonEventArgs e)
        {
            Debug.WriteLine("Releasing");
            ReleaseMouseCapture();
        }

        private void MainWindow_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Debug.WriteLine("Capturing");
            CaptureMouse();
            // This does not work either: Mouse.Capture(this, CaptureMode.SubTree);
        }

    }

我希望看到所有的 mousemove 事件,以便我可以拖动 window,但我只看到外面的 mouseup 事件,只有当光标在 [=21= 内时才会发生 mousemoves ].

经过一番研究,我对这个问题有了答案。当鼠标离开应用程序时,WPF 应用程序看不到鼠标 window,因此如果您想要一些自定义拖动行为,则有必要使用 interrop 全局捕获鼠标。我创建了以下 class 以启用 DPI 感知 window 为任何 WPF window 跨多个显示器拖动 window:

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Media;

[assembly: DisableDpiAwareness]

namespace Talisman
{

    // --------------------------------------------------------------------------
    /// <summary>
    /// Enables dragging of a WPF window in a way that is per-monitor DPI sensitive.
    /// 
    /// HOW TO USE
    /// Add a DraggingLogic member variable and put this code in your window constructor:
    ///     _draggingLogic = new DraggingLogic(this);
    ///     
    /// If you want to do special things when the window moves or when it is clicked:
    ///     _draggingLogic.OnPositionChanged += (xm, ym) => {/* whatever you want here */};
    ///     _draggingLogic.OnClick += () => {/* whatever you want here */};
    ///
    /// </summary>
    // --------------------------------------------------------------------------
    public class DraggingLogic
    {
        public event Action<double, double> OnPositionChanged;
        public event Action OnClick;

        /// <summary>
        /// Factor to convert Horizontal screen coordinates
        /// </summary>
        public double DpiCorrectionX { get; set; }
        /// <summary>
        /// Factor to convertVertical  screen coordinates
        /// </summary>
        public double DpiCorrectionY { get; set; }

        public double WpfDpiX { get; set; }
        public double WpfDpiY { get; set; }

        #region INTERROP - Mouse interaction

        private static int _mouseHookHandle;
        private delegate int HookProc(int nCode, int wParam, IntPtr lParam);
        private static HookProc _mouseDelegate;

        private const int WH_MOUSE_LL = 14;
        private const int WM_LBUTTONUP = 0x0202;
        private const int WM_MOUSEMOVE = 0x0200;

        [StructLayout(LayoutKind.Sequential)]
        private struct POINT
        {
            public int x;
            public int y;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct MSLLHOOKSTRUCT
        {
            public POINT pt;
            public uint mouseData;
            public uint flags;
            public uint time;
            public IntPtr dwExtraInfo;
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto,
        CallingConvention = CallingConvention.StdCall, SetLastError = true)]
        private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto,
           CallingConvention = CallingConvention.StdCall, SetLastError = true)]
        private static extern int UnhookWindowsHookEx(int idHook);

        [DllImport("user32.dll", CharSet = CharSet.Auto,
             CallingConvention = CallingConvention.StdCall)]
        private static extern int CallNextHookEx(int idHook, int nCode, int wParam, IntPtr lParam);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetModuleHandle(string name);

        #endregion

        #region INTERROP - DPI

        [DllImport("User32.dll")]
        private static extern IntPtr MonitorFromPoint([In]System.Drawing.Point pt, [In]uint dwFlags);

        [DllImport("Shcore.dll")]
        private static extern IntPtr GetDpiForMonitor([In]IntPtr hmonitor, [In]DpiType dpiType, [Out]out uint dpiX, [Out]out uint dpiY);
        [DllImport("Shcore.dll")]
        private static extern IntPtr SetProcessDpiAwareness([In]DpiAwareness dpiAwareness); 

        public enum DpiType
        {
            Effective = 0,
            Angular = 1,
            Raw = 2,
        }

        public enum DpiAwareness
        {
            Unaware = 0,
            System = 1,
            PerMonitor = 2,
        }

        #endregion

        Screen _currentScreen;
        Window _dragMe;
        bool _dragging = false;
        double _dragDelta = 0;
        Point _lastMousePosition;
        Point _mouseStickyPosition;

        // --------------------------------------------------------------------------
        /// <summary>
        /// Get resource text using a loose naming scheme
        /// </summary>
        // --------------------------------------------------------------------------
        public DraggingLogic(Window dragme)
        {
            var result = SetProcessDpiAwareness(DpiAwareness.PerMonitor);
            dragme.MouseDown += HandleMouseDown;
            dragme.MouseMove += HandleMouseMove;
            dragme.MouseUp += HandleMouseUp;
            dragme.Loaded += Dragme_Loaded;
            _dragMe = dragme;
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// Dragme_Loaded - can't find DPI until the window is loaded
        /// </summary>
        // --------------------------------------------------------------------------
        private void Dragme_Loaded(object sender, RoutedEventArgs e)
        {
            var source = PresentationSource.FromVisual(_dragMe);
            WpfDpiX = 96.0 * source.CompositionTarget.TransformToDevice.M11;
            WpfDpiY = 96.0 * source.CompositionTarget.TransformToDevice.M22;
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// Figure out scaling for the DPI on a certain monitor
        /// </summary>
        // --------------------------------------------------------------------------
        public void CalculateDpiScaleFactors(Screen screen, DpiType dpiType)
        {
            var point = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
            var monitor = MonitorFromPoint(point, 2/*MONITOR_DEFAULTTONEAREST*/);
            Debug.WriteLine($"Monitor: {monitor}");
            var result = GetDpiForMonitor(monitor, dpiType, out var monitorDpiX, out var monitorDpiY);
            if(result != IntPtr.Zero)
            {
                monitorDpiX = monitorDpiY = 96;
            }
            DpiCorrectionX = 96.0 / monitorDpiX;
            DpiCorrectionY = 96.0 / monitorDpiY;
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// Mouse Down
        /// </summary>
        // --------------------------------------------------------------------------

        private void HandleMouseDown(object sender, MouseButtonEventArgs e)
        {
            var window = sender as Window;
            if (e.LeftButton == MouseButtonState.Pressed)
            {

                _dragging = true;
                _dragDelta = 0;
                _mouseStickyPosition = Mouse.GetPosition(window);
                _lastMousePosition = window.PointToScreen(Mouse.GetPosition(window));
                _currentScreen = GetScreenFromPoint(_lastMousePosition);
                CalculateDpiScaleFactors(_currentScreen, DpiType.Effective);

                CaptureGlobalMouse();
                e.Handled = true;
            }
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// Mouse Move
        /// </summary>
        // --------------------------------------------------------------------------
        private void HandleMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if (_dragging)
            {
                e.Handled = true;
            }
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// HandleGlobalMouseMove
        /// </summary>
        // --------------------------------------------------------------------------
        private void HandleGlobalMouseMove(Point mouseLocation)
        {
            var newPosition = mouseLocation; // This arrives without DPI correction
            var screen = GetScreenFromPoint(newPosition);

            // We need to do some fix up when we drag to another screen because
            // the DPI on the other screen could be different
            if(screen != null &&  screen.DeviceName != _currentScreen.DeviceName)
            {
                CalculateDpiScaleFactors(screen, DpiType.Effective);
                _lastMousePosition = newPosition;

                // Move the window to match the mouse position
                _dragMe.Left = (newPosition.X - _mouseStickyPosition.X)* DpiCorrectionX;
                _dragMe.Top = (newPosition.Y - _mouseStickyPosition.Y)* DpiCorrectionY;
                _currentScreen = screen;
            }

            var xMove = (newPosition.X - _lastMousePosition.X)* DpiCorrectionX;
            var yMove = (newPosition.Y - _lastMousePosition.Y)* DpiCorrectionY;
            _dragMe.Left += xMove;
            _dragMe.Top += yMove;
            _dragDelta += (_lastMousePosition - newPosition).Length;
            _lastMousePosition = newPosition;
            OnPositionChanged?.Invoke(xMove, yMove);
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// GetScreenFromPoint - return the screen from a raw point (presumably mouse coordinate)
        /// </summary>
        // --------------------------------------------------------------------------
        public Screen GetScreenFromPoint(Point point)
        {
            foreach (Screen screen in Screen.AllScreens)
            {
                if (screen.ContainsPoint(point.X, point.Y)) return screen;
            }
            return null;
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// Mouse Up
        /// </summary>
        // --------------------------------------------------------------------------
        private void HandleMouseUp(object sender, MouseButtonEventArgs e)
        {
            if (_dragging)
            {
                var window = sender as Window;
                // if the user didn't actually drag, then we want to treat this as a click
                if (_dragDelta < 3)
                {
                    OnClick?.Invoke();
                }
                _dragging = false;
                ReleaseGlobalMouse();
                if(e != null) e.Handled = true;
            }
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// MouseHookProc- allows us to handle global mouse events
        /// </summary>
        // --------------------------------------------------------------------------
        private int MouseHookProc(int nCode, int wParam, IntPtr lParam)
        {
            if (nCode >= 0)
            {
                switch (wParam)
                {
                    case WM_LBUTTONUP: HandleMouseUp(this, null); break;
                    case WM_MOUSEMOVE:
                        {
                            var mouseHookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
                            HandleGlobalMouseMove(new Point(mouseHookStruct.pt.x, mouseHookStruct.pt.y));
                            break;
                        }
                }
            }
            return CallNextHookEx(_mouseHookHandle, nCode, wParam, lParam);
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// CaptureGlobalMouse
        /// </summary>
        // --------------------------------------------------------------------------
        private void CaptureGlobalMouse()
        {
            if (_mouseHookHandle == 0)
            {
                _mouseDelegate = MouseHookProc;
                _mouseHookHandle = SetWindowsHookEx(WH_MOUSE_LL,
                    _mouseDelegate,
                    GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName),
                    0);
                if (_mouseHookHandle == 0)
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }
            }
        }

        // --------------------------------------------------------------------------
        /// <summary>
        /// ReleaseGlobalMouse
        /// </summary>
        // --------------------------------------------------------------------------
        private void ReleaseGlobalMouse()
        {
            if (_mouseHookHandle != 0)
            {
                int result = UnhookWindowsHookEx(_mouseHookHandle);
                _mouseHookHandle = 0;
                _mouseDelegate = null;
                if (result == 0)
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }
            }
        }
    }
}

您必须在对象本身上使用鼠标捕获,因此在 MouseDown 中您需要 ((IInputElement)sender).CaptureMouse() 并且在 MouseUp 中 ((IInputElement)sender).ReleaseMouseCapture().

或者,您也可以使用 MainWindow.CaptureMouse()

private void MainWindow_MouseUp(object sender, MouseButtonEventArgs e)
{
    Debug.WriteLine("Releasing");
    ((IInputElement)sender).ReleaseMouseCapture()
}

private void MainWindow_MouseDown(object sender, MouseButtonEventArgs e)
{
    Debug.WriteLine("Capturing");
    ((IInputElement)sender).CaptureMouse()
    // This does not work either: Mouse.Capture(this, CaptureMode.SubTree);
}