Chapter 7. Graphical User Interfaces

“Here’s Looking at You, Kid”

For most software systems, a graphical user interface (GUI) has become an expected part of the package. Even if the GUI acronym is new to you, chances are that you are already familiar with such interfaces—the windows, buttons, and menus that we use to interact with software programs. In fact, most of what we do on computers today is done with some sort of point-and-click graphical interface. From web browsers to system tools, programs are routinely dressed up with a GUI component to make them more flexible and easier to use.

In this part of the book, we will learn how to make Python scripts sprout such graphical interfaces, too, by studying examples of programming with the tkinter module, a portable GUI library that is a standard part of the Python system and the toolkit most widely used by Python programmers. As we’ll see, it’s easy to program user interfaces in Python scripts thanks to both the simplicity of the language and the power of its GUI libraries. As an added bonus, GUIs programmed in Python with tkinter are automatically portable to all major computer systems.

GUI Programming Topics

Because GUIs are a major area, I want to say a few more words about this part of the book before we get started. To make them easier to absorb, GUI programming topics are split over the next five chapters:

  • This chapter begins with a quick tkinter tutorial to teach coding basics. Interfaces are kept simple here on purpose, so you can master the fundamentals before moving on to the following chapter’s interfaces. On the other hand, this chapter covers all the basics: event processing, the pack geometry manager, using inheritance and composition in GUIs, and more. As we’ll see, object-oriented programming (OOP) isn’t required for tkinter, but it makes GUIs structured and reusable.

  • Chapters 8 and 9 take you on a tour of the tkinter widget set.[23] Roughly, Chapter 8 presents simple widgets and Chapter 9 covers more advanced widgets and related tools. Most of the interface devices you’re accustomed to seeing—sliders, menus, dialogs, images, and their kin—show up here. These two chapters are not a fully complete tkinter reference (which could easily fill a large book by itself), but they should be enough to help you get started coding substantial Python GUIs. The examples in these chapters are focused on widgets and tkinter tools, but Python’s support for code reuse is also explored along the way.

  • Chapter 10 covers more advanced GUI programming techniques. It includes an exploration of techniques for automating common GUI tasks with Python. Although tkinter is a full-featured library, a small amount of reusable Python code can make its interfaces even more powerful and easier to use.

  • Chapter 11 wraps up by presenting a handful of complete GUI programs that make use of coding and widget techniques presented in the four preceding chapters. We’ll learn how to implement text editors, image viewers, clocks, and more.

Because GUIs are actually cross-domain tools, other GUI examples will also show up throughout the remainder of this book. For example, we’ll later see complete email GUIs and calculators, as well as a basic FTP client GUI; additional examples such as tree viewers and table browsers are available externally in the book examples package. Chapter 11 gives a list of forward pointers to other tkinter examples in this text.

After we explore GUIs, in Part IV we’ll also learn how to build basic user interfaces within a web browser using HTML and Python scripts that run on a server—a very different model with advantages and tradeoffs all its own that are important to understand. Newer technologies such as the RIAs described later in this chapter build on the web browser model to offer even more interface choices.

For now, though, our focus here is on more traditional GUIs—known as “desktop” applications to some, and as “standalone” GUIs to others. As we’ll see when we meet FTP and email client GUIs in the Internet part of this book, though, such programs often connect to a network to do their work as well.

Running the Examples

One other point I’d like to make right away: most GUIs are dynamic and interactive interfaces, and the best I can do here is show static screenshots representing selected states in the interactions such programs implement. This really won’t do justice to most examples. If you are not working along with the examples already, I encourage you to run the GUI examples in this and later chapters on your own.

On Windows, the standard Python install comes with tkinter support built in, so all these examples should work immediately. Mac OS X comes bundled with a tkinter-aware Python as well. For other systems, Pythons with tkinter support are either provided with the system itself or are readily available (see the top-level README-PP4E.txt file in the book examples distribution for more details). Getting tkinter to work on your computer is worth whatever extra install details you may need to absorb, though; experimenting with these programs is a great way to learn about both GUI programming and Python itself.

Also see the description of book example portability in general in this book’s Preface. Although Python and tkinter are both largely platform neutral, you may run into some minor platform-specific issues if you try to run this book’s examples on platforms other than that used to develop this book. Mac OS X, for example, might pose subtle differences in some of the examples’ operation. Be sure to watch this book’s website for pointers and possible future patches for using the examples on other platforms.

Python GUI Development Options

Before we start wading into the tkinter pond, let’s begin with some perspective on Python GUI options in general. Because Python has proven to be such a good match for GUI work, this domain has seen much activity over the years. In fact, although tkinter is by most accounts still the most widely used GUI toolkit in the Python world, there are a variety of ways to program user interfaces in Python today. Some are specific to Windows or X Windows,[24] some are cross-platform solutions, and all have followings and strong points of their own. To be fair to all the alternatives, here is a brief inventory of GUI toolkits available to Python programmers as I write these words:

tkinter

An open source GUI library and the continuing de facto standard for portable GUI development in Python. Python scripts that use tkinter to build GUIs run portably on Windows, X Windows (Unix and Linux), and Macintosh OS X, and they display a native look-and-feel on each of these platforms today. tkinter makes it easy to build simple and portable GUIs quickly. Moreover, it can be easily augmented with Python code, as well as with larger extension packages such as Pmw (a third-party widget library); Tix (another widget library, and now a standard part of Python); PIL (an image-processing extension); and ttk (Tk themed widgets, also now a standard part of Python as of version 3.1). More on such extensions like these later in this introduction.

The underlying Tk library used by tkinter is a standard in the open source world at large and is also used by the Perl, Ruby, PHP, Common Lisp, and Tcl scripting languages, giving it a user base that likely numbers in the millions. The Python binding to Tk is enhanced by Python’s simple object model—Tk widgets become customizable and embeddable objects, instead of string commands. tkinter takes the form of a module package in Python 3.X, with nested modules that group some of its tools by functionality (it was formerly known as module Tkinter in Python 2.X, but was renamed to follow naming conventions, and restructured to provide a more hierarchical organization).

tkinter is mature, robust, widely used, and well documented. It includes roughly 25 basic widget types, plus various dialogs and other tools. Moreover, there is a dedicated book on the subject, plus a large library of published tkinter and Tk documentation. Perhaps most importantly, because it is based on a library developed for scripting languages, tkinter is also a relatively lightweight toolkit, and as such it meshes well with a scripting language like Python.

Because of such attributes, Python’s tkinter module ships with Python as a standard library module and is the basis of Python’s standard IDLE integrated development environment GUI. In fact, tkinter is the only GUI toolkit that is part of Python; all others on this list are third-party extensions. The underlying Tk library is also shipped with Python on some platforms (including Windows, Mac OS X, and most Linux and Unix-like systems). You can be reasonably sure that tkinter will be present when your script runs, and you can guarantee this if needed by freezing your GUI into a self-contained binary executable with tools like PyInstaller and py2exe (see the Web for details).

