Find Top Window

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Find Top Window

Post by SammyB »

What API can I use to find which window is on top at a particular location? I have a macro to perform a given number of mouse clicks at a given frequency, but it no longer works in one instance because the mouse-clicks cause short-lived pop-up windows. :groan: So, before I start the macro, I need to get the handle of the window on top. Then before I programmatically click, check who is on top and wait for any other windows to disappear. TIA!

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Re: Find Top Window

Post by SammyB »

I suppose that I could use a different algorithm: mouse click (which will activate the app) and GetActiveWindow. Then before the next mouse click, GetActiveWindow and skip the click if it's not the original window. I'll try it.

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Re: Find Top Window

Post by SammyB »

Looks like it should be GetForegroundWindow instead of GetActiveWindow

User avatar
HansV
Administrator
Posts: 72885
Joined: 16 Jan 2010, 00:14
Status: Microsoft MVP
Location: Wageningen, The Netherlands

Re: Find Top Window

Post by HansV »

And? Does that work?
Regards,
Hans

SpeakEasy
2StarLounger
Posts: 102
Joined: 27 Jun 2021, 10:46

Re: Find Top Window

Post by SpeakEasy »

API call that specifically answers your requirement to "find which window is on top at a particular location?" is WindowFromPoint, eg

Code: Select all

Option Explicit

Private Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
Private Declare Function WindowFromPoint Lib "user32" (ByVal X As Long, ByVal Y As Long) As Long 

Private Type POINTAPI
    X As Long
    Y As Long
End Type

Private mousePT As POINTAPI

Public Function WindowUnderCursor() As Long
    GetCursorPos mousePT 
    WindowUnderCursor = WindowFromPoint(mousePT.X, mousePT.Y)
End Function

Public Sub test()
    MsgBox WindowUnderCursor
End Sub

PJ_in_FL
5StarLounger
Posts: 950
Joined: 21 Jan 2011, 16:51
Location: Florida

Re: Find Top Window

Post by PJ_in_FL »

What's the declaration statements for a 64-bit system? Excel tells me I need to revise the statements for 64-bit and add PTRSAFE.
PJ in (usually sunny) FL

SpeakEasy
2StarLounger
Posts: 102
Joined: 27 Jun 2021, 10:46

Re: Find Top Window

Post by SpeakEasy »

Private Declare PtrSafe Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
Private Declare PtrSafe Function WindowFromPoint Lib "user32" (ByVal X As Long, ByVal Y As Long) As Long

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Re: Find Top Window

Post by SammyB »

Argggggggg, I loved SpeakEasy's WindowFromPoint solution, but it did not catch the stupid pop-up window: it gave me a number, but always the same one even though there were 11 times when the pop-up window received the click. Unfortunately, I cannot repeat it: I have no control over the program that I am clicking in. But, Google is your friend, just discovered that on 64-bit Windows, WindowFromPoint only takes one argument, a POINTAPI. I should have looked that up before writing the code (and should have told you, sorry). By tomorrow, I should be able to test it again. Tough to debug when you only get one try per day. I'll let you know tomorrow. Thanks!

Just noticed there is a question re declaration. This is the one that I will use.

Code: Select all

Public Type POINTAPI
    x As Long
    y As Long
End Type

Public Declare PtrSafe Function WindowFromPoint Lib "user32" (ByRef lpPoint As POINTAPI) As Long
No guarantee on that: cannot test it until tomorrow.

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Re: Find Top Window

Post by SammyB »

and that is the same as what SpeakEasy said, so it will probably work

SpeakEasy
2StarLounger
Posts: 102
Joined: 27 Jun 2021, 10:46

Re: Find Top Window

Post by SpeakEasy »

>Google is your friend, just discovered that on 64-bit Windows, WindowFromPoint only takes one argument, a POINTAPI. I should have looked that up before writing the code


Just because Google says it doesn't neccessarily make it true ...

Here's the thing. Whilst the formal API declaration in winuser.h does indeed indicate that the call takes a single POINT parameter, that is a C++ declaration, and as it stands it won't work in VBA. Why? Because we need to pass ByVal, and VBA cannot pass UDTs ByVal. The correct declaration for VBA is the one I gave.

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Re: Find Top Window

Post by SammyB »

