How to replace lambda callback handlers with callable class object callback handlers? - python

My following question is related to example 8-10 of Programming Python, 4th ed by Mark Lutz. The example is about creating a bar of simple buttons that launch dialog demos and return the values of those dialog calls in stdout.
The dialog table looks like the following:
#dialogTable.py
from tkinter.filedialog import askopenfilename
from tkinter.colorchooser import askcolor
from tkinter.messagebox import askquestion, showerror
from tkinter.simpledialog import askfloat
demos = {
'Open': askopenfilename,
'Color': askcolor,
'Query': lambda: askquestion('Warning', 'You typed "rm *"\nConfirm?'),
'Error': lambda: showerror('Error!', "He's dead, Jim"),
'Input': lambda: askfloat('Entry', 'Enter credit card number')
}
The code below creates the bar of buttons and makes them functional. My question relates to this part:
from tkinter import *
from dialogTable import demos # button callback handlers
from quitter import Quitter # attach a quit object to me
class Demo(Frame):
def __init__(self, parent=None, **options):
Frame.__init__(self, parent)
self.pack()
Label(self, text="Basic demos").pack()
for key in demos:
func = (lambda key=key: self.printit(key))
Button(self, text=key, command=func).pack(side=TOP, fill=BOTH)
Quitter(self).pack(side=TOP, fill=BOTH)
def printit(self, name):
print(name, 'returns =>', demos[name]()) # fetch, call, print
if __name__ == '__main__': Demo().mainloop()
Quitter is just a class for the quit button.
My question is how do I rewrite this code to use a callable class object (__call__) instead of a lambda to defer the call to the actual handler?

Well, in this case, you could do something to the effect of:
from tkinter import *
from dialogTable import demos # button callback handlers
from quitter import Quitter # attach a quit object to me
class Wrapper:
def __init__(self, func, key):
self.func = func
self.key = key
def __call__(self):
return self.func(self.key)
class Demo(Frame):
def __init__(self, parent=None, **options):
Frame.__init__(self, parent)
self.pack()
Label(self, text="Basic demos").pack()
for key in demos:
func = Wrapper(self.printit, key)
Button(self, text=key, command=func).pack(side=TOP, fill=BOTH)
Quitter(self).pack(side=TOP, fill=BOTH)
def printit(self, name):
print(name, 'returns =>', demos[name]()) # fetch, call, print
if __name__ == '__main__': Demo().mainloop()

Related

Reloading the specified function from separate files

In file a there is a button that calls the execution of a function from file b and it is removed. Then another button is created that should reset gui to its original state.
Unfortunately, I don't know how to properly configure it to make it work.
In trying to fix it, you ended up with the following errors:
ImportError: cannot import name 'GUI' from partially initialized module 'b' (most likely due to a circular import) (c:\Users\user\Desktop\english_app\b.py)
TypeError: back() missing 1 required positional argument: 'root'
file a:
from tkinter import *
from b import dest
from functools import partial
class Gui:
def __init__(self, root):
self.b = Button(root, text='destroy', command=partial(dest, root))
self.b.pack()
if __name__ == "__main__":
root = Tk()
Gui(root)
mainloop()
file b:
from tkinter import *
class dest:
def __init__(self, root):
for widget in root.winfo_children():
widget.destroy()
self.b_back = Button(root, text="Back", command=self.back)
self.b_back.pack()
def back(self, root):
for widget in root.winfo_children():
widget.destroy()
Gui(root)
Need to use import statement without the from to avoid circular imports.
For example:
file a.py:
from tkinter import *
import b
from functools import partial
class Gui:
def __init__(self, root):
self.b = Button(root, text='destroy', command=partial(b.dest, root))
self.b.pack()
if __name__ == "__main__":
root = Tk()
Gui(root)
mainloop()
file b.py:
from tkinter import *
import a
class dest:
def __init__(self, root):
for widget in root.winfo_children():
widget.destroy()
self.b_back = Button(root, text="Back", command=self.back(root))
self.b_back.pack()
def back(self, root):
for widget in root.winfo_children():
widget.destroy()
a.Gui(root)

