pythonware.com products ::: library ::: search ::: daily Python-URL!
   

Dialog Windows

Back   Next   

 Chapter 10. Dialog Windows

Table of Contents
Grid Layouts
Validating Data

While the standard dialogs described in the previous section may be sufficient for many simpler applications, most larger applications require more complicated dialogs. For example, to set configuration parameters for an application, you will probably want to let the user enter more than one value or string in each dialog.

Basically, creating a dialog window is no different from creating an application window. Just use the Toplevel widget, stuff the necessary entry fields, buttons, and other widgets into it, and let the user take care of the rest. (By the way, don't use the ApplicationWindow class for this purpose; it will only confuse your users).

But if you implement dialogs in this way, you may end up getting both your users and yourself into trouble. The standard dialogs all returned only when the user had finished her task and closed the dialog; but if you just display another toplevel window, everything will run in parallel. If you're not careful, the user may be able to display several copies of the same dialog, and both she and your application will be hopelessly confused.

In many situations, it is more practical to handle dialogs in a synchronous fashion; create the dialog, display it, wait for the user to close the dialog, and then resume execution of your application. The wait_window method is exactly what we need; it enters a local event loop, and doesn't return until the given window is destroyed (either via the destroy method, or explicitly via the window manager):

    widget.wait_window(window)

(Note that the method waits until the window given as an argument is destroyed; the only reason this is a method is to avoid namespace pollution).

In the following example, the MyDialog class creates a Toplevel widget, and adds some widgets to it. The caller then uses wait_window to wait until the dialog is closed. If the user clicks OK, the entry field's value is printed, and the dialog is then explicitly destroyed.

Example 10-1. Creating a simple dialog

# File: dialog1.py

from Tkinter import *

class MyDialog:

    def __init__(self, parent):

        top = self.top = Toplevel(parent)

        Label(top, text="Value").pack()

        self.e = Entry(top)
        self.e.pack(padx=5)

        b = Button(top, text="OK", command=self.ok)
        b.pack(pady=5)

    def ok(self):

        print "value is", self.e.get()

        self.top.destroy()


root = Tk()
Button(root, text="Hello!").pack()
root.update()

d = MyDialog(root)

root.wait_window(d.top)

If you run this program, you can type something into the entry field, and then click OK, after which the program terminates (note that we didn't call the mainloop method here; the local event loop handled by wait_window was sufficient). But there are a few problems with this example:

  • The root window is still active. You can click on the button in the root window also when the dialog is displayed. If the dialog depends on the current application state, letting the users mess around with the application itself may be disastrous. And just being able to display multiple dialogs (or even multiple copies of one dialog) is a sure way to confuse your users.

  • You have to explicitly click in the entry field to move the cursor into it, and also click on the OK button. Pressning Enter in the entry field is not sufficient.

  • There should be some controlled way to cancel the dialog (and as we learned earlier, we really should handle the WM_DELETE_WINDOW protocol too).

To address the first problem, Tkinter provides a method called grab_set, which makes sure that no mouse or keyboard events are sent to the wrong window.

The second problem consists of several parts; first, we need to explicitly move the keyboard focus to the dialog. This can be done with the focus_set method. Second, we need to bind the Enter key so it calls the ok method. This is easy, just use the bind method on the Toplevel widget (and make sure to modify the ok method to take an optional argument so it doesn't choke on the event object).

The third problem, finally, can be handled by adding an additional Cancel button which calls the destroy method, and also use bind and protocol to do the same when the user presses Escape or explicitly closes the window.

The following Dialog class provides all this, and a few additional tricks. To implement your own dialogs, simply inherit from this class and override the body and apply methods. The former should create the dialog body, the latter is called when the user clicks OK.

Example 10-2. A dialog support class

# File: tkSimpleDialog.py

from Tkinter import *
import os

class Dialog(Toplevel):

    def __init__(self, parent, title = None):

        Toplevel.__init__(self, parent)
        self.transient(parent)

        if title:
            self.title(title)

        self.parent = parent

        self.result = None

        body = Frame(self)
        self.initial_focus = self.body(body)
        body.pack(padx=5, pady=5)

        self.buttonbox()

        self.grab_set()

        if not self.initial_focus:
            self.initial_focus = self

        self.protocol("WM_DELETE_WINDOW", self.cancel)

        self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
                                  parent.winfo_rooty()+50))

        self.initial_focus.focus_set()

        self.wait_window(self)

    #
    # construction hooks

    def body(self, master):
        # create dialog body.  return widget that should have
        # initial focus.  this method should be overridden

        pass

    def buttonbox(self):
        # add standard button box. override if you don't want the
        # standard buttons
        
        box = Frame(self)

        w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
        w.pack(side=LEFT, padx=5, pady=5)
        w = Button(box, text="Cancel", width=10, command=self.cancel)
        w.pack(side=LEFT, padx=5, pady=5)

        self.bind("<Return>", self.ok)
        self.bind("<Escape>", self.cancel)

        box.pack()

    #
    # standard button semantics

    def ok(self, event=None):

        if not self.validate():
            self.initial_focus.focus_set() # put focus back
            return

        self.withdraw()
        self.update_idletasks()

        self.apply()

        self.cancel()

    def cancel(self, event=None):

        # put focus back to the parent window
        self.parent.focus_set()
        self.destroy()

    #
    # command hooks

    def validate(self):

        return 1 # override

    def apply(self):

        pass # override

The main trickery is done in the constructor; first, transient is used to associate this window with a parent window (usually the application window from which the dialog was launched). The dialog won't show up as an icon in the window manager (it won't appear in the task bar under Windows, for example), and if you iconify the parent window, the dialog will be hidden as well. Next, the constructor creates the dialog body, and then calls grab_set to make the dialog modal, geometry to position the dialog relative to the parent window, focus_set to move the keyboard focus to the appropriate widget (usually the widget returned by the body method), and finally wait_window.

