Chapter 8. A tkinter Tour, Part 1

“Widgets and Gadgets and GUIs, Oh My!”

This chapter is a continuation of our look at GUI programming in Python. The previous chapter used simple widgets—buttons, labels, and the like—to demonstrate the fundamentals of Python/tkinter coding. That was simple by design: it’s easier to grasp the big GUI picture if widget interface details don’t get in the way. But now that we’ve seen the basics, this chapter and the next move on to present a tour of more advanced widget objects and tools available in the tkinter library.

As we’ll find, this is where GUI scripting starts getting both practical and fun. In these two chapters, we’ll meet classes that build the interface devices you expect to see in real programs—e.g., sliders, check buttons, menus, scrolled lists, dialogs, graphics, and so on. After these chapters, the last GUI chapter moves on to present larger GUIs that utilize the coding techniques and the interfaces shown in all prior GUI chapters. In these two chapters, though, examples are small and self-contained so that we can focus on widget details.

This Chapter’s Topics

Technically, we’ve already used a handful of simple widgets in Chapter 7. So far we’ve met Label, Button, Frame, and Tk, and studied pack geometry management concepts along the way. Although all of these are basic, they represent tkinter interfaces in general and can be workhorses in typical GUIs. Frame containers, for instance, are the basis of hierarchical display layout.

In this and the following chapter, we’ll explore additional options for widgets we’ve already seen and move beyond the basics to cover the rest of the tkinter widget set. Here are some of the widgets and topics we’ll explore in this chapter:

  • Toplevel and Tk widgets

  • Message and Entry widgets

  • Checkbutton, Radiobutton, and Scale widgets

  • Images: PhotoImage and BitmapImage objects

  • Widget and window configuration options

  • Dialogs, both standard and custom

  • Low-level event binding

  • tkinter linked variable objects

  • Using the Python Imaging Library (PIL) extension for other image types and operations

After this chapter, Chapter 9 concludes the two-part tour by presenting the remainder of the tkinter library’s tool set: menus, text, canvases, animation, and more.

To make this tour interesting, I’ll also introduce a few notions of component reuse along the way. For instance, some later examples will be built using components written for prior examples. Although these two tour chapters introduce widget interfaces, this book is also about Python programming in general; as we’ll see, tkinter programming in Python can be much more than simply drawing circles and arrows.

Configuring Widget Appearance

So far, all the buttons and labels in examples have been rendered with a default look-and-feel that is standard for the underlying platform. With my machine’s color scheme, that usually means that they’re gray on Windows. tkinter widgets can be made to look arbitrarily different, though, using a handful of widget and packer options.

Because I generally can’t resist the temptation to customize widgets in examples, I want to cover this topic early on the tour. Example 8-1 introduces some of the configuration options available in tkinter.

Example 8-1. PP4EGuiTourconfig-label.py
from tkinter import *
root = Tk()
labelfont = ('times', 20, 'bold')                  # family, size, style
widget = Label(root, text='Hello config world')
widget.config(bg='black', fg='yellow')             # yellow text on black label
widget.config(font=labelfont)                      # use a larger font
widget.config(height=3, width=20)                  # initial size: lines,chars
widget.pack(expand=YES, fill=BOTH)
root.mainloop()

Remember, we can call a widget’s config method to reset its options at any time, instead of passing all of them to the object’s constructor. Here, we use it to set options that produce the window in Figure 8-1.

A custom label appearance
Figure 8-1. A custom label appearance

This may not be completely obvious unless you run this script on a real computer (alas, I can’t show it in color here), but the label’s text shows up in yellow on a black background, and with a font that’s very different from what we’ve seen so far. In fact, this script customizes the label in a number of ways:

Color

By setting the bg option of the label widget here, its background is displayed in black; the fg option similarly changes the foreground (text) color of the widget to yellow. These color options work on most tkinter widgets and accept either a simple color name (e.g., 'blue') or a hexadecimal string. Most of the color names you are familiar with are supported (unless you happen to work for Crayola). You can also pass a hexadecimal color identifier string to these options to be more specific; they start with a # and name a color by its red, green, and blue saturations, with an equal number of bits in the string for each. For instance, '#ff0000' specifies eight bits per color and defines pure red; “f” means four “1” bits in hexadecimal. We’ll come back to this hex form when we meet the color selection dialog later in this chapter.

Size

The label is given a preset size in lines high and characters wide by setting its height and width attributes. You can use this setting to make the widget larger than the tkinter geometry manager would by default.

Font

This script specifies a custom font for the label’s text by setting the label’s font attribute to a three-item tuple giving the font family, size, and style (here: Times, 20-point, and bold). Font style can be normal, bold, roman, italic, underline, overstrike, or combinations of these (e.g., “bold italic”). tkinter guarantees that Times, Courier, and Helvetica font family names exist on all platforms, but others may work, too (e.g., system gives the system font on Windows). Font settings like this work on all widgets with text, such as labels, buttons, entry fields, listboxes, and Text (the latter of which can even display more than one font at once with “tags”). The font option still accepts older X-Windows-style font indicators—long strings with dashes and stars—but the newer tuple font indicator form is more platform independent.

Layout and expansion

Finally, the label is made generally expandable and stretched by setting the pack expand and fill options we met in the last chapter; the label grows as the window does. If you maximize this window, its black background fills the whole screen and the yellow message is centered in the middle; try it.

In this script, the net effect of all these settings is that this label looks radically different from the ones we’ve been making so far. It no longer follows the Windows standard look-and-feel, but such conformance isn’t always important. For reference, tkinter provides additional ways to customize appearance that are not used by this script, but which may appear in others:

Border and relief

A bd= N widget option can be used to set border width, and a relief= S option can specify a border style; S can be FLAT, SUNKEN, RAISED, GROOVE, SOLID, or RIDGE—all constants exported by the tkinter module.

Cursor

A cursor option can be given to change the appearance of the mouse pointer when it moves over the widget. For instance, cursor='gumby' changes the pointer to a Gumby figure (the green kind). Other common cursor names used in this book include watch, pencil, cross, and hand2.

State

Some widgets also support the notion of a state, which impacts their appearance. For example, a state=DISABLED option will generally stipple (gray out) a widget on screen and make it unresponsive; NORMAL does not. Some widgets support a READONLY state as well, which displays normally but is unresponsive to changes.

Padding

Extra space can be added around many widgets (e.g., buttons, labels, and text) with the padx= N and pady= N options. Interestingly, you can set these options both in pack calls (where it adds empty space around the widget in general) and in a widget object itself (where it makes the widget larger).

To illustrate some of these extra settings, Example 8-2 configures the custom button captured in Figure 8-2 and changes the mouse pointer when it is positioned above it.

Config button at work
Figure 8-2. Config button at work
Example 8-2. PP4EGuiTourconfig-button.py
from tkinter import *
widget = Button(text='Spam', padx=10, pady=10)
widget.pack(padx=20, pady=20)
widget.config(cursor='gumby')
widget.config(bd=8, relief=RAISED)
widget.config(bg='dark green', fg='white')
widget.config(font=('helvetica', 20, 'underline italic'))
mainloop()

To see the effects generated by these two scripts’ settings, try out a few changes on your computer. Most widgets can be given a custom appearance in the same way, and we’ll see such options used repeatedly in this text. We’ll also meet operational configurations, such as focus (for focusing input) and others. In fact, widgets can have dozens of options; most have reasonable defaults that produce a native look-and-feel on each windowing platform, and this is one reason for tkinter’s simplicity. But tkinter lets you build more custom displays when you want to.

Note

For more on ways to apply configuration options to provide common look-and-feel for your widgets, refer back to Customizing Widgets with Classes, especially its ThemedButton examples. Now that you know more about configuration, its examples’ source code should more readily show how configurations applied in widget subclasses are automatically inherited by all instances and subclasses. The new ttk extension described in Chapter 7 also provides additional ways to configure widgets with its notion of themes; see the preceding chapter for more details and resources on ttk.

Top-Level Windows

tkinter GUIs always have an application root window, whether you get it by default or create it explicitly by calling the Tk object constructor. This main root window is the one that opens when your program runs, and it is where you generally pack your most important and long-lived widgets. In addition, tkinter scripts can create any number of independent windows, generated and popped up on demand, by creating Toplevel widget objects.

Each Toplevel object created produces a new window on the display and automatically adds it to the program’s GUI event-loop processing stream (you don’t need to call the mainloop method of new windows to activate them). Example 8-3 builds a root and two pop-up windows.

Example 8-3. PP4EGuiTour oplevel0.py
import sys
from tkinter import Toplevel, Button, Label

win1 = Toplevel()                  # two independent windows
win2 = Toplevel()                  # but part of same process

Button(win1, text='Spam', command=sys.exit).pack()
Button(win2, text='SPAM', command=sys.exit).pack()

Label(text='Popups').pack()        # on default Tk() root window
win1.mainloop()

The toplevel0 script gets a root window by default (that’s what the Label is attached to, since it doesn’t specify a real parent), but it also creates two standalone Toplevel windows that appear and function independently of the root window, as seen in Figure 8-3.

Two Toplevel windows and a root window
Figure 8-3. Two Toplevel windows and a root window

The two Toplevel windows on the right are full-fledged windows; they can be independently iconified, maximized, and so on. Toplevels are typically used to implement multiple-window displays and pop-up modal and nonmodal dialogs (more on dialogs in the next section). They stay up until they are explicitly destroyed or until the application that created them exits.

In fact, as coded here, pressing the X in the upper right corner of either of the Toplevel windows kills that window only. On the other hand, the entire program and all it remaining windows are closed if you press either of the created buttons or the main window’s X (more on shutdown protocols in a moment).

It’s important to know that although Toplevels are independently active windows, they are not separate processes; if your program exits, all of its windows are erased, including all Toplevel windows it may have created. We’ll learn how to work around this rule later by launching independent GUI programs.

Toplevel and Tk Widgets

A Toplevel is roughly like a Frame that is split off into its own window and has additional methods that allow you to deal with top-level window properties. The Tk widget is roughly like a Toplevel, but it is used to represent the application root window. Toplevel windows have parents, but Tk windows do not—they are the true roots of the widget hierarchies we build when making tkinter GUIs.

We got a Tk root for free in Example 8-3 because the Label had a default parent, designated by not having a widget in the first argument of its constructor call:

Label(text='Popups').pack()              # on default Tk() root window

Passing None to a widget constructor’s first argument (or to its master keyword argument) has the same default-parent effect. In other scripts, we’ve made the Tk root more explicit by creating it directly, like this:

root = Tk()
Label(root, text='Popups').pack()        # on explicit Tk() root window
root.mainloop()

In fact, because tkinter GUIs are a hierarchy, by default you always get at least one Tk root window, whether it is named explicitly, as here, or not. Though not typical, there may be more than one Tk root if you make them manually, and a program ends if all its Tk windows are closed. The first Tk top-level window created—whether explicitly by your code, or automatically by Python when needed—is used as the default parent window of widgets and other windows if no parent is provided.

You should generally use the Tk root window to display top-level information of some sort. If you don’t attach widgets to the root, it may show up as an odd empty window when you run your script (often because you used the default parent unintentionally in your code by omitting a widget’s parent and didn’t pack widgets attached to it). Technically, you can suppress the default root creation logic and make multiple root windows with the Tk widget, as in Example 8-4.

Example 8-4. PP4EGuiTour oplevel1.py
import tkinter
from tkinter import Tk, Button
tkinter.NoDefaultRoot()

win1 = Tk()         # two independent root windows
win2 = Tk()

Button(win1, text='Spam', command=win1.destroy).pack()
Button(win2, text='SPAM', command=win2.destroy).pack()
win1.mainloop()

When run, this script displays the two pop-up windows of the screenshot in Figure 8-3 only (there is no third root window). But it’s more common to use the Tk root as a main window and create Toplevel widgets for an application’s pop-up windows. Notice how this GUI’s windows use a window’s destroy method to close just one window, instead of sys.exit to shut down the entire program; to see how this method really does its work, let’s move on to window protocols.

Top-Level Window Protocols

Both Tk and Toplevel widgets export extra methods and features tailored for their top-level role, as illustrated in Example 8-5.

Example 8-5. PP4EGuiTour oplevel2.py
"""
pop up three new windows, with style
destroy() kills one window, quit() kills all windows and app (ends mainloop);
top-level windows have title, icon, iconify/deiconify and protocol for wm events;
there always is an application root window, whether by default or created as an
explicit Tk() object; all top-level windows are containers, but they are never
packed/gridded; Toplevel is like Frame, but a new window, and can have a menu;
"""

from tkinter import *
root = Tk()                                                     # explicit root

trees = [('The Larch!',         'light blue'),
         ('The Pine!',          'light green'),
         ('The Giant Redwood!', 'red')]

for (tree, color) in trees:
    win = Toplevel(root)                                        # new window
    win.title('Sing...')                                        # set border
    win.protocol('WM_DELETE_WINDOW', lambda:None)               # ignore close
    win.iconbitmap('py-blue-trans-out.ico')                     # not red Tk

    msg = Button(win, text=tree, command=win.destroy)           # kills one win
    msg.pack(expand=YES, fill=BOTH)
    msg.config(padx=10, pady=10, bd=10, relief=RAISED)
    msg.config(bg='black', fg=color, font=('times', 30, 'bold italic'))

root.title('Lumberjack demo')
Label(root, text='Main window', width=30).pack()
Button(root, text='Quit All', command=root.quit).pack()         # kills all app
root.mainloop()

This program adds widgets to the Tk root window, immediately pops up three Toplevel windows with attached buttons, and uses special top-level protocols. When run, it generates the scene captured in living black-and-white in Figure 8-4 (the buttons’ text shows up blue, green, and red on a color display).

Three Toplevel windows with configurations
Figure 8-4. Three Toplevel windows with configurations

There are a few operational details worth noticing here, all of which are more obvious if you run this script on your machine:

Intercepting closes: protocol

Because the window manager close event has been intercepted by this script using the top-level widget protocol method, pressing the X in the top-right corner doesn’t do anything in the three Toplevel pop ups. The name string WM_DELETE_WINDOW identifies the close operation. You can use this interface to disallow closes apart from the widgets your script creates. The function created by this script’s lambda:None does nothing but return None.

Killing one window (and its children): destroy

Pressing the big black buttons in any one of the three pop ups kills that pop up only, because the pop up runs the widget destroy method. The other windows live on, much as you would expect of a pop-up dialog window. Technically, this call destroys the subject widget and any other widgets for which it is a parent. For windows, this includes all their content. For simpler widgets, the widget is erased.

Because Toplevel windows have parents, too, their relationships might matter on a destroy—destroying a window, even the automatic or first-made Tk root which is used as the default parent, also destroys all its child windows. Since Tk root windows have no parents, they are unaffected by destroys of other windows. Moreover, destroying the last Tk root window remaining (or the only Tk root created) effectively ends the program. Toplevel windows, however, are always destroyed with their parents, and their destruction doesn’t impact other windows to which they are not ancestors. This makes them ideal for pop-up dialogs. Technically, a Toplevel can be a child of any type of widget and will be destroyed with it, though they are usually children of an automatic or explicit Tk.

Killing all windows: quit

To kill all the windows at once and end the GUI application (really, its active mainloop call), the root window’s button runs the quit method instead. That is, pressing the root window’s button ends the program. In general, the quit method immediately ends the entire application and closes all its windows. It can be called through any tkinter widget, not just through the top-level window; it’s also available on frames, buttons, and so on. See the discussion of the bind method and its <Destroy> events later in this chapter for more on quit and destroy.

Window titles: title

As introduced in Chapter 7, top-level window widgets (Tk and Toplevel) have a title method that lets you change the text displayed on the top border. Here, the window title text is set to the string 'Sing...' in the pop-ups to override the default 'tk'.

Window icons: iconbitmap

The iconbitmap method changes a top-level window’s icon. It accepts an icon or bitmap file and uses it for the window’s icon graphic when it is both minimized and open. On Windows, pass in the name of a .ico file (this example uses one in the current directory); it will replace the default red “Tk” icon that normally appears in the upper-lefthand corner of the window as well as in the Windows taskbar. On other platforms, you may need to use other icon file conventions if the icon calls in this book won’t work for you (or simply comment-out the calls altogether if they cause scripts to fail); icons tend to be a platform-specific feature that is dependent upon the underlying window manager.

Geometry management

Top-level windows are containers for other widgets, much like a standalone Frame. Unlike frames, though, top-level window widgets are never themselves packed (or gridded, or placed). To embed widgets, this script passes its windows as parent arguments to label and button constructors.

It is also possible to fetch the maximum window size (the physical screen display size, as a [width, height] tuple) with the maxsize() method, as well as set the initial size of a window with the top-level geometry(" width x height + x + y ") method. It is generally easier and more user-friendly to let tkinter (or your users) work out window size for you, but display size may be used for tasks such as scaling images (see the discussion on PyPhoto in Chapter 11 for an example).

In addition, top-level window widgets support other kinds of protocols that we will utilize later on in this tour:

State

The iconify and withdraw top-level window object methods allow scripts to hide and erase a window on the fly; deiconify redraws a hidden or erased window. The state method queries or changes a window’s state; valid states passed in or returned include iconic, withdrawn, zoomed (full screen on Windows: use geometry elsewhere), and normal (large enough for window content). The methods lift and lower raise and lower a window with respect to its siblings (lift is the Tk raise command, but avoids a Python reserved word). See the alarm scripts near the end of Chapter 9 for usage.

Menus

Each top-level window can have its own window menus too; both the Tk and the Toplevel widgets have a menu option used to associate a horizontal menu bar of pull-down option lists. This menu bar looks as it should on each platform on which your scripts are run. We’ll explore menus early in Chapter 9.

Most top-level window-manager-related methods can also be named with a “wm_” at the front; for instance, state and protocol can also be called wm_state and wm_protocol.

Notice that the script in Example 8-3 passes its Toplevel constructor calls an explicit parent widget—the Tk root window (that is, Toplevel(root)). Toplevels can be associated with a parent just as other widgets can, even though they are not visually embedded in their parents. I coded the script this way to avoid what seems like an odd feature; if coded instead like this:

win = Toplevel()                                    # new window

