0

我正在研究从一个引脚连接到另一个引脚的线路连接器控件。典型的 WPF 解决方案是在用户开始拖动连接线时使用鼠标捕获。不幸的是,如果用户位于有效的 pin 上,我需要一个鼠标悬停指示器。但是该指示器从未显示,因为当我之前已经捕获鼠标时,目标引脚从未获得鼠标事件。

我写了一个轻量级示例来显示我的问题:

<Window x:Class="WpfApp1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApp1"
    mc:Ignorable="d"
    WindowState="Maximized"
    Title="MainWindow" Height="350" Width="525">
    <Canvas>
        <CheckBox x:Name="EnableMouseCapture" IsChecked="True" Content="Enable Mouse Capture" />
        <Rectangle x:Name="Test" Fill="Blue" Width="40" Height="40" Canvas.Left="200" Canvas.Top="200" />
        <Line x:Name="Line" Stroke="Black" StrokeThickness="1" IsHitTestVisible="False" />
    </Canvas>
</Window>

以及文件背后的代码:

using System;
using System.Windows.Input;
using System.Windows.Media;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            Test.MouseEnter += TestOnMouseEnter;
            Test.MouseLeave += TestOnMouseLeave;

            MouseDown += OnMouseDown;
        }

        private void TestOnMouseEnter(object sender, MouseEventArgs mouseEventArgs)
        {
            Console.WriteLine("(Test) MouseEnter");

            Test.Fill = Brushes.Coral;
        }

        private void TestOnMouseLeave(object sender, MouseEventArgs mouseEventArgs)
        {
            Console.WriteLine("(Test) MouseLeave");

            Test.Fill = Brushes.Blue;
        }

        private void OnMouseMove(object sender, MouseEventArgs mouseEventArgs)
        {
            Console.WriteLine("(Window) MouseMove");

            var pos = mouseEventArgs.GetPosition(this);
            Line.X2 = pos.X;
            Line.Y2 = pos.Y;
        }

        private void OnMouseDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
        {
            Console.WriteLine("(Window) MouseDown");

            MouseUp += OnMouseUp;
            MouseMove += OnMouseMove;

            var pos = mouseButtonEventArgs.GetPosition(this);
            Line.X1 = pos.X;
            Line.Y1 = pos.Y;

            if (EnableMouseCapture.IsChecked == true)
            {
                CaptureMouse();
            }
        }

        private void OnMouseUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
        {
            Console.WriteLine("(Window) MouseUp");

            ReleaseMouseCapture();

            MouseUp -= OnMouseUp;
            MouseMove -= OnMouseMove;
        }
    }
}

如果启用了 Canvas 上的鼠标捕获,则不会调用该TestOnMouseEnter函数TestOnMouseLeave。如果禁用鼠标捕获,则会调用这两个函数。我知道这是 WPF 的典型行为,但有没有人知道我是如何得到通知的,即使另一个控件也有捕获?

4

2 回答 2

2

在评估了一些替代解决方案后,我找到了另一种解决问题的方法。它使用的是 Win32 api。所以有两种可能的方法来解决这类问题。Colins 方式更像 WPF,但您需要手动模拟鼠标事件,这可能很麻烦。不幸的是,第二种解决方案使用非托管 Win32 挂钩,但 WPF 鼠标事件系统可以不受限制地工作。

这是我的示例代码:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;

namespace WpfApp1
{
    public static class NativeMouseHook
    {
        private static readonly Dictionary<MouseMessages, List<MouseEventHandler>> MouseHandlers = new Dictionary<MouseMessages, List<MouseEventHandler>>();
        private static readonly Dictionary<MouseMessages, List<MouseButtonEventHandler>> MouseButtonHandlers = new Dictionary<MouseMessages, List<MouseButtonEventHandler>>();
        private static readonly Dictionary<MouseMessages, List<MouseWheelEventHandler>> MouseWheelHandlers = new Dictionary<MouseMessages, List<MouseWheelEventHandler>>();

        public static void RegisterMouseHandler(MouseMessages mouseMessage, MouseEventHandler handler)
        {
            AddHandler(mouseMessage, MouseHandlers, handler);
            Start();
        }

        public static void UnregisterMouseHandler(MouseMessages mouseMessage, MouseEventHandler handler)
        {
            RemoveHandler(mouseMessage, MouseHandlers, handler);
            CheckAndStop();
        }

        public static void RegisterMouseHandler(MouseMessages mouseMessage, MouseButtonEventHandler handler)
        {
            AddHandler(mouseMessage, MouseButtonHandlers, handler);
            Start();
        }

