8.2.3 常见 GUI 构件的用法

本节介绍一些最常见的 GUI 构件的用法。为了便于讨论,我们使用 Python 的命令行界 面①来交互式地执行语句,这样可以执行一条语句就立即看到其执行效果。读者也可以一边 阅读本书,一边在计算机上动手练习。

GUI 编程首先要做的是导入 Tkinter 模块并创建根窗口:

from Tkinter import * 
root = Tk()

可以看到,屏幕上立即出现一个根窗口,接下去就可以向根窗口中加入子构件了。

当所有构件添加完毕,通过执行下列语句来启动主循环:

root.mainloop()

这时可以看到 Python 命令行界面的>>>提示符没有了,表明现在 Tkinter 程序接管了控制权。 当最后关闭根窗口时,所有构件也都同时撤销。 下面演示各种构件的用法时,我们既可以在同一个根窗口中演示多个构件的用法,也可以每次演示新构件时重新创建根窗口。由于这两种做法并不影响本节的主旨,为了行文简洁, 我们对此不加区别,以上这几个导入模块、创建根窗口和进入主循环的步骤也不重复列出, 读者练习时可根据需要补上有关步骤。

标签

标签用于在窗口中显示文本信息,Tkinter 定义了类 Label 来支持标签构件的创建。创建 标签时需要指定父构件和文本内容,前者由 Label 构造器的第一个参数指定,后者用属性 text 指定。例如前面已经见过的:

>>> aLabel = Label(root,text="Hello World")

这条语句创建了一个标签构件实例,但该构件在窗口中仍然不可见。为使构件在窗口中 可见,需要调用布局管理器对该构件进行位置安排。仍然如前面已经见过的,下面这条语句 对标签对象 aLabel 调用方法 pack,意为用 Pack 布局管理器来安排这个标签的位置:

① Python 的 GUI 环境 IDLE 本身是用 Tkinter 写的程序,因此无法在 IDLE 中交互式地创建 Tkinter 窗口和其 他构件。

>>> aLabel.pack()

于是标签在根窗口中得到显示,同时根窗口的大小也改变了——变成刚好可以放置新加入的 标签构件(参见图 8.4)。这正是 Pack(“打包”)的效果,即所有东西以紧凑的方式布置。Pack 布局管理器简单易用,但不适合进行复杂布局,我们后面会介绍其他布局管理器。

标签构件除了 text 属性之外,还有其他许多属性。上例中只为标签的 text 属性提供了值 “Hello World”,其他属性(如字体、颜色等)都使用缺省值。下面这条语句为标签的更多属 性设置属性值:

>>> Label(root,text='Text in Red',fg='red',width=40).pack()

其中属性 fg 和 width 分别表示标签文本的颜色和标签的宽度,效果见图 8.6:

图 8.6 标签构件

注意,上面这条语句采用了将对象创建和对象方法调用结合在一起的语法①:先用构造器 Label(…)创建一个标签对象,然后直接对这个对象调用方法 pack(),而不是先定义一个变 量作为新建对象的引用,然后通过变量引用来调用对象方法。显然,对象创建与对象方法调 用合并的写法不适合需要多次引用一个对象的场合。另外,初学者容易犯的一个错误是写成:

w = Label(...).pack()

这条语句实际上是将 pack 的返回结果(None)赋值给 w,而非 Label(…)的返回结果(新建 的标签对象)。为了避免这类微妙的错误,最好将创建构件和构件布局的语句分开。

如果希望在程序运行过程中改变标签的文本内容,可以利用 8.2.2 中介绍的设置构件对象 属性的方法来重新设置标签对象的 text 属性值。很多 GUI 应用程序的窗口底部都有一个状态 栏(例如 Word 窗口的状态栏),用来显示程序的一些状态信息,这可以用标签构件来实现, 状态的变化可以通过修改标签对象的 text 属性值来实现。

按钮

按钮也叫命令按钮,是界面中的一个矩形区域(通常长度大于高度),矩形内部是描述性 的标题(例如常见的“确认”和“取消”)。对按钮的操作是用鼠标点击,其作用是命令程序 去执行预定义的操作。按钮可以说是图形界面中最常见的构件,是用户命令程序执行某项任 务的基本手段。

