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.

No comments:

Post a Comment