Python GUI Guide: Introduction to Tkinter a learn.sparkfun.com tutorial

Available online at: http://sfe.io/t803

Contents

Introduction

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.

Raspberry Pi with sensor dashboard and live plot

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:

Optional Materials

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

Raspberry Pi LCD - 7" Touchscreen

Raspberry Pi LCD - 7" Touchscreen

LCD-13733
$60.00
54
Multimedia Wireless Keyboard

Multimedia Wireless Keyboard

WIG-14271
$29.95 $19.95
4

SmartiPi Touch

PRT-14059
8 Retired

Prepare the Software

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 the Tkinter package.

Follow the steps outlined in the Prepare Your Pi section of the Graph Sensor Data with Python and Matplotlib tutorial to install Raspbian and configure Python 3. You will only need to perform the last "Install Dependencies" step if you plan to replicate the final example in this guide (integrating Matplotlib with Tkinter).

Suggested Reading

If you aren't familiar with the following concepts, we recommend checking out these tutorials before continuing:

Raspberry gPIo

How to use either Python or C++ to drive the I/O lines on a Raspberry Pi.

Preassembled 40-pin Pi Wedge Hookup Guide

Using the Preassembled Pi Wedge to prototype with the Raspberry Pi B+.

Raspberry Pi 3 Starter Kit Hookup Guide

Guide for getting going with the Raspberry Pi 3 Model B and Raspberry Pi 3 Model B+ starter kit.

Getting Started with the Raspberry Pi Zero Wireless

Learn how to setup, configure and use the smallest Raspberry Pi yet, the Raspberry Pi Zero - Wireless.

Python Programming Tutorial: Getting Started with the Raspberry Pi

This guide will show you how to write programs on your Raspberry Pi using Python to control hardware.

How to Use Remote Desktop on the Raspberry Pi with VNC

Use RealVNC to connect to your Raspberry Pi to control the graphical desktop remotely across the network.

Graph Sensor Data with Python and Matplotlib

Use matplotlib to create a real-time plot of temperature data collected from a TMP102 sensor connected to a Raspberry Pi.
Please note: If you have trouble seeing any of the images throughout this tutorial, feel free to click on it to get a better look!
Python Logo

Hello, World!

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.

language:python
import tkinter as tk

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

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

# Lay out label
label.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).

Tkinter Hello, World! application

Code to Note

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

language:python
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.

Next, we create a root window by calling Tkinter's constructor, tk.Tk().

language:python
root = tk.Tk()

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:

language:python
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:

language:python
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.

Hierarchy diagram showing object ownership

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

language:python
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:

language:python
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.

Widget Description Example
Button Clickable area with text that calls an associated function whenever the user clicks in the area. Tkinter Button widget
Canvas General purpose widget that can display simple graphics or provide an area to implement a custom widget. Tkinter Canvas widget
Checkbutton Allows a user to read and select between two distinct values (e.g. on/off). Also known as a "checkbox." Tkinter Checkbutton widget
Entry Area that allows the user to enter one line of text. For entering multiple lines, use the Text widget. Tkinter Entry widget
Frame A container for other widgets. A Frame can be useful for grouping other widgets together in a complex layout. Tkinter Frame widget
Label Used to display text or an image that may not be edited by the user. Tkinter Label widget
LabelFrame A rectangular area that contains other widgets, but unlike a Frame, a border and label may be added to help group widgets together. Tkinter LabelFrame widget
Listbox Displays a list of text alternatives. The user can choose (highlight) one or more options. Tkinter Listbox widget
Menu Used to create menus and submenus within an interface. Can be used to create the always-shown "menu bar" popular in many GUI applications. Tkinter Menu widget
Menubutton Obsolete as of Tk 8.0. Use Menu widget instead. See Menu
Message Used to display static text, like Label, but allows for multiple lines, text wrapping, and maintaining aspect ratios. Tkinter Message widget
OptionMenu Drop-down (or pop-up) menu that allows users to select one option from several options. Tkinter OptionMenu widget
PanedWindow Container for one or more widgets split into multiple "panes." These panes can be resized by the user by dragging the serparator line(s) (known as "sashes"). Tkinter PanedWindow widget
Radiobutton Several Radiobuttons can be used together to allow the user to select one option out of a group of options. Tkinter Radiobutton widget
Scale User can select a numerical value by moving a slider. Tkinter Scale widget
Scrollbar Paired with a Canvas, Entry, Listbox, or Text widget to allow for scrolling within that widget. Tkinter Scrollbar widget
Spinbox Allows the user to select only one option out of a list of options. Tkinter Spinbox widget
Text Area used to display and edit multiple lines of text. Can be used as a full text editor by the user. Tkinter Text widget
Toplevel A container for other widgets (much like the Frame widget) that appears in its own window. It can be useful for creating other application windows or pop-up notifications. Tkinter Toplevel widget

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

language:python
import tkinter as tk

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

# Create a set of options and variable for the OptionMenu
options = ["Option 1", "Option 2", "Option 3"]
selected_option = tk.StringVar()
selected_option.set(options[0])

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

###############################################################################
# Create widgets

# Create widgets
button = tk.Button(root, text="Button")
canvas = tk.Canvas(root, bg='white', width=50, height=50)
checkbutton = tk.Checkbutton(root, text="Checkbutton")
entry = tk.Entry(root, text="Entry", width=10)
frame = tk.Frame(root)
label = tk.Label(root, text="Label")
labelframe = tk.LabelFrame(root, text="LabelFrame", padx=5, pady=5)
listbox = tk.Listbox(root, height=3)
menu = tk.Menu(root)
# Menubutton: deprecated, use Menu instead
message = tk.Message(root, text="Message", width=50)
optionmenu = tk.OptionMenu(root, selected_option, *options)
panedwindow = tk.PanedWindow(root, sashrelief=tk.SUNKEN)
radiobutton_1 = tk.Radiobutton( root, 
                                text="Option 1", 
                                variable=radio_option,
                                value=1)
radiobutton_2 = tk.Radiobutton( root, 
                                text="Option 2", 
                                variable=radio_option, 
                                value=2)
scale = tk.Scale(root, orient=tk.HORIZONTAL)
scrollbar = tk.Scrollbar(root)
spinbox = tk.Spinbox(root, values=(0, 2, 4, 10))
text = tk.Text(root, width=15, height=3)
toplevel = tk.Toplevel()

# Lay out widgets
button.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 instead
message.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

###############################################################################
# Add stuff to the widgets (if necessary)

# Draw something in the canvas
canvas.create_oval(5, 15, 35, 45, outline='blue')
canvas.create_line(10, 10, 40, 30, fill='red')

# Add a default value to the Entry widgets
entry.insert(0, "Entry")

# Create some useless buttons in the LabelFrame
button_yes = tk.Button(labelframe, text="YES")
button_no = tk.Button(labelframe, text="NO")

# Lay out buttons in the LabelFrame
button_yes.pack(side=tk.LEFT)
button_no.pack(side=tk.LEFT)

# Put some options in the Listbox
for item in ["Option 1", "Option 2", "Option 3"]:
    listbox.insert(tk.END, item)

# Add some options to the menu
menu.add_command(label="File")
menu.add_command(label="Edit")
menu.add_command(label="Help")

# Add the menu bar to the top of the window
root.config(menu=menu)

# Create some labels to add to the PanedWindow
label_left = tk.Label(panedwindow, text="LEFT")
label_right = tk.Label(panedwindow, text="RIGHT")

# Add the labels to the PanedWindow
panedwindow.add(label_left)
panedwindow.add(label_right)

# Put some default text into the Text widgets
text.insert(tk.END, "I am a\nText widget")

# Create some widgets to put in the Toplevel widget (window)
top_label = tk.Label(toplevel, text="A Toplevel window")
top_button = tk.Button(toplevel, text="OK", command=toplevel.destroy)

# Lay out widgets in the Toplevel pop-up window
top_label.pack()
top_button.pack()

###############################################################################
# Run!

root.mainloop()

Geometry Managers

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

Here is a table showing examples of the different geometry managers:

Manager Description Example
Grid The grid manager places widgets in a table format with rows and columns. It will avoid overlapping widgets and will resize rows/columns as necessary to fit the widgets. Tkinter grid layout example
Pack Pack is often the easiest geometry manager to use, as it just puts widgets in a single row or column (default). It "packs" the widgets by putting them side-by-side (or top-to-bottom). Tkinter pack layout example
Place The place geometry manager offers the most control but can be the most difficult to use. It allows you to specify the absolute or relative positions of the widgets in a window (or parent widget). Tkinter place layout example

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.

language:python
import tkinter as tk

###############################################################################
# Grid layout example

# Create the main window (grid layout)
root = tk.Tk()
root.title("Grid")

# Create widgets
label_grid_1 = tk.Label(root, text="Widget 1", bg='red')
label_grid_2 = tk.Label(root, text="Widget 2", bg='green')
label_grid_3 = tk.Label(root, text="Widget 3", bg='blue')

# Lay out widgets in a grid
label_grid_1.grid(row=0, column=2)
label_grid_2.grid(row=1, column=1)
label_grid_3.grid(row=2, column=0)

###############################################################################
# Pack layout example

# Create another window for pack layout
window_pack = tk.Toplevel()
window_pack.title("Pack")

# Create widgets
label_pack_1 = tk.Label(window_pack, text="Widget 1", bg='red')
label_pack_2 = tk.Label(window_pack, text="Widget 2", bg='green')
label_pack_3 = tk.Label(window_pack, text="Widget 3", bg='blue')

# Lay out widgets with pack
label_pack_1.pack()
label_pack_2.pack()
label_pack_3.pack()

###############################################################################
# Place layout example

