Search code examples
excelwinapicompseudocode

How to reference the COM objects of all the running Excel application instances, including hidden and without workbooks?


How can I get a complete list of references to each running Excel application instance (regardless of their amount of workbooks and visibility state)?


I know that I can use the Windows API to find each Excel workbook window (which has the window class name EXCEL7), get their handles to use with the AccessibleObjectFromWindow function, then dispatch and get the application COM object.

Though that only works with the Excel application instances that have at least one workbook visible. How can I also get the Excel application instances that are hidden and/or don't have workbooks?

The Excel application instance window (which has the window class name XLMAIN) doesn't retrieve any accessible object.

I'm looking for an explanation, with or without pseudocode, or code of any programming language, as long as I'm able to understand and implement it myself (in Python).


Solution

  • I wanted to implement this in Python, though I asked the question without requiring the answer to be about Python since a general explanation (not having to be specific to a programming language) would probably be enough.

    After looking at the source code of the GetObject VB function implemented in Python by the win32com.client module I noticed that it calls the Moniker function:

    def Moniker(Pathname, clsctx = pythoncom.CLSCTX_ALL):
      """
        Python friendly version of GetObject's moniker functionality.
      """
      moniker, i, bindCtx = pythoncom.MkParseDisplayName(Pathname)
      dispatch = moniker.BindToObject(bindCtx, None, pythoncom.IID_IDispatch)
      return __WrapDispatch(dispatch, Pathname, clsctx=clsctx)
    

    The MkParseDisplayName function lead me to the objbase.h header's functions where I found the GetRunningObjectTable function which I wasn't aware of.

    After a while of searching multiple pieces of code about it and trying to put them together to do what I wanted without raising errors and making sure it only gets the Excel application instances (I added Microsoft Word to the code to show how to do it with other COM objects) without repeating, I put together the code below.

    from pythoncom import CreateBindCtx as create_bind_context, GetRunningObjectTable as get_running_object_table, IID_IDispatch as dispatch_interface_iid
    from win32com.client import Dispatch as dispatch
    
    running_object_table = get_running_object_table()
    bind_context = create_bind_context()
    excel_application_class_clsid = '{00024500-0000-0000-C000-000000000046}'
    word_application_class_clsid = '{000209FF-0000-0000-C000-000000000046}'
    excel_application_clsid = '{000208D5-0000-0000-C000-000000000046}'
    word_application_clsid = '{00020970-0000-0000-C000-000000000046}'
    excel_applications = []
    
    for moniker in running_object_table:
      name = moniker.GetDisplayName(bind_context, None)
      if all(clsid not in name for clsid in [excel_application_class_clsid, word_application_class_clsid]):
        continue
      unknown_com_interface = running_object_table.GetObject(moniker)
      dispatch_interface = unknown_com_interface.QueryInterface(dispatch_interface_iid)
      dispatch_clsid = str(dispatch_interface.GetTypeInfo().GetTypeAttr().iid)
      if dispatch_clsid not in [excel_application_clsid, word_application_clsid]:
        continue
      com_object = dispatch(dispatch=dispatch_interface)
      excel_application = com_object.Application
      if id(excel_application) not in [id(excel_application) for excel_application in excel_applications]:
        excel_applications.append(excel_application)
    
    input(excel_applications)
    

    The if checks was the way I found to filter out what I don't want, though I'm not sure if that's a good way to do it.

    The pywin32 package's documentation (which contains the win32com module as well as the documentation of the pythoncom module) has helped me a lot and together with the Windows API documentation I have learned quite a bit more about COM.

    It should be simple to see what's used in the code above for anyone wanting to do this in another programming language. Here's a list of the main things to help: GetRunningObjectTable function, CreateBindCtx function, IMoniker::GetDisplayName method, IRunningObjectTable::GetObject method, IUnknown::QueryInterface method, IDispatch::GetTypeInfo method and ITypeInfo::GetTypeAttr method.


    Function specific for Excel instances without Word:

    from pythoncom import (
      CreateBindCtx         as create_bind_context_com_interface,
      IID_IDispatch         as dispatch_com_interface_iid,
      GetRunningObjectTable as get_running_object_table_com_interface,
    )
    from win32com.client import (
      Dispatch as dispatch,
    )
    
    def get_excel_instances():
      '''
      Returns a list of the running Microsoft Excel application
      instances as component object model (COM) objects.
      '''
      running_object_table_com_interface = get_running_object_table_com_interface()
      bind_context_com_interface = create_bind_context_com_interface()
      excel_application_class_clsid = '{00024500-0000-0000-C000-000000000046}'
      excel_application_clsid = '{000208D5-0000-0000-C000-000000000046}'
      excel_instance_com_objects = []
      for moniker_com_interface in running_object_table_com_interface:
        display_name = moniker_com_interface.GetDisplayName(bind_context_com_interface, None)
        if excel_application_class_clsid not in display_name:
          continue
        unknown_com_interface = running_object_table_com_interface.GetObject(moniker_com_interface)
        dispatch_com_interface = unknown_com_interface.QueryInterface(dispatch_com_interface_iid)
        dispatch_clsid = str(object=dispatch_com_interface.GetTypeInfo().GetTypeAttr().iid)
        if dispatch_clsid != excel_application_clsid:
          continue
        excel_instance_com_object = dispatch(dispatch=dispatch_com_interface)
        excel_instance_com_objects.append(excel_instance_com_object)
      return excel_instance_com_objects
    
    excel_instances = get_excel_instances()
    input()