5.2.2 GUI 编程介绍
在举例之前,先简单介绍 GUI 应用开发。这将为你今后的学习提供一些通用的背景知识。
创建一个 GUI 应用就像艺术家作画一样。传统上,艺术家使用单一的画布开展创作。其工作方式如下:首先会从一块干净的石板开始,这相当于用来构建其余组件的顶层窗口对象。可以将其想象为房屋的地基或艺术家的画架。换句话说,必须在浇灌好混凝土或搭建起画架之后,才能把真实的结构或画布拼装在上面。在 Tkinter 中,这个基础称为顶层窗口对象。
窗口和控件
在 GUI 编程中,顶层的根窗口对象包含组成 GUI 应用的所有小窗口对象。它们可能是文字标签、按钮、列表框等。这些独立的 GUI 组件称为控件。所以当我们说创建一个顶层窗口时,只是表示需要一个地方来摆放所有的控件。在Python 中,一般会写成如下语句。
top = Tkinter.Tk() # or just Tk() with "from Tkinter import *"
Tkinter.Tk()返回的对象通常称为根窗口,这也是一些应用使用 root 而不是 top 来指代它的原因。顶层窗口是那些在应用中独立显示的部分。GUI 程序中可以有多个顶层窗口,但是其中只能有一个是根窗口。可以选择先把控件全部设计好,再添加功能;也可以边设计控件边添加功能(这意味着上述步骤中的第 3 步和第 4 步会混合起来做)。
控件可以独立存在,也可以作为容器存在。如果一个控件包含其他控件,就可以将其认为是那些控件的父控件。相应地,如果一个控件被其他控件包含,则将其认为是那个控件的子控件,而父控件就是下一个直接包围它的容器控件。
通常,控件有一些相关的行为,比如按下按钮、将文本写入文本框等。这些用户行为称为事件,而 GUI 对这类事件的响应称为回调。
事件驱动处理
事件可以包括按钮按下(及释放)、鼠标移动、敲击回车键等。一个 GUI 应用从开始到结束就是通过整套事件体系来驱动的。这种方式称为事件驱动处理。
最简单的鼠标移动就是一个带有回调的事件的例子。假设鼠标指针正停在 GUI 应用顶层窗口的某处。如果你将鼠标移动到应用的另一部分,鼠标移动的行为会被复制到屏幕的光标上,于是看起来像是根据你的手移动的。系统必须处理的这些鼠标移动事件可以绘制窗口上的指针移动。当释放鼠标时,不再有事件需要处理,此时屏幕会重新恢复闲置的状态。
事件驱动的 GUI 处理本质上非常适合于客户端/服务端架构。当启动一个 GUI 应用时, 需要一些启动步骤来准备核心部分的执行,就像网络服务器启动时必须先分配套接字并将其绑定到本地地址上一样。GUI 应用必须先创建所有的 GUI 组件,然后将它们绘制在屏幕上。这是布局管理器(geometry manager)的职责所在(稍后会详细介绍)。当布局管理器排列好所有控件(包括顶层窗口)后,GUI 应用进入其类似服务器的无限循环。这个循环会一直运行,直到出现 GUI 事件,进行处理,然后再等待更多的事件去处理。
布局管理器
Tk 有 3 种布局管理器来帮助控件集进行定位。最原始的一种称为 Placer。它的做法非常直接:你提供控件的大小和摆放位置,然后管理器就会将其摆放好。问题是你必须对所有控件进行这些操作,这样就会加重编程开发者的负担,因为这些操作本应该是自动完成的。
第二种布局管理器会是你主要使用的,它叫做 Packer,这个命名十分恰当,因为它会把控件填充到正确的位置(即指定的父控件中),然后对于之后的每个控件,会去寻找剩余的空间进行填充。这个处理很像是旅行时往行李箱中填充行李的过程。
第三种布局管理器是 Grid。你可以基于网格坐标,使用 Grid 来指定 GUI 控件的放置。Grid 会在它们的网格位置上渲染 GUI 应用中的每个对象。本章将使用Packer。
一旦Packer 确定好所有控件的大小和对齐方式,它就会在屏幕上将其放置妥当。
当所有控件摆放好后,可以让应用进入前述的无限主循环中。在 Tkinter 中,代码如下所示。
Tkinter.mainloop()
一般这是程序运行的最后一段代码。当进入主循环后,GUI 就从这里开始接管程序的执行。所有其他行为都会通过回调来处理,甚至包括退出应用。当选择File 菜单并单击Exit 菜单选项,或者直接关闭窗口时,就会调用一个回调函数来结束这个 GUI 应用。
5.2.3 顶层窗口:Tkinter.Tk()
之前提到过所有主要控件都是构建在顶层窗口对象之上的。该对象在 Tkinter 中使用 Tk类进行创建,然后进行如下实例化:
>>> import Tkinter >>> top = Tkinter.Tk()
在这个窗口中,可以放置独立的控件,也可以将多个组件拼凑在一起来构成 GUI 程序。那么有哪些种类的控件呢?现在就介绍这些Tk 控件。
5.2.4 Tk 控件
在本书写作时,总共有 18 种 Tk 控件,表 5-1 所示为这些控件的描述。最新的控件有
LabelFrame、PanedWindow 和 Spinbox,这些都是从 Python 2.3 版本开始增加的(通过 Tk 8.4)。
表 5-1 Tk 控 件
我们将不会对 Tk 控件进行详细介绍,因为已经有很多不错的文档可以供你参阅了,比如 Python 主站上的 Tkinter 主题页,或大量印刷资源和网上关于 Tcl/Tk 的资源(可以参见附录 B)。不过,后面会给出几个简单的例子来帮助你起步。
核心提示:默认参数是你的朋友 |
GUI 开发利用了Python 的默认参数,因为 Tkinter 的控件中有很多默认行为。除非你非常清楚自己所使用的每个控件的每个可用选项的用法,否则最好还是只关心你要设置的那些参数,而让系统去处理剩下的参数。这些默认值都是精心选择出来的。即使没有提供这些值,也不用担心应用程序在屏幕上的显示会有什么问题。作为一条基本规则,程序是由一系列优化后的默认参数创建的,只有当你知道如何精确定制你的控件时,才应该使用非默认值。 |
5.3 Tkinter 示例
现在来看下我们的第一组 GUI 脚本,其中的每个脚本都会介绍一个控件,并可能会展示一种使用控件的不同方式。几个非常基础的例子后,是一个中等难度的示例,该示例会与 GUI 编程实践有更多的关联性。
5.3.1 Label 控件
在示例 5-1 的 tkhello1.py 中,给出了 Tkinter 版本的“Hello World!”。特别 是,它会展示Tkinter 应用如何启动,并着重强调了 Label 控件。
示例 5-1 Label 控件演示(tkhello1.py)
我们的第一个 Tkinter 示例,除了“Hello Word!”,还能是什么呢?特别是,我们会介绍第一个控件:Label。
在第 1 行中,创建了一个顶层窗口。接下来是 Label 控件,它包含了那串久负盛名的字符串。然后让Packer 来管理和显示控件,最后调用 mainloop()运行这个 GUI 应用。图 5-1 所示为运行该 GUI 应用后的结果。
图 5-1 Tkinter 的 Label 控 件
5.3.2 Button 控件
下一个例子(tkhello2.py)与第一个例子很相似。不过,这里创建的控件是按钮,而不再是标签。示例 5-2 为其源代码。
示例 5-2 Button 控件演示(tkhello2.py)
这个例子和 tkhello1.py 非常相似,除了这里创建的是 Button 控件而不是 Label 控件外。
一开始的几行完全相同,只有在创建 Button 控件时有所区别。该按钮有一个额外的参数:Tkinter.quit()方法。该参数会给按钮安装一个回调函数,当按钮被按下(并且释放)后,整个程序就会退出。最后两行是通用的 pack()方法和 mainloop()调用。这个简单的按钮应用如图5-2 所示。
图 5-2 Tkinter 的 Button 控 件
5.3.3 Label 和Button 控件
在示例 5-3 中,会把 tkhello1.py 和tkhello2.py 结合到一起,组成既包含标签又包含按钮的tkhello3.py 脚本。此外,它还会使用更多的参数,而不只是满足于自动生成的默认值。
除了控件的额外参数之外,还可以看到 Packer 的一些参数。fill 参数告诉Packer 让QUIT 按钮占据剩余的水平空间,而 expand 参数则会引导它填充整个水平可视空间,将按钮拉伸到左右窗口边缘。
示例 5-3 Label 和 Button 控件演示(tkhello3.py)
本示例使用了 Label 和 Button 控件。相比于在创建控件时使用默认参数,这里指定了几个额外的参数,用于学习 Button 控件更多的知识及其配置方法。
如图 5-3 所示,在 Packer 没有收到其他指示时,所有控件都是垂直排列的(自上而下依次排列)。如果想要水平布局则需要创建一个新的 Frame 对象来添加按钮。该框架将作为单个子对象来代替父对象的位置(参见 5.3.6 节示例 5-6 中 listdir.py 模块的按钮)。
图 5-3 Tkinter 的 Label 和 Button 控 件
5.3.4 Label、Button 和 Scale 控件
最后一个简单例子 tkhello4.py 会额外调用Scale 控件。这里 Scale 用于与 Label 控件进行交互。Scale 滑块是用来控制 Label 控件中文字字体大小的工具。滑块的位置值越大,字体越大;反之亦然。示例 5-4 为 tkhello4.py 的代码。
本脚本的新功能包括一个 resize()回调函数(第 5~7 行),该函数会依附于 Scale 控件。当 Scale 控件的滑块移动时,这个函数就会被激活,用来调整Label 控件中的文本大小。
此外,还定义了顶层窗口的大小为 250*150(第 10 行)。本脚本与之前 3 个脚本的最后一个不同之处是导入 Tkinter 模块的属性到命名空间时使用的是 from Tkinter import *。尽管因为会污染命名空间而不推荐这种做法,但是这里依然如此使用的主要原因是这个应用会涉及对 Tkinter 属性的大量引用。直接导入 Tkinter 模块会造成访问每个属性时都需要使用其完整写法。而使用这种不推荐的简写方式虽然付出了一定代价,但是可以减少输入,并使得代码更加易读。
示例 5-4 Label、Button 和 Scale 控件演示(tkhello4.py)
最后要介绍的控件是 Scale,此外还会重点了解控件是如何通过回调函数(如 resize())与其他控件进行通信的。Label 控件中的文本会受到 Scale 控件上操作的影响。
如图 5-4 所示,滑块机制和当前的设定值都在窗口的主要部分中显示出来。同样,还可以从图 5-4 中看出,当用户移动滑动条/滑块到值 36 时 GUI 的状态。请注意,应用启动时滑块的初始值设定为 12(第 18 行)。
图 5-4 Tkinter 的 Label 、 Button 和 Scale 控 件
5.3.5 偏函数应用示例
在看一个更复杂的 GUI 应用之前,让我们先回顾一下 Core Python Programming 或 Core Python Language Fundamentals 书中介绍的偏函数应用(PFA)。
偏函数在 Python 2.5 版本中添加进来,是函数式编程一系列重要改进中的一部分。使用偏函数,可以通过有效地“冻结”那些预先确定的参数来缓存函数参数,然后在运行时,当获得需要的剩余参数后,可以将它们解冻,传递到最终的参数中,从而使用最终确定的所有参数去调用函数。
偏函数最好的一点是它不只局限于函数。偏函数可以用于可调用对象(任何包括函数接口的对象),只需要通过使用圆括号即可,包括类、方法或可调用实例。对于有很多可调用对象,并且许多调用都反复使用相同参数的情况,使用偏函数会非常合适。
GUI 编程是一个很好的偏函数用例,因为你很有可能需要 GUI 控件在外观上具有某种一致性,而这种一致性来自于使用相同参数创建相似的对象时。我们现在要实现一个应用,在这个应用中有很多按钮拥有相同的前景色和背景色。对于这种只有细微差别的按钮,每次都使用相同的参数创建相同的实例简直是一种浪费:前景色和背景色都是相同的,只有文本有一点不同。
本例中将使用交通路标来进行演示,在该应用中我们会尝试创建文字版本的路标,并将其根据标志类型进行区分,比如严重、警告、通知等(就像日志级别那样)。标志类型决定了 创建时的颜色方案。例如,严重级别标志是白底红字,警告级别标志是黄底黑字,通知(即标准级别)标志是白底黑字。在这里,“Do Not Enter”和“Wrong Way”标志属于严重级别, “Merging Traffic”和“Railroad Crossing”属于警告级别,而“Speed Limit”和“One Way”属于标准级别。
示例 5-5 中的应用会创建这些标志,当然它们只是按钮。当用户按下按钮时,会弹出相应的 Tk 对话框:严重/错误、警告或通知。虽然这不够令人兴奋,但仍然能够说清这些按钮是如何创建的。
示例 5-5 路标偏函数 GUI 应用(pfaGUI2.py)
根据标志类型创建拥有合适前景色和背景色的路标。使用偏函数可以帮助你“模板化”通用的 GUI 参数。
当你执行这个应用时,可以看到如图 5-5 所示的 GUI 输出。
图 5-5 Mac OS X 的XDarwin 下的路标偏函数 GUI 应用
逐行解释
第 1~18 行
先在应用中导入了 functools.partial()、几个 Tkinter 属性以及几个 Tk 对话框(第 1~5 行)。之后,根据类别定义了一些标志(第 7~18 行)。
第 20~28 行
Tk 对话框用做按钮的回调函数,将在创建每个按钮时使用它们(第 20~23 行)。之后启动 Tk,设置标题,并创建一个 QUIT 按钮(第 25~28 行)。
第 30~33 行
这些行展示了偏函数的魔法。我们使用了两阶偏函数。第一阶模板化了 Button 类和根窗口 top。这意味着每次调用 MyButton 时,它就会调用 Button 类(Tkinter.Button()会创建一个按钮),并将 top 作为它的第一个参数。我们将其冻结为MyButton。
第二阶偏函数会使用我们的第一阶偏函数,并对其进行模板化。我们会为每种标志类型创建单独的按钮类型。当用户创建一个严重类型的按钮 CritButton 时(比如通过调用CritButton()),它就会调用包含适当的按钮回调函数、前景色和背景色的 MyButton,或者说使用 top、回调函数和颜色这几个参数去调用 Button。你可以看到它是如何一步步展开并最终调用到最底层的,如果没有偏函数这个功能,这些调用本来应该是由你自己执行的。WarnButton 和ReguButton 也会执行同样的操作。
第 35~42 行
设置好按钮后,我们会根据标志列表将其创建出来。我们将使用一个 Python 可求值字符串,该字符串由正确的按钮名、传给按钮标签的文本参数以及 pack()操作组成。如果这是一个严重级别的标志,我们会把所有字符大写;否则,按照标题格式进行输出。第 39 行代码会用到 Python 2.5 版本开始引入的三元/条件操作符。每个按钮会通过 eval()函数进行实例化, 结果如图 5-5 所示。最后,我们进入主事件循环来启动 GUI 程序。
如果你使用的是 2.4 或更老的版本,可以使用“and/or”语法比较轻松地替代三元操作符,但是 functools.partial()就比较难移植过去了,所以还是推荐使用 2.5 或更新的版本来执行这个示例应用。
5.3.6 中级 Tkinter 示例
我们将使用一个更复杂的脚本来结束本节,即示例 5-6 中的 listdir.py。这个应用是一个目录树遍历工具。它会从当前目录开始,提供一个文件列表。双击列表中任意其他目录,就会使得工具切换到新目录中,用新目录中的文件列表代替旧文件列表。
示例 5-6 文件系统遍历 GUI(listdir.py)
这个稍高级的 GUI 程序扩展了控件的使用,新增了列表框、文本框和滚动条。此外,还增加了鼠标单击、键盘按下、滚动操作等回调函数。
在图 5-6 中,我们可以看到在 Windows 系统中这个 GUI 程序的样子。而该应用在 POSIX系统上的截图如图 5-7 所示。
图 5-6 Windows 下的目录列表 GUI 应用
逐行解释
第 1~5 行
最开始的这几行包括 UNIX 启动行,以及对 os 模块、time.sleep()函数和 Tkinter 模块所有属性的导入。
第 9~13 行
这几行定义了DirList 类的构造函数和一个代应用的对象。然后创建了第一个 Label 控件, 其中的文本是应用的主标题和版本号。
第 15~19 行
这里声明了 Tk 的一个变量 cwd,用于保存当前所在的目录名——之后我们会看到它是如何派上用场的。然后又创建了另一个Label 控件,用于显示当前的目录名。
第 21~29 行
这一部分定义了本 GUI 应用的核心部分 Listbox 控件 dirs,该控件包含了要列出的目录的文件列表。Scrollbar 可以让用户在文件数超过 Listbox 的大小时能够移动列表。上述这两个控件都包含在 Frame 控件中。通过使用 Listbox 的 bind()方法,Listbox 的列表项可以与回调函数(setDirAndGo)连接起来。
绑定意味着将一个回调函数与按键、鼠标操作或一些其他事件连接起来,当用户发起这类事件时,回调函数就会执行。当双击 Listbox 中的任意条目时,就会调用 setDirAndGo()函
数。而 Scrollbar 通过调用 Scrollbar.config()方法与 Listbox 连接起来。
图 5-7 UNIX 下的目录列表 GUI 应用
第 31~34 行
然后创建了一个文本框,用户可以在其中输入想要遍历的目录名,从而可以在 Listbox 中看到该目录中的文件列表。这里给这个文本框添加了一个回车键的绑定,这样用户除了可以单击按钮外,还可以敲击回车键来更新文件列表。我们之前在 Listbox 中看到过的鼠标绑定也是同样的应用。当用户双击 Listbox 中的条目时,与在文本框中手动输入目录名然后单击 Go 按钮有同样的效果。
第 36~53 行
接下来,定义了一个按钮的框架(bfm),用来放置 3 个按钮:一个“clear”按钮(clr)、一个“go”按钮(ls)和一个“quit”按钮(quit)。每个按钮在按下时都有其自己的配置和回调函数。
第 55~57 行
构造函数的最后一部分初始化 GUI 程序,并以当前工作目录作为起始点。
第 59~60 行
clrDir()方法会清空 Tk 字符串变量 cwd(包含当前活动目录)。该变量会跟踪我们当前所处的目录,更重要的是,当发生错误时可以帮助我们回到之前的目录。此外,你还会注意到回调函数中变量 ev 的默认值是 None。任何像这样的值都是由窗口系统传入的。它们在你的回调函数中可能会用到,也可能用不到。
第 62~69 行
setDirAndGo()方法设置要遍历的目录,并通过调用 doLS()实现遍历目录的行为。
第 71~108 行
到目前为止,doLS()是整个 GUI 应用的最关键部分。它会进行所有安全检查(比如,目标是否是一个目录?它是否存在?)。如果发生错误,之前的目录就会重设为当前目录。如果一切正常,就会调用 os.listdir()获取实际文件列表并在 Listbox 中进行替换。当后台忙于拉取新目录中的信息时,突出显示的蓝色条就会变成红色,直到新目录设置完毕后,它又会变回蓝色。
第 110~115 行
listdir.py 的最后一段代码是这段代码的最主要部分。只有当直接调用脚本时,main()函数才会执行。当 main()函数运行时,会创建 GUI 应用,然后调用 mainloop()来启动 GUI 程序, 之后由其控制应用的执行。
把这个应用的其他部分作为练习留给读者,推荐读者把整个应用看成一系列控件和函数的组合,这样可以更易于理解。如果你清晰地了解了每一部分,那么整个脚本也就不再那么令人畏惧了。
我们希望已经详细介绍了使用 Python 和Tkinter 进行 GUI 编程的方法。请记住,熟悉 Tkinter编程的最好方法是实践以及模仿示例!Python 的发行包中有很多可以供你学习的演示应用。
如果你下载的是源码包,可以在 Lib/lib-tk、Lib/idlelib 和Demo/tkinter 中找到 Tkinter 的演示代码。如果你把 Win32 版本的 Python 安装在 C:\Python2x 上,可以在 Lib/lig-tk 和 Lib\idlelib 中获取演示代码。后面的那个目录包含了最重要的 Tkinter 示例应用:IDLE IDE 本身。此外,还有很多有关 Tk 编程的书籍可以作为进一步的参考,其中还有一本是专门写 Tkinter 的。