Python GUI Guide: Introduction to Tkinter

Python is generally more popular as a sequential programming language that is called from the command line interface (CLI). However, several frameworks exist that offer the ability to create slick graphical user interfaces (GUI) with Python. Combined with a single board computer, like the Raspberry Pi, this ability to build GUIs opens up new possibilities to create your own dashboard for watching metrics, explore virtual instrumentation (like LabVIEW), or make pretty buttons to control your hardware.

In this tutorial, we’ll go through the basics of Tkinter (pronounced “Tee-Kay-Inter”, as it’s the “TK Interface” framework), which is the default GUI package that comes bundled with Python. Other frameworks exist, such as wxPython, PyQt, and Kivy. While some of these might be more powerful, Tkinter is easy to learn, comes with Python, and shares the same open source license as Python.

Later in the tutorial, we’ll show how to control various pieces of hardware from a Tkinter GUI and then how to pull a Matplotlib graph into an interface. The first part of the tutorial (Tkinter basics) can be accomplished on any computer without special hardware. The parts that require controlling hardware or reading from a sensor will be shown on a Raspberry Pi.

Notice: This tutorial was written with Raspbian version "June 2018" and Python version 3.5.3. Other versions may affect how some of the steps in this guide are performed.

Required Materials

To work through the activities in this tutorial, you will need a few pieces of hardware:

You have several options when it comes to working with the Raspberry Pi. Most commonly, the Pi is used as a standalone computer, which requires a monitor, keyboard, and mouse (listed below). To save on costs, the Pi can also be used as a headless computer (without a monitor, keyboard, and mouse).

Note that for this tutorial, you will need access to the Raspbian (or other Linux) graphical interface (known as the desktop). As a result, the two recommended ways to interact with your Pi is through a monitor, keyboard, and mouse or by using Virtual Network Computing (VNC).

Before diving in to Tkinter and connecting hardware, you’ll need to install and configure a few pieces of software. You can work through the first example with just Python, but you’ll need a Raspberry Pi for the other sections that involve connecting hardware (we’ll be using the RPi.GPIO and SMBus packages).

Tkinter comes with Python. If Python is installed, you will automatically have access to the Tkinter package.

Let’s start with a basic example. If you don’t have a Raspberry Pi, you can install Python on your computer to run this demo and the “Temperature Converter” experiment.

Run the Program

Copy the following into a new file. Save it, and give it a name like tkinter_hello.py.

Copy Code

import tkinter as tk

# Create the main windowroot = tk.Tk()root.title("My GUI")

# Create labellabel = tk.Label(root, text="Hello, World!")

# Lay out labellabel.pack()

# Run forever!root.mainloop()

Run the program from the command line with python tkinter_hello.py. You should see a new window pop up with the phrase “Hello, World!” If you expand the window, you should see the phrase “My GUI” set in the title bar (you might have to expand the window to see it).

Code to Note

Let’s break down the relatively simple program. In the first line,

Copy Code

import tkinter as tk

we import the Tkinter module and shorten the name to tk. We do this to save us some typing in the rest of the code: we just need to type tk instead of tkinter.

In other Tkinter guides, you might see the import written as from tkinter import *. This says to import all classes, functions, and variables from the Tkinter package into the global space. While this might make typing easier (e.g. you would only need to type Tk() instead of tk.Tk()), it has the downside of cluttering your global workspace. In a larger application, you would need to keep track of all these global variables in your head, which can be quite difficult! For example, Tkinter has a variable named E (which we’ll see in a later example), and it’s much easier to remember that you mean Tkinter’s version of E (rather than E from another module) by having to write tk.E.

This automatically creates a graphical window with the necessary title bar, minimize, maximize, and close buttons (the size and location of these are based on your operating system’s preferences). We save a handle to this window in the variable root. This handle allows us to put other things in the window and reconfigure it (e.g. size) as necessary. For example, we can change the name in the title bar by calling the title method in the root window:

Copy Code

root.title("My GUI")

In this window, we can add various control elements, known as widgets. Widgets can include things like buttons, labels, text entry boxes, and so on. Here, we create a Label widget:

