मैं (कुछ विस्तृत सेटअप कारणों से) टिंकर विजेट्स से वास्तविक command कॉलबैक फ़ंक्शन को पुनः प्राप्त करने का प्रयास कर रहा हूं, उदाहरण के लिए एक बटन b के लिए कॉलबैक सेट करना

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

दोनों

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

जो मुझे लगता है कि दोनों बराबर हैं

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

केवल "2277504761920<lambda\>" जैसी स्ट्रिंग लौटाएगा न कि वास्तविक कमांड फ़ंक्शन। क्या वास्तविक कॉलबैक फ़ंक्शन प्राप्त करने का कोई तरीका है?

3
JacobP 17 मार्च 2021, 10:49

3 जवाब

सबसे बढ़िया उत्तर

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

तथा

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

हम पाते हैं कि टिंकर CallWrapper वर्ग में फ़ंक्शन को लपेटता है। इसका मतलब है कि अगर हमें सभी CallWrapper ऑब्जेक्ट मिलते हैं तो हम फ़ंक्शन को पुनर्प्राप्त कर सकते हैं। @ हसिक के सुझाव का उपयोग करके बंदर को CallWrapper वर्ग के साथ काम करना आसान है, जिसके साथ काम करना आसान है, हम आसानी से सभी CallWrapper ऑब्जेक्ट प्राप्त कर सकते हैं।

यह मेरा समाधान @ हुसिक के सुझाव के साथ लागू किया गया है:

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

जैसा कि @hussic ने टिप्पणियों में कहा था कि एक समस्या है कि सूची (tk.call_wappers) को केवल जोड़ा जा रहा है। यदि आपके पास .after टिंकर लूप है, तो यह समस्या स्पष्ट हो जाएगी क्योंकि हर बार .after को एक ऑब्जेक्ट कहा जाता है जिसे सूची में जोड़ा जाएगा। इसे ठीक करने के लिए आप tk.call_wappers.clear() का उपयोग करके सूची को मैन्युअल रूप से साफ़ करना चाह सकते हैं। मैंने यह सुनिश्चित करने के लिए __slots__ सुविधा का उपयोग करने के लिए इसे बदल दिया है कि यह बहुत अधिक जगह नहीं लेता है लेकिन यह समस्या का समाधान नहीं करता है।

4
TheLizzard 18 मार्च 2021, 16:26

यह एक अधिक जटिल समाधान है। यह dict tkinterfuncs से मानों को हटाने के लिए Misc._register, Misc.deletecommand और Misc.destroy को पैच करता है। इस उदाहरण में यह जांचने के लिए कई प्रिंट हैं कि मूल्यों को जोड़ा और हटा दिया गया है।

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()
3
TheLizzard 18 मार्च 2021, 21:05

मैं किसी भी मामले की कल्पना नहीं कर सकता और मुझे बिल्कुल भी यकीन नहीं है अगर यह आपके प्रश्न का उत्तर देता है लेकिन यह शायद आप जो खोज रहे हैं उसके बराबर है:


invoke बटन का तरीका मेरे जैसा ही लगता है। तो समाधान-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()

यदि यह वह नहीं है जिसे आप ढूंढ रहे हैं तो आप फ़ंक्शन को टीसीएल स्तर पर कॉल कर सकते हैं। समाधान-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()

समाधान 3, return आपके कार्य को 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()
1
Atlas435 17 मार्च 2021, 17:38