        public static void UnregisterMouseHandler(MouseMessages mouseMessage, MouseButtonEventHandler handler)
        {
            RemoveHandler(mouseMessage, MouseButtonHandlers, handler);
            CheckAndStop();
        }

        public static void RegisterMouseHandler(MouseMessages mouseMessage, MouseWheelEventHandler handler)
        {
            AddHandler(mouseMessage, MouseWheelHandlers, handler);
            Start();
        }

        public static void UnregisterMouseHandler(MouseMessages mouseMessage, MouseWheelEventHandler handler)
        {
            RemoveHandler(mouseMessage, MouseWheelHandlers, handler);
            CheckAndStop();
        }

        private static void AddHandler<T>(MouseMessages mouseMessage, Dictionary<MouseMessages, List<T>> targetHandlerDictionary, T handler)
        {
            if (!targetHandlerDictionary.ContainsKey(mouseMessage))
            {
                targetHandlerDictionary.Add(mouseMessage, new List<T>());
            }

            targetHandlerDictionary[mouseMessage].Add(handler);
        }

        private static void RemoveHandler<T>(MouseMessages mouseMessage, Dictionary<MouseMessages, List<T>> targetHandlerDictionary, T handler)
        {
            if (targetHandlerDictionary.ContainsKey(mouseMessage))
            {
                var handlerList = targetHandlerDictionary[mouseMessage];
                handlerList.Remove(handler);

                if (handlerList.Count == 0)
                {
                    targetHandlerDictionary.Remove(mouseMessage);
                }
            }
        }

        private static void CheckAndStop()
        {
            if (MouseHandlers.Count == 0 && MouseButtonHandlers.Count == 0 && MouseWheelHandlers.Count == 0)
            {
                Stop();
            }
        }

        private static void Start()
        {
            if (_hookId == IntPtr.Zero)
            {
                _hookId = SetHook(Proc);
            }
        }

        private static void Stop()
        {
            if (_hookId != IntPtr.Zero)
            {
                UnhookWindowsHookEx(_hookId);
                _hookId = IntPtr.Zero;
            }
        }

        private static readonly LowLevelMouseProc Proc = HookCallback;
        private static IntPtr _hookId = IntPtr.Zero;

        private static IntPtr SetHook(LowLevelMouseProc proc)
        {
            using (var curProcess = Process.GetCurrentProcess())
            {
                using (var curModule = curProcess.MainModule)
                {
                    return SetWindowsHookEx(WH_MOUSE_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
                }
            }
        }

        private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0)
            {
                var hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));

                switch ((MouseMessages)wParam)
                {
                    case MouseMessages.WM_LBUTTONDOWN:
                        CallHandler(MouseMessages.WM_LBUTTONDOWN, MouseButtonHandlers, new MouseButtonEventArgs(Mouse.PrimaryDevice, (int)hookStruct.time, MouseButton.Left));
                        break;
                    case MouseMessages.WM_LBUTTONUP:
                        CallHandler(MouseMessages.WM_LBUTTONUP, MouseButtonHandlers, new MouseButtonEventArgs(Mouse.PrimaryDevice, (int)hookStruct.time, MouseButton.Left));
                        break;
                    case MouseMessages.WM_MOUSEMOVE:
                        CallHandler(MouseMessages.WM_MOUSEMOVE, MouseHandlers, new MouseEventArgs(Mouse.PrimaryDevice, (int)hookStruct.time));
                        break;
                    case MouseMessages.WM_MOUSEWHEEL:
                        CallHandler(MouseMessages.WM_MOUSEWHEEL, MouseWheelHandlers, new MouseWheelEventArgs(Mouse.PrimaryDevice, (int)hookStruct.time, 0));
                        break;
                    case MouseMessages.WM_RBUTTONDOWN:
                        CallHandler(MouseMessages.WM_LBUTTONDOWN, MouseButtonHandlers, new MouseButtonEventArgs(Mouse.PrimaryDevice, (int)hookStruct.time, MouseButton.Right));
                        break;
                    case MouseMessages.WM_RBUTTONUP:
                        CallHandler(MouseMessages.WM_LBUTTONUP, MouseButtonHandlers, new MouseButtonEventArgs(Mouse.PrimaryDevice, (int)hookStruct.time, MouseButton.Right));
                        break;
                }
            }

            return CallNextHookEx(_hookId, nCode, wParam, lParam);
        }

        private static void CallHandler<T>(MouseMessages mouseMessage, Dictionary<MouseMessages, List<T>> targetHandlerDictionary, EventArgs args)
        {
            if (targetHandlerDictionary.ContainsKey(mouseMessage))
            {
                var handlerList = targetHandlerDictionary[mouseMessage];
                foreach (var handler in handlerList.Cast<Delegate>())
                {
                    handler.DynamicInvoke(null, args);
                }
            }
        }

        private const int WH_MOUSE_LL = 14;

        public enum MouseMessages
        {
            WM_LBUTTONDOWN = 0x0201,
            WM_LBUTTONUP = 0x0202,
            WM_MOUSEMOVE = 0x0200,
            WM_MOUSEWHEEL = 0x020A,
            WM_RBUTTONDOWN = 0x0204,
            WM_RBUTTONUP = 0x0205
        }

        [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")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool GetCursorPos(ref Win32Point pt);

        [StructLayout(LayoutKind.Sequential)]
        internal struct Win32Point
        {
            public Int32 X;
            public Int32 Y;
        };
        public static Point GetMousePosition()
        {
            Win32Point w32Mouse = new Win32Point();
            GetCursorPos(ref w32Mouse);
            return new Point(w32Mouse.X, w32Mouse.Y);
        }

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

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);
    }
}