Copy Code

label = tk.Label(root, text="Hello, World!")

Notice that when we create any widget, we must pass it a reference to its parent object (the object that will contain our new widget). In this example, we want the root window to be the parent object of our label (i.e. root will own your label object). We also set the default message in the label to be the classic “Hello, World!”

When it comes to creating GUIs with Tkinter, it’s generally a good idea to create your widgets first and then lay out your widgets together within the same hierarchy. In this example, root is at the top of our hierarchy followed by our label object under that.

After creating our label, we lay it out using the pack() geometry manager.

Copy Code

label.pack()

A geometry manager is a piece of code that runs (as part of the Tkinter framework–we don’t see the backend parts) to organize our widgets based on criteria that we set. pack() just tells the geometry manager to put widgets in the same row or column. It’s usually the easiest to use if you just want one or a few widgets to appear (and not necessarily be nicely organized).

Finally, we tell Tkinter to start running:

Copy Code

root.mainloop()

Note that if we don’t call mainloop(), nothing will appear on our screen. This method says to take all the widgets and objects we created, render them on our screen, and respond to any interactions (such as button pushes, which we’ll cover in the next example). When we exit out of the main window (e.g. by pressing the close window button), the program will exit out of mainloop().

Tkinter Overview

This section is meant to give you an overview of the “building blocks” that are available in Tkinter and is in no way a complete list of classes, functions, and variables. The official Python docs and TkDocs offer a more comprehensive overview of the Tkinter package. Examples will be discussed in more details throughout this tutorial, but feel free to refer back to these reference tables as you build your own application.

Widgets

A widget is a controllable element within the GUI, and all Tkinter widgets have a shared set of methods. The Tkinter guide on effbot.org offers an easy-to-read reference for the core widget methods (these are the methods that all widgets have access to, regardless of which individual widget you might be using).

The following table shows all the core widgets with an example screenshot. Click on the widget name to view its reference documentation from effbot.org.

The code below was used to create the example widgets in the above table. Note that they are for demonstration purposes only, as much of the functionality has not been implemented (e.g. no functions for button pushes).

Copy Code

import tkinter as tk

# Create the main windowroot = tk.Tk()root.title("My GUI")

# Create a set of options and variable for the OptionMenuoptions = "Option 1", "Option 2", "Option 3"selected_option = tk.StringVar()selected_option.set(options0)

# Create a variable to store options for the Radiobuttonsradio_option = tk.IntVar()

# Lay out widgetsbutton.pack(padx=5, pady=5)canvas.pack(padx=5, pady=5)checkbutton.pack(padx=5, pady=5)entry.pack(padx=5, pady=5)frame.pack(padx=5, pady=10)label.pack(padx=5, pady=5)labelframe.pack(padx=5, pady=5)listbox.pack(padx=5, pady=5)# Menu: See below for adding the menu bar at the top of the window# Menubutton: deprecated, use Menu insteadmessage.pack(padx=5, pady=5)optionmenu.pack(padx=5, pady=5)panedwindow.pack(padx=5, pady=5)radiobutton_1.pack(padx=5)radiobutton_2.pack(padx=5)scale.pack(padx=5, pady=5)scrollbar.pack(padx=5, pady=5)spinbox.pack(padx=5, pady=5)text.pack(padx=5, pady=5)# Toplevel: does not have a parent or geometry manager, as it is its own window

Just instantiating (creating) a widget does not necessarily mean that it will appear on the screen (with the exception of Toplevel(), which automatically creates a new window). To get the widget to appear, we need to tell the parent widget where to put it. To do that, we use one of Tkinter’s three geometery managers (also known as layout managers).

A geometry manager is some code that runs on the backend of Tkinter (we don’t interact with the geometry managers directly). We simply choose which geometry manager we want to use and give it some parameters to work with.

The three geometry managers are: grid, pack, and place. You should never mix geometry managers within the same hierarchy, but you can embed different managers within each other (for example, you can lay out a frame widget with grid in a Toplevel and then use pack to put different widgets within the frame).

