对话框窗口

虽然上一节中描述的标准对话框对许多简单的应用程序来说已经够用了,但是大多数复杂的应用程序需要更复杂的对话框。例如腰围应用程序设置配置参数,你科宁希望让用户在每个对话框中输入多个值或者字符串。

基本上,创建对话框窗口与创建应用程序窗口没有区别,只要使用Toplevel组件,在其中填入必要的输入字段,按钮和其他组件,并让用户处理其余部分(顺便提一下,不要使用ApplicationWindow类来实现这个目的,它会混淆你的客户)

当然我们使用对话框窗体有一些技巧,不让会让自己陷入麻烦。只有当用户完成任务并关闭对话框时,在返回标准对话框;但是如果你只是显示另一个顶级窗口,那么所有窗口都讲并行运行,令用户困惑。

大多数情况下,更加实用的是以同步方式处理对话框:创建对话框,显示它,等待用户关闭对话框,然后恢复应用程序的执行。我们可以用wait_windows方法,这个方法进入一个本地事件循环,并且不返回,直到给定的窗口被销毁(通过destroy方法,或者明确的通过窗口管理器销毁)

widget.wait_window(window1)

在下面的例子中,MyDialog类创建了一个Toplevel组件,并向其添加了一些组件。然后调用者使用wait_window等待,直到对话框关闭。如果用户单击“确定”,将打印输入字段的值,然后显示销毁对话框。

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)

当运行这个程序,你可以在输入框中输入一些内容,点击OK按钮,接着程序就终止了(注意我们没有调用mainloop方法,本地的事件循环仅仅靠wait_window就能实现)但这个例子有以下问题:

1.根窗口依然被激活,你可以点击根窗口的按钮,而对话框依然显示,如果对话框依赖当前应用程序的状态,而用户乱操作应用程序本身就可能变成灾难,显示多个对话框也会混淆用户

2.你必须光标移到对话框中的输入框,还要单击确定按钮,比较繁琐

3.应该有一些受控的方式来取消对话框,比如我们前面学到的wm_delete_window协议

为了解决问题1.Tkinter提供了一个叫grab_set的方法,它确保不让鼠标或键盘事件发送到错误的窗口

第二个问题的解决办法有几个部分组成;首先,我们要明确的将键盘焦点移动到对话框,可以用focus_set方法来完成。其次我们要绑定Enter键,再调用ok方法。

第三个问题,我们可以添加一个额外的取消按钮来调用destroy方法,当然也可以使用协议来做

以下就是实现这些思想的新的Dialog类

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


主要的技巧就在构造函数中。首先使用了 transient使用(瞬态)函数,用于将这个窗口和他的父窗口(通常是启动对话框的应用程序窗口)相关联,该对话框不会再窗口管理器中显示为图标(例如,它不会显示在windows下的任务栏中),如果你对父窗口进行图标化,该对话框也会被隐藏。接下来狗仔函数创建对话框的body,接着调用grab_set 是对话框模块化,geometry函数相对父窗口定位对话框,focus_set把键盘焦点移动到合适的组件上(通常返回body的方法,如果重写的话),最后调用wait_window

注意文明使用protocol 方法来确保无论按cancel按钮还是其他显式的调用退出都会执行 cancel方法。我们还绑定了回车键 到OK回调函数,Escape键调用Cancel函数。我们使用default=ACTIVE使得OK 按钮作为默认激活的按钮。

使用这个类比刚才的解释要简单的多,只要在body方法中创建必要的组件,并提取结果,并在apply方法中执行你想要做的操作,这里有个简单的例子(稍后我们仔细讲讲网格法)

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 = int(self.e1.get())
        second = int(self.e2.get())

        print first, second # or something

以下是运行样子。

spacer.gif

注意,body方法的返回值是可选的,这个返回值是你希望获得焦点的那个控件,如果你觉得无所谓,你可以直接返回None或者干脆不返回

上面那个例子确实在apply方法中做了具体的操作(好吧,应该有比仅仅打印出来更好的操作),但除了操作之外,你应该把从文本框中得到的输入值保存在实例的属性中。比如这样:

    ...

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

d = MyDialog(root)

print d.result

注意,如果对话框取消的话,apply方法不会被调用,result属性也不会被设置。Dialog类的构造函数会把它设置为None,所以在使用result之前,你可以简单的测试一下。如果你希望返回其他属性,确保在body方法中初始化了,或者在apply方法中简单的把result设置为1,并且在使用其他属性之前测试它)

Grid布局

当我们设计应用程序的时候使用pack 比较方便,但在做这个对话框窗体就不太好用了。一个典型的对话框窗口包含大量的输入框和检查框,要对齐比较难。比如下面这个例子

Simple Dialog Layout

8d6be299bb44b167f1e62531f57a773f.gif

如果使用pack管理,我们要做很多很复杂,现在我们有更方便的办法了grid

Grid把父窗口网格化,每一个控件放在一个格子里

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)

这段代码使用了grid方法中的row和column参数,告诉管理器在哪个格子里放控件,最小行和最小列从0开始。在这里检查框放置在Label和Entry的下面,columnspan参数表示这个控件占据了多列。下面就是样子了:

spacer.gif

如果你仔细观察,你会发现刚才这一段代码和那个类中的代码不同,label文本紧贴左边,比较代码你会发现差别就在代码中的sticky。

当显示组件时,grid管理器循环遍历所有组件,计算每行核实的宽度,以及每列核实高度,对于单元格大于组件的,默认组件在格子中间,这个sticky选项就能靠某一边,可以考哪些边呢?可以靠这些: EWSNNWNE SESW中的一个,如果设置为E+W,那么就会被横向拉伸

其他详细信息看grid 管理器这章

验证数据

如果用户的输入非法怎么办,在下面的例子中,如果输入框写入的内容不是×××,appley方法将会提交一个异常

    ...

    def apply(self):
        try:
            first = int(self.e1.get())
            second = int(self.e2.get())
            dosomething((first, second))
        except ValueError:
            tkMessageBox.showwarning(
                "Bad input",
                "Illegal values, please try again"

            )

这个解决拌饭有个问题,当apply方法被调用时,ok方法已经关闭了对话框,而且当我们返回时对话框都已经销毁了。这是故意这么设计的,为什么呢?当我们在apply方法中执行一个比较耗时的任务,那么当我们完成之前对话框都没有移除的话,我们就会觉得很奇怪。在这个Dialog勒种其实已经预留下了解决办法,一个独立的validate方法,用这个来判断对话框是否被移除了。

在以下的例子中,我们仅仅把代码从apply中转椅到validate中,并且把结果保存在这个实例的属性中。然后再在apply方法中处理这些工作

    ...

    def validate(self):
        try:
            first= int(self.e1.get())
            second = int(self.e2.get())
            self.result = first, second
            return 1
        except ValueError:
            tkMessageBox.showwarning(
                "Bad input",
                "Illegal values, please try again"
            )
            return 0

    def apply(self):
        dosomething(self.result)