Tkinter passing callback function result to another class

I wonder how to pass return value from one class to another class in tkinter.
In my program I have DataChosenForm class where I want to choose option in Combobox and pass this result to another class ReturnData to set a variable in a Label.
import tkinter as tk
from tkinter import ttk
class DataChosenForm(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
chosen = tk.LabelFrame(self, text="wybór")
chosen.grid(row=0)
self.combo = ttk.Combobox(chosen)
self.combo['values'] = ('wizz', 'ryanair', 'lot')
self.combo.grid(row=0, column=2, padx=80, pady=10)
self.combo.bind("<<ComboboxSelected>>", self.callback)
def callback(self, event=None):
if event.widget.get() == 'wizz':
print('wizz')
return 'wizz'
elif event.widget.get() == 'ryanair':
print('ryanair')
return 'ryanair'
elif event.widget.get() == 'lot':
print('lot')
return 'lot'
class ReturnData(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
var = tk.StringVar()
message_box = tk.LabelFrame(self, text="wynik")
message_box.grid(row=1)
mb = tk.Label(message_box, textvariable=var,anchor='nw')
mb.pack(padx=120, pady=30)
class Application(tk.Tk):
def __init__(self):
super().__init__()
self.title("program do wyszukiwania cen lotów")
self.geometry('300x200')
self.resizable(width=False, height=False)
DataChosenForm(self).grid(row=0, column=0)
ReturnData(self).grid(row=1)
if __name__ == "__main__":
app = Application()
app.mainloop()
You could first display the combobox DataChosenForm(self).grid(row=0, column=0) without calling the ReturnData in the Application class.
Then, in the callback() method collect the choice choice = event.widget.get() and pass it to ReturnData. This would mean, however, that the LabelFrame is displayed only after a choice is made.
import tkinter as tk
from tkinter import ttk
class DataChosenForm(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
chosen = tk.LabelFrame(self, text="wybór")
chosen.grid(row=0)
self.combo = ttk.Combobox(chosen)
self.combo['values'] = ('wizz', 'ryanair', 'lot')
self.combo.grid(row=0, column=2, padx=80, pady=10)
self.combo.bind("<<ComboboxSelected>>", self.callback)
def callback(self, event=None):
choice = event.widget.get()
print(choice)
ReturnData(self, choice).grid(row=1)
class ReturnData(tk.Frame):
def __init__(self, parent, choice):
super().__init__(parent)
message_box = tk.LabelFrame(self, text="wynik")
message_box.grid(row=1)
mb = tk.Label(message_box, text=choice, anchor='nw')
mb.pack(padx=120, pady=30)
class Application(tk.Tk):
def __init__(self):
super().__init__()
self.title("program do wyszukiwania cen lotów")
self.geometry('300x200')
self.resizable(width=False, height=False)
DataChosenForm(self).grid(row=0, column=0)
if __name__ == "__main__":
app = Application()
app.mainloop()
I think #Julia has the basically the right architecture for your tkinter application, but her answer could be improved by using a tkinter Variable — because all widgets associated with one will automatically update their displayed value whenever the Variable is changed (by one of them or something else).
Here's a little documentation on Variable Classes. Note that since ttk.Combobox with have all the methods of a tk.Entry widgets, here's a bit of documentation about them (which also happens to illustrate the "pattern" of using of a StringVar in conjunction with one so it also applies to a ttk.Comboboxs).
Generally, you can tell a widget to use a tkinter Variable by specifying an instance of one as the option textvariable= keyword argument when the widget is created. You can also set the option using the partial dictionary interface most widgets support, so assignments like widget['textvariable'] = variable are another way to make use of them — the code below makes use of both of these ways.
Here's Julia's code modified to use a tk.StringVar. Note the Combobox doesn't need a callback function to bind the <<ComboboxSelected>> event to it, so all that complexity has been eliminated.
import tkinter as tk
from tkinter import ttk
class DataChosenForm(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
choice = tk.LabelFrame(self, text="wybór")
choice.grid(row=0)
self.combo = ttk.Combobox(choice)
self.combo['textvariable'] = parent.var # Use shared variable.
self.combo['values'] = ('wizzair', 'ryanair', 'lot')
self.combo.grid(row=0, column=2, padx=80, pady=10)
class ReturnData(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
message_box = tk.LabelFrame(self, text="wynik")
message_box.grid(row=1)
mb = tk.Label(message_box, textvariable=parent.var, # Use shared variable.
anchor='nw', width=20)
mb.pack(padx=120, pady=30)
class Application(tk.Tk):
def __init__(self):
super().__init__()
self.title("program do wyszukiwania cen lotów")
self.geometry('300x200')
self.resizable(width=False, height=False)
self.var = tk.StringVar(value='Dokonać wyboru') # Create shared variable.
DataChosenForm(self).grid(row=0, column=0)
ReturnData(self).grid(row=1)
if __name__ == "__main__":
app = Application()
app.mainloop()
You can just pass other class or it's field to __init__ of the DataChosenForm, and to callback function from there, where then you can change class/field directly. Here's what I mean, but I use TreeView:
import tkinter as tk
from tkinter import ttk
class ConnectedClass:
def __init__(self):
self.received = "will be changed"
class TreeViewWrapper(ttk.Treeview):
def __init__(self, master, connected_class, **kw):
super().__init__(master, **kw)
parents = []
for i in range(10):
parent = self.insert("", "end", text="Item %s" % i, tags=str(i))
for i in range(3):
self.insert(parent, "end", text="Item %s" % i, tags=str(i))
self.bind("<Control-r>", lambda e: self.pass_to_other(e, connected_class))
def pass_to_other(self, _, connected_class):
items = self.selection()
connected_class.received = items
class App:
def __init__(self):
self.root = tk.Tk()
self.con_class = ConnectedClass()
self.tree = TreeViewWrapper(self.root,self.con_class)
self.tree.pack()
self.root.bind("<Control-p>",lambda e:print(self.con_class.received))
self.root.mainloop()
if __name__ == "__main__":
app = App()

subclass Label in tkinter

I need to subclass a Label widget, so that the browser can open a link while the mouse clicks on it.
Here is the code snippet of what I've done so far.
from tkinter import *
import webbrowser
class HyperLinkLabel(Label):
def __init__(self, link, *args, **kwargs):
Label.__init__(self, *args, **kwargs)
self.link = link
self.bind("<Button-1>", self.click_callback)
def click_callback(self):
webbrowser.open_new(self.link)
if __name__ == '__main__':
master = Tk()
root = Frame(master)
label1 = HyperLinkLabel(root, link='https://www.google.com')
label1.config(text='hello')
label1.pack()
root.master.minsize(100, 50)
root.mainloop()
You should declare master (or parent) parameter and pass it to Label constructor. (root from the perspect of caller)
event handle should have event parameter even though you don't use it. Otherwise TypeError exception is thrown.
Pack frame so that widgets inside it is visible.
from tkinter import *
import webbrowser
class HyperLinkLabel(Label):
def __init__(self, master, link, *args, **kwargs): # <-- pass master parameter
Label.__init__(self, master, *args, **kwargs) # <-- pass master parameter
self.link = link
self.bind("<Button-1>", self.click_callback)
def click_callback(self, event): # <--- missed event parameter
webbrowser.open_new(self.link)
if __name__ == '__main__':
master = Tk()
root = Frame(master)
label1 = HyperLinkLabel(root, link='https://www.google.com')
label1.config(text='hello')
label1.pack()
root.pack() # <-- should pack frame; otherwise link widget is not visible
root.master.minsize(100, 50)
root.mainloop()

Connect an Object to an item in a Treeview Widget

I created a ttk/Treeview Widget in Tkinter using Python 3. I would like to connect an object to its name which is listed in the tree view. To illustrate this I created following example.
import tkinter as tk
from tkinter import ttk
class myclass:
def __init__(self, name, value):
self.name=name
self.value=value
class maintree(ttk.Treeview):
def __init__(self, master):
super().__init__(master)
self.master = master
self.my_objects= [myclass("object"+str(_), _) for _ in range(1,11)]
for my_object in self.my_objects:
self.insert("", "end", text=my_object.name)
def main():
root = tk.Tk()
maintree(root).grid()
root.mainloop()
if __name__ == '__main__':
main()
In this example I would like to get the my_class instance corresponding to the selected name in the treeview to do something (ie, display the value of the currently selected my_class object).
I only know about the item IDs but I don't know how to connect something to an item itself. I have the feeling that I have some misconception about how treeview is supposed to work.
I appreciate your Help!
The insert method has no command option. However, you can tag each inserted item and bind the tag to an event. So, I used the object name as tag and then bound it to execute some method of the object on mouse left click.
import tkinter as tk
from tkinter import ttk
class MyClass:
def __init__(self, name, value):
self.name=name
self.value=value
def callback(self, event=None):
# event=None is a trick to be able to call the method both directly and
# from a binding (that will pass the event argument to the function)
print(self.name, self.value)
class MainTree(ttk.Treeview):
def __init__(self, master):
super().__init__(master)
self.master = master
self.my_objects= [MyClass("object"+str(_), _) for _ in range(1,11)]
for my_object in self.my_objects:
self.insert("", "end", text=my_object.name, tags=(my_object.name,))
self.tag_bind(my_object.name, '<Button-1>', my_object.callback)
def main():
root = tk.Tk()
MainTree(root).grid()
root.mainloop()
if __name__ == '__main__':
main()

Application hangs after deiconify(), Tkinter

I'm trying to do Hide/Show for my application with withdraw()/deiconify() in Tkinter, but after deiconify() method call my app hangs. Run this code on Win7.
What do I do wrong?
import Tkinter as tk
import threading
class MyApp(object):
def __init__(self, parent):
self.root = parent
self.root.geometry('400x300')
self.root.title('My Application')
btn = tk.Button(parent, text='Hide', command=self.onClick)
btn.pack()
def onClick(self):
self.hide()
self.t = threading.Timer(3, self.show)
self.t.start()
def hide(self):
print 'hide()'
print 'state: ', self.root.state()
print 'withdraw()'
self.root.withdraw()
print 'state: ', self.root.state()
def show(self):
print 'show()'
print 'state: ', self.root.state()
print 'deiconify()'
self.root.deiconify()
print 'state: ', self.root.state()
print 'show end'
if __name__ == '__main__':
root = tk.Tk()
app = MyApp(root)
root.mainloop()
UPD: there is a working sample:
import Tkinter as tk
import sched
import time
class MyApp(object):
def __init__(self, parent):
self.root = parent
self.root.geometry('400x300')
btn = tk.Button(parent, text='Hide', command=self.onClick)
btn.pack()
self.scheduler = sched.scheduler(time.time, time.sleep)
def onClick(self):
self.hide()
self.scheduler.enter(3, 1, self.show, ())
self.scheduler.run()
def hide(self):
self.root.withdraw()
def show(self):
self.root.deiconify()
if __name__ == '__main__':
root = tk.Tk()
app = MyApp(root)
root.mainloop()
Tkinter is not thread safe, and you are calling self.root.deiconify() from a thread. That is most likely the source of your problem. You'll have to re-architect your solution to have the thread use a thread-safe queue to request that the main loop make calls into Tkinter.
There's a whole lot you can do with Tkinter without using threads. Are you certain you need them?

Categories

Resources