and if no Tk root yet exists, this call actually generates a default Tk root window to serve as the Toplevel’s parent, just like any other widget call without a parent argument. The problem is that this makes the position of the following line crucial:

root = Tk()                                         # explicit root

If this line shows up above the Toplevel calls, it creates the single root window as expected. But if you move this line below the Toplevel calls, tkinter creates a default Tk root window that is different from the one created by the script’s explicit Tk call. You wind up with two Tk roots just as in Example 8-4. Move the Tk call below the Toplevel calls and rerun it to see what I mean. You’ll get a fourth window that is completely empty! As a rule of thumb, to avoid such oddities, make your Tk root windows early on and make them explicit.

All of the top-level protocol interfaces are available only on top-level window widgets, but you can often access them by going through other widgets’ master attributes—links to the widget parents. For example, to set the title of a window in which a frame is contained, say something like this:

theframe.master.title('Spam demo')    # master is the container window

Naturally, you should do so only if you’re sure that the frame will be used in only one kind of window. General-purpose attachable components coded as classes, for instance, should leave window property settings to their client applications.

Top-level widgets have additional tools, some of which we may not meet in this book. For instance, under Unix window managers, you can also set the name used on the window’s icon (iconname). Because some icon options may be useful when scripts run on Unix only, see other Tk and tkinter resources for more details on this topic. For now, the next scheduled stop on this tour explores one of the more common uses of top-level windows.

Dialogs

Dialogs are windows popped up by a script to provide or request additional information. They come in two flavors, modal and nonmodal:

Modal

These dialogs block the rest of the interface until the dialog window is dismissed; users must reply to the dialog before the program continues.

Nonmodal

These dialogs can remain on-screen indefinitely without interfering with other windows in the interface; they can usually accept inputs at any time.

Regardless of their modality, dialogs are generally implemented with the Toplevel window object we met in the prior section, whether you make the Toplevel or not. There are essentially three ways to present pop-up dialogs to users with tkinter—by using common dialog calls, by using the now-dated Dialog object, and by creating custom dialog windows with Toplevels and other kinds of widgets. Let’s explore the basics of all three schemes.

Standard (Common) Dialogs

Because standard dialog calls are simpler, let’s start here first. tkinter comes with a collection of precoded dialog windows that implement many of the most common pop ups programs generate—file selection dialogs, error and warning pop ups, and question and answer prompts. They are called standard dialogs (and sometimes common dialogs) because they are part of the tkinter library, and they use platform-specific library calls to look like they should on each platform. A tkinter file open dialog, for instance, looks like any other on Windows.

All standard dialog calls are modal (they don’t return until the dialog box is dismissed by the user), and they block the program’s main window while they are displayed. Scripts can customize these dialogs’ windows by passing message text, titles, and the like. Since they are so simple to use, let’s jump right into Example 8-6 (coded as a .pyw file here to avoid a shell pop up when clicked in Windows).

Example 8-6. PP4EGuiTourdlg1.pyw
from tkinter import *
from tkinter.messagebox import *

def callback():
    if askyesno('Verify', 'Do you really want to quit?'):
        showwarning('Yes', 'Quit not yet implemented')
    else:
        showinfo('No', 'Quit has been cancelled')

errmsg = 'Sorry, no Spam allowed!'
Button(text='Quit', command=callback).pack(fill=X)
Button(text='Spam', command=(lambda: showerror('Spam', errmsg))).pack(fill=X)
mainloop()

A lambda anonymous function is used here to wrap the call to showerror so that it is passed two hardcoded arguments (remember, button-press callbacks get no arguments from tkinter itself). When run, this script creates the main window in Figure 8-5.

dlg1 main window: buttons to trigger pop ups
Figure 8-5. dlg1 main window: buttons to trigger pop ups

When you press this window’s Quit button, the dialog in Figure 8-6 is popped up by calling the standard askyesno function in the tkinter package’s messagebox module. This looks different on Unix and Macintosh systems, but it looks like you’d expect when run on Windows (and in fact varies its appearance even across different versions and configurations of Windows—using my default Window 7 setup, it looks slightly different than it did on Windows XP in the prior edition).

The dialog in Figure 8-6 blocks the program until the user clicks one of its buttons; if the dialog’s Yes button is clicked (or the Enter key is pressed), the dialog call returns with a true value and the script pops up the standard dialog in Figure 8-7 by calling showwarning.

dlg1 askyesno dialog (Windows 7)
Figure 8-6. dlg1 askyesno dialog (Windows 7)
dlg1 showwarning dialog
Figure 8-7. dlg1 showwarning dialog

There is nothing the user can do with Figure 8-7’s dialog but press OK. If No is clicked in Figure 8-6’s quit verification dialog, a showinfo call creates the pop up in Figure 8-8 instead. Finally, if the Spam button is clicked in the main window, the standard dialog captured in Figure 8-9 is generated with the standard showerror call.

dlg1 showinfo dialog
Figure 8-8. dlg1 showinfo dialog
dlg1 showerror dialog
Figure 8-9. dlg1 showerror dialog

All of this makes for a lot of window pop ups, of course, and you need to be careful not to rely on these dialogs too much (it’s generally better to use input fields in long-lived windows than to distract the user with pop ups). But where appropriate, such pop ups save coding time and provide a nice native look-and-feel.

A “smart” and reusable Quit button

Let’s put some of these canned dialogs to better use. Example 8-7 implements an attachable Quit button that uses standard dialogs to verify the quit request. Because it’s a class, it can be attached and reused in any application that needs a verifying Quit button. Because it uses standard dialogs, it looks as it should on each GUI platform.

Example 8-7. PP4EGuiTourquitter.py
"""
a Quit button that verifies exit requests;
to reuse, attach an instance to other GUIs, and re-pack as desired
"""

from tkinter import *                          # get widget classes
from tkinter.messagebox import askokcancel     # get canned std dialog

class Quitter(Frame):                          # subclass our GUI
    def __init__(self, parent=None):           # constructor method
        Frame.__init__(self, parent)
        self.pack()
        widget = Button(self, text='Quit', command=self.quit)
        widget.pack(side=LEFT, expand=YES, fill=BOTH)

    def quit(self):
        ans = askokcancel('Verify exit', "Really quit?")
        if ans: Frame.quit(self)

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

This module is mostly meant to be used elsewhere, but it puts up the button it implements when run standalone. Figure 8-10 shows the Quit button itself in the upper left, and the askokcancel verification dialog that pops up when Quit is pressed.

Quitter, with askokcancel dialog
Figure 8-10. Quitter, with askokcancel dialog

If you press OK here, Quitter runs the Frame quit method to end the GUI to which this button is attached (really, the mainloop call). But to really understand how such a spring-loaded button can be useful, we need to move on and study a client GUI in the next section.

A dialog demo launcher bar

So far, we’ve seen a handful of standard dialogs, but there are quite a few more. Instead of just throwing these up in dull screenshots, though, let’s write a Python demo script to generate them on demand. Here’s one way to do it. First of all, in Example 8-8 we write a module to define a table that maps a demo name to a standard dialog call (and we use lambda to wrap the call if we need to pass extra arguments to the dialog function).

Example 8-8. PP4EGuiTourdialogTable.py
# define a name:callback demos table

from tkinter.filedialog   import askopenfilename        # get standard dialogs
from tkinter.colorchooser import askcolor               # they live in Lib	kinter
from tkinter.messagebox   import askquestion, showerror
from tkinter.simpledialog import askfloat