# Create another window for pack layout
window_place = tk.Toplevel()
window_place.title("Place")

# Create widgets
label_place_1 = tk.Label(window_place, text="Widget 1", bg='red')
label_place_2 = tk.Label(window_place, text="Widget 2", bg='green')
label_place_3 = tk.Label(window_place, text="Widget 3", bg='blue')

# Lay out widgets with pack
label_place_1.place(relx=0, rely=0.1)
label_place_2.place(relx=0.1, rely=0.2)
label_place_3.place(relx=0.7, rely=0.6)

###############################################################################
# Run!

root.mainloop()

Variables

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:

Manager Description Examples
BooleanVar Works with Boolean values. True, False
DoubleVar Works with floating point values. -68.0, 2.718281
IntVar Works with integer values. -3, 0, 42
StringVar Works with strings. "Hello", "world!"

If you want to see this in action, run the following code:

language:python
import tkinter as tk

# Declare global variables
counter = None

# This function is called whenever the button is pressed
def count():

    global counter

    # Increment counter by 1
    counter.set(counter.get() + 1)

# Create the main window
root = tk.Tk()
root.title("Counter")

# Tkinter variable for holding a counter
counter = tk.IntVar()
counter.set(0)

# Create widgets (note that command is set to count and not count() )
label_counter = tk.Label(root, width=7, textvariable=counter)
button_counter = tk.Button(root, text="Count", command=count)

# Lay out widgets
label_counter.pack()
button_counter.pack()

# Run forever!
root.mainloop()

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

Example of a Tkinter IntVar being used to store a number

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:

language:python
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:

language:python
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).

GUI design sketch with a 3x3 grid overlay

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.

language:python
import tkinter as tk

# Declare global variables
temp_c = None
temp_f = None

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

    global temp_c
    global temp_f

    # Convert Celsius to Fahrenheit and update label (through textvariable)
    try:
        val = temp_c.get()
        temp_f.set((val * 9.0 / 5) + 32)
    except:
        pass

# Create the main window
root = tk.Tk()
root.title("Temperature Converter")

# Create the main container
frame = tk.Frame(root)

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

# Allow middle cell of grid to grow when window is resized
frame.columnconfigure(1, weight=1)
frame.rowconfigure(1, weight=1)

# Variables for holding temperature data
temp_c = tk.DoubleVar()
temp_f = tk.DoubleVar()

# Create widgets
entry_celsius = tk.Entry(frame, width=7, textvariable=temp_c)
label_unitc = tk.Label(frame, text="°C")
label_equal = tk.Label(frame, text="is equal to")
label_fahrenheit = tk.Label(frame, textvariable=temp_f)
label_unitf = tk.Label(frame, text="°F")
button_convert = tk.Button(frame, text="Convert", command=convert)

# Lay out widgets
entry_celsius.grid(row=0, column=1, padx=5, pady=5)
label_unitc.grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
label_equal.grid(row=1, column=0, padx=5, pady=5, sticky=tk.E)
label_fahrenheit.grid(row=1, column=1, padx=5, pady=5)
label_unitf.grid(row=1, column=2, padx=5, pady=5, sticky=tk.W)
button_convert.grid(row=2, column=1, columnspan=2, padx=5, pady=5, sticky=tk.E)

# Place cursor in entry box by default
entry_celsius.focus()

# Run forever!
root.mainloop()

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."

Tkinter temperature converter GUI example

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.

language:python
# Create the main container
frame = tk.Frame(root)

# Lay out the main container, specify that we want it to grow with window size
frame.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.

Hierarchy diagram showing how to use a frame as a container for widgets in Tkinter

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

language:python
# Allow middle cell of grid to grow when window is resized
frame.columnconfigure(1, weight=1)
frame.rowconfigure(1, weight=1)

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 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.

Example of Tkinter resizing the frame widget to fill the window

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:

Sticky anchors for widgets in Tkinter

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

language:python
# Place cursor in entry box by default
entry_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.

Having trouble seeing the diagrams? Click on them to see the full-size version!

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:

Pi Wedge Fritzing diagram to connect LED and button

Connecting directly to the Raspberry Pi:

Raspberry Pi Fritzing diagram to connect LED and button

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:

language:bash
pip install rpi.gpio

Copy the following code into a new file:

language:python
import tkinter as tk
from tkinter import font

import RPi.GPIO as GPIO

# Declare global variables
button_on = None
button_off = None

# Pin definitions
led_pin = 12

# This gets called whenever the ON button is pressed
def on():

    global button_on
    global button_off

    # Disable ON button, enable OFF button, and turn on LED
    button_on.config(state=tk.DISABLED, bg='gray64')
    button_off.config(state=tk.NORMAL, bg='gray99')
    GPIO.output(led_pin, GPIO.HIGH)

# This gets called whenever the OFF button is pressed
def off():

    global button_on
    global button_off

    # Disable OFF button, enable ON button, and turn off LED
    button_on.config(state=tk.NORMAL, bg='gray99')
    button_off.config(state=tk.DISABLED, bg='gray64')
    GPIO.output(led_pin, GPIO.LOW)

