vba Find Text box in an application using FindWind

2019-08-25 03:18发布

问题:

I have an MS Access form that contains Button to open an application. The application is created using c#. I want to get the TextBox in the Form so that I will set a value on it using the MS Access project.

I am using the following code:

hwndParent = FindWindow(vbNullString, "Form1")

If hwndParent <> 0 Then
   TextBoxHandle = FindWindowEx(hwndParent, 0&, "WindowsForms10.EDIT.app.0.3cb5890_r6_ad1", vbNullString)
   SendMessage TextBoxHandle, WM_SETTEXT, 0&, ByVal strText
End If

Above code is working on my workstation: Windows 10 Pro.

When I open the MS Access in windows 8. it can't find the TextBox.

TextBoxHandle always return 0 in Windows 8. I am sure that the issue is with 3rd parameter in FinWindowEx. I used spy++ from Microsoft to get the value WindowsForms10.EDIT.app.0.3cb5890_r6_ad1 cause when I try to just enter "Edit", it does not work.

回答1:

Edit: Adjusted answer using information about dynamic name of class from Hans Passant.

First, we're going to declare WinAPI functions to be able to iterate through all windows and get their class name.

Declare PtrSafe Function FindWindowExW Lib "user32" (ByVal hWndParent As LongPtr, Optional ByVal hwndChildAfter As LongPtr, Optional ByVal lpszClass As LongPtr, Optional ByVal lpszWindow As LongPtr) As LongPtr
Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameW" (ByVal hWnd As LongPtr, ByVal lpClassName As LongPtr, ByVal nMaxCount As Long) As Long

Then, we're going to declare a helper function to get the class name from a hWnd:

Public Function GetWindowClass(hWnd As LongPtr) As String
    Dim buf(512) As Byte
    GetClassName hWnd, varPtr(buf(0)), 255
    GetWindowClass = Replace(CStr(buf), Chr(0), "")
End Function

Then, we're going to iterate through all top-level windows, and return the hWnd from the one matching that class name:

Public Function getThehWnd(hWndParent) As LongPtr
    Dim hWnd As LongPtr
    hWnd = FindWindowExW(hWndParent)
    Do While hWnd <> 0
        If GetWindowClass(hWnd) Like "WindowsForms10.EDIT.app.0.*" Then
            getThehWnd = hWnd
            Exit Function
        End If
        hWnd = FindWindowExW(hWndParent, hWnd)
    Loop
End Function

Old answer:

There are numerous things that can go wrong when calling WinAPI functions from VBA with strings. These include passing a string that's not terminated by a null string, and passing a string that's in the wrong encoding.

For that first case, you get unstable behavior. If the string happens to be stored somewhere where there are a lot of zero's in memory, it works. Else, it continues reading bytes from memory and appending them to the string until it finds two bytes that happen to both be 0.

The first case is easily fixed by appending a null character to the end of your string:

TextBoxHandle = FindWindowEx(hwndParent, 0&, "WindowsForms10.EDIT.app.0.3cb5890_r6_ad1" & Chr(0), vbNullString)

Note that you should probably also make that last argument optional. Entering vbNullString there passes a pointer to a zero-length string, that might also not be delimited by a null character, causing WinAPI to read subsequent characters till it finds 2 null bytes. Setting the type to LongPtr and passing 0 (the default value) passes an actual null pointer, which WinAPI expects when no string gets put in.

The second code is more difficult. I tend to use bytearrays to make sure VBA doesn't do weird things

Dim className As Byte(1024)
className = "WindowsForms10.EDIT.app.0.3cb5890_r6_ad1" 'Yes, this is valid, and assigns the first part of the bytearray to a string
FindWindowExW(hwndParent, 0&, VarPtr(className(0)))

The corresponding declaration of FindWindowExW:

Declare PtrSafe Function FindWindowExW Lib "user32" (ByVal hWndParent As LongPtr, Optional ByVal hwndChildAfter As LongPtr, Optional ByVal lpszClass As LongPtr, Optional ByVal lpszWindow As String) As LongPtr

