Creating dynamically named gui objects in Python with tkinter - python

I'm learning to use tkinter in Python 3.6.4. I am creating a GUI with multiple instances of buttons. Two such instances are:
def createWidgets(self):
# first button
self.QUIT = Button(self)
self.QUIT["text"] = "Quit"
self.QUIT["command"] = self.quit
self.QUIT.pack()
# second button
self.Reset = Button(self)
self.Reset["text"] = "Reset"
self.Reset["command"] = "some other function, tbd"
What I want to learn is how to abstract the instantiation of buttons such that each instance in the createWidgets method is based on a method something like this:
createButton( self, text, command, fg, bg, hgt, wth, cursor ):
What I don't know is how to control the naming of the button as:
self.QUIT
self.Reset
where the property or name following the "." operator can be passed to the createButton as a property by which the button is created and named.

Simply expanding on what Brian said, this code will get you going. The button objects are stored in a widget dictionary. Here is one way to put this together:
import tkinter as tk
import sys
root = tk.Tk()
class CustomButton(tk.Button):
def __init__(self, parent, **kwargs):
tk.Button.__init__(self, parent)
for attribute,value in kwargs.items():
try:
self[attribute] = value
except:
raise
def doReset():
print("doRest not yet implemented")
if __name__ == '__main__':
widget = {}
widget['quit'] = CustomButton(root, text='Quit', command=sys.exit)
widget['reset'] = CustomButton(root, text='Reset', command=doReset)
for button in widget.keys():
widget[button].pack()
root.mainloop()

Related

Changing one OptionMenu changes the second one

In my code below I have two option menus which are populated with the same list. In the final application the list is generated by importing a .csv file.
The user should be able to select two entries from the list.
Now the problem is, that changing the first option menu, will change instead the second one.
The second one, however, works as expected.
I guess the function update_file_list_selection() and lambda function is implemented badly.
import tkinter as tk
from tkinter import ttk
class File_Selection():
def __init__(self, frame, text):
self.frame = frame
self.text = text
self.label_file = tk.Label(self.frame, text=text)
self.label_file.pack()
self.variable_file = tk.StringVar(self.frame)
self.option_list = ["no file loaded"]
self.variable_file.set(self.option_list[0])
self.optionmenu_file = tk.OptionMenu(self.frame, self.variable_file,
*self.option_list)
self.optionmenu_file.pack()
class View:
def __init__(self, view, update_list):
self.view = view
self.view.title("Test")
self.view.geometry("320x240")
self.view.resizable(False, False)
self.frame = tk.Frame(self.view)
self.frame.pack()
self.button = tk.Button(self.frame, text="Update", command=update_list)
self.button.pack()
self.file_one = File_Selection(self.frame, "File 1")
self.file_two = File_Selection(self.frame, "File 2")
class Controller:
def __init__(self):
self.root = tk.Tk()
self.view = View(self.root, lambda: self.update_file_list_selection())
self.files = ["File 1", "File 2", "File 3", "File 4"]
def run(self):
self.root.mainloop()
def update_file_list_selection(self):
self.active_file_selection = [self.view.file_one, self.view.file_two]
for file_selection in self.active_file_selection:
self.menu = file_selection.optionmenu_file["menu"]
self.menu.delete(0, "end")
for x in self.files:
file_selection.option_list.append(x)
self.menu.add_command(label=x,
command=lambda value=x: file_selection.variable_file.set(value))
file_selection.variable_file.set(self.files[0])
if __name__ == "__main__":
c = Controller()
c.run()
I guess the function update_file_list_selection() and lambda function is implemented badly.
That is a correct guess.
The reason is a common problem with using lambda - when you do command=lambda value=x: file_selection.variable_file.set(value), the value of file_selection won't be the value from the loop, it will end up being the value of the final time that variable was set. You can solve this by binding the value to the lambda as a default argument:
self.menu.add_command(label=x, command=lambda value=x, fs=file_selection: fs.variable_file.set(value))
The above will make sure that inside the lambda body, fs will be set to the value of file_selection at the time the menu item is made rather than the value at the time the item is selected.
You'll still end up with OptionMenu items that don't behave exactly the same as normal OptionMenu items, but in this specific example that doesn't seem to matter since you don't have a command associated with the OptionMenu as a whole.

Passing objects into tabs inside instance of class

