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.
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.
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.
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.
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:
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.
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.
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.
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:
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.
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
.
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.
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.
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.
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.
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.
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.
The two Toplevel
windows on
the right are full-fledged windows; they can be independently
iconified, maximized, and so on. Toplevel
s 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
Top
level
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 Toplevel
s 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.
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. Top
level
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.
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.
Both Tk
and Toplevel
widgets export extra methods and features tailored for their
top-level role, as illustrated in Example 8-5.
""" 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
Top
level
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).
There are a few operational details worth noticing here, all of which are more obvious if you run this script on your machine:
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
.
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
.
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
.
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'
.
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.
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:
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.
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)
). Toplevel
s 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 Top
level
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 are windows popped up by a script to provide or request additional information. They come in two flavors, modal and nonmodal:
These dialogs block the rest of the interface until the dialog window is dismissed; users must reply to the dialog before the program continues.
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 Toplevel
s and other
kinds of widgets. Let’s explore the basics of all three
schemes.
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).
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.
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
.
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.
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.
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.
""" 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.
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.
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).
# 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.
"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.
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 dialog
Table
(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.
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.
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.
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.
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 (None
s of
the Python variety, that is).
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.
""" 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.
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.
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.
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
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.
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.
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.
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.
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 Top
level
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.
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.
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.
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.
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).
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
.
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.
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.
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.
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
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.
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.
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.
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.
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.
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()
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).
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.
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.
""" 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()
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.
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.
# 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.
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.
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.
""" 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
StringVar
s 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.
# 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 StringVar
s 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.
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).
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.
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 Checkbutton
s implements a
multiple-choice interface by assigning each button a variable of
its own.
A collection of Radiobutton
s 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 Checkbutton
s and attaches
an instance of the verifying Quitter button we built earlier in the
tour.
"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 Check
button
s 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 Checkbutton
s—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
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.
# 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.
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.
# 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 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.
"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.
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
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.
# 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.
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.
""" 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.
# 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.
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.
# 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 (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 option—get
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.
"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).
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
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.
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
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.
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.
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.
""" 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).
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
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.
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.
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.
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.
""" 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.
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.
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.
""" 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.
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.
""" 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.
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.
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:
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.
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.
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.
""" 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.
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.
In tkinter, graphical images are displayed by creating independent PhotoImage
or Bitmap
Image
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.
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.
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):
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()
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.
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()
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:
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.
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.
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.
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.
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]
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.
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.
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.
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 Bitmap
Image
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.
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.
""" 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).
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.
""" 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.
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.
""" 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.
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.
""" 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.
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.
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.
""" 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()
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.
""" 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.
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.
""" 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.
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.