To debug problems and identify specific windows, I use the following function to iterate through all top and child windows, instead of Spy++. This one has the advantage of running in VBA, so you can set breakpoints and watches, which means you can very easily determine the class name and parent window of all open windows:

Public Sub IterateAllWindows(Optional hWnd As LongPtr, Optional EnumLevel = 0)
    Dim hwndChild As LongPtr
    If hWnd <> 0 Then
        Debug.Print String(EnumLevel, "-");
        Debug.Print hWnd & ":";
        Debug.Print GetWindowName(hWnd);
        Debug.Print "(" & GetWindowClass(hWnd) & ")"
        hwndChild = FindWindowExW(hWnd)
        Do While hwndChild <> 0
            IterateAllWindows hwndChild, EnumLevel:=EnumLevel + 1
            hwndChild = FindWindowExW(hWnd, hwndChild)
        Loop
    Else
        Dim hWndTopLevel As LongPtr
        hWndTopLevel = GetTopWindow
        Do While hWndTopLevel <> 0
            Debug.Print String(EnumLevel, "-");
            Debug.Print hWndTopLevel & ":";
            Debug.Print GetWindowName(hWndTopLevel);
            Debug.Print "(" & GetWindowClass(hWndTopLevel) & ")"
            hwndChild = FindWindowExW(hWndTopLevel)
            Do While hwndChild <> 0
                IterateAllWindows hwndChild, EnumLevel:=EnumLevel + 1
                hwndChild = FindWindowExW(hWndTopLevel, hwndChild)
            Loop
            hWndTopLevel = GetWindow(hWndTopLevel, 2)
        Loop
    End If
End Sub

This uses the following 2 helper functions:

Public Function GetWindowName(hWnd As LongPtr) As String
    Dim buf(512) As Byte
    GetWindowText hWnd, varPtr(buf(0)), 255
    GetWindowName = Replace(CStr(buf), Chr(0), "")
End Function

Public Function GetWindowClass(hWnd As LongPtr) As String
    Dim buf(512) As Byte
    GetClassName hWnd, varPtr(buf(0)), 255
    GetWindowClass = Replace(CStr(buf), Chr(0), "")
End Function

Corresponding WinAPI declarations for that sub:

Declare PtrSafe Function GetTopWindow Lib "user32" (Optional ByVal hWnd As LongPtr) As LongPtr
Declare PtrSafe Function GetWindow Lib "user32" (ByVal hWnd As LongPtr, ByVal wCmd As Integer) As LongPtr
Declare PtrSafe Function GetWindowText Lib "user32" Alias "GetWindowTextW" (ByVal hWnd As LongPtr, ByVal lpString As Any, ByVal nMaxCount As Long) As Long
Declare PtrSafe Function FindWindowExW Lib "user32" (ByVal hWndParent As LongPtr, Optional ByVal hwndChildAfter As LongPtr, Optional ByVal lpszClass As LongPtr, Optional ByVal lpszWindow As LongPtr) As LongPtr
Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameW" (ByVal hWnd As LongPtr, ByVal lpClassName As LongPtr, ByVal nMaxCount As Long) As Long

Running this function with a watch on that class name should help you identify if it's top-level or a child window, and if it's a child window, which class it belongs to. You can also modify it to return the hWnd independent of nesting (by using an If getWindowClass = "WindowsForms10.EDIT.app.0.3cb5890_r6_ad1" Then or by checking the title).



回答2:

I think you should use Spy to conduct the same investigations on Windows 8 as you (presumably) did on Windows 10. Something must be different there, else your code would work.

Sidenote (because it bit me in the past): make sure you run the version of Spy whose 'bitness' (32 bit / 64 bit) matches the application you're interested in, otherwise message logging doesn't work.

Also, sorry for my previous post, it was a load of cr@p.

Edit Ah ha! Hans comments above that the class name is dynamically generated, so that's your problem. So now we know.