简体   繁体   English

从 TKinter 小部件检索/取回命令回调 function

[英]Retrieve/get back command callback function from TKinter widget

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我(出于一些精心设置的原因)试图从 tkinter 小部件中检索实际command回调 function,例如为按钮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?只会返回类似"2277504761920<lambda\>"的字符串,而不是实际的命令 function。有没有办法获得实际的回调 function?

Looking at tkinter.__init__.py :看着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.我们得到 tkinter 将 function 包装在CallWrapper class 中。 That means that if we get all of the CallWrapper objects we can recover the function.这意味着如果我们获得所有CallWrapper对象,我们就可以恢复 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.使用@hussic 的猴子建议使用更易于使用的 class 修补CallWrapper class,我们可以轻松获取所有CallWrapper对象。

This is my solution implemented with @hussic's suggestion:这是我根据@hussic 的建议实施的解决方案:

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.正如@hussic 在评论中所说,存在列表( tk.call_wappers )仅被附加到的问题。 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.如果您有一个.after tkinter 循环,则问题将很明显,因为每次.after被称为 object 将被添加到列表中。 To fix this you might want to manually clear the list using tk.call_wappers.clear() .要解决此问题,您可能需要使用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.我将其更改为使用__slots__功能以确保它不会占用大量空间,但这并不能解决问题。

This is a more complex solution.这是一个更复杂的解决方案。 It patches Misc._register , Misc.deletecommand and Misc.destroy to delete values from dict tkinterfuncs.它修补Misc._registerMisc.deletecommandMisc.destroy以从dict tkinterfuncs 中删除值。 In this example there are many print to check that values are added and removed from the dict.在这个例子中,有很多 print 来检查是否在字典中添加和删除了值。

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

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.按钮的invoke方法似乎与我相当。 So solution-1 would be:所以解决方案1将是:

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.如果这不是您要找的,您可以在 tcl 级别调用 function。 Solution-2 :解决方案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 :解决方案 3 ,将通过invoke return您的 function:

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

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. 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.该包装器包含对 python function 的引用以及对小部件的引用。 To get a callback for a widget you can iterate over the instances of the wrapper in order to get back the original function.要获取小部件的回调,您可以遍历包装器的实例以取回原始 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.然后就可以直接调用这个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.当我单击任一按钮时,我会在控制台 output 中看到它,这证明get_callback方法返回一个可调用对象。

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

Button is a object you can assign attributes just define your function outside the button and assign the function ass a attribute按钮是一个 object 你可以分配属性只需在按钮外定义你的 function 并为 function 分配一个属性

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我一直在寻找同样的问题,但我找不到任何好的答案然后我创建了我的实际上这很容易

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM