Building Your First Python GUI With Tkinter

Written by pictureinthenoise | Published 2022/11/14
Tech Story Tags: python | python-tutorials | learn-to-code-python | python-basics | tkinter | pythongui | gui

TLDRTkinter is Python's standard for building GUIs and is included with the Python Standard Library. This guide walks through the design of a simple GUI application to explain basic Tkinter concepts, including the layout of GUI elements, capturing user input, and binding GUI elements to callback methods. via the TL;DR App

Introduction

Perhaps you just started learning Python and have built one or two simple applications. Congratulations! But, now what? Well, how about building a graphical user interface ("GUI") to interact with your shiny, new Python app?

There are several options to build GUI applications with Python, including PyQt and wxPython. This guide, however, will introduce you to Tkinter.

Tkinter, which stands for "Tk interface", is Python's standard for building GUIs and is included with the Python Standard Library. It is a binding to the Tk GUI toolkit, a free, open-source library of GUI widgets that can be used to build graphical interfaces in a variety of programming languages.

This tutorial walks through the design of a basic GUI application that displays the local time in a timezone selected by the user. The steps build the application progressively and describe some of the key concepts when working with Tkinter, including the layout of GUI elements, capturing user input, and binding GUI elements to callback methods.

Prerequisites

To complete this guide, you will need to have:

  • Installed Python.
  • Installed pip.
  • Installed the pytz timezone library. The library can be installed using pip.

pip install pytz

This guide will use the terminology root window and main window interchangeably.

Step 1 - Create a New Python Application and Set Required Imports

Create a new Python application named timezone.py and add the following import statements to import the Tkinter, datetime, and pytz modules.

import tkinter as tk
import datetime
import pytz

Step 2 - Add Timezones

This guide incorporates the set of United States timezones which are a small fraction of the timezones supported by pytz. The functionality of the application can be extended by adding additional pytz timezone names. A complete list of timezones available with pytz can be output by running the following command:

print(pytz.all_timezones)

The set of United States timezones available via pytz is specified as a list.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

Step 3 - Create an Instance of the Tk Class

Instantiating the Tk class creates a root window which will serve as the main window of the timezone application's GUI.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()

Step 4 - Set Up the Main Window

This step configures the root window, particularly its title, geometry, and resizeability.

Step 4a - Setting the Main Window Title

The window title is set using the title method.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()
root.title("Simple Timezone Application")

Step 4b - Setting Main Window Width and Height

The window width and height values, expressed in pixels, can be assigned to variables to make the code easier to follow.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()
root.title("Simple Timezone Application")

window_width = 450
window_height = 175

Step 4c - Calculating Main Window Center Position

The main window can be placed anywhere on the screen, e.g. in the center, in the upper-left corner, etc. A centered window provides a nice "look" and the center position for the main window can be determined using the root window's winfo_screenwidth() and winfo_screenheight() methods along with some simple math. These two methods return the screen width and screen height which are used to calculate the appropriate (x,y) coordinates to center the main window. As with the window width and height, the center position values can also be assigned to variables to make the code more readable.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()
root.title("Simple Timezone Application")
window_width = 450
window_height = 175
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int((screen_width - window_width)/2)
center_y = int((screen_height - window_height)/2)

Note that the center_x and center_y values are also expressed in pixels and int is used to ensure both calculated values are integers.

Step 4d - Setting Main Window Geometry

The root window geometry, which specifies the size and position of the main window, is set using the root window's geometry method. To make things simple, you can use a formatted sting literal, or f-string, that will interpolate the geometry variable expressions at runtime. As per the following code block, the f-string literal is passed to the geometry method as a parameter.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()
root.title("Simple Timezone Application")
window_width = 450
window_height = 175
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int((screen_width - window_width)/2)
center_y = int((screen_height - window_height)/2)
root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")

Step 4e - Setting Main Window Resizability

The resizability of the root window along its x and y axes can be set via the root window's resizable method. The resizable method accepts height and width parameters, in that order. A True or non-zero value for either parameter specifies that the main window is resizable along the associated axis. A False or 0 value for either parameter specifies that the main window is not resizable along the given axis. The timer application will use a value of False for both height and width to prevent the main window from being resized.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()
root.title("Simple Timezone Application")
window_width = 450
window_height = 175
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int((screen_width - window_width)/2)
center_y = int((screen_height - window_height)/2)
root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
root.resizable(False, False)

