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:

AccHitTest: Returns the Form Control the Mouse Clicked On
This undocumented form function can help you write a generic click event for label controls.

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:

  1. 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)
  2. 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.