下列语句在根窗口 root 中创建了一个按钮构件:

>>> quitButton = Button(root,text="Quit",command=root.quit)

对按钮构件来说是最重要的属性是 command,它用于将按钮与某个函数或方法进行关 联。传递给 command 的值是一个函数(或方法),该函数就是点击按钮时要执行的操作。上 例中将按钮与根窗口 root 的内建方法 quit 相关联,其功能是退出主循环。注意,传递给 command 属性的是函数对象,函数名后不能加括号,切记 f 是函数对象,而 f()是函数调用(的 结果)。

按钮构件在窗口中的位置也需要用布局管理器来安排,例如用 pack 布局管理器:

>>> quitButton.pack()

从显示结果(图 8.7)可以看到,与前述标签的情形一样,quitButton 按钮在窗口中以紧凑方 式布置并变得可见。由于 pack 方法是在 Tkinter 的基类中定义的,而所有构件都是这个基类 的子类,从而都继承了这个方法,所以对标签和按钮以及其他各种构件都可以调用 pack 方法。

① 参见第 7 章。

图 8.7 按钮构件

为了验证按钮的功能,我们先进入主循环:

>>> root.mainloop()

然后点击 Quit 按钮,可以看到又回到了 Python 解释器的提示符状态,这就是 root.quit 方法 的作用。

实际应用程序中通常由程序员自己定义与按钮相关联的函数,以实现某种操作。下面这 个例子为按钮定义了一个简单的显示信息的函数:

>>> def hiButton():
...     print 'hi there'
...
>>> Button(root,text='print',command=hiButton).pack()

如果点击按钮,会看到在控制台显示信息“hi there”。 按钮构件还有其他许多属性,如宽度、文本颜色、按钮边框的 3D 风格、活动状态等等,

更多细节可参看本章后面的附录。

勾选钮

勾选钮也称为勾选框或复选框,用于向用户提供一个选项,用户对该选项有“选中”或 “不选”两种选择。勾选钮在外观上由一个小方框和一个相邻的描述性标题组成,未选中时 方框为空白,选中时方框中出现勾号,再次选择一个已打勾的勾选钮将取消选择。对勾选钮 的选择操作一般是用鼠标点击小方框或标题。Tkinter 的类 Checkbutton 实现了勾选钮构件, 其最简单的用法如下:

>>> Checkbutton(root,text="A Choice").pack()

如果程序中需要查询和设置选项的状态,可以将勾选钮与一个控制变量关联。控制变量 是 IntVar 类的实例,值为 1 或 0,分别对应选中和未选中状态。用法如下:

>>> v = IntVar()
>>> Checkbutton(root,text="A Choice",variable=v).pack()

程序中可以通过 v.get()和 v.set()来查询或设置勾选钮的状态。 在实际应用中,通常将多个勾选钮组合为一组,为用户提供多个相关的选项,用户可以选中 0 个、1 个或多个选项。例如:

>>> Checkbutton(root,text="Math").pack()
>>> Checkbutton(root,text="Python").pack()
>>> Checkbutton(root,text="English").pack()

执行效果如图 8.8 所示。为了在程序中获得各勾选钮的状态,可以为每个勾选钮关联一个不 同的控制变量。

图 8.8 勾选钮构件

图 8.8 中各勾选钮排列的不太美观,这是因为 pack 在布局时默认采用了居中对齐方式。 布局管理器提供了其他对齐方式,详见 8.2.4。此外,在实际 GUI 设计中,为了表示一组勾 选钮的相关性,通常会用一个框架容器将它们组合起来(参见图 8.11)。

单选钮

和勾选钮类似,单选钮也是列出选项供用户选择,并且通常也是由若干个单选钮构成一 组来提供多个相关的选项。但与勾选钮不同的是,同组的单选钮在任意时刻只能有一个被选 中;每当换选其他单选钮时,原先选中的单选钮即被取消①。

