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