ICS 32 Winter 2022
Notes and Examples: Tkinter Basics


Background

When we interact with software on our own computers and mobile devices, more often than not, we interact with graphical user interfaces (GUIs). Programs with GUIs stand in stark contrast to the programs we've built this quarter, which have featured text-mode user interfaces that print text in the Python shell and read text similarly. While there is plenty of real software out there that uses only scrolling, textual shells, we spend a lot of our time in day-to-day use of computers and mobile devices manipulating GUIs instead, so it's natural that we should want to learn how to build GUIs ourselves. Python can help: Among other things in its standard library, it includes a module called tkinter — technically, an interface to a GUI toolkit called Tk, which is where it gets its name — that allows us to build GUIs using Python. This code example begins our exploration of the tkinter module.


Where to find more information about tkinter

When you want to know more about the tkinter library, there is a place where you can get a lot of good information. Almost surely, you'll want to do things in your own GUIs that we didn't end up having time to cover in lecture and don't show up in these examples. In general, I'm happy to help, though you'll find that my knowledge of this library is not encyclopedic, so there's a chance you'll stump me and I'll send you, most particularly, to a good online tkinter reference linked below.


Creating an empty GUI window

As a first step toward learning to build a GUI, we can create a completely empty GUI window using tkinter surprisingly easily. Fire up a Python shell and type these three things into it.

>>> import tkinter
>>> window = tkinter.Tk()
>>> window.mainloop()

Creating a tkinter.Tk object creates a tkinter-based GUI, which causes a window to pop up on your screen; however, that window will be (more or less) inactive. Notice that when you call the mainloop() method on the window, the Python shell becomes inactive — you aren't given another prompt to enter your next statement or expression. However, once you close the window (in the normal way you close a window using your operating system), the Python shell will then prompt you for another statement or expression. While that might seem like an irrelevant detail, it reveals something important about the way that tkinter-based GUIs behave: They are built using event-based programming, which is similar to what we did when we used PyGame, but the difference is that the loop that processes input events is inside of the tkinter library, rather than being something we'll be writing ourselves.


Event-based programming

We make a tkinter GUI active by calling the mainloop() method on its main window. The name mainloop actually tells us a lot about what's actually going on. A tkinter GUI has a main loop (also sometimes called an event loop) that watches for a variety of input events, such as mouse movements, mouse button clicks, or pressing and releasing keys on the keyboard. This is not only a complex task, but one that all GUIs require, so rather than require every implementor to handle these tasks manually, tkinter handles them automatically. Unlike in PyGame, tkinter provides the event loop; we don't have to write it.

That main loop is why calling the mainloop() method causes your own code to stop making progress: The loop doesn't end until the window is dismissed. This leaves open an interesting question: If tkinter circles through a long-running main loop as long as the window is displayed, how can your code do anything?

The answer lies in the use of a technique called event-based programming. While tkinter "runs the show," so to speak, you tell it what events your code would be interested in being told about. When any of those events happens, a function or method in your code will be called to process that event; when that function or method returns, tkinter's main loop goes back to watching for inputs. There are two lessons that we can take from this:

  1. Unlike the programs we've been writing to date, in a program with a tkinter-based GUI, we don't handle all of the control flow in the program; instead, we create the GUI, "wire up" a set of event handlers, and our code is inactive until one of those event handlers is called.
  2. If we were to write an event-handling function or method that ran for a long time — say, one that downloaded a large file via the Internet — the entire GUI will be unresponsive until our event handler returned. We won't worry about this for now, since our event handlers will be fast in early examples, but it will be a problem worth keeping in mind as we broaden our skills. (Note that this is the same problem we face in PyGame if we spend too much time in any one iteration of our game loop. Frames of our animation will then be dropped, our ability to handle inputs will be paused, and so on. And it's a problem for the same reason: It's getting to the top of our main loop that allows our program to make progress, so doing anything that prevents us from getting to the top of the main loop again for an indeterminate amount of time is problematic.)

Widgets and options

A window in a tkinter GUI contains a collection of building blocks called widgets. tkinter includes a variety of built-in widgets that will sound familiar when you think about GUI-based programs you've used; examples include buttons, text labels, fields into which you can enter text, checkboxes, menus, and scrollbars. In addition to using the widgets built into the library, you can also build your own if you need something custom that isn't provided, though that's not a bridge we'll need to cross in our work.

Each widget has a set of options that can be configured when the widget is first created, by passing keyword arguments to its constructor. For example, we might create a button this way:

button = tkinter.Button(master = window, text = 'Hello!', font = ('Helvetica', 20))

Here, we've created a button and set three of its options:


Layout

So, as we've seen, a window is a container in which a collection of widgets is displayed. But if there's a window with a label, several buttons, and a checkbox, where will the widgets be displayed within the window? This issue is handled by something called a layout manager (also called a geometry manager), whose role is to decide how to arrange widgets within an area, and how that arrangement should change as the area's size changes (e.g., when a window is resized). There are two layout managers provided by tkinter: pack and grid. You can also build your own (using a third layout manager called place), though that's well beyond the scope of what we'll do this quarter.

You can specify the layout rules for each widget by calling one of three methods on it: pack(), grid(), or place(). pack() is, by far, the simplest, but its simplicity means that it lacks the flexibility to express anything but a very simple layout. grid() is built around a few relatively straightforward rules, but those rules turn out to be surprisingly flexible; we'll see that in subsequent examples.


Behaviors and events

As we talked about above, tkinter's main loop watches for various inputs and converts them to events. Your code can register an interest in those events by specifying functions or methods that tkinter will call each time one of those events occur. (Remember that tkinter's main loop is "in charge" here. It will call your function or method, then when your code returns, control will flow back into tkinter's main loop to process the next event.)

Some widgets have behaviors built into them, which convert lower-level events like keypresses or mouse movement into higher-level behaviors like "The button was pressed." For example, the Button widget has an option called command; if you set this option to a function that takes no parameters, the function will be called whenever the button is pressed.

def hello_button_pressed() -> None:
    # Do whatever you want to do when the button is pressed
    ...

...

button = tkinter.Button(
    master = window, text = 'Hello!', font = ('Helvetica', 20),
    command = hello_button_pressed)

Notice that we're not setting the command option to be the result of calling the hello_button_pressed function (i.e., there are no parentheses after hello_button_pressed when we set the command option). Instead, we're setting it to the function itself, literally asking tkinter to call this function later, when the button is pressed. This is the essence of event-based programming: We specify the functions that should be called later when events occur.

Event binding

When a widget doesn't expose exactly the behavior you need, you can achieve what you need by instead doing what's called event binding. Binding to an event on some widget is done by calling the bind() method on that widget. For example, given the button we created previously, we could bind to the event that's generated when the mouse cursor moves into the button by doing this:

def cursor_entered_button(event: tkinter.Event) -> None:
    # Do whatever you want to do when the button is entered
    ...

...

button.bind('<Enter>', cursor_entered_button)

The first parameter to bind() is the description of the event (or sequence of events) you're interested in. In this case, we specified the name '<Enter>', which is an event that's generated when the mouse cursor moves into a widget. The second parameter is an event handler function; event handler functions are required to take a single parameter, into which will be passed an event object that describes the event. Different kinds of events carry different attributes, though they all carry certain ones (e.g., the attribute widget, which specifies which widget the event applied to). For example, mouse movement events tell you where the mouse was moved to, keyboard events tell you which key was pressed, and so on. (This is similar to the event objects we saw in PyGame.)

For more information about what events can be bound, how to specify modifiers, how to combine events into sequences, and what attributes are available within event objects for different kinds of events, check out Chapter 54 of the tkinter reference.


A first example

To bring all of these concepts together into a single example, the code example below demonstrates a tkinter GUI containing a single button, whose text changes when you move your mouse into and out of it, and which causes a message to be printed to the Python shell when clicked.