Introduction
This article is about creating ActiveX controls in Visual Basic 6 that has two extra mouse Events:
MouseLeave
: raised when the cursor get out of the control
MouseHover
: Raised when the user pauses the cursor over the control for a defined time (default is 400 milliseconds)
A famous approach to achieve this is to use a Timer control with a small interval. And in the timer event, the programmer checks the cursor location. (I do hate this. It's Painful and needs a lot of work and overhead to track the cursor).
Another way is to start using VB.NET which has these events built-in. (But you should have stronger reasons to switch to .NET !!).
The alternative way used in this article is to let Windows send you MouseLeave
, MouseHover
Messages (Events).
How To Do This?
We need 3 things to achieve this:
- To tell Windows that you want it to send you the required events.
This is achieved by calling TrackMouseEvent
API function specifying Events you need and the hover time you want.This is done in the main module (mdlProc.bas) in the RequestTracking
Function.
Dim trk As tagTRACKMOUSEEVENT
trk.cbSize = 16
trk.dwFlags = TME_LEAVE Or TME_HOVER
trk.dwHoverTime = trak.HoverTime
trk.hwndTrack = trak.hwnd
TrackMouseEvent trk
- To receive the message when Windows sends it.
Visual Basic does not have a built-in mechanism to receive custom messages. You can only choose from a list of events in the form or control code window.
So we need to Subclass the control's window to intercept all messages sent to the window.Then we can handle the messages we need and forward the rest to the original window procedure. This is done by calling the SetWindowLong
API to set the new window procedure:
SetWindowLong(ctl.hwnd, GWL_WNDPROC, AddressOf WindowProc)
The WindowProc
Function is defined in mdlProc.bas like this:
Private Function WindowProc(ByVal hwnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
We need to handle 3 specific messages: WM_MOUSELEAVE
, WM_MOUSEHOVER
, WM_MOUSEMOVE
and forward other messages (as well as the WM_MOUSEMOVE
message) directly to the original window procedure:
WindowProc = CallWindowProc(trak.PrevProc, hwnd, uMsg, wParam, lParam)
- We need to dispatch the message to the window.
Note that all messages are sent to the WindowProc
Function. But we may have multiple controls on the form. so we want to know which control was this message originally sent to.
To make this, we use a collection trackCol
to hold references to clsTrackInfo
objects. And the keys of the collection are the window handles (hwnd
). I use window handles as keys because WindowProc
Function receives the window handle as a parameter. So we can use it to lookup the clsTrackInfo
object in the collection.
To add the control to the collection:
trackCol.Add trak, CStr(trak.hwnd)
To search for the required control:
Set trak = trackCol.Item(CStr(hwnd))
Then we use this code to check the value of the message and take the required action:
If uMsg = WM_MOUSELEAVE Then
trak.RaiseMouseLeave
ElseIf uMsg = WM_MOUSEHOVER Then
trak.RaiseMouseHover
ElseIf uMsg = WM_MOUSEMOVE Then
RequestTracking trak
WindowProc = CallWindowProc(trak.prevProc, hwnd, uMsg, wParam, lParam)
Else
WindowProc = CallWindowProc(trak.prevProc, hwnd, uMsg, wParam, lParam)
End If
Skeleton of the Control
In the mdlProc.bas I use the clsTrackInfo
to be stored in the trackCol
collection. These objects in the collection are used to connect the module code to the UserControl
.
It makes more sense to store references to the UserControl
directly. But this causes the Terminate
event not to be raised in some cases due to circular references.
(More about this in :Knowledge base)
Control's Skeleton Code
Note that I declared MyTrak
with events:
Dim WithEvents MyTrak As clsTrackInfo
The code is as follows:
Option Explicit
Public Event MouseLeave()
Public Event MouseHover()
Dim WithEvents MyTrak As clsTrackInfo
Private Sub MyTrak_MouseHover()
RaiseEvent MouseHover
End Sub
Private Sub MyTrak_MouseLeave()
RaiseEvent MouseLeave
End Sub
Public Property Get HoverTime() As Long
HoverTime = MyTrak.HoverTime
End Property
Public Property Let HoverTime(newHoverTime As Long)
MyTrak.HoverTime = newHoverTime
PropertyChanged "HoverTime"
End Property
Private Sub UserControl_InitProperties()
Set MyTrak = New clsTrackInfo
End Sub
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
Set MyTrak = New clsTrackInfo
MyTrak.hwnd = UserControl.hwnd
MyTrak.HoverTime = PropBag.ReadProperty("HoverTime", 400)
If Ambient.UserMode Then
StartTrack MyTrak
End If
End Sub
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
PropBag.WriteProperty "HoverTime", MyTrak.HoverTime, 400
End Sub
Private Sub UserControl_Terminate()
EndTrack MyTrak
Set MyTrak = Nothing
End Sub
I handle MyTrak_MouseHover
and MyTrak_MouseLeave
events of MyTrak
object to raise the required events.
Notes
StartTrack
is called in the UserControl_ReadProperties
to start tracking the events and add the control to the trackCol
Collection, and EndTrack
is called in the UserControl_Terminate
event to end tracking and remove the control from the trackCol
Collection.
I used UserControl_ReadProperties
not UserControl_Initialize
to be able to check the Ambient.UserMode
property which is not available in the UserControl_Initialize
event.
WM_MOUSEHOVER
is sent when the user pauses the mouse over the control for a specific time. The default hover time is 400 milliseconds (the same as Windows default) but you can change it.
- After the first time windows sends the
WM_MOUSEHOVER
or WM_MOUSELEAVE
events, it does not resend them till you re-request this. So I call RequestTracking
when WM_MOUSEMOVE
message is sent.
- Set the
Instancing
property of clsTrackInfo
to private
.
- Take care when changing this article's code or generally when using window subclassing in Visual Basic. My IDE crashed many times before I could make it work fine!!.
- Handle all errors in
MouseLeave
, MouseHover
and MouseMove
Event handlers. Any unhandled errors can make the IDE or the application crash or give more errors. So using On Error ... goto..
or On Error Resume Next
is advisable.
Also in the error trapping (Tools->Options->General tab), select break on unhandled errors or break in class module not break on all errors.
- It's always better not to end your application using
End
or by clicking End in the IDE... This causes Terminate
Events not to be called.
If You Don't Understand All the Above
You still can use the code.
- Create a new ActiveX Control project.
- Add the mdlProc.bas, clsTrackInfo.cls to the project.
- Copy and paste the skeleton code above to your control.
Please feel free to contact the author for any questions or comments using this forum.
History
- 24th April, 2004: Initial post