# Use "GPIO" pin numbering
GPIO.setmode(GPIO.BCM)

# Set LED pin as output and turn it off by default
GPIO.setup(led_pin, GPIO.OUT)
GPIO.output(led_pin, GPIO.LOW)

# Create the main window
root = tk.Tk()
root.title("LED Switch")

# Create the main container
frame = tk.Frame(root)

# Lay out the main container
frame.pack()

# Create widgets
button_font = font.Font(family='Helvetica', size=24, weight='bold')
button_on = tk.Button(frame, text="ON", width=4, command=on, 
                        state=tk.NORMAL, font=button_font, bg='gray99')
button_off = tk.Button(frame, text="OFF", width=4, command=off, 
                        state=tk.DISABLED, font=button_font, bg='gray64')

# Lay out widgets
button_on.grid(row=0, column=0)
button_off.grid(row=1, column=0)

# Run forever!
root.mainloop()

# Neatly release GPIO resources once window is closed
GPIO.cleanup()

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!

Virtual light switch on the Raspberry Pi with Tkinter and Python controlling an LED

Code to Note:

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

language:python
button_on.config(state=tk.DISABLED, bg='gray64')
button_off.config(state=tk.NORMAL, bg='gray99')

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

language:python
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:

language:python
import tkinter as tk

import RPi.GPIO as GPIO

# Declare global variables
pwm = None

# Pin definitions
led_pin = 12

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

    global pwm

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

# Use "GPIO" pin numbering
GPIO.setmode(GPIO.BCM)

# Set LED pin as output
GPIO.setup(led_pin, GPIO.OUT)

# Initialize pwm object with 50 Hz and 0% duty cycle
pwm = GPIO.PWM(led_pin, 50)
pwm.start(0)

# Create the main window and set initial size
root = tk.Tk()
root.title("LED Dimmer")
root.geometry("150x300")

# Create the main container
frame = tk.Frame(root)

# Lay out the main container (center it in the window)
frame.place(relx=0.5, rely=0.5, anchor=tk.CENTER)

# Create scale widget
scale = tk.Scale(   frame,
                    orient=tk.VERTICAL,
                    from_=100,
                    to=0,
                    length=200,
                    width=50,
                    sliderlength=50,
                    showvalue=False,
                    command=dim )

# Lay out widget in frame
scale.pack()

# Run forever!
root.mainloop()

# Stop, cleanup, and exit when window is closed
pwm.stop()
GPIO.cleanup()

Give the file a name like tkinter_dimmer.py and run it. You should see a Scale widget pop up in a new window. Try sliding it, and you should see the attached LED get brighter.

Virtual dimmer switch made with Python and Tkinter used to control the brightness of an LED

Code to Note:

When constructing our Scale widget, we need to set a good number of attributes:

language:python
scale = tk.Scale(   frame,
                    orient=tk.VERTICAL,
                    from_=100,
                    to=0,
                    length=200,
                    width=50,
                    sliderlength=50,
                    showvalue=False,
                    command=dim )

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:

language:python
import tkinter as tk

import RPi.GPIO as GPIO

# Declare global variables
root = None
canvas = None
circle = None

# Pins definitions
btn_pin = 4

# Parameters
canvas_width = 100
canvas_height = 100
radius = 30

# Check on button state and update circle color
def poll():

    global root
    global btn_pin
    global canvas
    global circle

    # Make circle red if button is pressed and black otherwise
    if GPIO.input(btn_pin):
        canvas.itemconfig(circle, fill='black')
    else:
        canvas.itemconfig(circle, fill='red')

    # Schedule the poll() function for another 10 ms from now
    root.after(10, poll)

# Set up pins
GPIO.setmode(GPIO.BCM)
GPIO.setup(btn_pin, GPIO.IN)

# Create the main window
root = tk.Tk()
root.title("Button Responder")

# Create the main container
frame = tk.Frame(root)

# Lay out the main container (center it in the window)
frame.place(relx=0.5, rely=0.5, anchor=tk.CENTER)

# Create a canvas widget
canvas = tk.Canvas(frame, width=canvas_width, height=canvas_height)

# Lay out widget in frame
canvas.pack()

# Calculate top left and bottom right coordinates of the circle
x0 = (canvas_width / 2) - radius
y0 = (canvas_height / 2) - radius
x1 = (canvas_width / 2) + radius
y1 = (canvas_height / 2) + radius

# Draw circle on canvas
circle = canvas.create_oval(x0, y0, x1, y1, width=4, fill='black')

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

# Run forever
root.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.

Pushing a button to control a Tkinter element on a Raspberry Pi

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!

language:python
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:

language:python
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.

We created a circle on the screen by drawing on the canvas with:

language:python
circle = canvas.create_oval(x0, y0, x1, y1, width=4, fill='black')

