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 easy 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. As we'll see, it's easy to program user interfaces in Python scripts, thanks both to 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. To make them easier to absorb, GUI programming topics are split over the next four chapters of this book:
· 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, OOP (object-oriented programming) isn't required for Tkinter, but it makes GUIs structured and reusable.
· Chapter 7 and Chapter 8 take you on a tour of the Tkinter widget set.[1] Roughly, Chapter 7, presents simple widgets, and Chapter 8, covers more advanced widgets and related tools. Most of the interfaces devices you're accustomed to seeing show up here: sliders, menus, dialogs, images, and their kin. These two chapters are not a fully complete Tkinter reference (which could easily fill a large book by itself), but 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 9, presents more complex examples that use coding and widget techniques presented in the three preceding chapters. It begins with 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 easy to use. This chapter wraps up by presenting a handful of complete GUI programs that implement text editors, image viewers, clocks, and more.
Because GUIs are really cross-domain tools, other GUI examples will also show up throughout the remainder of this book. For example, we'll later see email GUIs, calculators, tree viewers, table browsers, and so on. See the fourth GUI chapter for a list of forward pointers to other Tkinter examples in this text.
One 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 screen shots 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. For other systems, Pythons with Tkinter support are readily available as well (see Appendix B, and the top-level README-PP2E.txt file for more details). It's 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.
Has Anyone Noticed That "GUI" Are the First Three Letters of "GUIDO"?Python's creator didn't originally set out to build a GUI development tool, but Python's ease of use and rapid turnaround have made this one of its primary roles. From an implementation perspective, GUIs in Python are really just instances of C extensions, and extendability was one of the main ideas behind Python. When a script builds push-buttons and menus, it ultimately talks to a C library; and when a script responds to a user event, a C library ultimately talks back to Python. But from a practical point of view, GUIs are a critical part of modern systems, and an ideal domain for a tool like Python. As we'll see, Python's simple syntax and object-oriented flavor blend well with the GUI model -- it's natural to represent each device drawn on a screen as a Python class. Moreover, Python's quick turnaround lets programmers experiment with alternative layouts and behavior rapidly, in ways not possible with traditional development techniques. In fact, you can usually make a change to a Python-based GUI, and observe its effects in a matter of seconds. Don't try this with C or C++. |
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 in recent years. In fact, although Tkinter is the most widely used GUI toolkit in Python, there is a variety of ways to program user interfaces in Python today. Some are specific to Windows or X Windows,[2] some are cross-platform solutions, and all have followings and strong points all their own. To be fair to all the alternatives, here is a brief inventory of GUI toolkits available to Python programmers as I write these words:
Tkinter
(shipped with Python)
An open-source, portable GUI library, used as the de facto standard for GUI development in Python. Tkinter makes it easy to build simple GUIs quickly, and can be straightforwardly augmented with larger component frameworks in Python. Python scripts that use Tkinter to build GUIs run portably on Windows, X Windows (Unix), and Macintosh, and display a native look-and-feel on each of these. The underlying Tk library used by Tkinter is a standard in the open source world at large, and is also used by the Perl and Tcl scripting languages.
wxPython (http://wxpython.org)
An open-source Python interface for wxWindows -- a portable GUI class framework originally written to be used from the C++ programming language. The wxPython system is an extension module that wraps wxWindows 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. At this writing, wxPython code is portable to Windows and Unix-like platforms, but not the Macintosh. The underlying wxWindows library best supports Windows and GTK (on Unix), but it is generally portable to Windows, Unix-like platforms, and the Macintosh.
JPython (http://www.jython.org)
As we will see in Chapter 15, JPython (a.k.a. "Jython") is a Python port for Java, which 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 the portability of Java and its libraries. A new package named jTkinter also provides a Tkinter port to JPython using Java's JNI; if installed, Python scripts may also use Tkinter to build GUIs under JPython.
KDE and Qt (http://www.thekompany.com/projects/pykde)
Gnome and GTK
(ftp://ftp.daa.com.au/pub/james/python)
On Linux, developers can find Python interfaces to the underlying GUI libraries at the core of the KDE and Gnome window systems. The PyKDE and PyQt extension packages provide access to KDE development libraries (PyKDE requires PyQt). The gnome-python and PyGTK extension packages export Gnome and GTK toolkit calls for use in Python scripts (gnome-python requires PyGTK). Both of these sets of extensions are as portable as the underlying libraries they use. Today, the Qt class library runs on Unix and Windows, KDE runs on Linux and Unix platforms, and GTK and Gnome run on Linux and Unix platforms (though a Windows port of GTK is in the works). See relevant web sites for more recent details.
MFC (http://www.python.org/windows)
The Windows win32all.exe extensions package for Python, available at Python's web site and on this book's CD-ROM, includes wrappers for the Microsoft Foundation Classes (MFC) framework -- a development library that includes user interface components. With the Windows extensions, Python programs can construct Windows GUIs using the same MFC calls applied in languages such as Visual C++. Pythonwin, an MFC sample program that implements a Python development GUI, is included with the extensions package. This is a Windows-only solution, but may be an appealing option for developers with a prior intellectual investment in using the MFC framework from Visual C++.
WPY (http://www.python.org/ftp/python/wpy)
An MFC-like GUI library for Python, ported to run on both X Windows for Unix (where it uses Tk) and Windows for PCs (where it uses MFC). WPY scripts run unchanged on each platform, but use MFC coding styles.
X11 (http://www.cwi.nl/ftp/sjoerd/index.html)
Interfaces to the raw X Windows and Motif libraries also exist for Python. They provide maximum control over the X11 development environment, but are an X-only solution.
There are other lesser-known GUI toolkits for Python, and new ones are likely to emerge by the time you read this book (e.g., the newly announced Python port to the .NET framework on Windows may offer user interface options as well).[3] Moreover, package and web site names like those in this list mutate over time. For an up-to-date list of available tools, see http://www.python.org and the new "Vaults of Parnassus" third-party packages site currently at http://www.vex.net/parnassus.
Of all these 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. Tkinter's portability, availability, accessibility, documentation, and extensions have made it the most widely used Python GUI solution for many years running. Here are some of the reasons why:
Accessibility
Tkinter is generally regarded as a lightweight toolkit, and one of the simplest GUI solutions for Python available today. Unlike larger frameworks, it is easy to get started in Tkinter right away, without first having to grasp a much larger class interaction model. As we'll see, programmers can create simple Tkinter GUIs in a few lines of Python code, and scale up to writing industrial-strength GUIs gradually.
Portability
A Python script that builds a GUI with Tkinter will run without source code changes on all major window platforms today: Microsoft Windows, X Windows (on Unix and Linux), and the Macintosh. Further, that same script will provide a native look-and-feel to its users on each of these platforms. A Python/Tkinter script looks like a Windows program on Windows; on Unix and Linux, it provides the same interaction but sports an appearance familiar to X Windows users; and on the Mac, it looks like a Mac program should.
Availability
Tkinter is a standard module in the Python library, shipped with the interpreter. If you have Python, you have Tkinter. Moreover, most Python installation packages (including the standard Python self-installer for Windows) 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.[4] 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, it tends to receive more development time and effort 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 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. 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 (see the Python books list at http://www.python.org for details). Besides books, you can also now 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 6-2, the Tk to Tkinter conversion guide, at the end of this chapter for help reading Tk documentation). For instance, the 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.
Because Tkinter is so widely used, programmers also have access to precoded Python extensions designed to work with or augment it. For instance:
PIL (http://www.pythonware.com/products/pil/)
The Python Imaging Library is an open source extension package that adds image-processing tools to Python. Among other things, it extends the basic Tkinter image object set, to add support for displaying many image file types (see Section 7.9 at the end of Chapter 7 for details). Besides developing PIL, PythonWare is also building GUI development tools for Python and Tkinter programming, known as PythonWorks; visit their web site for more details.
PMW (http://www.dscpl.com.au/pmw)
Python Mega Widgets is an extension toolkit for building high-level compound widgets in Python using the Tkinter module. It extends the Tkinter API with a collection of more sophisticated widgets for advanced GUI development, and a framework for implementing some of your own. Among the precoded and extensible megawidgets shipped with the package are notebooks, comboboxes, selection widgets, paned widgets, scrolled widgets, dialog windows, button boxes, 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.
IDLE
(shipped with Python)
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). As described in Appendix B, IDLE provides syntax-coloring text editors for Python code, point-and-click debugging, and more, and is an example of Tkinter's utility.
If you plan on doing any commercial-grade GUI development with Tkinter, you'll probably want to explore extensions such as PMW and PIL after learning Tkinter basics in this text. They can save development time and add pizzazz to your GUIs. See the Python-related web sites 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 API in turn interfaces with the underlying window system: Microsoft Windows, X Windows on Unix, or Macintosh.
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., mouseclicks). 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 Part V, we'll know these transfers by their C integration terms, extending and embedding.[5]
Luckily, Python programmers don't normally need to care about all this 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 mouseclicks, 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 like 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 just 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 OO techniques such as inheritance and composition to customize and extend the base set of Tkinter classes. Larger Tkinter GUIs generally are 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 details. Let's start out by quickly stepping through a few small examples that illustrate basic concepts, and show the windows they create on the screen. The examples will become more sophisticated as we move along.
The usual first example for GUI systems is to show how to display a "Hello World" message in a window. As coded in Example 6-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 6-1 on Windows.
This isn't much to write home about yet; but notice that this is a completely functional, independent window on the computer's display. It can be maximized to take up the entire screen, minimized to hide it in the system bar, and resized. Click on the window's "X" box in the top right to kill the window and exit the program.
The script that builds this window is also fully portable -- when this same file is run on Linux it produces a similar window, but it behaves according to the underlying Linux window manager. For instance, Figures Figure 6-2 and Figure 6-3 show this simple script in action on the Linux X Windows system, under the KDE and Gnome window managers, respectively. Even on the same operating system, the same Python code yields a different look and feel for different window systems.
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:
1. Loads a widget class from the Tkinter module
2. Makes an instance of the imported Label class
3. Packs (arranges) the new Label in its parent widget
4. Calls mainloop to bring up the window and start the Tkinter event loop
The mainloop method called last puts the label on the screen and enters a Tkinter wait state, which watches for user-generated GUI events. Within the mainloop function, Tkinter internally monitors things like the keyboard and mouse, to detect user-generated events. Because of this model, the mainloop call here never returns to our script while the GUI is displayed on-screen.[6] As we'll see when we reach larger scripts, the only way we can get anything done after calling mainloop is to register callback handlers to respond to events.
Note that you really need both steps 3 and 4 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). Since the concepts illustrated by this simple script are at the core of most Tkinter programs, let's take a deeper look a 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 only need to set most options 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 side top.
We'll visit the packer repeatedly in this chapter, and use it in many of the examples in this book. In Chapter 8 we will also meet an alternative grid geometry manager and layout system that arranges widgets within a container in tabular form (i.e., by rows and columns). A third alternative, 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.
Like all Python code, the module in Example 6-1 can be started in a number of ways: by running it as a top-level program file:
C:\...\PP2E\Gui\Intro>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 that 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. It can even be run from a C program, by calling the appropriate embedding API function (see Chapter 20, for details).
In other words, there really are no special rules to follow when running GUI Python code. The Tkinter interface (and Tk itself) are 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.
Earlier in this book we learned that if a program's name ends in a .pyw extension instead of .py, 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 6-2 in a Windows explorer creates just the window in Figure 6-1.
...same as gui1.py...
You can also avoid the DOS popup 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 isn't necessary -- there is no notion of a streams popup 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 its names with the module's name, as in Example 6-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 6-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 only export things that we really need, so it's one of the few for which the * import form is relatively safe to apply.[7] 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 Tkconstants, 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 as the parent here, instead of None. Tk represents the main ("root") window of the program -- the one that starts when the program does. Tk 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 7, 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 too, and this lets us shave Example 6-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 Tk). But relying on that default is less useful once we start building larger displays -- things like labels are more typically attached to other widget containers.
Top-level windows, like the one all of the coding variants seen thus far build, can normally be resized by the user -- simply drag out the window with your mouse. Figure 6-4 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 6-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, widget are not expanded when their parent is. But in this script, names YES and BOTH (imported from the Tkinter module) specify that the label should grow along with its parent, the main window. It does, in Figure 6-5.
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 only can occupy its allocated space, and is no bigger than its assigned size. The expand and fill options let us be more specific about such things:
· The expand=YES option asks the packer to expand the allocated space for the widget in general, into any unclaimed space in the widget's parent.
· The fill option can be used to stretch the widget to occupy all of its allocated space.
Combinations of these two options produce different layout and resizing effects, some of which only become meaningful 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, 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.
This all 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, and stretch me to fill the expanded space in both directions." For our "Hello World" example, the net result is that 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 6-7, the text option of the label is set after it is constructed, by assigning to the widget's text key -- widget objects overload 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 6-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 6-6. In general terms, top-level windows like the Tk root here export window-manager interfaces: things that have to do with the border around the window, not its contents.
Just for fun, this version also centers the label on resizes by setting the expand and fill pack options too. 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 all spelled out, rather than defaulted.
Finally, if you are both a minimalist and nostalgic for old Python code, you can also program this "Hello World" example as in Example 6-9.
from Tkinter import *
Label(None, {'text': 'Hello GUI world!', Pack: {'side': 'top'}}).mainloop()
This makes the window in just two lines -- albeit gruesome ones! This scheme relies on an old coding style that was widely used until Python 1.3 that passed configuration options in a dictionary instead of keyword arguments.[8] 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 please don't do this -- use keywords to pass options, and 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 built-in apply function now also allows you to pass an explicit dictionary of keyword arguments in its third argument slot, so there's no compelling reason to ever use the pre-1.3 Tkinter dictionary call form at all.
In gui1c.py (shown in Example 6-4), I started packing labels without assigning them to names. This works, and 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 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 need 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() # okay
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 on to 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 really is retained, even if not by our code.[9]
In other words, your scripts don't generally need to care about widget object lifetimes, and it's okay to make widgets and pack them right away in the same statement. But that does not mean that it's okay 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 name widget, but it does not. 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') # okay 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 allows you to hold onto the widget for later processing. On the other hand, scripts that compose layouts often add widgets once and for all when they are created, and never need to reconfigure them later; assigning to long-lived names in such program is pointless and unnecessary.[10]
So far, we've learned how to display messages in labels, and 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. The program in Example 6-10 creates the window in Figure 6-7.
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 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 an earlier chapter, the built-in sys.exit function simply shuts down the calling program. Here, that means pressing this button makes the window go away.
Just as for labels, there are other ways to code buttons. Example 6-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 (really, it ends the current mainloop event loop call).
from Tkinter import *
root = Tk()
Button(root, text='press', command=root.quit).pack(side=LEFT)
root.mainloop()
This version produces the window in Figure 6-8. Because we didn't tell the button to expand into all available space, it does not.
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, and generate a window like Figure 6-9. This makes the packer allocate all available space to the button, but does not stretch the button to fill that space.
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, and create the scene in Figure 6-10.
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 6-11, 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 6-12. In fact, for a good time, 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 only expand 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. We will revisit the packer geometry manager when we meet multiple-widget displays later in this tutorial, and again when we study the alternative grid call in Chapter 8.
In the simple button examples in the previous 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 6-12 defines a callback handler of its own in Python.
from Tkinter import *
def quit(): # a custom callback handler
print 'Hello, I must be going...' # kill windows and process
import sys; 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 6-13. 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. Inside quit, the print 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 a raw_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:\...\PP2E\Gui\Intro>python gui3.py
Hello, I must be going...
C:\...\PP2E\Gui\Intro>
Normally, such messages would be displayed in another window, 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 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 a self, for bound-methods).
To make the last paragraph 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 with default argument values to specify the extra data needed. We'll see how this can be useful later in this part of the book, but to illustrate the basic idea, Example 6-13 shows what this example looks like when recoded to use a lambda instead of a def.
from Tkinter import *
from sys import stdout, exit # lambda generates a function
widget = Button(None, # but contains just an expression
text='Hello event world',
command=(lambda: stdout.write('Hello lambda world\n') or exit()) )
widget.pack()
widget.mainloop()
This code is a bit tricky because lambdas can only contain an expression; to emulate the original script, this version uses an or operator to force two expressions to be run, and writes to stdout to mimic a print. More typically, lambdas are used to pass along extra data to a callback handler using defaults:
def handler(X, Y): # would normallly be called with no args
use original X and Y here...
X = something here...
Y = something else
here...
Button(text='ni', command=(lambda save1=X, save2=Y: handler(save1, save2)) )
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. Since default arguments are evaluated and saved when the lambda runs (not when its result is later called), they are a way to remember objects that must be accessed again later, during event processing. We'll see this put to more concrete use later. If its syntax confuses you, remember that a lambda expression like the one in the preceding code can usually be coded as a simple nested def statement instead:
X =
something here...
Y =
something else here...
def func(save1=X, save2=Y): handler(save1, save2)
Button(text='ni', command=func)
Class bound methods work particularly well as callback handlers too: they record both an instance to send the event to, and an associated method to call. As a preview, Example 6-14 shows Example 6-12 rewritten to register a bound class method, instead of a function or lambda result.
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
import sys; 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, is 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 method like 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 too: simply assign it to self instance attributes:
class someGuiClass:
def __init__(self):
self.X = something here...
self.Y = something else here...
Button(text='Hi', command=self.handler )
def handler(self):
use self.X, self.Y here...
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.
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, as in Example 6-15.
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
import sys; 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 too -- Python invokes its __call__ method to handle the call operation made in Tkinter on the button press. Notice that 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 GUI window, but print different messages to stdout when their button is pressed:
C:\...\PP2E\Gui\Intro>python gui3.py
Hello, I must be going...
C:\...\PP2E\Gui\Intro>python gui3b.py
Hello lambda world
C:\...\PP2E\Gui\Intro>python gui3c.py
Hello class method world
C:\...\PP2E\Gui\Intro>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 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:
Button command options
As we've just seen, button press events are intercepted by providing a callable object in widget command options. This is true of other kinds of button-like widgets we'll meet in Chapter 7 (e.g., radio and check buttons, scales).
Menu command options
In the upcoming Tkinter tour chapters, we'll also find that a command option is used to specify callback handlers for menu selections.
Scrollbar protocols
Scrollbar 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 scrollbar automatically moves the widget, and vice versa.
General widget bind methods
A more general Tkinter event bind method mechanism can be used to register callback handlers for lower-level interface events -- key presses, mouse movement and clicks, and so on. Unlike command callbacks, bind callbacks receive an event object argument (an instance of the Tkinter Event class), that gives context about the event -- subject widget, screen coordinates, etc.
Window manager protocols
In addition, scripts can also intercept window manager events (e.g., window close requests) by tapping into the window manager protocol method mechanism available on top-level window objects: setting a handler for WM_DELETE_WINDOW, for instance, takes over window close buttons.
Scheduled event callbacks
Finally, Tkinter scripts can also register callback handlers to be called 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 8.
Of all these, 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 6-16 uses bind,not command, to catch button presses.
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.
import sys; 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 mouseclicks (<Button-1>) and double-left mouseclicks (<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 7.
When run, this script makes the same window again (see Figure 6-13). 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:\...\PP2E\Gui\Intro>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 like 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 larger than a single button, and other ways to use classes in GUI work.
It's time to start building user interfaces with more than one widget. Example 6-17 makes the window shown in Figure 6-14.
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:\...\PP2E\Gui\Intro>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 will be cut out of the display if it is made too small -- widgets packed later are cut out first. For example, Figure 6-15 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 packed is always the last to go away. For instance, if the label is packed last, Figure 6-16 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.
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 scrollbars normally pack them before the items they scroll (e.g., text, lists), so that the scrollbars 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, by specifying win in the first argument to the Label and Button constructors, they are attached to the Frame by Tkinter (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 more subtle 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, it also determines how their side settings play out in the generated display.
Here's how the packer's layout system works:
1. The packer starts out with an available space cavity that includes the entire parent container (e.g., the whole Frame or top-level window).
2. As each widget is packed on a side, that widget is given the entire requested side in the remaining space cavity, and the space cavity is shrunk.
3. Later pack requests are given an entire side of what is left, after earlier pack requests have shrunk the cavity.
4. After widgets are given cavity space, expand divides up 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 in Figure 6-17, even though you've only moved the label code one line down in the source file (contrast with Figure 6-14).
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.[11] In the original version of this example, the label spans the entire top side just because it is the first packed, not because of its side option.
Beyond all this, 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 makes the window in Figure 6-18:
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 these all grow along with their window, though, we also need to make the container frame expandable -- widgets only expand beyond their initial packer arrangement if all of their parents expand too:
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 fill that space both ways, it and all its attached children expand along with the window. Figure 6-19 shows how.
And as if that isn't flexible enough, 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 -- more space than needed to show the button, so it shows up in the middle of that side by default as in Figure 6-17 (i.e., anchored to the center). Setting the anchor to N moves it to the top of its side, as shown in Figure 6-20.
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, 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 pushbuttons, for example, might be easier laid out in a frame of its own than 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 6-18 builds the window in Figure 6-21.
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()
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 prints a message and exits when pressed. 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 is invoked, not a simple function.
The **config argument here is assigned unmatched keyword arguments; they're passed along to the Button constructor. 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? It allows widgets to be configured by subclassing, instead of passing in options. HelloButton is a true button; we pass in configuration options as usual when one is made. But we can also specify callback handlers by overriding the callback method in subclasses, as shown in Example 6-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()
Instead of exiting, this MyButton button prints to stdout and stays up when pressed. Here is its standard output after being pressed a few times:
C:\PP2ndEd\examples\PP2E\Gui\Intro>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. But the 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 OO techniques. The next example provides yet another way to arrange for specialization.
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 9. For now, Example 6-20 is a simple example to illustrate the concept. Script gui6.py produces the window in Figure 6-22.
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 = 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:\...\PP2E\Gui\Intro>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 Example 6-10, Example 6-11, and Example 6-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, because each class instance is a distinct namespace.
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 important, 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 6-21 creates the window shown in Figure 6-23.
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:\...\PP2E\Gui\Intro>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 6-22 is yet another specialized Frame itself, but attaches an instance of the original Hello class in a more OO fashion. When run as a top-level program, it creates a window identical to the one shown in Figure 6-23.
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 only attached 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. Imagine replacing the Hello call in this script with a call to attach an already-coded and fully debugged calculator object, and 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 6-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, then adds a second Extend button on the right, as shown in Figure 6-24.
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 the superclass):
C:\...\PP2E\Gui\Intro>python gui6d.py
hello 42
hello 42
hello 42
hello 42
But pressing the new Extend button on the right, 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 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 up-front 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 benefits previously mentioned by creating standalone classes not derived from Tkinter Frames or other widgets. For instance, the class in Example 6-24 generates the window shown in Figure 6-25.
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 = self.data + 1
print 'Hello number', self.data
if __name__ == '__main__': HelloPackage().top.mainloop()
When run, the Hye button here prints to stdout, and Bye closes and exits the GUI, much as before:
C:\...\PP2E\Gui\Intro>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 only serves 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 self. Moreover, all references to the object as widget must descend to the embedded frame -- as in the top.mainloop call to start the GUI.
This makes for a bit more coding within the class, but 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 only get 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.[12] It can happen, of course; but frankly, I've never seen a real Tkinter name clash in widget subclasses in some eight 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 6-21for the Frame subclass fail in Example 6-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 never changes. The class could make this better by defining a method that always routes unknown attribute fetches to the embedded Frame, as in Example 6-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().top.mainloop()
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 have learned the core concepts of Python/Tkinter programming, and met a handful of simple widget objects along the way -- labels, buttons, frames, and the packer geometry manager. We've seen enough to construct simple interfaces, but 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 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 6-1 lists the kinds of widgets we'll meet there, in roughly their order of appearance. Note that this table lists only widget classes; along the way, we will also meet a few additional widget-related topics that don't appear in this table.
Table 6-1. Tkinter Widget Classes |
|
Widget
Class |
Description |
Label |
A simple message area |
Button |
A simple labeled pushbutton widget |
Frame |
A container for attaching and arranging
other widget objects |
Toplevel, Tk |
A new window managed by the window manager |
Message |
A multiline label |
Entry |
A simple single-line text-entry field |
Checkbutton |
A two-state button widget, typically used
for multiple-choice selections |
Radiobutton |
A two-state button widget, typically used
for single-choice selections |
Scale |
A slider widget with scalable positions |
PhotoImage |
An image object used for displaying
full-color images on other widgets |
BitmapImage |
An image object used for displaying bitmap
images on other widgets |
Menu |
A set of options associated with a Menubutton or top-level window |
Menubutton |
A button that opens a Menu of selectable options and submenus |
Scrollbar |
A control for scrolling other widgets
(e.g., listbox, canvas, text) |
Listbox |
A list of selection names |
Text |
A multiline text browse/edit widget, with
support for fonts, etc. |
Canvas |
A graphic drawing area, which supports
lines, circles, photos, text, etc. |
We've already met the 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 7 covers the first widgets in this table up to but not including Menu, and Chapter 8 presents widgets lower in this table.
Besides the widget classes in this table, there are additional classes and tools in the Tkinter library, many of which we'll explore in the following two chapters as well:
Geometry management
pack, grid, place
Tkinter linked variables
StringVar, IntVar, DoubleVar, BooleanVar
Composite widgets
Dialog, ScrolledText, OptionMenu
Scheduled callbacks
Widget after, wait, and update methods
Other tools
Standard dialogs, clipboard, bind and Event, widget configuration options, custom and modal dialogs, animation techniques
Most Tkinter widgets are familiar user interface devices. Some are remarkably rich in functionality. For instance, the Text class implements a sophisticated multiline text widget that supports fonts, colors, and special effects, and is powerful enough to implement a web browser, and the Canvas class provides extensive drawing tools powerful enough for image-processing applications. Beyond this, Tkinter extensions such as PMW 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:
Creation
Widgets are created as class instance objects by calling a widget class.
Masters (parents)
Parents are previously created objects, passed to widget-class constructors.
Widget options
Options are constructor or config keyword arguments, or indexed keys.
Operations
Widget operations (actions) become Tkinter widget class object methods.
Callbacks
Callback handlers are any callable objects: function, method, lambda, etc.
Extension
Widgets are extended using Python class inheritance mechanisms.
Composition
Interfaces are constructed by attaching objects, not concatenating names.
Linked variables (next chapter)
Variables associated with widgets are Tkinter class objects with methods.
In Python, widget creation commands (e.g., button) are Python class names that start with an uppercase letter (e.g., Button), two-word widget operations (e.g., add command) become a single method name with an underscore (e.g., add_command), and the "configure" method can be abbreviated as "config" as in Tcl. In Chapter 7, 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 6-2 shows some of the primary language mappings in more concrete terms.
Table 6-2. Tk to Tkinter Mappings |
||
Operation |
Tcl/Tk |
Python/Tkinter |
Creation |
frame .panel |
panel = Frame() |
Masters |
button .panel.quit |
quit = Button(panel) |
Options |
button .panel.go -fg black |
go = Button(panel, fg='black') |
Configure |
.panel.go config -bg red |
go.config(bg='red') go['bg'] = 'red' |
Actions |
.popup invoke |
popup.invoke() |
Packing |
pack .panel -side left -fill x |
panel.pack(side=LEFT, fill=X) |
Some of these differences are more than just syntactic, of course. For instance, Python builds an internal widget object tree based on parent arguments passed to widget constructors, without ever requiring concatenated widget pathname strings. Once you've made a widget object, you can use it directly by 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 OO techniques adds an entirely new component to Tk development; you get the same widgets, plus Python's support for code structure and reuse.
[1] 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. [back]
[2] 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 platforms,
respectively. It's possible to run X Windows on top of a Microsoft operating
system and Windows emulators on Unix and Linux, but not common. [back]
[3] In Part III, we'll learn how to build user interfaces
within a web browser. For now, we'll focus on more traditional GUIs that may or
may not be connected to a network. [back]
[4] Some Python distributions on Unix-like platforms still
come without Tk support bundled, so you may need to add it on your machine. On
some Unix and Linux platforms, you may also need to set your Tcl/Tk library
shell variables to use Tkinter. See Appendix B for install details; you can
usually sidestep Tk build details by finding an alternative Python distribution
with Tk bundled (e.g., Linux RPMs). [back]
[5] Since I brought it up: Tkinter is structured as a
combination of the Python-coded Tkinter
module file, and an extension module called _tkinter that is written in C. _tkinter
interfaces with the Tk library and dispatches callbacks back to Python objects
using embedding tools; Tkinter adds a
class-based interface on top of _tkinter.
You should always import Tkinter (not _tkinter) in your scripts, though; the latter is an
implementation module for internal use only (it was oddly named for a reason). [back]
[6] 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 may also occur
in response to explicit quit method calls that
terminate nested event loops but leave the GUI at large open. You'll see why
this matters in Chapter 7. [back]
[7] If you study file Tkinter.py in the Python
source library, 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. [back]
[8] In fact, Python's pass-by-name keyword arguments were
first introduced to help clean up Tkinter calls like this. 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 amount of characters you need to type and debug. [back]
[9] Ex-Tcl programmers in the audience may be interested to
know that 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 path names in the widget tree. A label
attached to a container, for instance, might have an assigned name like .8220096.8219408 inside Tkinter. You don't need 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. [back]
[10] In Chapter 7, 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 temporarily unset an
associated Tk variable if reclaimed, but this is uncommon and less harmful. [back]
[11] 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 works the same as
saying that widgets packed first are clipped last. [back]
[12] If you study the Tkinter.py module's source
code, you'll notice that many of the attribute names it creates start with a
single underscore to make them unique; others do not because they are
potentially useful outside of the Tkinter implementation (e.g., self.master, self.children). Oddly,
most of Tkinter still does not use the new Python "pseudo-private
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 common in widget subclasses. [back]