单选钮的外观是一个小圆框加上相邻的描述性标题,未选中时圆框内是空白,选中时圆 框中出现一个圆点。对单选钮的选择操作是用鼠标点击小圆框或标题。Tkinter 提供的 Radiobutton 类可支持单选钮的创建。例如最简单的单选钮可用如下语句创建:

>>> Radiobutton(root,text="One").pack()

如上所述,实际应用中都是将若干个相关的单选钮组合成一个组,使得每次只能有一个 单选钮被选中。为了实现单选钮的组合,可以先创建一个 IntVar 或 StringVar 类型的控制变量, 然后将同组的每个单选钮的 variable 属性都设置成该控制变量。由于多个单选钮共享一个控 制变量,而控制变量每次只能取一个值,所以选中一个单选钮就会导致取消另一个。

为了在程序中获取当前被选中的单选钮的信息,可以为同组中的每个单选钮设置 value 属性的值。这样,当选中一个单选钮时,控制变量即被设置为它的 value 值,程序中即可通 过控制变量的当前值来判断是哪个单选钮被选中了。还要注意,value 属性的值应当与控制变 量的类型匹配:如果控制变量是 IntVar 类型,则应为每个单选钮赋予不同的整数值;如果控 制变量是 StringVar,则应为每个单选钮赋予不同的字符串值。下面这个例子将三个单选钮组 合成一组:

>>> v = IntVar()
>>> v.set(1)
>>> Radiobutton(root,text="One",variable=v,value=1).pack()
>>> Radiobutton(root,text="Two",variable=v,value=2).pack()
>>> Radiobutton(root,text="Three",variable=v,value=3).pack()

其中三个单选钮共享一个 IntVar 型的控制变量 v,且分别设置了值 1、2、3。第二行 v.set(1) 的作用是设置初始默认选项。上述语句序列的执行效果如图 8.9 所示。另外,图中三个单选 钮是居中对齐,不太美观,可以用布局管理器设置对齐方式,详见 8.2.4。

图 8.9 单选钮构件

文本编辑区

文本编辑区是允许用户输入和编辑数据的区域,用户使用键盘在这个区域中输入文本,

输入过程中随时可以进行编辑,如光标定位、修改、插入等。有的文本编辑区只允许输入一 行文本,有的则允许输入多行。文本编辑区一般可以设置行和列的大小,并且可以通过滚动 条来显示、编辑更多的文本。

① 单选钮的英文是 radio button,名称源自收音机中预置电台按钮,选一个台的同时就取消了另一个台。

Tkinter 提供的 Entry 类可以实现单行文本的输入和编辑,不妨称之为录入框。下面的语 句创建并布置一个录入框构件:

>>> Entry(root).pack()

运行结果是在窗口中出现一行空白区域,点击此区域后会出现一个闪烁的光标,这时就可以 在其中输入文本了。图 8.10 是输入了一些文本后的效果:

图 8.10 录入框构件

当用户输入了数据之后,应用程序显然需要某种手段来获取用户的输入,以便对数据进行处理。为此,可以通过 Entry 对象的 textvariable 属性将录入框与一个 StringVar 类型的控制 变量相关联,具体做法如下:

>>> v = StringVar()
>>> e = Entry(root,textvariable = v)
>>> e.pack()

此后程序中就可以利用 v.get()来获取录入框中的文本内容。假设用户在录入框内键入了文 本“hello”,那么就有

>>> print v.get() hello

另外,程序中还可以通过 v.set()来设置录入框的内容:

>>> v.set('new text')

可以看到录入框中的文本立即变成了“new text”。 很多应用程序利用录入框作为用户登录系统时输入用户名和密码的界面元素,其中密码录入框一般不回显用户的输入,而是用“”代替,这在 Tkinter 中很容易做到,只需将录入 框对象的 show 属性设置为“”即可:

e.config(show='*')

除了 Entry 类,Tkinter 还提供一个支持多行文本录入与编辑的文本区构件类:Text。两 者的用法是类似的:

>>> Text(root).pack()

运行结果是在窗口中出现了一个多行的空白区域,在此区域可输入、编辑多行文本。Text 构 件的用途非常多,用法也比 Entry 复杂,这里就不详细介绍了。

框架

利用前面几节介绍的基本构件虽然能够实现简单的图形界面,但不足以搭建出美观的复 杂图形界面。为了将基本构件在界面中有层次地组织起来,还需要构件容器。如我们在 8.1.2 中描述的,框架就是一种容器,其主要用途是将一组相关的基本构件组合成为一个“复合” 构件。利用框架对窗口进行模块化分隔,即可建立复杂的图形界面结构。每个框架都是一个 独立的区域,可以独立地对其中包含的子构件进行布局。

Tkinter 提供了 Frame 类来创建框架构件,框架的宽度和高度分别用 width 和 height 属性 来设置,框架的边框粗细用 bd(或 border、borderwidth)属性来设置(默认值为 0,即没有 边框),边框的 3D 风格用 relief 属性来设置(默认是与环境融合的 flat 风格,其他可选的值 还有 groove、sunken、raised 和 ridge)。框架构件和基本构件一样需要先创建再布局,例如:

>>> f = Frame(root,width=300,height=400,bd=4,relief="groove")
>>> f.pack()

这个框架的边框风格是 groove,读者可以试试其他边框风格分别是什么样子的。顺便说一下, relief 属性也适用于按钮构件。

下面这个例子演示了如何将框架用作容器来组合构件,图 8.11 是其执行效果。

>>> f = Frame(root,bd=4,relief="groove")
>>> f.pack()
>>> Checkbutton(f,text="Math").pack()
>>> Checkbutton(f,text="Python").pack()
>>> Checkbutton(f,text="English").pack()

图 8.11 利用框架组合构件

除了作为容器来组合多个构件的用途,框架还可用于图形界面的空间分隔或填充,例如:

>>> Label(root,text="one").pack()
>>> Frame(height=2,bd=1,relief="sunken").pack(fill=X,pady=5)
>>> Label(text="two").pack()

其中第二行语句定义的框架只起着分隔两个标签构件的作用,该框架在根窗口中以 x 方向填 满(fill=X)、y 方向上下各填充 5 个像素空间(pady=5)的方式进行 Pack 布局。效果见图 8.12:

图 8.12 利用框架分隔构件

后文(见 8.4.1)会介绍,我们经常将 GUI 应用程序封装成类,尤其是封装成 Frame 的 子类。这样做的好处是可以将程序逻辑与用户界面融为一体,界面元素之间的交互情况对外 部是隐藏的。

菜单

菜单也是 GUI 最常用的构件之一。就像饭店里用的菜单一样,菜单构件是一个由许多菜 单项组成的列表,每个菜单项表示一条命令或一个选项。用户通过鼠标或键盘选择菜单项, 以执行命令或选中选项,由此可见菜单兼具命令按钮、勾选钮和单选钮的功能。另一方面, 菜单又不像大量命令按钮、勾选钮和单选钮那么占空间,因为菜单在不用时通常是“合上” 的,在界面中只是一个占用极少空间的菜单按钮,直到用户点击菜单按钮才会展开整个菜单。 图形界面中一般有多个菜单,它们通常以相邻的方式布置在一起,形成窗口的菜单栏,并且 一般置于窗口顶端。

除了菜单栏里的菜单,GUI 中还常用一种弹出式菜单,这种菜单平时在界面中是不可见 的,当用户在界面中点击鼠标右键时才会“弹出”一个与点击位置相关的菜单。

有时候菜单中一个菜单项的作用是展开另一个菜单,形成“级联式”菜单。

Tkinter 提供 Menu 类用于创建菜单构件,具体用法是先创建一个菜单构件实例,并与某 个窗口(根窗口或者顶层窗口)进行关联,然后再为该菜单添加菜单项。与根窗口关联的菜 单实际上构成了根窗口的菜单栏。菜单项可以是简单命令、级联式菜单、勾选钮或一组单选 钮,分别用 add_command、add_cascade、add_checkbutton 和 add_radiobutton 来添加。为了使 菜单结构清晰,还可以用 add_separator 在菜单中添加分割线。

