Showing posts with label FindWindowEx. Show all posts
Showing posts with label FindWindowEx. Show all posts

Friday, 8 May 2020

Python COM Component to get windows handles hierarchy

In this blog I give a Python COM component that returns a windows handle hierarchy as found in Spy++. It returns the details in one large table.

So I had cause to poke around the windows hierarchy for Excel and I had previously written code to query the Windows API and get all the windows handles in a tree just like Spy++ but I chose to revisit the code with Python. Also, I chose to return the results in tabular form.

Here is the Python listing

import pythoncom 
import os
import logging
import win32gui
import win32con 

class LocalsEnhancedErrorMessager(object):
    @staticmethod
    def Enhance(ex, localsString):
        locals2 = "n Locals:{ " + (",n".join(localsString[1:-1].split(","))) + " }"
        if hasattr(ex,"message"):
            return "Error:" + ex.message + locals2
        else:
            return "Error:" + str(ex) + locals2


class PythonFindWindow(object):
    _reg_clsid_ = "{490784B6-5174-4794-8888-769DE4688B2C}"
    _reg_progid_ = 'PythonInVBA.PythonFindWindow'
    _public_methods_ = ['FindAllXlMainWindows','FindXlMainWindowWithCaptionFragment','FindChildWindows']
    _reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER ## uncomment this for a separate COM Exe server instead of in-process DLL

    def FindAllXlMainWindows(self):
        try:
            logging.basicConfig(filename =  (os.path.dirname(os.path.realpath(__file__))) + '\app2.log', 
                        format="%(asctime)s: %(message)s", 
                        level=logging.INFO, datefmt="%H:%M:%S")

            windows = []

            hwnd = win32gui.FindWindowEx(0,0,"XLMAIN",None)
            while hwnd != 0:
                windows.append(hwnd)
                hwnd = win32gui.FindWindowEx(0,hwnd,"XLMAIN",None)

            logging.info('PythonFindWindow.FindAllXlMainWindows completed')
            return windows
        except Exception as ex:
            msg = "PythonFindWindow.FindAllXlMainWindows error:" + LocalsEnhancedErrorMessager.Enhance(ex,str(locals()))
            logging.info(msg)
            return msg

    def FindXlMainWindowWithCaptionFragment(self, captionStringFragment):
        try:
            logging.basicConfig(filename =  (os.path.dirname(os.path.realpath(__file__))) + '\app2.log', 
                        format="%(asctime)s: %(message)s", 
                        level=logging.INFO, datefmt="%H:%M:%S")

            windows = []

            hwnd = win32gui.FindWindowEx(0,0,"XLMAIN",None)
            while hwnd != 0:
                caption = win32gui.GetWindowText(hwnd)
                if captionStringFragment in caption:
                    windows.append(hwnd)
                hwnd = win32gui.FindWindowEx(0,hwnd,"XLMAIN",None)

            logging.info('PythonFindWindow.FindXlMainWindowWithCaptionFragment completed')
            return windows
        except Exception as ex:
            msg = "PythonFindWindow.FindXlMainWindowWithCaptionFragment error:" + LocalsEnhancedErrorMessager.Enhance(ex,str(locals()))
            logging.info(msg)
            return msg


    def FindChildWindows(self, parentHandle, selectStyles):
        try:
            logging.basicConfig(filename =  (os.path.dirname(os.path.realpath(__file__))) + '\app2.log', 
                        format="%(asctime)s: %(message)s", 
                        level=logging.INFO, datefmt="%H:%M:%S")

            windows = []
            hwnd = parentHandle
            row = [hwnd,0,"{0:#0{1}x}".format(hwnd,8), 
                                win32gui.GetWindowText(hwnd), 
                                win32gui.GetClassName(hwnd),
                                win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)]
            windows.append(row)

            self.FindChildWindowsInner(parentHandle,windows, selectStyles,0)
            
            logging.info('PythonFindWindow.FindChildWindows completed')
            return windows
        except Exception as ex:
            msg = "PythonFindWindow.FindChildWindows error:" + LocalsEnhancedErrorMessager.Enhance(ex,str(locals()))
            logging.info(msg)
            return msg

    def FindChildWindowsInner(self, parentHandle, windows, selectStyles, depth):
        try:

            hwnd = win32gui.FindWindowEx(parentHandle,0,None,None)
            while hwnd != 0:
                style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
                stylesSelected = True if selectStyles is None else (style & selectStyles)!=0
                if stylesSelected:
                    row = [hwnd,parentHandle,"{0:#0{1}x}".format(hwnd,8), 
                                     win32gui.GetWindowText(hwnd), 
                                     win32gui.GetClassName(hwnd),
                                     style]
                    windows.append(row)
                    self.FindChildWindowsInner(hwnd, windows, selectStyles, depth+1)
                hwnd = win32gui.FindWindowEx(parentHandle,hwnd,None,None)
            
            
            return windows
        except Exception as ex:
            msg = "PythonFindWindow.FindChildWindowsInner error:" + LocalsEnhancedErrorMessager.Enhance(ex,str(locals()))
            logging.info(msg)
            return msg



