Classic VB Corner

Hooking the Mouse

Subclass multiple controls with a single class module to add the missing MouseEnter and MouseLeave events.

Subclassing in Classic VB has always been a bit of a pain. In fact, it wasn't even possible until VB5 offered the AddressOf operator to facilitate system callbacks. Recently, I wrote about the SetWindowSubclass API which offers an incredibly slick way to hook into any in-process window's message stream. In this article, I'll show you how to build a class that can provide message processing for multiple controls no matter their type. If you haven't read the previous article, please do before continuing here as this article will assume that as pretext.

To illustrate the utility of this method, I chose to fill in a widely recognized gap in functionality: the lack of MouseEnter and MouseLeave events. Oddly enough, Windows offers both WM_MOUSELEAVE and WM_MOUSEHOVER notifications, but doesn't offer a WM_MOUSEENTER message. So it's up to us to recognize this event ourselves, which can be done by monitoring WM_MOUSEMOVE. When that message arrives for the first time, we know that the mouse has just entered the hWnd of interest. A static variable remembers this handle, and is reset when the mouse later leaves the window.

The only remaining trick is enabling the leave and hover notifications. For this, we call the TrackMouseEvent API when we first detect the mouse entering a watched window. We can use this call to tell Windows we want to be notified when the mouse leaves the window, as well as when it first comes to rest for a given "hover" interval. Raising a MouseHover event in the latter case allows such things as providing informational materials, but only after the user brings the mouse to a rest. Here's the whole message processing scheme:

Private Function IHookXP_Message(ByVal hWnd As Long, _
   ByVal uiMsg As Long, ByVal wParam As Long, _
   ByVal lParam As Long, ByVal dwRefData As Long) As Long

   Static hWndLast As Long
   
   ' Special processing for messages we care about.
   Select Case uiMsg
      Case WM_MOUSEMOVE
         If hWnd <> hWndLast Then
            hWndLast = hWnd  'Set flag
            RaiseEvent MouseEnter(hWnd)
            ' Start tracking for Leave event.
            StartTrackingMouse hWnd, TME_LEAVE Or TME_HOVER
         End If
      
      Case WM_MOUSEHOVER
         RaiseEvent MouseHover(hWnd)
      
      Case WM_MOUSELEAVE
         hWndLast = 0  'Clear flag
         RaiseEvent MouseLeave(hWnd)
   End Select
   
   ' Pass back to default message handler.
   IHookXP_Message = HookDefault(hWnd, uiMsg, wParam, lParam)
End Function

Private Function StartTrackingMouse _
   (ByVal hWnd As Long, ByVal Flags As Long) As Long
   Dim tme As TRACKMOUSEEVENT_STRUCT
   ' Wrap all the mess into a tidy little procedure.
   With tme
      .cbSize = Len(tme)
      .dwFlags = Flags
      If .dwFlags And TME_HOVER Then
         .dwHoverTime = m_HoverTime
      End If
      .hWndTrack = hWnd
   End With
   ' Return results.
   StartTrackingMouse = TrackMouseEvent(tme)
End Function

Notice that the raised events are passing an hWnd parameter back to the client. As I mentioned earlier, this class is designed to hook messages for any number of controls. In fact, here's the code I use to set up the class in Form_Load event of the demo on my site:

Private WithEvents m_ME As CHookMouseEvents

Private Sub Form_Load()
   ' Start watching for mouse events.
   Set m_ME = New CHookMouseEvents
   m_ME.HoverTime = 1000 'milliseconds
   m_ME.Add Me
   m_ME.Add Text1
   m_ME.Add HScroll1
   m_ME.Add VScroll1
   m_ME.Add Combo1
   m_ME.Add Option1
   m_ME.Add Check1
   m_ME.Add List1
   m_ME.Add Drive1
   m_ME.Add Dir1
   m_ME.Add File1
End Sub

Yeah, it's a form with every one of the visible intrinsic controls included. I've built the CHookMouseEvents class to function somewhat similarly to a Collection on steroids. In fact, it's using a native Collection object to store references to all the hooked windows. The code is simple, and allows great ease of setup:

Private Sub Class_Initialize()
   ' Set defaults
   m_Enabled = defEnabled
   m_HoverTime = defHoverTime
   Set m_Objects = New Collection