The code below was used to create the examples shown in the above table. Note that it creates 3 windows (1 with the Tk() constructor call and 2 others with Toplevel()) and uses different geometry managers to lay out 3 widgets in each.

If you want to dynamically change a widget’s displayed value or text (e.g. change the text in a Label), you need to use one of Tkinter’s Variables. This is because Python has no way of letting Tkinter know that a variable has been changed (known as tracing). As a result, we need to use a wrapper class for these variables.

Each Tkinter Variable has a get() and set() method so you can read and change with the Variable’s value. This page also gives you a list of other methods available to each Variable. You must choose the appropriate Variable for the values you plan to work with, and this table shows you which Variables you have available:

You should see a window appear with a number and button. Try pressing the button a few times to watch the number increment.

In the program, we create a button that calls the count() function whenever it is pressed. We also create an IntVar named counter and set its initial value to 0. Take a look at where we create the label:

Copy Code

label_counter = tk.Label(root, width=7, textvariable=counter)

You’ll notice that we assign our IntVar (counter) to the textvariable parameter. This tells Tkinter that whenever the counter variable is changed, the Label widget should automatically update its displayed text. This saves us from having to write a custom loop where we manually update all the displayed information!

From here, all we need to do is worry about updating the counter variable each time the button is pressed. In the count() function, we do that with:

Copy Code

counter.set(counter.get() + 1)

Notice that we can’t get the IntVar’s value by the normal means, and we can’t set it with the equals sign (=). We need to use the get() and set() methods, respectively.

Experiment 1: Temperature Converter

Before we connect any hardware, it can be helpful to try a more complicated example to get a feel for laying out a GUI and using Tkinter to build your vision. We’ll start with a simple example: a Celsius-to-Fahrenheit temperature converter.

The Vision

Before writing any code, pull out a piece of paper and a pencil (or dry erase board and marker). Sketch out how you want your GUI to look: where should labels go? Do you want text entry fields at the top or the bottom? Should you have a “Quit” button, or let the user click the window’s “Close” button?

Here is what I came up with for our simple converter application. Note that we can divide it into a simple 3x3 grid, as shown by the red lines (more or less–we might need to nudge some of the widgets to fit into their respective cells).

As a result, we can determine that using the grid manager would be the best for this layout.

Implementation

Copy the following code into your Python editor.

Copy Code

import tkinter as tk

# Declare global variablestemp_c = Nonetemp_f = None

# This function is called whenever the button is presseddef convert():

Give it a name like tkinter_temp_converter.py, and run it. You should see a new window appear with an area to type in a number (representing degrees Celsius). Press the “Convert” button, and the equivalent temperature in Fahrenheit should appear in the label next to “°F.”

Code to Note

Let’s break down some of the code we saw in the previous example. After importing Tkinter, we define our convert() function. This is a callback, as we pass the function as an argument to our button, and Tkinter calls our function whenever a button press event occurs (i.e. we never call convert() directly).

While not completely necessary, you’ll notice that we declared temp_c and temp_f as global variables, as we want to be able to access them from within the function, and they were not passed in as parameters. Additionally, you’ll see that we calculate temp_f within a try/except block. If the user enters a string (instead of numbers), our conversion will fail, so we just tell Python to ignore the exception and don’t perform any calculation on the incorrectly typed data.

The next thing we do is create a frame within our main window and use the fill and expand parameters to allow it to grow with the window size.

Copy Code

# Create the main containerframe = tk.Frame(root)

# Lay out the main container, specify that we want it to grow with window sizeframe.pack(fill=tk.BOTH, expand=True)

In our previous examples, we had been placing our widgets directly in the main window. This is generally not considered good practice, so we pack a frame within the window first, and then put our widgets within the frame. By doing this, we can more easily control how the widgets behave when we resize the window.

Before creating our widgets, we tell the frame that it should expand with the window if the user resizes it:

Note that because we used the pack() manager for the frame, we must tell column/rowconfigure that the location of the cell is (1, 1). If were using these methods with a grid() manager, we could specify different rows and columns. The weight parameter tells the geometry manager how it should resize the given row/column proportionally to all the others. By setting this to 1 for each configure method, we’re telling Tkinter to just resize the whole frame to fill the window. To learn more, see the Handling Resize section of this TkDocs article.

After that, we create our Tkinter Variables, temp_c and temp_f and create all of our widgets that belong in the frame. Note that we assign our convert() function to our button with command=convert parameter assignment. By doing this, we tell Tkinter that we want to call the convert() function whenever the button is pressed.

We then lay out each of the widgets using the grid geometry manager. We go through each widget and assign it to a cell location. There is nothing in the top-left cell (0, 0), so we leave it blank. We put the text entry widget in the next column over (0, 1) followed by the units (0, 2). We replicate this process for the next row (row 1) with the label for “is equal to,” the solution, and the units for Fahrenheit. Finally, we add the convert button to the bottom-right. However, because the button is wider than the unit labels, we have it take up two cells: (2, 1) and (2, 2). We do that with columnspan=2, which works like the “merge” command in most modern spreadsheet programs.

Note that in some of the .grid() layout calls, we use the sticky parameter. This tells the geometry manager how to put the widget in its cell. By default (i.e. not using sticky), the widget will be centered within the cell. tk.W says that the widget should be left-aligned in its cell, and tk.E says it should be right-alighted. You can use the following anchor constants to align your widget:

Finally, before running our mainloop, we tell Tkinter that the Entry box should have focus.

Copy Code

# Place cursor in entry box by defaultentry_celsius.focus()

This places the cursor in the entry box so the user can immediately begin typing without having to click on the Entry widget.

Experiment 2: Lights and Buttons

Let’s connect some hardware! If you want to dig deeper into user interface design, a book on design theory, like this one, might be a good place to start. By controlling hardware, we can begin to connect GUI design to the real world. Want to make your own Nest-style thermostat? This is a good place to start.

Hardware Connections

We’ll start with a few basic examples that show how to control an LED and respond to a physical button push. Connect the LED, button, and resistors as shown in the diagrams.

If you have a Pi Wedge, it can make connecting to external hardware on a breadboard easier. If you don’t, you can still connect directly to the Raspberry Pi with jumper wires.

Connecting through a Pi Wedge:

Connecting directly to the Raspberry Pi:

Code Part 1: LED Light Switch

Depending on your version of Raspbian, the RPi.GPIO package may or may not be already installed (e.g. Raspbian Lite does not come with some Python packages pre-installed). In a terminal, enter the following:

Save your file with a name like tkinter_switch.py and run it with python tkinter_switch.py. You should see a new window pop up with two buttons: ON and OFF. OFF should be grayed out, so try pressing ON. The LED should turn on and OFF should now be the only available button to press. Press it, and the LED should turn off. This is the software version of a light switch!

Code to Note:

In the on() and off() function definitions, we enable and disable the buttons by using the .config() method. For example:

.config() allows us to dynamically change the attributes of widgets even after they’ve been created. In our switch example, we change their state and bg (background color) to create the effect of the switch being active or “grayed out.”

You might also notice that we use the font module within Tkinter to create a custom font for the buttons. This allows us to change the typeface as well as make the font bigger and bolder. We then assign this new font to the buttons' text with font=button_font.

At the end of the code, we place the following line after our root.mainloop():

Copy Code

GPIO.cleanup()

This line tells Linux to release the resources it was using to handle all the pin toggling that we were doing after we close the main window. Without this line, we would get a warning next time we ran the program (or tried to use the RPi.GPIO module).

Code Part 2: Dimmer Switch

Since we proved we can turn an LED on and off, let’s try dimming it. In fact, let’s make a virtual dimmer switch! In a new file, copy in the following code:

Copy Code

import tkinter as tk

import RPi.GPIO as GPIO

# Declare global variablespwm = None

# Pin definitionsled_pin = 12

# This gets called whenever the scale is changed--change brightness of LEDdef dim(i):

global pwm

# Change the duty cycle based on the slider value pwm.ChangeDutyCycle(float(i))

