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()
Related
I defined a GeneralFrame class that inherits from tk.LabelFrame, which contains other widgets like labels and entries:
class GeneralFrame(tk.LabelFrame):
def __init__(self, master, eCount, lCount):
super().__init__()
self.grid(padx=5, pady=5)
self.entry_widget(eCount)
self.label_widget(lCount)
def entry_widget(self, eCount):
self.e = {}
for i in range(0, eCount):
self.e[i] = tk.Entry(self, width=6)
self.e[i].grid(row=i, column=1, sticky='w')
self.e[i].delete(0, tk.END)
def label_widget(self, lCount):
self.l = {}
for i in range(0, lCount):
self.l[i] = tk.Label(self)
self.l[i].grid(row=i, column=0, sticky='w')
How can I use this class in a TopLevel window?
I've tried like this but it places the frame_save in parent window not TopLevel:
def openNewWindow():
newWindow = Toplevel(window)
newWindow.title('Saved Data')
newWindow.geometry('200x200')
frame_save = GeneralFrame(newWindow, eCount=5, lCount=5)
frame_save.configure(text='Saved data',font=("Helvetica",14,"bold"))
frame_save.grid(row=0, column=0)
labels_text = ['label1','label2','label3','label4','label5']
[frame_save.l[i].configure(text=labels_text[i]) for i in range(0,5)]
And general use in parent window:
window = tk.Tk()
window.geometry("980x500")
window.resizable(1,1)
window.title('Calculator')
class GeneralFrame(tk.LabelFrame):
[code]
frame_1 = GeneralFrame(window, eCount=5, lCount=5)
frame_2 = GeneralFrame(window, eCount=5, lCount=5)
def Frame_1():
[code]
def Frame_2():
[code]
Frame_1()
Frame_2()
window.mainloop()
You need to pass master when calling super().__init__. Otherwise, the actual frame has a default master, and the default master is the root window.
super().__init__(master)
Also, I encourage you to not call self.grid inside the __init__. The way tkinter widgets are designed, it's expected that the code that creates the widgets also calls pack, place, or grid on the widget. Otherwise your class can only ever be used in a parent that uses grid.
It works if I do this:
class GeneralFrame(tk.LabelFrame):
def __init__(self, master, eCount, lCount):
#super().__init__()
tk.LabelFrame.__init__(self,master)
I'm currently using tkinter to create a GUI for my program. If I open the golf quiz window and open the help window, then close the golf quiz window and re-open it, I am able to click the help window button and open another instance of the help button. How do I set the Help button to be disabled while the Help window is open?
from tkinter import *
from functools import partial
class Welcome_Screen:
def __init__(self, parent):
self.welcome_screen_frame = Frame(width=200, height=200, pady=10)
self.welcome_screen_frame.grid()
self.quiz_welcome_screen_label = Label(self.welcome_screen_frame, text = "quiz game", font="Arial 20 bold", padx=10)
self.quiz_welcome_screen_label.grid(row=0)
self.welcome_screen_buttons_frame = Frame(self.welcome_screen_frame)
self.welcome_screen_buttons_frame.grid(row=2)
self.golf_quiz_welcome_screen_button = Button(self.welcome_screen_buttons_frame, text="Golf Quiz", font="Arial 10 bold", command=self.golf_quiz_game, padx=10, pady=10)
self.golf_quiz_welcome_screen_button.grid(row=2, column=0, padx=5)
def golf_quiz_game(self):
get_golf_quiz_game = golf_quiz_game(self)
class golf_quiz_game:
def __init__(self, partner):
partner.golf_quiz_welcome_screen_button.config(DISABLED)
self.golf_quiz_box = Toplevel()
self.golf_quiz_box.protocol('WM_DELETE_WINDOW', partial(self.close_golf_quiz_game, partner))
self.golf_quiz_frame = Frame(self.golf_quiz_box)
self.golf_quiz_frame.grid()
self.golf_quiz_heading = Label(self.golf_quiz_frame, text="Golf Quiz game",
font="arial 18 bold", padx=10, pady=10)
self.golf_quiz_heading.grid(row=0)
self.golf_quiz_history_help_dismiss_buttons_frame = Frame(self.golf_quiz_frame)
self.golf_quiz_history_help_dismiss_buttons_frame.grid(row=6, pady=10)
self.help_button = Button(self.golf_quiz_history_help_dismiss_buttons_frame, text="Help", font="Arial 10 bold",command=self.Help, padx=10, pady=10)
self.help_button.grid(row=6, column=1, padx=5)
def close_golf_quiz_game(self, partner):
partner.golf_quiz_welcome_screen_button.config(state=NORMAL)
self.golf_quiz_box.destroy()
def Help(self):
get_help = Help(self)
class Help:
def __init__(self, partner):
partner.help_button.config(state=DISABLED)
self.help_box = Toplevel()
self.help_box.protocol('WM_DELETE_WINDOW', partial(self.close_Help, partner))
self.help_frame = Frame(self.help_box)
self.help_frame.grid()
self.help_heading = Label(self.help_frame, text="Help", font="arial 18 bold")
self.help_heading.grid(row=0)
self.help_text = Label(self.help_frame, text="Test",
width=60, wrap=400)
self.help_text.grid(row=1)
self.help_button = Button(self.help_frame, text="Dismiss", width=10, font="Arial 10 bold", command=partial(self.close_Help, partner), padx=10, pady=10)
self.help_button.grid(row=2, pady=10)
def close_Help(self, partner):
if partner.help_button.winfo_exists():
partner.help_button.config(state=NORMAL)
self.help_box.destroy()
# main routine
if __name__ == "__main__":
root = Tk()
root.title("quiz game")
something = Welcome_Screen(root)
root.mainloop()
Here is how you can do that (this is also the size (linewise (approx.)) of the minimal reproducible example which you should have provided):
from tkinter import Tk, Toplevel, Button
def close_top(top):
btn.config(state='normal')
top.destroy()
def open_help():
btn.config(state='disabled')
top = Toplevel(root)
top.protocol('WM_DELETE_WINDOW', lambda: close_top(top))
top.focus_force()
# put the rest of help stuff here
root = Tk()
btn = Button(root, text='Help', command=open_help)
btn.pack()
root.mainloop()
Class based approach:
from tkinter import Tk, Toplevel, Button
# this would be the window from where you open the help window
class MainWindow(Tk):
def __init__(self):
Tk.__init__(self)
self.btn = Button(self, text='Help',
command=lambda: self.open_help(self.btn))
self.btn.pack()
def open_help(self, btn):
HelpWindow(self, btn)
# this would be the help window
class HelpWindow(Toplevel):
def __init__(self, master, button):
Toplevel.__init__(self, master)
self.button = button
self.button.config(state='disabled')
self.focus_force()
self.protocol('WM_DELETE_WINDOW', self.close)
def close(self):
self.button.config(state='normal')
self.destroy()
MainWindow().mainloop()
Few things:
First of you can simply inherit from container and window classes that way you don't have to separately create them in the class and you can easily reference them in the class using just self
.focus_force() does what it says, it forces focus on the widget
Important (suggestions)
I strongly advise against using wildcard (*) when importing something, You should either import what You need, e.g. from module import Class1, func_1, var_2 and so on or import the whole module: import module then You can also use an alias: import module as md or sth like that, the point is that don't import everything unless You actually know what You are doing; name clashes are the issue.
I strongly suggest following PEP 8 - Style Guide for Python Code. Function and variable names should be in snake_case, class names in CapitalCase. Don't have space around = if it is used as a part of keyword argument (func(arg='value')) but use if it is used for assigning a value (variable = 'some value'). Have two blank lines around function and class declarations.
You need to make use of class variables inside golf_quiz_game in order to have only one instance of Help window:
class golf_quiz_game:
# class variables
_help_button = None # reference to instance "Help" button
_help_win = None # reference to instance "Help" window
def __init__(self, partner):
...
# set button state based on whether "Help" window is open or not
self.help_button.config(state="normal" if self.__class__._help_win is None else "disabled")
# update class reference of "Help" button
self.__class__._help_button = self.help_button
def close_golf_quiz_game(self, partner):
partner.golf_quiz_welcome_screen_button.config(state=NORMAL)
self.golf_quiz_box.destroy()
# update class reference of "Help" button
self.__class__._help_button = None
def Help(self):
if self.__class__._help_win is None:
# no "Help" window is open, create one
self.__class__._help_win = Help(self)
def help_closed(self):
if self.__class__._help_button:
# enable the "Help" button
self.__class__._help_button.config(state="normal")
# update "Help" window status
self.__class__._help_win = None
class Help:
...
def close_Help(self, partner):
self.help_box.destroy()
# notify partner that "Help" window is closed
partner.help_closed()
This code of a minimal example is functional:
from tkinter import *
textbox =str()
def openpopup():
popupwindow = Toplevel(root)
global textbox
textbox = Text(popupwindow, height=20, width=40,font="Courier")
textbox.pack()
textbox.delete(1.0, END)
textbox.insert(1.0,"start")
Button(popupwindow, text="do it", command=changepopup).pack()
def changepopup():
global textbox
textbox.delete(1.0, END)
textbox.insert(1.0,"changed text")
root = Tk()
Button(root, text="open", command=openpopup).pack()
mainloop()
my goal is to open a popup dynamically on userinput and then have various gui elements interact.
I managed to do this using global. I've read using global variables should be avoided.
What is the recommended way of going about this? Can I avoid using globals? I am aware that this is an issue of scoping, this is how I came up with this "solution". I am not so familiar with OOP but I have a hunch this might be a solution here.
Question: Can I avoid using globals?
Yes, consider this OOP solution without any global.
Reference:
- 9.5. Inheritance
- class-and-instance-variables
- Dialog Windows
import tkinter as tk
from tkinter import tkSimpleDialog
class Popup(tkSimpleDialog.Dialog):
# def buttonbox(self):
# override if you don't want the standard buttons
def body(self, master):
self.text_content = ''
self.text = tk.Text(self)
self.text.pack()
return self.text # initial focus
def apply(self):
self.text_content = self.text.get(1.0, tk.END)
class App(tk.Tk):
def __init__(self):
super().__init__()
btn = tk.Button(self, text='Popup', command=self.on_popup)
btn.pack()
def on_popup(self):
# The widget `Popup(Dialog)`, waits to be destroyed.
popup = Popup(self, title='MyPopup')
print(popup.text_content)
if __name__ == '__main__':
App().mainloop()
The object-oriented way would be to create a class representing "popup" objects. The class' initializer method, __init__(), can create the popup's widgets as well as act as a storage area for the contents of the Text widget. This avoids needing a global variable because methods of class all has an first argument usually call self the is instance of the class.
Any data needed can be stored as attributes of self and can easily be "shared" all the methods of the class.
The other primary way to avoid global variables is by explicitly passing them as arguments to other callables — like main() does in the sample code below.
Here's an example based on the code in your question:
from tkinter import *
class Popup:
def __init__(self, parent):
popup_window = Toplevel(parent)
self.textbox = Text(popup_window, height=20, width=40, font="Courier")
self.textbox.pack()
self.textbox.insert(1.0, "start")
btn_frame = Frame(popup_window)
Button(btn_frame, text="Do it", command=self.do_it).pack(side=LEFT)
Button(btn_frame, text="Close", command=popup_window.destroy).pack(side=LEFT)
btn_frame.pack()
def do_it(self):
self.clear()
self.textbox.insert(1.0, "changed text")
def clear(self):
self.textbox.delete(1.0, END)
def main():
root = Tk()
Button(root, text="Open", command=lambda: Popup(root)).pack()
root.mainloop()
if __name__ == '__main__':
main()
The global you created, textbox is unnecessary. You can simply remove it from your program. and still get the same behavior
# textbox = str()
I hope my answer was helpful.
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()
I am attempting to have a function in python that clears the screen upon a button being pressed. I am aware of grid_remove but am unsure of how to use it. Also is there a way to clear everything from a specific function, ie both "hi" and "clear"?
from tkinter import *
class Movies:
def __init__(self, master):
hi = Label(text = "Hello")
hi.grid(row = 0, column = 0)
clear = Button(text = "Click", command=self.clear)
clear.grid(row = 1, column = 0)
def clear(self):
hi.grid_remove()
root = Tk()
gui = Movies(root)
root.geometry("100x200+0+0")
root.mainloop()
You could use the built in winfo_children method if you're just wanting to toggle hiding / showing all of the widgets in whatever parent holds the widgets. Small example:
from tkinter import *
class Movies:
def __init__(self, master):
self.master = master
self.state = 1
for i in range(5):
Label(self.master, text='Label %d' % i).grid(row=0, column=i)
self.magic_btn = Button(self.master, text='Make the Magic!',
command=self.magic)
self.magic_btn.grid(columnspan=5)
def magic(self):
self.state = not self.state
for widget in self.master.winfo_children(): #iterate over all child widgets in the parent
#Comment out to clear the button too, or leave to toggle widget states
if widget != self.magic_btn: #or some other widget you want to stay shown
if self.state:
widget.grid()
else:
widget.grid_remove()
print(self.state)
root = Tk()
gui = Movies(root)
root.mainloop()