Tkinter - test create button, get command value [duplicate] - python

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?

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

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

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.

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'>

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

Related

How to get the value of an Entry created in a def?

I'm working on a project and i would like to get the Value of an Entry created in a def (turned on by a button on Tkinter)
So I have my main tkinter menu, with a button which will call the def "panier".
The def "panier" is creating the Entry "value" and another button to call a second def "calcul".
The second def "calcul" will do things with the value of Entry...
But then, in the def "calcul", when i'm trying to do value.get() it tells "NameError: name 'value' is not defined"
Here is the code, btw the Entry must be created by the def...
from tkinter import *
def panier():
value=Entry(test)
value.pack()
t2=Button(test,text="Validate",command=calcul)
t2.pack()
def calcul(value):
a=value.get()
#here will be the different calculations I'll do
test=Tk()
t1=Button(test,text="Button",command=panier)
t1.pack()
test.mainloop()
Appreciate every feedback :)
You can make the variable global like this:
from tkinter import *
def panier():
global value
value = Entry(test)
value.pack()
t2 = Button(test, text="Validate", command=calcul)
t2.pack()
def calcul():
a = value.get()
print(a)
#here will be the different calculations I'll do
test = Tk()
t1 = Button(test, text="Button", command=panier)
t1.pack()
test.mainloop()
The global value line makes the variable global so you can use it anywhere in your program.
You can also pass in the variable as an argument like what #JacksonPro suggested
t2 = Button(test, text="Validate", command=lambda: calcul(value))
This is one way to do it. Globally create a collection (list or dictionary) to hold a reference to the Entry. When you create the Entry, add it to the collection. I made it with either a list or dictionary for holding the references, so toggle the commented variations in all three places to try it both ways.
import tkinter as tk
def panier():
for item in ('value', ):
ent = tk.Entry(test)
collection.append(ent)
# collection[item] = ent
ent.pack()
t2 = tk.Button(test,text="Validate",command=calcul)
t2.pack()
def calcul():
a = collection[0].get()
# a = collection['value'].get()
print(a)
collection = []
# collection = {}
test = tk.Tk()
t1 = tk.Button(test, text="Button", command=panier)
t1.pack()
test.mainloop()

Passing arguments down in Tkinter to a method

I'm struggling to get my method working correctly. I've thought about using a lambda function which I did for another problem and that worked, however here it does not seem to work. I'm trying to change the functions to methods and for some reason, my method is not working correctly since it has no reference to the tree. I've tried using a lambda function although that does not work.
My error:
NameError: name 'tree' is not defined
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib/python3.8/tkinter/__init__.py", line 1883, in __call__
return self.func(*args)
File "/home/bitvivaz/Documents/Software Development/Python/PasswordManager/mainFrame.py", line 54, in select
print([tree.item(x) for x in tree.selection()])
NameError: name 'tree' is not defined
Here is my code:
import tkinter as tk
import tkinter.ttk as ttk
from encryption import encrypted_password, decrypt_password
import backend as db
def get_credentials(tree):
'''Retrieves all credentials from the database and inserts it into the tree widget'''
for row in db.show_credentials():
tree.insert("", 'end', text=row['name'], values=(
row['username'], decrypt_password(row['password'])))
class MainframeApp:
def __init__(self, master=None):
# build ui
frame_main = ttk.Frame(master)
frame_main.config(height='600', width='600')
frame_main.grid()
# Creates tree widget
tree = ttk.Treeview(frame_main)
tree["columns"] = ("one", "two")
tree.column("#0")
tree.column("one")
tree.column("two")
tree.heading("#0", text="Website")
tree.heading("one", text="Username")
tree.heading("two", text="Password")
tree.grid(padx='5', pady='5', rowspan='20')
get_credentials(tree)
tree.bind("<<TreeviewSelect>>", self.select, "+")
button_add = ttk.Button(frame_main)
button_add.config(text='Add')
button_add.grid(column='1', padx='5', row='0')
button_delete = ttk.Button(frame_main)
button_delete.config(text='Delete')
button_delete.grid(column='1', padx='5', row='1')
button_logout = ttk.Button(frame_main)
button_logout.config(text='Logout')
button_logout.grid(column='1', padx='5', row='2')
# Main widget
self.mainwindow = frame_main
def select(self, e):
print([tree.item(x) for x in tree.selection()])
def run(self):
self.mainwindow.mainloop()
if __name__ == '__main__':
root = tk.Tk()
root.title("Password Manager")
app = MainframeApp(root)
app.run()
When you make use of a class structure, certain variables are available across methods (class or instance variables), others are not (local variables).
In your case, you need to define variables you need across methods as instance variables, that is, rather than:
tree = ttk.Treeview(frame_main)
You declare:
self.tree = ttk.Treeview(frame_main)
Then you can reference the variable across methods as self.tree.