Wow, good information! But, I thought that if you did not specify how the parameter is passed (and you did not), then it is passed by reference. And, that would mean that our declarations were the same. At least I can test this out. I will write a test macro & post it here. Thanks!

SpeakEasy
2StarLounger
Posts: 102
Joined: 27 Jun 2021, 10:46

Re: Find Top Window

Post by SpeakEasy »

>and you did not

I didn't for GetCursorPos because that function does NOT require the UDT to be passed ByVal. (because they are [out] parameters if you check the C ++ declaration)

But that isn't the API call being called into question; both my declarations (32bit and 64bit) for WindowFromPoint are clearly using ByVal (since they are [in] parameters)

And if you try to use

Public Declare PtrSafe Function WindowFromPoint Lib "user32" (ByRef lpPoint As POINTAPI) As Long

you'll get compile error 'bad DLL calling convention' error

Change it to ByVal, and you'll get a 'User-defined type may not be passed ByVal' error

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Re: Find Top Window

Post by SammyB »

Be patient, lol. I am super picky about API declarations. Here is my test program. Application.hWnd, WindowFromPoint, and GetForegroundWindow all return the same value, so either WindowFromPoint or GetForegroundWindow should work in my clicker macro.

Code: Select all

Option Explicit

Private Type POINTAPI
    x As Long
    y As Long
End Type

Private Declare PtrSafe Function GetCursorPos Lib "user32" ( _
    ByRef lpPoint As POINTAPI) As Long

Private Declare PtrSafe Sub mouse_event Lib "user32" ( _
    ByVal dwFlags As Long, _
    ByVal dx As Long, _
    ByVal dy As Long, _
    ByVal cButtons As Long, _
    ByVal dwExtraInfo As Long)

Private Const MOUSEEVENTF_LEFTDOWN = &H2 ' left button down
Private Const MOUSEEVENTF_LEFTUP = &H4 ' left button up
Private Const MOUSEEVENTF_MIDDLEDOWN = &H20 ' middle button down
Private Const MOUSEEVENTF_MIDDLEUP = &H40 ' middle button up
Private Const MOUSEEVENTF_RIGHTDOWN = &H8 ' right button down
Private Const MOUSEEVENTF_RIGHTUP = &H10 ' right button up

Private Declare PtrSafe Function WindowFromPoint Lib "user32" ( _
    ByRef lpPoint As POINTAPI) As Long

Private Declare PtrSafe Function GetForegroundWindow Lib "user32" () As Long

Public Sub WhoIsOnFirst()
    Dim p As POINTAPI, hX As Long, hC As Long, hT As Long
    MsgBox "Move the cursor over the Title Bar of an Excel Window & press the Enter Key"
    GetCursorPos p
    ' Click the mouse & activate the excel window
    mouse_event MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0
    mouse_event MOUSEEVENTF_LEFTUP, 0, 0, 0, 0
    hX = Application.hwnd
    hC = WindowFromPoint(p)
    hT = GetForegroundWindow
    Debug.Print hX, hC, hT
End Sub[\code]

SpeakEasy
2StarLounger
Posts: 102
Joined: 27 Jun 2021, 10:46

Re: Find Top Window

Post by SpeakEasy »

>Application.hWnd, WindowFromPoint, and GetForegroundWindow all return the same value

Is this your assumption, or have you tested this? I ask because

