2
votes

I'm trying to create an Adorner that will follow the mouse during a drag & drop operation. It needs to do so even when the mouse is dragging over an element that has AllowDrop set to False.

Problems:

  • Normal mouse events (e.g. MouseMove) don't fire during Drag & Drop
  • Drag & Drop events (e.g. DragOver, GiveFeedback) only fire on valid drop targets (elements with AllowDrop set to true)

I need to track:

  • Where the mouse is
  • When the mouse moves

Without any of the above events, there's no easy way I can find to do this.

I've solved #1 by using the native GetCursorPos method. This can reliably get the position of the mouse whenever I want.

My remaining problem is getting notified when the mouse moves. Is there any way I can get mouse movement notifications during a drag & drop operation, even when dragging over elements with AllowDrop set to false?

Note: I don't want to use a timer and just continuously refresh the position (if I can help it), I'd much rather use actual mouse input.

1

1 Answers

1
votes

Wow, I didn't expect this one to be so tough.

My first attempt was to try and bypass WPF and go straight to the native window message pump. But it turns out that even the standard WM_MOUSEMOVE message doesn't come through during a drag and drop operation. Digging deeper (through the ole2.dll source code), I discovered that a separate, invisible window gets created during the drag which eats up all the normal messages and instead interfaces with the drop targets directly (which is probably why the normal WPF mouse events don't fire in the first place).

I was worried that might be the end of it, until I discovered hooks, which let you get a hold of messages before they're consumed by the active window. Using the WH_MOUSE hook I was able to intercept the WM_MOUSEMOVE message and place my Adorner accordingly.

I'm not going to post all the code for the Adorner here, but I will give you the P/Invoke code I used to track the mouse:

Module NativeMethods
    <DllImport("user32.dll")>
    Public Function SetWindowsHookEx(ByVal idHook As HookType, ByVal lpfn As [Delegate], ByVal hInstance As IntPtr, ByVal threadId As Integer) As IntPtr
    End Function

    <DllImport("user32.dll")>
    Public Function CallNextHookEx(ByVal hhk As IntPtr, ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
    End Function

    <DllImport("user32.dll")>
    Public Function UnhookWindowsHookEx(ByVal hhk As IntPtr) As Boolean
    End Function

    <StructLayout(LayoutKind.Sequential)>
    Friend Structure Win32Point
        Public X As Int32
        Public Y As Int32

        Public Shared Widening Operator CType(Point As Win32Point) As Drawing.Point
            Return New Drawing.Point(Point.X, Point.Y)
        End Operator

        Public Shared Widening Operator CType(Point As Win32Point) As Windows.Point
            Return New Windows.Point(Point.X, Point.Y)
        End Operator
    End Structure

    Const WM_MOUSEMOVE As Integer = 512

    Enum HookType As Integer
        WH_JOURNALRECORD = 0
        WH_JOURNALPLAYBACK = 1
        WH_KEYBOARD = 2
        WH_GETMESSAGE = 3
        WH_CALLWNDPROC = 4
        WH_CBT = 5
        WH_SYSMSGFILTER = 6
        WH_MOUSE = 7
        WH_HARDWARE = 8
        WH_DEBUG = 9
        WH_SHELL = 10
        WH_FOREGROUNDIDLE = 11
        WH_CALLWNDPROCRET = 12
        WH_KEYBOARD_LL = 13
        WH_MOUSE_LL = 14
    End Enum

    Public Delegate Function HookProc(ByVal code As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer

    <StructLayout(LayoutKind.Sequential)>
    Structure MOUSEHOOKSTRUCT
        Public pt As Win32Point
        Public hwnd As IntPtr
        Public wHitTestCode As UInteger
        Public dwExtraInfo As IntPtr
    End Structure
End Module

Class MouseTracker
    Private HookHandle As IntPtr
    Private HookDelegate As New HookProc(AddressOf NativeHook)

    Private Sub AddNativeHook()
#Disable Warning BC40000 ' Type or member is obsolete
        HookHandle = SetWindowsHookEx(HookType.WH_MOUSE, HookDelegate, IntPtr.Zero, AppDomain.GetCurrentThreadId())
#Enable Warning BC40000 ' Type or member is obsolete
    End Sub

    Private Sub RemoveNativeHook()
        UnhookWindowsHookEx(HookHandle)
    End Sub

    Private Function NativeHook(code As Integer, wParam As IntPtr, lParam As IntPtr) As Integer
        If code >= 0 Then
            If wParam = WM_MOUSEMOVE Then
                Dim data = Marshal.PtrToStructure(Of MOUSEHOOKSTRUCT)(lParam)
                'From here you can use Visual.PointFromScreen(data.pt) to get the coordinates of the mouse relative to any WPF Visual.
                'Then you do whatever you want with that!
            End If
        End If

        Return CallNextHookEx(IntPtr.Zero, code, wParam, lParam)
    End Function
End Class

If you need more information, I heavily referenced:
pinvoke.net: https://pinvoke.net/default.aspx/user32/SetWindowsHookEx.html
Microsoft docs on hooks: https://docs.microsoft.com/en-us/windows/win32/winmsg/about-hooks