def run():
    # this code is to be run in Microsoft Visual Studio by pressing F5
    # it is a developer's entry.  for production instantiate the COM component
    try:

        print("Executing run")
        print((os.path.dirname(os.path.realpath(__file__))))

        logging.basicConfig(filename = (os.path.dirname(os.path.realpath(__file__))) + '\app2.log', 
                        format="%(asctime)s: %(message)s", 
                        level=logging.INFO, datefmt="%H:%M:%S")

        fw = PythonFindWindow()

        xlMains = fw.FindAllXlMainWindows()
        
        windowList = fw.FindChildWindows(xlMains[0], win32con.WS_VISIBLE)

        logging.info('called PythonFindWindow.FindChildWindows ...n')

        logging.info('finishing run()n')
    except Exception as ex:
        print(ex)

def RegisterCOMServers():
    print("Registering COM servers...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(PythonFindWindow)

if __name__ == '__main__':
    
    RegisterCOMServers()
    run()

and here is some sample VBA client code...

Option Explicit

Const WS_VISIBLE As Long = &H10000000   'WS_VISIBLE = 0x10000000

Sub Test()
    Dim obj As Object
    Set obj = VBA.CreateObject("PythonInVBA.PythonFindWindow")
    
    Dim vXlMains
    vXlMains = obj.FindXlMainWindowWithCaptionFragment("MyWorkbook")
    
    Dim vWindows

    vWindows = obj.FindChildWindows(vXlMains(0), Empty)  '* No styles to select with, so selects all
    vWindows = obj.FindChildWindows(vXlMains(0), WS_VISIBLE)  '* Only shows those that are visible (and whose ancestors are visible)
    
    Dim lRow As Long
    For lRow = LBound(vWindows, 1) To UBound(vWindows, 1)
        If vWindows(lRow, 4) = "EXCEL7" Then
            Stop
        End If
    Next
    Stop
End Sub

Once the table is returned one can dig in and find what you want, much better to take a whole snapshot recursing down through the hierarchy then to piece together separate calls to FindWindow in my humble opinion.

Sunday, 14 April 2019

VBA - Windows API - Find all windows by class and then filter by process id

I needed some code to find all the top windows of Excel, that is of class XLMAIN and then filter these by process id. This is because elsewhere I am writing some C# code to acquire an instance of Excel via its window handles but all I have been given is a process id. Because I like to test logic in VBA I have written a VBA version. I am happy to share.

To begin, I use FindWindowEx to find all the windows I am interested in. All Excel instances have a main window of a class XLMAIN, and all of these have a parent window of the desktop. So supply zero for the first argument to signify the desktop and supply XLMAIN as the third argument, the fourth argument ignored. The second argument is used but varies, it allows us to loop through multiple results, we supply the previous result to get the next result. So I wrote a function to collect these to a collection, the function is called AllWindowsByClass(). The function is paramterised so that it can find windows of other class types.

Once in possession of a collection of window handles, I want to filter by a process id that I have been given. GetWindowThreadProcessId is the windows API that is best for this. In the function FilterWindowHandlesByProcessId() I loop through the a collection of handles and filter them to a new collection.

Enjoy!


Option Explicit
Option Private Module

Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWndParent As Long, _
ByVal hWndChildAfter As Long, ByVal lpszClass As String, ByVal lpszWindow As String) As Long

Private Declare Function GetWindowThreadProcessId Lib "user32.dll" (ByVal hwnd As Long, lpdwProcessId As Long) As Long

Private Function AllWindowsByClass(ByVal sClass As String) As VBA.Collection
    Dim colRet As VBA.Collection
    Set colRet = New VBA.Collection
    
    Dim hWndParent As Long
    
    hWndParent = FindWindowEx(0, 0, sClass, vbNullString)
    While hWndParent <> 0
        colRet.Add hWndParent
        hWndParent = FindWindowEx(0, hWndParent, sClass, vbNullString)
    
    Wend
    
    Set AllWindowsByClass = colRet
End Function


Private Function FilterWindowHandlesByProcessId(ByVal colWindowsHandles As VBA.Collection, ByVal lFilterProcessId As Long) As VBA.Collection
    Dim colRet As VBA.Collection
    Set colRet = New VBA.Collection

    Dim lLoop As Long
    For lLoop = 1 To colWindowsHandles.Count
        Dim lWinHandle As Long
        lWinHandle = colWindowsHandles.Item(lLoop)
    
        Dim lProcessId As Long
        GetWindowThreadProcessId lWinHandle, lProcessId
        
        If lProcessId = lFilterProcessId Then
            colRet.Add lWinHandle
        End If
    Next

    Set FilterWindowHandlesByProcessId = colRet
End Function



Here are some test functions but they have hard coded values that were valid for me and I determined whilst looking at Spy++ and other Windows diagnostic tools. Nevertheless they demonstrate how to call the above functions.



'************************************************************************************************
'* TEST FUNCTIONS
'************************************************************************************************

Private Sub TestAllWindowsByClass()
    Dim col As VBA.Collection
    Set col = AllWindowsByClass("XLMAIN")
    Debug.Assert col.Count = 2  'may differ for you!
