Как получить MouseEvents для элемента управления, даже если другой элемент управления уже захватил мышь
Я работаю над линейным разъемом управления, который подключается от одного контакта к другому. Типичное решение WPF - использовать захват мыши, когда пользователь начинает перетаскивать линию соединения. К сожалению, мне нужен указатель мыши, если пользователь находится над правильным пин-кодом. Но индикатор никогда не отображается, потому что целевой пин никогда не получает события мыши, когда я уже захватил мышь раньше.
Я написал облегченный образец, чтобы показать мою проблему:
<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;
}
}
}
Если захват мыши на холсте включен, функция TestOnMouseEnter
а также TestOnMouseLeave
не называются. Если захват мыши отключен, эти две функции вызывают. Я знаю, что это типичное поведение WPF, но кто-нибудь, возможно, знал, как я узнаю, что даже другой элемент управления захватил?
2 ответа
После оценки некоторых альтернативных решений я нахожу другой способ решения проблемы. Он использует Win32 API. Таким образом, есть два возможных способа решения этой проблемы. Путь Колинса больше похож на 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);
}
}
}
Ну, насколько я понимаю, это MouseCapture для того, чтобы сделать код аккуратным, так почему бы просто не использовать захват мыши.
https://msdn.microsoft.com/en-us/library/ms771301.aspx
Захват мыши из msdn: когда объект захватывает мышь, все связанные с мышью события обрабатываются так, как если бы объект с захватом мыши выполнял событие, даже если указатель мыши находится над другим объектом.
Что значит "Захватить мышь" в WPF?
Захват мыши полезен для перетаскивания, потому что только элемент управления захватом получает события мыши, пока не отпущен. Весь код перетаскивания может существовать в одном элементе управления, а не распределяться по нескольким элементам управления.
Если это мешает вашему приложению вести себя так, как вы хотите, то почему бы просто не избегать его использования? Похоже, цель захвата мыши побеждает то, что вы пытаетесь достичь.
Я нашел похожий вопрос к этому, если вы хотите взглянуть:
Как запустить MouseEnter для одного объекта, если другой объект имеет mousecapture?
Изменить: у меня был пример здесь, но он не работал должным образом, в основном вам нужно вручную нажать на тест, а затем вызвать ввод мыши и выход мыши вручную.