XAML:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        WindowState="Maximized"
        Title="MainWindow" Height="350" Width="525">
    <Canvas>
        <Rectangle x:Name="Test" Fill="Blue" Width="40" Height="40" Canvas.Left="200" Canvas.Top="200" />
        <Line x:Name="Line" Stroke="Black" StrokeThickness="1" IsHitTestVisible="False" />
    </Canvas>
</Window>

后面的代码:

using System;
using System.Windows.Input;
using System.Windows.Media;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            Test.MouseEnter += TestOnMouseEnter;
            Test.MouseLeave += TestOnMouseLeave;

            MouseDown += OnMouseDown;
        }

        private void TestOnMouseEnter(object sender, MouseEventArgs mouseEventArgs)
        {
            Console.WriteLine("(Test) MouseEnter");

            Test.Fill = Brushes.Coral;
        }

        private void TestOnMouseLeave(object sender, MouseEventArgs mouseEventArgs)
        {
            Console.WriteLine("(Test) MouseLeave");

            Test.Fill = Brushes.Blue;
        }

        private void OnMouseMove(object sender, MouseEventArgs mouseEventArgs)
        {
            Console.WriteLine("(Window) MouseMove");

            var pos = NativeMouseHook.GetMousePosition();
            Line.X2 = pos.X;
            Line.Y2 = pos.Y;
        }

        private void OnMouseDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
        {
            Console.WriteLine("(Window) MouseDown");

            NativeMouseHook.RegisterMouseHandler(NativeMouseHook.MouseMessages.WM_MOUSEMOVE, (MouseEventHandler)OnMouseMove);
            NativeMouseHook.RegisterMouseHandler(NativeMouseHook.MouseMessages.WM_LBUTTONUP, OnMouseUp);

            var pos = mouseButtonEventArgs.GetPosition(this);
            Line.X1 = pos.X;
            Line.Y1 = pos.Y;
        }

        private void OnMouseUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
        {
            Console.WriteLine("(Window) MouseUp");

            NativeMouseHook.UnregisterMouseHandler(NativeMouseHook.MouseMessages.WM_MOUSEMOVE, (MouseEventHandler)OnMouseMove);
            NativeMouseHook.UnregisterMouseHandler(NativeMouseHook.MouseMessages.WM_LBUTTONUP, OnMouseUp);
        }
    }
}
于 2017-10-10T11:49:35.863 回答
1

好吧,我理解它的方式是 MouseCapture 可以使代码整洁,所以你为什么不直接不使用鼠标捕获。

https://msdn.microsoft.com/en-us/library/ms771301.aspx

来自 msdn 的鼠标捕获:当一个对象捕获鼠标时,所有与鼠标相关的事件都被视为具有鼠标捕获的对象执行该事件,即使鼠标指针位于另一个对象上也是如此。

WPF中的“捕获鼠标”是什么意思?

捕获鼠标对于拖动很有用,因为只有捕获控件在释放之前接收鼠标事件。所有的拖动代码都可以存在于一个控件中,而不是分散在多个控件中。

如果这妨碍了您的应用程序按照您想要的方式运行,那么为什么不直接避免使用它呢?听起来鼠标捕获的目的会破坏您想要实现的目标。

如果您想看一下,我也发现了与此类似的问题:

如果另一个对象有鼠标捕获,如何为一个对象触发 MouseEnter?

编辑:我在这里确实有一个例子,但它不能正常工作,基本上你需要手动点击测试,然后手动触发鼠标进入和鼠标离开。

于 2017-10-06T12:16:14.920 回答