If you want to learn more about Tkinter's drawing methods, check out this guide on python-course.eu. Additionally, effbot.org has a good reference for all the different drawing methods.

Experiment 3: Sensor Dashboard

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.

Hardware Connections

Connect a TMP102 Temperature Sensor breakout and APDS-9301 Ambient Light Sensor breakout to the Raspberry Pi as shown in the diagrams below.

Connecting through a Pi Wedge:

Connecting TMP102 and APDS-9301 to a Raspberry Pi through a Pi Wedge

Connecting directly to the Raspberry Pi:

Connecting TMP102 and APDS-9301 to a Raspberry Pi

Sensor Modules

To simplify our I2C reading and writing, we're going to copy in Python modules to read data from the TMP102 and APDS-9301 sensors. Open a new file named tmp102.py:

language:bash
nano tmp102.py

Copy in the following Python code:

language:python
import smbus

# Module variables
i2c_ch = 1
bus = None

# TMP102 address on the I2C bus
i2c_address = 0x48

# Register addresses
reg_temp = 0x00
reg_config = 0x01

# Calculate the 2's complement of a number
def twos_comp(val, bits):
    if (val & (1 << (bits - 1))) != 0:
        val = val - (1 << bits)
    return val

# Read temperature registers and calculate Celsius
def read_temp():

    global bus

    # Read temperature registers
    val = bus.read_i2c_block_data(i2c_address, reg_temp, 2)
    temp_c = (val[0] << 4) | (val[1] >> 4)

    # Convert to 2s complement (temperatures can be negative)
    temp_c = twos_comp(temp_c, 12)

    # Convert registers value to temperature (C)
    temp_c = temp_c * 0.0625

    return temp_c

# Initialize communications with the TMP102
def init():

    global bus

    # Initialize I2C (SMBus)
    bus = smbus.SMBus(i2c_ch)

    # Read the CONFIG register (2 bytes)
    val = bus.read_i2c_block_data(i2c_address, reg_config, 2)

    # Set to 4 Hz sampling (CR1, CR0 = 0b10)
    val[1] = val[1] & 0b00111111
    val[1] = val[1] | (0b10 << 6)

    # Write 4 Hz sampling back to CONFIG
    bus.write_i2c_block_data(i2c_address, reg_config, val)

    # Read CONFIG to verify that we changed it
    val = bus.read_i2c_block_data(i2c_address, reg_config, 2)

Save the code with ctrl + x, press y, and press enter. This module allows us to call init() and read_temp() functions to initialize and read temperature data from the TMP102.

Similarly, we need to create a module for our APDS-9301. Create a new file named apds9301.py:

language:bash
nano apds9301.py

Copy in the following code:

language:python
import smbus

# Module variables
i2c_ch = 1
bus = None

# APDS-9301 address on the I2C bus
apds9301_addr = 0x39

# Register addresses
apds9301_control_reg = 0x80
apds9301_timing_reg = 0x81
apds9301_data0low_reg = 0x8C
apds9301_data1low_reg = 0x8E

# Initialize communications and turn on the APDS-9301
def init():

    global bus

    # Initialize I2C (SMBus)
    bus = smbus.SMBus(i2c_ch)

    # Read the CONTROL register (1 byte)
    val = bus.read_i2c_block_data(apds9301_addr, apds9301_control_reg, 1)

    # Set POWER to on in the CONTROL register
    val[0] = val[0] & 0b11111100
    val[0] = val[0] | 0b11

    # Enable the APDS-9301 by writing back to CONTROL register
    bus.write_i2c_block_data(apds9301_addr, apds9301_control_reg, val)

# Read light data from sensor and calculate lux
def read_lux():

    global bus

    # Read channel 0 light value and combine 2 bytes into 1 number
    val = bus.read_i2c_block_data(apds9301_addr, apds9301_data0low_reg, 2)
    ch0 = (val[1] << 8) | val[0]

    # Read channel 1 light value and combine 2 bytes into 1 number
    val = bus.read_i2c_block_data(apds9301_addr, apds9301_data1low_reg, 2)
    ch1 = (val[1] << 8) | val[0]

    # Make sure we don't divide by 0
    if ch0 == 0.0:
        return 0.0

    # Calculate ratio of ch1 and ch0
    ratio = ch1 / ch0

    # Assume we are using the default 13.7 ms integration time on the sensor
    # So, scale raw light values by 1/0.034 as per the datasheet
    ch0 *= 1 / 0.034
    ch1 *= 1 / 0.034

    # Assume we are using the default low gain setting
    # So, scale raw light values by 16 as per the datasheet
    ch0 *= 16;
    ch1 *= 16;

    # Calculate lux based on the ratio as per the datasheet
    if ratio <= 0.5:
        return (0.0304 * ch0) - ((0.062 * ch0) * ((ch1/ch0) ** 1.4))
    elif ratio <= 0.61:
        return (0.0224 * ch0) - (0.031 * ch1)
    elif ratio <= 0.8:
        return (0.0128 * ch0) - (0.0153 * ch1)
    elif ratio <= 1.3:
        return (0.00146 * ch0) - (0.00112*ch1)
    else:
        return 0.0