Step 5 - Specify a Grid for Widget Layout

Widgets can be arranged on top of the main window in different ways using geometry managers. Generally, widgets can be arranged in 3 ways:

  1. Packed using the pack() method. The pack() geometry manager organizes widgets into blocks before they are added to a root window or parent widget. An imperfect but perhaps useful way to think about packing is to think about adding grocery items to a grocery bag. The items aren't necessarily added to pre-defined locations in the bag. Rather, they are packed one after another, using up available space until all items have been put in the bag. The pack() geometry manager works in a similar way.

  2. Placed using the place() method. The place() geometry manager places widgets into specific, pre-defined positions in the root window or parent widget. This geometry manager is obviously useful when building precise GUI layouts.

  3. Arranged as a grid using the grid() method. Grids are 2-dimensional row/column tables. Widgets are added to a grid by specifying the particular row and column where the widget should be placed. Grids are setup using the root window's columnconfigure and rowconfigure methods. Each of these methods has an index and a weight attribute. The index attribute specifies the position of the column or row using a starting basis of 0. The weight attribute specifies the size of a particular column or row relative to other columns. For example, if column 0 has weight 1 and column 1 has weight 3, then column 1 will be 3 times as large as column 0.

Grids are handy when placing elements next to each other and they are relatively simple to implement. The timezone application will use a main window grid divided into 4 equal columns.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()
root.title("Simple Timezone Application")
window_width = 450
window_height = 175
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int((screen_width - window_width)/2)
center_y = int((screen_height - window_height)/2)
root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
root.resizable(False, False)

root.columnconfigure(0, weight=1)
root.columnconfigure(1, weight=1)
root.columnconfigure(2, weight=1)
root.columnconfigure(3, weight=1)

Step 6 - Add Widgets

In this step, widgets are added to the main window grid defined in Step 4. The timezone application will use 3 TKinter widgets:

  • Label
  • Listbox
  • Button

Tkinter's Label and Button widgets are exactly as they are named. The Label widget allows the programmer to display text (i.e. a label), and the Button widget is used to display buttons. The Listbox widget is used to select a value (or values) from a list of options.

Of course, there are other widgets beyond the 3 that are listed above, such as the SpinBox, Entry, and Message widgets. The formal Tk commands documentation is a useful resource to learn more about the widgets available through the Tk toolkit.

Step 6a - Create Widgets

An instance of each widget class is created for a given widget used with the GUI design. The timezone application uses 4 widget instances: 2 Label widgets, 1 Listbox widget, and 1 Button widget.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()
root.title("Simple Timezone Application")
window_width = 450
window_height = 175
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int((screen_width - window_width)/2)
center_y = int((screen_height - window_height)/2)
root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
root.resizable(False, False)

root.columnconfigure(0, weight=1)
root.columnconfigure(1, weight=1)
root.columnconfigure(2, weight=1)
root.columnconfigure(3, weight=1)

# Instance of Label widget class for selection prompt.
select_timezone_label = tk.Label(root, text="Please select a timezone.")

# Instance of Listbox class for selection of timezone from list.
list_var = tk.Variable(value=timezones)
select_timezone_listbox = tk.Listbox(root, listvariable=list_var, height=1)

# Instance of Button class to get the local time in the selected timezone.
select_timezone_button = tk.Button(root, text="Get Time")

# Second instance of the Label class to display the local time in the selected timezone.
time_label = tk.Label(root, text="")

As seen in the code block above, each widget instance is "attached" to the root window. The Listbox widget also requires instantiation of a special Tkinter Variable() class which is used to supply the widget with the list of timezone options that the user can select from via the Listbox listvariable attribute. The text displayed by each Label widget can be configured using the text attribute. The text value for time_label is left blank since it will be set dynamically each time the user "gets" the time for a selected timezone.

Step 6b - Place Widgets on the Grid

The widget instances created in Step 5a can be placed on the grid defined in Step 4 using the grid() method. The timezone application will use the following grid() method attributes to define the layout:

  • column: Specifies the particular column where the widget will be placed using a 0 basis.

  • row: Specifies the particular row where the widget will be placed using a 0 basis.

  • columnspan: Specifies that widget should span the specified number of columns. For example, a widget with a columnspan=3 value will span 3 columns even if the widget itself is smaller than 3 columns.

  • sticky: Tkinter's default behavior is to place a widget in the horizontal and vertical center of the cell (i.e. particular row/column position) where it is placed. This default behavior can be overridden using the sticky attribute which uses compass-like values, including NW, N, NE, W, E, SW, S, and SE, to align the widget at a particular location within the widget's cell. For example, sticky=tK.W specifies that the widget should be aligned to the west corner of its grid cell. Analogously, sticky=tK.E specifies that the widget should be aligned to the east corner of its grid cell.

  • padx, pady: The padx and pady attributes are used to add x-axis and y-axis padding respectively with values specified in pixels. Naturally, padding can provide a more professional looking GUI ensuring that widgets do not "bump up" directly against the edges of the root window or other widgets.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()
root.title("Simple Timezone Application")
window_width = 450
window_height = 175
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int((screen_width - window_width)/2)
center_y = int((screen_height - window_height)/2)
root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
root.resizable(False, False)

root.columnconfigure(0, weight=1)
root.columnconfigure(1, weight=1)
root.columnconfigure(2, weight=1)
root.columnconfigure(3, weight=1)

# Instance of Label widget class for selection prompt.
select_timezone_label = tk.Label(root, text="Please select a timezone.")

# Instance of Listbox class for selection of timezone from list.
list_var = tk.Variable(value=timezones)
select_timezone_listbox = tk.Listbox(root, listvariable=list_var, height=1)

# Instance of Button class to get the local time in the selected timezone.
select_timezone_button = tk.Button(root, text="Get Time")

# Second instance of the Label class to display the local time in the selected timezone.
time_label = tk.Label(root, text="")

# Place widgets on grid.
select_timezone_label.grid(column=0, row=0, columnspan=4, sticky=tk.W, padx=10, pady=10)
select_timezone_listbox.grid(column=0, row=1, columnspan=3, sticky=tk.EW, padx=10, pady=10)
select_timezone_button.grid(column=4, row=1, sticky=tk.E, padx=10, pady=10)
time_label.grid(column=0, row=4, columnspan=4, sticky=tk.W, padx=10, pady=10)

Step 7 - Create Get Time Button Callback Method

A callback method needs to be defined to handle events when the user click on the select_timezone_button button created in Step 5.

Step 7a - Binding the Get Time Button

Before defining the callback logic, it is helpful to first bind the button to the method name that will eventually encompass the callback code. The bind() method can be used in conjunction with a lambda function to bind the select_timezone_button to the specified callback method. Also, note that the lambda function is used to pass references to the select_timezone_listbox and time_label widgets as callback parameters. These callback parameters are actually not necessary since select_timezone_listbox and time_label are within the global scope. However, it is arguably useful to demonstrate how arguments can be passed to the callback function.

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

root = tk.Tk()
root.title("Simple Timezone Application")
window_width = 450
window_height = 175
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int((screen_width - window_width)/2)
center_y = int((screen_height - window_height)/2)
root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
root.resizable(False, False)

root.columnconfigure(0, weight=1)
root.columnconfigure(1, weight=1)
root.columnconfigure(2, weight=1)
root.columnconfigure(3, weight=1)

# Instance of Label widget class for selection prompt.
select_timezone_label = tk.Label(root, text="Please select a timezone.")

# Instance of Listbox class for selection of timezone from list.
list_var = tk.Variable(value=timezones)
select_timezone_listbox = tk.Listbox(root, listvariable=list_var, height=1)

# Instance of Button class to get the local time in the selected timezone.
select_timezone_button = tk.Button(root, text="Get Time")

# Second instance of the Label class to display the local time in the selected timezone.
time_label = tk.Label(root, text="")

# Place widgets on grid.
select_timezone_label.grid(column=0, row=0, columnspan=4, sticky=tk.W, padx=10, pady=10)
select_timezone_listbox.grid(column=0, row=1, columnspan=3, sticky=tk.EW, padx=10, pady=10)
select_timezone_button.grid(column=4, row=1, sticky=tk.E, padx=10, pady=10)
time_label.grid(column=0, row=4, columnspan=4, sticky=tk.W, padx=10, pady=10)

select_timezone_button.bind("<Button>", lambda e, args=[select_timezone_listbox, time_label]: get_timezone_time(e, args))

Step 7b - Defining Callback Logic

The callback logic to handle button click events is defined below.

def get_timezone_time(e, args):
    select_timezone_listbox = args[0]
    time_label = args[1]
    selection_index = select_timezone_listbox.curselection()
    selected_timezone = select_timezone_listbox.get(selection_index)
    
    now_time = datetime.datetime.now()
    tz_time = now_time.astimezone(pytz.timezone(selected_timezone))
    tz_formatted = tz_time.strftime("%H:%M:%S")

    time_label.configure({"text": f"The time in {selected_timezone} is {tz_formatted}."})
    time_label.update()

The curselection() and get() methods are used to retrieve the user's selected timezone from the reference to the select_timezone_listbox widget. The user's current time is then converted into the selected timezone time. Finally, the configure method is used to change the text attribute of the reference to the time_label with the local time in the selected timezone. Note that the update() method is used to "force" the time_label widget to update itself with the new text value.

Step 8 - Complete and Run the Application

The root window's mainloop() method is applied as the last line of code. The mainloop() method will run the GUI in an infinite loop until the user exits.

The completed code is as follows:

import tkinter as tk
import datetime
import pytz

timezones = ["US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa"]

def get_timezone_time(e, args):
    select_timezone_listbox = args[0]
    time_label = args[1]
    selection_index = select_timezone_listbox.curselection()
    selected_timezone = select_timezone_listbox.get(selection_index)
    
    now_time = datetime.datetime.now()
    tz_time = now_time.astimezone(pytz.timezone(selected_timezone))
    tz_formatted = tz_time.strftime("%H:%M:%S")

    time_label.configure({"text": f"The time in {selected_timezone} is {tz_formatted}."})
    time_label.update()

root = tk.Tk()
root.title("Simple Timezone Application")
window_width = 450
window_height = 175
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int((screen_width - window_width)/2)
center_y = int((screen_height - window_height)/2)
root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")
root.resizable(False, False)

root.columnconfigure(0, weight=1)
root.columnconfigure(1, weight=1)
root.columnconfigure(2, weight=1)
root.columnconfigure(3, weight=1)

# Instance of Label widget class for selection prompt.
select_timezone_label = tk.Label(root, text="Please select a timezone.")

# Instance of Listbox class for selection of timezone from list.
list_var = tk.Variable(value=timezones)
select_timezone_listbox = tk.Listbox(root, listvariable=list_var, height=1)

# Instance of Button class to get the local time in the selected timezone.
select_timezone_button = tk.Button(root, text="Get Time")

# Second instance of the Label class to display the local time in the selected timezone.
time_label = tk.Label(root, text="")

# Place widgets on grid.
select_timezone_label.grid(column=0, row=0, columnspan=4, sticky=tk.W, padx=10, pady=10)
select_timezone_listbox.grid(column=0, row=1, columnspan=3, sticky=tk.EW, padx=10, pady=10)
select_timezone_button.grid(column=4, row=1, sticky=tk.E, padx=10, pady=10)
time_label.grid(column=0, row=4, columnspan=4, sticky=tk.W, padx=10, pady=10)

# Bind button to callback.
select_timezone_button.bind("<Button>", lambda e, args=[select_timezone_listbox, time_label]: get_timezone_time(e, args))

root.mainloop()

When the application is launched, the GUI should look like:

To use the application, click the list box and use your keyboard’s up/down cursor keys to scroll through the timezone options. Click on the Get Time button to display the current time in your selected timezone.

Next Steps

Congratulations on building your first Python GUI application with Tkinter! As mentioned in the Introduction, this guide was designed to introduce you to a few basic concepts. The Python Standard Library documentation and the Tk commands documentation references mentioned earlier are two fantastic resources to help you learn about more advanced Tkinter features and functionality.

Tkinter GUI applications are sometimes criticized as having a non-native look-and-feel. That may be true. But the toolkit widgets are highly configurable and Tkinter GUI applications can be built relatively quickly without the need to install any external Python packages.

Beyond formal documentation, there are innumerable tutorials available via the Internet to learn about building more sophisticated GUI applications with Tkinter.


Written by pictureinthenoise | Computational linguistics. At the end of the beginning.
Published by HackerNoon on 2022/11/14