Results 1 to 5 of 5

Thread: [RESOLVED] Listview columnheader No wndproc on mouse move :(

  1. #1

    Thread Starter
    Hyperactive Member imadthemad's Avatar
    Join Date
    May 2005
    Posts
    344

    Resolved [RESOLVED] Listview columnheader No wndproc on mouse move :(

    Hi

    I've made a custom listview control with much cooler visual styles but i'm having a problem with capturing mousemove over the listview's columnheader

    I've found that this has been an issue in the past, and found this thread http://www.vbforums.com/showthread.p...-gt-mouse-move but where the author says in the end he used wndproc to capture mousemove over the columnheader, i've found wndproc wasn't firing when i mousemove over the columnheader

    The columnheaders are ownerdrawn, if i use the default draw for columnheaders there seems to be wndproc messages firing when i move over from one column header to another but im unable to put my own graphics when i put default draw.

    The only workaround i've been able to do is fire a background thread that loops detection of mouse position when mouse enters over the header area of the list view and kill it when the mouse leaves the header area, but as you can imagine that's quite a crude method of solving this problem.

    Any ideas or help is appreciated. Thanks!
    Basketball-NBA Dream status - quit
    Programming-Next Bill Gates Dream status - quit
    Becoming a doctor dream - LIVING IT: Med 2\4

  2. #2
    VB For Fun Edgemeal's Avatar
    Join Date
    Sep 2006
    Location
    WindowFromPoint
    Posts
    4,255

    Re: Listview columnheader No wndproc on mouse move :(

    What if you set up a NativeWindow to watch the LV header messages?

    Rough idea, tested from a LV on a form,
    Dim hc As New HeaderControl(Me.ListViewOwnerDraw1)

    Code:
    Imports System.Runtime.InteropServices
    
    Friend Class HeaderControl
        Inherits NativeWindow
    
        <DllImport("user32.dll", EntryPoint:="SendMessage")> _
        Private Shared Function SendMessage(hwnd As IntPtr, wMsg As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr
        End Function
    
        Private Const LVM_GETHEADER As UInt32 = &H101F
        Private Const WM_MOUSEMOVE As Int32 = &H200
    
        Public Sub New(LV As ListView)
            'Get handle to LV header
            Dim hWnd_LV_Header As IntPtr = SendMessage(LV.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero)
            If hWnd_LV_Header <> IntPtr.Zero Then
                Me.AssignHandle(hWnd_LV_Header)
                AddHandler LV.HandleDestroyed, AddressOf LV_destroy ' To release handle when LV destroyed?
            End If
        End Sub
    
        Private Sub LV_destroy(sender As Object, e As EventArgs)
            Me.ReleaseHandle()
        End Sub
    
        ' subclass LV header...
        Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
            Select Case m.Msg
                Case WM_MOUSEMOVE ' https://msdn.microsoft.com/en-us/library/windows/desktop/ms645616%28v=vs.85%29.aspx
                    ' show mouse location over LV column header
                    Debug.WriteLine("x={0}, y={1}", m.LParam.ToInt32 And &HFFFF&, m.LParam.ToInt32 \ &H10000 And &HFFFF&) ' http://support.microsoft.com/kb/112651
            End Select
            MyBase.WndProc(m)
        End Sub
    
    End Class
    Last edited by Edgemeal; Feb 1st, 2015 at 09:34 AM.

  3. #3

    Thread Starter
    Hyperactive Member imadthemad's Avatar
    Join Date
    May 2005
    Posts
    344

    Re: Listview columnheader No wndproc on mouse move :(

    Okay guys so after ALL this hard work, I took a breather and properly looked at it, and i found out the problem ISN'T really due to the header mousemove capabilities.

    Yes, not having mousemove capacity when the mouse moves over the headers is a very sucky limitation (because you can't tell the headers when to draw based on where your mouse is), it turns out despite this limitation drawevents ARE firing properly when it's time for a new header to be "hot".

    It's funny coz the research i did to find a solution kept leading to more complicated answers like subclassing and even completely redrawing the headerbar on a new control (which takes SO much effort)

    Quite simply, here is the solution so read closely:

    When moving your mouse through the header columns, the "draw" trigger ONLY gets triggered when the cursor has changed to a split cursor, which could be outside of the bounds of the column you are gonna resize.
    NO DRAW EVENT GETS FIRED AGAIN when the mouse gets inside the bounds the bounds of the column you are gonna resize IF it came from a split cursor.
    This is why what many people before me have called "buggy" header drawing, isn't randomnly buggy. It's only buggy when ur mouse is within one columnheader's bounds and moves to the columnheader's bounds of the column header before it.
    So as it turns out it is not the control that is buggy or sloppy, it is the code determining which column is highlighted that is actually sloppy (because all people who have had this issue before me where using code that says the highlighted column is the one that contains the mouse in its box, when really the more proper and obsessive-compulsive perfectionist thing to do is that if the cursor has become a resize cursor, meant to resize a column, the highlighted column should be the one that the user would resize if he were to click and drag, even if the mouse\cursor is still outside the bounds of that column header's box!).

    Therefore, find below the CORRECT and the INCORRECT way to determine whether a header is highlighted:

    INCORRECT:
    Code:
     
    Private Sub Listview_DrawColumnHeader(ByVal sender As Object, ByVal e As System.Windows.Forms.DrawListViewColumnHeaderEventArgs) Handles Me.DrawColumnHeader
            Dim listview1 As ListView = sender
            Dim highlighted As Boolean
            Dim mouseCoordinates As Drawing.Point = PointToClient(MousePosition)
            If e.Bounds.Contains(mouseCoordinates) Then highlighted = True 'WRONG. If the header fired mousemove this code will work but it would actually be imperfect if you are OCD, because you would be able to increase the width of a columnheader without having that column header be highlighted.
        End Sub
    Correct code example:

    Code:
        Private Sub Listview_DrawColumnHeader(ByVal sender As Object, ByVal e As System.Windows.Forms.DrawListViewColumnHeaderEventArgs) Handles Me.DrawColumnHeader
            Dim listview1 As ListView = sender
            Dim highlighted As Boolean
            Dim mouseCoordinates As Drawing.Point = PointToClient(MousePosition)
            If e.Bounds.Contains(mouseCoordinates) Then highlighted = True
            Dim rectR As Rectangle = e.Bounds
            rectR.Y = e.Bounds.Y
            rectR.Height = e.Bounds.Height
            If e.ColumnIndex = 0 Then rectR.Width += 8 'i found that the cursor changes to a split cursor 8 units after the bounds of the header
            If e.ColumnIndex <> 0 And e.ColumnIndex < listview1.Columns.Count - 1 Then rectR.X += 8 ' neccessary to increase X and not width of the bounds because u don't want overlap between 2 header bounds.
            If e.ColumnIndex = listview1.Columns.Count - 1 Then rectR.X += 8 : rectR.Width -= 2 'for the last column of your listview, if it is close to the listview's border, the cursor changes sooner.
        End Sub
    Note that as I was making this post I found out that this code is NOT perfect because the cursor doesn't always change at exactly 8 units from the header's bounds
    e.g. if your header's width is small enough, the cursor change occurs only at 6 or 4 units away from the header's bounds.

    I'm going to look up WndProc messages to tell me when the cursor is changed to a resize one, then i'll adjust the code to highlight the column that precedes the split cursor.
    And maybe i'll repost it on this thread.

    If you miss when the cursor becomes a split cursor, that's when you have problems because the drawcolumns event doesnt fire again.

    I'm gonna mark this thread as resolved and give myself a pat on the back for discovering this solution (all this time me and others have blamed the listview control for inherently not firing drawcolumns correctly when the mouse is moved over headers. Turns out the draw event fires just fine, it's about knowing which column is rightfully highlighted when it fires!)

    One extra useful tip:
    If your listview is scrolled horizontally, the mouse coordinates can not be related to the bounds in the drawcolumneventargs (which are translated by as much as the listview is scrolled).
    The workaround I use for this (since my listview is never empty) is the following
    Code:
            Dim rectR As Rectangle = listview1.Items(0).SubItems(e.ColumnIndex).Bounds
            rectR.Y = e.Bounds.Y
            rectR.Height = e.Bounds.Height
    Last edited by imadthemad; Feb 1st, 2015 at 05:31 PM.
    Basketball-NBA Dream status - quit
    Programming-Next Bill Gates Dream status - quit
    Becoming a doctor dream - LIVING IT: Med 2\4

  4. #4

    Thread Starter
    Hyperactive Member imadthemad's Avatar
    Join Date
    May 2005
    Posts
    344

    Re: [RESOLVED] Listview columnheader No wndproc on mouse move :(

    My final solution without having to override wndproc is posted below.

    (so actually credit goes to the thread i mentioned in my first post, HDM_Hittest is the best way to do it. But there's no need to capture WM_Mousemove, or even subclass for that matter. You just need the headercontrol's handle.)

    And thx edgemeal i used ur code for LVM_GetHeader

    Code:
     
    Dim HBC As HeaderBarControl
        Public Class HeaderBarControl
            Private Const LVM_GETHEADER As UInt32 = &H101F
            Private Const WM_MOUSEMOVE As Int32 = &H200
            Private Const HDM_FIRST = &H1200
            Private Const HDM_HITTEST = (HDM_FIRST + 6)
            Friend Const HHT_ONHEADER As Integer = &H2
            Friend Const HHT_ONDIVIDER As Integer = &H4
            Friend Const HHT_ONDIVOPEN As Integer = &H8
            Friend Const HHT_ABOVE As Integer = &H100
            Friend Const HHT_BELOW As Integer = &H200
            Friend Const HHT_TORIGHT As Integer = &H400
            Friend Const HHT_TOLEFT As Integer = &H800
            Public mHandle As IntPtr
            Public Function ColumnHitTest(ByVal cursorPos As Point) As Integer
                Dim info As New HD_HITTESTINFO
                info.flags = HHT_ONHEADER Or HHT_ONDIVIDER
                info.pt = cursorPos
                SendMessage(mHandle, HDM_HITTEST, 0, info)
                Return info.item
            End Function
            <System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint:="SendMessage")> _
      Private Shared Function SendMessage1(ByVal hwnd As IntPtr, ByVal wMsg As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
            End Function
            Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _
            (ByVal hWnd As IntPtr, ByVal wMsg As Integer, ByVal wParam As Integer, ByRef lParam As HD_HITTESTINFO) As IntPtr
            <System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)> Private Structure HD_HITTESTINFO
                Dim pt As Point
                Dim flags As Int32
                Dim item As Int32
            End Structure
            Public Sub New(ByVal LV As ListView)
                mHandle = SendMessage1(LV.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero)
            End Sub
        End Class
    
        Private Sub LV_DrawColumnHeader(ByVal listview1 As MadListView, ByVal e As System.Windows.Forms.DrawListViewColumnHeaderEventArgs) Handles Me.DrawColumnHeader
            Dim isHighlighted As Boolean = False
            Dim mouseCoordinates As Drawing.Point = PointToClient(MousePosition)
            Dim rectR As Rectangle = listview1.Items(0).Subitems(e.ColumnIndex).Bounds
            mouseCoordinates.X -= (rectR.X - e.Bounds.X)
            If HBC.ColumnHitTest(mouseCoordinates) = e.ColumnIndex Then isHighlighted = True
         End Sub
    Basketball-NBA Dream status - quit
    Programming-Next Bill Gates Dream status - quit
    Becoming a doctor dream - LIVING IT: Med 2\4

  5. #5
    New Member
    Join Date
    Nov 2015
    Posts
    1

    Re: [RESOLVED] Listview columnheader No wndproc on mouse move :(

    I had trouble with this and came up with the following after looking at countless solutions which didn't seem to work as I wanted.

    Code:
        Public Declare Function GetWindowRect Lib "user32" (ByVal HWND As Integer, ByRef lpRect As RECT) As Integer
        Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" _
            (ByVal hWnd As IntPtr, ByVal wMsg As Int32, ByVal wParam As Int32, ByVal lParam As String) As Int32
    
    
        Public Structure RECT
            Dim Left As Integer
            Dim Top As Integer
            Dim Right As Integer
            Dim Bottom As Integer
        End Structure
    
    
        Private LVM_FIRST As Integer = &H1000
        Private LVM_GETHEADER As Integer = (LVM_FIRST + 31)
        Private LVM_GETCOLUMNWIDTH As Integer = (LVM_FIRST + 29)
    
    
        Private Function getHoverHeader(ByVal theListview As ListView) As Integer
    
            Dim columHeaderAreaHandle As Integer = 0
            Dim ColumnHeaderArea As RECT
            Dim mousePosition As Point = Control.MousePosition 'Get the mouse position relative to the screen
            Dim mouseHoverColumn As Integer = -1
    
            'Get the handle of theColumnHeaderArea
            columHeaderAreaHandle = SendMessage(theListview.Handle, LVM_GETHEADER, 0, 0)
    
            'Get the bounds of the ColumnHeaderArea relative to the screen
            GetWindowRect(columHeaderAreaHandle, ColumnHeaderArea)
    
            'Check if the mouse is over the ColumnHeaderArea
    
            If (mousePosition.X >= ColumnHeaderArea.Left And mousePosition.X < ColumnHeaderArea.Right) And _
                (mousePosition.Y >= ColumnHeaderArea.Top And mousePosition.Y < ColumnHeaderArea.Bottom) Then
    
                Dim colWidth As Integer = 0
                Dim colIndexCounter As Integer = -1
                Dim totalColWidths As Integer = ColumnHeaderArea.Left 'offset the value with the ColumnHeaderArea left position
    
                'Calculate which ColumnHeader the mouse is over in the ColumnHeaderArea
                'by adding the column widths together until they total more than the mouse left position
                Do
                    colIndexCounter = colIndexCounter + 1
                    colWidth = SendMessage(theListview.Handle, LVM_GETCOLUMNWIDTH, colIndexCounter, 0)
                    totalColWidths = totalColWidths + colWidth
                Loop Until totalColWidths >= mousePosition.X
    
                If (totalColWidths - mousePosition.X) > (colWidth - 8) Then
                    'If the mouse position is 8 pixels from the left side of the ColumnHeader then the previous
                    'ColumnHeader needs to be selected as its expecting to be resized.
                    mouseHoverColumn = colIndexCounter - 1
                Else
                    mouseHoverColumn = colIndexCounter
                End If
    
                'The _mouseHoverColumn cannot be less than 0 as the mouse position has already been confirmed
                'to be inside the ColumnHeaderArea
                If mouseHoverColumn < 0 Then mouseHoverColumn = 0
            End If
    
            Return mouseHoverColumn
        End Function
    
    
        ' Draws column headers.
        Private Sub listView1_DrawColumnHeader(ByVal sender As Object, _
            ByVal e As DrawListViewColumnHeaderEventArgs) _
            Handles ListView1.DrawColumnHeader
    
            Dim sf As New StringFormat()
            sf.Trimming = StringTrimming.EllipsisCharacter
            sf.FormatFlags = StringFormatFlags.NoWrap
            sf.LineAlignment = StringAlignment.Center
            sf.Alignment = StringAlignment.Near
    
            Try
                ' Store the column text alignment, letting it default
                ' to Left if it has not been set to Center or Right.
                Select Case e.Header.TextAlign
                    Case HorizontalAlignment.Center
                        sf.Alignment = StringAlignment.Center
                    Case HorizontalAlignment.Right
                        sf.Alignment = StringAlignment.Far
                End Select
    
                Try
                    If e.Header.Index = getHoverHeader(ListView1) Then
                        If e.State = ListViewItemStates.Selected Then
                            ControlPaint.DrawButton(e.Graphics, e.Bounds, ButtonState.Pushed)
                        Else
                            ControlPaint.DrawButton(e.Graphics, e.Bounds, ButtonState.Flat)
                        End If
                        e.Graphics.DrawString(e.Header.Text, ListView1.Font, Brushes.Red, e.Bounds, sf)
                    Else
                        e.DrawBackground()
                        e.Graphics.DrawString(e.Header.Text, ListView1.Font, Brushes.Black, e.Bounds, sf)
                    End If
                Finally
                    'headerFont.Dispose()
                End Try
    
            Finally
                sf.Dispose()
            End Try
        End Sub

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width