Although tkinter is easy to use, its text and canvas widgets are powerful enough to implement web pages, three-dimensional visualization, and animation. In addition, a variety of systems aim to provide GUI builders for Python/tkinter today, including GUI Builder (formerly part of the Komodo IDE and relative of SpecTCL), Rapyd-Tk, xRope, and others (though this set has historically tended to change much over time; see http://wiki.python.org/moin/GuiProgramming or search the Web for updates). As we will see, though, tkinter is usually so easy to code that GUI builders are not widely used. This is especially true once we leave the realm of the static layouts that builders typically support.

wxPython

A Python interface for the open source wxWidgets (formerly called wxWindows) library, which is a portable GUI class framework originally written to be used from the C++ programming language. The wxPython system is an extension module that wraps wxWidgets classes. This library is generally considered to excel at building sophisticated interfaces and is probably the second most popular Python GUI toolkit today, behind tkinter. GUIs coded in Python with wxPython are portable to Windows, Unix-like platforms, and Mac OS X.

Because wxPython is based on a C++ class library, most observers consider it to be more complex than tkinter: it provides hundreds of classes, generally requires an object-oriented coding style, and has a design that some find reminiscent of the MFC class library on Windows. wxPython often expects programmers to write more code, partly because it is a more functional and thus complex system, and partly because it inherits this mindset from its underlying C++ library.

Moreover, some of wxPython’s documentation is oriented toward C++, though this story has been improved recently with the publication of a book dedicated to wxPython. By contrast, tkinter is covered by one book dedicated to it, large sections of other Python books, and an even larger library of existing literature on the underlying Tk toolkit. Since the world of Python books has been remarkably dynamic over the years, though, you should investigate the accuracy of these observations at the time that you read these words; some books fade, while new Python books appear on a regular basis.

On the other hand, in exchange for its added complexity, wxPython provides a powerful toolkit. wxPython comes with a richer set of widgets out of the box than tkinter, including trees and HTML viewers—things that may require extensions such as Pmw, Tix, or ttk in tkinter. In addition, some prefer the appearance of the interfaces it renders. BoaConstructor and wxDesigner, among other options, provide a GUI builder that generates wxPython code. Some wxWidgets tools also support non-GUI Python work as well. For a quick look at wxPython widgets and code, run the demo that comes with the system (see http://wxpython.org/, or search the Web for links).

PyQt

A Python interface to the Qt toolkit (now from Nokia, formerly by Trolltech), and perhaps the third most widely used GUI toolkit for Python today. PyQt is a full-featured GUI library and runs portably today on Windows, Mac OS X, and Unix and Linux. Like wxPython, Qt is generally more complex, yet more feature rich, than tkinter; it contains hundreds of classes and thousands of functions and methods. Qt grew up on Linux but became portable to other systems over time; reflecting this heritage, the PyQt and PyKDE extension packages provide access to KDE development libraries (PyKDE requires PyQt). The BlackAdder and Qt Designer systems provide GUI builders for PyQt.

Perhaps Qt’s most widely cited drawback in the past has been that it was not completely open source for full commercial use. Today, Qt provides both GPL and LGPL open source licensing, as well as commercial license options. The LGPL and GPL versions are open source, but conform to GPL licensing constraints (GPL may also impose requirements beyond those of the Python BSD-style license; you must, for example, make your source code freely available to end users).

PyGTK

A Python interface to GTK, a portable GUI library originally used as the core of the Gnome window system on Linux. The gnome-python and PyGTK extension packages export Gnome and GTK toolkit calls. At this writing, PyGTK runs portably on Windows and POSIX systems such as Linux and Mac OS X (according to its documentation, it currently requires that an X server for Mac OS X has been installed, though a native Mac version is in the works).

Jython

Jython (the system formerly known as JPython) is a Python implementation for Java, which compiles Python source code to Java bytecode, and gives Python scripts seamless access to Java class libraries on the local machine. Because of that, Java GUI libraries such as swing and awt become another way to construct GUIs in Python code run by the JPython system. Such solutions are obviously Java specific and limited in portability to that of Java and its libraries. Furthermore, swing may be one of the largest and most complex GUI option for Python work. A new package named jTkinter also provides a tkinter port to Jython using Java’s JNI; if installed, Python scripts may also use tkinter to build GUIs under Jython. Jython also has Internet roles we’ll meet briefly in Chapter 12.

IronPython

In a very similar vein, the IronPython system—an implementation of the Python language for the .NET environment and runtime engine, which, among other things, compiles Python programs to .NET bytecode—also offers Python scripts GUI construction options in the .NET framework. You write Python code, but use C#/.NET components to construct interfaces, and applications at large. IronPython code can be run on .NET on Windows, but also on Linux under the Mono implementation of .NET, and in the Silverlight client-side RIA framework for web browsers (discussed ahead).

PythonCard

An open source GUI builder and library built on top of the wxPython toolkit and considered by some to be one of Python’s closest equivalents to the kind of GUI builders familiar to Visual Basic developers. PythonCard describes itself as a GUI construction kit for building cross-platform desktop applications on Windows, Mac OS X, and Linux, using the Python language.

Dabo

An open source GUI builder also built on wxPython, and a bit more. Dabo is a portable, three-tier, cross-platform desktop application development framework, inspired by Visual FoxPro and written in Python. Its tiers support database access, business logic, and user interface. Its open design is intended to eventually support a variety of databases and multiple user interfaces (wxPython, tkinter, and even HTML over HTTP).

Rich Internet Applications (RIAs)

Although web pages rendered with HTML are also a kind of user interface, they have historically been too limited to include in the general GUI category. However, some observers would extend this category today to include systems which allow browser-based interfaces to be much more dynamic than traditional web pages have allowed. Because such systems provide widget toolkits rendered by web browsers, they can offer some of the same portability benefits as web pages in general.

The going buzzword for this brave new breed of toolkits is rich Internet applications (RIAs). It includes AJAX and JavaScript-oriented frameworks for use on the client, such as:

Flex

An open source framework from Adobe and part of the Flash platform

Silverlight

A Microsoft framework which is also usable on Linux with Mono’s Moonlight, and can be accessed by Python code with the IronPython system described above

JavaFX

A Java platform for building RIAs which can run across a variety of connected devices

pyjamas

An AJAX-based port of the Google Web Toolkit to Python, which comes with a set of interface widgets and compiles Python code that uses those widgets into JavaScript, to be run in a browser on a client

The HTML5 standard under development proposes to address this domain as well.

Web browsers ultimately are “desktop” GUI applications, too, but are more pervasive than GUI libraries, and can be generalized with RIA tools to render other GUIs. While it’s possible to build a widget-based GUI with such frameworks, they can also add overheads associated with networking in general and often imply a substantially heavier software stack than traditional GUI toolkits. Indeed, in order to morph browsers into general GUI platforms, RIAs may imply extra software layers and dependencies, and even multiple programming languages. Because of that, and because not everyone codes for the Web today (despite what you may have heard), we won’t include them in our look at traditional standalone/desktop GUIs in this part of the book.

See the Internet part for more on RIAs and user interfaces based on browsers, and be sure to watch for news and trends on this front over time. The interactivity these tools provide is also a key part of what some refer to as “Web 2.0” when viewed more from the perspective of the Web than GUIs. Since we’re concerned with the latter here (and since user interaction is user interaction regardless of what jargon we use for it), we’ll postpone further enumeration of this topic until the next part of the book.

Platform-specific options

Besides the portable toolkits like tkinter, wxPython, and PyQt, and platform-agnostic approaches such as RIAs, most major platforms have nonportable options for Python-coded GUIs as well. For instance, on Macintosh OS X, PyObjC provides a Python binding to Apple’s Objective-C/Cocoa framework, which is the basis for much Mac development. On Windows, the PyWin32 extensions package for Python includes wrappers for the C++ Microsoft Foundation Classes (MFC) framework (a library that includes interface components), as well as Pythonwin, an MFC sample program that implements a Python development GUI. Although .NET technically runs on Linux, too, the IronPython system mentioned earlier offers additional Windows-focused options.

See the websites of these toolkits for more details. There are other lesser-known GUI toolkits for Python, and new ones are likely to emerge by the time you read this book (in fact, IronPython was new in the third edition, and RIAs are new in the fourth). Moreover, packages like those in this list are prone to mutate over time. For an up-to-date list of available tools, search the Web or browse http://www.python.org and the PyPI third-party packages index maintained there.

tkinter Overview

Of all the prior section’s GUI options, though, tkinter is by far the de facto standard way to implement portable user interfaces in Python today, and the focus of this part of the book. The rationale for this approach was explained in Chapter 1; in short, we elected to present one toolkit in satisfying depth instead of many toolkits in less-than-useful fashion. Moreover, most of the tkinter programming concepts you learn here will translate directly to any other GUI toolkit you choose to utilize.

tkinter Pragmatics

Perhaps more to the point, though, there are pragmatic reasons that the Python world still gravitates to tkinter as its de facto standard portable GUI toolkit. Among them, tkinter’s accessibility, portability, availability, documentation, and extensions have made it the most widely used Python GUI solution for many years running:

Accessibility

tkinter is generally regarded as a lightweight toolkit and one of the simplest GUI solutions for Python available today. Unlike larger frameworks, it is easy to get started in tkinter right away, without first having to grasp a much larger class interaction model. As we’ll see, programmers can create simple tkinter GUIs in a few lines of Python code and scale up to writing industrial-strength GUIs gradually. Although the tkinter API is basic, additional widgets can be coded in Python or obtained in extension packages such as Pmw, Tix, and ttk.

Portability

A Python script that builds a GUI with tkinter will run without source code changes on all major windowing platforms today: Microsoft Windows, X Windows (on Unix and Linux), and the Macintosh OS X (and also ran on Mac classics). Further, that same script will provide a native look-and-feel to its users on each of these platforms. In fact, this feature became more apparent as Tk matured. A Python/tkinter script today looks like a Windows program on Windows; on Unix and Linux, it provides the same interaction but sports an appearance familiar to X Windows users; and on the Mac, it looks like a Mac program should.

Availability

tkinter is a standard module in the Python library, shipped with the interpreter. If you have Python, you have tkinter. Moreover, most Python installation packages (including the standard Python self-installer for Windows, that provided on Mac OS X, and many Linux distributions) come with tkinter support bundled. Because of that, scripts written to use the tkinter module work immediately on most Python interpreters, without any extra installation steps. tkinter is also generally better supported than its alternatives today. Because the underlying Tk library is also used by the Tcl and Perl programming languages (and others), it tends to receive more development resources than other toolkits available.

Naturally, other factors such as documentation and extensions are important when using a GUI toolkit, too; let’s take a quick look at the story tkinter has to tell on these fronts as well.

tkinter Documentation

This book explores tkinter fundamentals and most widgets tools, and it should be enough to get started with substantial GUI development in Python. On the other hand, it is not an exhaustive reference to the tkinter library or extensions to it. Happily, at least one book dedicated to using tkinter in Python is now commercially available as I write this paragraph, and others are on the way (search the Web for details). Besides books, you can also find tkinter documentation online; a complete set of tkinter manuals is currently maintained on the Web at http://www.pythonware.com/library.

In addition, because the underlying Tk toolkit used by tkinter is also a de facto standard in the open source scripting community at large, other documentation sources apply. For instance, because Tk has also been adopted by the Tcl and Perl programming languages, Tk-oriented books and documentation written for both of these are directly applicable to Python/tkinter as well (albeit, with some syntactic mapping).

Frankly, I learned tkinter by studying Tcl/Tk texts and references—just replace Tcl strings with Python objects and you have additional reference libraries at your disposal (see Table 7-2, the Tk-to-tkinter conversion guide, at the end of this chapter for help reading Tk documentation). For instance, the book Tcl/Tk Pocket Reference (O’Reilly) can serve as a nice supplement to the tkinter tutorial material in this part of the book. Moreover, since Tk concepts are familiar to a large body of programmers, Tk support is also readily available on the Net.

After you’ve learned the basics, examples can help, too. You can find tkinter demo programs, besides those you’ll study in this book, at various locations around the Web. Python itself includes a set of demo programs in the Demos kinter subdirectory of its source distribution package. The IDLE development GUI mentioned in the next section makes for an interesting code read as well.

tkinter Extensions

Because tkinter is so widely used, programmers also have access to precoded Python extensions designed to work with or augment it. Some of these may not yet be available for Python 3.X as I write this but are expected to be soon. For instance:

Pmw

Python Mega Widgets is an extension toolkit for building high-level compound widgets in Python using the tkinter module. It extends the tkinter API with a collection of more sophisticated widgets for advanced GUI development and a framework for implementing some of your own. Among the precoded and extensible megawidgets shipped with the package are notebooks, combo boxes, selection widgets, paned widgets, scrolled widgets, dialog windows, button boxes, balloon help, and an interface to the Blt graph widget.

The interface to Pmw megawidgets is similar to that of basic tkinter widgets, so Python scripts can freely mix Pmw megawidgets with standard tkinter widgets. Moreover, Pmw is pure Python code, and so requires no C compiler or tools to install. To view its widgets and the corresponding code you use to construct them, run the demosAll.py script in the Pmw distribution package. You can find Pmw at http://pmw.sourceforge.net.

Tix

Tix is a collection of more than 40 advanced widgets, originally written for Tcl/Tk but now available for use in Python/tkinter programs. This package is now a Python standard library module, called tkinter.tix. Like Tk, the underlying Tix library is also shipped today with Python on Windows. In other words, on Windows, if you install Python, you also have Tix as a preinstalled library of additional widgets.

Tix includes many of the same devices as Pmw, including spin boxes, trees, tabbed notebooks, balloon help pop ups, paned windows, and much more. See the Python library manual’s entry for the Tix module for more details. For a quick look at its widgets, as well as the Python source code used to program them, run the tixwidgets.py demonstration program in the Demo ix directory of the Python source distribution (this directory is not installed by default on Windows and is prone to change—you can generally find it after fetching and unpacking Python’s source code from Python.org).

ttk

Tk themed widgets, ttk, is a relatively new widget set which attempts to separate the code implementing a widget’s behavior from that implementing its appearance. Widget classes handle state and callback invocation, whereas widget appearance is managed separately by themes. Much like Tix, this extension began life separately, but was very recently incorporated into Python’s standard library in Python 3.1, as module tkinter.ttk.

Also like Tix, this extension comes with advanced widget types, some of which are not present in standard tkinter itself. Specifically, ttk comes with 17 widgets, 11 of which are already present in tkinter and are meant as replacements for some of tkinter’s standard widgets, and 6 of which are new—Combobox, Notebook, Progressbar, Separator, Sizegrip and Treeview. In a nutshell, scripts import from the ttk module after tkinter in order to use its replacement widgets and configure style objects possibly shared by multiple widgets, instead of configuring widgets themselves.

As we’ll see in this chapter, it’s possible to provide a common look-and-feel for a set of widgets with standard tkinter, by subclassing its widget classes using normal OOP techniques (see Customizing Widgets with Classes). However, ttk offers additional style options and advanced widget types. For more details on ttk widgets, see the entry in the Python library manual or search the Web; this book focuses on tkinter fundamentals, and tix and ttk are both too large to cover in a useful fashion here.

PIL

The Python Imaging Library (PIL) is an open source extension package that adds image-processing tools to Python. Among other things, it provides tools for image thumbnails, transforms, and conversions, and it extends the basic tkinter image object to add support for displaying many image file types. PIL, for instance, allows tkinter GUIs to display JPEG, TIFF, and PNG images not supported by the base tkinter toolkit itself (without extension, tkinter supports GIFs and a handful of bitmap formats). See the end of Chapter 8 for more details and examples; we’ll use PIL in this book in a number of image-related example scripts. PIL can be found at http://www.pythonware.com or via a web search.

IDLE

The IDLE integrated Python development environment is both written in Python with tkinter and shipped and installed with the Python package (if you have a recent Python interpreter, you should have IDLE too; on Windows, click the Start button, select the Programs menu, and click the Python entry to find it). IDLE provides syntax-coloring text editors for Python code, point-and-click debugging, and more, and is an example of tkinter’s utility.

Others

Many of the extensions that provide visualization tools for Python are based on the tkinter library and its canvas widget. See the PyPI website and your favorite web search engine for more tkinter extension examples.

If you plan to do any commercial-grade GUI development with tkinter, you’ll probably want to explore extensions such as Pmw, PIL, Tix, and ttk after learning tkinter basics in this text. They can save development time and add pizzazz to your GUIs. See the Python-related websites mentioned earlier for up-to-date details and links.

tkinter Structure

From a more nuts-and-bolts perspective, tkinter is an integration system that implies a somewhat unique program structure. We’ll see what this means in terms of code in a moment, but here is a brief introduction to some of the terms and concepts at the core of Python GUI programming.

Implementation structure

Strictly speaking, tkinter is simply the name of Python’s interface to Tk—a GUI library originally written for use with the Tcl programming language and developed by Tcl’s creator, John Ousterhout. Python’s tkinter module talks to Tk, and the Tk library in turn interfaces with the underlying window system: Microsoft Windows, X Windows on Unix, or whatever GUI system your Python uses on your Macintosh. The portability of tkinter actually stems from the underling Tk library it wraps.

Python’s tkinter adds a software layer on top of Tk that allows Python scripts to call out to Tk to build and configure interfaces and routes control back to Python scripts that handle user-generated events (e.g., mouse clicks). That is, GUI calls are internally routed from Python script, to tkinter, to Tk; GUI events are routed from Tk, to tkinter, and back to a Python script. In Chapter 20, we’ll know these transfers by their C integration terms, extending and embedding.

Technically, tkinter is today structured as a combination of the Python-coded tkinter module package’s files and an extension module called _tkinter that is written in C. _tkinter interfaces with the Tk library using extending tools and dispatches callbacks back to Python objects using embedding tools; tkinter simply adds a class-based interface on top of _tkinter. You should almost always import tkinter in your scripts, though, not _tkinter; the latter is an implementation module meant for internal use only (and was oddly named for that reason).

Programming structure

Luckily, Python programmers don’t normally need to care about all this integration and call routing going on internally; they simply make widgets and register Python functions to handle widget events. Because of the overall structure, though, event handlers are usually known as callback handlers, because the GUI library “calls back” to Python code when events occur.

In fact, we’ll find that Python/tkinter programs are entirely event driven: they build displays and register handlers for events, and then do nothing but wait for events to occur. During the wait, the Tk GUI library runs an event loop that watches for mouse clicks, keyboard presses, and so on. All application program processing happens in the registered callback handlers in response to events. Further, any information needed across events must be stored in long-lived references such as global variables and class instance attributes. The notion of a traditional linear program control flow doesn’t really apply in the GUI domain; you need to think in terms of smaller chunks.

In Python, Tk also becomes object oriented simply because Python is object oriented: the tkinter layer exports Tk’s API as Python classes. With tkinter, we can either use a simple function-call approach to create widgets and interfaces, or apply object-oriented techniques such as inheritance and composition to customize and extend the base set of tkinter classes. Larger tkinter GUIs are generally constructed as trees of linked tkinter widget objects and are often implemented as Python classes to provide structure and retain state information between events. As we’ll see in this part of the book, a tkinter GUI coded with classes almost by default becomes a reusable software component.

Climbing the GUI Learning Curve

On to the code; let’s start out by quickly stepping through a few small examples that illustrate basic concepts and show the windows they create on the computer display. The examples will become progressively more sophisticated as we move along, but let’s get a handle on the fundamentals first.

“Hello World” in Four Lines (or Less)

The usual first example for GUI systems is to show how to display a “Hello World” message in a window. As coded in Example 7-1, it’s just four lines in Python.

Example 7-1. PP4EGuiIntrogui1.py
from tkinter import Label                               # get a widget object
widget = Label(None, text='Hello GUI world!')           # make one
widget.pack()                                           # arrange it
widget.mainloop()                                       # start event loop

This is a complete Python tkinter GUI program. When this script is run, we get a simple window with a label in the middle; it looks like Figure 7-1 on my Windows 7 laptop (I stretched some windows in this book horizontally to reveal their window titles; your platform’s window system may vary).

“Hello World” (gui1) on Windows
Figure 7-1. “Hello World” (gui1) on Windows

This isn’t much to write home about yet, but notice that this is a completely functional, independent window on the computer’s display. It can be maximized to take up the entire screen, minimized to hide it in the system bar, and resized. Click on the window’s “X” box in the top right to kill the window and exit the program.

The script that builds this window is also fully portable. Run this script on your machine to see how it renders. When this same file is run on Linux it produces a similar window, but it behaves according to the underlying Linux window manager. Even on the same operating system, the same Python code might yields a different look-and-feel for different window systems (for instance, under KDE and Gnome on Linux). The same script file would look different still when run on Macintosh and other Unix-like window managers. On all platforms, though, its basic functional behavior will be the same.

tkinter Coding Basics

The gui1 script is a trivial example, but it illustrates steps common to most tkinter programs. This Python code does the following:

  1. Loads a widget class from the tkinter module

  2. Makes an instance of the imported Label class

  3. Packs (arranges) the new Label in its parent widget

  4. Calls mainloop to bring up the window and start the tkinter event loop

The mainloop method called last puts the label on the screen and enters a tkinter wait state, which watches for user-generated GUI events. Within the mainloop function, tkinter internally monitors things such as the keyboard and mouse to detect user-generated events. In fact, the tkinter mainloop function is similar in spirit to the following pseudo-Python code:

def mainloop():
    while the main window has not been closed:
        if an event has occurred:
            run the associated event handler function

Because of this model, the mainloop call in Example 7-1 never returns to our script while the GUI is displayed on-screen.[25] When we write larger scripts, the only way we can get anything done after calling mainloop is to register callback handlers to respond to events.

This is called event-driven programming, and it is perhaps one of the most unusual aspects of GUIs. GUI programs take the form of a set of event handlers that share saved information rather than of a single main control flow. We’ll see how this looks in terms of real code in later examples.

Note that for code in a script file, you really need to do steps 3 and 4 in the preceding list to open this script’s GUI. To display a GUI’s window at all, you need to call mainloop; to display widgets within the window, they must be packed (or otherwise arranged) so that the tkinter geometry manager knows about them. In fact, if you call either mainloop or pack without calling the other, your window won’t show up as expected: a mainloop without a pack shows an empty window, and a pack without a mainloop in a script shows nothing since the script never enters an event wait state (try it). The mainloop call is sometimes optional when you’re coding interactively, but you shouldn’t rely on this in general.

Since the concepts illustrated by this simple script are at the core of most tkinter programs, let’s take a deeper look at some of them before moving on.

Making Widgets

When widgets are constructed in tkinter, we can specify how they should be configured. The gui1 script passes two arguments to the Label class constructor:

  • The first is a parent-widget object, which we want the new label to be attached to. Here, None means “attach the new Label to the default top-level window of this program.” Later, we’ll pass real widgets in this position to attach our labels to other container objects.

  • The second is a configuration option for the Label, passed as a keyword argument: the text option specifies a text string to appear as the label’s message. Most widget constructors accept multiple keyword arguments for specifying a variety of options (color, size, callback handlers, and so on). Most widget configuration options have reasonable defaults per platform, though, and this accounts for much of tkinter’s simplicity. You need to set most options only if you wish to do something custom.

As we’ll see, the parent-widget argument is the hook we use to build up complex GUIs as widget trees. tkinter works on a “what-you-build-is-what-you-get” principle—we construct widget object trees as models of what we want to see on the screen, and then ask the tree to display itself by calling mainloop.

Geometry Managers

The pack widget method called by the gui1 script invokes the packer geometry manager, one of three ways to control how widgets are arranged in a window. tkinter geometry managers simply arrange one or more widgets within a container (sometimes called a parent or master). Both top-level windows and frames (a special kind of widget we’ll meet later) can serve as containers, and containers may be nested inside other containers to build hierarchical displays.

The packer geometry manager uses constraint option settings to automatically position widgets in a window. Scripts supply higher-level instructions (e.g., “attach this widget to the top of its container, and stretch it to fill its space vertically”), not absolute pixel coordinates. Because such constraints are so abstract, the packer provides a powerful and easy-to-use layout system. In fact, you don’t even have to specify constraints. If you don’t pass any arguments to pack, you get default packing, which attaches the widget to the top side of its container.

We’ll visit the packer repeatedly in this chapter and use it in many of the examples in this book. In Chapter 9, we will also meet an alternative grid geometry manager—a layout system that arranges widgets within a container in tabular form (i.e., by rows and columns) and works well for input forms. A third alternative, called the placer geometry manager system, is described in Tk documentation but not in this book; it’s less popular than the pack and grid managers and can be difficult to use for larger GUIs coded by hand.

Running GUI Programs

Like all Python code, the module in Example 7-1 can be started in a number of ways—by running it as a top-level program file:

C:...PP4EGuiIntro> python gui1.py

by importing it from a Python session or another module file:

>>> import gui1

by running it as a Unix executable if we add the special #! line at the top:

% gui1.py &

and in any other way Python programs can be launched on your platform. For instance, the script can also be run by clicking on the file’s name in a Windows file explorer, and its code can be typed interactively at the >>> prompt.[26] It can even be run from a C program by calling the appropriate embedding API function (see Chapter 20 for details on C integration).

In other words, there are really no special rules to follow when launching GUI Python code. The tkinter interface (and Tk itself) is linked into the Python interpreter. When a Python program calls GUI functions, they’re simply passed to the embedded GUI system behind the scenes. That makes it easy to write command-line tools that pop up windows; they are run the same way as the purely text-based scripts we studied in the prior part of this book.

Avoiding DOS consoles on Windows

In Chapters 3 and 6 we noted that if a program’s name ends in a .pyw extension rather than a .py extension, the Windows Python port does not pop up a DOS console box to serve as its standard streams when the file is launched by clicking its filename icon. Now that we’ve finally started making windows of our own, that filename trick will start to become even more useful.

If you just want to see the windows that your script makes no matter how it is launched, be sure to name your GUI scripts with a .pyw if they might be run on Windows. For instance, clicking on the file in Example 7-2 in a Windows explorer creates just the window in Figure 7-1.

Example 7-2. PP4EGuiIntrogui1.pyw
...same as gui1.py...

You can also avoid the DOS pop up on Windows by running the program with the pythonw.exe executable, not python.exe (in fact, .pyw files are simply registered to be opened by pythonw). On Linux, the .pyw doesn’t hurt, but it isn’t necessary; there is no notion of a streams pop up on Unix-like machines. On the other hand, if your GUI scripts might run on Windows in the future, adding an extra “w” at the end of their names now might save porting effort later. In this book, .py filenames are still sometimes used to pop up console windows for viewing printed messages on Windows.

tkinter Coding Alternatives

As you might expect, there are a variety of ways to code the gui1 example. For instance, if you want to make all your tkinter imports more explicit in your script, grab the whole module and prefix all of its names with the module’s name, as in Example 7-3.

Example 7-3. PP4EGuiIntrogui1b.py—import versus from
import tkinter
widget = tkinter.Label(None, text='Hello GUI world!')
widget.pack()
widget.mainloop()

That will probably get tedious in realistic examples, though—tkinter exports dozens of widget classes and constants that show up all over Python GUI scripts. In fact, it is usually easier to use a * to import everything from the tkinter module by name in one shot. This is demonstrated in Example 7-4.

Example 7-4. PP4EGuiIntrogui1c.py—roots, sides, pack in place
from tkinter import *
root = Tk()
Label(root, text='Hello GUI world!').pack(side=TOP)
root.mainloop()

The tkinter module goes out of its way to export only what we really need, so it’s one of the few for which the * import form is relatively safe to apply.[27] The TOP constant in the pack call here, for instance, is one of those many names exported by the tkinter module. It’s simply a variable name (TOP="top") preassigned in constants, a module automatically loaded by tkinter.

When widgets are packed, we can specify which side of their parent they should be attached to—TOP, BOTTOM, LEFT, or RIGHT. If no side option is sent to pack (as in prior examples), a widget is attached to its parent’s TOP by default. In general, larger tkinter GUIs can be constructed as sets of rectangles, attached to the appropriate sides of other, enclosing rectangles. As we’ll see later, tkinter arranges widgets in a rectangle according to both their packing order and their side attachment options. When widgets are gridded, they are assigned row and column numbers instead. None of this will become very meaningful, though, until we have more than one widget in a window, so let’s move on.

Notice that this version calls the pack method right away after creating the label, without assigning it a variable. If we don’t need to save a widget, we can pack it in place like this to eliminate a statement. We’ll use this form when a widget is attached to a larger structure and never again referenced. This can be tricky if you assign the pack result, though, but I’ll postpone an explanation of why until we’ve covered a few more basics.

We also use a Tk widget class instance, instead of None, as the parent here. Tk represents the main (“root”) window of the program—the one that starts when the program does. An automatically created Tk instance is also used as the default parent widget, both when we don’t pass any parent to other widget calls and when we pass the parent as None. In other words, widgets are simply attached to the main program window by default. This script just makes this default behavior explicit by making and passing the Tk object itself. In Chapter 8, we’ll see that Toplevel widgets are typically used to generate new pop-up windows that operate independently of the program’s main window.

In tkinter, some widget methods are exported as functions, and this lets us shave Example 7-5 to just three lines of code.

Example 7-5. PP4EGuiIntrogui1d.py—a minimal version
from tkinter import *
Label(text='Hello GUI world!').pack()
mainloop()

The tkinter mainloop can be called with or without a widget (i.e., as a function or method). We didn’t pass Label a parent argument in this version, either: it simply defaults to None when omitted (which in turn defaults to the automatically created Tk object). But relying on that default is less useful once we start building larger displays. Things such as labels are more typically attached to other widget containers.

Widget Resizing Basics

Top-level windows, such as the one built by all of the coding variants we have seen thus far, can normally be resized by the user; simply drag out the window with your mouse. Figure 7-2 shows how our window looks when it is expanded.

Expanding gui1
Figure 7-2. Expanding gui1

This isn’t very good—the label stays attached to the top of the parent window instead of staying in the middle on expansion—but it’s easy to improve on this with a pair of pack options, demonstrated in Example 7-6.

Example 7-6. PP4EGuiIntrogui1e.py—expansion
from tkinter import *
Label(text='Hello GUI world!').pack(expand=YES, fill=BOTH)
mainloop()

When widgets are packed, we can specify whether a widget should expand to take up all available space, and if so, how it should stretch to fill that space. By default, widgets are not expanded when their parent is. But in this script, the names YES and BOTH (imported from the tkinter module) specify that the label should grow along with its parent, the main window. It does so in Figure 7-3.

gui1e with widget resizing
Figure 7-3. gui1e with widget resizing

Technically, the packer geometry manager assigns a size to each widget in a display based on what it contains (text string lengths, etc.). By default, a widget can occupy only its allocated space and is no bigger than its assigned size. The expand and fill options let us be more specific about such things:

expand=YES option

Asks the packer to expand the allocated space for the widget in general into any unclaimed space in the widget’s parent.

fill option

Can be used to stretch the widget to occupy all of its allocated space.

Combinations of these two options produce different layout and resizing effects, some of which become meaningful only when there are multiple widgets in a window. For example, using expand without fill centers the widget in the expanded space, and the fill option can specify vertical stretching only (fill=Y), horizontal stretching only (fill=X), or both (fill=BOTH). By providing these constraints and attachment sides for all widgets in a GUI, along with packing order, we can control the layout in fairly precise terms. In later chapters, we’ll find that the grid geometry manager uses a different resizing protocol entirely, but it provides similar control when needed.

All of this can be confusing the first time you hear it, and we’ll return to this later. But if you’re not sure what an expand and fill combination will do, simply try it out—this is Python, after all. For now, remember that the combination of expand=YES and fill=BOTH is perhaps the most common setting; it means “expand my space allocation to occupy all available space on my side, and stretch me to fill the expanded space in both directions.” For our “Hello World” example, the net result is that the label grows as the window is expanded, and so is always centered.

Configuring Widget Options and Window Titles

So far, we’ve been telling tkinter what to display on our label by passing its text as a keyword argument in label constructor calls. It turns out that there are two other ways to specify widget configuration options. In Example 7-7, the text option of the label is set after it is constructed, by assigning to the widget’s text key. Widget objects overload (intercept) index operations such that options are also available as mapping keys, much like a dictionary.

Example 7-7. PP4EGuiIntrogui1f.py—option keys
from tkinter import *
widget = Label()
widget['text'] = 'Hello GUI world!'
widget.pack(side=TOP)
mainloop()

More commonly, widget options can be set after construction by calling the widget config method, as in Example 7-8.

Example 7-8. PP4EGuiIntrogui1g.py—config and titles
from tkinter import *
root = Tk()
widget = Label(root)
widget.config(text='Hello GUI world!')
widget.pack(side=TOP, expand=YES, fill=BOTH)
root.title('gui1g.py')
root.mainloop()

The config method (which can also be called by its synonym, configure) can be called at any time after construction to change the appearance of a widget on the fly. For instance, we could call this label’s config method again later in the script to change the text that it displays; watch for such dynamic reconfigurations in later examples in this part of the book.

Notice that this version also calls a root.title method; this call sets the label that appears at the top of the window, as pictured in Figure 7-4. In general terms, top-level windows like the Tk root here export window-manager interfaces—i.e., things that have to do with the border around the window, not its contents.

gui1g with expansion and a window title
Figure 7-4. gui1g with expansion and a window title

Just for fun, this version also centers the label upon resizes by setting the expand and fill pack options. In fact, this version makes just about everything explicit and is more representative of how labels are often coded in full-blown interfaces; their parents, expansion policies, and attachments are usually spelled out rather than defaulted.

One More for Old Times’ Sake

Finally, if you are a minimalist and you’re nostalgic for old Python coding styles, you can also program this “Hello World” example as in Example 7-9.

Example 7-9. PP4EGuiIntrogui1-old.py—dictionary calls
from tkinter import *
Label(None, {'text': 'Hello GUI world!', Pack: {'side': 'top'}}).mainloop()

This makes the window in just two lines, albeit arguably gruesome ones! This scheme relies on an old coding style that was widely used until Python 1.3, which passed configuration options in a dictionary instead of keyword arguments.[28] In this scheme, packer options can be sent as values of the key Pack (a class in the tkinter module).

The dictionary call scheme still works and you may see it in old Python code, but it’s probably best to not do this in code you type. Use keywords to pass options, and use explicit pack method calls in your tkinter scripts instead. In fact, the only reason I didn’t cut this example completely is that dictionaries can still be useful if you want to compute and pass a set of options dynamically.

On the other hand, the func(*pargs, **kargs) syntax now also allows you to pass an explicit dictionary of keyword arguments in its third argument slot:

options = {'text': 'Hello GUI world!'}
layout  = {'side': 'top'}
Label(None, **options).pack(**layout)        # keyword must be strings

Even in dynamic scenarios where widget options are determined at run time, there’s no compelling reason to ever use the pre-1.3 tkinter dictionary call form.

Packing Widgets Without Saving Them

In gui1c.py (shown in Example 7-4), I started packing labels without assigning them to names. This works, and it is an entirely valid coding style, but because it tends to confuse beginners at first glance, I need to explain why it works in more detail here.

In tkinter, Python class objects correspond to real objects displayed on a screen; we make the Python object to make a screen object, and we call the Python object’s methods to configure that screen object. Because of this correspondence, the lifetime of the Python object must generally correspond to the lifetime of the corresponding object on the screen.

Luckily, Python scripts don’t usually have to care about managing object lifetimes. In fact, they do not normally need to maintain a reference to widget objects created along the way at all unless they plan to reconfigure those objects later. For instance, it’s common in tkinter programming to pack a widget immediately after creating it if no further reference to the widget is required:

Label(text='hi').pack()                        # OK

This expression is evaluated left to right, as usual. It creates a new label and then immediately calls the new object’s pack method to arrange it in the display. Notice, though, that the Python Label object is temporary in this expression; because it is not assigned to a name, it would normally be garbage collected (destroyed and reclaimed) by Python immediately after running its pack method.

However, because tkinter emits Tk calls when objects are constructed, the label will be drawn on the display as expected, even though we haven’t held onto the corresponding Python object in our script. In fact, tkinter internally cross-links widget objects into a long-lived tree used to represent the display, so the Label object made during this statement actually is retained, even if not by our code.[29]

In other words, your scripts don’t generally have to care about widget object lifetimes, and it’s OK to make widgets and pack them immediately in the same statement without maintaining a reference to them explicitly in your code.

But that does not mean that it’s OK to say something like this:

widget = Label(text='hi').pack()               # wrong!
...use widget...

This statement almost seems like it should assign a newly packed label to widget, but it does not do this. In fact, it’s really a notorious tkinter beginner’s mistake. The widget pack method packs the widget but does not return the widget thus packed. Really, pack returns the Python object None; after such a statement, widget will be a reference to None, and any further widget operations through that name will fail. For instance, the following fails, too, for the same reason:

Label(text='hi').pack().mainloop()             # wrong!

Since pack returns None, asking for its mainloop attribute generates an exception (as it should). If you really want to both pack a widget and retain a reference to it, say this instead:

widget = Label(text='hi')                      # OK too
widget.pack()
...use widget...

This form is a bit more verbose but is less tricky than packing a widget in the same statement that creates it, and it allows you to hold onto the widget for later processing. It’s probably more common in realistic scripts that perform more complex widget configuration and layouts.

On the other hand, scripts that compose layouts often add some widgets once and for all when they are created and never need to reconfigure them later; assigning to long-lived names in such programs is pointless and unnecessary.

Note

In Chapter 8, we’ll meet two exceptions to this rule. Scripts must manually retain a reference to image objects because the underlying image data is discarded if the Python image object is garbage collected. tkinter variable class objects also temporarily unset an associated Tk variable if reclaimed, but this is uncommon and less harmful.

Adding Buttons and Callbacks

So far, we’ve learned how to display messages in labels, and we’ve met tkinter core concepts along the way. Labels are nice for teaching the basics, but user interfaces usually need to do a bit more…like actually responding to users. To show how, the program in Example 7-10 creates the window in Figure 7-5.

Example 7-10. PP4EGuiIntrogui2.py
import sys
from tkinter import *
widget = Button(None, text='Hello widget world', command=sys.exit)
widget.pack()
widget.mainloop()
A button on the top
Figure 7-5. A button on the top

Here, instead of making a label, we create an instance of the tkinter Button class. It’s attached to the default top level window as before on the default TOP packing side. But the main thing to notice here is the button’s configuration arguments: we set an option called command to the sys.exit function.

For buttons, the command option is the place where we specify a callback handler function to be run when the button is later pressed. In effect, we use command to register an action for tkinter to call when a widget’s event occurs. The callback handler used here isn’t very interesting: as we learned in Chapter 5, the built-in sys.exit function simply shuts down the calling program. Here, that means that pressing this button makes the window go away.

Just as for labels, there are other ways to code buttons. Example 7-11 is a version that packs the button in place without assigning it to a name, attaches it to the LEFT side of its parent window explicitly, and specifies root.quit as the callback handler—a standard Tk object method that shuts down the GUI and so ends the program. Technically, quit ends the current mainloop event loop call, and thus the entire program here; when we start using multiple top-level windows in Chapter 8, we’ll find that quit usually closes all windows, but its relative destroy erases just one window.

Example 7-11. PP4EGuiIntrogui2b.py
from tkinter import *
root = Tk()
Button(root, text='press', command=root.quit).pack(side=LEFT)
root.mainloop()

This version produces the window in Figure 7-6. Because we didn’t tell the button to expand into all available space, it does not do so.

A button on the left
Figure 7-6. A button on the left

In both of the last two examples, pressing the button makes the GUI program exit. In older tkinter code, you may sometimes see the string exit assigned to the command option to make the GUI go away when pressed. This exploits a tool in the underlying Tk library and is less Pythonic than sys.exit or root.quit.

Widget Resizing Revisited: Expansion

Even with a GUI this simple, there are many ways to lay out its appearance with tkinter’s constraint-based pack geometry manager. For example, to center the button in its window, add an expand=YES option to the button’s pack method call in Example 7-11. The line of changed code looks like this:

Button(root, text='press', command=root.quit).pack(side=LEFT, expand=YES)

This makes the packer allocate all available space to the button but does not stretch the button to fill that space. The result is the window captured in Figure 7-7.

pack(side=LEFT, expand=YES)
Figure 7-7. pack(side=LEFT, expand=YES)

If you want the button to be given all available space and to stretch to fill all of its assigned space horizontally, add expand=YES and fill=X keyword arguments to the pack call. This will create the scene in Figure 7-8.

pack(side=LEFT, expand=YES, fill=X)
Figure 7-8. pack(side=LEFT, expand=YES, fill=X)

This makes the button fill the whole window initially (its allocation is expanded, and it is stretched to fill that allocation). It also makes the button grow as the parent window is resized. As shown in Figure 7-9, the button in this window does expand when its parent expands, but only along the X horizontal axis.

Resizing with expand=YES, fill=X
Figure 7-9. Resizing with expand=YES, fill=X

To make the button grow in both directions, specify both expand=YES and fill=BOTH in the pack call; now resizing the window makes the button grow in general, as shown in Figure 7-10. In fact, for more fun, maximize this window to fill the entire screen; you’ll get one very big tkinter button indeed.

Resizing with expand=YES, fill=BOTH
Figure 7-10. Resizing with expand=YES, fill=BOTH

In more complex displays, such a button will expand only if all of the widgets it is contained by are set to expand too. Here, the button’s only parent is the Tk root window of the program, so parent expandability isn’t yet an issue; in later examples, we’ll need to make enclosing Frame widgets expandable too. We will revisit the packer geometry manager when we meet multiple-widget displays that use such devices later in this tutorial, and again when we study the alternative grid call in Chapter 9.

Adding User-Defined Callback Handlers

In the simple button examples in the preceding section, the callback handler was simply an existing function that killed the GUI program. It’s not much more work to register callback handlers that do something a bit more useful. Example 7-12 defines a callback handler of its own in Python.

Example 7-12. PP4EGuiIntrogui3.py
import sys
from tkinter import *

def quit():                                  # a custom callback handler
    print('Hello, I must be going...')       # kill windows and process
    sys.exit()

widget = Button(None, text='Hello event world', command=quit)
widget.pack()
widget.mainloop()

The window created by this script is shown in Figure 7-11. This script and its GUI are almost identical to the last example. But here, the command option specifies a function we’ve defined locally. When the button is pressed, tkinter calls the quit function in this file to handle the event, passing it zero arguments. Inside quit, the print call statement types a message on the program’s stdout stream, and the GUI process exits as before.

A button that runs a Python function
Figure 7-11. A button that runs a Python function

As usual, stdout is normally the window that the program was started from unless it’s been redirected to a file. It’s a pop-up DOS console if you run this program by clicking it on Windows; add an input call before sys.exit if you have trouble seeing the message before the pop up disappears. Here’s what the printed output looks like back in standard stream world when the button is pressed; it is generated by a Python function called automatically by tkinter:

C:...PP4EGuiIntro> python gui3.py
Hello, I must be going...

C:...PP4EGuiIntro>

Normally, such messages would be displayed in the GUI, but we haven’t gotten far enough to know how just yet. Callback functions usually do more, of course (and may even pop up new independent windows altogether), but this example illustrates the basics.

In general, callback handlers can be any callable object: functions, anonymous functions generated with lambda expressions, bound methods of class or type instances, or class instances that inherit a __call__ operator overload method. For Button press callbacks, callback handlers always receive no arguments (other than an automatic self, for bound methods); any state information required by the callback handler must be provided in other ways—as global variables, class instance attributes, extra arguments provided by an indirection layer, and so on.

To make this a bit more concrete, let’s take a quick look at some other ways to code the callback handler in this example.

Lambda Callback Handlers

Recall that the Python lambda expression generates a new, unnamed function object when run. If we need extra data passed in to the handler function, we can register lambda expressions to defer the call to the real handler function, and specify the extra data it needs.

Later in this part of the book, we’ll see how this can be more useful, but to illustrate the basic idea, Example 7-13 shows what Example 7-12 looks like when recoded to use a lambda instead of a def.

Example 7-13. PP4EGuiIntrogui3b.py
import sys
from tkinter import *                        # lambda generates a function

widget = Button(None,                        # but contains just an expression
             text='Hello event world',
             command=(lambda: print('Hello lambda world') or sys.exit()) )

widget.pack()
widget.mainloop()

This code is a bit tricky because lambdas can contain only an expression; to emulate the original script, this version uses an or operator to force two expressions to be run ( print works as the first, because it’s a function call in Python 3.X—we don’t need to resort to using sys.stdout directly).

Deferring Calls with Lambdas and Object References

More typically, lambdas are used to provide an indirection layer that passes along extra data to a callback handler (I omit pack and mainloop calls in the following snippets for simplicity):

def handler(A, B):              # would normally be called with no args
    ...use A and B...

X = 42
Button(text='ni', command=(lambda: handler(X, 'spam')))      # lambda adds arguments

Although tkinter invokes command callbacks with no arguments, such a lambda can be used to provide an indirect anonymous function that wraps the real handler call and passes along information that existed when the GUI was first constructed. The call to the real handler is, in effect, deferred, so we can add the extra arguments it requires. Here, the value of global variable X and string 'spam' will be passed to arguments A and B, even though tkinter itself runs callbacks with no arguments. The net effect is that the lambda serves to map a no-argument function call to one with arguments supplied by the lambda.

If lambda syntax confuses you, remember that a lambda expression such as the one in the preceding code can usually be coded as a simple def statement instead, nested or otherwise. In the following code, the second function does exactly the same work as the prior lambda—by referencing it in the button creation call, it effectively defers invocation of the actual callback handler so that extra arguments can be passed:

def handler(A, B):              # would normally be called with no args
    ...use A and B...

X = 42
def func():                     # indirection layer to add arguments
    handler(X, 'spam')

Button(text='ni', command=func)

To make the need for deferrals more obvious, notice what happens if you code a handler call in the button creation call itself without a lambda or other intermediate function—the callback runs immediately when the button is created, not when it is later clicked. That’s why we need to wrap the call in an intermediate function to defer its invocation:

def handler(name):
    print(name)

Button(command=handler('spam'))              # BAD: runs the callback now!

Using either a lambda or a callable reference serves to defer callback invocation until the event later occurs. For example, using a lambda to pass extra data with an inline function definition that defers the call:

def handler(name):
    print(name)

Button(command=(lambda: handler('spam')))    # OK: wrap in a lambda to defer

is always equivalent to the longer, and to some observers less convenient, double-function form:

def handler(name):
    print(name)

def temp():
    handler('spam')

Button(command=temp)                         # OK: refence but do not call

We need only the zero-argument lambda or the zero-argument callable reference, though, not both—it makes no sense to code a lambda which simply calls a function if no extra data must be passed in and only adds an extra pointless call:

def handler(name):
    print(name)

def temp():
    handler('spam')

Button(command=(lambda: temp()))             # BAD: this adds a pointless call!

As we’ll see later, this includes references to other callables like bound methods and callable instances which retain state in themselves—if they take zero arguments when called, we can simply name them at widget construction time, and we don’t need to wrap them in a superfluous lambda.

Callback Scope Issues

Although the prior section’s lambda and intermediate function techniques defer calls and allow extra data to be passed in, they also raise some scoping issues that may seem subtle at first glance. This is core language territory, but it comes up often in practice in conjunction with GUI.

Arguments versus globals

For instance, notice that the handler function in the prior section’s initial code could also refer to X directly, because it is a global variable (and would exist by the time the code inside the handler is run). Because of that, we might make the handler a one-argument function and pass in just the string 'spam' in the lambda:

def handler(A):                           # X is in my global scope, implicitly
    ...use global X and argument A...

X = 42
Button(text='ni', command=(lambda: handler('spam')))

For that matter, A could be moved out to the global scope too, to remove the need for lambda here entirely; we could register the handler itself and cut out the middleman.

Although simple in this trivial example, arguments are generally preferred to globals, because they make external dependencies more explicit, and so make code easier to understand and change. In fact, the same handler might be usable in other contexts, if we don’t couple it to global variables’ values. While you’ll have to take it on faith until we step up to larger examples with more complex state retention needs, avoiding globals in callbacks and GUIs in general both makes them more reusable, and supports the notion of multiple instances in the same program. It’s good programming practice, GUI or not.

Passing in enclosing scope values with default arguments

More subtly, notice that if the button in this example was constructed inside a function rather than at the top level of the file, name X would no longer be global but would be in the enclosing function’s local scope; it seems as if it would disappear after the function exits and before the callback event occurs and runs the lambda’s code:

def handler(A, B):
    ...use A and B...

def makegui():
    X = 42
    Button(text='ni', command=(lambda: handler(X, 'spam')))       # remembers X

makegui()
mainloop()   # makegui's scope is gone by this point

Luckily, Python’s enclosing scope reference model means that the value of X in the local scope enclosing the lambda function is automatically retained, for use later when the button press occurs. This usually works as we want today, and automatically handles variable references in this role.

To make such enclosing scope usage explicit, though, default argument values can also be used to remember the values of variables in the enclosing local scope, even after the enclosing function returns. In the following code, for instance, the default argument name X (on the left side of the X=X default) will remember object 42, because the variable name X (on the right side of the X=X) is evaluated in the enclosing scope, and the generated function is later called without any arguments:

def handler(A, B):              # older Pythons: defaults save state
    ...use A and B...

def makegui():
    X = 42
    Button(text='ni', command=(lambda X=X: handler(X, 'spam')))

Since default arguments are evaluated and saved when the lambda runs (not when the function it creates is later called), they are a way to explicitly remember objects that must be accessed again later, during event processing. Because tkinter calls the lambda function later with no arguments, all its defaults are used.

This was not an issue in the original version of this example because name X lived in the global scope, and the code of the lambda will find it there when it is run. When nested within a function, though, X may have disappeared after the enclosing function exits.

Passing in enclosing scope values with automatic references

While they can make some external dependencies more explicit, defaults are not usually required (since Python 2.2, at least) and are not used for this role in best practice code today. Rather, lambdas simply defer the call to the actual handler and provide extra handler arguments. Variables from the enclosing scope used by the lambda are automatically retained, even after the enclosing function exits.

The prior code listing, for example, can today normally be coded as we did earlier—name X in the handler will be automatically mapped to X in the enclosing scope, and so effectively remember what X was when the button was made:

def makegui():
    X = 42                                                    # X is retained auto
    Button(text='ni', command=(lambda: handler(X, 'spam')))   # no need for defaults

We’ll see this technique put to more concrete use later. When using classes to build your GUI, for instance, the self argument is a local variable in methods, and is thus automatically available in the bodies of lambda functions. There is no need to pass it in explicitly with defaults:

class Gui:
    def handler(self, A, B):
        ...use self, A and B...
    def makegui(self):
        X = 42
        Button(text='ni', command=(lambda: self.handler(X, 'spam')))

Gui().makegui()
mainloop()

When using classes, though, instance attributes can provide extra state for use in callback handlers, and so provide an alternative to extra call arguments. We’ll see how in a moment. First, though, we need to take a quick non-GUI diversion into a dark corner of Python’s scope rules to understand why default arguments are still sometimes necessary to pass values into nested lambda functions, especially in GUIs.

But you must still sometimes use defaults instead of enclosing scopes

Although you may still see defaults used to pass in enclosing scope references in some older Python code, automatic enclosing scope references are generally preferred today. In fact, it seems as though the newer nested scope lookup rules in Python automate and replace the previously manual task of passing in enclosing scope values with defaults altogether.

Well, almost. There is a catch. It turns out that within a lambda (or def), references to names in the enclosing scope are actually resolved when the generated function is called, not when it is created. Because of this, when the function is later called, such name references will reflect the latest or final assignments made to the names anywhere in the enclosing scope, which are not necessarily the values they held when the function was made. This holds true even when the callback function is nested only in a module’s global scope, not in an enclosing function; in either case, all enclosing scope references are resolved at function call time, not at function creation time.

This is subtly different from default argument values, which are evaluated once when the function is created, not when it is later called. Because of that, defaults can still be useful for remembering the values of enclosing scope variables as they were when you made the function. Unlike enclosing scope name references, defaults will not have a different value if the variable later changes in the enclosing scope, between function creation and call. (In fact, this is why mutable defaults like lists retain their state between calls—they are created only once, when the function is made, and attached to the function itself.)

This is normally a nonissue, because most enclosing scope references name a variable that is assigned just once in the enclosing scope (the self argument in class methods, for example). But this can lead to coding mistakes if not understood, especially if you create functions within a loop; if those functions reference the loop variable, it will evaluate to the value it was given on the last loop iteration in all the functions generated. By contrast, if you use defaults instead, each function will remember the current value of the loop variable, not the last.

Because of this difference, nested scope references are not always sufficient to remember enclosing scope values, and defaults are sometimes still required today. Let’s see what this means in terms of code. Consider the following nested function (this section’s code snippets are saved in file defaults.py in the examples package, if you want to experiment with them).

def simple():
    spam = 'ni'
    def action():
        print(spam)         # name maps to enclosing function
    return action

act = simple()              # make and return nested function
act()                       # then call it: prints 'ni'

This is the simple case for enclosing scope references, and it works the same way whether the nested function is generated with a def or a lambda. But notice that this still works if we assign the enclosing scope’s spam variable after the nested function is created:

def normal():
    def action():
        return spam         # really, looked up when used
    spam = 'ni'
    return action

act = normal()
print(act())                # also prints 'ni'

As this implies, the enclosing scope name isn’t resolved when the nested function is made—in fact, the name hasn’t even been assigned yet in this example. The name is resolved when the nested function is called. The same holds true for lambdas:

def weird():
    spam = 42
    return (lambda: spam * 2)       # remembers spam in enclosing scope

act = weird()
print(act())                        # prints 84

So far, so good. The spam inside this nested lambda function remembers the value that this variable had in the enclosing scope, even after the enclosing scope exits. This pattern corresponds to a registered GUI callback handler run later on events. But once again, the nested scope reference really isn’t being resolved when the lambda is run to create the function; it’s being resolved when the generated function is later called. To make that more apparent, look at this code:

def weird():
    tmp = (lambda: spam * 2)        # remembers spam
    spam = 42                       # even though not set till here
    return tmp

act = weird()
print(act())                        # prints 84

Here again, the nested function refers to a variable that hasn’t even been assigned yet when that function is made. Really, enclosing scope references yield the latest setting made in the enclosing scope, whenever the function is called. Watch what happens in the following code:

def weird():
    spam = 42
    handler = (lambda: spam * 2)     # func doesn't save 42 now
    spam = 50
    print(handler())                 # prints 100: spam looked up now
    spam = 60
    print(handler())                 # prints 120: spam looked up again now

weird()

Now, the reference to spam inside the lambda is different each time the generated function is called! In fact, it refers to what the variable was set to last in the enclosing scope at the time the nested function is called, because it is resolved at function call time, not at function creation time.

In terms of GUIs, this becomes significant most often when you generate callback handlers within loops and try to use enclosing scope references to remember extra data created within the loops. If you’re going to make functions within a loop, you have to apply the last example’s behavior to the loop variable:

def odd():
    funcs = []
    for c in 'abcdefg':
       funcs.append((lambda: c))      # c will be looked up later
    return funcs                      # does not remember current c

for func in odd():
    print(func(), end=' ')            # OOPS: print 7 g's, not a,b,c,... !

Here, the func list simulates registered GUI callback handlers associated with widgets. This doesn’t work the way most people expect it to. The variable c within the nested function will always be g here, the value that the variable was set to on the final iteration of the loop in the enclosing scope. The net effect is that all seven generated lambda functions wind up with the same extra state information when they are later called.

Analogous GUI code that adds information to lambda callback handlers will have similar problems—all buttons created in a loop, for instance, may wind up doing the same thing when clicked! To make this work, we still have to pass values into the nested function with defaults in order to save the current value of the loop variable (not its future value):

def odd():
    funcs = []
    for c in 'abcdefg':
       funcs.append((lambda c=c: c))    # force to remember c now
    return funcs                        # defaults eval now

for func in odd():
    print(func(), end=' ')              # OK: now prints a,b,c,...

This works now only because the default, unlike an external scope reference, is evaluated at function creation time, not at function call time. It remembers the value that a name in the enclosing scope had when the function was made, not the last assignment made to that name anywhere in the enclosing scope. The same is true even if the function’s enclosing scope is a module, not another function; if we don’t use the default argument in the following code, the loop variable will resolve to the same value in all seven functions:

funcs = []                              # enclosing scope is module
for c in 'abcdefg':                     # force to remember c now
   funcs.append((lambda c=c: c))        # else prints 7 g's again

for func in funcs:
    print(func(), end=' ')              # OK: prints a,b,c,...

The moral of this story is that enclosing scope name references are a replacement for passing values in with defaults, but only as long as the name in the enclosing scope will not change to a value you don’t expect after the nested function is created. You cannot generally reference enclosing scope loop variables within a nested function, for example, because they will change as the loop progresses. In most other cases, though, enclosing scope variables will take on only one value in their scope and so can be used freely.

We’ll see this phenomenon at work in later examples that construct larger GUIs. For now, remember that enclosing scopes are not a complete replacement for defaults; defaults are still required in some contexts to pass values into callback functions. Also keep in mind that classes are often a better and simpler way to retain extra state for use in callback handlers than are nested functions. Because state is explicit in classes, these scope issues do not apply. The next two sections cover this in detail.

Bound Method Callback Handlers

Let’s get back to coding GUIs. Although functions and lambdas suffice in many cases, bound methods of class instances work particularly well as callback handlers in GUIs—they record both an instance to send the event to and an associated method to call. For instance, Example 7-14 shows Examples 7-12 and 7-13 rewritten to register a bound class method rather than a function or lambda result.

Example 7-14. PP4EGuiIntrogui3c.py
import sys
from tkinter import *

class HelloClass:
    def __init__(self):
        widget = Button(None, text='Hello event world', command=self.quit)
        widget.pack()

    def quit(self):
        print('Hello class method world')   # self.quit is a bound method
        sys.exit()                          # retains the self+quit pair

HelloClass()
mainloop()

On a button press, tkinter calls this class’s quit method with no arguments, as usual. But really, it does receive one argument—the original self object—even though tkinter doesn’t pass it explicitly. Because the self.quit bound method retains both self and quit, it’s compatible with a simple function call; Python automatically passes the self argument along to the method function. Conversely, registering an unbound instance method that expects an argument, such as HelloClass.quit, won’t work, because there is no self object to pass along when the event later occurs.

Later, we’ll see that class callback handler coding schemes provide a natural place to remember information for use on events—simply assign the information to self instance attributes:

class someGuiClass:
    def __init__(self):
        self.X = 42
        self.Y = 'spam'
        Button(text='Hi', command=self.handler)
    def handler(self):
        ...use self.X, self.Y...

Because the event will be dispatched to this class’s method with a reference to the original instance object, self gives access to attributes that retain original data. In effect, the instance’s attributes retain state information to be used when events occur. Especially in larger GUIs, this is a much more flexible technique than global variables or extra arguments added by lambdas.

Callable Class Object Callback Handlers

Because Python class instance objects can also be called if they inherit a __call__ method to intercept the operation, we can pass one of these to serve as a callback handler too. Example 7-15 shows a class that provides the required function-like interface.

Example 7-15. PP4EGuiIntrogui3d.py
import sys
from tkinter import *

class HelloCallable:
    def __init__(self):                        # __init__ run on object creation
        self.msg = 'Hello __call__ world'

    def __call__(self):
        print(self.msg)                        # __call__ run later when called
        sys.exit()                             # class object looks like a function

widget = Button(None, text='Hello event world', command=HelloCallable())
widget.pack()
widget.mainloop()

Here, the HelloCallable instance registered with command can be called like a normal function; Python invokes its __call__ method to handle the call operation made in tkinter on the button press. In effect, the general __call__ method replaces a specific bound method in this case. Notice how self.msg is used to retain information for use on events here; self is the original instance when the special __call__ method is automatically invoked.

All four gui3 variants create the same sort of GUI window (Figure 7-11), but print different messages to stdout when their button is pressed:

C:...PP4EGuiIntro> python gui3.py
Hello, I must be going...

C:...PP4EGuiIntro> python gui3b.py
Hello lambda world

C:...PP4EGuiIntro> python gui3c.py
Hello class method world

C:...PP4EGuiIntro> python gui3d.py
Hello __call__ world

There are good reasons for each callback coding scheme (function, lambda, class method, callable class), but we need to move on to larger examples in order to uncover them in less theoretical terms.

Other tkinter Callback Protocols

For future reference, also keep in mind that using command options to intercept user-generated button press events is just one way to register callbacks in tkinter. In fact, there are a variety of ways for tkinter scripts to catch events:

Button command options

As we’ve just seen, button press events are intercepted by providing a callable object in widget command options. This is true of other kinds of button-like widgets we’ll meet in Chapter 8 (e.g., radio and check buttons and scales).

Menu command options

In the upcoming tkinter tour chapters, we’ll also find that a command option is used to specify callback handlers for menu selections.

Scroll bar protocols

Scroll bar widgets register handlers with command options, too, but they have a unique event protocol that allows them to be cross-linked with the widget they are meant to scroll (e.g., listboxes, text displays, and canvases): moving the scroll bar automatically moves the widget, and vice versa.

General widget bind methods

A more general tkinter event bind method mechanism can be used to register callback handlers for lower-level interface events—key presses, mouse movement and clicks, and so on. Unlike command callbacks, bind callbacks receive an event object argument (an instance of the tkinter Event class) that gives context about the event—subject widget, screen coordinates, and so on.

Window manager protocols

In addition, scripts can also intercept window manager events (e.g., window close requests) by tapping into the window manager protocol method mechanism available on top-level window objects. Setting a handler for WM_DELETE_WINDOW, for instance, takes over window close buttons.

Scheduled event callbacks

Finally, tkinter scripts can also register callback handlers to be run in special contexts, such as timer expirations, input data arrival, and event-loop idle states. Scripts can also pause for state-change events related to windows and special variables. We’ll meet these event interfaces in more detail near the end of Chapter 9.

Binding Events

Of all the options listed in the prior section, bind is the most general, but also perhaps the most complex. We’ll study it in more detail later, but to let you sample its flavor now, Example 7-16 rewrites the prior section’s GUI again to use bind, not the command keyword, to catch button presses.

Example 7-16. PP4EGuiIntrogui3e.py
import sys
from tkinter import *

def hello(event):
    print('Press twice to exit')             # on single-left click

def quit(event):                             # on double-left click
    print('Hello, I must be going...')       # event gives widget, x/y, etc.
    sys.exit()

widget = Button(None, text='Hello event world')
widget.pack()
widget.bind('<Button-1>', hello)             # bind left mouse clicks
widget.bind('<Double-1>', quit)              # bind double-left clicks
widget.mainloop()

In fact, this version doesn’t specify a command option for the button at all. Instead, it binds lower-level callback handlers for both left mouse clicks (<Button-1>) and double-left mouse clicks (<Double-1>) within the button’s display area. The bind method accepts a large set of such event identifiers in a variety of formats, which we’ll meet in Chapter 8.

When run, this script makes the same window as before (see Figure 7-11). Clicking on the button once prints a message but doesn’t exit; you need to double-click on the button now to exit as before. Here is the output after clicking twice and double-clicking once (a double-click fires the single-click callback first):

C:...PP4EGuiIntro> python gui3e.py
Press twice to exit
Press twice to exit
Press twice to exit
Hello, I must be going...

Although this script intercepts button clicks manually, the end result is roughly the same; widget-specific protocols such as button command options are really just higher-level interfaces to events you can also catch with bind.

We’ll meet bind and all of the other tkinter event callback handler hooks again in more detail later in this book. First, though, let’s focus on building GUIs that are larger than a single button and explore a few other ways to use classes in GUI work.

Adding Multiple Widgets

It’s time to start building user interfaces with more than one widget. Example 7-17 makes the window shown in Figure 7-12.

Example 7-17. PP4EGuiIntrogui4.py
from tkinter import *

def greeting():
    print('Hello stdout world!...')

win = Frame()
win.pack()
Label(win,  text='Hello container world').pack(side=TOP)
Button(win, text='Hello', command=greeting).pack(side=LEFT)
Button(win, text='Quit',  command=win.quit).pack(side=RIGHT)

win.mainloop()
A multiple-widget window
Figure 7-12. A multiple-widget window

This example makes a Frame widget (another tkinter class) and attaches three other widget objects to it, a Label and two Buttons, by passing the Frame as their first argument. In tkinter terms, we say that the Frame becomes a parent to the other three widgets. Both buttons on this display trigger callbacks:

  • Pressing the Hello button triggers the greeting function defined within this file, which prints to stdout again.

  • Pressing the Quit button calls the standard tkinter quit method, inherited by win from the Frame class (Frame.quit has the same effect as the Tk.quit we used earlier).

Here is the stdout text that shows up on Hello button presses, wherever this script’s standard streams may be:

C:...PP4EGuiIntro> python gui4.py
Hello stdout world!...
Hello stdout world!...
Hello stdout world!...
Hello stdout world!...

The notion of attaching widgets to containers turns out to be at the core of layouts in tkinter. Before we go into more detail on that topic, though, let’s get small.

Widget Resizing Revisited: Clipping

Earlier, we saw how to make widgets expand along with their parent window, by passing expand and fill options to the pack geometry manager. Now that we have a window with more than one widget, I can let you in on one of the more useful secrets in the packer. As a rule, widgets packed first are clipped last when a window is shrunk. That is, the order in which you pack items determines which items will be cut out of the display if it is made too small. Widgets packed later are cut out first. For example, Figure 7-13 shows what happens when the gui4 window is shrunk interactively.

gui4 gets small
Figure 7-13. gui4 gets small

Try reordering the label and button lines in the script and see what happens when the window shrinks; the first one packed is always the last to go away. For instance, if the label is packed last, Figure 7-14 shows that it is clipped first, even though it is attached to the top: side attachments and packing order both impact the overall layout, but only packing order matters when windows shrink. Here are the changed lines:

Button(win, text='Hello', command=greeting).pack(side=LEFT)
Button(win, text='Quit',  command=win.quit).pack(side=RIGHT)
Label(win,  text='Hello container world').pack(side=TOP)
Label packed last, clipped first
Figure 7-14. Label packed last, clipped first

tkinter keeps track of the packing order internally to make this work. Scripts can plan ahead for shrinkage by calling pack methods of more important widgets first. For instance, on the upcoming tkinter tour, we’ll meet code that builds menus and toolbars at the top and bottom of the window; to make sure these are lost last as a window is shrunk, they are packed first, before the application components in the middle. Similarly, displays that include scroll bars normally pack them before the items they scroll (e.g., text, lists) so that the scroll bars remain as the window shrinks.

Attaching Widgets to Frames

In larger terms, the critical innovation in this example is its use of frames: Frame widgets are just containers for other widgets, and so give rise to the notion of GUIs as widget hierarchies, or trees. Here, win serves as an enclosing window for the other three widgets. In general, though, by attaching widgets to frames, and frames to other frames, we can build up arbitrary GUI layouts. Simply divide the user interface into a set of increasingly smaller rectangles, implement each as a tkinter Frame, and attach basic widgets to the frame in the desired screen position.

In this script, when you specify win in the first argument to the Label and Button constructors, tkinter attaches them to the Frame (they become children of the win parent). win itself is attached to the default top-level window, since we didn’t pass a parent to the Frame constructor. When we ask win to run itself (by calling mainloop), tkinter draws all the widgets in the tree we’ve built.

The three child widgets also provide pack options now: the side arguments tell which part of the containing frame (i.e., win) to attach the new widget to. The label hooks onto the top, and the buttons attach to the sides. TOP, LEFT, and RIGHT are all preassigned string variables imported from tkinter. Arranging widgets is a bit subtler than simply giving a side, though, but we need to take a quick detour into packer geometry management details to see why.

Layout: Packing Order and Side Attachments

When a widget tree is displayed, child widgets appear inside their parents and are arranged according to their order of packing and their packing options. Because of this, the order in which widgets are packed not only gives their clipping order, but also determines how their side settings play out in the generated display.

Here’s how the packer’s layout system works:

  1. The packer starts out with an available space cavity that includes the entire parent container (e.g., the whole Frame or top-level window).

  2. As each widget is packed on a side, that widget is given the entire requested side in the remaining space cavity, and the space cavity is shrunk.

  3. Later pack requests are given an entire side of what is left, after earlier pack requests have shrunk the cavity.

  4. After widgets are given cavity space, expand divides any space left, and fill and anchor stretch and position widgets within their assigned space.

For instance, if you recode the gui4 child widget creation logic like this:

Button(win, text='Hello', command=greeting).pack(side=LEFT)
Label(win,  text='Hello container world').pack(side=TOP)
Button(win, text='Quit',  command=win.quit).pack(side=RIGHT)

you will wind up with the very different display shown in Figure 7-15, even though you’ve moved the label code only one line down in the source file (contrast with Figure 7-12).

Packing the label second
Figure 7-15. Packing the label second

Despite its side setting, the label does not get the entire top of the window now, and you have to think in terms of shrinking cavities to understand why. Because the Hello button is packed first, it is given the entire LEFT side of the Frame. Next, the label is given the entire TOP side of what is left. Finally, the Quit button gets the RIGHT side of the remainder—a rectangle to the right of the Hello button and under the label. When this window shrinks, widgets are clipped in reverse order of their packing: the Quit button disappears first, followed by the label.[30]

In the original version of this example (Figure 7-12), the label spans the entire top side just because it is the first one packed, not because of its side option. In fact, if you look at Figure 7-14 closely, you’ll see that it illustrates the same point—the label appeared between the buttons, because they had already carved off the entire left and right sides.

The Packer’s Expand and Fill Revisited

Beyond the effects of packing order, the fill option we met earlier can be used to stretch the widget to occupy all the space in the cavity side it has been given, and any cavity space left after all packing is evenly allocated among widgets with the expand=YES we saw before. For example, coding this way creates the window in Figure 7-16 (compare this to Figure 7-15):

Button(win, text='Hello', command=greeting).pack(side=LEFT,fill=Y)
Label(win,  text='Hello container world').pack(side=TOP)
Button(win, text='Quit', command=win.quit).pack(side=RIGHT, expand=YES, fill=X)
Packing with expand and fill options
Figure 7-16. Packing with expand and fill options

To make all of these grow along with their window, though, we also need to make the container frame expandable; widgets expand beyond their initial packer arrangement only if all of their parents expand, too. Here are the changes in gui4.py:

win = Frame()
win.pack(side=TOP, expand=YES, fill=BOTH)
Button(win, text='Hello', command=greeting).pack(side=LEFT, fill=Y)
Label(win,  text='Hello container world').pack(side=TOP)
Button(win, text='Quit', command=win.quit).pack(side=RIGHT, expand=YES,fill=X)

When this code runs, the Frame is assigned the entire top side of its parent as before (that is, the top parcel of the root window); but because it is now marked to expand into unused space in its parent and to fill that space both ways, it and all of its attached children expand along with the window. Figure 7-17 shows how.

gui4 gets big with an expandable frame
Figure 7-17. gui4 gets big with an expandable frame

Using Anchor to Position Instead of Stretch

And as if that isn’t flexible enough, the packer also allows widgets to be positioned within their allocated space with an anchor option, instead of filling that space with a fill. The anchor option accepts tkinter constants identifying all eight points of the compass (N, NE, NW, S, etc.) and CENTER as its value (e.g., anchor=NW). It instructs the packer to position the widget at the desired position within its allocated space, if the space allocated for the widget is larger than the space needed to display the widget.

The default anchor is CENTER, so widgets show up in the middle of their space (the cavity side they were given) unless they are positioned with anchor or stretched with fill. To demonstrate, change gui4 to use this sort of code:

Button(win, text='Hello', command=greeting).pack(side=LEFT, anchor=N)
Label(win,  text='Hello container world').pack(side=TOP)
Button(win, text='Quit',  command=win.quit).pack(side=RIGHT)

The only thing new here is that the Hello button is anchored to the north side of its space allocation. Because this button was packed first, it got the entire left side of the parent frame. This is more space than is needed to show the button, so it shows up in the middle of that side by default, as in Figure 7-15 (i.e., anchored to the center). Setting the anchor to N moves it to the top of its side, as shown in Figure 7-18.

Anchoring a button to the north
Figure 7-18. Anchoring a button to the north

Keep in mind that fill and anchor are applied after a widget has been allocated cavity side space by its side, packing order, and expand extra space request. By playing with packing orders, sides, fills, and anchors, you can generate lots of layout and clipping effects, and you should take a few moments to experiment with alternatives if you haven’t already. In the original version of this example, for instance, the label spans the entire top side just because it is the first packed.

As we’ll see later, frames can be nested in other frames, too, in order to make more complex layouts. In fact, because each parent container is a distinct space cavity, this provides a sort of escape mechanism for the packer cavity algorithm: to better control where a set of widgets show up, simply pack them within a nested subframe and attach the frame as a package to a larger container. A row of push buttons, for example, might be easier laid out in a frame of its own than if mixed with other widgets in the display directly.

Finally, also keep in mind that the widget tree created by these examples is really an implicit one; tkinter internally records the relationships implied by passed parent widget arguments. In OOP terms, this is a composition relationship—the Frame contains a Label and Buttons. Let’s look at inheritance relationships next.

Customizing Widgets with Classes

You don’t have to use OOP in tkinter scripts, but it can definitely help. As we just saw, tkinter GUIs are built up as class-instance object trees. Here’s another way Python’s OOP features can be applied to GUI models: specializing widgets by inheritance. Example 7-18 builds the window in Figure 7-19.

Example 7-18. PP4EGuiIntrogui5.py
from tkinter import *

class HelloButton(Button):
    def __init__(self, parent=None, **config):         # add callback method
        Button.__init__(self, parent, **config)        # and pack myself
        self.pack()                                    # could config style too
        self.config(command=self.callback)

    def callback(self):                                # default press action
        print('Goodbye world...')                      # replace in subclasses
        self.quit()

if __name__ == '__main__':
    HelloButton(text='Hello subclass world').mainloop()
A button subclass in action
Figure 7-19. A button subclass in action

This example isn’t anything special to look at: it just displays a single button that, when pressed, prints a message and exits. But this time, it is a button widget we created on our own. The HelloButton class inherits everything from the tkinter Button class, but adds a callback method and constructor logic to set the command option to self.callback, a bound method of the instance. When the button is pressed this time, the new widget class’s callback method, not a simple function, is invoked.

The **config argument here is assigned unmatched keyword arguments in a dictionary, so they can be passed along to the Button constructor. The **config in the Button constructor call unpacks the dictionary back into keyword arguments (it’s actually optional here, because of the old-style dictionary widget call form we met earlier, but doesn’t hurt). We met the config widget method called in HelloButton’s constructor earlier; it is just an alternative way to pass configuration options after the fact (instead of passing constructor arguments).

Standardizing Behavior and Appearance

So what’s the point of subclassing widgets like this? In short, it allows sets of widgets made from the customized classes to look and act the same. When coded well, we get both “for free” from Python’s OOP model. This can be a powerful technique in larger programs.

Common behavior

Example 7-18 standardizes behavior—it allows widgets to be configured by subclassing instead of by passing in options. In fact, its HelloButton is a true button; we can pass in configuration options such as its text as usual when one is made. But we can also specify callback handlers by overriding the callback method in subclasses, as shown in Example 7-19.

Example 7-19. PP4EGuiIntrogui5b.py
from gui5 import HelloButton

class MyButton(HelloButton):        # subclass HelloButton
    def callback(self):             # redefine press-handler method
        print("Ignoring press!...")

if __name__ == '__main__':
    MyButton(None, text='Hello subclass world').mainloop()

This script makes the same window; but instead of exiting, this MyButton button, when pressed, prints to stdout and stays up. Here is its standard output after being pressed a few times:

C:...PP4EGuiIntro> python gui5b.py
Ignoring press!...
Ignoring press!...
Ignoring press!...
Ignoring press!...

Whether it’s simpler to customize widgets by subclassing or passing in options is probably a matter of taste in this simple example. But the larger point to notice is that Tk becomes truly object oriented in Python, just because Python is object oriented—we can specialize widget classes using normal class-based and object-oriented techniques. In fact this applies to both widget behavior and appearance.

Common appearance

For example, although we won’t study widget configuration options until the next chapter, a similar customized button class could provide a standard look-and-feel different from tkinter’s defaults for every instance created from it, and approach the notions of “styles” or “themes” in some GUI toolkits:

class ThemedButton(Button):                             # config my style too
    def __init__(self, parent=None, **configs):         # used for each instance
        Button.__init__(self, parent, **configs)        # see chapter 8 for options
        self.pack()
        self.config(fg='red', bg='black', font=('courier', 12), relief=RAISED, bd=5)

B1 = ThemedButton(text='spam', command=onSpam)  # normal button widget objects
B2 = ThemedButton(text='eggs')                  # but same appearance by inheritance
B2.pack(expand=YES, fill=BOTH)

This code is something of a preview; see file gui5b-themed.py in the examples package for a complete version, and watch for more on its widget configuration options in Chapter 8. But it illustrates the application of common appearance by subclassing widgets directly—every button created from its class looks the same, and will pick up any future changes in its configurations automatically.

Widget subclasses are a programmer’s tool, of course, but we can also make such configurations accessible to a GUI’s users. In larger programs later in the book (e.g., PyEdit, PyClock, and PyMailGUI), we’ll sometimes achieve a similar effect by importing configurations from modules and applying them to widgets as they are built. If such external settings are used by a customized widget subclass like our ThemedButton above, they will again apply to all its instances and subclasses (for reference, the full version of the following code is in file gui5b-themed-user.py):

from user_preferences import bcolor, bfont, bsize   # get user settings

class ThemedButton(Button):
    def __init__(self, parent=None, **configs):
        Button.__init__(self, parent, **configs)
        self.pack()
        self.config(bg=bcolor, font=(bfont, bsize))

ThemedButton(text='spam', command=onSpam)  # normal button widget objects
ThemedButton(text='eggs', command=onEggs)  # all inherit user preferences

class MyButton(ThemedButton):              # subclasses inherit prefs too
    def __init__(self, parent=None, **configs):
        ThemedButton.__init__(self, parent,  **configs)
        self.config(text='subclass')

MyButton(command=onSpam)

Again, more on widget configuration in the next chapter; the big picture to take away here is that customizing widget classes with subclasses allows us to tailor both their behavior and their appearance for an entire set of widgets. The next example provides yet another way to arrange for specialization—as customizable and attachable widget packages, usually known as components.

Reusable GUI Components with Classes

Larger GUI interfaces are often built up as subclasses of Frame, with callback handlers implemented as methods. This structure gives us a natural place to store information between events: instance attributes record state. It also allows us to both specialize GUIs by overriding their methods in new subclasses and attach them to larger GUI structures to reuse them as general components. For instance, a GUI text editor implemented as a Frame subclass can be attached to and configured by any number of other GUIs; if done well, we can plug such a text editor into any user interface that needs text editing tools.

We’ll meet such a text editor component in Chapter 11. For now, Example 7-20 illustrates the concept in a simple way. The script gui6.py produces the window in Figure 7-20.

Example 7-20. PP4EGuiIntrogui6.py
from tkinter import *

class Hello(Frame):                              # an extended Frame
    def __init__(self, parent=None):
        Frame.__init__(self, parent)             # do superclass init
        self.pack()
        self.data = 42
        self.make_widgets()                      # attach widgets to self

    def make_widgets(self):
        widget = Button(self, text='Hello frame world!', command=self.message)
        widget.pack(side=LEFT)

    def message(self):
        self.data += 1
        print('Hello frame world %s!' % self.data)

if __name__ == '__main__': Hello().mainloop()
A custom Frame in action
Figure 7-20. A custom Frame in action

This example pops up a single-button window. When pressed, the button triggers the self.message bound method to print to stdout again. Here is the output after pressing this button four times; notice how self.data (a simple counter here) retains its state between presses:

C:...PP4EGuiIntro> python gui6.py
Hello frame world 43!
Hello frame world 44!
Hello frame world 45!
Hello frame world 46!

This may seem like a roundabout way to show a Button (we did it in fewer lines in Examples 7-10, 7-11, and 7-12). But the Hello class provides an enclosing organizational structure for building GUIs. In the examples prior to the last section, we made GUIs using a function-like approach: we called widget constructors as though they were functions and hooked widgets together manually by passing in parents to widget construction calls. There was no notion of an enclosing context, apart from the global scope of the module file containing the widget calls. This works for simple GUIs but can make for brittle code when building up larger GUI structures.

But by subclassing Frame as we’ve done here, the class becomes an enclosing context for the GUI:

  • Widgets are added by attaching objects to self, an instance of a Frame container subclass (e.g., Button).

  • Callback handlers are registered as bound methods of self, and so are routed back to code in the class (e.g., self.message).

  • State information is retained between events by assigning to attributes of self, visible to all callback methods in the class (e.g., self.data).

  • It’s easy to make multiple copies of such a GUI component, even within the same process, because each class instance is a distinct namespace.

  • Classes naturally support customization by inheritance and by composition attachment.

In a sense, entire GUIs become specialized Frame objects with extensions for an application. Classes can also provide protocols for building widgets (e.g., the make_widgets method here), handle standard configuration chores (like setting window manager options), and so on. In short, Frame subclasses provide a simple way to organize collections of other widget-class objects.

Attaching Class Components

Perhaps more importantly, subclasses of Frame are true widgets: they can be further extended and customized by subclassing and can be attached to enclosing widgets. For instance, to attach the entire package of widgets that a class builds to something else, simply create an instance of the class with a real parent widget passed in. To illustrate, running the script in Example 7-21 creates the window shown in Figure 7-21.

Example 7-21. PP4EGuiIntrogui6b.py
from sys import exit
from tkinter import *                    # get Tk widget classes
from gui6 import Hello                   # get the subframe class

parent = Frame(None)                     # make a container widget
parent.pack()
Hello(parent).pack(side=RIGHT)           # attach Hello instead of running it

Button(parent, text='Attach', command=exit).pack(side=LEFT)
parent.mainloop()
An attached class component on the right
Figure 7-21. An attached class component on the right

This script just adds Hello’s button to the right side of parent—a container Frame. In fact, the button on the right in this window represents an embedded component: its button really represents an attached Python class object. Pressing the embedded class’s button on the right prints a message as before; pressing the new button exits the GUI by a sys.exit call:

C:...PP4EGuiIntro> python gui6b.py
Hello frame world 43!
Hello frame world 44!
Hello frame world 45!
Hello frame world 46!

In more complex GUIs, we might instead attach large Frame subclasses to other container components and develop each independently. For instance, Example 7-22 is yet another specialized Frame itself, but it attaches an instance of the original Hello class in a more object-oriented fashion. When run as a top-level program, it creates a window identical to the one shown in Figure 7-21.

Example 7-22. PP4EGuiIntrogui6c.py
from tkinter import *                    # get Tk widget classes
from gui6 import Hello                   # get the subframe class

class HelloContainer(Frame):
    def __init__(self, parent=None):
        Frame.__init__(self, parent)
        self.pack()
        self.makeWidgets()

    def makeWidgets(self):
        Hello(self).pack(side=RIGHT)     # attach a Hello to me
        Button(self, text='Attach', command=self.quit).pack(side=LEFT)

if __name__ == '__main__': HelloContainer().mainloop()

This looks and works exactly like gui6b but registers the added button’s callback handler as self.quit, which is just the standard quit widget method this class inherits from Frame. The window this time represents two Python classes at work—the embedded component’s widgets on the right (the original Hello button) and the container’s widgets on the left.

Naturally, this is a simple example (we attached only a single button here, after all). But in more practical user interfaces, the set of widget class objects attached in this way can be much larger. If you imagine replacing the Hello call in this script with a call to attach an already coded and fully debugged calculator object, you’ll begin to better understand the power of this paradigm. If we code all of our GUI components as classes, they automatically become a library of reusable widgets, which we can combine in other applications as often as we like.

Extending Class Components

When GUIs are built with classes, there are a variety of ways to reuse their code in other displays. To extend Hello instead of attaching it, we just override some of its methods in a new subclass (which itself becomes a specialized Frame widget). This technique is shown in Example 7-23.

Example 7-23. PP4EGuiIntrogui6d.py
from tkinter import *
from gui6 import Hello

class HelloExtender(Hello):
    def make_widgets(self):                       # extend method here
        Hello.make_widgets(self)
        Button(self, text='Extend', command=self.quit).pack(side=RIGHT)

    def message(self):
        print('hello', self.data)                 # redefine method here

if __name__ == '__main__': HelloExtender().mainloop()

This subclass’s make_widgets method here first builds the superclass’s widgets and then adds a second Extend button on the right, as shown in Figure 7-22.

A customized class’s widgets, on the left
Figure 7-22. A customized class’s widgets, on the left

Because it redefines the message method, pressing the original superclass’s button on the left now prints a different string to stdout (when searching up from self, the message attribute is found first in this subclass, not in the superclass):

C:...PP4EGuiIntro> python gui6d.py
hello 42
hello 42
hello 42
hello 42

But pressing the new Extend button on the right, which is added by this subclass, exits immediately, since the quit method (inherited from Hello, which inherits it from Frame) is the added button’s callback handler. The net effect is that this class customizes the original to add a new button and change message’s behavior.

Although this example is simple, it demonstrates a technique that can be powerful in practice: to change a GUI’s behavior, we can write a new class that customizes its parts rather than changing the existing GUI code in place. The main code need be debugged only once and can be customized with subclasses as unique needs arise.

The moral of this story is that tkinter GUIs can be coded without ever writing a single new class, but using classes to structure your GUI code makes it much more reusable in the long run. If done well, you can both attach already debugged components to new interfaces and specialize their behavior in new external subclasses as needed for custom requirements. Either way, the initial upfront investment to use classes is bound to save coding time in the end.

Standalone Container Classes

Before we move on, I want to point out that it’s possible to reap most of the class-based component benefits previously mentioned by creating standalone classes not derived from tkinter Frames or other widgets. For instance, the class in Example 7-24 generates the window shown in Figure 7-23.

Example 7-24. PP4EGuiIntrogui7.py
from tkinter import *

class HelloPackage:                            # not a widget subbclass
    def __init__(self, parent=None):
        self.top = Frame(parent)               # embed a Frame
        self.top.pack()
        self.data = 0
        self.make_widgets()                    # attach widgets to self.top

    def make_widgets(self):
        Button(self.top, text='Bye', command=self.top.quit).pack(side=LEFT)
        Button(self.top, text='Hye', command=self.message).pack(side=RIGHT)

    def message(self):
        self.data += 1
        print('Hello number', self.data)

if __name__ == '__main__': HelloPackage().top.mainloop()
A standalone class package in action
Figure 7-23. A standalone class package in action

When run, the Hye button here prints to stdout and the Bye button closes and exits the GUI, much as before:

C:...PP4EGuiIntro> python gui7.py
Hello number 1
Hello number 2
Hello number 3
Hello number 4

Also as before, self.data retains state between events, and callbacks are routed to the self.message method within this class. Unlike before, the HelloPackage class is not itself a kind of Frame widget. In fact, it’s not a kind of anything—it serves only as a generator of namespaces for storing away real widget objects and state. Because of that, widgets are attached to a self.top (an embedded Frame), not to self. Moreover, all references to the object as a widget must descend to the embedded frame, as in the top.mainloop call to start the GUI at the end of the script.

This makes for a bit more coding within the class, but it avoids potential name clashes with both attributes added to self by the tkinter framework and existing tkinter widget methods. For instance, if you define a config method in your class, it will hide the config call exported by tkinter. With the standalone class package in this example, you get only the methods and instance attributes that your class defines.

In practice, tkinter doesn’t use very many names, so this is not generally a big concern.[31] It can happen, of course; but frankly, I’ve never seen a real tkinter name clash in widget subclasses in some 18 years of Python coding. Moreover, using standalone classes is not without other downsides. Although they can generally be attached and subclassed as before, they are not quite plug-and-play compatible with real widget objects. For instance, the configuration calls made in Example 7-21 for the Frame subclass fail in Example 7-25.

Example 7-25. PP4EGuiIntrogui7b.py
from tkinter import *
from gui7 import HelloPackage      # or get from gui7c--__getattr__ added

frm = Frame()
frm.pack()
Label(frm, text='hello').pack()

part = HelloPackage(frm)
part.pack(side=RIGHT)              # FAILS!--need part.top.pack(side=RIGHT)
frm.mainloop()

This won’t quite work, because part isn’t really a widget. To treat it as such, you must descend to part.top before making GUI configurations and hope that the name top is never changed by the class’s developer. In other words, it exposes some of the class’s internals. The class could make this better by defining a method that always routes unknown attribute fetches to the embedded Frame, as in Example 7-26.

Example 7-26. PP4EGuiIntrogui7c.py
import gui7
from tkinter import *

class HelloPackage(gui7.HelloPackage):
    def __getattr__(self, name):
        return getattr(self.top, name)                  # pass off to a real widget

if __name__ == '__main__': HelloPackage().mainloop()    # invokes __getattr__!

As is, this script simply creates Figure 7-23 again; changing Example 7-25 to import this extended HelloPackage from gui7c, though, produces the correctly-working window in Figure 7-24.

A standalone class package in action
Figure 7-24. A standalone class package in action

Routing attribute fetches to nested widgets works this way, but that then requires even more extra coding in standalone package classes. As usual, though, the significance of all these trade-offs varies per application.

The End of the Tutorial

In this chapter, we learned the core concepts of Python/tkinter programming and met a handful of simple widget objects along the way—e.g., labels, buttons, frames, and the packer geometry manager. We’ve seen enough to construct simple interfaces, but we have really only scratched the surface of the tkinter widget set.

In the next two chapters, we will apply what we’ve learned here to study the rest of the tkinter library, and we’ll learn how to use it to generate the kinds of interfaces you expect to see in realistic GUI programs. As a preview and roadmap, Table 7-1 lists the kinds of widgets we’ll meet there in roughly their order of appearance. Note that this table lists only widget classes; along the way, we will also meet a few additional widget-related topics that don’t appear in this table.

Table 7-1. tkinter widget classes

Widget class

Description

Label

A simple message area

Button

A simple labeled push-button widget

Frame

A container for attaching and arranging other widget objects

Toplevel, Tk

A new window managed by the window manager

Message

A multiline label

Entry

A simple single-line text-entry field

Checkbutton

A two-state button widget, typically used for multiple-choice selections

Radiobutton

A two-state button widget, typically used for single-choice selections

Scale

A slider widget with scalable positions

PhotoImage

An image object used for displaying full-color images on other widgets

BitmapImage

An image object used for displaying bitmap images on other widgets

Menu

A set of options associated with a Menubutton or top-level window

Menubutton

A button that opens a Menu of selectable options and submenus

Scrollbar

A control for scrolling other widgets (e.g., listbox, canvas, text)

Listbox

A list of selection names

Text

A multiline text browse/edit widget, with support for fonts, and so on

Canvas

A graphic drawing area, which supports lines, circles, photos, text, and so on

We’ve already met Label, Button, and Frame in this chapter’s tutorial. To make the remaining topics easier to absorb, they are split over the next two chapters: Chapter 8 covers the first widgets in this table up to but not including Menu, and Chapter 9 presents widgets that are lower in this table.

Besides the widget classes in this table, there are additional classes and tools in the tkinter library, many of which we’ll explore in the following two chapters as well:

Geometry management

pack, grid, place

tkinter linked variables

StringVar, IntVar, DoubleVar, BooleanVar

Advanced Tk widgets

Spinbox, LabelFrame, PanedWindow

Composite widgets

Dialog, ScrolledText, OptionMenu

Scheduled callbacks

Widget after, wait, and update methods

Other tools

Standard dialogs, clipboard, bind and Event, widget configuration options, custom and modal dialogs, animation techniques

Most tkinter widgets are familiar user interface devices. Some are remarkably rich in functionality. For instance, the Text class implements a sophisticated multiline text widget that supports fonts, colors, and special effects and is powerful enough to implement a web browser’s page display. The similarly feature-rich Canvas class provides extensive drawing tools powerful enough for visualization and other image-processing applications. Beyond this, tkinter extensions such as the Pmw, Tix, and ttk packages described at the start of this chapter add even richer widgets to a GUI programmer’s toolbox.

Python/tkinter for Tcl/Tk Converts

At the start of this chapter, I mentioned that tkinter is Python’s interface to the Tk GUI library, originally written for the Tcl language. To help readers migrating from Tcl to Python and to summarize some of the main topics we met in this chapter, this section contrasts Python’s Tk interface with Tcl’s. This mapping also helps make Tk references written for other languages more useful to Python developers.

In general terms, Tcl’s command-string view of the world differs widely from Python’s object-based approach to programming. In terms of Tk programming, though, the syntactic differences are fairly small. Here are some of the main distinctions in Python’s tkinter interface:

Creation

Widgets are created as class instance objects by calling a widget class.

Masters (parents)

Parents are previously created objects that are passed to widget-class constructors.

Widget options

Options are constructor or config keyword arguments or indexed keys.

Operations

Widget operations (actions) become tkinter widget class object methods.

Callbacks

Callback handlers are any callable objects: function, method, lambda, and so on.

Extension

Widgets are extended using Python class inheritance mechanisms.

Composition

Interfaces are constructed by attaching objects, not by concatenating names.

Linked variables (next chapter)

Variables associated with widgets are tkinter class objects with methods.

In Python, widget creation commands (e.g., button) are Python class names that start with an uppercase letter (e.g., Button), two-word widget operations (e.g., add command) become a single method name with an underscore (e.g., add_command), and the “configure” method can be abbreviated as “config,” as in Tcl. In Chapter 8, we will also see that tkinter “variables” associated with widgets take the form of class instance objects (e.g., StringVar, IntVar) with get and set methods, not simple Python or Tcl variable names. Table 7-2 shows some of the primary language mappings in more concrete terms.

Table 7-2. Tk-to-tkinter mappings

Operation

Tcl/Tk

Python/tkinter

Creation

Frame .panel

panel = Frame()

Masters

button .panel.quit

quit = Button(panel)

Options

button .panel.go -fg black

go = Button(panel, fg='black')

Configure

.panel.go config -bg red

go.config(bg='red') go['bg'] = ‘red’

Actions

.popup invoke

popup.invoke()

Packing

pack .panel -side left -fill x

panel.pack(side=LEFT, fill=X)

Some of these differences are more than just syntactic, of course. For instance, Python builds an internal widget object tree based on parent arguments passed to widget constructors, without ever requiring concatenated widget pathname strings. Once you’ve made a widget object, you can use it directly by object reference. Tcl coders can hide some dotted pathnames by manually storing them in variables, but that’s not quite the same as Python’s purely object-based model.

Once you’ve written a few Python/tkinter scripts, though, the coding distinctions in the Python object world will probably seem trivial. At the same time, Python’s support for object-oriented techniques adds an entirely new component to Tk development; you get the same widgets, plus Python’s support for code structure and reuse.



[23] The term “widget set” refers to the objects used to build familiar point-and-click user interface devices—push buttons, sliders, input fields, and so on. tkinter comes with Python classes that correspond to all the widgets you’re accustomed to seeing in graphical displays. Besides widgets, tkinter also comes with tools for other activities, such as scheduling events to occur, waiting for socket data to arrive, and so on.

[24] In this book, “Windows” refers to the Microsoft Windows interface common on PCs, and “X Windows” refers to the X11 interface most commonly found on Unix and Linux platforms. These two interfaces are generally tied to the Microsoft and Unix (and Unix-like) platforms, respectively. It’s possible to run X Windows on top of a Microsoft operating system and Windows emulators on Unix and Linux, but it’s not common. As if to muddy the waters further, Mac OS X supports Python’s tkinter on both X Windows and the native Aqua GUI system directly, in addition to platform-specific cocoa options (though it’s usually not too misleading to lump OS X in with the “Unix-like” crowd).

[25] Technically, the mainloop call returns to your script only after the tkinter event loop exits. This normally happens when the GUI’s main window is closed, but it may also occur in response to explicit quit method calls that terminate nested event loops but leave open the GUI at large. You’ll see why this matters in Chapter 8.

[26] Tip: As suggested earlier, when typing tkinter GUI code interactively, you may or may not need to call mainloop to display widgets. This is required in the current IDLE interface, but not from a simple interactive session running in a system console window. In either case, control will return to the interactive prompt when you kill the window you created. Note that if you create an explicit main-window widget by calling Tk() and attach widgets to it (described later), you must call this again after killing the window; otherwise, the application window will not exist.

[27] If you study the main tkinter file in the Python source library (currently, Lib kinter\__init__.py), you’ll notice that top-level module names not meant for export start with a single underscore. Python never copies over such names when a module is accessed with the * form of the from statement. The constants module is today constants.py in the same module package directory, though this can change (and has) over time.

[28] In fact, Python’s pass-by-name keyword arguments were first introduced to help clean up tkinter calls such as this one. Internally, keyword arguments really are passed as a dictionary (which can be collected with the **name argument form in a def header), so the two schemes are similar in implementation. But they vary widely in the number of characters you need to type and debug.

[29] Ex-Tcl programmers in the audience may be interested to know that, at least at the time I was writing this footnote, Python not only builds the widget tree internally, but uses it to automatically generate widget pathname strings coded manually in Tcl/Tk (e.g., .panel.row.cmd). Python uses the addresses of widget class objects to fill in the path components and records pathnames in the widget tree. A label attached to a container, for instance, might have an assigned name such as .8220096.8219408 inside tkinter. You don’t have to care, though. Simply make and link widget objects by passing parents, and let Python manage pathname details based on the object tree. See the end of this chapter for more on Tk/tkinter mappings.

[30] Technically, the packing steps are just rerun again after a window resize. But since this means that there won’t be enough space left for widgets packed last when the window shrinks, it is as if widgets packed first are clipped last.

[31] If you study the tkinter module’s source code (today, mostly in file __init__.py in Lib kinter), you’ll notice that many of the attribute names it creates start with a single underscore to make them unique from yours; others do not because they are potentially useful outside of the tkinter implementation (e.g., self.master, self.children). Curiously, at this writing most of tkinter still does not use the Python “pseudoprivate attributes” trick of prefixing attribute names with two leading underscores to automatically add the enclosing class’s name and thus localize them to the creating class. If tkinter is ever rewritten to employ this feature, name clashes will be much less likely in widget subclasses. Most of the attributes of widget classes, though, are methods intended for use in client scripts; the single underscore names are accessible too, but are less likely to clash with most names of your own.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset