Tkinter入门
前页 |  | 后页

# Chapter 10. Dialog Windows

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("&lt;Return>", self.ok)
            self.bind("&lt;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.

前页 | Home | 后页
Data Entry | Up | Validating Data