I have been studying very hard to learn OOP. I understand that objects can be an instance of a class. Classes have parameters and methods and are like an 'object constructor' which the object is created from. I am reorganizing the code in my first project to allow whatever possible to be part of a class. Right now I am working on the GUI but I am having trouble understanding the process in constructing the GUI with classes. Particularly having tabs inside the class and adding objects into each tab.
Here is an example of how my code currently looks:
import tkinter
from tkinter import ttk
class Win:
def __init__(self, master):
nb = ttk.Notebook(master, width=390, height=470)
nb.pack()
tab = ttk.Frame(nb)
nb.add(tab, text='title')
tab2 = ttk.Frame(nb)
nb.add(tab2, text='Graphs')
tab3 = ttk.Frame(nb)
nb.add(tab3, text='Messages')
tab4 = ttk.Frame(nb)
nb.add(tab4, text='Instructions')
label = tkinter.Label(tab, text='text')
label.grid(row=0, column=0, sticky=tkinter.N, pady=10)
menu = tkinter.Menu(master, tearoff=False)
master.config(menu=menu)
subMenu = tkinter.Menu(menu, tearoff=False)
menu.add_cascade(label="File", menu=subMenu)
subMenu.add_separator()
subMenu.add_command(label='Exit', command=master.destroy)
root = tkinter.Tk()
root.title("SC")
root.geometry('400x500')
root.resizable(width=False, height=False)
main_win = Win(root)
root.mainloop()
To put objects in each tab within main_win, what do I do? I tried putting objects below main_win and then passing the parameter main_win in the object but that does not seem to work. Should I have a class for making tabs then create an object tab and pass new objects into that?
Thanks in advance for the help. Was unable to find this specific answer anywhere.
This code shows one way of adding things to the Notebook tabs. It implements some of abarnert's suggestions, plus a few of my own ideas. I've separated the Notebook off into its own class, and moved the Tk root initialization code into the main GUI class.
I don't claim that this is the best way to do these things, I'm just illustrating a few possibilities to inspire you. ;)
import tkinter as tk
from tkinter import ttk
class GUI(tk.Tk):
def __init__(self):
super().__init__()
self.title("SC")
self.geometry('400x500')
self.resizable(width=False, height=False)
names = ['Title', 'Graphs', 'Messages', 'Instructions']
self.nb = self.create_notebook(names)
self.menu = self.create_menus()
# We can also add items to the Notebook here
tab = self.nb.tabs['Instructions']
tk.Label(tab, text='You should\nread these\ninstructions').pack()
self.mainloop()
def create_notebook(self, names):
nb = MyNotebook(self, names)
nb.pack()
def add_label(parent, text, row, column):
label = tk.Label(parent, text=text)
label.grid(row=row, column=column, sticky=tk.N, pady=10)
return label
# Add some labels to each tab
tab = nb.tabs['Title']
for i in range(3):
add_label(tab, 't' + str(i), i, 0)
tab = nb.tabs['Graphs']
for i in range(3):
add_label(tab, 'g' + str(i), 0, i)
tab = nb.tabs['Messages']
for i in range(3):
add_label(tab, 'm' + str(i), i, i)
return nb
def create_menus(self):
menu = tk.Menu(self, tearoff=False)
self.config(menu=menu)
subMenu = tk.Menu(menu, tearoff=False)
menu.add_cascade(label="File", menu=subMenu)
subMenu.add_separator()
subMenu.add_command(label='Exit', command=self.destroy)
return menu
class MyNotebook(ttk.Notebook):
''' A customised Notebook that remembers its tabs in a dictionary '''
def __init__(self, master, names):
super().__init__(master, width=390, height=470)
# Create tabs & save them by name in a dictionary
self.tabs = {}
for name in names:
self.tabs[name] = tab = ttk.Frame(self)
self.add(tab, text=name)
GUI()
I do most of the work of creating the Notebook and the Menu in separate methods of GUI. I could have put that code inside GUI.__init__ but it's more modular to do it in separate methods, and it stops the .__init__ method from getting huge.
I've saved the Notebook & Menu as instance attributes self.nb and self.menu. That's not really necessary here, they could just be local variables of GUI.__init__, eg, nb and menus. But storing them as attributes makes them accessible from other methods of GUI, which may be necessary when you add more stuff to the class.
When you derive a class from a parent class, like MyNotebook from ttk.Notebook (or GUI from tk.Tk), if the child class doesn't have its own __init__ method then the parent's __init__ will automatically get called when you create a child instance. But if the child has its own __init__ then the parent __init__ won't get called automatically. But we need the stuff in the parent __init__ to get done to our new instance of MyNotebook in order for the stuff that inherits from ttk.Notebook to be initialized. So the MyNotebook.__init__ does the super call to make that happen.
Generally, if a child class doesn't define a method that the parent class defines then when that method is called on a child instance the version from the parent will be called. And if the child does redefine an inherited method you will often want to call the parent method inside the child method at same stage, and it's usual to use super to do that. __init__ is a bit special because it normally gets called automatically to initialize the instance after it's been created.
Here's a simpler version that doesn't use child classes. It also has a Button widget on the root window which prints a string when you click it.
import tkinter as tk
from tkinter import ttk
class GUI:
def __init__(self):
root = tk.Tk()
root.title("SC")
root.geometry('400x500')
root.resizable(width=False, height=False)
names = ['Title', 'Graphs', 'Messages', 'Instructions']
self.nb = self.create_notebook(root, names)
self.menu = self.create_menus(root)
# We can also add items to the Notebook here
tab = self.nb.tabs['Instructions']
tk.Label(tab, text='You should\nread these\ninstructions').pack()
btn = tk.Button(root, text='Click', command=self.button_command)
btn.pack()
root.mainloop()
def button_command(self):
print('The button was clicked')
def create_notebook(self, root, names):
nb = ttk.Notebook(root, width=390, height=450)
nb.pack()
# Create tabs & save them by name in a dictionary
nb.tabs = {}
for name in names:
nb.tabs[name] = tab = ttk.Frame(root)
nb.add(tab, text=name)
def add_label(parent, text, row, column):
label = tk.Label(parent, text=text)
label.grid(row=row, column=column, sticky=tk.N, pady=10)
return label
# Add some labels to each tab
tab = nb.tabs['Title']
for i in range(3):
add_label(tab, 't' + str(i), i, 0)
tab = nb.tabs['Graphs']
for i in range(3):
add_label(tab, 'g' + str(i), 0, i)
tab = nb.tabs['Messages']
for i in range(3):
add_label(tab, 'm' + str(i), i, i)
return nb
def create_menus(self, root):
menu = tk.Menu(root, tearoff=False)
root.config(menu=menu)
subMenu = tk.Menu(menu, tearoff=False)
menu.add_cascade(label="File", menu=subMenu)
subMenu.add_separator()
subMenu.add_command(label='Exit', command=root.destroy)
return menu
GUI()

Python Tkinter custom msgbox fails during import

I am trying to create a custom text entry box that has a text entry field in the center and can return the user's input. I'm specifically trying to make something that can be imported and re-used in other programs. My current iteration uses an "inputdialog" class that works fine for getting the input when it's in it's own .py file, but if I try to import the module into another script, the "ok" button seemingly does nothing, and the "x" button throws an "ImportError: cannot import name 'inputdialog'" error after closing the pop-up.
The following is my code which currently works if run as a standalone script:
from tkinter import *
class inputdialog:
def __init__(self):
self.value = None
self.root = Tk()
self.root.withdraw()
self.top = Toplevel(self.root)
Label(self.top, text="Value").pack()
self.e = Entry(self.top)
self.e.pack(padx=5)
b = Button(self.top, text="OK", command=self.ok)
b.pack(pady=5, padx=5, side="right")
self.root.mainloop()
def ok(self):
self.value = self.e.get()
self.root.destroy()
if __name__ == "__main__":
test = inputdialog()
print(test.value)
The following is how I've imported this module into another program, which does not currently work:
if __name__ == "__main__":
# These two lines below are needed to make sure that "askopenfilename"
# doesn't show it's top level tkinter window
root = Tk()
root.withdraw()
entdbemp = askopenfilename(title="Please select a file: ")
# Here is my non-working module call
master = inputdialog()
print(master.value)
Why would my "OK" button cease to function when imported? Is there any way I can fix my code to allow it to be imported? Is there a better way of trying to make an importable text entry module using tkinter?
The problem has nothing to do with importing. The problem is that in your second example you call Tk() twice: once in each file. Also, when you call 'destroy' you are only removing the GUI from the screen, you are not exiting the mainloop.
To make this work, you need to remove the lines that create a root in the inputdialog class, move the mainloop() call to a point after starting the class, and then call the quit method for the toplevel window (not the root window):
from tkinter import *
class inputdialog:
def __init__(self, master=None):
self.value = None
self.top = Toplevel(master)
Label(self.top, text="Value").pack()
self.e = Entry(self.top)
self.e.pack(padx=5)
b = Button(self.top, text="OK", command=self.ok)
b.pack(pady=5, padx=5, side="right")
def ok(self):
self.value = self.e.get()
self.top.quit()
if __name__ == "__main__":
root = Tk()
root.withdraw()
master = inputdialog(root)
root.mainloop()
print(master.value)
If you want to be neat and proper: rather than making a class that wraps around a different class, in OOP and GUIs we like to make a subclass:
import tkinter as tk
class inputdialog(tk.Toplevel):
def __init__(self, master=None):
tk.Toplevel.__init__(self, master)
self.value = None
tk.Label(self, text="Value").pack()
self.e = tk.Entry(self)
self.e.pack(padx=5)
b = tk.Button(self, text="OK", command=self.ok)
b.pack(pady=5, padx=5, side="right")
def ok(self):
self.value = self.e.get()
self.quit()
if __name__ == "__main__":
root = tk.Tk()
root.withdraw()
master = inputdialog(root)
root.mainloop()
print(master.value)
Also, wildcard imports (from module import *) are ugly and against PEP8; don't use them.
BTW, the easygui package has done all this already; you may just want to install and use that.

Python Tkinter Input Box

Good day.
I am trying to create my own input box for use in my project.
basically what i am trying to do is run my main form which will call the second. the user will provide some data on the second and when the press the ok/close button on the second for the data will be passed back to the first. similar in functionality to the inputbox.
here is what i have created, but being new to python i am not sure where i am going wrong/nor can i quick figure out when to put the return.
My Class is here
import tkinter as tk
class MainWindow():
def __init__(self, parent):
top = self.top = tk.Toplevel(parent)
self.myLabel = tk.Label(top, text='Enter a Grouping Name')
self.myLabel.pack()
self.myEntryBox = tk.Entry(top)
self.myEntryBox.focus_set()
self.myEntryBox.pack()
self.mySubmitButton = tk.Button(top, text='OK', command=self.DestWin)
self.mySubmitButton.pack()
def DestWin(self):
self.top.destroy()
The method to call it is here
abc=configurator.MainWindow(root)
Not exactly sure what you are trying to achieve, but if you are trying to get values from one window to another, below you can find an extended example based on your code.
import tkinter as tk
class MainWindow():
def __init__(self, parent):
top = self.top = tk.Toplevel(parent)
self.myLabel = tk.Label(top, text='Enter a Grouping Name')
self.myLabel.pack()
self.myEntryBox = tk.Entry(top)
self.myEntryBox.focus_set()
self.myEntryBox.pack()
self.mySubmitButton = tk.Button(top, text='OK', command=self.DestWin)
self.mySubmitButton.pack()
def DestWin(self):
# call callback function setting value in MyFrame
self.callback(self.myEntryBox.get())
self.top.destroy()
def set_callback(self, a_func):
self.callback = a_func
class MyFrame(tk.Frame):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.pack()
self.myLabel1 = tk.Label(parent, text='Click OK to enter the group name')
self.myLabel1.pack()
self.mySubmitButton1 = tk.Button(parent, text='OK', command=self.get_group_name)
self.mySubmitButton1.pack()
def get_group_name(self):
mw = MainWindow(None)
# provide callback to MainWindow so that it can return results to MyFrame
mw.set_callback(self.set_label)
def set_label(self, astr = ''):
self.myLabel1['text'] = astr
root = tk.Tk()
mf = MyFrame(root)
root.mainloop()
The screenshot:
The text from the right window, when OK is pressed, will be shown in the left window. This is achieved through callbacks. MainWindow takes a callback function, and when you press OK, it is executed. The callback is set_label from MyFrame.
Hope this helps.

Tkinter Global Binding

Is it possible to bind all widgets to one command, with a single line? It would be nice if I could type in one line as opposed to doing each widget individually.
You would use the bind_all method on the root window. This will then apply to all widgets (unless you remove the bindtag "all" from some widgets). Note that these bindings fire last, so you can still override the application-wide binding on specific widgets if you wish.
Here's a contrived example:
import Tkinter as tk
class App:
def __init__(self):
root = tk.Tk()
root.bind_all("<1>", self.woot)
label1 = tk.Label(text="Label 1", name="label1")
label2 = tk.Label(text="Label 2", name="label2")
entry1 = tk.Entry(name="entry1")
entry2 = tk.Entry(name="entry2")
label1.pack()
label2.pack()
entry1.pack()
entry2.pack()
root.mainloop()
def woot(self, event):
print "woot!", event.widget
app=App()
You might also be interested in my answer to the question How to bind self events in Tkinter Text widget after it will binded by Text widget? where I talk a little more about bindtags.
If you have a list that contains all your widgets, you could iterate over them and assign the events.
You mean something like this code which handles all mouse events handled with single function?
from Tkinter import *
class ButtonHandler:
def __init__(self):
self.root = Tk()
self.root.geometry('600x500+200+200')
self.mousedown = False
self.label = Label(self.root, text=str(self.mousedown))
self.can = Canvas(self.root, width='500', height='400', bg='white')
self.can.bind("<Motion>",lambda x:self.handler(x,'motion'))
self.can.bind("<Button-1>",lambda x:self.handler(x,'press'))
self.can.bind("<ButtonRelease-1>",lambda x:self.handler(x,'release'))
self.label.pack()
self.can.pack()
self.root.mainloop()
def handler(self,event,button_event):
print('Handler %s' % button_event)
if button_event == 'press':
self.mousedown = True
elif button_event == 'release':
self.mousedown = False
elif button_event == 'motion':
if self.mousedown:
r = 5
self.can.create_oval(event.x-r, event.y-r, event.x+r, event.y+r, fill="orange")
self.label.config(text=str(self.mousedown))
button_event = ButtonHandler()
You could also just define a function that calls on all your widgets, and call that function. Or better yet create a class that call on your widgets in init and import the class...

Categories