a) the code you present should not run without errors.
b) Even if the code worked, I would not expect the results to be the same for each of the methods (in particular, I would suggest that GetForegroundWindow does not do what you think it is doing; it has nothing directly to do with the window's position in the z-order), although in your specific example here Application.hWnd and GetForegroundWindow will return the same result. Applying GetAncestor to the result returned from WindowFromPoint with gaFlags set to GA_ROOT should get the same result as GetForegroundWindow

User avatar
rory
5StarLounger
Posts: 751
Joined: 24 Jan 2010, 15:56

Re: Find Top Window

Post by rory »

Per the MS docs, for 64bit applications:

Code: Select all

#If Win64 Then
Declare PtrSafe Function WindowFromPoint Lib "user32" Alias "WindowFromPoint" (ByVal Point As LongLong) As LongPtr

' Copies a POINTAPI into a LongLong.  For an API requiring a ByVal POINTAPI parameter,
' this LongLong can be passed in instead.  Example API's include WindowFromPoint,
' ChildWindowFromPoint, ChildWindowFromPointEx, DragDetect, and MenuItemFromPoint.
Function PointToLongLong(point As POINTAPI) As LongLong
    Dim ll As LongLong
    Dim cbLongLong As LongPtr
    
    cbLongLong = LenB(ll)
    
    ' make sure the contents will fit
    If LenB(point) = cbLongLong Then
        CopyMemory ll, point, cbLongLong
    End If
    
    PointToLongLong = ll
End Function
#Else
Regards,
Rory

SpeakEasy
2StarLounger
Posts: 102
Joined: 27 Jun 2021, 10:46

Re: Find Top Window

Post by SpeakEasy »

Yep, that should work!

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Re: Find Top Window

Post by SammyB »

Thanks Rory. That explains why I was having so much trouble using WindowFromPoint. I ended up using GetForegroundWindow. It also has issues: "The foreground window can be NULL in certain circumstances, such as when a window is losing activation," which is exactly what is happening when there is a popup. Fortunately that is when I don't want to count the mouse click. So, I wrote a function:

Code: Select all

Private Function ClickAndGetHandle(iMilli As Long) As Long
    Dim h As Long
    mouse_event MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0
    mouse_event MOUSEEVENTF_LEFTUP, 0, 0, 0, 0
    h = GetForegroundWindow
    Do While h = 0
        iStopLoop = iStopLoop + 1
        Debug.Print "Skip #" & iStopLoop
        If iStopLoop > 200 Then End
        Sleep iMilli \ 2
        h = GetForegroundWindow
    Loop
    ClickAndGetHandle = h
End Function
iStopLoop is a module-level global. I stuck it in because I figured that I would probable have an infinite loop, but so far that hasn't happened.

I also had issues with getting the wrong handle especially on the first click, so I started with clicking until I got the same handle twice in a row, then just counted clicks with that handle. That got me close, just off by several clicks & close is good enough. I just don't want to wear out my hand clicking. Here is the macro:

Code: Select all

Public Sub ClickerNoPopup()
    Dim cp1 As POINTAPI, cp2 As POINTAPI
    Dim n As Long, s As String
    s = InputBox("Enter number of clicks, move to position, & press the Enter key")
    n = CInt(s)

'   In case instructions were not followed, wait until cursor stops
    Do
        GetCursorPos cp1
        Sleep 500
        GetCursorPos cp2
    Loop Until cp1.x = cp2.x And cp1.y = cp2.y
    Sleep 500
    Dim i As Long, iMilli As Long
    iMilli = 100

'   Make sure we have the correct handle
    iStopLoop = 0
    Dim h1 As Long, h2 As Long
    Do
        h1 = ClickAndGetHandle(iMilli)
        Debug.Print "h1", h1
        h2 = ClickAndGetHandle(iMilli)
        Debug.Print "h2", h2
    Loop Until h1 = h2
    
    For i = 1 To n - 2
        Do
            h2 = ClickAndGetHandle(iMilli)
            Debug.Print "h ", h2
        Loop Until h1 = h2
        Sleep iMilli / 2
    Next i
End Sub
Thanks everyone for your help! Sorry it took so long to get back with the solution, but this is not for work & work is currently a monstrous macro.

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Re: Find Top Window

Post by SammyB »

Well, I spoke too soon: had an infinite loop (or would have had one without iStopLoop). Looks like the window handle changed or I had the wrong handle initially. Can that happen?

As I said above, testing is difficult. I only get a buildup of a need for 100 mouse clicks per day & it takes about 20 clicks before popups start, so stopping the loop at 200 wiped out my chances for another test. Maybe I can figure it out tomorrow.

SammyB
Lounger
Posts: 48
Joined: 04 Mar 2010, 16:32

Re: Find Top Window

Post by SammyB »

Well, for now, we will never know the answer. The vendor that was throwing up the annoying pop-ups fixed their code, so my mouse clicker works great. :scratch: Now, if I could finish my massive data mining macros, I will be happy.

User avatar
HansV
Administrator
Posts: 72885
Joined: 16 Jan 2010, 00:14
Status: Microsoft MVP
Location: Wageningen, The Netherlands

Re: Find Top Window

Post by HansV »

Anyway, that's a good result.
Regards,
Hans