demos = {
    'Open':  askopenfilename,
    'Color': askcolor,
    'Query': lambda: askquestion('Warning', 'You typed "rm *"
Confirm?'),
    'Error': lambda: showerror('Error!', "He's dead, Jim"),
    'Input': lambda: askfloat('Entry', 'Enter credit card number')
}

I put this table in a module so that it might be reused as the basis of other demo scripts later (dialogs are more fun than printing to stdout). Next, we’ll write a Python script, shown in Example 8-9, which simply generates buttons for all of this table’s entries—use its keys as button labels and its values as button callback handlers.

Example 8-9. PP4EGuiTourdemoDlg.py
"create a bar of simple buttons that launch dialog demos"

from tkinter import *              # get base widget set
from dialogTable import demos      # button callback handlers
from quitter import Quitter        # attach a quit object to me

class Demo(Frame):
    def __init__(self, parent=None, **options):
        Frame.__init__(self, parent, **options)
        self.pack()
        Label(self, text="Basic demos").pack()
        for (key, value) in demos.items():
            Button(self, text=key, command=value).pack(side=TOP, fill=BOTH)
        Quitter(self).pack(side=TOP, fill=BOTH)

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

This script creates the window shown in Figure 8-11 when run as a standalone program; it’s a bar of demo buttons that simply route control back to the values of the table in the module dialogTable when pressed.

demoDlg main window
Figure 8-11. demoDlg main window

Notice that because this script is driven by the contents of the dialogTable module’s dictionary, we can change the set of demo buttons displayed by changing just dialogTable (we don’t need to change any executable code in demoDlg). Also note that the Quit button here is an attached instance of the Quitter class of the prior section whose frame is repacked to stretch like the other buttons as needed here—it’s at least one bit of code that you never have to write again.

This script’s class also takes care to pass any **options constructor configuration keyword arguments on to its Frame superclass. Though not used here, this allows callers to pass in configuration options at creation time (Demo(o=v)), instead of configuring after the fact (d.config(o=v)). This isn’t strictly required, but it makes the demo class work just like a normal tkinter frame widget (which is what subclassing makes it, after all). We’ll see how this can be used to good effect later.

We’ve already seen some of the dialogs triggered by this demo bar window’s other buttons, so I’ll just step through the new ones here. Pressing the main window’s Query button, for example, generates the standard pop up in Figure 8-12.

demoDlg query, askquestion dialog
Figure 8-12. demoDlg query, askquestion dialog

This askquestion dialog looks like the askyesno we saw earlier, but actually it returns either string "yes" or "no" (askyesno and askokcancel return True or False instead—trivial but true). Pressing the demo bar’s Input button generates the standard askfloat dialog box shown in Figure 8-13.

demoDlg input, askfloat dialog
Figure 8-13. demoDlg input, askfloat dialog

This dialog automatically checks the input for valid floating-point syntax before it returns, and it is representative of a collection of single-value input dialogs (askinteger and askstring prompt for integer and string inputs, too). It returns the input as a floating-point number object (not as a string) when the OK button or Enter key is pressed, or the Python None object if the user clicks Cancel. Its two relatives return the input as integer and string objects instead.

When the demo bar’s Open button is pressed, we get the standard file open dialog made by calling askopenfilename and captured in Figure 8-14. This is Windows 7’s look-and-feel; it can look radically different on Macs, Linux, and older versions of Windows, but appropriately so.

demoDlg open, askopenfilename dialog
Figure 8-14. demoDlg open, askopenfilename dialog

A similar dialog for selecting a save-as filename is produced by calling asksaveasfilename (see the Text widget section in Chapter 9 for a first example). Both file dialogs let the user navigate through the filesystem to select a subject filename, which is returned with its full directory pathname when Open is pressed; an empty string comes back if Cancel is pressed instead. Both also have additional protocols not demonstrated by this example:

  • They can be passed a filetypes keyword argument—a set of name patterns used to select files, which appear in the pull-down list near the bottom of the dialog.

  • They can be passed an initialdir (start directory), initialfile (for “File name”), title (for the dialog window), defaultextension (appended if the selection has none), and parent (to appear as an embedded child instead of a pop-up dialog).

  • They can be made to remember the last directory selected by using exported objects instead of these function calls—a hook we’ll make use of in later longer-lived examples.

Another common dialog call in the tkinter filedialog module, askdirectory, can be used to pop up a dialog that allows users to choose a directory rather than a file. It presents a tree view that users can navigate to pick the desired directory, and it accepts keyword arguments including initialdir and title. The corresponding Directory object remembers the last directory selected and starts there the next time the dialog is shown.

We’ll use most of these interfaces later in the book, especially for the file dialogs in the PyEdit example in Chapter 11, but feel free to flip ahead for more details now. The directory selection dialog will show up in the PyPhoto example in Chapter 11 and the PyMailGUI example in Chapter 14; again, skip ahead for code and screenshots.

Finally, the demo bar’s Color button triggers a standard askcolor call, which generates the standard color selection dialog shown in Figure 8-15.

demoDlg color, askcolor dialog
Figure 8-15. demoDlg color, askcolor dialog

If you press its OK button, it returns a data structure that identifies the selected color, which can be used in all color contexts in tkinter. It includes RGB values and a hexadecimal color string (e.g., ((160, 160, 160), '#a0a0a0')). More on how this tuple can be useful in a moment. If you press Cancel, the script gets back a tuple containing two nones (Nones of the Python variety, that is).

Printing dialog results and passing callback data with lambdas

The dialog demo launcher bar displays standard dialogs and can be made to display others by simply changing the dialogTable module it imports. As coded, though, it really shows only dialogs; it would also be nice to see their return values so that we know how to use them in scripts. Example 8-10 adds printing of standard dialog results to the stdout standard output stream.

Example 8-10. PP4EGuiTourdemoDlg-print.py
"""
similar, but show return values of dialog calls;  the lambda saves data from
the local scope to be passed to the handler (button press handlers normally
get no arguments, and enclosing scope references don't work for loop variables)
and works just like a nested def statement: def func(key=key): self.printit(key)
"""

from tkinter import *              # get base widget set
from dialogTable import demos      # button callback handlers
from quitter import Quitter        # attach a quit object to me

class Demo(Frame):
    def __init__(self, parent=None):
        Frame.__init__(self, parent)
        self.pack()
        Label(self, text="Basic demos").pack()
        for key in demos:
            func = (lambda key=key: self.printit(key))
            Button(self, text=key, command=func).pack(side=TOP, fill=BOTH)
        Quitter(self).pack(side=TOP, fill=BOTH)

    def printit(self, name):
        print(name, 'returns =>', demos[name]())     # fetch, call, print

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

This script builds the same main button-bar window, but notice that the callback handler is an anonymous function made with a lambda now, not a direct reference to dialog calls in the imported dialogTable dictionary:

# use enclosing scope lookup
func = (lambda key=key: self.printit(key))

We talked about this in the prior chapter’s tutorial, but this is the first time we’ve actually used lambda like this, so let’s get the facts straight. Because button-press callbacks are run with no arguments, if we need to pass extra data to the handler, it must be wrapped in an object that remembers that extra data and passes it along, by deferring the call to the actual handler. Here, a button press runs the function generated by the lambda, an indirect call layer that retains information from the enclosing scope. The net effect is that the real handler, printit, receives an extra required name argument giving the demo associated with the button pressed, even though this argument wasn’t passed back from tkinter itself. In effect, the lambda remembers and passes on state information.

Notice, though, that this lambda function’s body references both self and key in the enclosing method’s local scope. In all recent Pythons, the reference to self just works because of the enclosing function scope lookup rules, but we need to pass key in explicitly with a default argument or else it will be the same in all the generated lambda functions—the value it has after the last loop iteration. As we learned in Chapter 7, enclosing scope references are resolved when the nested function is called, but defaults are resolved when the nested function is created. Because self won’t change after the function is made, we can rely on the scope lookup rules for that name, but not for loop variables like key.

In earlier Pythons, default arguments were required to pass all values in from enclosing scopes explicitly, using either of these two techniques:

# use simple defaults
func = (lambda self=self, name=key: self.printit(name))

# use a bound method default
func = (lambda handler=self.printit, name=key: handler(name))

Today, we can get away with the simpler enclosing -scope reference technique for self, though we still need a default for the key loop variable (and you may still see the default forms in older Python code).

Note that the parentheses around the lambdas are not required here; I add them as a personal style preference just to set the lambda off from its surrounding code (your mileage may vary). Also notice that the lambda does the same work as a nested def statement here; in practice, though, the lambda could appear within the call to Button itself because it is an expression and it need not be assigned to a name. The following two forms are equivalent:

for (key, value) in demos.items():
    func = (lambda key=key: self.printit(key))          # can be nested i Button()

for (key, value) in demos.items():
    def func(key=key): self.printit(key)                # but def statement cannot

You can also use a callable class object here that retains state as instance attributes (see the tutorial’s __call__ example in Chapter 7 for hints). But as a rule of thumb, if you want a lambda’s result to use any names from the enclosing scope when later called, either simply name them and let Python save their values for future use, or pass them in with defaults to save the values they have at lambda function creation time. The latter scheme is required only if the variable used may change before the callback occurs.

When run, this script creates the same window (Figure 8-11) but also prints dialog return values to standard output; here is the output after clicking all the demo buttons in the main window and picking both Cancel/No and then OK/Yes buttons in each dialog:

C:...PP4EGuiTour> python demoDlg-print.py
Color returns => (None, None)
Color returns => ((128.5, 128.5, 255.99609375), '#8080ff')
Query returns => no
Query returns => yes
Input returns => None
Input returns => 3.14159
Open returns =>
Open returns => C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Launcher.py
Error returns => ok

Now that I’ve shown you these dialog results, I want to next show you how one of them can actually be useful.

Letting users select colors on the fly

The standard color selection dialog isn’t just another pretty face—scripts can pass the hexadecimal color string it returns to the bg and fg widget color configuration options we met earlier. That is, bg and fg accept both a color name (e.g., blue) and an askcolor hex RGB result string that starts with a # (e.g., the #8080ff in the last output line of the prior section).

This adds another dimension of customization to tkinter GUIs: instead of hardcoding colors in your GUI products, you can provide a button that pops up color selectors that let users choose color preferences on the fly. Simply pass the color string to widget config methods in callback handlers, as in Example 8-11.

Example 8-11. PP4EGuiToursetcolor.py
from tkinter import *
from tkinter.colorchooser import askcolor

def setBgColor():
    (triple, hexstr) = askcolor()
    if hexstr:
        print(hexstr)
        push.config(bg=hexstr)

root = Tk()
push = Button(root, text='Set Background Color', command=setBgColor)
push.config(height=3, font=('times', 20, 'bold'))
push.pack(expand=YES, fill=BOTH)
root.mainloop()

This script creates the window in Figure 8-16 when launched (its button’s background is a sort of green, but you’ll have to trust me on this). Pressing the button pops up the color selection dialog shown earlier; the color you pick in that dialog becomes the background color of this button after you press OK.

setcolor main window
Figure 8-16. setcolor main window

Color strings are also printed to the stdout stream (the console window); run this on your computer to experiment with available color settings:

C:...PP4EGuiTour> python setcolor.py
#0080c0
#408080
#77d5df

Other standard dialog calls

We’ve seen most of the standard dialogs and we’ll use these pop ups in examples throughout the rest of this book. But for more details on other calls and options available, either consult other tkinter documentation or browse the source code of the modules used at the top of the dialogTable module in Example 8-8; all are simple Python files installed in the tkinter subdirectory of the Python source library on your machine (e.g., in C:Python31Lib on Windows). And keep this demo bar example filed away for future reference; we’ll reuse it later in the tour for callback actions when we meet other button-like widgets.

The Old-Style Dialog Module

In older Python code, you may see dialogs occasionally coded with the standard tkinter dialog module. This is a bit dated now, and it uses an X Windows look-and-feel; but just in case you run across such code in your Python maintenance excursions, Example 8-12 gives you a feel for the interface.

Example 8-12. PP4EGuiTourdlg-old.py
from tkinter import *
from tkinter.dialog import Dialog

class OldDialogDemo(Frame):
    def __init__(self, master=None):
        Frame.__init__(self, master)
        Pack.config(self)  # same as self.pack()
        Button(self, text='Pop1', command=self.dialog1).pack()
        Button(self, text='Pop2', command=self.dialog2).pack()

    def dialog1(self):
        ans = Dialog(self,
                     title   = 'Popup Fun!',
                     text    = 'An example of a popup-dialog '
                               'box, using older "Dialog.py".',
                     bitmap  = 'questhead',
                     default = 0, strings = ('Yes', 'No', 'Cancel'))
        if ans.num == 0: self.dialog2()

    def dialog2(self):
        Dialog(self, title   = 'HAL-9000',
                     text    = "I'm afraid I can't let you do that, Dave...",
                     bitmap  = 'hourglass',
                     default = 0, strings = ('spam', 'SPAM'))

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

If you supply Dialog a tuple of button labels and a message, you get back the index of the button pressed (the leftmost is index zero). Dialog windows are modal: the rest of the application’s windows are disabled until the Dialog receives a response from the user. When you press the Pop2 button in the main window created by this script, the second dialog pops up, as shown in Figure 8-17.

Old-style dialog
Figure 8-17. Old-style dialog

This is running on Windows, and as you can see, it is nothing like what you would expect on that platform for a question dialog. In fact, this dialog generates an X Windows look-and-feel, regardless of the underlying platform. Because of both Dialog’s appearance and the extra complexity required to program it, you are probably better off using the standard dialog calls of the prior section instead.

Custom Dialogs

The dialogs we’ve seen so far have a standard appearance and interaction. They are fine for many purposes, but often we need something a bit more custom. For example, forms that request multiple field inputs (e.g., name, age, shoe size) aren’t directly addressed by the common dialog library. We could pop up one single-input dialog in turn for each requested field, but that isn’t exactly user friendly.

Custom dialogs support arbitrary interfaces, but they are also the most complicated to program. Even so, there’s not much to it—simply create a pop-up window as a Toplevel with attached widgets, and arrange a callback handler to fetch user inputs entered in the dialog (if any) and to destroy the window. To make such a custom dialog modal, we also need to wait for a reply by giving the window input focus, making other windows inactive, and waiting for an event. Example 8-13 illustrates the basics.

Example 8-13. PP4EGuiTourdlg-custom.py
import sys
from tkinter import *
makemodal = (len(sys.argv) > 1)

def dialog():
    win = Toplevel()                                     # make a new window
    Label(win,  text='Hard drive reformatted!').pack()   # add a few widgets
    Button(win, text='OK', command=win.destroy).pack()   # set destroy callback
    if makemodal:
        win.focus_set()          # take over input focus,
        win.grab_set()           # disable other windows while I'm open,
        win.wait_window()        # and wait here until win destroyed
    print('dialog exit')         # else returns right away

root = Tk()
Button(root, text='popup', command=dialog).pack()
root.mainloop()

This script is set up to create a pop-up dialog window in either modal or nonmodal mode, depending on its makemodal global variable. If it is run with no command-line arguments, it picks nonmodal style, captured in Figure 8-18.

Nonmodal custom dialogs at work
Figure 8-18. Nonmodal custom dialogs at work

The window in the upper right is the root window here; pressing its “popup” button creates a new pop-up dialog window. Because dialogs are nonmodal in this mode, the root window remains active after a dialog is popped up. In fact, nonmodal dialogs never block other windows, so you can keep pressing the root’s button to generate as many copies of the pop-up window as will fit on your screen. Any or all of the pop ups can be killed by pressing their OK buttons, without killing other windows in this display.

Making custom dialogs modal

Now, when the script is run with a command-line argument (e.g., python dlg-custom.py 1), it makes its pop ups modal instead. Because modal dialogs grab all of the interface’s attention, the main window becomes inactive in this mode until the pop up is killed; you can’t even click on it to reactivate it while the dialog is open. Because of that, you can never make more than one copy of the pop up on-screen at once, as shown in Figure 8-19.

A modal custom dialog at work
Figure 8-19. A modal custom dialog at work

In fact, the call to the dialog function in this script doesn’t return until the dialog window on the left is dismissed by pressing its OK button. The net effect is that modal dialogs impose a function call–like model on an otherwise event-driven programming model; user inputs can be processed right away, not in a callback handler triggered at some arbitrary point in the future.

Forcing such a linear control flow on a GUI takes a bit of extra work, though. The secret to locking other windows and waiting for a reply boils down to three lines of code, which are a general pattern repeated in most custom modal dialogs.

win.focus_set()

Makes the window take over the application’s input focus, as if it had been clicked with the mouse to make it the active window. This method is also known by the synonym focus, and it’s also common to set the focus on an input widget within the dialog (e.g., an Entry) rather than on the entire window.

win.grab_set()

Disables all other windows in the application until this one is destroyed. The user cannot interact with other windows in the program while a grab is set.

win.wait_window()

Pauses the caller until the win widget is destroyed, but keeps the main event-processing loop (mainloop) active during the pause. That means that the GUI at large remains active during the wait; its windows redraw themselves if covered and uncovered, for example. When the window is destroyed with the destroy method, it is erased from the screen, the application grab is automatically released, and this method call finally returns.

Because the script waits for a window destroy event, it must also arrange for a callback handler to destroy the window in response to interaction with widgets in the dialog window (the only window active). This example’s dialog is simply informational, so its OK button calls the window’s destroy method. In user-input dialogs, we might instead install an Enter key-press callback handler that fetches data typed into an Entry widget and then calls destroy (see later in this chapter).

Other ways to be modal

Modal dialogs are typically implemented by waiting for a newly created pop-up window’s destroy event, as in this example. But other schemes are viable too. For example, it’s possible to create dialog windows ahead of time, and show and hide them as needed with the top-level window’s deiconify and withdraw methods (see the alarm scripts near the end of Chapter 9 for details). Given that window creation speed is generally fast enough as to appear instantaneous today, this is much less common than making and destroying a window from scratch on each interaction.

It’s also possible to implement a modal state by waiting for a tkinter variable to change its value, instead of waiting for a window to be destroyed. See this chapter’s later discussion of tkinter variables (which are class objects, not normal Python variables) and the wait_variable method discussed near the end of Chapter 9 for more details. This scheme allows a long-lived dialog box’s callback handler to signal a state change to a waiting main program, without having to destroy the dialog box.

Finally, if you call the mainloop method recursively, the call won’t return until the widget quit method has been invoked. The quit method terminates a mainloop call, and so normally ends a GUI program. But it will simply exit a recursive mainloop level if one is active. Because of this, modal dialogs can also be written without wait method calls if you are careful. For instance, Example 8-14 works the same way as the modal mode of dlg-custom.

Example 8-14. PP4EGuiTourdlg-recursive.py
from tkinter import *

def dialog():
    win = Toplevel()                                      # make a new window
    Label(win,  text='Hard drive reformatted!').pack()    # add a few widgets
    Button(win, text='OK', command=win.quit).pack()       # set quit callback
    win.protocol('WM_DELETE_WINDOW', win.quit)            # quit on wm close too!

    win.focus_set()          # take over input focus,
    win.grab_set()           # disable other windows while I'm open,
    win.mainloop()           # and start a nested event loop to wait
    win.destroy()
    print('dialog exit')

root = Tk()
Button(root, text='popup', command=dialog).pack()
root.mainloop()

If you go this route, be sure to call quit rather than destroy in dialog callback handlers (destroy doesn’t terminate the mainloop level), and be sure to use protocol to make the window border close button call quit too (or else it won’t end the recursive mainloop level call and may generate odd error messages when your program finally exits). Because of this extra complexity, you’re probably better off using wait_window or wait_variable, not recursive mainloop calls.

We’ll see how to build form-like dialogs with labels and input fields later in this chapter when we meet Entry, and again when we study the grid manager in Chapter 9. For more custom dialog examples, see ShellGui (Chapter 10), PyMailGUI (Chapter 14), PyCalc (Chapter 19), and the nonmodal form.py (Chapter 12). Here, we’re moving on to learn more about events that will prove to be useful currency at later tour destinations.

Binding Events

We met the bind widget method in the prior chapter, when we used it to catch button presses in the tutorial. Because bind is commonly used in conjunction with other widgets (e.g., to catch return key presses for input boxes), we’re going to make a stop early in the tour here as well. Example 8-15 illustrates more bind event protocols.

Example 8-15. PP4EGuiTourind.py
from tkinter import *

def showPosEvent(event):
    print('Widget=%s X=%s Y=%s' % (event.widget, event.x, event.y))

def showAllEvent(event):
    print(event)
    for attr in dir(event):
        if not attr.startswith('__'):
            print(attr, '=>', getattr(event, attr))

def onKeyPress(event):
    print('Got key press:', event.char)

def onArrowKey(event):
    print('Got up arrow key press')

def onReturnKey(event):
    print('Got return key press')

def onLeftClick(event):
    print('Got left mouse button click:', end=' ')
    showPosEvent(event)

def onRightClick(event):
    print('Got right mouse button click:', end=' ')
    showPosEvent(event)

def onMiddleClick(event):
    print('Got middle mouse button click:', end=' ')
    showPosEvent(event)
    showAllEvent(event)

def onLeftDrag(event):
    print('Got left mouse button drag:', end=' ')
    showPosEvent(event)

def onDoubleLeftClick(event):
    print('Got double left mouse click', end=' ')
    showPosEvent(event)
    tkroot.quit()

tkroot = Tk()
labelfont = ('courier', 20, 'bold')                # family, size, style
widget = Label(tkroot, text='Hello bind world')
widget.config(bg='red', font=labelfont)            # red background, large font
widget.config(height=5, width=20)                  # initial size: lines,chars
widget.pack(expand=YES, fill=BOTH)

widget.bind('<Button-1>',  onLeftClick)            # mouse button clicks
widget.bind('<Button-3>',  onRightClick)
widget.bind('<Button-2>',  onMiddleClick)          # middle=both on some mice
widget.bind('<Double-1>',  onDoubleLeftClick)      # click left twice
widget.bind('<B1-Motion>', onLeftDrag)             # click left and move

widget.bind('<KeyPress>',  onKeyPress)             # all keyboard presses
widget.bind('<Up>',        onArrowKey)             # arrow button pressed
widget.bind('<Return>',    onReturnKey)            # return/enter key pressed
widget.focus()                                     # or bind keypress to tkroot
tkroot.title('Click Me')
tkroot.mainloop()

Most of this file consists of callback handler functions triggered when bound events occur. As we learned in Chapter 7, this type of callback receives an event object argument that gives details about the event that fired. Technically, this argument is an instance of the tkinter Event class, and its details are attributes; most of the callbacks simply trace events by displaying relevant event attributes.

When run, this script makes the window shown in Figure 8-20; it’s mostly intended just as a surface for clicking and pressing event triggers.

A bind window for the clicking
Figure 8-20. A bind window for the clicking

The black-and-white medium of the book you’re holding won’t really do justice to this script. When run live, it uses the configuration options shown earlier to make the window show up as black on red, with a large Courier font. You’ll have to take my word for it (or run this on your own).

But the main point of this example is to demonstrate other kinds of event binding protocols at work. We saw a script that intercepted left and double-left mouse clicks with the widget bind method in Chapter 7, using event names <Button-1> and <Double-1>; the script here demonstrates other kinds of events that are commonly caught with bind:

<KeyPress>

To catch the press of a single key on the keyboard, register a handler for the <KeyPress> event identifier; this is a lower-level way to input data in GUI programs than the Entry widget covered in the next section. The key pressed is returned in ASCII string form in the event object passed to the callback handler (event.char). Other attributes in the event structure identify the key pressed in lower-level detail. Key presses can be intercepted by the top-level root window widget or by a widget that has been assigned keyboard focus with the focus method used by this script.

<B1-Motion>

This script also catches mouse motion while a button is held down: the registered <B1-Motion> event handler is called every time the mouse is moved while the left button is pressed and receives the current X/Y coordinates of the mouse pointer in its event argument (event.x, event.y). Such information can be used to implement object moves, drag-and-drop, pixel-level painting, and so on (e.g., see the PyDraw examples in Chapter 11).

<Button-3>, <Button-2>

This script also catches right and middle mouse button clicks (known as buttons 3 and 2). To make the middle button 2 click work on a two-button mouse, try clicking both buttons at the same time; if that doesn’t work, check your mouse setting in your properties interface (the Control Panel on Windows).

<Return>, <Up>

To catch more specific kinds of key presses, this script registers for the Return/Enter and up-arrow key press events; these events would otherwise be routed to the general <KeyPress> handler and require event analysis.

Here is what shows up in the stdout output stream after a left click, right click, left click and drag, a few key presses, a Return and up-arrow press, and a final double-left click to exit. When you press the left mouse button and drag it around on the display, you’ll get lots of drag event messages; one is printed for every move during the drag (and one Python callback is run for each):

C:...PP4EGuiTour> python bind.py
Got left mouse button click: Widget=.25763696 X=376 Y=53
Got right mouse button click: Widget=.25763696 X=36 Y=60
Got left mouse button click: Widget=.25763696 X=144 Y=43
Got left mouse button drag: Widget=.25763696 X=144 Y=45
Got left mouse button drag: Widget=.25763696 X=144 Y=47
Got left mouse button drag: Widget=.25763696 X=145 Y=50
Got left mouse button drag: Widget=.25763696 X=146 Y=51
Got left mouse button drag: Widget=.25763696 X=149 Y=53
Got key press: s
Got key press: p
Got key press: a
Got key press: m
Got key press: 1
Got key press: -
Got key press: 2
Got key press: .
Got return key press
Got up arrow key press
Got left mouse button click: Widget=.25763696 X=300 Y=68
Got double left mouse click Widget=.25763696 X=300 Y=68

For mouse-related events, callbacks print the X and Y coordinates of the mouse pointer, in the event object passed in. Coordinates are usually measured in pixels from the upper-left corner (0,0), but are relative to the widget being clicked. Here’s what is printed for a left, middle, and double-left click. Notice that the middle-click callback dumps the entire argument—all of the Event object’s attributes (less internal names that begin with “__” which includes the __doc__ string, and default operator overloading methods inherited from the implied object superclass in Python 3.X). Different event types set different event attributes; most key presses put something in char, for instance:

C:...PP4EGuiTour> python bind.py
Got left mouse button click: Widget=.25632624 X=6 Y=6
Got middle mouse button click: Widget=.25632624 X=212 Y=95
<tkinter.Event object at 0x018CA210>
char => ??
delta => 0
height => ??
keycode => ??
keysym => ??
keysym_num => ??
num => 2
send_event => False
serial => 17
state => 0
time => 549707945
type => 4
widget => .25632624
width => ??
x => 212
x_root => 311
y => 95
y_root => 221
Got left mouse button click: Widget=.25632624 X=400 Y=183
Got double left mouse click Widget=.25632624 X=400 Y=183

Other bind Events

Besides those illustrated in this example, a tkinter script can register to catch additional kinds of bindable events. For example:

  • <ButtonRelease> fires when a button is released (<ButtonPress> is run when the button first goes down).

  • <Motion> is triggered when a mouse pointer is moved.

  • <Enter> and <Leave> handlers intercept mouse entry and exit in a window’s display area (useful for automatically highlighting a widget).

  • <Configure> is invoked when the window is resized, repositioned, and so on (e.g., the event object’s width and height give the new window size). We’ll make use of this to resize the display on window resizes in the PyClock example of Chapter 11.

  • <Destroy> is invoked when the window widget is destroyed (and differs from the protocol mechanism for window manager close button presses). Since this interacts with widget quit and destroy methods, I’ll say more about the event later in this section.

  • <FocusIn> and <FocusOut> are run as the widget gains and loses focus.

  • <Map> and <Unmap> are run when a window is opened and iconified.

  • <Escape>, <BackSpace>, and <Tab> catch other special key presses.

  • <Down>, <Left>, and <Right> catch other arrow key presses.

This is not a complete list, and event names can be written with a somewhat sophisticated syntax of their own. For instance:

  • Modifiers can be added to event identifiers to make them even more specific; for instance, <B1-Motion> means moving the mouse with the left button pressed, and <KeyPress-a> refers to pressing the “a” key only.

  • Synonyms can be used for some common event names; for instance, <ButtonPress-1>, <Button-1>, and <1> mean a left mouse button press, and <KeyPress-a> and <Key-a> mean the “a” key. All forms are case sensitive: use <Key-Escape>, not <KEY-ESCAPE>.

  • Virtual event identifiers can be defined within double bracket pairs (e.g., <<PasteText>>) to refer to a selection of one or more event sequences.

In the interest of space, though, we’ll defer to other Tk and tkinter reference sources for an exhaustive list of details on this front. Alternatively, changing some of the settings in the example script and rerunning can help clarify some event behavior, too; this is Python, after all.

More on <Destroy> events and the quit and destroy methods

Before we move on, one event merits a few extra words: the <Destroy> event (whose name is case significant) is run when a widget is being destroyed, as a result of both script method calls and window closures in general, including those at program exit. If you bind this on a window, it will be triggered once for each widget in the window; the callback’s event argument widget attribute gives the widget being destroyed, and you can check this to detect a particular widget’s destruction. If you bind this on a specific widget instead, it will be triggered once for that widget’s destruction only.

It’s important to know that a widget is in a “half dead” state (Tk’s terminology) when this event is triggered—it still exists, but most operations on it fail. Because of that, the <Destroy> event is not intended for GUI activity in general; for instance, checking a text widget’s changed state or fetching its content in a <Destroy> handler can both fail with exceptions. In addition, this event’s handler cannot cancel the destruction in general and resume the GUI; if you wish to intercept and verify or suppress window closes when a user clicks on a window’s X button, use WM_DELETE_WINDOW in top-level windows’ protocol methods as described earlier in this chapter.

You should also know that running a tkinter widget’s quit method does not trigger any <Destroy> events on exit, and even leads to a fatal Python error on program exit in 3.X if any <Destroy> event handlers are registered. Because of this, programs that bind this event for non-GUI window exit actions should usually call destroy instead of quit to close, and rely on the fact that a program exits when the last remaining or only Tk root window (default or explicit) is destroyed as described earlier. This precludes using quit for immediate shutdowns, though you can still run sys.exit for brute-force exits.

A script can also perform program exit actions in code run after the mainloop call returns, but the GUI is gone completely at this point, and this code is not associated with any particular widget. Watch for more on this event when we study the PyEdit example program in Chapter 11; at the risk of spoiling the end of this story, we’ll find it unusable for verifying changed text saves.

Message and Entry

The Message and Entry widgets allow for display and input of simple text. Both are essentially functional subsets of the Text widget we’ll meet later; Text can do everything Message and Entry can, but not vice versa.

Message

The Message widget is simply a place to display text. Although the standard showinfo dialog we met earlier is perhaps a better way to display pop-up messages, Message splits up long strings automatically and flexibly and can be embedded inside container widgets any time you need to add some read-only text to a display. Moreover, this widget sports more than a dozen configuration options that let you customize its appearance. Example 8-16 and Figure 8-21 illustrate Message basics, and demonstrates how Message reacts to horizontal stretching with fill and expand; see Chapter 7 for more on resizing and Tk or tkinter references for other options Message supports.

Example 8-16. PP4EGui ourmessage.py
from tkinter import *
msg = Message(text="Oh by the way, which one's Pink?")
msg.config(bg='pink', font=('times', 16, 'italic'))
msg.pack(fill=X, expand=YES)
mainloop()
A Message widget at work
Figure 8-21. A Message widget at work

Entry

The Entry widget is a simple, single-line text input field. It is typically used for input fields in form-like dialogs and anywhere else you need the user to type a value into a field of a larger display. Entry also supports advanced concepts such as scrolling, key bindings for editing, and text selections, but it’s simple to use in practice. Example 8-17 builds the input window shown in Figure 8-22.

Example 8-17. PP4EGui ourentry1.py
from tkinter import *
from quitter import Quitter

def fetch():
    print('Input => "%s"' % ent.get())             # get text

root = Tk()
ent = Entry(root)
ent.insert(0, 'Type words here')                   # set text
ent.pack(side=TOP, fill=X)                         # grow horiz

ent.focus()                                        # save a click
ent.bind('<Return>', (lambda event: fetch()))      # on enter key
btn = Button(root, text='Fetch', command=fetch)    # and on button
btn.pack(side=LEFT)
Quitter(root).pack(side=RIGHT)
root.mainloop()
entry1 caught in the act
Figure 8-22. entry1 caught in the act

On startup, the entry1 script fills the input field in this GUI with the text “Type words here” by calling the widget’s insert method. Because both the Fetch button and the Enter key are set to trigger the script’s fetch callback function, either user event gets and displays the current text in the input field, using the widget’s get method:

C:...PP4EGuiTour> python entry1.py
Input => "Type words here"
Input => "Have a cigar"

We met the <Return> event earlier when we studied bind; unlike button presses, these lower-level callbacks get an event argument, so the script uses a lambda wrapper to ignore it. This script also packs the entry field with fill=X to make it expand horizontally with the window (try it out), and it calls the widget focus method to give the entry field input focus when the window first appears. Manually setting the focus like this saves the user from having to click the input field before typing. Our smart Quit button we wrote earlier is attached here again as well (it verifies exit).

Programming Entry widgets

Generally speaking, the values typed into and displayed by Entry widgets are set and fetched with either tied “variable” objects (described later in this chapter) or Entry widget method calls such as this one:

ent.insert(0, 'some text')          # set value
value = ent.get()                   # fetch value (a string)

The first parameter to the insert method gives the position where the text is to be inserted. Here, “0” means the front because offsets start at zero, and integer 0 and string '0' mean the same thing (tkinter method arguments are always converted to strings if needed). If the Entry widget might already contain text, you also generally need to delete its contents before setting it to a new value, or else new text will simply be added to the text already present:

ent.delete(0, END)                  # first, delete from start to end
ent.insert(0, 'some text')          # then set value

The name END here is a preassigned tkinter constant denoting the end of the widget; we’ll revisit it in Chapter 9 when we meet the full-blown and multiple-line Text widget (Entry’s more powerful cousin). Since the widget is empty after the deletion, this statement sequence is equivalent to the prior one:

ent.delete('0', END)                # delete from start to end
ent.insert(END, 'some text')        # add at end of empty text

Either way, if you don’t delete the text first, new text that is inserted is simply added. If you want to see how, try changing the fetch function in Example 8-17 to look like this—an “x” is added at the beginning and end of the input field on each button or key press:

def fetch():
    print('Input => "%s"' % ent.get())        # get text
    ent.insert(END, 'x')                      # to clear: ent.delete('0', END)
    ent.insert(0, 'x')                        # new text simply added

In later examples, we’ll also see the Entry widget’s state='disabled' option, which makes it read only, as well as its show='*' option, which makes it display each character as a * (useful for password-type inputs). Try this out on your own by changing and running this script for a quick look. Entry supports other options we’ll skip here, too; see later examples and other resources for additional details.

Laying Out Input Forms

As mentioned, Entry widgets are often used to get field values in form-like displays. We’re going to create such displays often in this book, but to show you how this works in simpler terms, Example 8-18 combines labels, entries, and frames to achieve the multiple-input display captured in Figure 8-23.

Example 8-18. PP4EGuiTourentry2.py
"""
use Entry widgets directly
lay out by rows with fixed-width labels: this and grid are best for forms
"""

from tkinter import *
from quitter import Quitter
fields = 'Name', 'Job', 'Pay'

def fetch(entries):
    for entry in entries:
        print('Input => "%s"' % entry.get())        # get text

def makeform(root, fields):
    entries = []
    for field in fields:
        row = Frame(root)                           # make a new row
        lab = Label(row, width=5, text=field)       # add label, entry
        ent = Entry(row)
        row.pack(side=TOP, fill=X)                  # pack row on top
        lab.pack(side=LEFT)
        ent.pack(side=RIGHT, expand=YES, fill=X)    # grow horizontal
        entries.append(ent)
    return entries

if __name__ == '__main__':
    root = Tk()
    ents = makeform(root, fields)
    root.bind('<Return>', (lambda event: fetch(ents)))
    Button(root, text='Fetch',
                 command= (lambda: fetch(ents))).pack(side=LEFT)
    Quitter(root).pack(side=RIGHT)
    root.mainloop()
entry2 (and entry3) form displays
Figure 8-23. entry2 (and entry3) form displays

The input fields here are just simple Entry widgets. The script builds an explicit list of these widgets to be used to fetch their values later. Every time you press this window’s Fetch button, it grabs the current values in all the input fields and prints them to the standard output stream:

C:...PP4EGuiTour> python entry2.py
Input => "Bob"
Input => "Technical Writer"
Input => "Jack"

You get the same field dump if you press the Enter key anytime this window has the focus on your screen; this event has been bound to the whole root window this time, not to a single input field.

Most of the art in form layout has to do with arranging widgets in a hierarchy. This script builds each label/entry row as a new Frame attached to the window’s current TOP; fixed-width labels are attached to the LEFT of their row, and entries to the RIGHT. Because each row is a distinct Frame, its contents are insulated from other packing going on in this window. The script also arranges for just the entry fields to grow vertically on a resize, as in Figure 8-24.

entry2 (and entry3) expansion at work
Figure 8-24. entry2 (and entry3) expansion at work

Going modal again

Later on this tour, we’ll see how to make similar form layouts with the grid geometry manager, where we arrange by row and column numbers instead of frames. But now that we have a handle on form layout, let’s see how to apply the modal dialog techniques we met earlier to a more complex input display.

Example 8-19 uses the prior example’s makeform and fetch functions to generate a form and prints its contents, much as before. Here, though, the input fields are attached to a new Toplevel pop-up window created on demand, and an OK button is added to the new window to trigger a window destroy event that erases the pop up. As we learned earlier, the wait_window call pauses until the destroy happens.

Example 8-19. PP4EGuiTourentry2-modal.py
# make form dialog modal; must fetch before destroy with entries

from tkinter import *
from entry2 import makeform, fetch, fields

def show(entries, popup):
    fetch(entries)                  # must fetch before window destroyed!
    popup.destroy()                 # fails with msgs if stmt order is reversed

def ask():
    popup = Toplevel()              # show form in modal dialog window
    ents = makeform(popup, fields)
    Button(popup, text='OK', command=(lambda: show(ents, popup))).pack()
    popup.grab_set()
    popup.focus_set()
    popup.wait_window()             # wait for destroy here

root = Tk()
Button(root, text='Dialog', command=ask).pack()
root.mainloop()

When you run this code, pressing the button in this program’s main window creates the blocking form input dialog in Figure 8-25, as expected.

entry2-modal (and entry3-modal) displays
Figure 8-25. entry2-modal (and entry3-modal) displays

But a subtle danger is lurking in this modal dialog code: because it fetches user inputs from Entry widgets embedded in the popped-up display, it must fetch those inputs before destroying the pop-up window in the OK press callback handler. It turns out that a destroy call really does destroy all the child widgets of the window destroyed; trying to fetch values from a destroyed Entry not only doesn’t work, but also generates a traceback with error messages in the console window. Try reversing the statement order in the show function to see for yourself.

To avoid this problem, we can either be careful to fetch before destroying, or use tkinter variables, the subject of the next section.

tkinter “Variables” and Form Layout Alternatives

Entry widgets (among others) support the notion of an associated variable—changing the associated variable changes the text displayed in the Entry, and changing the text in the Entry changes the value of the variable. These aren’t normal Python variable names, though. Variables tied to widgets are instances of variable classes in the tkinter module library. These classes are named StringVar, IntVar, DoubleVar, and BooleanVar; you pick one based on the context in which it is to be used. For example, a StringVar class instance can be associated with an Entry field, as demonstrated in Example 8-20.

Example 8-20. PP4EGuiTourentry3.py
"""
use StringVar variables
lay out by columns: this might not align horizontally everywhere (see entry2)
"""

from tkinter import *
from quitter import Quitter
fields = 'Name', 'Job', 'Pay'

def fetch(variables):
    for variable in variables:
        print('Input => "%s"' % variable.get())     # get from var

def makeform(root, fields):
    form = Frame(root)                              # make outer frame
    left = Frame(form)                              # make two columns
    rite = Frame(form)
    form.pack(fill=X)
    left.pack(side=LEFT)
    rite.pack(side=RIGHT, expand=YES, fill=X)       # grow horizontal

    variables = []
    for field in fields:
        lab = Label(left, width=5, text=field)      # add to columns
        ent = Entry(rite)
        lab.pack(side=TOP)
        ent.pack(side=TOP, fill=X)                  # grow horizontal
        var = StringVar()
        ent.config(textvariable=var)                # link field to var
        var.set('enter here')
        variables.append(var)
    return variables

if __name__ == '__main__':
    root = Tk()
    vars = makeform(root, fields)
    Button(root, text='Fetch', command=(lambda: fetch(vars))).pack(side=LEFT)
    Quitter(root).pack(side=RIGHT)
    root.bind('<Return>', (lambda event: fetch(vars)))
    root.mainloop()

Except for the fact that this script initializes input fields with the string 'enter here', it makes a window virtually identical in appearance and function to that created by the script entry2 (see Figures 8-23 and 8-24). For illustration purposes, the window is laid out differently—as a Frame containing two nested subframes used to build the left and right columns of the form area—but the end result is the same when it is displayed on screen (for some GUIs on some platforms, at least: see the note at the end of this section for a discussion of why layout by rows instead of columns is generally preferred).

The main thing to notice here, though, is the use of StringVar variables. Instead of using a list of Entry widgets to fetch input values, this version keeps a list of StringVar objects that have been associated with the Entry widgets, like this:

ent = Entry(rite)
var = StringVar()
ent.config(textvariable=var)                # link field to var

Once you’ve tied variables in this way, changing and fetching the variable’s value:

var.set('text here')
value = var.get()

will really change and fetch the corresponding display’s input field value.[32] The variable object get method returns as a string for StringVar, an integer for IntVar, and a floating-point number for DoubleVar.

Of course, we’ve already seen that it’s easy to set and fetch text in Entry fields directly, without adding extra code to use variables. So, why the bother about variable objects? For one thing, it clears up that nasty fetch-after-destroy peril we met in the prior section. Because StringVars live on after the Entry widgets they are tied to have been destroyed, it’s OK to fetch input values from them long after a modal dialog has been dismissed, as shown in Example 8-21.

Example 8-21. PP4EGuiTourentry3-modal.py
# can fetch values after destroy with stringvars

from tkinter import *
from entry3 import makeform, fetch, fields

def show(variables, popup):
    popup.destroy()                 # order doesn't matter here
    fetch(variables)                # variables live on after window destroyed

def ask():
    popup = Toplevel()              # show form in modal dialog window
    vars = makeform(popup, fields)
    Button(popup, text='OK', command=(lambda: show(vars, popup))).pack()
    popup.grab_set()
    popup.focus_set()
    popup.wait_window()             # wait for destroy here

root = Tk()
Button(root, text='Dialog', command=ask).pack()
root.mainloop()

This version is the same as the original (shown in Example 8-19 and Figure 8-25), but show now destroys the pop up before inputs are fetched through StringVars in the list created by makeform. In other words, variables are a bit more robust in some contexts because they are not part of a real display tree. For example, they are also commonly associated with check buttons, radio boxes, and scales in order to provide access to current settings and link multiple widgets together. Almost coincidentally, that’s the topic of the next section.

Note

We laid out input forms two ways in this section: by row frames with fixed-width labels (entry2), and by column frames (entry3). In Chapter 9 we’ll see a third form technique: layouts using the grid geometry manager. Of these, gridding, and the rows with fixed-width labels of entry2 tend to work best across all platforms.

Laying out by column frames as in entry3 works only on platforms where the height of each label exactly matches the height of each entry field. Because the two are not associated directly, they might not line up properly on some platforms. When I tried running some forms that looked fine on Windows XP on a Linux machine, labels and their corresponding entries did not line up horizontally.

Even the simple window produced by entry3 looks slightly askew on closer inspection. It only appears the same as entry2 on some platforms because of the small number of inputs and size defaults. On my Windows 7 netbook, the labels and entries start to become horizontally mismatched if you add 3 or 4 additional inputs to entry3’s fields tuple.

If you care about portability, lay out your forms either with the packed row frames and fixed/maximum-width labels of entry2, or by gridding widgets by row and column numbers instead of packing them. We’ll see more on such forms in the next chapter. And in Chapter 12, we’ll write a form-construction tool that hides the layout details from its clients altogether (including its use case client in Chapter 13).

Checkbutton, Radiobutton, and Scale

This section introduces three widget types: the Checkbutton (a multiple-choice input widget), the Radiobutton (a single-choice device), and the Scale (sometimes known as a “slider”). All are variations on a theme and are somewhat related to simple buttons, so we’ll explore them as a group here. To make these widgets more fun to play with, we’ll reuse the dialogTable module shown in Example 8-8 to provide callbacks for widget selections (callbacks pop up dialog boxes). Along the way, we’ll also use the tkinter variables we just met to communicate with these widgets’ state settings.

Checkbuttons

The Checkbutton and Radiobutton widgets are designed to be associated with tkinter variables: clicking the button changes the value of the variable, and setting the variable changes the state of the button to which it is linked. In fact, tkinter variables are central to the operation of these widgets:

  • A collection of Checkbuttons implements a multiple-choice interface by assigning each button a variable of its own.

  • A collection of Radiobuttons imposes a mutually exclusive single-choice model by giving each button a unique value and the same tkinter variable.

Both kinds of buttons provide both command and variable options. The command option lets you register a callback to be run immediately on button-press events, much like normal Button widgets. But by associating a tkinter variable with the variable option, you can also fetch or change widget state at any time by fetching or changing the value of the widget’s associated variable.

Since it’s a bit simpler, let’s start with the tkinter Checkbutton. Example 8-22 creates the set of five captured in Figure 8-26. To make this more useful, it also adds a button that dumps the current state of all Checkbuttons and attaches an instance of the verifying Quitter button we built earlier in the tour.

demoCheck in action
Figure 8-26. demoCheck in action
Example 8-22. PP4EGuiTourdemoCheck.py
"create a bar of check buttons that run dialog demos"

from tkinter import *             # get base widget set
from dialogTable import demos     # get canned dialogs
from quitter import Quitter       # attach a quitter object to "me"

class Demo(Frame):
    def __init__(self, parent=None, **options):
        Frame.__init__(self, parent, **options)
        self.pack()
        self.tools()
        Label(self, text="Check demos").pack()
        self.vars = []
        for key in demos:
            var = IntVar()
            Checkbutton(self,
                        text=key,
                        variable=var,
                        command=demos[key]).pack(side=LEFT)
            self.vars.append(var)

    def report(self):
        for var in self.vars:
            print(var.get(), end=' ')   # current toggle settings: 1 or 0
        print()

    def tools(self):
        frm = Frame(self)
        frm.pack(side=RIGHT)
        Button(frm, text='State', command=self.report).pack(fill=X)
        Quitter(frm).pack(fill=X)

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

In terms of program code, check buttons resemble normal buttons; they are even packed within a container widget. Operationally, though, they are a bit different. As you can probably tell from this figure (and can better tell by running this live), a check button works as a toggle—pressing one changes its state from off to on (from deselected to selected); or from on to off again. When a check button is selected, it has a checked display, and its associated IntVar variable has a value of 1; when deselected, its display is empty and its IntVar has a value of 0.

To simulate an enclosing application, the State button in this display triggers the script’s report method to display the current values of all five toggles on the stdout stream. Here is the output after a few clicks:

C:...PP4EGuiTour> python demoCheck.py
0 0 0 0 0
1 0 0 0 0
1 0 1 0 0
1 0 1 1 0
1 0 0 1 0
1 0 0 1 1

Really, these are the values of the five tkinter variables associated with the Checkbuttons with variable options, but they give the buttons’ values when queried. This script associates IntVar variables with each Checkbutton in this display, since they are 0 or 1 binary indicators. StringVars will work here, too, although their get methods would return strings '0' or '1' (not integers) and their initial state would be an empty string (not the integer 0).

This widget’s command option lets you register a callback to be run each time the button is pressed. To illustrate, this script registers a standard dialog demo call as a handler for each of the Checkbuttons—pressing a button changes the toggle’s state but also pops up one of the dialog windows we visited earlier in this tour (regardless of its new state).

Interestingly, you can sometimes run the report method interactively, too—when working as follows in a shell window, widgets pop up as lines are typed and are fully active, even without calling mainloop (though this may not work in some interfaces like IDLE if you must call mainloop to display your GUI):

C:...PP4EGuiTour> python
>>> from demoCheck import Demo
>>> d = Demo()
>>> d.report()
0 0 0 0 0
>>> d.report()
1 0 0 0 0
>>> d.report()
1 0 0 1 1

Check buttons and variables

When I first studied check buttons, my initial reaction was: why do we need tkinter variables here at all when we can register button-press callbacks? Linked variables may seem superfluous at first glance, but they simplify some GUI chores. Instead of asking you to accept this blindly, though, let me explain why.

Keep in mind that a Checkbutton’s command callback will be run on every press, whether the press toggles the check button to a selected or a deselected state. Because of that, if you want to run an action immediately when a check button is pressed, you will generally want to check the button’s current value in the callback handler. Because there is no check button “get” method for fetching values, you usually need to interrogate an associated variable to see if the button is on or off.

Moreover, some GUIs simply let users set check buttons without running command callbacks at all and fetch button settings at some later point in the program. In such a scenario, variables serve to automatically keep track of button settings. The demoCheck script’s report method represents this latter approach.

Of course, you could manually keep track of each button’s state in press callback handlers, too. Example 8-23 keeps its own list of state toggles and updates it manually on command press callbacks.

Example 8-23. PP4EGuiTourdemo-check-manual.py
# check buttons, the hard way (without variables)

from tkinter import *
states = []                            # change object not name
def onPress(i):                        # keep track of states
    states[i] = not states[i]          # changes False->True, True->False

root = Tk()
for i in range(10):
    chk = Checkbutton(root, text=str(i), command=(lambda i=i: onPress(i)) )
    chk.pack(side=LEFT)
    states.append(False)
root.mainloop()
print(states)                          # show all states on exit

The lambda here passes along the pressed button’s index in the states list. Otherwise, we would need a separate callback function for each button. Here again, we need to use a default argument to pass the loop variable into the lambda, or the loop variable will be its value on the last loop iteration for all 10 of the generated functions (each press would update the tenth item in the list; see Chapter 7 for background details on this). When run, this script makes the 10–check button display in Figure 8-27.

Manual check button state window
Figure 8-27. Manual check button state window

Manually maintained state toggles are updated on every button press and are printed when the GUI exits (technically, when the mainloop call returns); it’s a list of Boolean state values, which could also be integers 1 or 0 if we cared to exactly imitate the original:

C:...PP4EGuiTour> python demo-check-manual.py
[False, False, True, False, True, False, False, False, True, False]

This works, and it isn’t too horribly difficult to manage manually. But linked tkinter variables make this task noticeably easier, especially if you don’t need to process check button states until some time in the future. This is illustrated in Example 8-24.

Example 8-24. PP4EGuiTourdemo-check-auto.py
# check buttons, the easy way

from tkinter import *
root = Tk()
states = []
for i in range(10):
    var = IntVar()
    chk = Checkbutton(root, text=str(i), variable=var)
    chk.pack(side=LEFT)
    states.append(var)
root.mainloop()                               # let tkinter keep track
print([var.get() for var in states])          # show all states on exit (or map/lambda)

This looks and works the same way, but there is no command button-press callback handler at all, because toggle state is tracked by tkinter automatically:

C:...PP4EGuiTour> python demo-check-auto.py
[0, 0, 1, 1, 0, 0, 1, 0, 0, 1]

The point here is that you don’t necessarily have to link variables with check buttons, but your GUI life will be simpler if you do. The list comprehension at the very end of this script, by the way, is equivalent to the following unbound method and lambda/bound-method map call forms:

print(list(map(IntVar.get, states)))
print(list(map(lambda var: var.get(), states)))

Though comprehensions are common in Python today, the form that seems clearest to you may very well depend upon your shoe size…

Radio Buttons

Radio buttons are toggles too, but they are generally used in groups: just like the mechanical station selector pushbuttons on radios of times gone by, pressing one Radiobutton widget in a group automatically deselects the one pressed last. In other words, at most, only one can be selected at one time. In tkinter, associating all radio buttons in a group with unique values and the same variable guarantees that, at most, only one can ever be selected at a given time.

Like check buttons and normal buttons, radio buttons support a command option for registering a callback to handle presses immediately. Like check buttons, radio buttons also have a variable attribute for associating single-selection buttons in a group and fetching the current selection at arbitrary times.

In addition, radio buttons have a value attribute that lets you tell tkinter what value the button’s associated variable should have when the button is selected. Because more than one radio button is associated with the same variable, you need to be explicit about each button’s value (it’s not just a 1 or 0 toggle scenario). Example 8-25 demonstrates radio button basics.

Example 8-25. PP4EGuiTourdemoRadio.py
"create a group of radio buttons that launch dialog demos"

from tkinter import *                # get base widget set
from dialogTable import demos        # button callback handlers
from quitter import Quitter          # attach a quit object to "me"

class Demo(Frame):
    def __init__(self, parent=None, **options):
        Frame.__init__(self, parent, **options)
        self.pack()
        Label(self, text="Radio demos").pack(side=TOP)
        self.var = StringVar()
        for key in demos:
            Radiobutton(self, text=key,
                              command=self.onPress,
                              variable=self.var,
                              value=key).pack(anchor=NW)
        self.var.set(key) # select last to start
        Button(self, text='State', command=self.report).pack(fill=X)
        Quitter(self).pack(fill=X)

    def onPress(self):
        pick = self.var.get()
        print('you pressed', pick)
        print('result:', demos[pick]())

    def report(self):
        print(self.var.get())

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

Figure 8-28 shows what this script generates when run. Pressing any of this window’s radio buttons triggers its command handler, pops up one of the standard dialog boxes we met earlier, and automatically deselects the button previously pressed. Like check buttons, radio buttons are packed; this script packs them to the top to arrange them vertically, and then anchors each on the northwest corner of its allocated space so that they align well.

demoRadio in action
Figure 8-28. demoRadio in action

Like the check button demo script, this one also puts up a State button to run the class’s report method and to show the current radio state (the button selected). Unlike the check button demo, this script also prints the return values of dialog demo calls that are run as its buttons are pressed. Here is what the stdout stream looks like after a few presses and state dumps; states are shown in bold:

C:...PP4EGuiTour> python demoRadio.py
you pressed Input
result: 3.14
Input
you pressed Open
result: C:/PP4thEd/Examples/PP4E/Gui/Tour/demoRadio.py
Open
you pressed Query
result: yes
Query

Radio buttons and variables

So, why variables here? For one thing, radio buttons also have no “get” widget method to fetch the selection in the future. More importantly, in radio button groups, the value and variable settings turn out to be the whole basis of single-choice behavior. In fact, to make radio buttons work normally at all, it’s crucial that they are all associated with the same tkinter variable and have distinct value settings. To truly understand why, though, you need to know a bit more about how radio buttons and variables do their stuff.

We’ve already seen that changing a widget changes its associated tkinter variable, and vice versa. But it’s also true that changing a variable in any way automatically changes every widget it is associated with. In the world of radio buttons, pressing a button sets a shared variable, which in turn impacts other buttons associated with that variable. Assuming that all radio buttons have distinct values, this works as you expect it to work. When a button press changes the shared variable to the pressed button’s value, all other buttons are deselected, simply because the variable has been changed to a value not their own.

This is true both when the user selects a button and changes the shared variable’s value implicitly, but also when the variable’s value is set manually by a script. For instance, when Example 8-25 sets the shared variable to the last of the demo’s names initially (with self.var.set), it selects that demo’s button and deselects all the others in the process; this way, only one is selected at first. If the variable was instead set to a string that is not any demo’s name (e.g., ' '), all buttons would be deselected at startup.

This ripple effect is a bit subtle, but it might help to know that within a group of radio buttons sharing the same variable, if you assign a set of buttons the same value, the entire set will be selected if any one of them is pressed. Consider Example 8-26, which creates Figure 8-29, for instance. All buttons start out deselected this time (by initializing the shared variable to none of their values), but because radio buttons 0, 3, 6, and 9 have value 0 (the remainder of division by 3), all are selected if any are selected.

Radio buttons gone bad?
Figure 8-29. Radio buttons gone bad?
Example 8-26. PP4EGuiTourdemo-radio-multi.py
# see what happens when some buttons have same value

from tkinter import *
root = Tk()
var = StringVar()
for i in range(10):
    rad = Radiobutton(root, text=str(i), variable=var, value=str(i % 3))
    rad.pack(side=LEFT)
var.set(' ') # deselect all initially
root.mainloop()

If you press 1, 4, or 7 now, all three of these are selected, and any existing selections are cleared (they don’t have the value “1”). That’s not normally what you want—radio buttons are usually a single-choice group (check buttons handle multiple-choice inputs). If you want them to work as expected, be sure to give each radio button the same variable but a unique value across the entire group. In the demoRadio script, for instance, the name of the demo provides a naturally unique value for each button.

Radio buttons without variables

Strictly speaking, we could get by without tkinter variables here, too. Example 8-27, for instance, implements a single-selection model without variables, by manually selecting and deselecting widgets in the group, in a callback handler of its own. On each press event, it issues deselect calls for every widget object in the group and select for the one pressed.

Example 8-27. PP4EGuiTourdemo-radio-manual.py
"""
radio buttons, the hard way (without variables)
note that deselect for radio buttons simply sets the button's
associated value to a null string, so we either need to still
give buttons unique values, or use checkbuttons here instead;
"""

from tkinter import *
state = ''
buttons = []

def onPress(i):
    global state
    state = i
    for btn in buttons:
        btn.deselect()
    buttons[i].select()

root = Tk()
for i in range(10):
    rad = Radiobutton(root, text=str(i),
                            value=str(i), command=(lambda i=i: onPress(i)) )
    rad.pack(side=LEFT)
    buttons.append(rad)

onPress(0)                   # select first initially
root.mainloop()
print(state)                 # show state on exit

This works. It creates a 10-radio button window that looks just like the one in Figure 8-29 but implements a single-choice radio-style interface, with current state available in a global Python variable printed on script exit. By associating tkinter variables and unique values, though, you can let tkinter do all this work for you, as shown in Example 8-28.

Example 8-28. PP4EGuiTourdemo-radio-auto.py
# radio buttons, the easy way

from tkinter import *
root = Tk()                     # IntVars work too
var  = IntVar(0)                # select 0 to start
for i in range(10):
    rad = Radiobutton(root, text=str(i), value=i, variable=var)
    rad.pack(side=LEFT)
root.mainloop()
print(var.get())                # show state on exit

This works the same way, but it is a lot less to type and debug. Notice that this script associates the buttons with an IntVar, the integer type sibling of StringVar, and initializes it to zero (which is also its default); as long as button values are unique, integers work fine for radio buttons too.

Hold onto your variables!

One minor word of caution: you should generally hold onto the tkinter variable object used to link radio buttons for as long as the radio buttons are displayed. Assign it to a module global variable, store it in a long-lived data structure, or save it as an attribute of a long-lived class instance object as done by demoRadio. Just make sure you retain a reference to it somehow. You normally will in order to fetch its state anyhow, so it’s unlikely that you’ll ever care about what I’m about to tell you.

But in the current tkinter, variable classes have a __del__ destructor that automatically unsets a generated Tk variable when the Python object is reclaimed (i.e., garbage collected). The upshot is that all of your radio buttons may be deselected if the variable object is collected, at least until the next press resets the Tk variable to a new value. Example 8-29 shows one way to trigger this.

Example 8-29. PP4EGuiTourdemo-radio-clear.py
# hold on to your radio variables (an obscure thing, indeed)

from tkinter import *
root = Tk()

def radio1():                   # local vars are temporary
    #global tmp                 # making it global fixes the problem
    tmp = IntVar()
    for i in range(10):
        rad = Radiobutton(root, text=str(i), value=i, variable=tmp)
        rad.pack(side=LEFT)
    tmp.set(5)  # select 6th button

radio1()
root.mainloop()

This should come up with button “5” selected initially, but it doesn’t. The variable referenced by local tmp is reclaimed on function exit, the Tk variable is unset, and the 5 setting is lost (all buttons come up unselected). These radio buttons work fine, though, once you start pressing them, because that resets the internal Tk variable. Uncommenting the global statement here makes 5 start out set, as expected.

This phenomenon seems to have grown even worse in Python 3.X: not only is “5” not selected initially, but moving the mouse cursor over the unselected buttons seems to select many at random until one is pressed. (In 3.X we also need to initialize a StringVar shared by radio buttons as we did in this section’s earlier examples, or else its empty string default selects all of them!)

Of course, this is an atypical example—as coded, there is no way to know which button is pressed, because the variable isn’t saved (and command isn’t set). It makes little sense to use a group of radio buttons at all if you cannot query its value later. In fact, this is so obscure that I’ll just refer you to demo-radio-clear2.py in the book’s examples distribution for an example that works hard to trigger this oddity in other ways. You probably won’t care, but you can’t say that I didn’t warn you if you ever do.

Scales (Sliders)

Scales (sometimes called “sliders”) are used to select among a range of numeric values. Moving the scale’s position with mouse drags or clicks moves the widget’s value among a range of integers and triggers Python callbacks if registered.

Like check buttons and radio buttons, scales have both a command option for registering an event-driven callback handler to be run right away when the scale is moved, and a variable option for associating a tkinter variable that allows the scale’s position to be fetched and set at arbitrary times. You can process scale settings when they are made, or let the user pick a setting for later use.

In addition, scales have a third processing optionget and set methods that scripts may call to access scale values directly without associating variables. Because scale command movement callbacks also get the current scale setting value as an argument, it’s often enough just to provide a callback for this widget, without resorting to either linked variables or get/set method calls.

To illustrate the basics, Example 8-30 makes two scales—one horizontal and one vertical—and links them with an associated variable to keep them in sync.

Example 8-30. PP4EGuiTourdemoScale.py
"create two linked scales used to launch dialog demos"

from tkinter import *                # get base widget set
from dialogTable import demos        # button callback handlers
from quitter import Quitter          # attach a quit frame to me

class Demo(Frame):
    def __init__(self, parent=None, **options):
        Frame.__init__(self, parent, **options)
        self.pack()
        Label(self, text="Scale demos").pack()
        self.var = IntVar()
        Scale(self, label='Pick demo number',
                    command=self.onMove,                   # catch moves
                    variable=self.var,                     # reflects position
                    from_=0, to=len(demos)-1).pack()
        Scale(self, label='Pick demo number',
                    command=self.onMove,                   # catch moves
                    variable=self.var,                     # reflects position
                    from_=0, to=len(demos)-1,
                    length=200, tickinterval=1,
                    showvalue=YES, orient='horizontal').pack()
        Quitter(self).pack(side=RIGHT)
        Button(self, text="Run demo", command=self.onRun).pack(side=LEFT)
        Button(self, text="State",    command=self.report).pack(side=RIGHT)

    def onMove(self, value):
        print('in onMove', value)

    def onRun(self):
        pos = self.var.get()
        print('You picked', pos)
        demo = list(demos.values())[pos]    # map from position to value (3.X view)
        print(demo())                       # or demos[ list(demos.keys())[pos] ]()

    def report(self):
        print(self.var.get())

if __name__ == '__main__':
    print(list(demos.keys()))
    Demo().mainloop()

Besides value access and callback registration, scales have options tailored to the notion of a range of selectable values, most of which are demonstrated in this example’s code:

  • The label option provides text that appears along with the scale, length specifies an initial size in pixels, and orient specifies an axis.

  • The from_ and to options set the scale range’s minimum and maximum values (note that from is a Python reserved word, but from_ is not).

  • The tickinterval option sets the number of units between marks drawn at regular intervals next to the scale (the default means no marks are drawn).

  • The resolution option provides the number of units that the scale’s value jumps on each drag or left mouse click event (defaults to 1).

  • The showvalue option can be used to show or hide the scale’s current value next to its slider bar (the default showvalue=YES means it is drawn).

Note that scales are also packed in their container, just like other tkinter widgets. Let’s see how these ideas translate in practice; Figure 8-30 shows the window you get if you run this script live on Windows 7 (you get a similar one on Unix and Mac machines).

demoScale in action
Figure 8-30. demoScale in action

For illustration purposes, this window’s State button shows the scales’ current values, and “Run demo” runs a standard dialog call as before, using the integer value of the scales to index the demos table. The script also registers a command handler that fires every time either of the scales is moved and prints their new positions. Here is a set of messages sent to stdout after a few moves, demo runs (italic), and state requests (bold):

C:...PP4EGuiTour> python demoScale.py
['Color', 'Query', 'Input', 'Open', 'Error']
in onMove 0
in onMove 0
in onMove 1
1
in onMove 2
You picked 2
123.0
in onMove 3
3
You picked 3
C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Launcher.py

Scales and variables

As you can probably tell, scales offer a variety of ways to process their selections: immediately in move callbacks, or later by fetching current positions with variables or scale method calls. In fact, tkinter variables aren’t needed to program scales at all—simply register movement callbacks or call the scale get method to fetch scale values on demand, as in the simpler scale example in Example 8-31.

Example 8-31. PP4EGuiTourdemo-scale-simple.py
from tkinter import *
root = Tk()
scl = Scale(root, from_=-100, to=100, tickinterval=50, resolution=10)
scl.pack(expand=YES, fill=Y)

def report():
    print(scl.get())

Button(root, text='state', command=report).pack(side=RIGHT)
root.mainloop()

Figure 8-31 shows two instances of this program running on Windows—one stretched and one not (the scales are packed to grow vertically on resizes). Its scale displays a range from −100 to 100, uses the resolution option to adjust the current position up or down by 10 on every move, and sets the tickinterval option to show values next to the scale in increments of 50. When you press the State button in this script’s window, it calls the scale’s get method to display the current setting, without variables or callbacks of any kind:

C:...PP4EGuiTour> python demo-scale-simple.py
0
60
-70
A simple scale without variables
Figure 8-31. A simple scale without variables

Frankly, the only reason tkinter variables are used in the demoScale script at all is to synchronize scales. To make the demo interesting, this script associates the same tkinter variable object with both scales. As we learned in the last section, changing a widget changes its variable, but changing a variable also changes all the widgets it is associated with. In the world of sliders, moving the slide updates that variable, which in turn might update other widgets associated with the same variable. Because this script links one variable with two scales, it keeps them automatically in sync: moving one scale moves the other, too, because the shared variable is changed in the process and so updates the other scale as a side effect.

Linking scales like this may or may not be typical of your applications (and borders on deep magic), but it’s a powerful tool once you get your mind around it. By linking multiple widgets on a display with tkinter variables, you can keep them automatically in sync, without making manual adjustments in callback handlers. On the other hand, the synchronization could be implemented without a shared variable at all by calling one scale’s set method from a move callback handler of the other. I’ll leave such a manual mutation as a suggested exercise, though. One person’s deep magic might be another’s useful hack.

Running GUI Code Three Ways

Now that we’ve built a handful of similar demo launcher programs, let’s write a few top-level scripts to combine them. Because the demos were coded as both reusable classes and scripts, they can be deployed as attached frame components, run in their own top-level windows, and launched as standalone programs. All three options illustrate code reuse in action.

Attaching Frames

To illustrate hierarchical GUI composition on a grander scale than we’ve seen so far, Example 8-32 arranges to show all four of the dialog launcher bar scripts of this chapter in a single container. It reuses Examples 8-9, 8-22, 8-25, and 8-30.

Example 8-32. PP4EGuiTourdemoAll-frm.py
"""
4 demo class components (subframes) on one window;
there are 5 Quitter buttons on this one window too, and each kills entire gui;
GUIs can be reused as frames in container, independent windows, or processes;
"""

from tkinter import *
from quitter import Quitter
demoModules = ['demoDlg', 'demoCheck', 'demoRadio', 'demoScale']
parts = []

def addComponents(root):
    for demo in demoModules:
        module = __import__(demo)                       # import by name string
        part = module.Demo(root)                        # attach an instance
        part.config(bd=2, relief=GROOVE)                # or pass configs to Demo()
        part.pack(side=LEFT, expand=YES, fill=BOTH)     # grow, stretch with window
        parts.append(part)                              # change list in-place

def dumpState():
    for part in parts:                                  # run demo report if any
        print(part.__module__ + ':', end=' ')
        if hasattr(part, 'report'):
           part.report()
        else:
           print('none')

root = Tk()                                             # make explicit root first
root.title('Frames')
Label(root, text='Multiple Frame demo', bg='white').pack()
Button(root, text='States', command=dumpState).pack(fill=X)
Quitter(root).pack(fill=X)
addComponents(root)
root.mainloop()

Because all four demo launcher bars are coded as frames which attach themselves to parent container widgets, this is easier than you might think: simply pass the same parent widget (here, the root window) to all four demo constructor calls, and repack and configure the demo objects as desired. Figure 8-32 shows this script’s graphical result—a single window embedding instances of all four of the dialog demo launcher demos we saw earlier. As coded, all four embedded demos grow and stretch with the window when resized (try taking out the expand=YES to keep their sizes more constant).

demoAll_frm: nested subframes
Figure 8-32. demoAll_frm: nested subframes

Naturally, this example is artificial, but it illustrates the power of composition when applied to building larger GUI displays. If you pretend that each of the four attached demo objects was something more useful, like a text editor, calculator, or clock, you’ll better appreciate the point of this example.

Besides demo object frames, this composite window also contains no fewer than five instances of the Quitter button we wrote earlier (all of which verify the request and any one of which can end the GUI) and a States button to dump the current values of all the embedded demo objects at once (it calls each object’s report method, if it has one). Here is a sample of the sort of output that shows up in the stdout stream after interacting with widgets on this display; States output is in bold:

C:...PP4EGuiTour> python demoAll_frm.py
in onMove 0
in onMove 0
demoDlg: none
demoCheck: 0 0 0 0 0
demoRadio: Error
demoScale: 0
you pressed Input
result: 1.234
in onMove 1
demoDlg: none
demoCheck: 1 0 1 1 0
demoRadio: Input
demoScale: 1
you pressed Query
result: yes
in onMove 2
You picked 2
None
in onMove 3
You picked 3
C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Launcher.py
3
Query
1 1 1 1 0
demoDlg: none
demoCheck: 1 1 1 1 0
demoRadio: Query
demoScale: 3

Importing by name string

The only substantially tricky part of this script is its use of Python’s built-in __import__ function to import a module by a name string. Look at the following two lines from the script’s addComponents function:

module = __import__(demo)             # import module by name string
part = module.Demo(root)              # attach an instance of its Demo

This is equivalent to saying something like this:

import 'demoDlg'
part = 'demoDlg'.Demo(root)

However, the preceding code is not legal Python syntax—the module name in import statements and dot expressions must be a Python variable, not a string; moreover, in an import the name is taken literally (not evaluated), and in dot syntax must evaluate to the object (not its string name). To be generic, addComponents steps through a list of name strings and relies on __import__ to import and return the module identified by each string. In fact, the for loop containing these statements works as though all of these statements were run:

import demoDlg, demoRadio, demoCheck, demoScale
part = demoDlg.Demo(root)
part = demoRadio.Demo(root)
part = demoCheck.Demo(root)
part = demoScale.Demo(root)

But because the script uses a list of name strings, it’s easier to change the set of demos embedded—simply change the list, not the lines of executable code. Further, such data-driven code tends to be more compact, less redundant, and easier to debug and maintain. Incidentally, modules can also be imported from name strings by dynamically constructing and running import statements, like this:

for demo in demoModules:
    exec('from %s import Demo' % demo)       # make and run a from
    part = eval('Demo')(root)                # fetch known import name by string

The exec statement compiles and runs a Python statement string (here, a from to load a module’s Demo class); it works here as if the statement string were pasted into the source code where the exec statement appears. The following achieves the same effect by running an import statement instead:

for demo in demoModules:
    exec('import %s' % demo)                 # make and run an import
    part = eval(demo).Demo(root)             # fetch module variable by name too

Because it supports any sort of Python statement, these exec/eval techniques are more general than the __import__ call, but can also be slower, since they must parse code strings before running them.[33] However, that slowness may not matter in a GUI; users tend to be significantly slower than parsers.

Configuring at construction time

One other alternative worth mentioning: notice how Example 8-32 configures and repacks each attached demo frame for its role in this GUI:

def addComponents(root):
    for demo in demoModules:
        module = __import__(demo)                       # import by name string
        part = module.Demo(root)                        # attach an instance
        part.config(bd=2, relief=GROOVE)                # or pass configs to Demo()
        part.pack(side=LEFT, expand=YES, fill=BOTH)     # grow, stretch with window

Because the demo classes use their **options arguments to support constructor arguments, though, we could configure at creation time, too. For example, if we change this code as follows, it produces the slightly different composite window captured in Figure 8-33 (stretched a bit horizontally for illustration, too; you can run this as demoAll-frm-ridge.py in the examples package):

def addComponents(root):
    for demo in demoModules:
        module = __import__(demo)                       # import by name string
        part = module.Demo(root, bd=6, relief=RIDGE)    # attach, config instance
        part.pack(side=LEFT, expand=YES, fill=BOTH)     # grow, stretch with window

Because the demo classes both subclass Frame and support the usual construction argument protocols, they become true widgets—specialized tkinter frames that implement an attachable package of widgets and support flexible configuration techniques.

demoAll_frm: configure when constructed
Figure 8-33. demoAll_frm: configure when constructed

As we saw in Chapter 7, attaching nested frames like this is really just one way to reuse GUI code structured as classes. It’s just as easy to customize such interfaces by subclassing rather than embedding. Here, though, we’re more interested in deploying an existing widget package than changing it, so attachment is the pattern we want. The next two sections show two other ways to present such precoded widget packages to users—in pop-up windows and as autonomous programs.

Independent Windows

Once you have a set of component classes coded as frames, any parent will work—both other frames and brand-new, top-level windows. Example 8-33 attaches instances of all four demo bar objects to their own independent Toplevel windows, instead of the same container.

Example 8-33. PP4EGuiTourdemoAll-win.py
"""
4 demo classes in independent top-level windows;
not processes: when one is quit all others go away, because all windows run in
the same process here; make Tk() first here, else we get blank default window
"""

from tkinter import *
demoModules = ['demoDlg', 'demoRadio', 'demoCheck', 'demoScale']

def makePopups(modnames):
    demoObjects = []
    for modname in modnames:
        module = __import__(modname)          # import by name string
        window = Toplevel()                   # make a new window
        demo   = module.Demo(window)          # parent is the new window
        window.title(module.__name__)
        demoObjects.append(demo)
    return demoObjects

def allstates(demoObjects):
    for obj in demoObjects:
        if hasattr(obj, 'report'):
            print(obj.__module__, end=' ')
            obj.report()

root = Tk()                                   # make explicit root first
root.title('Popups')
demos = makePopups(demoModules)
Label(root, text='Multiple Toplevel window demo', bg='white').pack()
Button(root, text='States', command=lambda: allstates(demos)).pack(fill=X)
root.mainloop()

We met the Toplevel class earlier; every instance generates a new window on your screen. The net result is captured in Figure 8-34. Each demo runs in an independent window of its own instead of being packed together in a single display.

demoAll_win: new Toplevel windows
Figure 8-34. demoAll_win: new Toplevel windows

The main root window of this program appears in the lower left of this screenshot; it provides a States button that runs the report method of each demo object, producing this sort of stdout text:

C:...PP4EGuiTour> python demoAll_win.py
in onMove 0
in onMove 0
in onMove 1
you pressed Open
result: C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Launcher.py
demoRadio Open
demoCheck 1 1 0 0 0
demoScale 1

As we learned earlier in this chapter, Toplevel windows function independently, but they are not really independent programs. Destroying just one of the demo windows in Figure 8-34 by clicking the X button in its upper right corner closes just that window. But quitting any of the windows shown in Figure 8-34—by a demo window’s Quit buttons or the main window’s X—quits them all and ends the application, because all run in the same program process. That’s OK in some applications, but not all. To go truly rogue we need to spawn processes, as the next section shows.

Running Programs

To be more independent, Example 8-34 spawns each of the four demo launchers as independent programs (processes), using the launchmodes module we wrote at the end of Chapter 5. This works only because the demos were written as both importable classes and runnable scripts. Launching them here makes all their names __main__ when run, because they are separate, stand-alone programs; this in turn kicks off the mainloop call at the bottom of each of their files.

Example 8-34. PP4EGuiTourdemoAll-prg.py
"""
4 demo classes run as independent program processes: command lines;
if one window is quit now, the others will live on; there is no simple way to
run all report calls here (though sockets and pipes could be used for IPC), and
some launch schemes may drop child program stdout and disconnect parent/child;
"""

from tkinter import *
from PP4E.launchmodes import PortableLauncher
demoModules = ['demoDlg', 'demoRadio', 'demoCheck', 'demoScale']

for demo in demoModules:                        # see Parallel System Tools
    PortableLauncher(demo, demo + '.py')()      # start as top-level programs

root = Tk()
root.title('Processes')
Label(root, text='Multiple program demo: command lines', bg='white').pack()
root.mainloop()

Make sure the PP4E directory’s container is on your module search path (e.g., PYTHONPATH) to run this; it imports an example module from a different directory. As Figure 8-35 shows, the display generated by this script is similar to the prior one; all four demos come up in windows of their own.

This time, though, these are truly independent programs: if any one of the five windows here is quit, the others live on. The demos even outlive their parent, if the main window is closed. On Windows, in fact, the shell window where this script is started becomes active again when the main window is closed, even though the spawned demos continue running. We’re reusing the demo code as program, not module.

demoAll_prg: independent programs
Figure 8-35. demoAll_prg: independent programs

Launching GUIs as programs other ways: multiprocessing

If you backtrack to Chapter 5 to study the portable launcher module used by Example 8-34 to start programs, you’ll see that it works by using os.spawnv on Windows and os.fork/exec on others. The net effect is that the GUI processes are effectively started by launching command lines. These techniques work well, but as we learned in Chapter 5, they are members of a larger set of program launching tools that also includes os.popen, os.system, os.startfile, and the subprocess and multiprocessing modules; these tools can vary subtly in how they handle shell window connections, parent process exits, and more.

For example, the multiprocessing module we studied in Chapter 5 provides a similarly portable way to run our GUIs as independent processes, as demonstrated in Example 8-35. When run, it produces the exact same windows shown in Figure 8-35, except that the label in the main window is different.

Example 8-35. PP4EGuiTourdemoAll-prg-multi.py
"""
4 demo classes run as independent program processes: multiprocessing;
multiprocessing allows us to launch named functions with arguments,
but not lambdas, because they are not pickleable on Windows (Chapter 5);
multiprocessing also has its own IPC tools like pipes for communication;
"""

from tkinter import *
from multiprocessing import Process
demoModules = ['demoDlg', 'demoRadio', 'demoCheck', 'demoScale']

def runDemo(modname):                     # run in a new process
    module = __import__(modname)          # build gui from scratch
    module.Demo().mainloop()

if __name__ == '__main__':
    for modname in demoModules:                               # in __main__ only!
        Process(target=runDemo, args=(modname,)).start()

    root = Tk()                                               # parent process GUI
    root.title('Processes')
    Label(root, text='Multiple program demo: multiprocessing', bg='white').pack()
    root.mainloop()

Operationally, this version differs on Windows only in that:

  • The child processes’ standard output shows up in the window where the script was launched, including the outputs of both dialog demos themselves and all demo windows’ State buttons.

  • The script doesn’t truly exit if any children are still running: the shell where it is launched is blocked if the main process’s window is closed while children are still running, unless we set the child processes’ daemon flag to True before they start as we saw in Chapter 5—in which case all child programs are automatically shut down when their parent is (but parents may still outlive their children).

Also observe how we start a simple named function in the new Process. As we learned in Chapter 5, the target must be pickleable on Windows (which essentially means importable), so we cannot use lambdas to pass extra data in the way we typically could in tkinter callbacks. The following coding alternatives both fail with errors on Windows:

Process(target=(lambda: runDemo(modname))).start()            # these both fail!

Process(target=(lambda: __import__(modname).Demo().mainloop())).start()

We won’t recode our GUI program launcher script with any of the other techniques available, but feel free to experiment on your own using Chapter 5 as a resource. Although not universally applicable, the whole point of tools like the PortableLauncher class is to hide such details so we can largely forget them.

Cross-program communication

Spawning GUIs as programs is the ultimate in code independence, but it makes the lines of communication between components more complex. For instance, because the demos run as programs here, there is no easy way to run all their report methods from the launching script’s window pictured in the upper right of Figure 8-35. In fact, the States button is gone this time, and we only get PortableLauncher messages in stdout as the demos start up in Example 8-34:

C:...PP4EGuiTour> python demoAll_prg.py
demoDlg
demoRadio
demoCheck
demoScale

On some platforms, messages printed by the demo programs (including their own State buttons) may show up in the original console window where this script is launched; on Windows, the os.spawnv call used to start programs by launchmodes in Example 8-34 completely disconnects the child program’s stdout stream from its parent, but the multiprocessing scheme of Example 8-35 does not. Regardless, there is no direct way to call all demos’ report methods at once—they are spawned programs in distinct address spaces, not imported modules.

Of course, we could trigger report methods in the spawned programs with some of the Inter-Process Communication (IPC) mechanisms we met in Chapter 5. For instance:

  • The demos could be instrumented to catch a user signal, and could run their report in response.

  • The demos could also watch for request strings sent by the launching program to show up in pipes or fifos; the demoAll launching program would essentially act as a client, and the demo GUIs as servers that respond to client requests.

  • Independent programs can also converse this same way over sockets, the general IPC tool introduced in Chapter 5, which we’ll study in depth in Part IV. The main window might send a report request and receive its result on the same socket (and might even contact demos running remotely).

  • If used, the multiprocessing module has IPC tools all its own, such as the object pipes and queues we studied in Chapter 5, that could also be leveraged: demos might listen on this type of pipe, too.

Given their event-driven nature, GUI-based programs like our demos also need to avoid becoming stuck in wait states—they cannot be blocked while waiting for requests on IPC devices like those above, or they won’t be responsive to users (and might not even redraw themselves). Because of that, they may also have be augmented with threads, timer-event callbacks, nonblocking input calls, or some combination of such techniques to periodically check for incoming messages on pipes, fifos, or sockets. As we’ll see, the tkinter after method call described near the end of the next chapter is ideal for this: it allows us to register a callback to run periodically to check for incoming requests on such IPC tools.

We’ll explore some of these options near the end of Chapter 10, after we’ve looked at GUI threading topics. But since this is well beyond the scope of the current chapter’s simple demo programs, I’ll leave such cross-program extensions up to more parallel-minded readers for now.

Coding for reusability

A postscript: I coded the demo launcher bars deployed by the last four examples to demonstrate all the different ways that their widgets can be used. They were not developed with general-purpose reusability in mind; in fact, they’re not really useful outside the context of introducing widgets in this book.

That was by design; most tkinter widgets are easy to use once you learn their interfaces, and tkinter already provides lots of configuration flexibility by itself. But if I had it in mind to code checkbutton and radiobutton classes to be reused as general library components, they would have to be structured differently:

Extra widgets

They would not display anything but radio buttons and check buttons. As is, the demos each embed State and Quit buttons for illustration, but there really should be just one Quit per top-level window.

Geometry management

They would allow for different button arrangements and would not pack (or grid) themselves at all. In a true general-purpose reuse scenario, it’s often better to leave a component’s geometry management up to its caller.

Usage mode limitations

They would either have to export complex interfaces to support all possible tkinter configuration options and modes, or make some limiting decisions that support one common use only. For instance, these buttons can either run callbacks at press time or provide their state later in the application.

Example 8-36 shows one way to code check button and radio button bars as library components. It encapsulates the notion of associating tkinter variables and imposes a common usage mode on callers—state fetches rather than press callbacks—to keep the interface simple.

Example 8-36. PP4EGuiTouruttonbars.py
"""
check and radio button bar classes for apps that fetch state later;
pass a list of options, call state(), variable details automated
"""

from tkinter import *

class Checkbar(Frame):
    def __init__(self, parent=None, picks=[], side=LEFT, anchor=W):
        Frame.__init__(self, parent)
        self.vars = []
        for pick in picks:
            var = IntVar()
            chk = Checkbutton(self, text=pick, variable=var)
            chk.pack(side=side, anchor=anchor, expand=YES)
            self.vars.append(var)
    def state(self):
        return [var.get() for var in self.vars]

class Radiobar(Frame):
    def __init__(self, parent=None, picks=[], side=LEFT, anchor=W):
        Frame.__init__(self, parent)
        self.var = StringVar()
        self.var.set(picks[0])
        for pick in picks:
            rad = Radiobutton(self, text=pick, value=pick, variable=self.var)
            rad.pack(side=side, anchor=anchor, expand=YES)
    def state(self):
        return self.var.get()

if __name__ == '__main__':
    root = Tk()
    lng = Checkbar(root, ['Python', 'C#', 'Java', 'C++'])
    gui = Radiobar(root, ['win', 'x11', 'mac'], side=TOP, anchor=NW)
    tgl = Checkbar(root, ['All'])

    gui.pack(side=LEFT, fill=Y)
    lng.pack(side=TOP,  fill=X)
    tgl.pack(side=LEFT)
    lng.config(relief=GROOVE, bd=2)
    gui.config(relief=RIDGE,  bd=2)

    def allstates():
        print(gui.state(), lng.state(), tgl.state())

    from quitter import Quitter
    Quitter(root).pack(side=RIGHT)
    Button(root, text='Peek', command=allstates).pack(side=RIGHT)
    root.mainloop()

To reuse these classes in your scripts, import and call them with a list of the options that you want to appear in a bar of check buttons or radio buttons. This module’s self-test code at the bottom of the file gives further usage details. It generates Figure 8-36—a top-level window that embeds two Checkbars, one Radiobar, a Quitter button to exit, and a Peek button to show bar states—when this file is run as a program instead of being imported.

buttonbars self-test window
Figure 8-36. buttonbars self-test window

Here’s the stdout text you get after pressing Peek—the results of these classes’ state methods:

x11 [1, 0, 1, 1] [0]
win [1, 0, 0, 1] [1]

The two classes in this module demonstrate how easy it is to wrap tkinter interfaces to make them easier to use; they completely abstract away many of the tricky parts of radio button and check button bars. For instance, you can forget about linked variable details completely if you use such higher-level classes instead—simply make objects with option lists and call their state methods later. If you follow this path to its logical conclusion, you might just wind up with a higher-level widget library on the order of the Pmw package mentioned in Chapter 7.

On the other hand, these classes are still not universally applicable; if you need to run actions when these buttons are pressed, for instance, you’ll need to use other high-level interfaces. Luckily, Python/tkinter already provides plenty. Later in this book, we’ll again use the widget combination and reuse techniques introduced in this section to construct larger GUIs like text editors, email clients and calculators. For now, this first chapter in the widget tour is about to make one last stop—the photo shop.

Images

In tkinter, graphical images are displayed by creating independent PhotoImage or BitmapImage objects, and then attaching those image objects to other widgets via image attribute settings. Buttons, labels, canvases, text, and menus can display images by associating prebuilt image objects in this way. To illustrate, Example 8-37 throws a picture up on a button.

Example 8-37. PP4EGuiTourimgButton.py
gifdir = "../gifs/"
from tkinter import *
win = Tk()
igm = PhotoImage(file=gifdir + "ora-pp.gif")
Button(win, image=igm).pack()
win.mainloop()

I could try to come up with a simpler example, but it would be tough—all this script does is make a tkinter PhotoImage object for a GIF file stored in another directory, and associate it with a Button widget’s image option. The result is captured in Figure 8-37.

imgButton in action
Figure 8-37. imgButton in action

PhotoImage and its cousin, BitmapImage, essentially load graphics files and allow those graphics to be attached to other kinds of widgets. To open a picture file, pass its name to the file attribute of these image objects. Though simple, attaching images to buttons this way has many uses; in Chapter 9, for instance, we’ll use this basic idea to implement toolbar buttons at the bottom of a window.

Canvas widgets—general drawing surfaces covered in more detail in the next chapter—can display pictures too. Though this is a bit of a preview for the upcoming chapter, basic canvas usage is straightforward enough to demonstrate here; Example 8-38 renders Figure 8-38 (shrunk here for display):

Example 8-38. PP4EGuiTourimgCanvas.py
gifdir = "../gifs/"
from tkinter import *
win = Tk()
img = PhotoImage(file=gifdir + "ora-lp4e.gif")
can = Canvas(win)
can.pack(fill=BOTH)
can.create_image(2, 2, image=img, anchor=NW)           # x, y coordinates
win.mainloop()
An image on canvas
Figure 8-38. An image on canvas

Buttons are automatically sized to fit an associated photo, but canvases are not (because you can add objects to a canvas later, as we’ll see in Chapter 9). To make a canvas fit the picture, size it according to the width and height methods of image objects, as in Example 8-39. This version will make the canvas smaller or larger than its default size as needed, lets you pass in a photo file’s name on the command line, and can be used as a simple image viewer utility. The visual effect of this script is captured in Figure 8-39.

Example 8-39. PP4EGuiTourimgCanvas2.py
gifdir = "../gifs/"
from sys import argv
from tkinter import *
filename = argv[1] if len(argv) > 1 else 'ora-lp4e.gif'   # name on cmdline?

win = Tk()
img = PhotoImage(file=gifdir + filename)
can = Canvas(win)
can.pack(fill=BOTH)
can.config(width=img.width(), height=img.height())        # size to img size
can.create_image(2, 2, image=img, anchor=NW)
win.mainloop()
Sizing the canvas to match the photo
Figure 8-39. Sizing the canvas to match the photo

Run this script with other filenames to view other images (try this on your own):

C:...PP4EGuiTour> imgCanvas2.py ora-ppr-german.gif

And that’s all there is to it. In Chapter 9, we’ll see images show up again in the items of a Menu, in the buttons of a window’s toolbar, in other Canvas examples, and in the image-friendly Text widget. In later chapters, we’ll find them in an image slideshow (PyView), in a paint program (PyDraw), on clocks (PyClock), in a generalized photo viewer (PyPhoto), and so on. It’s easy to add graphics to GUIs in Python/tkinter.

Once you start using photos in earnest, though, you’re likely to run into two tricky bits that I want to warn you about here:

Supported file types

At present, the standard tkinter PhotoImage widget supports only GIF, PPM, and PGM graphic file formats, and BitmapImage supports X Windows-style .xbm bitmap files. This may be expanded in future releases, and you can convert photos in other formats to these supported formats ahead of time, of course. But as we’ll see later in this chapter, it’s easy to support additional image types with the PIL open source extension toolkit and its PhotoImage replacement.

Hold on to your images!

Unlike all other tkinter widgets, an image is utterly lost if the corresponding Python image object is garbage collected. That means you must retain an explicit reference to image objects for as long as your program needs them (e.g., assign them to a long-lived variable name, object attribute, or data structure component). Python does not automatically keep a reference to the image, even if it is linked to other GUI components for display. Moreover, image destructor methods erase the image from memory. We saw earlier that tkinter variables can behave oddly when reclaimed, too (they may be unset), but the effect is much worse and more likely to happen with images. This may change in future Python releases, though there are good reasons for not retaining big image files in memory indefinitely; for now, though, images are a “use it or lose it” widget.

Fun with Buttons and Pictures

I tried to come up with an image demo for this section that was both fun and useful. I settled for the fun part. Example 8-40 displays a button that changes its image at random each time it is pressed.

Example 8-40. PP4EGuiTouruttonpics-func.py
from tkinter import *                # get base widget set
from glob import glob                # filename expansion list
import demoCheck                     # attach checkbutton demo to me
import random                        # pick a picture at random
gifdir = '../gifs/'                  # where to look for GIF files

def draw():
    name, photo = random.choice(images)
    lbl.config(text=name)
    pix.config(image=photo)

root=Tk()
lbl = Label(root,  text="none", bg='blue', fg='red')
pix = Button(root, text="Press me", command=draw, bg='white')
lbl.pack(fill=BOTH)
pix.pack(pady=10)
demoCheck.Demo(root, relief=SUNKEN, bd=2).pack(fill=BOTH)

files = glob(gifdir + "*.gif")                              # GIFs for now
images = [(x, PhotoImage(file=x)) for x in files]           # load and hold
print(files)
root.mainloop()

This code uses a handful of built-in tools from the Python library:

  • The Python glob module we first met in Chapter 4 gives a list of all files ending in .gif in a directory; in other words, all GIF files stored there.

  • The Python random module is used to select a random GIF from files in the directory: random.choice picks and returns an item from a list at random.

  • To change the image displayed (and the GIF file’s name in a label at the top of the window), the script simply calls the widget config method with new option settings; changing on the fly like this changes the widget’s display dynamically.

Just for fun, this script also attaches an instance of the demoCheck check button demo bar from Example 8-22, which in turn attaches an instance of the Quitter button we wrote earlier in Example 8-7. This is an artificial example, of course, but it again demonstrates the power of component class attachment at work.

Notice how this script builds and holds on to all images in its images list. The list comprehension here applies a PhotoImage constructor call to every .gif file in the photo directory, producing a list of (filename, imageobject) tuples that is saved in a global variable (a map call using a one-argument lambda function could do the same). Remember, this guarantees that image objects won’t be garbage collected as long as the program is running. Figure 8-40 shows this script in action on Windows.

Although it may not be obvious in this grayscale book, the name of the GIF file being displayed is shown in red text in the blue label at the top of this window. This program’s window grows and shrinks automatically when larger and smaller GIF files are displayed; Figure 8-41 shows it randomly picking a taller photo globbed from the image directory.

buttonpics in action
Figure 8-40. buttonpics in action
buttonpics showing a taller photo
Figure 8-41. buttonpics showing a taller photo

And finally, Figure 8-42 captures this script’s GUI displaying one of the wider GIFs, selected completely at random from the photo file directory.[34]

buttonpics gets political
Figure 8-42. buttonpics gets political

While we’re playing, let’s recode this script as a class in case we ever want to attach or customize it later (it could happen, especially in more realistic programs). It’s mostly a matter of indenting and adding self before global variable names, as shown in Example 8-41.

Example 8-41. PP4EGuiTouruttonpics.py
from tkinter import *                # get base widget set
from glob import glob                # filename expansion list
import demoCheck                     # attach check button example to me
import random                        # pick a picture at random
gifdir = '../gifs/'                  # default dir to load GIF files

class ButtonPicsDemo(Frame):
    def __init__(self, gifdir=gifdir, parent=None):
        Frame.__init__(self, parent)
        self.pack()
        self.lbl = Label(self,  text="none", bg='blue', fg='red')
        self.pix = Button(self, text="Press me", command=self.draw, bg='white')
        self.lbl.pack(fill=BOTH)
        self.pix.pack(pady=10)
        demoCheck.Demo(self, relief=SUNKEN, bd=2).pack(fill=BOTH)
        files = glob(gifdir + "*.gif")
        self.images = [(x, PhotoImage(file=x)) for x in files]
        print(files)

    def draw(self):
        name, photo = random.choice(self.images)
        self.lbl.config(text=name)
        self.pix.config(image=photo)

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

This version works the same way as the original, but it can now be attached to any other GUI where you would like to include such an unreasonably silly button.

Viewing and Processing Images with PIL

As mentioned earlier, Python tkinter scripts show images by associating independently created image objects with real widget objects. At this writing, tkinter GUIs can display photo image files in GIF, PPM, and PGM formats by creating a PhotoImage object, as well as X11-style bitmap files (usually suffixed with an .xbm extension) by creating a BitmapImage object.

This set of supported file formats is limited by the underlying Tk library, not by tkinter itself, and may expand in the future (it has not in many years). But if you want to display files in other formats today (e.g., the popular JPEG format), you can either convert your files to one of the supported formats with an image-processing program or install the PIL Python extension package mentioned at the start of Chapter 7.

PIL, the Python Imaging Library, is an open source system that supports nearly 30 graphics file formats (including GIF, JPEG, TIFF, PNG, and BMP). In addition to allowing your scripts to display a much wider variety of image types than standard tkinter, PIL also provides tools for image processing, including geometric transforms, thumbnail creation, format conversions, and much more.

PIL Basics

To use its tools, you must first fetch and install the PIL package: see http://www.pythonware.com (or search for “PIL” on the web). Then, simply use special PhotoImage and BitmapImage objects imported from the PIL ImageTk module to open files in other graphic formats. These are compatible replacements for the standard tkinter classes of the same name, and they may be used anywhere tkinter expects a PhotoImage or BitmapImage object (i.e., in label, button, canvas, text, and menu object configurations).

That is, replace standard tkinter code such as this:

from tkinter import *
imgobj = PhotoImage(file=imgdir + "spam.gif")
Button(image=imgobj).pack()

with code of this form:

from tkinter import *
from PIL import ImageTk
photoimg = ImageTk.PhotoImage(file=imgdir + "spam.jpg")
Button(image=photoimg).pack()

or with the more verbose equivalent, which comes in handy if you will perform image processing in addition to image display:

from tkinter import *
from PIL import Image, ImageTk
imageobj = Image.open(imgdir + "spam.jpeg")
photoimg = ImageTk.PhotoImage(imageobj)
Button(image=photoimg).pack()

In fact, to use PIL for image display, all you really need to do is install it and add a single from statement to your code to get its replacement PhotoImage object after loading the original from tkinter. The rest of your code remains unchanged but will be able to display JPEG, PNG, and other image types:

from tkinter import *
from PIL.ImageTk import PhotoImage                 # <== add this line
imgobj = PhotoImage(file=imgdir + "spam.png")
Button(image=imgobj).pack()

PIL installation details vary per platform; on Windows, it is just a matter of downloading and running a self-installer. PIL code winds up in the Python install directory’s Libsite-packages; because this is automatically added to the module import search path, no path configuration is required to use PIL. Simply run the installer and import the PIL package’s modules. On other platforms, you might untar or unZIP a fetched source code archive and add PIL directories to the front of your PYTHONPATH setting; see the PIL system’s website for more details. (In fact, I am using a pre-release version of PIL for Python 3.1 in this edition; it should be officially released by the time you read these words.)

There is much more to PIL than we have space to cover here. For instance, it also provides image conversion, resizing, and transformation tools, some of which can be run as command-line programs that have nothing to do with GUIs directly. Especially for tkinter-based programs that display or process images, PIL will likely become a standard component in your software tool set.

See http://www.pythonware.com for more information, as well as online PIL and tkinter documentation sets. To help get you started, though, we’ll close out this chapter with a handful of real scripts that use PIL for image display and processing.

Displaying Other Image Types with PIL

In our earlier image examples, we attached widgets to buttons and canvases, but the standard tkinter toolkit allows images to be added to a variety of widget types, including simple labels, text, and menu entries. Example 8-42, for instance, uses unadorned tkinter to display a single image by attaching it to a label, in the main application window. The example assumes that images are stored in an images subdirectory, and it allows the image filename to be passed in as a command-line argument (it defaults to spam.gif if no argument is passed). It also joins file and directory names more portably with os.path.join, and it prints the image’s height and width in pixels to the standard output stream, just to give extra information.

Example 8-42. PP4EGuiPILviewer-tk.py
"""
show one image with standard tkinter photo object;
as is this handles GIF files, but not JPEG images; image filename listed in
command line, or default; use a Canvas instead of Label for scrolling, etc.
"""

import os, sys
from tkinter import *                    # use standard tkinter photo object
                                         # GIF works, but JPEG requires PIL
imgdir  = 'images'
imgfile = 'london-2010.gif'
if len(sys.argv) > 1:                    # cmdline argument given?
    imgfile = sys.argv[1]
imgpath = os.path.join(imgdir, imgfile)

win = Tk()
win.title(imgfile)
imgobj = PhotoImage(file=imgpath)        # display photo on a Label
Label(win, image=imgobj).pack()
print(imgobj.width(), imgobj.height())   # show size in pixels before destroyed
win.mainloop()

Figure 8-43 captures this script’s display on Windows 7, showing the default GIF image file. Run this from the system console with a filename as a command-line argument to view other files in the images subdirectory (e.g., python viewer_tk.py filename.gif).

tkinter GIF display
Figure 8-43. tkinter GIF display

Example 8-42 works, but only for image types supported by the base tkinter toolkit. To display other image formats, such as JPEG, we need to install PIL and use its replacement PhotoImage object. In terms of code, it’s simply a matter of adding one import statement, as illustrated in Example 8-43.

Example 8-43. PP4EGuiPILviewer-pil.py
"""
show one image with PIL photo replacement object
handles many more image types; install PIL first: placed in Libsite-packages
"""

import os, sys
from tkinter import *
from PIL.ImageTk import PhotoImage       # <== use PIL replacement class
                                         # rest of code unchanged
imgdir  = 'images'
imgfile = 'florida-2009-1.jpg'           # does gif, jpg, png, tiff, etc.
if len(sys.argv) > 1:
    imgfile = sys.argv[1]
imgpath = os.path.join(imgdir, imgfile)

win = Tk()
win.title(imgfile)
imgobj = PhotoImage(file=imgpath)        # now JPEGs work!
Label(win, image=imgobj).pack()
win.mainloop()
print(imgobj.width(), imgobj.height())   # show size in pixels on exit

With PIL, our script is now able to display many image types, including the default JPEG image defined in the script and captured in Figure 8-44. Again, run with a command-line argument to view other photos.

tkinter+PIL JPEG display
Figure 8-44. tkinter+PIL JPEG display

Displaying all images in a directory

While we’re at it, it’s not much extra work to allow viewing all images in a directory, using some of the directory path tools we met in the first part of this book. Example 8-44, for instance, simply opens a new Toplevel pop-up window for each image in a directory (given as a command-line argument or a default), taking care to skip nonimage files by catching exceptions—error messages are both printed and displayed in the bad file’s pop-up window.

Example 8-44. PP4EGuiPILviewer-dir.py
"""
display all images in a directory in pop-up windows
GIFs work in basic tkinter, but JPEGs will be skipped without PIL
"""

import os, sys
from tkinter import *
from PIL.ImageTk import PhotoImage          # <== required for JPEGs and others

imgdir = 'images'
if len(sys.argv) > 1: imgdir = sys.argv[1]
imgfiles = os.listdir(imgdir)               # does not include directory prefix

main = Tk()
main.title('Viewer')
quit = Button(main, text='Quit all', command=main.quit, font=('courier', 25))
quit.pack()
savephotos = []

for imgfile in imgfiles:
    imgpath = os.path.join(imgdir, imgfile)
    win = Toplevel()
    win.title(imgfile)
    try:
        imgobj = PhotoImage(file=imgpath)
        Label(win, image=imgobj).pack()
        print(imgpath, imgobj.width(), imgobj.height())      # size in pixels
        savephotos.append(imgobj)                            # keep a reference
    except:
        errmsg = 'skipping %s
%s' % (imgfile, sys.exc_info()[1])
        Label(win, text=errmsg).pack()

main.mainloop()

Run this code on your own to see the windows it generates. If you do, you’ll get one main window with a Quit button to kill all the windows at once, plus as many pop-up image view windows as there are images in the directory. This is convenient for a quick look, but not exactly the epitome of user friendliness for large directories! The sample images directory used for testing, for instance, has 59 images, yielding 60 pop-up windows; those created by your digital camera may have many more. To do better, let’s move on to the next section.

Creating Image Thumbnails with PIL

As mentioned, PIL does more than display images in a GUI; it also comes with tools for resizing, converting, and more. One of the many useful tools it provides is the ability to generate small, “thumbnail” images from originals. Such thumbnails may be displayed in a web page or selection GUI to allow the user to open full-size images on demand.

Example 8-45 is a concrete implementation of this idea—it generates thumbnail images using PIL and displays them on buttons which open the corresponding original image when clicked. The net effect is much like the file explorer GUIs that are now standard on modern operating systems, but by coding this in Python, we’re able to control its behavior and to reuse and customize its code in our own applications. In fact, we’ll reuse the makeThumbs function here repeatedly in other examples. As usual, these are some of the primary benefits inherent in open source software in general.

Example 8-45. PP4EGuiPILviewer_thumbs.py
"""
display all images in a directory as thumbnail image buttons that display
the full image when clicked; requires PIL for JPEGs and thumbnail image
creation;  to do: add scrolling if too many thumbs for window!
"""

import os, sys, math
from tkinter import *
from PIL import Image                   # <== required for thumbs
from PIL.ImageTk import PhotoImage      # <== required for JPEG display

def makeThumbs(imgdir, size=(100, 100), subdir='thumbs'):
    """
    get thumbnail images for all images in a directory; for each image, create
    and save a new thumb, or load and return an existing thumb;  makes thumb
    dir if needed;  returns a list of (image filename, thumb image object);
    caller can also run listdir on thumb dir to load;  on bad file types may
    raise IOError, or other;  caveat: could also check file timestamps;
    """
    thumbdir = os.path.join(imgdir, subdir)
    if not os.path.exists(thumbdir):
        os.mkdir(thumbdir)

    thumbs = []
    for imgfile in os.listdir(imgdir):
        thumbpath = os.path.join(thumbdir, imgfile)
        if os.path.exists(thumbpath):
            thumbobj = Image.open(thumbpath)            # use already created
            thumbs.append((imgfile, thumbobj))
        else:
            print('making', thumbpath)
            imgpath = os.path.join(imgdir, imgfile)
            try:
                imgobj = Image.open(imgpath)            # make new thumb
                imgobj.thumbnail(size, Image.ANTIALIAS) # best downsize filter
                imgobj.save(thumbpath)                  # type via ext or passed
                thumbs.append((imgfile, imgobj))
            except:                                     # not always IOError
                print("Skipping: ", imgpath)
    return thumbs

class ViewOne(Toplevel):
    """
    open a single image in a pop-up window when created;  photoimage
    object must be saved: images are erased if object is reclaimed;
    """
    def __init__(self, imgdir, imgfile):
        Toplevel.__init__(self)
        self.title(imgfile)
        imgpath = os.path.join(imgdir, imgfile)
        imgobj  = PhotoImage(file=imgpath)
        Label(self, image=imgobj).pack()
        print(imgpath, imgobj.width(), imgobj.height())   # size in pixels
        self.savephoto = imgobj                           # keep reference on me

def viewer(imgdir, kind=Toplevel, cols=None):
    """
    make thumb links window for an image directory: one thumb button per image;
    use kind=Tk to show in main  app window, or Frame container (pack);  imgfile
    differs per loop: must save with a default;  photoimage objs must be saved:
    erased if reclaimed; packed row frames (versus grids, fixed-sizes, canvas);
    """
    win = kind()
    win.title('Viewer: ' + imgdir)
    quit = Button(win, text='Quit', command=win.quit, bg='beige')   # pack first
    quit.pack(fill=X, side=BOTTOM)                                  # so clip last
    thumbs = makeThumbs(imgdir)
    if not cols:
        cols = int(math.ceil(math.sqrt(len(thumbs))))     # fixed or N x N

    savephotos = []
    while thumbs:
        thumbsrow, thumbs = thumbs[:cols], thumbs[cols:]
        row = Frame(win)
        row.pack(fill=BOTH)
        for (imgfile, imgobj) in thumbsrow:
            photo   = PhotoImage(imgobj)
            link    = Button(row, image=photo)
            handler = lambda savefile=imgfile: ViewOne(imgdir, savefile)
            link.config(command=handler)
            link.pack(side=LEFT, expand=YES)
            savephotos.append(photo)
    return win, savephotos

if __name__ == '__main__':
    imgdir = (len(sys.argv) > 1 and sys.argv[1]) or 'images'
    main, save = viewer(imgdir, kind=Tk)
    main.mainloop()

Notice how this code’s viewer must pass in the imgfile to the generated callback handler with a default argument; because imgfile is a loop variable, all callbacks will have its final loop iteration value if its current value is not saved this way (all buttons would open the same image!). Also notice we keep a list of references to the photo image objects; photos are erased when their object is garbage collected, even if they are currently being displayed. To avoid this, we generate references in a long-lived list.

Figure 8-45 shows the main thumbnail selection window generated by Example 8-45 when viewing the default images subdirectory in the examples source tree (resized here for display). As in the previous examples, you can pass in an optional directory name to run the viewer on a directory of your own (for instance, one copied from your digital camera). Clicking a thumbnail button in the main window opens a corresponding image in a pop-up window; Figure 8-46 captures one.

Simple thumbnail selection GUI, simple row frames
Figure 8-45. Simple thumbnail selection GUI, simple row frames
Thumbnail viewer pop-up image window
Figure 8-46. Thumbnail viewer pop-up image window

Much of Example 8-45’s code should be straightforward by now. It lays out thumbnail buttons in row frames, much like prior examples (see the input forms layout alternatives earlier in this chapter). Most of the PIL-specific code in this example is in the makeThumbs function. It opens, creates, and saves the thumbnail image, unless one has already been saved (i.e., cached) to a local file. As coded, thumbnail images are saved in the same image format as the original full-size photo.

We also use the PIL ANTIALIAS filter—the best quality for down-sampling (shrinking); this does a better job on low-resolution GIFs. Thumbnail generation is essentially just an in-place resize that preserves the original aspect ratio. Because there is more to this story than we can cover here, though, I’ll defer to PIL and its documentation for more details on that package’s API.

We’ll revisit thumbnail creation again briefly in the next chapter to create toolbar buttons. Before we move on, though, three variations on the thumbnail viewer are worth quick consideration—the first underscores performance concepts and the others have to do with improving on the arguably odd layout of Figure 8-45.

Performance: Saving thumbnail files

As is, the viewer saves the generated thumbnail image in a file, so it can be loaded quickly the next time the script is run. This isn’t strictly required—Example 8-46, for instance, customizes the thumbnail generation function to generate the thumbnail images in memory, but never save them.

There is no noticeable speed difference for very small image collections. If you run these alternatives on larger image collections, though, you’ll notice that the original version in Example 8-45 gains a big performance advantage by saving and loading the thumbnails to files. On one test with many large image files on my machine (some 320 images from a digital camera memory stick and an admittedly underpowered laptop), the original version opens the GUI in roughly just 5 seconds after its initial run to cache thumbnails, compared to as much as 1 minute and 20 seconds for Example 8-46: a factor of 16 slower. For thumbnails, loading from files is much quicker than recalculation.

Example 8-46. PP4EGuiPILviewer-thumbs-nosave.py
"""
same, but make thumb images in memory without saving to or loading from files:
seems just as fast for small directories, but saving to files makes startup much
quicker for large image collections; saving may be needed in some apps (web pages)
"""

import os, sys
from PIL import Image
from tkinter import Tk
import viewer_thumbs

def makeThumbs(imgdir, size=(100, 100), subdir='thumbs'):
    """
    create thumbs in memory but don't cache to files
    """
    thumbs = []
    for imgfile in os.listdir(imgdir):
        imgpath = os.path.join(imgdir, imgfile)
        try:
            imgobj = Image.open(imgpath)          # make new thumb
            imgobj.thumbnail(size)
            thumbs.append((imgfile, imgobj))
        except:
            print("Skipping: ", imgpath)
    return thumbs

if __name__ == '__main__':
    imgdir = (len(sys.argv) > 1 and sys.argv[1]) or 'images'
    viewer_thumbs.makeThumbs = makeThumbs
    main, save = viewer_thumbs.viewer(imgdir, kind=Tk)
    main.mainloop()

Layout options: Gridding

The next variations on our viewer are purely cosmetic, but they illustrate tkinter layout concepts. If you look at Figure 8-45 long enough, you’ll notice that its layout of thumbnails is not as uniform as it could be. Individual rows are fairly coherent because the GUI is laid out by row frames, but columns can be misaligned badly due to differences in image shape. Different packing options don’t seem to help (and can make matters even more askew—try it), and arranging by column frames would just shift the problem to another dimension. For larger collections, it could become difficult to locate and open specific images.

With just a little extra work, we can achieve a more uniform layout by either laying out the thumbnails in a grid, or using uniform fixed-size buttons. Example 8-47 positions buttons in a row/column grid by using the tkinter grid geometry manager—a topic we will explore in more detail in the next chapter, so like the canvas, you should consider some of this code to be a preview and segue, too. In short, grid arranges its contents by row and column; we’ll learn all about the stickiness of the Quit button here in Chapter 9.

Example 8-47. PP4EGuiPILviewer-thumbs-grid.py
"""
same as viewer_thumbs, but uses the grid geometry manager to try to achieve
a more uniform layout; can generally achieve the same with frames and pack
if buttons are all fixed and uniform in size;
"""

import sys, math
from tkinter import *
from PIL.ImageTk import PhotoImage
from viewer_thumbs import makeThumbs, ViewOne

def viewer(imgdir, kind=Toplevel, cols=None):
    """
    custom version that uses gridding
    """
    win = kind()
    win.title('Viewer: ' + imgdir)
    thumbs = makeThumbs(imgdir)
    if not cols:
        cols = int(math.ceil(math.sqrt(len(thumbs))))     # fixed or N x N

    rownum = 0
    savephotos = []
    while thumbs:
        thumbsrow, thumbs = thumbs[:cols], thumbs[cols:]
        colnum = 0
        for (imgfile, imgobj) in thumbsrow:
            photo   = PhotoImage(imgobj)
            link    = Button(win, image=photo)
            handler = lambda savefile=imgfile: ViewOne(imgdir, savefile)
            link.config(command=handler)
            link.grid(row=rownum, column=colnum)
            savephotos.append(photo)
            colnum += 1
        rownum += 1

    Button(win, text='Quit', command=win.quit).grid(columnspan=cols, stick=EW)
    return win, savephotos

if __name__ == '__main__':
    imgdir = (len(sys.argv) > 1 and sys.argv[1]) or 'images'
    main, save = viewer(imgdir, kind=Tk)
    main.mainloop()

Figure 8-47 displays the effect of gridding—our buttons line up in rows and columns in a more uniform fashion than in Figure 8-45, because they are positioned by both row and column, not just by rows. As we’ll see in the next chapter, gridding can help any time our displays are two-dimensional by nature.

Gridded thumbnail selection GUI
Figure 8-47. Gridded thumbnail selection GUI

Layout options: Fixed-size buttons

Gridding helps—rows and columns align regularly now—but image shape still makes this less than ideal. We can achieve a layout that is perhaps even more uniform than gridding by giving each thumbnail button a fixed size. Buttons are sized to their images (or text) by default, but we can always override this if needed. Example 8-48 does the trick. It sets the height and width of each button to match the maximum dimension of the thumbnail icon, so it is neither too thin nor too high. Assuming all thumbnails have the same maximum dimension (something our thumb-maker ensures), this will achieve the desired layout.

Example 8-48. PP4EGuiPILviewer-thumbs-fixed.py
"""
use fixed size for thumbnails, so align regularly; size taken from image
object, assume all same max; this is essentially what file selection GUIs do;
"""

import sys, math
from tkinter import *
from PIL.ImageTk import PhotoImage
from viewer_thumbs import makeThumbs, ViewOne

def viewer(imgdir, kind=Toplevel, cols=None):
    """
    custom version that lays out with fixed-size buttons
    """
    win = kind()
    win.title('Viewer: ' + imgdir)
    thumbs = makeThumbs(imgdir)
    if not cols:
        cols = int(math.ceil(math.sqrt(len(thumbs))))      # fixed or N x N

    savephotos = []
    while thumbs:
        thumbsrow, thumbs = thumbs[:cols], thumbs[cols:]
        row = Frame(win)
        row.pack(fill=BOTH)
        for (imgfile, imgobj) in thumbsrow:
            size    = max(imgobj.size)                     # width, height
            photo   = PhotoImage(imgobj)
            link    = Button(row, image=photo)
            handler = lambda savefile=imgfile: ViewOne(imgdir, savefile)
            link.config(command=handler, width=size, height=size)
            link.pack(side=LEFT, expand=YES)
            savephotos.append(photo)

    Button(win, text='Quit', command=win.quit, bg='beige').pack(fill=X)
    return win, savephotos

if __name__ == '__main__':
    imgdir = (len(sys.argv) > 1 and sys.argv[1]) or 'images'
    main, save = viewer(imgdir, kind=Tk)
    main.mainloop()

Figure 8-48 shows the results of applying a fixed size to our buttons; all are the same size now, using a size taken from the images themselves. The effect is to display all thumbnails as same-size tiles regardless of their shape, so they are easier to view. Naturally, other layout schemes are possible as well; experiment with some of the configuration options in this code on your own to see their effect on the display.

Fixed-size thumbnail selection GUI, row frames
Figure 8-48. Fixed-size thumbnail selection GUI, row frames

Scrolling and canvases (ahead)

The thumbnail viewer scripts presented in this section work well for reasonably sized image directories, and you can use smaller thumbnail size settings for larger image collections. Perhaps the biggest limitation of these programs, though, is that the thumbnail windows they create will become too large to handle (or display at all) if the image directory contains very many files.

Even with the sample images directory used for this book, we lost the Quit button at the bottom of the display in the last two figures because there are too many thumbnail images to show. To illustrate the difference, the original Example 8-45 packs the Quit button first for this very reason—so it is clipped last, after all thumbnails, and thus remains visible when there are many photos. We could do a similar thing for the other versions, but we’d still lose thumbnails if there were too many. A directory from your camera with many images might similarly produce a window too large to fit on your computer’s screen.

To do better, we could arrange the thumbnails on a widget that supports scrolling. The open source Pmw package includes a handy scrolled frame that may help. Moreover, the standard tkinter Canvas widget gives us more control over image displays (including placement by absolute pixel coordinates) and supports horizontal and vertical scrolling of its content.

In fact, in the next chapter, we’ll code one final extension to our script which does just that—it displays thumbnails in a scrolled canvas, and so it handles large collections much better. Its thumbnail buttons are fixed-size as in our last example here, but are positioned at computed coordinates. I’ll defer further details here, though, because we’ll study that extension in conjunction with canvases in the next chapter. And in Chapter 11, we’ll apply this technique to an even more full-featured image program called PyPhoto.

To learn how these programs do their jobs, though, we need to move on to the next chapter, and the second half of our widget tour.



[32] Historic anecdote: In a now-defunct tkinter release shipped with Python 1.3, you could also set and fetch variable values by calling them like functions, with and without an argument (e.g., var(value) and var()). Today, you call variable set and get methods instead. For unknown reasons, the function call form stopped working years ago, but you may still see it in older Python code (and in first editions of at least one O’Reilly Python book). If a fix made in the name of aesthetics breaks working code, is it really a fix?

[33] As we’ll see later in this book, exec can also be dangerous if it is running code strings fetched from users or network connections. That’s not an issue for the hardcoded strings used internally in this example.

[34] This particular image is not my creation; it appeared as a banner ad on developer-related websites such as Slashdot when the book Learning Python was first published in 1999. It generated enough of a backlash from Perl zealots that O’Reilly eventually pulled the ad altogether. Which may be why, of course, it later appeared in this book.

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

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