How to get MouseEvents for a control even if another control already captured the mouse

1.3k Views Asked by At

I'm working on a line connector control which connects from one pin to another. The typical WPF solution is to use mouse capturing when the user starts to drag the connection line. Unfortunately I need a mouse over indicator if the user is over a valid pin. But the indicator is never shown because the target pin never gets the mouse events when I already captured the mouse before.

I wrote a lighweight sample to show my problem:

<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>

And the code behind file:

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;
        }
    }
}

If mouse capturing on the Canvas is enabled, the function TestOnMouseEnter and TestOnMouseLeave are not called. If mouse capturing is disabled these two functions are getting called. I know that this is a WPF typical behavior, but did anyone maybe know how I get informed even another control has the capture?

2

There are 2 best solutions below

1
On

Well, the way I understand it is that MouseCapture is there to make the code neat, so why don't you just not use mouse capturing.

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

Mouse Capturing from msdn: When an object captures the mouse, all mouse related events are treated as if the object with mouse capture perform the event, even if the mouse pointer is over another object.

What does it mean to "Capture the mouse" in WPF?

Capturing the mouse is useful for dragging because only the capturing control receives the mouse events until released. All the dragging code can exist in the one control, rather than being spread over multiple controls.

If this is getting in the way of making your app behave the way you want then why not just avoid using it? Sounds like the purpose of mouse capturing defeats what you are trying to achieve.

I found a similar question to this too, if you wanna take a look:

How to fire MouseEnter for one object if another object has mousecapture?

Edit: I did have an example in here but it didn't work properly, basically you need to manually hit test and then trigger your mouse enter and mouse leave manually.

0
On

After evaluating some alternative solutions I find another way to solve the problem. It is using the Win32 api. So there are two possible ways to solve this kind of problem. Colins way is more WPF like but you need to emulate the mouse events manually which can be a hassle. The second solution unfortunately uses unmanaged Win32 hooks but the WPF mouse event system works without limitations.

Here's my sample code:

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>

Code behind:

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);
        }
    }
}