aurel
aurel

Reputation: 1137

How can I get a handle to a window of executed process in VBA?

In my VBA application I start IExplore process with:

Shell sIE, vbMaximizedFocus

Now I need to resize created window. For that I can use SetWindowPos function, which takes a handle to the window as one of the arguments. And I don't have that handle...

I would use FindWindowLike function (which goes threw windows, compares caption with pattern and returns array of handles of windows with matching caption), but I cann't rely on window caption. I cann't just resize all of the IE windows also.

So, I was thinking of using SOMETHING that would give me a handle of a window to the process I just ran. Shell does not provide this.

I have some example code, how to do this in C++ using CoCreateInstance function:

    CoCreateInstance(CLSID_InternetExplorer, NULL, CLSCTX_LOCAL_SERVER, IID_IWebBrowser2, (void**)&m_pBrowser);
        if (m_pBrowser)
        {
            pom  = buffer;
            m_pBrowser->put_Visible(VARIANT_TRUE);
            m_pBrowser->Navigate(pom, &(_variant_t(flaga)), &vDummy, &vDummy, &vDummy);
            m_pBrowser->get_HWND((long *)&hWnd);
            if (hWnd != NULL)
            {
             ...
             ...

I would've port this to VBA, but I'm not so sure, what to put for fourth parameter:

riid [in] Reference to the identifier of the interface to be used to communicate with the object.

Well I don't know witch interface I should pass... I'm not even sure if I can use it in VBA.

So. Is there a way to execute process, which would provide me a handle to it's window?

Upvotes: 0

Views: 6249

Answers (2)

Bughater
Bughater

Reputation: 73

I 'googled' and tested the 'solutions' quite a long time before getting a clear answer for this issue. The essential point is that you should exactly know what you intend to do with the Hwnd !

The reason is that while the PID remains constant, the corresponding active Hwnd may change - for example when the window starts [or gets] minimized. In such case, I didn't success in finding the 'real' window I was looking for from the PID (i.e. to restore it). Note that the window text caption may change, too… (check using test function below)

The following function retrieves 'the' Hwnd from the PID (from CodeGuru, modified):

Public Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
Public Declare PtrSafe Function GetParent Lib "user32.dll" _
(ByVal hwnd As LongPtr) As LongPtr
Public Declare PtrSafe Function GetWindowThreadProcessId Lib "user32" _
(ByVal hwnd As LongPtr, lpdwProcessId As Long) As Long

Function WndFromPID(ByVal Xpid As LongPtr) As LongPtr
Dim Nhwnd As LongPtr, Npid As Long, Nthread_id As Long
' Get the first window handle.
Nhwnd = FindWindow(vbNullString, vbNullString)  ' NOT (ByVal 0&, ByVal 0& ) !
' Loop until we find the target or we run out of windows
Do While Nhwnd <> 0
    ' See if this window has a parent. If not, it is a top-level window
    If GetParent(Nhwnd) = 0 Then
        ' This is a top-level window. See if it has the target instance handle
        Nthread_id = GetWindowThreadProcessId(Nhwnd, Npid)
        If Npid = Xpid Then
            WndFromPID = Nhwnd      ' This is the target
            Exit Do
        End If
    End If
    Nhwnd = GetWindow(Nhwnd, 2)     ' Examine the next window [2 = GW_HWNDNEXT]
Loop    
End Function

As explained above, this returns nothing useful if your window is minimized (eventually with another text caption). Hence, I recommend to search for the desired window using a partial text caption (from VBforum, modified):

Public Declare PtrSafe Function IsWindowVisible Lib "user32" _
(ByVal hwnd As LongPtr) As Long
' *** this is just a flag, you may indeed see an invisible window and vice versa *** 
Public Declare PtrSafe Function GetWindow Lib "user32" _
(ByVal hwnd As LongPtr, ByVal wCmd As Long) As LongPtr
Public Declare PtrSafe Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
(ByVal hwnd As LongPtr, ByVal lpString As String, ByVal cch As Long) As Long
Public Declare PtrSafe Function GetWindowTextLength Lib "user32" _
Alias "GetWindowTextLengthA" (ByVal hwnd As LongPtr) As Long
Public Function FindWindowHandle(PartialTitle As String, Sichtbar As Byte) As LongPtr
Dim lhWndP As LongPtr
If GetHandleFromPartialCaption(lhWndP, PartialTitle) = True Then
    Select Case Sichtbar
        Case 0              ' invisible
            If IsWindowVisible(lhWndP) = False Then
                ' MsgBox "INVISIBLE Window Handle: " & lhWndP, vbOKOnly + vbInformation
                FindWindowHandle = lhWndP
            End If
        Case 1              ' visible
            If IsWindowVisible(lhWndP) = True Then
                ' MsgBox "VISIBLE Window Handle: " & lhWndP, vbOKOnly + vbInformation
                FindWindowHandle = lhWndP
            End If
        Case Else           ' all
            FindWindowHandle = lhWndP
            ' MsgBox "Window Handle: " & lhWndP, vbOKOnly + vbInformation
    End Select
Else
    ' MsgBox "No Window '" & PartialTitle & "…' found!", vbOKOnly + vbExclamation
End If
End Function

Private Function GetHandleFromPartialCaption(ByRef lWnd As LongPtr, _
ByVal sCaption As String) As Boolean  ' = subfunction ofFindWindowHandle()  
Dim lhWndP As LongPtr, sStr As String, GW_HWNDNEXT As Byte
GW_HWNDNEXT = 2  
GetHandleFromPartialCaption = False
lhWndP = FindWindow(vbNullString, vbNullString) 'PARENT WINDOW
Do While lhWndP <> 0
    sStr = String(GetWindowTextLength(lhWndP) + 1, Chr$(0))
    GetWindowText lhWndP, sStr, Len(sStr)
    sStr = Left$(sStr, Len(sStr) - 1)
    If InStr(1, sStr, sCaption) > 0 Then
        GetHandleFromPartialCaption = True
        lWnd = lhWndP
        Exit Do
    End If
    lhWndP = GetWindow(lhWndP, GW_HWNDNEXT)
Loop
End Function

Thus, please check what suits you better in your particular case. You may use the following test sub (just replace the apps by yours):

Sub WindowTitle()  
Dim MyAppPID As LongPtr, WndHdl As LongPtr
Dim Dummy, WinText As String, MaxChar As Long
MaxChar = 255
WinText = String(MaxChar, " ")
MyAppPID = Shell("C:\Program Files (x86)\MyPhoneExplorer\" _
    & "MyPhoneExplorer.exe action=connect flags=fromjumplist", vbHide)
'    MyAppPID = Shell("C:\Program Files (x86)\CodeTwo\QR Code Desktop" _
    & " Reader & Generator\CodeTwo QR Code Desktop Reader & Generator.exe")    
Debug.Print "PID-method:"
WndHdl = WndFromPID(MyAppPID)  ' *** not always the desired window ***
Debug.Print MyAppPID & " ——> " & WndHdl
Dummy = GetWindowText(WndHdl, WinText, MaxChar)
Debug.Print IIf(Dummy > 0, WinText, "————————")    
Debug.Print "alternative method:"
WndHdl = FindWindowHandle("MyPhone", 2)     ' 2 = all windows
'    WndHdl = FindWindowHandle("CodeTwo", 2)
Debug.Print MyAppPID & " ——> " & WndHdl
Dummy = GetWindowText(WndHdl, WinText, MaxChar)
Debug.Print IIf(Dummy > 0, WinText, "————————")    
Call Shell("C:\Windows\SysWOW64\taskkill.exe /f /pid " & MyAppPID)        
End Sub

Upvotes: 1

Fink
Fink

Reputation: 3436

To go along with Tim Williams. You can do it quite easily by using create object to get an IE object vs using a shell call. This makes it easier since you have access to the object, and don't need try to look up the window handle after the fact.

Global Const SW_MAXIMIZE = 3
Global Const SW_SHOWMINIMIZED = 2
Global Const SW_SHOWNORMAL = 1

Declare Function apiShowWindow Lib "user32" Alias "ShowWindow" _
            (ByVal hwnd As Long, ByVal nCmdShow As Long) As Long

Public Function Test()

    Dim ie As Object

    'reference "Microsoft Internet Controls (ieframe.dll)", and
    'cast ie as "InternetExplorer" if you wish to use intellisense
    Set ie = CreateObject("InternetExplorer.Application")

    ie.Visible = True

    apiShowWindow ie.hwnd, SW_MAXIMIZE
End Function

If you are dealing with multiple monitors or need more control over the window, then it gets a little more tricky. Here is one of my previous answers to address that: Is it possible to launch a browser window in VBA maximized in the current monitor?

EDIT. Set window to certain position:

Public Type RECT
   x1 As Long
   y1 As Long
   x2 As Long
   y2 As Long
End Type

Public Enum SetWindowPosFlags
     SWP_ASYNCWINDOWPOS = &H4000
     SWP_DEFERERASE = &H2000
     SWP_DRAWFRAME = &H20
     SWP_FRAMECHANGED = &H20
     SWP_HIDEWINDOW = &H80
     SWP_NOACTIVATE = &H10
     SWP_NOCOPYBITS = &H100
     SWP_NOMOVE = &H2
     SWP_NOOWNERZORDER = &H200
     SWP_NOREDRAW = &H8
     SWP_NOREPOSITION = SWP_NOOWNERZORDER
     SWP_NOSENDCHANGING = &H400
     SWP_NOSIZE = &H1
     SWP_NOZORDER = &H4
     SWP_SHOWWINDOW = &H40
End Enum

Public Enum SpecialWindowHandles
    HWND_TOP = 0
    HWND_BOTTOM = 1
    HWND_TOPMOST = -1
    HWND_NOTOPMOST = -2
End Enum


'taken from IE's ReadyState MSDN Specs
Enum READYSTATE
    READYSTATE_UNINITIALIZED = 0
    READYSTATE_LOADING = 1
    READYSTATE_LOADED = 2
    READYSTATE_INTERACTIVE = 3
    READYSTATE_COMPLETE = 4
End Enum

Declare Function SetWindowPos Lib "user32.dll" (ByVal hWnd As Long, ByVal hWndInsertAfter As SpecialWindowHandles, ByVal X As Integer, ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, ByVal uFlags As SetWindowPosFlags) As Boolean

Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Public Sub RunIt()

    Dim ie As Object
    Dim r As RECT

    Set ie = CreateObject("InternetExplorer.Application")

        'draws a 400 pixel x 400 pixel window in position 0 (top left)
    r.x1 = 0
    r.y1 = 0
    r.x2 = r.x1 + 400
    r.y2 = r.y1 + 400

        'HWND_TOP sets the Z Order to our IE Object
        'x2 - x1 ==> Width (In Pixels)
        'y2 - y2 ==> Height (In Pixels)
    SetWindowPos ie.hWnd, HWND_TOP, r.x1, r.y1, (r.x2 - r.x1), (r.y2 - r.y1), SWP_ASYNCWINDOWPOS

    ie.Visible = True
    ie.Navigate "www.google.com"

    'wait until navigated
    Do While ie.Busy Or ie.READYSTATE <> READYSTATE.READYSTATE_COMPLETE
        Sleep 60
    Loop

End Sub

Upvotes: 2

Related Questions