GetCtlAtMouse(): A Reliable Way to Return the Current Control Object Under the Mouse Cursor in Access
In a previous article, I wrote about the hidden and undocumented Form method, accHitTest:
In the comments section of that article, the Access UI Wizard himself, Alessandro Grimaldi (author of the new book, "Access on Steroids"), posted this follow-up question:
Hi Mike, and thanks for bringing this hidden feature to light. I just have a problem. I tried to use the test in the Detail_MouseMove() event, but as soon as the mouse hits the detail section of the form, this line:Set accObject = Me.accHitTest(pt.X, pt.Y)
throws the error 424 "Object required". I can't figure it out. Do you know what I'm doing wrong?
Honestly, Alessandro, I had not done much with accHitTest besides the very simple function I adapted from Karl Donaubauer's sample code for the article.
The question intrigued me, so I spent some time digging into the function to see if I could figure out what was going on.
BLUF: Bottom Line Up Front
The accHitTest method raises the 424, "Object required" error if you pass it coordinates to an empty area of the form.
The Earlier Example Had a Fatal Flaw
Here's an excerpt from my generalized ShowControlName() function in the earlier article:
Set accObject = Frm.AccHitTest(pt.x, pt.y)
If Not accObject Is Nothing Then
MsgBox "You just clicked on " & accObject.Name
End If
The line If Not accObject Is Nothing Then
does not do anything. While it makes sense that the AccHitTest method would return Nothing
if there was no associated control at that coordinate, that's not what happens. Instead, the method call fails with error 424, "Object required," before we even get to the line that checks for Nothing
.
GetCtlAtMouse(): A Better General Function
With this new information in hand, we can build a more robust solution.
I see no way around using On Error Resume Next
in this situation. However, to avoid swallowing unexpected errors, we follow up the call to OERN
with a Select Case
statement that handles the two outcomes we expect: (1) success when the mouse is over a control; and (2) failure when the mouse is over an empty area of the form. If we get an error number other than 0 or 424, we will show an error message and/or reraise the error so that it can bubble up the call stack (my preferred approach).
Here's the code:
Private Type POINTAPI
X As Long
Y As Long
End Type
Private Declare PtrSafe Function GetCursorPos Lib "user32.dll" _
(ByRef lpPoint As POINTAPI) As Long
' ----------------------------------------------------------------
' Procedure : GetCtlAtMouse
' Date : 1/17/2024
' Author : Mike Wolfe
' Source : https://nolongerset.com/getctlatmouse/
' Purpose : Returns the current control object under the mouse or
' Nothing if it is an empty area of the form.
' ----------------------------------------------------------------
Function GetCtlAtMouse(Frm As Form) As Access.Control
'Get the current position of the mouse
Dim pt As POINTAPI
GetCursorPos pt
'Attempt to get the control under the mouse cursor
On Error Resume Next
Set GetCtlAtMouse = Frm.AccHitTest(pt.X, pt.Y)
'We save the error information because `On Error Goto 0` will clear it
Dim ErrNum As Long, ErrDescription As String
ErrNum = Err.Number
ErrDescription = Err.Description
'Restore error handling in this procedure so future errors bubble up
On Error GoTo 0
Select Case ErrNum
Case 0 'No error; return control object
Debug.Assert Not GetCtlAtMouse Is Nothing
Exit Function
Case 424 'Object required; mouse over empty area of form; return Nothing
Debug.Assert GetCtlAtMouse Is Nothing
Exit Function
Case Else 'Unexpected error; we better report this one
'I prefer to rethrow the error so that it bubbles up the call stack;
' to use this approach, uncomment the 'Throw' line and comment out
' the MsgBox line
'https://nolongerset.com/throwing-errors-in-vba/
'Throw "Error [{0}]: {1}", ErrNum, ErrDescription
MsgBox ErrDescription, vbExclamation, _
"Error " & ErrNum & " in GetCtlAtMouse()"
End Select
End Function
ShowCtlName(): New and Improved
We can now reference our new function in an updated ShowControlName() function:
Function ShowControlName(Frm As Form)
Dim Ctl As Access.Control
Set Ctl = GetCtlAtMouse(Frm)
If Not Ctl Is Nothing Then
MsgBox "You are on " & Ctl.Name
End If
End Function
This time the If Not Ctl Is Nothing Then
line actually works!
A Final Note About Detail_MouseMove()
Unfortunately, calling this function from the Detail_MouseMove() event will probably not be very effective.
The problem is that form and section MouseMove events only get raised when the mouse moves over empty areas of the form or section. When the mouse moves over a control, the Detail_MouseMove() event does not get raised. Instead, the control's MouseMove event is raised.
I assume that Alessandro is trying to execute some code whenever the mouse moves over a certain control–or over any/every control.
The section or form MouseMove events seem like the obvious way to do that, but alas, Access does not work that way. To make that work, we would need an Access Mouse Preview form property like the Key Preview property for keyboard events.
Instead, there are two primary options:
- Select all the controls on the form (or section) and use the property sheet to assign a function to the On Mouse Move property, such as:
=PrintCtlName()
(see implementation below) - Create a class and use the
With Events
keyword to handle the Mouse Move event for every control on the form (for a demonstration of this technique, check out my Arrow Key Nav presentation)
Here's a sample function that you can call from the On Mouse Move property of every control on a form to print out the current coordinates and control name to the immediate window whenever the mouse passes over a control:
Function PrintCtlName()
Dim Ctl As Control
Set Ctl = GetCtlAtMouse(Me)
If Not Ctl Is Nothing Then
Dim pt As POINTAPI
GetCursorPos pt
Debug.Print pt.X; " "; pt.Y; " "; Ctl.Name
End If
End Function
Here's a bit of output from the Immediate Window when testing the above code:
Cover image generated by DALL-E-3.