Save and exit with ctrl + x, y, and enter. Like our tmp102 module, we can call init() and read_lux() to initialize and read the ambient light values from the APDS-9301 sensor.

Note: Make sure that tmp102.py and apds9301.py are in the same directory as your main application code. Otherwise, your import statements will not be able to find your modules.

Code Part 1: Fullscreen Numerical Dashboard

Let's start by making a simple display that takes up the full screen and shows the numerical temperature and ambient light values. Copy the following code into a new file:

language:python
import tkinter as tk
import tkinter.font as tkFont

import tmp102
import apds9301

###############################################################################
# Parameters and global variables

# Declare global variables
root = None
dfont = None
frame = None
temp_c = None
lux = None

# Global variable to remember if we are fullscreen or windowed
fullscreen = False

###############################################################################
# Functions

# Toggle fullscreen
def toggle_fullscreen(event=None):

    global root
    global fullscreen

    # Toggle between fullscreen and windowed modes
    fullscreen = not fullscreen
    root.attributes('-fullscreen', fullscreen)
    resize()

# Return to windowed mode
def end_fullscreen(event=None):

    global root
    global fullscreen

    # Turn off fullscreen mode
    fullscreen = False
    root.attributes('-fullscreen', False)
    resize()

# Automatically resize font size based on window size
def resize(event=None):

    global dfont
    global frame

    # Resize font based on frame height (minimum size of 12)
    # Use negative number for "pixels" instead of "points"
    new_size = -max(12, int((frame.winfo_height() / 10)))
    dfont.configure(size=new_size)

# Read values from the sensors at regular intervals
def poll():

    global root
    global temp_c
    global lux

    # Update labels to display temperature and light values
    try:
        val = round(tmp102.read_temp(), 2)
        temp_c.set(val)
        val = round(apds9301.read_lux(), 1)
        lux.set(val)
    except:
        pass

    # Schedule the poll() function for another 500 ms from now
    root.after(500, poll)

###############################################################################
# Main script

# Create the main window
root = tk.Tk()
root.title("The Big Screen")

# Create the main container
frame = tk.Frame(root)

# Lay out the main container (expand to fit window)
frame.pack(fill=tk.BOTH, expand=1)

# Variables for holding temperature and light data
temp_c = tk.DoubleVar()
lux = tk.DoubleVar()

# Create dynamic font for text
dfont = tkFont.Font(size=-24)

# Create widgets
label_temp = tk.Label(frame, text="Temperature:", font=dfont)
label_celsius = tk.Label(frame, textvariable=temp_c, font=dfont)
label_unitc = tk.Label(frame, text="°C", font=dfont)
label_light = tk.Label(frame, text="Light:", font=dfont)
label_lux = tk.Label(frame, textvariable=lux, font=dfont)
label_unitlux = tk.Label(frame, text="lux", font=dfont)
button_quit = tk.Button(frame, text="Quit", font=dfont, command=root.destroy)

# Lay out widgets in a grid in the frame
label_temp.grid(row=0, column=0, padx=5, pady=5, sticky=tk.E)
label_celsius.grid(row=0, column=1, padx=5, pady=5, sticky=tk.E)
label_unitc.grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
label_light.grid(row=1, column=0, padx=5, pady=5, sticky=tk.E)
label_lux.grid(row=1, column=1, padx=5, pady=5, sticky=tk.E)
label_unitlux.grid(row=1, column=2, padx=5, pady=5, sticky=tk.W)
button_quit.grid(row=2, column=2, padx=5, pady=5)

# Make it so that the grid cells expand out to fill window
for 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 fullscreen
root.bind('<F11>', toggle_fullscreen)
root.bind('<Escape>', end_fullscreen)

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

# Initialize our sensors
tmp102.init()
apds9301.init()

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

# Start in fullscreen mode and run
toggle_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.

Fullscreen GUI with live sensor data made with Python and Tkinter

Code to Note:

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

language:python
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:

language:python
root.bind('<F11>', toggle_fullscreen)
root.bind('<Escape>', end_fullscreen)

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:

language:python
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:

language:python
root.bind('<Configure>', resize)

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

language:python
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:

language:python
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:

language:python
button_quit = tk.Button(frame, text="Quit", font=dfont, command=root.destroy)

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:

language:bash
sudo apt-get update
sudo apt-get install libatlas3-base libffi-dev at-spi2-core python3-gi-cairo
pip install cairocffi
pip install matplotlib

In a new file, copy in the following code:

language:python
import datetime as dt
import tkinter as tk
import tkinter.font as tkFont

import matplotlib.figure as figure
import matplotlib.animation as animation
import matplotlib.dates as mdates
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tmp102
import apds9301