By default, scales appear horizontally, so we use orient=tk.VERTICAL to have it work like a dimmer switch you might find in a house. Next, the Scale will count from 0 to 100 by default, but 0 will be at the top (in the vertical orientation). As a result, we swap the from_ and to parameters so that 0 starts at the bottom. Also, notice that from_ has an underscore after it; from is a reserved keyword in Python, so Tkinter had to name it something else. We use 0 through 100, as those are the acceptable values for the pwm.ChangeDutyCycle() method parameter.

We can adjust the size and shape of the Scale with length and width. We made it a little bigger than default so that you can manipulate it more easily on a touchscreen. The slider part of the Scale (the part you click and drag) can by sized with sliderlength. Once again, we make the slider larger so that it’s easier to work with on a touchscreen.

By default, the numerical value of the Scale is shown next to the slider. We want to turn that off to provide a cleaner interface. The user only needs to drag the slider to a relative position; the exact number does not quite translate to perceived brightness anyway.

Additionally, by setting command=dim, we tell Tkinter that we want to call the dim() function every time the slider is moved. This allows us to set up a callback where we can adjust the PWM value of the LED each time the user interacts with the Scale.

Finally, notice that the dim(i) function now takes a parameter, i. Unlike our button functions in previous examples (which do not take any parameters), the Scale widget requires its callback (as set by command=dim) to accept one parameter. This parameter is the value of the slider; each time the slider is moved, dim(i) is called and i is set to the value of the slider (0-100 in this case).

Code Part 3: Respond to a Button Push

Blinking LEDs is fun, but what about responding to some kind of user input (I don’t mean on the screen)? Responding to physical button pushes can be important if you don’t want your user to have to use a touchscreen or mouse and keyboard. In a new file, enter the following code:

# Schedule the poll() function to be called periodicallyroot.after(10, poll)

# Run foreverroot.mainloop()

Save it (with a name like tkinter_button.py), and run it. You should see a black circle appear in the middle of your window. When you press the button on the breadboard, the circle should turn red. Neat.

Code to Note:

The most important part of this example is how to poll for a physical button press (or any other hardware interaction on the Pi’s GPIO pins). You might have noticed that root.mainloop() is blocking. That is, it takes over your program, and your Python script essentially stops running while it sits in the mainloop method. In reality, there is a lot going on in the background: Tkinter is looking for and responding to events, resizing widgets as necessary, and drawing things to your screen. But for our purposes, it looks like the script just sits there while the GUI is displayed (if the user closes the main GUI window, root.mainloop() will exit).

Since we have a blocking method call, how do we check for a button push? That’s where the .after() method comes into play. We set up the poll() function to be called every 10 ms without being in any sort of loop. Since we’ve moved into the realm of event-driven programming, we must do everything with callbacks!

Copy Code

root.after(10, poll)

This line says that after 10 ms, the poll() function should be called. The bulk of the poll() function is fairly straightforward: we see if the button has been pressed (the button’s GPIO pin is low), and we change the color of the circle in the canvas if it is. However, the end of the function is very important:

Copy Code

root.after(10, poll)

That’s right, at the very end of the poll() function, we tell our main window that it should call the poll() function again! This makes it so that we are checking the state of the button many times every second. We will use this concept of polling for hardware information (separately from the GUI mainloop()) in the next experiment.

In this next experiment, we’re going to connect a couple of I2C sensors and display their information in real time on our monitor. We start by just showing the sensor’s numerical values and then bring in Matplotlib to create a live updating graph of that data. Note that these are just example sensors; feel free to use whatever sensors you’d like for your particular application.

# Make it so that the grid cells expand out to fill windowfor i in range(0, 3): frame.rowconfigure(i, weight=1)for i in range(0, 3): frame.columnconfigure(i, weight=1)

# Bind F11 to toggle fullscreen and ESC to end fullscreenroot.bind('<F11>', toggle_fullscreen)root.bind('<Escape>', end_fullscreen)

# Have the resize() function be called every time the window is resizedroot.bind('<Configure>', resize)

# Initialize our sensorstmp102.init()apds9301.init()

# Schedule the poll() function to be called periodicallyroot.after(500, poll)

# Start in fullscreen mode and runtoggle_fullscreen()root.mainloop()

