8.6. Tkinter Coding Alternatives
As you might expect, there are a variety of ways to code the gui1 example. For instance, if you want to make all your Tkinter imports more explicit in your script, grab the whole module and prefix all of its names with the module's name, as in Example 8-3.
Example 8-3. PP3E\Gui\Intro\gui1b.pyimport versus from
That will probably get tedious in realistic examples, thoughTkinter 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 8-4.
Example 8-4. PP3E\Gui\Intro\gui1c.pyroots, sides, pack in place
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.[*] 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 toTOP, 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 programthe 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 9, 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 8-5 to just three lines of code.
Example 8-5. PP3E\Gui\Intro\gui1d.pya minimal version
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 such as labels are more typically attached to other widget containers.
8.6.1. Widget Resizing Basics
Top-level windows, such as the one built by all of the coding variants we have seen thus far, can normally be resized by the user; simply drag out the window with your mouse. Figure 8-4 shows how our window looks when it is expanded.
Figure 8-4. Expanding gui1
This isn't very goodthe label stays attached to the top of the parent window instead of staying in the middle on expansionbut it's easy to improve on this with a pair of pack options, demonstrated in Example 8-6.
Example 8-6. PP3E\Gui\Intro\gui1e.pyexpansion
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 8-5.
Figure 8-5. gui1e with widget resizing
Technically, the packer geometry manager assigns a size to each widget in a display based on what it contains (text string lengths, etc.). By default, a widget can occupy only its allocated space and is no bigger than its assigned size. The expand and fill options let us be more specific about such things:
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, 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.
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 outthis 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 the label grows as the window is expanded, and so is always centered.
8.6.2. Configuring Widget Options and Window Titles
So far, we've been telling Tkinter what to display on our label by passing its text as a keyword argument in label constructor calls. It turns out that there are two other ways to specify widget configuration options. In Example 8-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.
Example 8-7. PP3E\Gui\Intro\guif.pyoption keys
More commonly, widget options can be set after construction by calling the widget config method, as in Example 8-8.
Example 8-8. PP3E\Gui\Intro\gui1g.pyconfig and titles
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 8-6. In general terms, top-level windows like the Tk root here export window-manager interfacesi.e., things that have to do with the border around the window, not its contents.
Figure 8-6. gui1g with expansion and a window title
Just for fun, this version also centers the label upon resizes by setting the expand and fill pack options. In fact, this version makes just about everything explicit, and is more representative of how labels are often coded in full-blown interfaces; their parents, expansion policies, and attachments are usually spelled out rather than defaulted.
8.6.3. One More for Old Times' Sake
Finally, if you are a minimalist and you're nostalgic for old Python code, you can also program this "Hello World" example as in Example 8-9.
Example 8-9. PP3E\Gui\Intro\gui1-old.pydictionary calls
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.[*] 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 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 built-in apply function (and newer func (*pargs, **kargs) syntax) 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.
8.6.4. Packing Widgets Without Saving Them
In gui1c.py (shown in Example 8-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.[*]
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. 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. 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 programs is pointless and unnecessary.[*]