pythonpython-3.xexcelwin32compyxll

How can I retrieve an Excel IRibbonUI instance in Python using win32com?


I'm trying to invalidate specific controls on an Excel add-in ribbon using Python's win32com package in order to have their callbacks re-fire.

Reading over Microsoft's Object References and the IRibbonUI documentation all I've seen recommended is to save a reference to the ribbon in its onLoad callback. This is also how I've accomplished this in the past when coding in VBA. Unfortunately, I'm not sure how to get to that point using python's win32com package.

I'm using PyXLL as a means to code the add-in using Python and for future distribution as an xll instead of xlam. By using this I'm able to access the ribbon com object in the ribbon onLoad callback, but not within the onClick callbacks of the controls included in the ribbon, since the only parameter passed into those functions is the control's com object.

Some relavent debugging info

class DispatchBaseClass:
    def __init__(self, oobj=None):
        if oobj is None:
            oobj = pythoncom.new(self.CLSID)
        elif isinstance(oobj, DispatchBaseClass):
            try:
                oobj = oobj._oleobj_.QueryInterface(
                    self.CLSID, pythoncom.IID_IDispatch
                )  # Must be a valid COM instance
            except pythoncom.com_error as details:
                import winerror

                # Some stupid objects fail here, even tho it is _already_ IDispatch!!??
                # Eg, Lotus notes.
                # So just let it use the existing object if E_NOINTERFACE
                if details.hresult != winerror.E_NOINTERFACE:
                    raise
                oobj = oobj._oleobj_
        self.__dict__["_oleobj_"] = oobj  # so we dont call __setattr__
def ribbon_loaded(ribbon):
    r_class = ribbon.__class__

    # was thinking I could pickle this class for use elsewhere
    class RibbonUI(r_class):  # also tried type(r_class)
        CLSID = ribbon.CLSID
    
    # just checking below that this would work

    # if CLSID is not set on the new class above __init__ fails
    # but, even when provided __new__ fails with the next line
    new_ribbon = RibbonUI()

    new_ribbon.InvalidateControl("ribbon_control_1")

Without setting CLSID on the new class the error is

2022-07-06 20:48:09,294 - INFO : ribbon.CLSID: IID('{000C03A7-0000-0000-C000-000000000046}')
2022-07-06 20:48:09,314 - ERROR : Error calling ribbon function 'components.ribbon.ribbon_loaded'
2022-07-06 20:48:15,364 - ERROR : Traceback (most recent call last):
2022-07-06 20:48:15,364 - ERROR :   File "C:\Users\mhill\PycharmProjects\excel-addin\components\ribbon.py", line 46, in ribbon_loaded
2022-07-06 20:48:15,365 - ERROR :     new_ribbon = RibbonUI()
2022-07-06 20:48:15,366 - ERROR :   File "c:\users\mhill\PyCharmProjects\excel-addin\.venv\lib\site-packages\win32com\client\__init__.py", line 514, in __init__
2022-07-06 20:48:15,366 - ERROR :     oobj = pythoncom.new(self.CLSID)
2022-07-06 20:48:15,366 - ERROR : pywintypes.com_error: (-2147221164, 'Class not registered', None, None)

When creating the new class and specifying the the CLSID class variable the error becomes

2022-07-06 20:48:22,429 - INFO : ribbon.CLSID: IID('{000C03A7-0000-0000-C000-000000000046}')
2022-07-06 20:48:22,429 - ERROR : Error calling ribbon function 'components.ribbon.ribbon_loaded'
2022-07-06 20:48:28,949 - ERROR : Traceback (most recent call last):
2022-07-06 20:48:28,950 - ERROR :   File "C:\Users\mhill\PycharmProjects\excel-addin\components\ribbon.py", line 46, in ribbon_loaded
2022-07-06 20:48:28,951 - ERROR :     new_ribbon = RibbonUI()
2022-07-06 20:48:28,951 - ERROR : TypeError: type.__new__() takes exactly 3 arguments (0 given)

Related question mentions something about creating an IDTExtensibility2 object, but not quite sure what to do with that.

The question

Apart from mimicking saving a global reference to the ribbon object from within the ribbon's onLoad callback as Microsoft's documentation suggests, how can I properly save a reference to the IRibbonUI com object when in the ribbon onLoad callback and be able to access it in the controls' onClick callbacks?


Solution

  • Sigh, looks like the global variable option is the recommended route

    Excerpt

    ...
    For example, in your ribbon XML you would add the onLoad action to the customUI element as follows (assuming your code is in a module named your_module.py):

    <customUI
     xmlns="http://schemas.microsoft.com/office/2009/07/customui"
     onLoad="your_module.on_load"
    >
     <!-- your ribbon definition here -->
    </customUI>
    

    Then in your module ("your_module.py" is used above) you would add the action function "on_load" that takes the IRibbonUI object. You can then save it as a global variable so that you can call the "Invalidate" method later.

    _ribbon = None
    
    def on_load(control):
        global _ribbon
        _ribbon = control
    
    # Later when you want to invalidate a control you would call
    _ribbon.InvalidateControl(control_id)