###############################################################################
# Parameters and global variables

# Parameters
update_interval = 60000 # Time (ms) between polling/animation updates
max_elements = 1440     # Maximum number of elements to store in plot lists

# Declare global variables
root = None
dfont = None
frame = None
canvas = None
ax1 = None
temp_plot_visible = None


# Global variable to remember various states
fullscreen = False
temp_plot_visible = True
light_plot_visible = True

###############################################################################
# Functions

# Toggle fullscreen
def toggle_fullscreen(event=None):

    global root
    global fullscreen

    # Toggle between fullscreen and windowed modes
    fullscreen = not fullscreen
    root.attributes('-fullscreen', fullscreen)
    resize(None)   

# Return to windowed mode
def end_fullscreen(event=None):

    global root
    global fullscreen

    # Turn off fullscreen mode
    fullscreen = False
    root.attributes('-fullscreen', False)
    resize(None)

# Automatically resize font size based on window size
def resize(event=None):

    global dfont
    global frame

    # Resize font based on frame height (minimum size of 12)
    # Use negative number for "pixels" instead of "points"
    new_size = -max(12, int((frame.winfo_height() / 15)))
    dfont.configure(size=new_size)

# Toggle the temperature plot
def toggle_temp():

    global canvas
    global ax1
    global temp_plot_visible

    # Toggle plot and axis ticks/label
    temp_plot_visible = not temp_plot_visible
    ax1.collections[0].set_visible(temp_plot_visible)
    ax1.get_yaxis().set_visible(temp_plot_visible)
    canvas.draw()

# Toggle the light plot
def toggle_light():

    global canvas
    global ax2
    global light_plot_visible

    # Toggle plot and axis ticks/label
    light_plot_visible = not light_plot_visible
    ax2.get_lines()[0].set_visible(light_plot_visible)
    ax2.get_yaxis().set_visible(light_plot_visible)
    canvas.draw()

# This function is called periodically from FuncAnimation
def animate(i, ax1, ax2, xs, temps, lights, temp_c, lux):

    # Update data to display temperature and light values
    try:
        new_temp = round(tmp102.read_temp(), 2)
        new_lux = round(apds9301.read_lux(), 1)
    except:
        pass

    # Update our labels
    temp_c.set(new_temp)
    lux.set(new_lux)

    # Append timestamp to x-axis list
    timestamp = mdates.date2num(dt.datetime.now())
    xs.append(timestamp)

    # Append sensor data to lists for plotting
    temps.append(new_temp)
    lights.append(new_lux)

    # Limit lists to a set number of elements
    xs = xs[-max_elements:]
    temps = temps[-max_elements:]
    lights = lights[-max_elements:]

    # Clear, format, and plot light values first (behind)
    color = 'tab:red'
    ax1.clear()
    ax1.set_ylabel('Temperature (C)', color=color)
    ax1.tick_params(axis='y', labelcolor=color)
    ax1.fill_between(xs, temps, 0, linewidth=2, color=color, alpha=0.3)

    # Clear, format, and plot temperature values (in front)
    color = 'tab:blue'
    ax2.clear()
    ax2.set_ylabel('Light (lux)', color=color)
    ax2.tick_params(axis='y', labelcolor=color)
    ax2.plot(xs, lights, linewidth=2, color=color)

    # Format timestamps to be more readable
    ax1.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
    fig.autofmt_xdate()

    # Make sure plots stay visible or invisible as desired
    ax1.collections[0].set_visible(temp_plot_visible)
    ax2.get_lines()[0].set_visible(light_plot_visible)

# Dummy function prevents segfault
def _destroy(event):
    pass

###############################################################################
# Main script

# Create the main window
root = tk.Tk()
root.title("Sensor Dashboard")

# Create the main container
frame = tk.Frame(root)
frame.configure(bg='white')

# Lay out the main container (expand to fit window)
frame.pack(fill=tk.BOTH, expand=1)

# Create figure for plotting
fig = figure.Figure(figsize=(2, 2))
fig.subplots_adjust(left=0.1, right=0.8)
ax1 = fig.add_subplot(1, 1, 1)

# Instantiate a new set of axes that shares the same x-axis
ax2 = ax1.twinx()

# Empty x and y lists for storing data to plot later
xs = []
temps = []
lights = []

# Variables for holding temperature and light data
temp_c = tk.DoubleVar()
lux = tk.DoubleVar()

# Create dynamic font for text
dfont = tkFont.Font(size=-24)

# Create a Tk Canvas widget out of our figure
canvas = FigureCanvasTkAgg(fig, master=frame)
canvas_plot = canvas.get_tk_widget()