End Sub

Public Function Add(obj As Object) As Boolean
   On Error Resume Next
   m_Objects.Add obj, FmtHex(obj.hWnd, 8)
   If Err.Number = 0 Then 'success
      If HookSet(obj.hWnd, Me) Then
         Add = True
      End If
   Else
      Debug.Print Err.Number, Err.Description
   End If
End Function

Public Function Count() As Long
   Count = m_Objects.Count
End Function

Public Function Item(ByVal hWnd As Long) As Object
   On Error Resume Next
   Set Item = m_Objects.Item(FmtHex(hWnd, 8))
End Function

Public Function Remove(obj As Object) As Boolean
   On Error Resume Next
   Remove = UnhookOne(obj.hWnd)
End Function

Obviously, this will only work for objects that expose an hWnd property. The HookSet procedure called in the Add method was discussed in detail in the previous article. It's simply a call to SetWindowSubclass that uses an ObjPtr to the IHookXP interface passed to it as the ultimate destination for the hooked messages. To remove an object from message processing, we just remove it from the collection and call HookClear (see previous article) which is a shortcut to RemoveWindowSubclass.

No clean-up is necessary in the form that's hosting all the controls and sinking the messages raised by CHookMouseEvents. The class handles all the dirty work by watching for WM_NCDESTROY messages in the IHookXP_Message callback method, and calling UnhookOne just as it would in the Remove method. The class also takes care to call UnhookAll in its own Terminate event:

Private Sub Class_Terminate()
   ' Tear down
   Call UnhookAll
   Set m_Objects = Nothing
End Sub 

Private Sub UnhookAll()
   Dim obj As Object
   ' Clear existing hook.
   For Each obj In m_Objects
      Call HookClear(obj.hWnd, Me)
   Next obj
End Sub

And that's really all there is to it. So, to add this processing to your project, you simply drop in the three modules I supply (CHookMouseEvents.cls, MHookXP.bas, IHookXP.cls), declare an instance of CHookMouseEvents using WithEvents, and hand it the objects you'd like to be notified of mouse activity for.

Adding support for the extra two buttons on five-button mice is also incredibly simple. Just plug in handlers for three more notifications in your message processor:

Private Function IHookXP_Message(ByVal hWnd As Long, _
   ByVal uiMsg As Long, ByVal wParam As Long, _
   ByVal lParam As Long, ByVal dwRefData As Long) As Long

   Static hWndLast As Long
   
   ' Special processing for messages we care about.
   Select Case uiMsg
      Case WM_XBUTTONDOWN
         If m_Enabled Then
            RaiseEvent XButtonDown(hWnd, _
               WordHi(wParam), WordLo(lParam), WordHi(lParam))
         End If
         
      Case WM_XBUTTONUP
         If m_Enabled Then
            RaiseEvent XButtonUp(hWnd, _
               WordHi(wParam), WordLo(lParam), WordHi(lParam))
         End If
         
      Case WM_XBUTTONDBLCLK
         If m_Enabled Then
            RaiseEvent XButtonDblClick(hWnd, _
               WordHi(wParam), WordLo(lParam), WordHi(lParam))
         End If
   End Select
   
   ' Pass back to default message handler.
   IHookXP_Message = HookDefault(hWnd, uiMsg, wParam, lParam)
End Function

For each of these notifications, the Button value is stored in the high word of wParam, and the X/Y mouse coordinates are stored in the low and high words (respectively) of lParam. I've expanded the HookXP sample on my site to include this new demo of mouse event processing. To me, the fascinating aspect of this class is how it handles hooking multiple windows, while taking care of all the "dirty housekeeping" with what's actually very minimal code.

As always when subclassing with native code, be safe. Unhandled errors can be deadly. Save before running.

About the Author

Karl E. Peterson wrote Q&A, Programming Techniques, and various other columns for VBPJ and VSM from 1995 onward, until Classic VB columns were dropped entirely in favor of other languages. Similarly, Karl was a Microsoft BASIC MVP from 1994 through 2005, until such community contributions were no longer deemed valuable. He is the author of VisualStudioMagazine.com's new Classic VB Corner column. You can contact him through his Web site if you'd like to suggest future topics for this column.

comments powered by Disqus

Featured

Subscribe on YouTube