tkinter is taking a different master than given

I have a Toplevel widget which asks the User what widget he want to spawn and then asks for cnf. Whatfor and stuff is not important.
The cnf will be asked in an Scrollframe-Table-something (I don't really know how to describe it ^^'). Extra for that Scrollframe and its Scrollbar I made a Frame, so I can easily pack it left and right. But somehow the Scrollframe is taking the Tk window (root of my toplevel) as master.
Here is the code - I can't find my mistake:
from tkinter import _cnfmerge as cnfmerge
from tkinter import *
class Scrollframe(Frame):
def __init__(self,master=None,height=200,width=200,**kw):
if 'yscrollcommand' in kw:
self.ysc=kw['yscrollcommand']
del kw['yscrollcommand']
else: ysc=None
if 'pad' in kw:
self.pad=kw['pad']
del kw['pad']
else: self.pad=0
Frame.__init__(self,height=height,width=width)
self.scrollframe=Frame(self,**kw)
self.scrollframe.place(x=0,y=0,relwidth=1)
self.config(bg=self.scrollframe['bg'])
self.bind('<Configure>',self.adopt)
self.widgets,self.scrollable={},False
def adopt(self,event=None):
if self.scrollframe.winfo_height()>self.winfo_height():
self.scrollable=True
self.scrollframe.place(y=0)
self.ysc(0,0)
else:
self.scrollable=False
self.ysc(0,1)
def addItem(self,widget=None,cnf={},**kw):
if widget:
cnf=cnfmerge((cnf,kw))
if 'width' in cnf: del cnf['width']
obj=widget(self.scrollframe,cnf)
if len(self.widgets)==0 and self.pad!=0: obj.pack(fill=X)
else: obj.pack(fill=X,pady=(self.pad,0))
id_=str(id(obj))+widget.__name__
obj.bind('<Destroy>',lambda event: self.delItem(id_),'+')
self.widgets[id_]=obj
return id_
def getItem(self,id):
return self.widgets[id]
def delItem(self,id):
try: self.widgets[id].destroy()
except TclError: del self.widgets[id]
except KeyError: pass
def yview(self,*args):
try: delta=int(args[1])
except ValueError: delta=float(args[1])
maxnegscroll=self.winfo_height()-self.scrollframe.winfo_height()
if isinstance(delta,float):
if maxnegscroll<0: self.scrollframe.place(y=int(maxnegscroll*delta))
delta=abs(int(self.scrollframe.place_info()['y'])/maxnegscroll)
self.ysc(delta,delta)
else:
delta=-delta*3
if int(self.scrollframe.place_info()['y'])+delta<maxnegscroll: self.scrollframe.place(y=maxnegscroll)
elif int(self.scrollframe.place_info()['y'])+delta>0: self.scrollframe.place(y=0)
else: self.scrollframe.place(y=int(self.scrollframe.place_info()['y'])+delta)
delta=abs(int(self.scrollframe.place_info()['y'])/maxnegscroll)
self.ysc(delta,delta)
class CreateWindow(Toplevel):
def __init__(self,master=None):
Toplevel.__init__(self,master,height=458,width=400)
self.grab_set()
self.resizable(False,False)
self.title('Neues Item')
self.vars,self.cnf,self.cnfids={},{},{}
cnf=create_dict(bg='gainsboro',width=380)
Frame(self,cnf=cnf,height=39).place(x=10,y=10)
Frame(self,cnf=cnf,height=103).place(x=10,y=59)
Frame(self,cnf=cnf,height=220).place(x=10,y=172)
bottom=Frame(self,cnf=cnf,height=46)
bottom.pack_propagate(False)
bottom.place(x=10,y=402)
var,values,self.oldwidget=StringVar(value='Frame'),list(_tkinter_widgets.keys())[2:],'Frame'
for i in range(len(values)): values[i]=values[i].__name__
Spinbox(self,values=values,textvar=var,state=READONLY,cursor='arrow',command=self.refresh,buttonuprelief=FLAT,buttondownrelief=FLAT,wrap=True).place(x=20,y=20)
self.vars['widget']=var
Label(self,text='Höhe:',bg='gainsboro',anchor=W,bd=1).place(x=20,y=69)
var=StringVar()
Entry(self,textvar=var,justify=CENTER,width=40).place(x=136,y=69)
self.vars['height']=var
Label(self,text='Breite:',bg='gainsboro',anchor=W,bd=1).place(x=20,y=98)
var=StringVar()
Entry(self,textvar=var,justify=CENTER,width=40).place(x=136,y=98)
self.vars['width']=var
var=BooleanVar(value=True)
Checkbutton(self,onvalue=True,offvalue=False,text='Farbe übernehmen (falls vorhanden)',variable=var,cursor='hand2',bg='gainsboro',activebackground='gainsboro').place(x=20,y=127)
self.vars['takecolor']=var
cnfsframe=Frame(self,height=200,width=360)
cnfsframe.pack_propagate(0)
cnfsframe.place(x=20,y=182)
sb=Scrollbar(cnfsframe)
sb.pack(fill=Y,side=RIGHT)
self.cnfs=Scrollframe(master=cnfsframe,width=360-17,height=200,yscrollcommand=sb.set)
self.cnfs.pack(fill=Y,side=LEFT)
sb.config(command=self.cnfs.yview)
for arg in _tkinter_widgets[Frame]:
id=self.cnfs.addItem(Frame,height=19,width=360)
obj=self.cnfs.getItem(id)
var=StringVar()
Entry(obj,width=35,justify=CENTER,textvar=var).place(x=146,y=0)
Label(obj,text=arg,bd=1).place(x=0,y=0)
self.cnf[arg],self.cnfids[arg]=var,id
Button(bottom,text='Bestätigen',command=self.confirm,width=12,height=1).pack(side=LEFT,padx=10,pady=10)
Button(bottom,text='Abbrechen',command=self.destroy,width=12,height=1).pack(side=RIGHT,padx=(0,10),pady=10)
def refresh(self):
self.vars['height'].set(''),self.vars['width'].set(''),self.vars['takecolor'].set(True)
for arg in _tkinter_widgets[eval(self.oldwidget)]:
self.cnfs.delItem(self.cnfids[arg])
del self.cnfids[arg],self.cnf[arg]
for arg in _tkinter_widgets[eval(self.vars['widget'].get())]:
id=self.cnfs.addItem(Frame,height=19,width=360)
obj=self.cnfs.getItem(id)
obj.pack_propagate(False)
var=StringVar()
Entry(obj,width=35,justify=CENTER,textvar=var).pack(side=RIGHT)
Label(obj,text=arg,bd=1).pack(fill=X,side=LEFT)
self.cnf[arg],self.cnfids[arg]=var,id
self.oldwidget=self.vars['widget'].get()
self.focus()
def confirm(self):
raise NotImplementedError #first I'll have to fix that scrollframe issue xD
if __name__=='__main__':
t=Tk()
cw=CreateWindow(t)
Before someone asks what self.scrollable in Scrollframe is for: Its for the MouseWheel binding I'll implement later.
In this line, you are not passing the master to the super-class' __init__:
Frame.__init__(self,height=height,width=width)
Just change it to:
Frame.__init__(self,master=master, height=height,width=width)
That said, it is a general Python recommendation to use super() instead of hardcoding the superclass name:
super().__init__(master=master, height=height, width=width)

