Retrieve/get back command callback function from TKinter widget Retrieve/get back command callback function from TKinter widget tkinter tkinter

Retrieve/get back command callback function from TKinter widget


Looking at tkinter.__init__.py:

class BaseWidget:    ...    def _register(self, func, subst=None, needcleanup=1):        """Return a newly created Tcl function. If this        function is called, the Python function FUNC will        be executed. An optional function SUBST can        be given which will be executed before FUNC."""        f = CallWrapper(func, subst, self).__call__        name = repr(id(f))        try:            func = func.__func__        except AttributeError:            pass        try:            name = name + func.__name__        except AttributeError:            pass        self.tk.createcommand(name, f)        if needcleanup:            if self._tclCommands is None:                self._tclCommands = []            self._tclCommands.append(name)        return name

and

class CallWrapper:    """Internal class. Stores function to call when some user    defined Tcl function is called e.g. after an event occurred."""    def __init__(self, func, subst, widget):        """Store FUNC, SUBST and WIDGET as members."""        self.func = func        self.subst = subst        self.widget = widget    def __call__(self, *args):        """Apply first function SUBST to arguments, than FUNC."""        try:            if self.subst:                args = self.subst(*args)            return self.func(*args)        except SystemExit:            raise        except:            self.widget._report_exception()

We get that tkinter wraps the function in the CallWrapper class. That means that if we get all of the CallWrapper objects we can recover the function. Using @hussic's suggestion of monkey patching the CallWrapper class with a class that is easier to work with, we can easily get all of the CallWrapper objects.

This is my solution implemented with @hussic's suggestion:

import tkinter as tktk.call_wappers = [] # A list of all of the `MyCallWrapper` objectsclass MyCallWrapper:    __slots__ = ("func", "subst", "__call__")    def __init__(self, func, subst, widget):        # We aren't going to use `widget` because that can take space        # and we have a memory leak problem        self.func = func        self.subst = subst        # These are the 2 lines I added:        # First one appends this object to the list defined up there        # the second one uses lambda because python can be tricky if you        # use `id(<object>.<function>)`.        tk.call_wappers.append(self)        self.__call__ = lambda *args: self.call(*args)    def call(self, *args):        """Apply first function SUBST to arguments, than FUNC."""        try:            if self.subst:                args = self.subst(*args)            return self.func(*args)        except SystemExit:            raise        except:            if tk._default_root is None:                raise            else:                tk._default_root._report_exception()tk.CallWrapper = MyCallWrapper # Monkey patch tkinter# If we are going to monkey patch `tk.CallWrapper` why not also `tk.getcommand`?def getcommand(name):    for call_wapper in tk.call_wappers:        candidate_name = repr(id(call_wapper.__call__))        if name.startswith(candidate_name):            return call_wapper.func    return Nonetk.getcommand = getcommand# This is the testing code:def myfunction():    print("Hi")root = tk.Tk()button = tk.Button(root, text="Click me", command=myfunction)button.pack()commandname = button.cget("command")# This is how we are going to get the function into our variable:myfunction_from_button = tk.getcommand(commandname)print(myfunction_from_button)root.mainloop()

As @hussic said in the comments there is a problem that the list (tk.call_wappers) is only being appended to. THe problem will be apparent if you have a .after tkinter loop as each time .after is called an object will be added to the list. To fix this you might want to manually clear the list using tk.call_wappers.clear(). I changed it to use the __slots__ feature to make sure that it doesn't take a lot of space but that doesn't solve the problem.


This is a more complex solution. It patches Misc._register, Misc.deletecommand and Misc.destroy to delete values from dict tkinterfuncs. In this example there are many print to check that values are added and removed from the dict.

import tkinter as tktk.tkinterfuncs = {} # name: funcdef registertkinterfunc(name, func):    """Register name in tkinterfuncs."""    # print('registered', name, func)    tk.tkinterfuncs[name] = func    return namedef deletetkinterfunc(name):    """Delete a registered func from tkinterfuncs."""    # some funcs ('tkerror', 'exit') are registered outside Misc._register    if name in tk.tkinterfuncs:        del tk.tkinterfuncs[name]        # print('delete', name, 'tkinterfuncs len:', len(tkinterfuncs))def _register(self, func, subst=None, needcleanup=1):    """Return a newly created Tcl function. If this    function is called, the Python function FUNC will    be executed. An optional function SUBST can    be given which will be executed before FUNC."""    name = original_register(self, func, subst, needcleanup)    return registertkinterfunc(name, func)def deletecommand(self, name):    """Internal function.    Delete the Tcl command provided in NAME."""    original_deletecommand(self, name)    deletetkinterfunc(name)def destroy(self):    """    Delete all Tcl commands created for    this widget in the Tcl interpreter.    """    if self._tclCommands is not None:        for name in self._tclCommands:            # print('- Tkinter: deleted command', name)            self.tk.deletecommand(name)            deletetkinterfunc(name)        self._tclCommands = Nonedef getcommand(self, name):    """    Gets the command from the name.    """    return tk.tkinterfuncs[name]original_register = tk.Misc.registertk.Misc._register = tk.Misc.register = _register original_deletecommand = tk.Misc.deletecommandtk.Misc.deletecommand = deletecommandtk.Misc.destroy = destroytk.Misc.getcommand = getcommandif __name__ == '__main__':    def f():        root.after(500, f)    root = tk.Tk()    root.after(500, f)    but1 = tk.Button(root, text='button1', command=f)    but1.pack()    but2 = tk.Button(root, text='button2', command=f)    but2.pack()    but3 = tk.Button(root, text='button3', command=lambda: print(3))    but3.pack()    print(root.getcommand(but1['command']))    print(root.getcommand(but2['command']))    print(root.getcommand(but3['command']))    but3['command'] = f    print(root.getcommand(but3['command']))    root.mainloop()


I cannot imagine any case and Im not sure at all if this answers your question but it maybe equivalent for what you are looking for:


The invoke method of the button seems pretty equivalent to me. So solution-1 would be:

import tkinter as tkdef hi():    print('hello')root = tk.Tk()b = tk.Button(root, text='test', command=hi)b.pack()cmd = b.invoke#cmd = lambda :b._do('invoke')root.mainloop()

If this isnt what you looking for you could call the function in tcl level. Solution-2:

import tkinter as tkdef hi():    print('hello')root = tk.Tk()b = tk.Button(root, text='test', command=hi)b.pack()cmd = lambda :root.tk.call(b['command'])#cmd= lambda :root.tk.eval(b['command'])cmd()root.mainloop()

Solution 3, would be to return your function by invoke:

import tkinter as tkdef hi():    print('hello')    return hiroot = tk.Tk()b = tk.Button(root, text='test', command=hi)b.pack()cmd = b.invoke()print(cmd) #still a string but comparableroot.mainloop()