Save the file with a name like tkinter_fullscreen.py and run it. Your entire screen should be taken over by the GUI, and you should see the local ambient temperature and light values displayed. Try covering the light sensor or breathing on the temperature sensor to change their values. Press esc to exit fullscreen or press F11 to toggle fullscreen on and off.

Code to Note:

To control having our application take up the entire screen, we use the following method:

Copy Code

root.attributes('-fullscreen', fullscreen)

where the fullscreen variable is a boolean (True or False). If you look toward the end of the code, you’ll see the following two lines:

These bind the key presses F11 and esc to the toggle_fullscreen() and end_fullscreen() functions, respectively. These allow the user to control if the application takes up the entire screen or is in a window.

We also use the rowconfigure() and columnconfigure() methods again to control how the grid cells resize within the window. We combine this with a dynamic font:

Copy Code

dfont = tkFont.Font(size=-24)

Note that the negative number (-24) means we want to specify the font size in pixels instead of “points.” We also have our resize() function called every time the window is resized with the following:

Copy Code

root.bind('<Configure>', resize)

In our resize() function, we calculate a new font size based on the height of the resized frame with:

Copy Code

new_size = -max(12, int((frame.winfo_height() / 10)))

This says that the new font size should be the height of the frame divided by 10, but no smaller than 12. We turn it into a negative value, as we want to specify font height in pixels instead of points (once again). We then set the new font size with:

Copy Code

dfont.configure(size=new_size)

Try it! With the application running, press esc to exit fullscreen mode and try resizing the window. You should see the text grow and shrink as necessary. It’s not perfect, as certain aspect ratios will cut off portions of the text, but it should work without a problem in fullscreen mode (the intended application).

If you are using a touchscreen, you might not have an easy way for users to resize the window or quit out of the application (in some instances, that might be a good thing, but for our example, we want users to be able to exit). To accomplish this, we add a “Quit” button to our GUI:

We assign the callback function to be root.destroy. This is a built-in method within Tkinter that says to close the associated window and exit out of mainloop.

You’ll also notice that we are relying on the after() method again to call our poll() function at regular intervals.

Code Part 2: Complete Dashboard with Plotting

Now it’s time to get fancy. Let’s take the basic dashboard concept and add plotting. To do this, we’ll need to pull in the Matplotlib package. If you have not already installed it, run the following commands in a terminal:

Save the program (with a fun name like tkinter_dashboard.py), and run it. You should see your sensor data displayed as numerical values as well as a plot that updates once per minute.

Try pushing the “Toggle Temperature” and “Toggle Light” buttons. You should see the graph of each one disappear and reappear with each button press. This demonstrates how you can make an interactive plot using both Tkinter and Matplotlib.

You can update the update_interval variable to have the sensors polled more quickly, but it can also be fun to poll once per minute (default) and let it run for a day, as I did in my office:

If you look closely at the graph, you can see that the temperatures fell a little after 7pm, rose again, and then fell once more just before the workday started at 9am the following morning. We can surmise that the building air conditioning was running at those times to make it cooler.

Additionally, you can see that someone came into the office in the 6-7pm timeframe, as the ambient light value picked up for a short amount. Considering I did not move the sensors the next day, it looks like either more lights were on, or it was a sunnier day outside, as more light was falling on the sensor.

Code to Note:

There is a lot going on in this example, so we’ll try to cover it as succinctly as possible. Many of the concepts from the previous example, like binding key presses to trigger toggling fullscreen, are still present. Animating a graph is covered in the previous Python tutorial that introduced Matplotlib (specifically, the section about updating a graph in real time). If you are not familiar with Matplotlib, we recommend working through the following tutorial:

In the animate() function, we read the sensors' data (just like we did in poll()) and append it to the end of some arrays. We use this to redraw the plots on the axes (which are ultimately drawn on the Tkinter canvas widget). Note that we used .fill_between() to create the translucent red graph for temperature and a regular .plot() to create the basic blue line graph for light value.

Creating a GUI can be a good way to offer an easy-to-use interface for your customer or create a slick-looking dashboard for yourself (and your team). If you would like to learn more about Tkinter, we recommend the following resources: