Search code examples
vb.netwinformslistvieweventsmouseleave

Listview MouseLeave event: the Header is not included


I've subscribed to the MouseLeave event of my ListView. The event should be raised when the Mouse Pointer leaves the ListView bounds.

That works, but when the Mouse Pointer enters the ListView's Header and then leaves the ListView bounds, the event is not be raised.

Private Sub LV1_test_MouseLeave(sender As Object, e As EventArgs) Handles LV1_test.MouseLeave
    // Not raised when the Pointer leaves the premises from the top of the ListView
End Sub

What can I do?


Solution

  • The ListView Header is actually a different object, its class name is SysHeader32.
    The Header is shown in Details View, but it's created along with the ListView, so it's there even if you cannot see it (if you have added at least one Column, that is).

    It's not a managed child Control of the ListView: the ListView.Controls collection is usually empty.
    But it's a child control of the SysListView32 native control from which the managed class derives, thus, you can get its handle and read its messages; the WM_MOUSELEAVE message, in this case.

    • We can get its handle using FindWinDowEx or SendMessage (with LVM_GETHEADER), assign the handle to a NativeWindow class, override its WndProc and intercept the messages we need to handle. On WM_MOUSELEAVE, the NativeWindow class raises an event that the parent ListView can subscribes to, raising its own MouseLeave event as a result.

    Since, as described, the Header is a distinct object, the ListView generates a MouseLeave event when the mouse pointer is moved over its Header. We need to override this behavior, so the MouseLeave event is only raised when the mouse Pointer leaves the ListView bounds completely.

    • We can override OnMouseLeave, verify whether the position returned by MousePosition (translated to client measures) falls within the ListView client bounds and let the method raise the MouseLeave event only when it doesn't.

    EDIT:
    Added WM_PARENTNOTIFY message check (for the WM_CREATE event notification) to handle the Header creation at run-time.


    Custom ListView Control:

    Now, if you subscribe to the MouseLeave event of this Custom Control, the event is raised only when the Mouse Pointer leaves the Client Area of the ListView, no matter where the Cursor is located.

    Imports System.ComponentModel
    Imports System.Runtime.InteropServices
    Imports System.Windows.Forms
    
    <DesignerCategory("Code")>
    Class ListViewCustom
        Inherits ListView
    
        Private Const LVM_GETHEADER As Integer = &H1000 + 31
    
        <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
        Friend Shared Function SendMessage(hWnd As IntPtr, uMsg As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr
        End Function
    
        Private sysHeader As SysHeader32 = Nothing
    
        Private Sub AddSysHeaderHandler()
            If DesignMode Then Return
            If sysHeader Is Nothing Then
                Dim sysHeaderHwnd = SendMessage(Me.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero)
                If sysHeaderHwnd <> IntPtr.Zero Then
                    sysHeader = New SysHeader32(sysHeaderHwnd)
                    AddHandler sysHeader.SysHeaderMouseLeave,
                        Sub(s, evt)
                            Me.OnMouseLeave(evt)
                        End Sub
                End If
            End If
        End Sub
    
        Protected Overrides Sub OnHandleCreated(e As EventArgs)
            MyBase.OnHandleCreated(e)
            AddSysHeaderHandler()
        End Sub
    
        Protected Overrides Sub OnMouseLeave(e As EventArgs)
            If Not Me.ClientRectangle.Contains(PointToClient(MousePosition)) Then
                MyBase.OnMouseLeave(e)
            End If
        End Sub
    
        ' Handles the Header creation at run-time
        Protected Overrides Sub WndProc(ByRef m As Message)
            Select Case m.Msg
                Case &H210 'WM_PARENTNOTIFY
                    Dim msg As Integer = m.WParam.ToInt32() And &HFFFF
                    Select Case msg
                        Case &H1 ' WM_CREATE
                            AddSysHeaderHandler()
                    End Select
            End Select
            MyBase.WndProc(m)
        End Sub
    
        Protected Overrides Sub Dispose(disposing As Boolean)
            If (disposing) Then sysHeader?.ReleaseHandle()
            MyBase.Dispose(disposing)
        End Sub
    
        Private Class SysHeader32
            Inherits NativeWindow
    
            Public Event SysHeaderMouseLeave As EventHandler(Of EventArgs)
    
            Public Sub New(handle As IntPtr)
                AssignHandle(handle)
            End Sub
    
            Protected Friend Overridable Sub OnSysHeaderMouseLeave(e As EventArgs)
                RaiseEvent SysHeaderMouseLeave(Me, e)
            End Sub
    
            Protected Overrides Sub WndProc(ByRef m As Message)
                Select Case m.Msg
                    Case &H2A3 'WM_MOUSELEAVE
                        OnSysHeaderMouseLeave(EventArgs.Empty)
                        m.Result = IntPtr.Zero
                        Exit Select
                    Case Else
                        ' NOP: Log other messages, add more cases...
                End Select
                MyBase.WndProc(m)
            End Sub
        End Class
    End Class