Note that we use the protocol method to make sure an explicit close is treated as a cancel, and in the buttonbox method, we bind the Enter key to OK, and Escape to Cancel. The default=ACTIVE call marks the OK button as a default button in a platform specific way.

Using this class is much easier than figuring out how it's implemented; just create the necessary widgets in the body method, and extract the result and carry out whatever you wish to do in the apply method. Here's a simple example (we'll take a closer look at the grid method in a moment).

Example 10-3. Creating a simple dialog, revisited

# File: dialog2.py

import tkSimpleDialog

class MyDialog(tkSimpleDialog.Dialog):

    def body(self, master):

        Label(master, text="First:").grid(row=0)
        Label(master, text="Second:").grid(row=1)

        self.e1 = Entry(master)
        self.e2 = Entry(master)

        self.e1.grid(row=0, column=1)
        self.e2.grid(row=1, column=1)
        return self.e1 # initial focus

    def apply(self):
        first = string.atoi(self.e1.get())
        second = string.atoi(self.e2.get())
        print first, second # or something

And here's the resulting dialog:

Figure 10-1. running the dialog2.py script

Note that the body method may optionally return a widget that should receive focus when the dialog is displayed. If this is not relevant for your dialog, simply return None (or omit the return statement).

The above example did the actual processing in the apply method (okay, a more realistic example should probably to something with the result, rather than just printing it). But instead of doing the processing in the apply method, you can store the entered data in an instance attribute:

        def apply(self):
            first = int(self.e1.get())
            second = int(self.e2.get())
            self.result = first, second
    
    d = MyDialog(root)
    print d.result

Note that if the dialog is cancelled, the apply method is never called, and the result attribute is never set. The Dialog constructor sets this attribute to None, so you can simply test the result before doing any processing of it. If you wish to return data in other attributes, make sure to initialize them in the body method (or simply set result to 1 in the apply method, and test it before accessing the other attributes).

 Grid Layouts

While the pack manager was convenient to use when we designed application windows, it may not be that easy to use for dialogs. A typical dialog may include a number of entry fields and check boxes, with corresponding labels that should be properly aligned. Consider the following simple example:

Figure 10-2. Simple Dialog Layout

To implement this using the pack manager, we could create a frame to hold the label "first:", and the corresponding entry field, and use side=LEFT when packing them. Add a corresponding frame for the next line, and pack the frames and the checkbutton into an outer frame using side=TOP. Unfortunately, packing the labels in this fashion makes it impossible to get the entry fields lined up, and if we use side=RIGHT to pack the entry field instead, things break down if the entry fields have different width. By carefully using width options, padding, side and anchor packer options, etc., we can get reasonable results with some effort. But there's a much easier way: use the grid manager instead.

This manager splits the master widget (typically a frame) into a 2-dimensional grid, or table. For each widget, you only have to specify where in this grid it should appear, and the grid managers takes care of the rest. The following body method shows how to get the above layout:

Example 10-4. Using the grid geometry maanager

# File: dialog3.py

def body(self, master):

    Label(master, text="First:").grid(row=0, sticky=W)
    Label(master, text="Second:").grid(row=1, sticky=W)

    self.e1 = Entry(master)
    self.e2 = Entry(master)

    self.e1.grid(row=0, column=1)
    self.e2.grid(row=1, column=1)

    self.cb = Checkbutton(master, text="Hardcopy")
    self.cb.grid(row=2, columnspan=2, sticky=W)

For each widget that should be handled by the grid manager, you call the grid method with the row and column options, telling the manager where to put the widget. The topmost row, and the leftmost column, is numbered 0 (this is also the default). Here, the checkbutton is placed beneath the label and entry widgets, and the columnspan option is used to make it occupy more than one cell. Here's the result:

Figure 10-3. Using the grid manager

If you look carefully, you'll notice a small difference between this dialog, and the dialog shown by the dialog2.py script. Here, the labels are aligned to the left margin. If you compare the code, you'll find that the only difference is an option called sticky.

When its time to display the frame widget, the grid geometry manager loops over all widgets, calculating a suitable width for each row, and a suitable height for each column. For any widget where the resulting cell turns out to be larger than the widget, the widget is centered by default. The sticky option is used to modify this behavior. By setting it to one of E, W, S, N, NW, NE, SE, or SW, you can align the widget to any side or corner of the cell. But you can also use this option to stretch the widget if necessary; if you set the option to E+W, the widget will be stretched to occupy the full width of the cell. And if you set it to E+W+N+S (or NW+SE, etc), the widget will be stretched in both directions. In practice, the sticky option replaces the fill, expand, and anchor options used by the pack manager.

The grid manager provides many other options allowing you to tune the look and behavior of the resulting layout. These include padx and pady which are used to add extra padding to widget cells, and many others. See the Grid Geometry Manager chapter for details.

Back   Next