End Sub

Private Sub TestFilterWindowHandlesByProcessId()
    
    Dim colWinHandles As VBA.Collection
    Set colWinHandles = AllWindowsByClass("XLMAIN")

    Debug.Assert colWinHandles.Count = 2

    Dim lTestProcessId As Long
    lTestProcessId = 24272 ' a process currently running on my PC, probably will differ for you!

    Dim colFiltered As VBA.Collection
    Set colFiltered = FilterWindowHandlesByProcessId(colWinHandles, lTestProcessId)

    Debug.Assert colFiltered.Count = 1
End Sub

Monday, 28 January 2019

VBA - Code to get Excel, Word, PowerPoint from window handle

The following code shows three Office applications accessible from their windows handle (please ensure you have them running before testing). They work via the Accessibility API. I had some fun writing code to find the general pattern and that generic code follows in the next post.

  1. Option Explicit
  2. Option Private Module
  3.  
  4. Private Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc.dll" ( _
  5.     ByVal hwnd As LongPtr, ByVal dwId As LongByRef riid As Any, ByRef ppvObject As ObjectAs Long
  6.  
  7. Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" _
  8.     (ByVal hWnd1 As LongByVal hwnd2 As LongByVal lpsz1 As StringByVal lpsz2 As StringAs Long
  9.  
  10. Public Function GetExcelAppObjectByIAccessible() As Object
  11.     Dim guid(0 To 3) As Long, acc As Object
  12.     guid(0) = &H20400 : guid(1) = &H0 : guid(2) = &HC0 : guid(3) = &H46000000
  13.  
  14.     Dim alHandles(0 To 2) As Long
  15.     alHandles(0) = FindWindowEx(0, 0, "XLMAIN", vbNullString)
  16.     alHandles(1) = FindWindowEx(alHandles(0), 0, "XLDESK", vbNullString)
  17.     alHandles(2) = FindWindowEx(alHandles(1), 0, "EXCEL7", vbNullString)
  18.     If AccessibleObjectFromWindow(alHandles(2), -16&, guid(0), acc) = 0 Then
  19.         Set GetExcelAppObjectByIAccessible = acc.Application
  20.     End If
  21. End Function
  22.  
  23.  
  24. Public Function GetWordAppObjectByIAccessible() As Object
  25.     Dim guid(0 To 3) As Long, acc As Object
  26.     guid(0) = &H20400 : guid(1) = &H0 : guid(2) = &HC0 : guid(3) = &H46000000
  27.  
  28.     Dim alHandles(0 To 3) As Long
  29.     alHandles(0) = FindWindowEx(0, 0, "OpusApp", vbNullString)
  30.     alHandles(1) = FindWindowEx(alHandles(0), 0, "_WwF", vbNullString)
  31.     alHandles(2) = FindWindowEx(alHandles(1), 0, "_WwB", vbNullString)
  32.     alHandles(3) = FindWindowEx(alHandles(2), 0, "_WwG", vbNullString)
  33.     If AccessibleObjectFromWindow(alHandles(3), -16&, guid(0), acc) = 0 Then
  34.         Set GetWordAppObjectByIAccessible = acc.Application
  35.     End If
  36. End Function
  37.  
  38.  
  39. Public Function GetPowerPointAppObjectByIAccessible() As Object
  40.     Dim guid(0 To 3) As Long, acc As Object
  41.     guid(0) = &H20400 : guid(1) = &H0 : guid(2) = &HC0 : guid(3) = &H46000000
  42.  
  43.     Dim alHandles(0 To 2) As Long
  44.     alHandles(0) = FindWindowEx(0, 0, "PPTFrameClass", vbNullString)
  45.     alHandles(1) = FindWindowEx(alHandles(0), 0, "MDIClient", vbNullString)
  46.     alHandles(2) = FindWindowEx(alHandles(1), 0, "mdiClass", vbNullString)
  47.     If AccessibleObjectFromWindow(alHandles(2), -16&, guid(0), acc) = 0 Then
  48.         Set GetPowerPointAppObjectByIAccessible = acc.Application
  49.     End If
  50. End Function
  51.  
  52. Sub TestGetExcelAppObjectByIAccessible()
  53.     Dim obj As Object
  54.     Set obj = GetExcelAppObjectByIAccessible()
  55.     Debug.Print obj.Name
  56. End Sub
  57.  
  58.  
  59. Sub TestGetWordAppObjectByIAccessible()
  60.     Dim obj As Object
  61.     Set obj = GetWordAppObjectByIAccessible()
  62.     Debug.Print obj.Name
  63. End Sub
  64.  
  65.  
  66. Sub TestGetPowerPointAppObjectByIAccessible()
  67.     Dim obj As Object
  68.     Set obj = GetPowerPointAppObjectByIAccessible()
  69.     Debug.Print obj.Name
  70. End Sub
  71.  
  72.  
  73.  
  74.  

We can visualise what is going on with the following output reports, first the Excel windows handles report ...

Next the Word windows handles report ...

Finally the PowerPoint windows handles report ...