This powerful GUI library creates some amazing things, with a little help.
Tkinter (tee-kay-inter) is a powerful library for developing graphic user interface (GUI) applications, on the Raspberry Pi where makers are concerned. In this edition of Secret Code, we take a deeper dive into the capabilities of Tkinter and what it can do.
THE BROAD OVERVIEW
Running terminal commands is fun, sure. But when our projects need to advance from the workbench to some type of real-world use, things need to get more sophisticated. Especially when your creation needs to be operated by someone other than yourself, often someone without code experience. Things need to be taken to a simpler interface which can be understood simply by looking at it.
One way to achieve this is with buttons and feedback mechanisms (screens, LEDs etc), but if you have a full touchscreen (or a standard screen and input device such as a mouse), then a GUI is generally the best approach. There are a few ways to create a GUI in the maker realm, but today we’re going to focus on Tkinter.
We have used Tkinter as the base for a variety of projects in the past. A GUI-based PWM controller all the way back in Issue #002, as well as an interface for our touchscreen timelapse camera slider in Issue #003. But we always focused on functionality over the aesthetics. Tkinter, after all, is fairly easy to get a rough implementation done, which makes it a great choice for simple applications. However, the user experience is easily improved with the inclusion of some more advanced graphics and styling.
STARTING OUT WITH TKINTER
We’re using a Raspberry Pi loaded with Raspbian Stretch to run our Tkinter GUI Applications. Out of the box, Raspbian Stretch comes with both Python 2, Python 3 and the Tkinter library installed. The code in this article uses Python 3 syntax.
To check which version you have installed, from the terminal run:
python --version
> Python 2.7.13
python3 --version
> Python 3.5.3
If running your application from the terminal, depending on your system set up, to use Python 3 you may have to run either:
python app.py
or
python3 app.py
Let’s dive right in and see some of the things we can do in Tkinter
Creating the Window
First, we will need to create the window instance. In Tkinter we can create a window for our GUI in just three lines of code:
from tkinter import *
root = Tk()
# your code here
root.mainloop()
First, we import Tkinter. This allows us to use the Tkinter package and classes.
Next, we create an instance of the Tk class and assign it to a variable. We will use root.
Finally, we call the mainloop() method. This will continue to run the Tkinter methods in our application in a loop, waiting for an event to occur until we close the window or instruct our code to exit.
Let’s try this out. Create a new file called app.py and enter the base code shown above. If you’re not using an IDE, run the following command in a terminal from the directory containing your Python code to run the program.
python3 app.py
You should now see the Tkinter window to house our apps. By default, the size of the window will only be relatively small, but can be easily re-sized.
Let’s now look at how to customise this window. For the following examples, add the additional code between the class instantiation and main loop as shown.
Customising the window
Background colour
To set a background colour add the following code:
root.configure(bg="black")
The bg option also supports hex colour codes:
root.configure(bg="#F9273E")
Window dimensions
We can specify the window dimensions by using the geometry method (measured in pixels).
root.geometry("700x500")
Or you can display fullscreen:
root.attributes("-fullscreen", True)
Keep in mind that you will get stuck in full-screen mode if you don’t create a way to exit. One way is to set up a key binding event that calls a function. In this example, we will call the end_fullscreen function when the escape key is pressed.
def end_fullscreen(event):
root.attributes("-fullscreen", False)
root.bind("<Escape>", end_fullscreen)
Add a window title
root.title("My Awesome App")
Menus in Tkinter
Tkinter also provides a unique widget for menus allowing you to quickly build menu's and bind the options to a callback function. We won't be exploring these in this edition but its good to know that they're available if you need them.
The grid Manager
Now that we have our window configured we can add some widgets such as text, buttons and inputs. But before we do that, let’s take a look at the grid system in Tkinter to learn how we can position our widgets. Tkinter uses a grid system with rows and columns similar to tables in a word processor or a spreadsheet. The rows and columns start at 0.
You can adjust the number of rows and columns as required.
The width and height of the rows and columns is calculated to fit the widgets within them. Widgets can also go across multiple cells on the grid by using the rowspan or columnspan option in the grid() method.
Example widgets using a grid layout:
w_1 = Label(height=5, width=30, bg="#92ae2a")
w_2 = Label(height=5, width=30, bg="#d71149")
w_3 = Label(height=5, width=30, bg="#00a6db")
w_4 = Label(height=5, width=30, bg="red")
w_5 = Label(text="Center!")
w_6 = Label(height=5, width=30, bg="blue")
w_7 = Button(text="Click me")
w_8 = Label(height=3, width=30, bg="green")
w_9 = Label(height=3, width=30, bg="white")
w_1.grid(row=0,column=0)
w_2.grid(row=0,column=1)
w_3.grid(row=0,column=2)
w_4.grid(row=1,column=0)
w_5.grid(row=1,column=1)
w_6.grid(row=1,column=2)
w_7.grid(row=2,column=0)
w_8.grid(row=2,column=1)
w_9.grid(row=2,column=2)
Adding Widgets
Labels
When adding a widget we first create an instance of its class, in this case, Label and assign it to our Tkinter instance.
label_1 = Label(root, text="Hello, World!")
We now have a label object which will be stored in memory. Before it is visible in the window though, we need to set its position. We will use grid positioning. The other positioning methods available are pack() and place().
label_1.grid(row=0, column=0)
Run the script and you should now see the label.
There are many attribute options available to customise widgets. We will only explore some of these. For the full listing of the options head over to http://effbot.org/tkinterbook/tkinter-index.htm#class-reference
In the following example, we are defining a lot. We adjust the foreground text colour, background colour, font, border, add padding to the y axis, and set the anchor to east ("e"), which aligns the text to the right:
label_1 = Label(root,
text="Hello, World!",
fg="#ffffff",
bg="red",
font="Verdana 24 bold",
width=20,
anchor="e",
pady=10,
borderwidth=5,
relief="solid")
Note: We can space the options over multiple lines to improve readability.
Entry Input
To capture user input we can add text inputs by using the Entry widget. The Entry widget allows input for one line of text. If you need multiple line input use the Text widget instead.
We’ll also add another label to place a description next to the Entry widget.
label_1 = Label(root, text="Hello, World!",
font="Verdana 24 bold")
label_2 = Label(root, text="What is your name?",
height=3)
entry_1 = Entry(root)
label_1.grid(row=0, column=0)
label_2.grid(row=1, column=0)
entry_1.grid(row=1, column=1)
Buttons
Lets now add a button inside our window:
button_1 = Button(root, text="Submit")
button_1.grid(row=2, column=1)
We now have a simple form, however clicking on the button does not do anything since we haven’t told our program to do something.
Next we will explore how to setup an event on the buttons widget and bind it to a function which executes when clicked.
Adding Logic
Let’s take these four widgets and add some logic that updates label_1 to display “Hello + your name” when you select the Submit button. Here’s the completed code.
from tkinter import *
def greeting():
label_1["text"] = "Hello, " + name.get() + "!"
root = Tk()
root.geometry("700x500")
root.title("My Awesome App")
name = StringVar()
label_1 = Label(root, text="Hello, World!",
font="Verdana 24 bold")
label_2 = Label(root, text="What is your name?",
height=3)
entry_1 = Entry(root, textvariable=name)
button_1 = Button(root, text="Submit",
command=greeting)
label_1.grid(row=0, column=0)
label_2.grid(row=1, column=0)
entry_1.grid(row=1, column=1)
button_1.grid(row=2, column=1)
root.mainloop()
First, create a variable and set the StringVar() Tkinter class since we are expecting to store a string from the Entry widget input. The other Tkinter variable Classes available are BooleanVar, DoubleVar and IntVar.
name = StringVar()
Now let’s bind this variable to the Entry widget. Update entry_1 and add the textvariable attribute with the name variable as the value.
entry_1 = Entry(root, textvariable=name)
Now when a user enters text the results are stored in the name variable. Next, we’ll create a function that takes the string stored in the name variable and uses it to update the text option on our first label.
def greeting():
label_1["text"] = "Hello, " + name.get() + "!"
Finally, add a click event to the button by adding the command option and assign our greeting function.
button_1 = Button(root, text="Submit", command=greeting)
When the user clicks the button, Python will execute the function. Now run the app to see the results:
As you’ve seen with the code above we can dynamically update the attribute options of our widgets by referring to the key such as label_1[“text”] or label_1[“bg”].
GETTING FANCY
We will build a GUI for a weather station that takes a reading from an environmental sensor and displays the temperature and humidity. We will also have a toggle button to switch from Celsius to Fahrenheit, and we will model this to suit a 400x250 touchscreen;
We are just building a proof of concept here to illustrate the code with mock data. You don’t require a touchscreen or sensor to follow along. If you get stuck you can find the final code in our online resources.
Let’s start with a base for our Tkinter program. Create a new file called weather.py and enter the following:
from tkinter import *
# add functions here
root = Tk()
root.title("Weather Station")
root.geometry("400x250")
# declare variables here
# add widgets here
root.mainloop()
Before starting to code, it is a good idea to sketch or wireframe where we would like to position the widgets.
Now we can work out what widgets will be required:
Now that we have a plan, we can begin by declaring the variables. Add the following:
fahrenheit = False
temperature_text = StringVar()
humidity_text = StringVar()
We will make use of the Fahrenheit variable later on when we create the toggle button.
The widgets
Now to create the widgets. We will use two labels, one for temperature and the other for humidity.
We can group these labels in another widget called Frame. Frames are good for grouping logically related widgets and essential when creating complex dynamic layouts. Frames are similar to divs in HTML as a parent container of child widgets. You can use multiple Frames to “change” the entire window content by swapping frames.
Create a Frame widget and set some padding so the container has space from the edge of the window.
frame_weather_data = Frame(root, padx = 10,
pady = 10)
Next, we will create the two labels and set their parent frame to the one just created. The frame also has a grid of its own.
label_temperature=Label(frame_weather_data,
textvariable=temperature_text,
anchor="w",
width=17,
font="Verdana 12 bold")
label_temperature.grid(row=0, column=0)
label_humidity=Label(frame_weather_data,
textvariable=humidity_text,
anchor="w",
width=19,
font="Verdana 12")
label_humidity.grid(row=1, column=0)
We have set the anchor options to align the widgets to the West(w) position. We’ve also linked the variables we created to each Label so that we can update the text.
Next, set the postion of the frame within the window:
frame_weather_data.grid(row=0, column=0)
We have our labels but there’s no data for them to display yet. Let’s make some functions that check for updates periodically, taking a reading from our mock sensor data.
First, we need to create the mock sensor data by returning a random int between a specified range.
def mock_sensor_data():
return {
"temperature": random.randint(1,35),
"humidity": random.randint(40,60)
}
We’ll also need to import the random package, place this at the top of the file below the Tkinter import:
from tkinter import *
import random
Next, create the following function which will fetch the mock sensor data then update the labels text:
def read_sensor():
# mock sensor data or connect your
# real sensor logic here
sensor_data = mock_sensor_data()
update_labels(sensor_data)
def update_labels(sensor):
temperature_text.set("Temperature: " +
str(sensor["temperature"]) + "°C")
humidity_text.set("Humidity: " +
str(sensor["humidity"]) + "%")
The functions are now in place to fetch the temperature and update the data, but we need to have a way to trigger this to happen. The following function will do these tasks then repeat every 5 seconds by calling itself, creating an update loop.
def check_for_updates():
read_sensor()
root.after(5000, check_for_updates)
Lastly, initiate the check_for_updates function when the program starts up by using the after_idle method. after_idle is a Tkinter callback method which will be executed when there are no more events to process in the mainloop. Add this just before the mainloop.
root.after_idle(check_for_updates)
root.mainloop()
With all these pieces in place, run the application. You should see the following:
Celsius to Fahrenheit Toggle
To convert between Celsius and Fahrenheit we need to create a toggle. We do this by adding a function that does the conversion.
def convert_to_fahrenheit(C):
F = (C * 9 / 5) + 32
return str(F)
Next, we modify the update_labels function to convert to Fahrenheit if the Fahrenheit variable is set to True.
def update_labels(sensor):
if fahrenheit:
temp = convert_to_fahrenheit
(sensor["temperature"]) + "°F"
else:
temp = str(sensor["temperature"]) + "°C"
temperature_text.set("Temperature: " + temp)
humidity_text.set("Humidity: " + str(sensor["humidity"]) + "%")
To create the toggle function, we “flip” the True/False boolean value that’s assigned to the Fahrenheit variable. We use “not”, which sets the opposite of the current boolean.
We also set global on the Fahrenheit variable as it’s located outside of the function scope.
def toggle_fahrenheit():
global fahrenheit
fahrenheit = not fahrenheit
read_sensor()
With the toggle logic in place, all that’s left is to create a button widget and bind it to the toggle_fahrenheit function when selected. We will also use the Place method to position the button to a particular set of coordinates.
button=Button(root, text="Toggle", height=3,
command=toggle_fahrenheit)
button.place(x=320, y=180)
Dynamic background images
To style our window we will add two background images that switch depending on the current temperature. One for hot and one for cold.
To achieve this, add the following code before the other widgets. It is important to put this code at the beginning of our code because the widgets are rendered in order. We don’t want the background image to appear on top, hiding the other widgets.
background_image = PhotoImage(file="hot.gif")
background = Label(root, image=background_image)
background.place
(x=0, y=0, relwidth=1, relheight=1)
Add in the two images (hot.gif and cold.gif) to the directory containing your Python script. We’ve provided the gif images in the online resources, or you can create your own. Just make sure they match the geometry dimensions of the Tkinter window we set. The PhotoImage class can read GIF, PGM and PPM images or a base64-encoded GIF file as a string. There are libraries available if you need to work with other file formats.
Run the application next. You should now see the background image. You may notice that, by default, the widgets will have a grey background. We cannot easily set a transparent background but we can set a background colour that matches the image.
To fix this, we need to apply a background colour to those widgets. Let’s store the colour in a vairable so that we can easily change it if required. Add:
primary_colour = "#bae4e3"
Next, add the “background=primary_colour” attribute to the frame, label_temperature, label_humidity and the button widgets.
button=Button(root,
text="Toggle",
background=primary_colour,
height=3,
command=toggle_fahrenheit)
Create the following function that takes the temperature and updates the background image if the condition is met.
def update_background_image(temp):
filename = "hot" if temp > 16 else "cold"
background_image["file"] = filename + ".gif"
Finally, add the following line to the end of the read_sensor function which calls the update_background_image function everytime new data is fetched.
update_background_image(sensor_data["temperature"])
WHERE TO FROM HERE?
Obviously, we have created a great looking GUI but with entirely fictitious mock-data. Now you can connect a real environmental sensor and add the logic to replace the mock data with real data.