How to call function from external source from button. [Python]

I've been desperately trying to get this section of code to work in my program. I essentially want to read in several options from a file, and create Tkinter buttons from those options. Creating the buttons is no issue; currently, I just can't make the code run the functions I want.
from Lib import StegosaurMainCode as Steg
...
class App:
def __init__(self, master, menu):
buttons = []
for counter in range(0, len(menu[0])):
text = menu[0][counter]
func = menu[1][counter]
att = menu[2][counter]
buttons.append(Button(text=text, command=lambda: Steg.func(att)))
frame = Frame(master)
for item in buttons:
item.pack()
frame.pack()
In this class, func is the function I want to call, Steg is the external code in another file, and att are the attributes for the function. I can't seem to figure out why Steg.func won't tries to call a function in Steg called "func" rather than the one described in the variable func
Have your lambda rebind its att parameter at each call.
class App:
def __init__(self, master, menu):
buttons = []
for counter in range(0, len(menu[0])):
text = menu[0][counter]
func = menu[1][counter]
att = menu[2][counter]
buttons.append(Button(text = text, command = lambda att = att: Steg.func(att)))
frame = Frame(master)
for item in buttons:
item.pack()
frame.pack()
Assuming that menu[1][counter] contains a string rather than a reference to an actual function, you need to get a reference to the function which you can then use as the value for the command attribute. You can do that with getattr:
func = getattr(steg, menu[1][counter])
Once you've done that, you can use func as if it were an actual function. However, you need to bind the variables to their current values, so you need to pass them as arguments to the lambda:
button = Button(text=text, command=lambda func=func, attr=att: func(att)))

Is there an analog to tk.IntVar() in wxPython?

I'm converting an old tkinter program to wxPython. One of the things from tk that I used liberally was tk.IntVar() and the like. Is there anything in wx that provides similar functionality?
Specifically, I'd like to be able to define module-level variables such as myvar = tk.StringVar(). Then when those variables are updated, have one or more UI elements update based on the new variable value just like what would happen with:
self.score = tk.Entry(self, textvariable=myvar.get())
here is how you would normally organize your app .... globals tend to be a bad idea
class MyNestedPanel(wx.Panel):
def __init__(self,*a,**kw):
...
self.user = wx.TextCtrl(self,-1)
def SetUser(self,username):
self.user.SetValue(username)
class MyMainPanel(wx.Panel):
def __init__(self,*a,**kw):
...
self.userpanel = MyNestedPanel(self,...)
def SetUsername(self,username):
self.userpanel.SetUser(username)
class MainFrame(wx.Frame):
def __init__(self,*a,**kw):
...
self.mainpanel = MyMainPanel(self,...)
def SetUsername(self,username):
self.mainpanel.SetUsername(username)
a = wx.App()
f = MainFrame(...)
f.Show()
a.MainLoop()
although you can make helper functions
def set_widget_value(widget,value):
if hasattr(widget,"SetWidgetValue"):
return widget.SetWidgetValue(value)
if isinstance(widget,wx.Choice):
return widget.SetStringSelection(value)
if hasattr(widget,"SetValue"):
return widget.SetValue(value)
if hasattr(widget,"SetLabel"):
return widget.SetLabel(value)
else:
raise Exception("Unknown Widget Type : %r"%widget)
def get_widget_value(widget):
if hasattr(widget,"GetWidgetValue"):
return widget.GetWidgetValue()
if isinstance(widget,wx.Choice):
return widget.GetStringSelection()
if hasattr(widget,"GetValue"):
return widget.GetValue()
if hasattr(widget,"GetLabel"):
return widget.GetLabel()
else:
raise Exception("Unknown Widget Type : %r"%widget)
class WidgetManager(wx.Panel):
def __init__(self,parent):
self._parent = parent
wx.Panel.__init__(self,parent,-1)
self.CreateWidgets()
def CreateWidgets(self):
#create all your widgets here
self.widgets = {}
def SetWidgetValue(self,value):
if isinstance(value,dict):
for k,v in value.items():
set_widget_value(self.widgets.get(k),v)
else:
raise Exception("Expected a dictionary but got %r"%value)
def GetWidgetValue(self):
return dict([(k,get_widget_value(v))for k,v in self.widgets])
and then use them like this https://gist.github.com/joranbeasley/37becd81ff2285fcc933

Categories