The dialog
demo launcher bar displays standard dialogs and can be
made to display others by simply changing thedialogTable
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 thestdout
standard output stream.
Example 8-10. PP4E\Gui\Tour\demoDlg-print.py
"""
similar, but show return values of dialog calls; the lambda saves data from
the local scope to be passed to the handler (button press handlers normally
get no arguments, and enclosing scope references don't work for loop variables)
and works just like a nested def statement: def func(key=key): self.printit(key)
"""
from tkinter import * # get base widget set
from dialogTable import demos # button callback handlers
from quitter import Quitter # attach a quit object to me
class Demo(Frame):
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.pack()
Label(self, text="Basic demos").pack()
for key in demos:
func = (lambda key=key: self.printit(key))
Button(self, text=key, command=func).pack(side=TOP, fill=BOTH)
Quitter(self).pack(side=TOP, fill=BOTH)
def printit(self, name):
print(name, 'returns =>', demos[name]()) # fetch, call, print
if __name__ == '__main__': Demo().mainloop()
This script builds the same main button-bar window, but notice
that the callback handler is an anonymous function made with a lambda
now, not a direct reference to dialog calls in the importeddialogTable
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 requiredname
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 bothself
andkey
in the enclosing method’s local scope.
In all recent Pythons, the reference toself
just works because of the enclosing
function scope lookup rules, but we need to passkey
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. Becauseself
won’t change after the
function is made, we can rely on the scope lookup rules for that name,
but not for loop variables likekey
.
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 forself
,
though we still need a default for thekey
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 nesteddef
statement here; in practice, though, the
lambda could appear within the call toButton
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:\...\PP4E\Gui\Tour>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 thebg
andfg
widget color configuration options we met
earlier. That is,bg
andfg
accept both a color name (e.g.,blue
) and anaskcolor
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 widgetconfig
methods in callback
handlers, as in
Example 8-11
.
Example 8-11. PP4E\Gui\Tour\setcolor.py
from tkinter import *
from tkinter.colorchooser import askcolor
def setBgColor():
(triple, hexstr) = askcolor()
if hexstr:
print(hexstr)
push.config(bg=hexstr)
root = Tk()
push = Button(root, text='Set Background Color', command=setBgColor)
push.config(height=3, font=('times', 20, 'bold'))
push.pack(expand=YES, fill=BOTH)
root.mainloop()
This script creates the window in
Figure 8-16
when launched (its button’s
background is a sort of green, but you’ll have to trust me on this).
Pressing the button pops up the color selection dialog shown earlier;
the color you pick in that dialog becomes the background color of this
button after you press OK.
Figure 8-16. setcolor main window
Color strings are also printed to thestdout
stream (the console window); run this
on your computer to experiment with available color settings:
C:\...\PP4E\Gui\Tour>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 thedialogTable
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:\Python31\Lib
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
tkinterdialog
module. This is a bit
dated now, and it uses an X Windows look-and-feel; but just in case you
run across such code in your Python maintenance excursions,
Example 8-12
gives you a feel for
the interface.
Example 8-12. PP4E\Gui\Tour\dlg-old.py
from tkinter import *
from tkinter.dialog import Dialog
class OldDialogDemo(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
Pack.config(self) # same as self.pack()
Button(self, text='Pop1', command=self.dialog1).pack()
Button(self, text='Pop2', command=self.dialog2).pack()
def dialog1(self):
ans = Dialog(self,
title = 'Popup Fun!',
text = 'An example of a popup-dialog '
'box, using older "Dialog.py".',
bitmap = 'questhead',
default = 0, strings = ('Yes', 'No', 'Cancel'))
if ans.num == 0: self.dialog2()
def dialog2(self):
Dialog(self, title = 'HAL-9000',
text = "I'm afraid I can't let you do that, Dave...",
bitmap = 'hourglass',
default = 0, strings = ('spam', 'SPAM'))
if __name__ == '__main__': OldDialogDemo().mainloop()
If you supplyDialog
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 theDialog
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
.
Figure 8-17. Old-style dialog
This is running on Windows, and as you can see, it is nothing like
what you would expect on that platform for a question dialog. In fact,
this dialog generates an X Windows look-and-feel, regardless of the
underlying platform. Because of bothDialog
’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 aTop
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.
Example 8-13. PP4E\Gui\Tour\dlg-custom.py
import sys
from tkinter import *
makemodal = (len(sys.argv) > 1)
def dialog():
win = Toplevel() # make a new window
Label(win, text='Hard drive reformatted!').pack() # add a few widgets
Button(win, text='OK', command=win.destroy).pack() # set destroy callback
if makemodal:
win.focus_set() # take over input focus,
win.grab_set() # disable other windows while I'm open,
win.wait_window() # and wait here until win destroyed
print('dialog exit') # else returns right away
root = Tk()
Button(root, text='popup', command=dialog).pack()
root.mainloop()
This script is set up to create a pop-up dialog window in either
modal or nonmodal mode, depending on itsmakemodal
global variable. If it is run with
no command-line arguments, it picks nonmodal style, captured in
Figure 8-18
.
Figure 8-18. Nonmodal custom dialogs at work
The window in the upper right is the root window here; pressing
its “popup” button creates a new pop-up dialog window. Because dialogs
are nonmodal in this mode, the root window remains active after a dialog
is popped up. In fact, nonmodal dialogs never block other windows, so
you can keep pressing the root’s button to generate as many copies of
the pop-up window as will fit on your screen. Any or all of the pop ups
can be killed by pressing their OK buttons, without killing other
windows in this display.
Now, when the script is run with a command-line argument (e.g.,python dlg-custom.py
), it makes its pop ups modal instead. Because modal
1
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
.
Figure 8-19. A modal custom dialog at work
In fact, the call to thedialog
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 synonymfocus
, and it’s also common to set the
focus on an input widget within the dialog (e.g., anEntry
) 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 thewin
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 thedestroy
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’sdestroy
method. In user-input dialogs, we might instead install an Enter
key-press callback handler that fetches data typed into anEntry
widget and then callsdestroy
(see later in this chapter).
Modal dialogs are typically implemented by waiting for a newly
created pop-up window’sdestroy
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’sdeiconify
andwithdraw
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
thewait_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 themainloop
method recursively, the call won’t
return until the widgetquit
method
has been invoked. Thequit
method
terminates amainloop
call, and so
normally ends a GUI program. But it will simply exit a recursivemainloop
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 ofdlg-custom
.
Example 8-14. PP4E\Gui\Tour\dlg-recursive.py
from tkinter import *
def dialog():
win = Toplevel() # make a new window
Label(win, text='Hard drive reformatted!').pack() # add a few widgets
Button(win, text='OK', command=win.quit).pack() # set quit callback
win.protocol('WM_DELETE_WINDOW', win.quit) # quit on wm close too!
win.focus_set() # take over input focus,
win.grab_set() # disable other windows while I'm open,
win.mainloop() # and start a nested event loop to wait
win.destroy()
print('dialog exit')
root = Tk()
Button(root, text='popup', command=dialog).pack()
root.mainloop()
If you go this route, be sure to callquit
rather thandestroy
in dialog callback handlers
(destroy
doesn’t terminate themainloop
level), and be sure to useprotocol
to make the window border
close button callquit
too (or else
it won’t end the recursivemainloop
level call and may generate odd error messages when your program
finally exits). Because of this extra complexity, you’re probably
better off usingwait_window
orwait_
variable
, not recursivemainloop
calls.
We’ll see how to build form-like dialogs with labels and input
fields later in this chapter when we meetEntry
, and again when we study thegrid
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
.