Multi-Window Apps with tkinter
A tool for your IA, not an exam topic: Many IA products have more than one screen, such as a main screen, a form for adding or editing a record, and a window that shows one record’s details. This page shows how to manage several screens in tkinter and share data between them. tkinter is not exam content, and the IA does not ask for any particular number of screens or windows. The open_window method, in the One Method for Many Windows section, does practise an examined idea: functions with parameters and the benefits of modularisation (B2.3.4). Writing in Java? The Java version of this page is Multi-Window Apps.
Overview
Many IA products have several screens, for example:
- A main screen showing a list or a summary
- An add or edit form for entering or changing a record
- A details window for one record
- A settings or About window
Recall from tkinter Basics that a program has one main window, made by tk.Tk(), and that a tk.Frame holds a group of widgets. This page covers two ways to display more than one screen: swap frames inside the one window (Approach 1), or open extra windows, each a tk.Toplevel (Approach 2). Either way, screens that work with the same data need a way to share it, which Approach 3 covers. Choose what suits your product; a product with one well-organised screen is also fine.
Make one tk.Tk() only. Every extra window is a tk.Toplevel that belongs to the main window. A second tk.Tk() creates a separate Tcl interpreter (the engine that runs tkinter’s windows), so widgets and tkinter variables made for one cannot be used with the other.
Approach 1: Swapping Screens in One Window
The simplest approach keeps one window and changes what it shows. Each screen is a class that builds its widgets inside a Frame. To switch screens, the program takes the current frame out of the window with pack_forget() and packs the next one. The frames, and anything typed into them, still exist while they are not shown.
import tkinter as tk
class HomeScreen():
def __init__(self, window, app):
self.frame = tk.Frame(window)
tk.Label(self.frame, text="Home").pack(pady=10)
tk.Button(self.frame, text="Go to Settings", command=app.show_settings).pack()
class SettingsScreen():
def __init__(self, window, app):
self.frame = tk.Frame(window)
tk.Label(self.frame, text="Settings").pack(pady=10)
tk.Button(self.frame, text="Back to Home", command=app.show_home).pack()
class App():
def __init__(self, window):
window.title("My App")
self._home = HomeScreen(window, self)
self._settings = SettingsScreen(window, self)
self._current = self._home
self._current.frame.pack(fill="both", expand=True)
def show_screen(self, screen):
self._current.frame.pack_forget() # take the current screen out of the window
self._current = screen
screen.frame.pack(fill="both", expand=True)
def show_home(self):
self.show_screen(self._home)
def show_settings(self):
self.show_screen(self._settings)
window = tk.Tk()
app = App(window)
window.mainloop()
Each screen is given the App object, so its buttons can call app.show_settings or app.show_home. fill="both", expand=True lets the frame fill the window when the window is resized.
Approach 2: Popup Windows (Toplevel)
For a form that opens over the main screen, such as Add Student, create a tk.Toplevel: a new window that belongs to the main window. Here the popup is a class, and it keeps its window in the attribute window so that the main screen can wait for it.
Creating a Popup Window
import tkinter as tk
from models import Student
class AddStudentWindow():
def __init__(self, parent, model):
self._model = model
self._saved = False
self.window = tk.Toplevel(parent)
self.window.title("Add Student")
self.window.transient(parent) # belongs to the main window (most systems keep it in front)
tk.Label(self.window, text="Name").grid(row=0, column=0, sticky="w", padx=8, pady=4)
self._name_entry = tk.Entry(self.window, width=25)
self._name_entry.grid(row=0, column=1, padx=8, pady=4)
tk.Label(self.window, text="Grade (0-100)").grid(row=1, column=0, sticky="w", padx=8, pady=4)
self._grade_entry = tk.Entry(self.window, width=25)
self._grade_entry.grid(row=1, column=1, padx=8, pady=4)
self._status = tk.Label(self.window, text="")
self._status.grid(row=2, column=0, columnspan=2)
tk.Button(self.window, text="Save", command=self.handle_save).grid(row=3, column=0, pady=8)
tk.Button(self.window, text="Cancel", command=self.window.destroy).grid(row=3, column=1, pady=8)
self.window.wait_visibility() # wait until it is on screen (some systems need this first)
self._name_entry.focus_set() # put the typing cursor in the first box
self.window.grab_set() # modal: the main window cannot be used while it is open
def handle_save(self):
try:
grade = int(self._grade_entry.get().strip())
except ValueError:
self._status.config(text="Grade must be a whole number.")
return
try:
student = Student(self._name_entry.get().strip(), grade)
except ValueError as error:
self._status.config(text=str(error)) # the Model's own message
return
self._model.add_student(student)
self._saved = True
self.window.destroy() # close the popup
def is_saved(self):
return self._saved
Student is the Model class from MVC with tkinter, kept in models.py.
Key Concepts
| Code | What it does |
|---|---|
tk.Toplevel(parent) | Creates a new window that belongs to the main window. It closes when the main window closes. |
transient(parent) | Marks the popup as belonging to the main window, so most systems keep it in front of it. |
grab_set() | Makes the popup modal: until it closes, the user cannot use the main window. wait_visibility() first waits until the popup is on screen, which some systems need before a grab, and focus_set() puts the typing cursor in the popup’s first box. |
parent.wait_window(popup.window) | Pauses the code that opened the popup until the popup closes (see the main window below). |
destroy() | Destroys the window: it closes, and it cannot be shown again. |
Opening It from the Main Window
def handle_add(self):
popup = AddStudentWindow(self._window, self._model)
self._window.wait_window(popup.window) # pause here until the popup closes
self.refresh_display()
wait_window() pauses handle_add until the popup has closed, while the program still responds to the popup’s own clicks. So refresh_display() runs after the user has saved or cancelled, and the new student is already in the list.
Without wait_window(), handle_add would carry straight on and refresh the list while the popup is still open, before the user has typed anything, so the new student would not appear until the next refresh.
Approach 3: Shared Model
Both approaches above need a way to share data between screens. One clear way is a shared Model: pass the same Model object to every screen that needs it. AddStudentWindow above is given the main window’s StudentList, so the student it adds goes into the list the main window shows.
flowchart TD
A["Main program<br>creates the Model once"] -- "passes model to" --> B["Main window"]
B -- "passes the same model to" --> C["AddStudentWindow"]
B -- "model" --> M[("StudentList<br>(the same object)")]
C -- "model" --> M
# In the main program:
model = StudentList() # one StudentList for the whole app
manager = StudentManager(window, model)
# In StudentManager, when opening the popup:
popup = AddStudentWindow(self._window, self._model) # the same StudentList object
Because both screens refer to the same StudentList object (two references to one object, as in Object References), a student added in the popup is already in the list that the main window reads when it refreshes.
Do not create a new Model in each screen. Each screen would then have its own separate data, and changes made in one would not appear in the other.
Another way: one shared controller for the whole app. Some apps keep the data that every screen needs in a singleton, a class that allows only one object of itself to be made. Any screen can reach it with AppController.get_instance(), so nothing has to be passed in. The cost is that any class can reach and change the data, which makes it harder to see where a change came from. MVC with tkinter: One Controller for Every Screen shows one. Passing the Model to the screens that need it, as above, makes it clear which screens use which data.
Passing Data to a Popup
Sometimes a popup needs one particular record: for example, an Edit Student window that opens already filled in with one student’s details. The main window finds the student and passes it in:
class EditStudentWindow():
def __init__(self, parent, student):
self._student = student # the same Student object that is in the list
self._saved = False
self.window = tk.Toplevel(parent)
self.window.title("Edit Student")
self.window.transient(parent) # belongs to the main window (most systems keep it in front)
tk.Label(self.window, text="Name: " + student.get_name()).pack(padx=10, pady=4)
self._grade_entry = tk.Entry(self.window, width=10)
self._grade_entry.insert(0, str(student.get_grade())) # start with the current grade
self._grade_entry.pack(padx=10, pady=4)
self._status = tk.Label(self.window, text="")
self._status.pack()
tk.Button(self.window, text="Save", command=self.handle_save).pack(pady=8)
self.window.wait_visibility()
self._grade_entry.focus_set()
self.window.grab_set()
def handle_save(self):
try:
self._student.set_grade(int(self._grade_entry.get().strip()))
except ValueError:
self._status.config(text="Enter a whole number from 0 to 100.")
return
self._saved = True
self.window.destroy()
def is_saved(self):
return self._saved
The popup is given the same Student object that is in the StudentList, so a change made in the popup changes the student in the list, and the main window only has to refresh. set_grade() refuses a grade outside 0 to 100 with a ValueError, just as int() does for text that is not a number, so one except covers both.
Getting an Answer Back
Sometimes the main window needs to know what the user did in the popup, for example whether they clicked Save or closed the popup without saving. The popup’s Save handler sets _saved to True, and is_saved() returns it. The main window still holds the popup object, so it can ask once wait_window() has returned:
def handle_edit(self):
selected = self._model.find_by_name(self._name_entry.get().strip())
if selected == None:
self._status.config(text="No student with that name.")
return
popup = EditStudentWindow(self._window, selected)
self._window.wait_window(popup.window)
if popup.is_saved() == True:
self._status.config(text="Changes saved.")
else:
self._status.config(text="No changes made.")
self.refresh_display()
One Method for Many Windows
As an app gains windows, the code that opens them starts to repeat. Each window class below builds its window in the same way, so one method that takes the class and the title as parameters can open any of them. This is the idea in B2.3.4: a function is a reusable block of code that can be called with different inputs.
class HelpWindow():
def __init__(self, parent):
self.window = tk.Toplevel(parent)
tk.Label(self.window, text="Type a name and a grade, then click Add.").pack(padx=20, pady=20)
class AboutWindow():
def __init__(self, parent):
self.window = tk.Toplevel(parent)
tk.Label(self.window, text="Student Manager, version 1").pack(padx=20, pady=20)
class MainWindow():
def __init__(self, window):
self._window = window
self._open = {} # each open window, by its title
tk.Button(window, text="Help", command=self.handle_help).pack(padx=20, pady=4)
tk.Button(window, text="About", command=self.handle_about).pack(padx=20, pady=4)
def open_window(self, window_class, title):
if title in self._open and self._open[title].window.winfo_exists() == 1:
self._open[title].window.deiconify() # already open: show it if it was minimised
self._open[title].window.lift() # and bring it to the front
else:
new_window = window_class(self._window)
new_window.window.title(title)
self._open[title] = new_window
def handle_help(self):
self.open_window(HelpWindow, "Help")
def handle_about(self):
self.open_window(AboutWindow, "About")
open_window is given a class, HelpWindow or AboutWindow, and calls it to make the window, so one method opens both. It also keeps a dictionary of the windows it has opened, by title. Clicking Help a second time shows the Help window again (even if it was minimised) and brings it to the front, instead of opening a second copy. winfo_exists() returns 1 while the window exists, including when it is minimised or hidden, and 0 once it has been closed with destroy() or its close button, so a closed window is opened again.
What this gains (the benefits of modularisation from Methods):
- A new window needs one line in its handler.
- A change to how every window opens, such as giving them all the same size, is made in one place.
- Each handler says in one line what it does.
A window that needs data, such as the Add Student form, is still opened as in Approach 2, so that the main window can pass the Model in and wait for it.
Closing, Hiding and Bringing a Window Back
| Code | What it does |
|---|---|
destroy() | Destroys the window: it closes, and its widgets, with anything typed into them, are gone. |
withdraw() | Hides the window. It still exists, with everything typed into it. |
deiconify() | Shows a hidden window again. |
lift() | Brings a window in front of the others. |
protocol("WM_DELETE_WINDOW", method) | Runs your method when the user clicks the window’s close button (X), instead of closing it straight away. |
Bringing the main window back. The main window can hide while another window is open, and come back when that window closes:
def show_help_instead(self):
self._help = HelpWindow(self._window)
self._help.window.title("Help")
self._help.window.protocol("WM_DELETE_WINDOW", self.close_help) # runs when its X is clicked
self._window.withdraw() # hide the main window
def close_help(self):
self._help.window.destroy()
self._window.deiconify() # show the main window again
A hidden main window keeps the app running. If the main window is withdrawn and nothing brings it back, the program has no window on screen but is still running. Bring the main window back, or end the app, whenever the last visible window closes.
Quitting the app. destroy() on the main window ends mainloop(), closes every Toplevel with it and ends the app. To ask before quitting, give the main window protocol("WM_DELETE_WINDOW", ...) with a method that asks with messagebox.askyesno and calls destroy() only if the user clicks Yes.
Complete Example: Student Manager with Add Popup
Student and StudentList are the Model classes from MVC with tkinter, kept in models.py, and AddStudentWindow is the popup from Approach 2, kept in add_popup.py. Put all the files in the same folder.
import tkinter as tk
from models import StudentList
from add_popup import AddStudentWindow
class StudentManager():
def __init__(self, window, model):
self._window = window
self._model = model
window.title("Student Manager")
self._display = tk.Listbox(window, width=40, height=10)
self._display.pack(padx=10, pady=10)
tk.Button(window, text="Add Student", command=self.handle_add).pack()
self._status = tk.Label(window, text="Ready")
self._status.pack(pady=(4, 10))
self.refresh_display()
def handle_add(self):
popup = AddStudentWindow(self._window, self._model)
self._window.wait_window(popup.window) # pause here until the popup closes
if popup.is_saved() == True:
self._status.config(text="Student added.")
else:
self._status.config(text="No student added.")
self.refresh_display()
def refresh_display(self):
self._display.delete(0, tk.END)
students = self._model.get_all()
for i in range(len(students)):
self._display.insert(tk.END, str(i + 1) + ". " + str(students[i]))
window = tk.Tk()
model = StudentList()
manager = StudentManager(window, model)
window.mainloop()
What this demonstrates:
- One
tk.Tk()main window, and the Add Student form as a modalToplevel - The main window waits for the popup with
wait_window(), then asks it whether a student was saved - Both windows share one
StudentList, so the main window only has to refresh - The popup checks the input and shows the Model’s own message when the Model refuses a value
Quick Check
Q1. An app already has its main window. What should it create for a second, separate window?
Q2. A main window opens an Add popup and then refreshes its list straight away. Students added in the popup do not appear until the next refresh. What is the best fix?
Q3. What does it mean for a popup window to be modal?
Q4. A program hides its main window with withdraw() while a Help window is open. What is true of the main window?
Q5. How does the Add Student popup on this page put its new student into the main window's list?
Practice Exercises
Core
-
Message popup (4 marks): Construct a main window with a Show Message button. Clicking it opens a modal popup (a
Toplevel) with a label that says “Hello from the popup.” and a Close button that closes the popup. While the popup is open, the user cannot use the main window. -
Screen swap (4 marks): Construct an app with two screens shown in the same window: a Home screen with a greeting and a Go to Settings button, and a Settings screen with an entry box for the user’s name and a Back to Home button. Clicking either button swaps the screen in the same window. When the user clicks Back to Home, the Home screen shows “Welcome, “ followed by the name typed on the Settings screen, or just “Welcome” if the box is empty.
Extension
-
Edit popup (6 marks): Construct the rest of the Edit Student feature from Passing Data to a Popup. Add a name box and an Edit button to the Complete Example’s main window. Extend
EditStudentWindowso it can change the name as well as the grade, with a Cancel button that closes it without changing anything. Add aset_namemethod toStudentthat refuses an empty name, as the constructor does. After the popup closes, the main window’s list shows the changes. -
Confirm before closing (6 marks): Construct a close check for the Student Manager’s main window: when the user clicks the window’s close button (X), a message box asks “Exit without saving?” with Yes and No buttons. Yes closes the app; No keeps it open. Use
protocol("WM_DELETE_WINDOW", ...)andmessagebox.askyesno.
Challenge
- Lost property office (8 marks): Construct a three-window tkinter app for a school’s lost property office:
- Main window: lists every item that has been handed in, showing whether it has been collected, and has Log Item and Item Details buttons.
- Log Item popup (modal): takes a description of the item and the place where it was found. An empty description or place is refused, with a message.
- Item Details popup (modal): shows everything about the item chosen on the main window, and has a Mark as Collected button.
All three windows work with one
LostPropertymodel object, which holds the items and the rules about them. The main window’s list is up to date whenever a popup closes. - Explain a list that does not update (6 marks): A quiz night app has a main window that lists the teams and an Add Team button. Its handler creates an
AddTeamWindow, giving it the main window’sTeamList, and then callsrefresh_display()on the next line. The popup adds the new team to thatTeamListand closes. The user adds a team, but the main window’s list does not show it. Explain why, describing in order what happens from the moment Add Team is clicked. Then describe two different ways to fix the program. You are not required to write code.
Connections
- Prerequisites: tkinter Basics: the main window, widgets, frames and event handling
- Prerequisites: MVC with tkinter: the Model classes and the shared controller
- Prerequisites: Object References: a shared Model is two references to one object
- Related: Methods: functions with parameters and the benefits of modularisation (B2.3.4)
- Java version: Multi-Window Apps: the same ideas with JavaFX stages and scenes
- Next: File I/O in Apps: saving data between sessions
- Next: Structuring Your IA: putting it all together