Welcome, Python coder! If you've learned the basics of Python and want to start designing powerful GUI applications, this book is for you.
By now, you have no doubt experienced the power and simplicity of Python. Perhaps you've written web services, performed data analysis, or administered servers. Perhaps you've written a game, automated routine tasks, or simply played around with code. But now you're ready to tackle the GUI.
With so much emphasis on web, mobile, and server-side programming, the development of simple desktop GUI applications seems increasingly like a lost art; many otherwise experienced developers have never learned to create one. What a tragedy! Desktop computers still play a vital role in work and home computing, and the ability to build simple, functional applications for this ubiquitous platform should be a part of every software developer's toolbox. Fortunately, for Python coders, that ability is well within reach thanks to Tkinter.
In this chapter, you will cover the following topics:
- Discovering Tkinter—a fast, fun, and easy-to-learn GUI library built right into the Python standard library
- Learning about IDLE—an editor and development environment written in Tkinter and bundled with Python
- Creating two
Hello Worldapplications to learn the basics of writing a Tkinter GUI
The Tk widget library originates from the Tool Command Language (Tcl) programming language. Tcl and Tk were created by John Ousterman while he was a professor at Berkeley in the late 1980s as an easier way to program engineering tools being used at the university. Because of its speed and relative simplicity, Tcl/Tk rapidly grew in popularity among academic, engineering, and Unix programmers. Much like Python itself, Tcl/Tk originated on the Unix platform and only later migrated to macOS and Windows. Tk's practical intent and Unix roots still inform its design today, and its simplicity compared to other toolkits is still a major strength.
Tkinter is a Python interface to the Tk GUI library and has been a part of the Python standard library since 1994 with the release of Python version 1.1, making it the de facto GUI library for Python. Documentation for Tkinter, along with links for further study, can be found in the standard library documentation at https://docs.python.org/3/library/tkinter.html.
Python coders who want to build a GUI have several toolkit options to choose from; unfortunately, Tkinter is often maligned or ignored as a legacy option. To be fair, it's not a glamorous technology that you can describe in trendy buzzwords and glowing hype. However, Tkinter is not only adequate for a wide variety of applications, it also has the following advantages that can't be ignored:
- It's in the standard library: With few exceptions, Tkinter is available wherever Python is available. There is no need to install
pip, create virtual environments, compile binaries, or search the web for installation packages. For simple projects that need to be done quickly, this is a clear advantage.
- It's stable: While Tkinter development has not stopped, it is slow and evolutionary. The API has been stable for years, the changes mainly being additional functionality and bug fixes. Your Tkinter code will likely run unaltered for years or decades to come.
- It's only a GUI toolkit: Unlike some other GUI libraries, Tkinter doesn't have its own threading library, network stack, or filesystem API. It relies on regular Python libraries for such things, so it's perfect for applying a GUI to existing Python code.
- It's simple and no-nonsense: Tkinter is straightforward, old-school object-oriented GUI design. To use Tkinter, you don't have to learn hundreds of widget classes, a markup or templating language, a new programming paradigm, client-server technologies, or a different programming language.
Tkinter is not perfect, of course. It also has the following disadvantages:
- Look and feel: It's often derided for its look and feel, which still bear a few artifacts from the 1990s Unix world. This has improved a great deal in the last few years, thanks to updates in Tk itself and the addition of themed widget libraries. We'll learn how to fix or avoid some of Tkinter's more archaic defaults throughout the book.
- Complex widgets: It also lacks more complex widgets, like rich text or HTML rendering widgets. As we'll see later in this book, Tkinter gives us the ability to create complex widgets by customizing and combining its simple ones.
Tkinter might be the wrong choice for a game UI or slick commercial application; however, for data-driven applications, simple utilities, configuration dialogs, and other business logic applications, Tkinter offers all that is needed and more.
Tkinter is included in the Python standard library for the Windows and macOS distributions. That means that, if you have Python on these platforms, you don't need to do anything to install Tkinter.
However, we're going to be exclusively focused on Python 3.x for this book; so, you need to make sure that this is the version you've got installed.
You can obtain Python 3 installers for Windows from the python.org website by performing the following steps:
- Go to http://www.python.org/downloads/windows.
- Select the latest Python 3 release. At the time of writing, the latest version is 3.6.4, with 3.7 promising to be out by publishing time.
- Under the
Filessection, select the Windows executable installer appropriate to your system's architecture (x86 for 32-bit Windows, x86_64 for 64-bit Windows).
- Launch the downloaded installer.
- Click on
Customize installation. Make sure the
tcl/tk and IDLEoption is checked (it should be by default).
- Continue through the installer with all defaults.
As of this writing, macOS ships with Python 2 and Tcl/Tk 8.5 built in. However, Python 2 is scheduled to be deprecated in 2020, and the code in this book will not work with it, so macOS users will need to install Python 3 to follow this book.
Let's perform the following steps to install Python3 on macOS:
- Go to http://www.python.org/downloads/mac-osx/.
- Select the latest Python 3 release. At the time of writing, the latest version is 3.6.4, but 3.7 should be out by publication time.
- Under the
Filessection, select and download
macOS 64-bit/32-bit installer.
- Launch the
.pkgfile that you've downloaded and follow the steps of the install wizard, selecting defaults.
There is currently no recommended way to upgrade to Tcl/Tk 8.6 on macOS, though it can be done with third-party tools if you wish. Most of our code will work with 8.5, though special mention is made when something is 8.6 only.
Most Linux distributions include both Python 2 and Python 3, however, Tkinter is not always bundled with it or installed by default.
To find out if Tkinter is installed, open a Terminal and try the following command:
python3 -m tkinter
This should open a simple window showing some information about Tkinter. If you get
ModuleNotFoundError instead, you will need to use your package manager to install your distribution's Tkinter package for Python 3. In most major distributions, including Debian, Ubuntu, Fedora, and openSUSE, this package is called
IDLE is an integrated development environment that is bundled with the Windows and macOS Python distributions (it's readily available in most Linux distributions as well, usually as IDLE or IDLE3). IDLE is written in Python using Tkinter, and it provides us with not only an editing environment for Python, but also a great example of Tkinter in action. So, while IDLE's rudimentary feature set may not be considered professional grade by many Python coders, and while you may already have a preferred environment for writing Python code, I encourage you to spend some time using IDLE as you go through this book.
Let's get familiar with IDLE's two primary modes: shell mode and editor mode.
When you launch IDLE, you begin in shell mode, which is simply a Python Read-Evaluate-Print-Loop (REPL) similar to what you get when you type
python in a terminal window.
Take a look at the shell mode in the following screenshot:
IDLE's shell has some nice features that you don't get from the command-line REPL, like syntax highlighting and tab-completion. The REPL is essential to the Python development process, as it gives you the ability to test code in real time and inspect classes and APIs without having to write complete scripts. We'll use the shell mode in later chapters to explore the features and behaviors of modules. If you don't have a shell window open, you can open one by clicking on
Start, then selecting
Run, and searching for Python shell.
Editor mode is for creating Python script files, which you can later run. When the book tells you to create a new file, this is the mode you'll use. To open a new file in the editor mode, simply navigate to
New File in the menu or hit Ctrl + N on the keyboard.
The following is a window where you can start typing a script:
You can run your script without leaving IDLE by hitting F5 in the editor mode; the output will show up in a shell window.
Before we start coding with Tkinter, let's take a quick look at what you can do with it by inspecting some of IDLE's UI. Navigate to
Configure IDLE from the main menu to open IDLE's configuration settings, where you can change IDLE's fonts, colors and theme, keyboard shortcuts, and default behaviors, as shown in the following screenshot:
Consider some of the following components that make up this user interface:
- There are drop-down lists and radio buttons that allow you to select between different options
- There are many push buttons that you can click on to execute actions
- There is a text window that can display multi-colored text
- There are labeled frames that contain groups of components
Each of these components is known as a widget; we're going to meet these widgets and more throughout this book and learn how to use them as they've been used here. We'll begin, however, with something much simpler.
Let's learn the basics of Tkinter by creating a simple
Hello World script for Tkinter by performing the following steps:
- Create a new file in IDLE or your favorite editor, enter the following code, and save it as
"""Hello World application for Tkinter""" from tkinter import * from tkinter.ttk import * root = Tk() label = Label(root, text="Hello World") label.pack() root.mainloop()
- Run this in IDLE by hitting F5 or in your terminal by typing the following command:
You should see a very tiny window pop up with the text
Hello World as shown in the following screenshot:
- Close the window and return to your editor screen. Let's break down this code and talk about what it does:
from tkinter import *: This imports the Tkinter library into the global namespace. This isn't best practice, because it fills your namespace with a lot of classes, which you might accidentally overwrite, but it's okay for very small scripts.
from tkinter.ttk import *: This imports the
ttkor themed Tk widget library. We'll be using this library throughout the book, as it adds a number of useful widgets and improves the look of existing widgets. Since we're doing the star import here, our Tk widgets will be replaced by the better-looking
ttkwidgets wherever applicable (for instance, our
root = Tk(): This creates our root or master application object. This represents the primary top-level window and main execution thread of the application, so there should be one and only one instance of Tk for every application.
label = Label(root, text="Hello World"): This creates a new
Labelobject. As the name implies, a
Labelobject is just a widget for displaying text (or images). Looking closer at this line, we see the following:
- The first argument we pass to
parentor master widget. Tkinter widgets are arranged in a hierarchy starting with the root window, each widget being contained by another. Any time you create a widget, your first argument will be the widget object that contains the new widget. In this case, we're placing our
Labelobject on the main application window.
- The second argument is a keyword argument that specifies the text to be displayed on the
- We store the new
Labelinstance in a variable,
label, so that we can do more to it later.
- The first argument we pass to
label.pack(): This places the new label widget onto its
parentwidget. In this case, we're using the
pack()method, which is the simplest of threegeometry managermethods you can use. We'll learn about these in more detail in future chapters.
root.mainloop(): This final line starts our main event loop. This loop is responsible for processing all the events—keystrokes, mouse clicks, and so on—and it will run until the program is quit. This is usually the last line of any Tkinter script, since any code after it won't run until the main window is closed.
Take a few moments and play around with this script by adding more widgets before the
root.mainloop() call. You can add more
Label objects or try
Button (which creates a clickable button) or
Entry (which creates a text entry field). Just like
Label, these widgets are initialized with a
parent object (use
root) and a
text parameter. Don't forget to call
pack() on your widget to add them to the window.
You can also try commenting out the
ttk import, to see if you notice a difference in the look of the widgets. Depending on your OS, it may look different or not.
Creating a GUI the way we just did works okay for very small scripts, but a much more scalable approach is to subclass Tkinter widgets to create component widgets that we will then assemble into a completed application.
Subclassing is simply a way of creating a new class based on an existing one, adding or changing only what is different in the new class. We will use subclassing extensively in this book to extend the functionality of Tkinter widgets.
Let's build a more robust
Hello World script that demonstrates some patterns we'll use throughout the remainder of the book. Take a look at the following steps:
- Create a file called
better_hello_tkinter.pyand begin with the following lines:
"""A better Hello World for Tkinter""" import tkinter as tk from tkinter import ttk
This time, we aren't doing the star imports; instead, we'll keep Tkinter and the
ttk objects in their own namespaces. This keeps our global namespace from being cluttered up and eliminates a potential source of bugs.
Star imports (
from module import *) are seen often in Python tutorials and example code, but in production code they should be avoided. Python modules can contain any number of classes, functions, or variables; when you do a star import, you import all of them, which can lead to one import overwriting the objects imported from another module. If you find a module name cumbersome to type over and over, alias it to something short, as we've done with Tkinter.
- Next, we create a new class called
HelloView, as follows:
class HelloView(tk.Frame): """A friendly little module""" def __init__(self, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs)
Our class is subclassed from
Frame class is a generic Tk widget that is typically used as a container for other widgets. We can add any number of widgets to the
Frame class, then treat the whole thing as though it were a single widget. This is a lot simpler in the long run than individually placing every last button, label, and input on a single master window. The first order of business in the constructor is to call
super() function gives us a reference to the super class (the class we've subclassed, in this case,
tk.Frame). By calling the super class constructor and passing along
**kwargs, our new
HelloWidget class can take any arguments that
Frame can take.
In older versions of Python,
super() had to be invoked with the name of the child class and a reference to the current instance, such as
super(MyChildClass, self). Python 3 allows you to call it with no arguments, but you will probably encounter code that uses the older invocation.
- Next, we're going to create two Tkinter variable objects to store the name and greeting strings, as follows:
self.name = tk.StringVar() self.hello_string = tk.StringVar() self.hello_string.set("Hello World")
Tkinter has a collection of variable types including
BooleanVar. You might wonder why we'd use these when Python has perfectly good data types for all of these (and more!). Tkinter variables are more than just containers for data: they have special functionality that regular Python variables lack, such as the ability to automatically propagate changes to all the widgets that reference them or trigger an event when they're changed. Here we'll use them as a way to access the data in a widget without having to keep or pass around references to the widget itself.
Notice that setting a value to a Tkinter variable requires use of the
set() method, rather than direct assignment. Likewise, retrieving the data requires use of a
get() method. Here, we set the value of
Hello World. We start building our view by creating a
Label object and
Entry, as follows:
name_label = ttk.Label(self, text="Name:") name_entry = ttk.Entry(self, textvariable=self.name)
Label() invocation looks familiar, but the
Entry object gets a new argument:
textvariable. By passing a Tkinter
StringVar variable to this argument, the contents of the
Entry box will be bound to the variable, and we can access it without needing to reference the widget. Whenever a user enters text in the
self.name will immediately be updated wherever it appears.
- Now, let's create
Button, as follows:
ch_button = ttk.Button(self, text="Change", command=self.on_change)
In the preceding code, we again have a new argument,
command, which takes a reference to a Python function or method. We call a function or method passed this way a callback, and, as you might expect, this callback will be called when the button is clicked. This is the simplest way to bind functions to a widget; later, we'll learn a more flexible method that will allow us to bind various keystrokes, mouse clicks, and other widget events to function or method calls.
Make sure you don't actually call your callback at this point—it should be
self.on_change(). The callback should be a reference to the function or method, not the output from it.
hello_label = ttk.Label(self, textvariable=self.hello_string, font=("TkDefaultFont", 64), wraplength=600)
Here we've passed our other
self.hello_string to the
textvariable argument; on a label, the
textvariable variable determines what will be displayed. By doing this, we can change the text on the label by simply changing
self.hello_string. We'll also set a much larger font by using the
fontargument, which takes a tuple in the format
You can enter any font name you want here, but it must be installed on the system to work. Tk has some built-in aliases that map to sensible fonts on every platform, such as
TkDefaultFont used here. We'll learn more about using fonts in Tkinter in Chapter 8, Improving the Look with Styles and Themes.
wraplength argument specifies how wide the text can be before it wraps to the next line. We want our text to wrap when it reaches the edge of the window; by default, label text does not wrap, so it would be cut off at the edge of the window. By setting the wrap length to 600 pixels, our text will wrap at the width of the screen.
- So far, our widgets have been created, but not yet placed on
HelloView. Let's arrange our widgets as follows:
name_label.grid(row=0, column=0, sticky=tk.W) name_entry.grid(row=0, column=1, sticky=(tk.W + tk.E)) ch_button.grid(row=0, column=2, sticky=tk.E) hello_label.grid(row=1, column=0, columnspan=3)
In this case, we're adding our widgets using the
grid() geometry manager, rather than the
pack() geometry manager we used before. As the name implies,
grid() allows us to position widgets on their
parent object using rows and columns, much like a spreadsheet or HTML table. Our first three widgets are arranged across three columns in row
hello_label will be on the second row (row
stickyargument takes a cardinal direction (
W—you can either use strings or the Tkinter constants), which specifies which side of the cell the contents must stick to. You can add these together to stick the widget to multiple sides; for example, by sticking the
name_entrywidget to both the east and west sides, it will stretch to fill the whole width of the column. The
grid() call for
hello_label uses the
columnspan argument. As you might expect, this causes the widget to span three grid columns. Since our first row established three columns for the grid layout, we need to span all three if we want this widget to fill the width of the application. Finally, we'll finish the
__init__() method by adjusting the grid configuration:
In the preceding code, the
columnconfigure() method is used to make changes to a widget's grid columns. Here, we're telling it to weight column
1 (the second column) more than the others. By doing this, the second column of the grid (where our entry lives) will expand horizontally and squash surrounding columns to their minimum widths. There is also a
rowconfigure() method for making similar changes to grid rows.
- Before we finish our
HelloModuleclass, we have to create the callback for
ch_button, as follows:
def on_change(self): if self.name.get().strip(): self.hello_string.set("Hello " + self.name.get()) else: self.hello_string.set("Hello World")
To get the value of the text entry, we call the
get() method of its text variable. If this variable contains any characters (notice we strip the white space), we'll set our hello text to greet the name entered; otherwise, we'll just greet the whole world.
Notice by using the
StringVar objects we don't have to interact directly with the widgets. This saved us from having to keep a lot of widget references in our class, but, more importantly, our variable could be updated from any number of sources or update any number of destinations without us having to explicitly write code to do so.
HelloViewcreated, we move onto the actual application class, as follows:
class MyApplication(tk.Tk): """Hello World Main Application""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.title("Hello Tkinter") self.geometry("800x600") self.resizable(width=False, height=False)
This time, we subclass
Tk, which will represent our main application object. There is some debate in the Tkinter world whether or not this is best practice. Since there can be only one
Tk object in the application, it could theoretically create problems if we want multiple
MyApplication objects somewhere down the line; for simple, single-window applications, it's perfectly fine.
- As with our module, we call
super().__init__()and pass along any arguments. Notice we don't need a
parentwidget this time, since the
Tkobject is the root window and has no
parent. Then there are the following three calls to configure our application window:
self.title(): This call sets the window title, which usually appears in the task list and/or window bar in our OS environment.
self.geometry(): This call sets the size of our window in pixels, in the format
x * y(width x height).
self.resizable(): This call sets whether the program window can be resized. We're disabling resizing here, both in width and height.
- We finish our application class by adding our view to the main window, as follows:
HelloView(self).grid(sticky=(tk.E + tk.W + tk.N + tk.S)) self.columnconfigure(0, weight=1)
Notice that we create and place
HelloView in a single line of code. We do this in situations where we don't need to keep a reference to the widget, but since
grid() does not return a value, you'll have to stick to the two-statement version if you want to access the widget later in your code.
Because we want the view to fill the application window, our
grid() call sticks it to all sides of its cell, and our
columnconfigure() call causes the first column to expand. Note that we've omitted the
column arguments; without them, and
grid() simply uses the first column of the next available row (in this case,
- With our classes defined, we'll start the actual execution of the code, as follows:
if __name__ == '__main__': app = MyApplication() app.mainloop()
if __name__ == '__main__': is a common idiom to check if a script is being run directly, such as when we type
python3 better_hello_world.py at a terminal. If we were to import this file as a module into another Python script, this check would be false and the code after would not be run. It's a good practice to put your program's main execution code below this check so that you can safely reuse your classes and functions in larger applications.
MyApplication is a subclass of
Tk, so it acts as the root window. We only need to create it and then start its main loop. Take a look at the following screenshot:
This was certainly overkill for a
Hello World application, but it demonstrates the use of subclassing to segment our application into modules, which will vastly simplify layouts and code organization as we build larger programs.
Now that you've installed Python 3, learned to use IDLE, gotten a taste of the simplicity and power of Tkinter, and have seen how to begin structuring it for more complicated applications, it's time to start writing a real application.
In the next chapter, you'll start your new job at ABQ AgriLabs and be presented with a problem that will need to be solved with your programming skills and Tkinter. You will learn how to dissect this problem, develop a program specification, and design a user-friendly application that will be part of the solution.