# Create other supporting widgets
label_temp = tk.Label(frame, text='Temperature:', font=dfont, bg='white')
label_celsius = tk.Label(frame, textvariable=temp_c, font=dfont, bg='white')
label_unitc = tk.Label(frame, text="C", font=dfont, bg='white')
label_light = tk.Label(frame, text="Light:", font=dfont, bg='white')
label_lux = tk.Label(frame, textvariable=lux, font=dfont, bg='white')
label_unitlux = tk.Label(frame, text="lux", font=dfont, bg='white')
button_temp = tk.Button(    frame, 
                            text="Toggle Temperature", 
                            font=dfont,
                            command=toggle_temp)
button_light = tk.Button(   frame,
                            text="Toggle Light",
                            font=dfont,
                            command=toggle_light)
button_quit = tk.Button(    frame,
                            text="Quit",
                            font=dfont,
                            command=root.destroy)

# Lay out widgets in a grid in the frame
canvas_plot.grid(   row=0, 
                    column=0, 
                    rowspan=5, 
                    columnspan=4, 
                    sticky=tk.W+tk.E+tk.N+tk.S)
label_temp.grid(row=0, column=4, columnspan=2)
label_celsius.grid(row=1, column=4, sticky=tk.E)
label_unitc.grid(row=1, column=5, sticky=tk.W)
label_light.grid(row=2, column=4, columnspan=2)
label_lux.grid(row=3, column=4, sticky=tk.E)
label_unitlux.grid(row=3, column=5, sticky=tk.W)
button_temp.grid(row=5, column=0, columnspan=2)
button_light.grid(row=5, column=2, columnspan=2)
button_quit.grid(row=5, column=4, columnspan=2)

# Add a standard 5 pixel padding to all widgets
for w in frame.winfo_children():
    w.grid(padx=5, pady=5)

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

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

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

# Call empty _destroy function on exit to prevent segmentation fault
root.bind("<Destroy>", _destroy)

# Initialize our sensors
tmp102.init()
apds9301.init()

# Call animate() function periodically
fargs = (ax1, ax2, xs, temps, lights, temp_c, lux)
ani = animation.FuncAnimation(  fig, 
                                animate, 
                                fargs=fargs, 
                                interval=update_interval)               

# Start in fullscreen mode and run
toggle_fullscreen()
root.mainloop()

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.

Raspberry Pi showing a live update of temperature and light sensor data wit Tkinter and Matplotlib

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:

Tkinter dashboard with live sensor data

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:

Graph Sensor Data with Python and Matplotlib

July 23, 2018

Use matplotlib to create a real-time plot of temperature data collected from a TMP102 sensor connected to a Raspberry Pi.

The key to embedding a Matplotlib graph into a Tkinter GUI is to work with Matplotlib's backend, which is why we import the following:

language:python
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

With that, we first create our Matplotlib figure and a set of axes to draw our plot on:

language:python
# Create figure for plotting
fig = figure.Figure(figsize=(2, 2))
fig.subplots_adjust(left=0.1, right=0.8)
ax1 = fig.add_subplot(1, 1, 1)

A few lines later, we create a Tkinter widget out of that figure:

language:python
# Create a Tk Canvas widget out of our figure
canvas = FigureCanvasTkAgg(fig, master=frame)
canvas_plot = canvas.get_tk_widget()

These lines use our imported FigureCanvasTkAgg function to take a figure and turn it into a Tkinter Canvas. We get a handle to this canvas and lay it out in our grid just like any other widget:

language:python
canvas_plot.grid(   row=0, 
                    column=0, 
                    rowspan=5, 
                    columnspan=4, 
                    sticky=tk.W+tk.E+tk.N+tk.S)

Instead of the periodic poll() callback that we used in the previous examples, we set up a FuncAnimation() to handle the polling and updating of the graph:

language:python
# Call animate() function periodically
fargs = (ax1, ax2, xs, temps, lights, temp_c, lux)
ani = animation.FuncAnimation(  fig, 
                                animate, 
                                fargs=fargs, 
                                interval=update_interval)

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.

For another example on importing Matplotlib into Tkinter, see this demo from the official Matplotlib documentation.

Resources and Going Further

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:

Try out the various widgets and play with different layouts to get the effect you are looking for.

Note: If you think the default Tkinter widgets look outdated (hello Windows 95!), check out the ttk themed widgets package. Widgets from this set have a much more modern and updated look to them.
Python Logo

Looking for even more inspiration? Check out these other Raspberry Pi projects:

Building Large LED Installations

Learn what it takes to build large LED installations from planning to power requirements to execution.

Bark Back Interactive Pet Monitor

Monitor and interact with pets through this dog bark detector project based on the Raspberry Pi!

Raspberry Pi Zero Helmet Impact Force Monitor

How much impact can the human body handle? This tutorial will teach you how to build your very own impact force monitor using a helmet, Raspberry Pi Zero, and accelerometer!

Using Flask to Send Data to a Raspberry Pi

In this tutorial, we'll show you how to use the Flask framework for Python to send data from ESP8266 WiFi nodes to a Raspberry Pi over an internal WiFi network.

learn.sparkfun.com | CC BY-SA 3.0 | SparkFun Electronics | Niwot, Colorado