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.
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.
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.
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:
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.
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).
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).
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 (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.
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).
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.
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).
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:
An open source framework from Adobe and part of the Flash platform
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
A Java platform for building RIAs which can run across a variety of connected devices
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.
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.
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.
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:
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.
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.
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.
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.
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:
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 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).
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.
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.
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.
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.
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.
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).
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.
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.
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.
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).
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.
The gui1
script is a
trivial example, but it illustrates steps common to
most tkinter programs. This Python code does the following:
Loads a widget class from the tkinter
module
Makes an instance of the imported Label
class
Packs (arranges) the new Label
in its parent widget
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.
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
.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
optionAsks the packer to expand the allocated space for the widget in general into any unclaimed space in the widget’s parent.
fill
optionCan 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
import sys from tkinter import * widget = Button(None, text='Hello widget world', command=sys.exit) widget.pack() widget.mainloop()
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.
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.
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
.
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.
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.
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.
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.
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.
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.
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.
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.
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
.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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).
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 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.
bind
methodsA 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.
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.
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.
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.
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.
It’s time to start building user interfaces with more than one widget. Example 7-17 makes the window shown in Figure 7-12.
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()
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.
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.
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)
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.
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.
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:
The packer starts out with an available space cavity that
includes the entire parent container (e.g., the whole Frame
or top-level window).
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.
Later pack requests are given an entire side of what is left, after earlier pack requests have shrunk the cavity.
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).
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.
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
)
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.
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.
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.
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.
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()
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).
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.
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.
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.
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.
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.
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()
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.
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.
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()
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.
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.
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.
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.
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.
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.
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()
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.
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.
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.
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.
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.
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:
pack
, grid
, place
StringVar
, IntVar
, DoubleVar
, BooleanVar
Spinbox
, LabelFrame
, PanedWindow
Dialog
, ScrolledText
, OptionMenu
Widget after
, wait
, and update
methods
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.
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:
Widgets are created as class instance objects by calling a widget class.
Parents are previously created objects that are passed to widget-class constructors.
Options are constructor or config
keyword arguments or indexed
keys.
Widget operations (actions) become tkinter widget class object methods.
Callback handlers are any callable objects: function, method, lambda, and so on.
Widgets are extended using Python class inheritance mechanisms.
Interfaces are constructed by attaching objects, not by concatenating names.
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.
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.