本文将对图形用户界面(Graphical User Interface,GUI)编程进行简要的介绍。我们将主要使用的GUI 工具包是Python 默认的GUI 库Tk,通过Python 的接口tkinter(“Tk interface”的缩写)可以访问Tk。
Tk 并不是最新和最好的,也没有包含最强大的GUI 构建模块集,但是它足够易用,你可以使用它构建能够运行在大多数平台下的GUI。
tkinter 是Python 的默认GUI 库。它基于Tk工具包,该工具包最初是为工具命令语言(Tool Command Language,Tcl)设计的。Tk 普及后,被移植到很多其他的脚本语言中,包括Perl(Perl/Tk)、Ruby(Ruby/Tk)和Python(tkinter)。结合Tk 的GUI 开发的可移植性与灵活性,以及与系统语言功能集成的脚本语言的简洁性,可以让你快速开发和实现很多与商业软件品质相当的GUI 应用。
tkinter 在系统中不是默认必须安装的,可以通过在Python 解释器中尝试导入Tkinter 模块(Python 1 和2 版本,在Python 3 中重命名为tkinter)来检查Tkinter 是否可用。
import tkinter
为了让tkinter 成为应用的一部分,你需要做些什么呢?让GUI 程序启动和运行起来需要以下5 个主要步骤:
创建一个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 应用。
控 件 | 描 述 |
---|---|
Button | 与Label 类似,但提供额外的功能,如鼠标悬浮、按下、释放以及键盘活动/事件 |
Canvas | 提供绘制形状的功能(线段、椭圆、多边形、矩形),可以包含图像或位图 |
Checkbutton | 一组选框,可以勾选其中的任意个(与HTML 的checkbox 输入类似) |
Entry | 单行文本框,用于收集键盘输入(与HTML 的文本输入类似) |
Frame | 包含其他控件的纯容器 |
Label | 用于包含文本或图像 |
LabelFrame | 标签和框架的组合,拥有额外的标签属性 |
Listbox | 给用户显示一个选项列表来进行选择 |
Menu | 按下Menubutton 后弹出的选项列表,用户可以从中选择 |
Menubutton | 用于包含菜单(下拉、级联等) |
Message | 消息。与Label 类似,不过可以显示成多行 |
PanedWindow | 一个可以控制其他控件在其中摆放的容器控件 |
Radiobutton | 一组按钮,其中只有一个可以“按下”(与HTML 的radio 输入类似) |
Scale | 线性“滑块”控件,根据已设定的起始值和终止值,给出当前设定的精确值 |
Scrollbar | 为Text、Canvas、Listbox、Enter 等支持的控件提供滚动功能 |
Spinbox Entry | 和Button 的组合,允许对值进行调整 |
Text | 多行文本框,用于收集(或显示)用户输入的文本(与HTML 的textarea 类似) |
Toplevel | 与Frame 类似,不过它提供了一个单独的窗口容器 |
import tkinter as tk
top = tk.Tk()
label = tk.Label(top, text='hello')
label.pack()
tk.mainloop()
import tkinter as tk
top = tk.Tk()
quit = tk.Button(top, text='start', command=top.quit)
quit.pack()
tk.mainloop()
它会从当前目录开始,提供一个文件列表。双击列表中任意其他目录,就会使得工具切换到新目录中,用新目录中的文件列表代替旧文件列表。
import os
from time import sleep
import tkinter as tk
from functools import partial as pto
class DirList(object):
"""读取并显示指定目录下的文件列表"""
def __init__(self, initdir=None):
self.top = tk.Tk()
self.label = tk.Label(self.top, text='DirList v1.1')
self.label.pack()
# 写入延迟
self.cwd = tk.StringVar(self.top)
self.dirl = tk.Label(self.top, fg='blue', font=('Helvetica', 12, 'bold'))
self.dirl.pack()
self.dirfm = tk.Frame(self.top)
self.dirsb = tk.Scrollbar(self.dirfm)
self.dirsb.pack(side=tk.RIGHT, fill=tk.Y)
self.dirs = tk.Listbox(self.dirfm, height=15, width=50,
yscrollcommand=self.dirsb.set)
self.dirs.bind('' , self.setDirAndGo)
self.dirsb.config(command=self.dirs.yview)
self.dirs.pack(side=tk.LEFT, fill=tk.BOTH)
self.dirfm.pack()
self.dirn = tk.Entry(self.top, width=50, textvariable=self.cwd)
self.dirn.bind('' , self.doLS)
self.dirn.pack()
self.bfm = tk.Frame(self.top)
self.clr = tk.Button(self.bfm, text='Clear', command=self.clrDir,
activeforeground='white', activebackground='blue')
self.ls = tk.Button(self.bfm, text='List Directory', command=self.doLS,
activeforeground='white', activebackground='green')
self.quit = tk.Button(self.bfm, text='Quit', command=self.top.quit,
activeforeground='white', activebackground='red')
self.clr.pack(side=tk.LEFT)
self.ls.pack(side=tk.LEFT)
self.quit.pack(side=tk.LEFT)
self.bfm.pack()
if initdir:
self.cwd.set(os.curdir)
self.doLS()
def clrDir(self, ev=None):
"""清空文件列表"""
self.cwd.set('')
def setDirAndGo(self, ev=None):
"""设置文件夹路径并读取"""
self.last = self.cwd.get()
self.dirs.config(selectbackgroung='red')
check = self.dirs.get(self.dirs.curselection())
if not check:
check = os.curdir
self.cwd.set(check)
self.doLS()
def doLS(self, ev=None):
error = ''
tdir = self.cwd.get()
if not tdir:
tdir = os.curdir
if not os.path.exists(tdir):
error = tdir + ':no such file'
elif not os.path.isdir(tdir):
error = tdir + ':not a directory'
if error:
self.cwd.set(error)
self.top.update()
sleep(2)
if not (hasattr(self, 'last') and self.last):
self.last = os.curdir
self.cwd.set(self.last)
self.dirs.config(selectbackground='LightSkyBlue')
self.top.update()
return
self.cwd.set('Fetching directory contents...')
self.top.update()
dirlist = os.listdir(tdir)
dirlist.sort()
os.chdir(tdir)
self.dirl.config(text=os.getcwd())
self.dirs.delete(0, tk.END)
self.dirs.insert(tk.END, os.curdir)
self.dirs.insert(tk.END, os.pardir)
for eachFile in dirlist:
self.dirs.insert(tk.END, eachFile)
self.cwd.set(os.curdir)
self.dirs.config(selectbackground='LightSkyBlue')
def main():
DirList(os.curdir)
tk.mainloop()
if __name__ == '__main__':
main()