Dialogs






Dialogs

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


Modal

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


Nonmodal

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

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

Standard (Common) Dialogs

Because standard dialog calls are simpler, let's start here first. Tkinter comes with a collection of precoded dialog windows that implement many of the most common pop ups programs generatefile 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 Figure.

PP3E\Gui\Tour\dlg1.pyw

from Tkinter import *
from tkMessageBox 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.

dlg1 main window: buttons to trigger pop ups


When you press this window's Quit button, the dialog in Figure pops up by calling the standard askyesno function in the tkmessagebox module. This looks different on Unix and Macintosh systems, but it looks like you'd expect when run on Windows. This dialog 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 by calling showwarning.

dlg1 askyesno dialog (Windows)


dlg1 showwarning dialog


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

dlg1 showinfo dialog


dlg1 showerror dialog


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

A "smart" and reusable Quit button

Let's put some of these canned dialogs to better use. Figure 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.

PP3E\Gui\Tour\xd5 uitter.py

#############################################
# a Quit button that verifies exit requests;
# to reuse, attach an instance to other GUIs
#############################################

from Tkinter import *                          # get widget classes
from tkMessageBox 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)
    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 shows the Quit button itself in the upper left, and the askokcancel verification dialog that pops up when Quit is pressed.

Quitter, with askokcancel dialog


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

A dialog demo launcher bar

So far, we've seen a handful of standard dialogs, but there are quite a few more. Instead of just throwing these up in dull screenshots, though, let's write a Python demo script to generate them on demand. Here's one way to do it. First of all, in Figure 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).

PP3E\Gui\Tour\dialogTable.py

# define a name:callback demos table

from tkFileDialog   import askopenfilename        # get standard dialogs
from tkColorChooser import askcolor               # they live in Lib/lib-tk
from tkMessageBox   import askquestion, showerror
from tkSimpleDialog import askfloat

demos = {
    'Open':  askopenfilename,
    'Color': askcolor,
    'Query': lambda: askquestion('Warning', 'You typed "rm *"\nConfirm?'),
    '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 Figure, which simply generates buttons for all of this table's entriesuse its keys as button labels and its values as button callback handlers.

PP3E\Gui\Tour\demoDlg.py

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, 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 when run as a standalone program; it's a bar of demo buttons that simply route control back to the values of the table in the module dialogTable when pressed.

demoDlg main window


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

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.

demoDlg query, askquestion dialog


This askquestion dialog looks like the askyesno we saw earlier, but actually it returns either string "yes" or "no" (askyesno and askokcancel return 1 or 0, TRue or false). Pressing the demo bar's Input button generates the standard askfloat dialog box shown in Figure.

demoDlg input, askfloat dialog


This dialog automatically checks the input for valid floating-point syntax before it returns, and 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. This is Windows' look-and-feel; it looks radically different on Linux, but appropriately so.

demoDlg open, askopenfilename dialog


A similar dialog for selecting a save-as filename is produced by calling asksaveasfilename (see the Text widget section in Chapter 10 for an 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 argumenta set of name patterns used to select files, which appear in the "Files of type" pull down at 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.

Another common dialog call in the tkFileDialog 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 12, but feel free to flip ahead for more details now. The directory selection dialog will show up in the PyPhoto example in Chapter 12 and the PyMailGUI example in Chapter 15; 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.

demoDlg color, askcolor dialog


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

Printing dialog results (and passing callback data with lambdas)

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

PP3E\Gui\Tour\demoDlg-print.py

##########################################################################
# same, 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 works just like a nested def
# statement of this form: 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, value) in demos.items( ):
            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. 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. 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 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 functionsthe value it has after the last loop iteration. As we learned in Chapter 8, 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 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 technique, though we still need a default for the 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 can 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))

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

You can also use a callable class object here that retains state as instance attributes (see the tutorial's _ _call_ _ example in Chapter 8 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 required variable may change before the callback occurs.

When run, this script prints dialog return values; here is the output after clicking all the demo buttons in the main window and picking both Cancel/No and OK/Yes buttons in each dialog:

C:\...\PP3E\Gui\Tour>python demoDlg-print.py
Error returns => ok
Input returns => None
Input returns => 3.14159
Open returns =>
Open returns => C:/PP2ndEd/examples/PP3E/Gui/Tour/demoDlg-print.py
Query returns => no
Query returns => yes
Color returns => (None, None)
Color returns => ((160, 160, 160), '#a0a0a0')

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

Letting users select colors on the fly

The standard color selection dialog isn't just another pretty facescripts 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 result string that starts with a # (e.g., the #a0a0a0 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 Figure.

PP3E\Gui\Tour\setcolor.py

from Tkinter import *
from tkColorChooser 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 when launched (its button's background is a sort of green, but you'll have to trust me on this). Pressing the button pops up the color selection dialog shown earlier; the color you pick in that dialog becomes the background color of this button after you press OK.

setcolor main window


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

C:\...\PP3E\Gui\Tour>python setcolor.py
#c27cc5
#5fe28c
#69d8cd

Other standard dialog calls

We've seen most of the standard dialogs and will 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; all are simple Python files installed in the lib-tk subdirectory of the Python source library on your machine. And keep this demo bar example filed away for future reference; we'll reuse it later in the tour when we meet other button-like widgets.

The Old-Style Dialog Module

In older Python code, you may see dialogs occasionally coded with the standard 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, Figure gives you a feel for the interface.

PP3E\Gui\Tour\dlg-old.py

from Tkinter import *
from 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.

Old-style dialog


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

Custom Dialogs

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

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

PP3E\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 its makemodal global variable. If it is run with no command-line arguments, it picks nonmodal style, captured in Figure.

Nonmodal custom dialogs at work


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

Making custom dialogs modal

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

A modal custom dialog at work


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

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


win.focus_set( )

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


win.grab_set( )

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


win.wait_window( )

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

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

Other ways to be modal

Modal dialogs are typically implemented by waiting for a newly created pop-up window's destroy event, as in this example. But other schemes are viable too. For example, it's possible to create dialog windows ahead of time, and show and hide them as needed with the top-level window's deiconify and withdraw methods (see the alarm scripts near the end of Chapter 10 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 discussion of Tkinter variables (which are class objects, not normal Python variables), and the wait_variable method discussed near the end of Chapter 10, 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, Figure works the same way as dlg-custom.

PP3E\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 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 will 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 10. For more custom dialog examples, see ShellGui (Chapter 11), PyMailGUI (Chapter 15), PyCalc (Chapter 21), and the nonmodal form.py (Chapter 13). Here, we're moving on to learn more about events that will prove to be useful currency at later tour destinations.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows