Python GUI Development with Tkinter

This is the first installment of our multi-part series on developing GUIs in Python using Tkinter. Check out the links below for the next parts to this series:

Introduction

If you're reading this article, there's a chance that you are one of those people who appreciate software operated via a simple command-line interface. It's quick, easy on your system's resources, and probably much faster to use for a keyboard virtuoso like yourself. However, it's no secret that if we want to reach a wider user-base with our software, offering only a command-line solution might scare a large portion of potential users off. For most people, the most obvious way of interacting with a program is using a GUI – a Graphical User Interface.

While using a GUI, the user interacts with and manipulates the elements of the interface called widgets. Some widgets, like buttons and checkboxes, let the user interact with the program. Others, like windows and frames, serve as containers for other widgets.

There are many packages for building GUIs in Python, but there's only one such package that is considered a de facto standard, and is distributed with all default Python installs. This package is called Tkinter. Tkinter is Python's binding to Tk - an open-source, cross-platform GUI toolkit.

Creating your First Window

As mentioned before, Tkinter is available with standard Python installs, so regardless of your operating system, creating your first window should be super quick. All you need are 3 lines of code:

import tkinter

root = tkinter.Tk()

root.mainloop()  

Output:

After importing the tkinter package in line 1, in line 3 we create our application's main (root) window widget. In order for the program to work properly, there should only be one root window widget in our interface, and, because all other widgets will be lower in the hierarchy than root, it has to be created before any other widgets.

In line 5, we initialize the root's mainloop. Thanks to this line, the window remains in a loop that waits for events (such as user interaction) and updates the interface accordingly. The loop ends when the user closes the window, or a quit() method is called.

Adding Simple Widgets to the Root Window

In the following example, we'll learn the general two-step philosophy of creating widgets, that can be applied to all widgets except windows. The first step is to create an instance of a specific widget's class. In the second step, we have to use one of the available methods to place the new widget inside another already-existing widget (a parent widget). The simplest widget you can put in your Tkinter interface is a label, which simply displays some text. The following example creates a simple label widget:

import tkinter

root = tkinter.Tk()

simple_label = tkinter.Label(root, text="Easy, right?")

simple_label.pack()

root.mainloop()  

Output:

We create the Label class instance in line 5 of the code above. In the first argument we point to the label's desired parent widget, which in this example is our root window. In the second argument we specify the text we want the label to display.

Then, in line 7, we apply a method of orienting our label inside the root window. The simplest method of orienting widgets that Tkinter offers is pack(). The label is the only widget inside the window, so it's simply displayed in the middle of the window.

We'll learn more on how it works in the next example, when we add another widget to the window. Note that the window's size automatically adjusts to the widget placed inside it.

Adding a Functional Button

Now, let's add something the user can interact with. The most obvious choice is a simple button. Let's put a button in our window that gives us an additional way of closing our window.

import tkinter

root = tkinter.Tk()

root.title("Hello!")

simple_label = tkinter.Label(root, text="Easy, right?")  
closing_button = tkinter.Button(root, text="Close window", command=root.destroy)

simple_label.pack()  
closing_button.pack()

root.mainloop()  

Ouput:

In line 8 we create our Button class instance in a very similar way we created our label. As you can probably see, though, we added a command argument where we tell the program what should happen after the button is clicked. In this case root's dramatically-sounding destroy() method is called, which will close our window when executed.

In lines 10 and 11 we again use the pack() method. This time we can understand it a bit better, as we now use it to place two widgets inside the window. Depending on the order in which we pack our widgets, the method just throws them one on top of the other, centered horizontally. The window's height and width adjust to the widgets' sizes.

You probably noticed another new line. In line 5, we specify the root window's title. Unfortunately, the widest widget of our interface is not wide enough for the window's title to become visible. Let's do something about it.

Controlling the Window's Size

Let's take a look at three new lines that will let us easily resize our window.

import tkinter

root = tkinter.Tk()

root.title("Hello!")

root.resizable(width="false", height="false")

root.minsize(width=300, height=50)  
root.maxsize(width=300, height=50)

simple_label = tkinter.Label(root, text="Easy, right?")  
closing_button = tkinter.Button(root, text="Close window", command=root.destroy)

simple_label.pack()  
closing_button.pack()

root.mainloop()  

Output:

In line 7 we define if the program's user should be able to modify the window's width and height. In this case, both arguments are set to "false", so the window's size depends only on our code. If it wasn't for lines 9 and 10, it would depend on sizes of the widgets oriented inside the window.

However, in this example, we use root's minsize and maxsize methods to control the maximum and minimum values of our window's width and height. Here, we define exactly how wide and tall the window is supposed to be, but I encourage you to play with these three lines to see how the resizing works depending on the size of our widgets, and on what minimum and maximum values we define.

More about Widget Orientation

As you probably already noticed, using the pack() method does not give us too much control over where the widgets end up after packing them in their parent containers. Not to say the pack() method is not predictable – it's just that obviously, sometimes throwing widgets into the window in a single column, where one widget is placed on top of the previous one, is not necessarily consistent with our sophisticated sense of aesthetics. For those cases, we can either use pack() with some clever arguments, or use grid() – another method of orienting widgets inside containers.

First, let's maybe give pack() one more chance. By modifying lines 15 and 16 from the previous example, we can slightly improve our interface:

simple_label.pack(fill="x")  
closing_button.pack(fill="x")  

Output:

In this simple manner we tell the pack() method to stretch the label and the button all the way along the horizontal axis. We can also change the way pack() throws new widgets inside the window. For example, by using the following argument:

simple_label.pack(side="left")  
closing_button.pack(side="left")  

Output:

