Desktop applications can be made up of more than one window. The main window is created by the tk.Tk
class and controls the life cycle of the application. Secondary windows, also known as popup or child windows, might be created on application startup or in response to an event (for example, a button press) via the tk.Toplevel
class. When the user closes the main window, secondary windows are also closed, thus ending the program execution. However, child windows can be opened and closed multiple times during the application life cycle.
The following code creates an application with a parent window and a child window. The main window contains a button that opens the secondary window. Inside the secondary window there is another button to close itself.
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
|
|
def open_secondary_window():
|
|
# Create secondary (or popup) window.
|
|
secondary_window = tk.Toplevel()
|
|
secondary_window.title("Secondary Window")
|
|
secondary_window.config(width=300, height=200)
|
|
# Create a button to close (destroy) this window.
|
|
button_close = ttk.Button(
|
|
secondary_window,
|
|
text="Close window",
|
|
command=secondary_window.destroy
|
|
)
|
|
button_close.place(x=75, y=75)
|
|
|
|
|
|
# Create the main window.
|
|
main_window = tk.Tk()
|
|
main_window.config(width=400, height=300)
|
|
main_window.title("Main Window")
|
|
# Create a button inside the main window that
|
|
# invokes the open_secondary_window() function
|
|
# when pressed.
|
|
button_open = ttk.Button(
|
|
main_window,
|
|
text="Open secondary window",
|
|
command=open_secondary_window
|
|
)
|
|
button_open.place(x=100, y=100)
|
|
main_window.mainloop()
|
Having two different windows, whenever we create a widget (be it a button or any other), we must specify its parent window (i.e., the window in which it is located) as the first argument. The button_open
is inside the main window, hence in line 27 the main_window
object is passed as the first argument. The same goes for the button_close
and the secondary window on line 12. Here is the result:
In order for the child window to get focus automatically once created, we use the focus()
method:
|
def open_secondary_window():
|
|
# Create secondary (or popup) window.
|
|
secondary_window = tk.Toplevel()
|
|
secondary_window.title("Secondary Window")
|
|
secondary_window.config(width=300, height=200)
|
|
# Create a button to close (destroy) this window.
|
|
button_close = ttk.Button(
|
|
secondary_window,
|
|
text="Close window",
|
|
command=secondary_window.destroy
|
|
)
|
|
button_close.place(x=75, y=75)
|
|
secondary_window.focus()
|
When the two windows are open, the user will be able to interact with both of them. If we want the user to be unable to use the main window while the secondary window is visible (known in the GUI jargon as modal window), we call the grab_set()
method:
|
def open_secondary_window():
|
|
# Create secondary (or popup) window.
|
|
secondary_window = tk.Toplevel()
|
|
secondary_window.title("Secondary Window")
|
|
secondary_window.config(width=300, height=200)
|
|
# Create a button to close (destroy) this window.
|
|
button_close = ttk.Button(
|
|
secondary_window,
|
|
text="Close window",
|
|
command=secondary_window.destroy
|
|
)
|
|
button_close.place(x=75, y=75)
|
|
secondary_window.focus()
|
|
secondary_window.grab_set() # Modal.
|
Both the main window and the child window provide the destroy()
method to close them programmatically. Note that main_window.destroy()
finishes the whole application.
Although this way of organizing the code can be useful for small applications, a better solution is to create a class for each window. So the code above in its object-oriented version would look something like this:
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
|
|
class SecondaryWindow(tk.Toplevel):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.config(width=300, height=200)
|
|
self.title("Secondary Window")
|
|
self.button_close = ttk.Button(
|
|
self,
|
|
text="Close window",
|
|
command=self.destroy
|
|
)
|
|
self.button_close.place(x=75, y=75)
|
|
self.focus()
|
|
self.grab_set()
|
|
|
|
|
|
class MainWindow(tk.Tk):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.config(width=400, height=300)
|
|
self.title("Main Window")
|
|
self.button_open = ttk.Button(
|
|
self,
|
|
text="Open secondary window",
|
|
command=self.open_secondary_window
|
|
)
|
|
self.button_open.place(x=100, y=100)
|
|
|
|
def open_secondary_window(self):
|
|
self.secondary_window = SecondaryWindow()
|
|
|
|
|
|
main_window = MainWindow()
|
|
main_window.mainloop()
|
This implementation has the benefit that widgets and methods of both windows are encapsulated within their respective objects (main_window
and secondary_window
), avoiding name collisions and reducing usage of global objects. The classes could even be in different modules: it's a common pattern in GUI development to put each window in its own source code file.
Other features are also easier to implement with this arrangement of windows into classes. For example, what happens if the user presses the button_open
more than once? If the child window is not modal (i.e., grab_set()
has not been called), the user will be allowed to open an arbitrary amount of child windows. This is generally an undesirable effect, so it is useful to add a constraint so that the SecondaryWindow
does not get open more than once at the same time.
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
|
|
class SecondaryWindow(tk.Toplevel):
|
|
|
|
# Class attribute that indicates whether this child window
|
|
# is being used (alive) or not.
|
|
alive = False
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.config(width=300, height=200)
|
|
self.title("Secondary Window")
|
|
self.button_close = ttk.Button(
|
|
self,
|
|
text="Close window",
|
|
command=self.destroy
|
|
)
|
|
self.button_close.place(x=75, y=75)
|
|
self.focus()
|
|
# Set the window as alive once created.
|
|
self.__class__.alive = True
|
|
|
|
def destroy(self):
|
|
# Restore the attribute on close.
|
|
self.__class__.alive = False
|
|
return super().destroy()
|
|
|
|
|
|
class MainWindow(tk.Tk):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.config(width=400, height=300)
|
|
self.title("Main Window")
|
|
self.button_open = ttk.Button(
|
|
self,
|
|
text="Open secondary window",
|
|
command=self.open_secondary_window
|
|
)
|
|
self.button_open.place(x=100, y=100)
|
|
|
|
def open_secondary_window(self):
|
|
if not SecondaryWindow.alive:
|
|
self.secondary_window = SecondaryWindow()
|
|
|
|
|
|
main_window = MainWindow()
|
|
main_window.mainloop()
|
The logic under this code is simple. We create the alive
attribute that is true when the window is in use and false otherwise, and always query it before instantiating the child window.
But what if we want to access an object within a child window from the parent window? For example, if we want to create a child window for the user to enter his name and then display it in a label in the parent window:
An elegant solution for this scenario is to use a callback function (heavily used in event-driven programming), which will be called by the child window when the entered name is available. This approach is similar to that used in buttons when passing a function name to the command
argument.
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
|
|
class InputWindow(tk.Toplevel):
|
|
|
|
def __init__(self, *args, callback=None, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# callback is a function that this window will call
|
|
# with the entered name as an argument once the button
|
|
# has been pressed.
|
|
self.callback = callback
|
|
self.config(width=300, height=90)
|
|
# Disable the button for resizing the window.
|
|
self.resizable(0, 0)
|
|
self.title("Enter Your Name")
|
|
self.entry_name = ttk.Entry(self)
|
|
self.entry_name.place(x=20, y=20, width=260)
|
|
self.button_done = ttk.Button(
|
|
self,
|
|
text="Done!",
|
|
command=self.button_done_pressed
|
|
)
|
|
self.button_done.place(x=20, y=50, width=260)
|
|
self.focus()
|
|
self.grab_set()
|
|
|
|
def button_done_pressed(self):
|
|
# Get the entered name and invoke the callback function
|
|
# passed when creating this window.
|
|
self.callback(self.entry_name.get())
|
|
# Close the window.
|
|
self.destroy()
|
|
|
|
|
|
class MainWindow(tk.Tk):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.config(width=400, height=300)
|
|
self.title("Main Window")
|
|
self.button_request_name = ttk.Button(
|
|
self,
|
|
text="Request name",
|
|
command=self.request_name
|
|
)
|
|
self.button_request_name.place(x=50, y=50)
|
|
self.label_name = ttk.Label(
|
|
self,
|
|
text="You have not entered your name yet."
|
|
)
|
|
self.label_name.place(x=50, y=150)
|
|
|
|
def request_name(self):
|
|
# Create the child window and pass the callback
|
|
# function by which we want to receive the entered
|
|
# name.
|
|
self.ventana_nombre = InputWindow(
|
|
callback=self.name_entered
|
|
)
|
|
|
|
def name_entered(self, name):
|
|
# This function is invoked once the user presses the
|
|
# "Done!" button within the secondary window. The entered
|
|
# name will be in the "name" argument.
|
|
self.label_name.config(
|
|
text="Your name is: " + name
|
|
)
|
|
|
|
|
|
main_window = MainWindow()
|
|
main_window.mainloop()
|