4

I am (for some elaborate setup reasons) trying to retrieve the actual command callback function from tkinter widgets, for example setting up a callback for a button b

import tkinter as tk
root = tk.Tk()
b = tk.Button(root, text='btn', command=lambda:print('foo'))

both

b['command']
b.cget('command')

which I think both are equivalent to

b.tk.call(b._w, 'cget', '-command')

will only return a string like "2277504761920<lambda\>" and not the actual command function. Is there a way to get the actual callback function?

8
  • 1
    Store the function in a list? And then just call it, or a variable is fine, since its lambda. Commented Mar 17, 2021 at 8:16
  • 1
    Found a solution. I will type the answer now Commented Mar 17, 2021 at 8:24
  • 3
    I guess you just could use b.invoke which is equivalent to the command. Commented Mar 17, 2021 at 8:34
  • 1
    @Saad that is why I had to go through python's memory using the gc library and find all of the CallWrapper object out of it. It still have a problem with there being multiple CallWrappers attached to 1 widget. Commented Mar 17, 2021 at 8:58
  • 1
    @TheLizzard: I like your solution but it can be simpler. Commented Mar 17, 2021 at 9:00

6 Answers 6

4

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 tk

def 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 tk

def 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 tk

def hi():
    print('hello')
    return hi

root = tk.Tk()
b = tk.Button(root, text='test', command=hi)
b.pack()
cmd = b.invoke()
print(cmd) #still a string but comparable
root.mainloop()
Sign up to request clarification or add additional context in comments.

Comments

3

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 tk

tk.tkinterfuncs = {} # name: func

def registertkinterfunc(name, func):
    """Register name in tkinterfuncs."""
    # print('registered', name, func)
    tk.tkinterfuncs[name] = func
    return name

def 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 = None

def getcommand(self, name):
    """
    Gets the command from the name.
    """
    return tk.tkinterfuncs[name]


original_register = tk.Misc.register
tk.Misc._register = tk.Misc.register = _register 
original_deletecommand = tk.Misc.deletecommand
tk.Misc.deletecommand = deletecommand
tk.Misc.destroy = destroy
tk.Misc.getcommand = getcommand

if __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()

7 Comments

btw there are 2 definitions of _register in __init__.py. The is also one in Variable._register you only override Misc._register.
You might want to remove the (debug?) prints
The _register in Variable is used by trace_add. ("Define a trace callback for the variable."). If you need the variable trace, it should be patched also. Yes the prints are for debug, can be removed.
Why don't you define a function (getcommand) and patch it in tkinter to make it easier to get the function? Also can you file it as a feature request to python?
Using a dict (tkinterfuncs) is easy to find func from name (es. tkinterfuncs[but1['command']]). But you are right, can be embedded in tk as you did.
|
2

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 tk

tk.call_wappers = [] # A list of all of the `MyCallWrapper` objects

class 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 None

tk.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.

14 Comments

Seems pretty deep, think tkinter needs to add something for this built in.
@CoolCloud I agree but most of the time you don't need to use it.
Thanks for the solution, this works but is a bit expensive so just storing the obj reference for now, but accepting it until there is another way to achieve it.
You can override tk.CallWrapper with your own class, it is an hack but it works.
@hussic That is a great idea. I hope you don't mind me using it to change my answer
|
2

When you assign a command to a widget, or bind a function to an event, the python function is wrapped in a tkinter.CallWrapper object. That wrapper contains a reference to the python function along with a reference to the widget. To get a callback for a widget you can iterate over the instances of the wrapper in order to get back the original function.

For example, something like this might work:

import tkinter as tk
import gc

def get_callback(widget):
    for obj in gc.get_objects():
        if isinstance(obj, tk.CallWrapper) and obj.widget == widget:
            return obj.func
    return None

You can then directly call the return value of this function. Consider the following block of code:

import tkinter as tk
import gc

def get_callback(widget):
    for obj in gc.get_objects():
        if isinstance(obj, tk.CallWrapper) and obj.widget == widget:
            return obj.func

def do_something():
    print(f"button1: {get_callback(button1)} type: {type(get_callback(button1))}")
    print(f"button2: {get_callback(button2)} type: {type(get_callback(button2))}")

root = tk.Tk()
button1 = tk.Button(root, text="do_something", command=do_something)
button2 = tk.Button(root, text="lambda", command=lambda: do_something())
button1.pack(padx=20, pady=20)
button2.pack(padx=20, pady=20)

root.mainloop()

When I click either button, I see this in the console output which proves that the get_callback method returns a callable.

button1: <function do_something at 0x103386040> type: <class 'function'>
button2: <function <lambda> at 0x103419700> type: <class 'function'>

2 Comments

Wouldn't that loop through all of the objects that are in memory including built in functions like print, str, ... as they are also technically objects gc keeps a track of? So if I had a lot of objects then wouldn't your method be slow? It's still going to be very fast but slower than @hussic's solution.
@TheLizzard: yes, it loops over every object. No, it's not slow unless you have millions of objects. I just tested it with 10,000 buttons with callbacks, resulting in over 77,000 python objects to be checked, and it ran in .0016 seconds. That seems plenty fast enough for any real-world scenario.
0

Button is a object you can assign attributes just define your function outside the button and assign the function ass a attribute

func_print = lambda: print("nice")
x = Button(..., command=func_print)
x.my_func = func_print

def something():
    x.my_func()

something()

>>> nice

I was looking same problem but I could not find any nice answer then I created mine actually it is very easy

Comments

0

#get function command

# Ante da instrução super() eu pego a função no argumento nomeado kw["command"] e manipulo a meu gosto.

from tkinter import *
from threading import Thread
class BtnCuston(Button):
    def __int__(self,master,**kw):
        self.__function = kW.get['command']
        kW['command']=self.executarcommandParalelo
        super().__init__(master,**kw)
    def getcommand(self):
        return self.__function
    def executarcommandParalelo(self):
        #Caso de uso se a função for longa demais
        self.th = Thread(target=self.__function)
        self.th.start()

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.