Create a New Window in Tk (tkinter)

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:

/images/create-a-new-window-in-tk-tkinter/toplevel-tkinter.gif

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:

/images/create-a-new-window-in-tk-tkinter/toplevel-input-tkinter.gif

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()

Comments