We can pack widgets in the same row, starting from the window's left side. However, pack() is not the only method of orienting the widgets inside their parent widgets. The method that gives the prettiest results is probably the grid() method, which lets us order the widgets in rows and columns. Take a look at the following example.

import tkinter

root = tkinter.Tk()

simple_label = tkinter.Label(root, text="Easy, right?")  
another_label = tkinter.Label(root, text="More text")  
closing_button = tkinter.Button(root, text="Close window", command=root.destroy)  
another_button = tkinter.Button(root, text="Do nothing")

simple_label.grid(column=0, row=0, sticky="ew")  
another_label.grid(column=0, row=1, sticky="ew")  
closing_button.grid(column=1, row=0, sticky="ew")  
another_button.grid(column=1, row=1, sticky="ew")

root.mainloop()  

Output:

To make this example a bit clearer, we got rid of the lines that changed the root window's title and size. In lines 6 and 8 we added one more label and one more button (note that clicking on it won't do anything as we haven't attached any command to it).

Most importantly though, pack() was replaced by grid() in all cases. As you can probably easily figure out, the arguments column and row let us define which cell of the grid our widget will occupy. Keep in mind that if you define the same coordinates for two different widgets, the one rendered further in your code will be displayed on top of the other one.

The sticky argument is probably not as obvious. Using this option we can stick the edges of our widgets to edges of their respective grid cells – northern (upper), southern (bottom), eastern (right) and western (left). We do that by passing a simple string that contains a configuration of letters n, s, e and w.

In our example, we stick the edges of all four widgets to their cells' eastern and western edges, therefore the string is ew. This results in the widgets being stretched horizontally. You can play with different configurations of those four letters. Their order in the string doesn't matter.

Now that you know two different methods of orienting the widgets, keep in mind that you should never mix grid() and pack() inside the same container.

Frames

Windows are not the only widgets that can contain other widgets. In order to make your complex interfaces clearer, it is usually a good idea to segregate your widgets into frames.

Let's try to do that with our four simple widgets:

import tkinter

root = tkinter.Tk()

frame_labels = tkinter.Frame(root, borderwidth="2", relief="ridge")  
frame_buttons = tkinter.Frame(root, borderwidth="2", relief="ridge")

simple_label = tkinter.Label(frame_labels, text="Easy, right?")  
another_label = tkinter.Label(frame_labels, text="More text")

closing_button = tkinter.Button(frame_buttons, text="Close window", command=root.destroy)  
another_button = tkinter.Button(frame_buttons, text="Do nothing")

frame_labels.grid(column=0, row=0, sticky="ns")  
frame_buttons.grid(column=1, row=0)

simple_label.grid(column=0, row=0, sticky="ew")  
another_label.grid(column=0, row=1, sticky="ew")

closing_button.pack(fill="x")  
another_button.pack(fill="x")

root.mainloop()  

Output:

Let's carefully go through the example shown above. In lines 5 and 6 we define two new Frame widgets. Obviously, in the first argument we point to their parent widget, which is the root window.

By default, the frames' borders are invisible, but let's say we would like to see where exactly they are placed. In order to show their borders, we have to give them a certain width (in our example, 2 pixels) and the style of relief (a 3D effect of sorts) in which the border will be drawn. There are 5 different relief styles to choose from - in our example, we use ridge.

Label and Button definitions were also modified slightly (lines 8-12). We wanted to place our labels in our frame_labels frame and our buttons in our frame_buttons frame. Thus, we had to replace their previous parent, root, with their respective new frame parents.

In lines 14 and 15, we orient the frames inside the root window using the grid() method. Then, we use the grid() method to orient the labels (lines 17-18), and the pack() method to orient the buttons (lines 20-21). The labels and buttons are now in separate containers, so nothing stops us from orienting the widgets using different methods.

Top Level Windows

Your interface shouldn't contain more than one root window – but you can create many windows that are children of the root window. The best way to do that is by using the Toplevel class.

import tkinter

root = tkinter.Tk()

new_window = tkinter.Toplevel()  
new_window.withdraw()

fame_labels = tkinter.Frame(root, borderwidth="2", relief="ridge")  
frame_buttons = tkinter.Frame(root, borderwidth="2", relief="ridge")

simple_label = tkinter.Label(frame_labels, text="Easy, right?")  
another_label = tkinter.Label(frame_labels, text="More text")

closing_button = tkinter.Button(frame_buttons, text="Close window", command=root.destroy)  
window_button = tkinter.Button(frame_buttons, text="Show new window", command=new_window.deiconify)

frame_labels.grid(column=0, row=0, sticky="ns")  
frame_buttons.grid(column=1, row=0)

simple_label.grid(column=0, row=0, sticky="ew")  
another_label.grid(column=0, row=1, sticky="ew")

closing_button.pack(fill="x")  
window_button.pack(fill="x")

root.mainloop()  

In the example above, we create our new window in line 5. Because a window is an entity that is not anchored inside any other widget, we don't have to point to its parent, nor orient it inside a parent widget.

We'd like to show the new window after a button is pressed. Line 5 displays it right away, so we use the withdraw() method in line 6 in order to hide it. We then modify the button definition in line 15.

Aside from the new variable name and text, the button now executes a command – the new_window object's method, deiconify, which will make the window reappear after the user clicks the window_button button.

Conclusions

As you can see, using Tkinter you can easily and quickly create GUIs for non-expert users of your software. The library is included in all Python installs, so building your first, simple window is only a couple of lines of code away. The examples shown above barely scratch the surface of the package's capabilities.

Keep reading and check out the second part of this Tkinter tutorial, which will teach you how to create complex, intuitive, and pretty Graphical User Interfaces.

Author image
Poznan, Poland
I use Python to develop bioinformatics tools that help biologists build and visualize structural models of important molecular machines.