例如,下列语句以交互方式创建了一个菜单(假设已经创建了根窗口 root):

>>> m = Menu(root)
>>> root.config(menu=m)
>>> m.add_command(label="File")
>>> m.add_command(label="Edit")

其中第一条语句创建菜单实例 m(作为 root 的子构件);第二条语句将 root 的 menu 属性设置 为 m ①,这导致将 m 布置于根窗口的顶部,形成菜单栏;第三、四条语句创建 m 的两个命 令菜单项。执行结果如图 8.13 所示。另外从上面的语句可以看到,菜单构件与前面介绍的构 件都不同,不需要调用布局管理器来使之可见,Tkinter 会自动布局并显示菜单。

图 8.13 菜单构件

上面这个例子其实没什么用,因为没有为命令菜单项指定相应的处理代码。另外,在实际应用中,窗口菜单栏里的菜单项通常是一个级联菜单,而不是简单命令菜单项。下面看一 个更实用的例子:

【程序 8.4】eg8_4.py

from Tkinter import *
def callback():
    print "hello from menu"
root = Tk()
m = Menu(root) 
root.config(menu = m) 
filemenu = Menu(m)
m.add_cascade(label="File", menu=filemenu) 
filemenu.add_command(label="New", command=callback)
filemenu.add_command(label="Open...", command=callback) 
filemenu.add_separator() 
filemenu.add_command(label="Exit", command=callback) 
helpmenu = Menu(m)
m.add_cascade(label="Help", menu=helpmenu) 
helpmenu.add_command(label="About...", command=callback)
mainloop()

① 也可采用 root['menu'] = m 的语法,参见 8.2.3.1。

程序 8.4 首先以根窗口为父构件创建菜单构件 m,接着将 m 设置为根窗口的菜单栏;然 后以 m 为父构件创建另两个菜单构件 filemenu 和 helpmenu,它们分别构成菜单 m 的菜单项 “File”和“Help”的级联菜单。菜单 filemenu 又由三个命令菜单项组成(中间有一道分隔 线),菜单 helpmenu 中只有一个命令菜单项。各个菜单在界面中的位置由 Tkinter 自动布局, 不需要调用布局管理器。为了简化程序,我们将所有命令菜单项都关联到同一个函数 callback, 实际应用程序当然应该为每个命令编制各自的处理函数。执行本程序并点击 File 菜单项之后 的结果如图 8.14 所示:

图 8.14 程序 8.4 的执行结果

顶层窗口

迄今我们所写的程序都只有一个窗口,即根窗口。如所熟知的,像 Word 之类的应用程 序是多窗口的,每打开一个文档都新开一个窗口。为了支持多窗口应用程序,Tkinter 提供 Toplevel 类用于创建顶层窗口构件。顶层窗口的外观与根窗口一样,可以独立地移动和改变 大小,并且不需要像其他构件那样必须在根窗口中进行布局后才显示。一个应用程序只能有 一个根窗口,但可以创建任意多个顶层窗口。例如:

>>> root = Tk()
>>> Label(root,text="hello").pack()
>>> top = Toplevel()
>>> Label(top,text="world").pack()

这个语句序列先创建根窗口,在根窗口中创建一个标签,然后创建了一个顶层窗口,又在顶 层窗口中创建了一个标签。执行结果如图 8.15 所示。

图 8.15 顶层窗口

对上例要说明的是,虽然创建 Toplevel 构件 top 时没有指定以根窗口 root 作为父构件, 但 top 确实是 root 的子构件,因此关闭 top 并不会结束程序,因为根窗口仍在工作;但若关 闭根窗口,则包含 top 在内的整个界面都会关闭。所以顶层窗口虽然具有相对的独立性,但 它不能脱离根窗口而存在。即使在没有根窗口的情况下直接创建顶层窗口,系统也会自动先 创建根窗口。

与根窗口类似,可以调用 Toplevel 类的 title 和 geometry 方法来设置它的标题和大小:

>>> top.title('hello toplevel')
>>> top.geometry('400x300')

当然也可以为顶层窗口建立菜单,方法和根窗口类似,这里就不赘述了。