part11-1 Python图形界面编程(Python GUI库介绍、Tkinter 组件介绍、布局管理器、事件处理)

Python 提供了大量的 GUI 库,可用于创建功能丰富的图形用户界面。这些 GUI 库大部分是第三方提供的。可选择熟悉的 GUI 库或者直接选择 Python 内置的 Tkinter 库开发图形界面程序。

图形用户组件以一种“搭积木”的方式组织在一起,成为实际可用的图形用户界面。图形用户界面还需要与用户交互操作,还应该为程序提供事件处理,事件处理负责让程序响应用户动态。

一、 Python 的 GUI 库

图形用户界面(Graphics User Interface),简称 GUI。

Python 的图形用户界面库有下面这些:

(1)、PyGObject:PyGObject 库为基于 GObject 的 C 函数库提供了内省绑定,这些库可以支持 GTK+3 图形界面工具集,因此 PyGObject 提供了丰富的图形界面组件。

(2)、PyGTK:PyGTK 基于老版本的 GTK+2 的库提供绑定,借助于底层 GTK+2 所提供的各种可视化元素和组件,同样可以开发出在 GNOME 桌面系统上运行的软件,因此它主要适用于 Linux/Unix 系统。PyGTK 对 GTK+2 的 C 语言进行了简单封装,提供了面向对象的编程接口。参考网址 https://wiki.python.org/moin/PyGtk

(3)、PyQt:PyQt 是 Python 编程语言和 Qt 库的成功融合。Qt 本身是一个扩展的 C++ GUI 应用开发框架,Qt 可以在 UNIX、Windows 和 Mac OS X 上完美运行,因此 PyQt 是建立在 Qt 基础上的 Python 包装。所以,PyQt 也能跨平台使用。

(4)、PySide:PySide 是由 Nokia 提供的对 Qt 工具集的新的包装库,其成熟度不如 PyQt。

(5)、wxPython:wxPython 是一个跨平台的 GUI 工具集,wxPython 以流行的 wxWidgets(原名 wxWindowss)为基础,提供了良好的跨平台外观。wxPython 在 Windows 上调用 Windows 的本地组件、在 MacOS 上调用 Mac OS X 的本地组件、在 Linux 上调用 Linux 的本地组件,这样可以让 GUI 程序在不同的平台上显示平台对应的风格。wxPython 是一个非常流行的跨平台的 GUI 库。官方 网址是 http://www.wxpython.org

可根据需要选择上面的 Python GUI 库做图形用户界面开发。如果需要开发跨平台的图形用户界面,则推荐使用 PyQt 或 wxPython。

这里使用的 GUI 库 Tkinter,它是 Python 自带的 GUI 库,直接导入 tkinter 包即可使用。

二、Tkinter GUI 编程的组件

可将一个窗口看成可被分解的一个空的容器,容器里装了大量的基本组件,通过设置这些基本组件的大小、位置等属性,就可以将该空的容器和基本组件组成一个整体的窗口。所以,可将图形界面编程看作是拼图游戏,创建图形用户界面的过程就是完成拼图的过程。

学习 GUI 编程的总体步骤可分为下面三步:

(1)、了解 GUI 库大致包含哪些组件,就相当于熟悉每个积木块到底是些什么东西。

(2)、掌握容器及容器对组件进行布局的方法。

(3)、逐个掌握各组件的用法,则相当于深入掌握每个积木块的功能和用法。

由于 Tkinter 库包含的 GUI 组件较多,且各组件之间存在错综复杂的继承关系。需要先通过类图来了解各 GUI 组件,以及它们之间的关系。Tkinter 的 GUI 组件之间的继承关系如图一所示。

                                                                                        图一 Tkinter 的 GUI 组件之间的继承关系

从上图可以看到,Tkinter 的 GUI 组件有两个根父类,它们都直接继承了 object 类。

(1)、Misc:它是所有组件的根父类。

(2)、Wm:它主要提供了一些与窗口管理器通信的功能函数。

对于 Misc 和 Wm 两个基类而言,GUI 编程并不需要直接使用它们,但由于它们是所有 GUI 组件的父类,因此 GUI 组件都可以直接使用它们的方法。

Misc 和 Wm 派生了一个子类:Tk,它代表应用程序的主窗口。因此,所有 Tkinter GUI 编程通常都需要直接或间接使用该窗口类。

BaseWidget 是所有组件的基类,它还派生了一个子类:Widget。Widget 代表一个通用的 GUI 组件。Tkinter 所有的 GUI 组件都是Widget 的子类。

Widget 的父类有四个,除 BaseWidget 之外,还有 Pack、Place 和 Grid,这三个父类都是布局管理器,它们负责管理所包含的组件的大小和位置。

剩下的是图一右边的 Widget 的子类,这些都是 Tkinter GUI 编程的各种 UI 组件,也就是“积木块”。下面是 GUI 组件功能的简单介绍。

Tkinter类 名称 简介
Toplevel 顶层 容器类,可用于为其他组件提供单独的容器;Toplevel有点类似于窗口
Button 按钮 代表按钮组件
Canvas 画布 提供绘图功能,包括绘制直线、矩形、椭圆、多边形、位图等
Checkbutton 复选框 可供用户勾选的复选框
Entry 单行输入框 用户可输入内容
Frame 容器 用于装载其他GUI组件
Label 标签 用于显示不可编辑的文本或图标
LabelFrame 容器 也是容器组件,类似于Frame,但它支持添加标题
Listbox 列表框 列出多个选项,供用户选择
Menu 菜单 菜单组件
Menubutton 菜单按钮 用来包含菜单的按钮(包括下拉式、层叠式等)
OptionMenu 菜单按钮 Menubutton的子类,也代表菜单按钮,可通过按钮打开一个菜单
Message 消息框 类似于标签,但可以显示多行文本;后来当当Label也能显示多行文本后,该组件基本不会用到
Radiobutton 单选按钮 可供用户点选的单选按钮
Scale 滑动条 拖动滑块可设定起始值和结束值,可显示当前位置的精确值
Spinbox 微调选择器 用户可通过该组件的向上、向下箭头选择不同的值
Scrollbar 滚动条 用于为组件(文本域、画布、列表框、文本框)提供滚动功能
Text 多行文本框 显示多行文本

下面来创建一个简单的图形窗口,代码如下:

# 在 Python 2.x 版本中使用下面这行导入
# from Tkinter import *
# 在 Python 3.x 版本中使用下这行
from tkinter import *
# 创建 Tk 对象,Tk 代表窗口
root = Tk()
# 设置窗口标题
root.title('窗口标题')
# 创建 Label 对象,第一个参数指定将该 Label 放入 root内
w = Label(root, text='hello tkinter!')
# 调用 pack 进行布局
w.pack()
# 启动主窗口
root.mainloop()

上面代码中创建了两个对象:Tk 和 Label。其中 Tk 代表顶级窗口,Label 代表一个简单的文本标签,因此需要指定将该 Label 放在哪个容器内。在这里创建 Label 时第一个参数指定了 root,表明该 Label 要放入 root 窗口内。运行结果省略。

此外,还有一种方式是不直接使用 Tk,只要创建 Frame 的子类,它的子类就会自动创建 Tk对象作为窗口。示例如下:

from tkinter import *

# 定义一个继承 Frame 类的 Application 类
class Application(Frame):
    def __init__(self, master=None):
        Frame.__init__(self, master)
        self.pack()     # 布局
        # 调用 initWidgets() 方法初始化界面
        self.initWidgets()

    def initWidgets(self):
        # 创建 Label 对象,第一个参数指定将 Label 放和 root 内
        w = Label(self)
        # 创建一个位图
        bm = PhotoImage(file = 'baidu.png')
        # 必须用一个不会被释放的变量引用该图片,否则该图片会被回收
        w.x = bm
        # 设置显示的图片是 bm
        w['image'] = bm
        w.pack()
        # 创建 Button 对象,第一个参数指定将该 Button 放入 root 内
        okButton = Button(self, text="确定")
        okButton['background'] = 'yellow'
        # okButton.configure(background='yellow')     # 这行与上面代码作用相同
        okButton.pack()

# 创建  Application 对象
app = Application()
# Frame 有一个默认的 master 属性,该属性值是  Tk 对象(窗口)
print(type(app.master))
# 通过 master 属性来设置窗口标题
app.master.title("百度一下,啥都知到")
# 启动主窗口的消息循环
app.mainloop()

上面代码中首先创建了 Frame 的子类 Application,并在该类的构造方法中调用了自定义方法 initWidgets() 方法,这个方法名可以任意取,接下来在实例方法中 initWidgets() 创建了两个组件,即 Label 和 Button。

在上面代码中只是创建了 Application 的实例(Frame 容器的子类),并未创建 Tk 对象(窗口),但是运行这段代码仍然是有窗口的。如果程序在创建任意 Widget 组件(甚至 Button)时没有指定 master 属性(即创建 Widget 组件时第一个参数传入 None)那么程序会自动为该 Widget 组件创建一个 Tk 窗口,因此 Python 会自动为 Application 实例创建 Tk 对象来作为它的 master。

该段代码与上一段代码的区别在于:这段代码创建 Label 和 Button 后,对 Label 进行了配置,设置了 Label 显示的背景图片;同时也对 Button 进行了配置,设置了 Button 的背景色。

另外,代码中的 w.x = bm 行是增加对图片对象的引用,防止 initWidgets() 方法结束时,阻止系统回收 PhotoImage 的图片。运上面的代码,在终端上的输出是:<class 'tkinter.Tk'>,以及输出如下图二所示的效果。

                                                       图二 配置 Label和Button

上面这段代码中的 initWidgets() 方法的代码,实际上只有3行代码:

(1)、创建GUI组件。相当于创建 “积木块”。

(2)、添加GUI组件,这里使用 pack() 方法添加。相当于把 “积木块” 添加进去。

(3)、配置GUI组件。

其中创建GUI 组件与创建其他 Python 对象没有什么区别,但通常至少要指定一个参数,用于设置该 GUI 组件属于哪个容器(Tk组件例外,因为该组件代表顶级窗口)。

配置 GUI 组件的两个时机:

(1)、在创建 GUI 组件时以关键字参数的方式配置。例如 Button(self, text="确定"),其中 text="确定" 就指定了该按钮上的文本是 “确定”。

(2)、在创建完成之后,以字典语法进行配置。例如 okButton['background']='yellow',这种语法使得 okButton 看上去就像一个字典,它用于配置 okButton 的 background 属性,从而改变该按钮的背景色。

上面两种方式是可以互换的。比如在创建按钮之后配置该按钮上的文本,代码如下:

okButton['text'] = '确定'

这行代码其实是调用 configure() 方法的简化写法。也就是说,这行代码等同于如下代码:

okButton.configure(text='确定')

也可以在创建按钮时就配置它的文本和背景色,代码如下:

# 创建 Button 对象时,就配置它的文本和背景色
okButton = Button(self,text="确定",background="yellow")

除可配置 background、image 等选项外,GUI 组件还可配置其它的选项,具体选项有哪些,可查看该组件的构造方法的帮助文档。例如要查看 Button 的构造方法的帮助文档,方法如下:

>>> import tkinter
>>> help(tkinter.Button.__init__)
Help on function __init__ in module tkinter:

__init__(self, master=None, cnf={}, **kw)
    Construct a button widget with the parent MASTER.

    STANDARD OPTIONS

        activebackground, activeforeground, anchor,
        background, bitmap, borderwidth, cursor,
        disabledforeground, font, foreground
        highlightbackground, highlightcolor,
        highlightthickness, image, justify,
        padx, pady, relief, repeatdelay,
        repeatinterval, takefocus, text,
        textvariable, underline, wraplength

    WIDGET-SPECIFIC OPTIONS

        command, compound, default, height,
        overrelief, state, width

从上面帮助文档可知,Button 支持两组选项:标准选项(STANDARD OPTIONS)和组件特定选项(WIDGET-SPECIFIC OPTIONS)。关于这些选项的含义,可以从名字上略知一二。GUI 组件的常见选项的含义如下图三所示,图中的选项是大部分GUI组件都支持的。

                         图三   GUI组件支持的通用选项

三、布局管理器

布局管理器是负责管理各组件的大小和位置的。此外,当用户调整窗口大小后,布局管理器还会自动调整窗口中各组件的大小和位置。

1、 Pack 布局管理器

使用 Pack 布局管理时,当向容器中添加组件时,这些组件会依次向后排名,排列方向可以是水平的,也可以是垂直的。Pack 布局的用法示例如下,下面这段代码向窗口中添加三个 Label 组件。

from tkinter import *

# 创建窗口并设置窗口标题
root = Tk()
root.title('Pack布局')
for i in range(3):
    lab = Label(root, text="第%d个Label" % (i + 1), bg='#eee')
    # 调用 pack 进行布局
    lab.pack()
# 启动主窗口的消息循环
root.mainloop()

上面代码中,首先创建了一个窗口,接着循环创建三个 Label,并对这三个 Label 使用 pack() 方法进行默认的 Pack 布局。运行这段代码,得到图四所示的界面。

                    图四   使用Pack布局

在使用 Pack 布局时,调用的 pack() 方法还可以传入多个选项,关于 pack() 方法的选项,可通过下面的方式查看:

>>> help(tkinter.Label.pack)
Help on function pack_configure in module tkinter:

pack_configure(self, cnf={}, **kw)
    Pack a widget in the parent widget. Use as options:
    after=widget - pack it after you have packed widget
    anchor=NSEW (or subset) - position widget according to
                              given direction
    before=widget - pack it before you will pack widget
    expand=bool - expand widget if parent size grows
    fill=NONE or X or Y or BOTH - fill widget if widget grows
    in=master - use master to contain this widget
    in_=master - see 'in' option description
    ipadx=amount - add internal padding in x direction
    ipady=amount - add internal padding in y direction
    padx=amount - add padding in x direction
    pady=amount - add padding in y direction
    side=TOP or BOTTOM or LEFT or RIGHT -  where to add this widget.

从上面输出信息可知,pack() 方法支持的选项有:

(1)、anchor:当可用空间大于组件所需求的大小时,该选项决定组件被放置在容器的何处。支持的选项有:N(北,代表上)、E(东,代表右)、S(南,代表下)、W(西,代表左)、NW(西北,代表左上)、NE(东北,右上)、SW(西南,左下)、SE(东南,右下)、CENTER(中,默认值)等这些值。

(2)、expand:该 bool 值指定当父容器增大时是否拉伸组件。

(3)、fill:设置组件是否沿水平或垂直方向填充。支持四个值:NONE、X、Y、BOTH,其中 NONE 表示不填充,BOTH 表示沿着两个方向填充。

(3)、ipadx:指定组件在 x 方向(水平)上的内部留白(padding)。

(4)、ipady:指定组件在 y 方向(垂直)上的内部留白(padding)。

(5)、padx:指定组件在 x 方向(水平)上与其他组件的间距。

(6)、pady:指定组件在 y 方向(垂直)上与其他组件的间距。

(7)、设置组件的添加位置,可设置为 TOP、BOTTOM、LEFT 或 RIGHT 四个值的其中之一。

当做出来的界面比较复杂时,就需要使用多个容器(Frame)分开布局,然后再将 Frame 添加到窗口中。示例如下:

from tkinter import *
class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建第一个容器
        fm1 = Frame(self.master)
        # 该容器放在左边
        fm1.pack(side=LEFT, fill=BOTH, expand=YES)
        # 向 fm1 中添加三个按钮
        # 设置按钮从顶部开始排列,且按钮只能在水平(X)方向上填充
        Button(fm1, text='第一个').pack(side=TOP, fill=X, expand=YES)
        Button(fm1, text='第二个').pack(side=TOP, fill=X, expand=YES)
        Button(fm1, text='第三个').pack(side=TOP, fill=X, expand=YES)
        # 创造第二个容器
        fm2 = Frame(self.master)
        # 该容器放在左边排列,就会挨着 fm1
        fm2.pack(side=LEFT, padx=10, expand=YES)
        # 向 fm2 中添加三个按钮
        # 设置按钮从右边开始排列
        Button(fm2, text='第一个').pack(side=RIGHT, fill=Y, expand=YES)
        Button(fm2, text='第二个').pack(side=RIGHT, fill=Y, expand=YES)
        Button(fm2, text='第三个').pack(side=RIGHT, fill=Y, expand=YES)
        # 创建第三个容器
        fm3 = Frame(self.master)
        # 该容器放在右边排列,就公挨着 fm1
        fm3.pack(side=RIGHT, padx=10, fill=BOTH, expand=YES)
        # 向 fm3 中添加三个按钮
        # 设置按钮从底部开始排列,且按钮只能在垂直(Y)方向上填充
        Button(fm3, text='第一个').pack(side=BOTTOM, fill=Y, expand=YES)
        Button(fm3, text='第二个').pack(side=BOTTOM, fill=Y, expand=YES)
        Button(fm3, text='第三个').pack(side=BOTTOM, fill=Y, expand=YES)

root = Tk()     # 创建顶层窗口
root.title('Pack布局')
display = App(root)
root.mainloop()

  

在上面代码中,创建了三个 Frame 容器,其中第一个 Frame 容器内包含三个从顶部(TOP)开始排列的按钮,这意味着这三个按钮会从上到下依次排列,且这三个按钮能在水平(X)方向上填充;第二个 Frame 容器内包含三个从右边(RIGHT)开始排列的按钮,这意味着这三个按钮会从右向左依次排列;第三个 Frame 容器内包含三个从底部(BOTTOM)开始排列的按钮,这意味着这三个按钮会从下到上依次排列,且这个三个按钮能在重复(Y)方向上填充。运行结果,如图五所示。

                       图五   复杂的 Pack 布局

从图五可以看到,fm1 的三个按钮是从上到下,并且可以在水平方向上填充;fm3 的三个按钮是从下到上,并且可以在垂直方向上填充。但是 fm2 的三个按钮虽然设置了 “fill=Y, expand=YES”,但是却不能在垂直方向上填充,在创建 fm2 这个容器时的代码是:

fm2.pack(side=LEFT, padx=10, expand=YES)

这说明 fm2 本身不在任何方向上填充,因此 fm2 内的三个按钮都不能填充。如果希望 fm2 空的三个按钮也能在垂直方向上填充,可将 fm2 的 pack() 方法改为如下代码:

fm2.pack(side=LEFT, padx=10, fill=BOTH, expand=YES)

由上可知,Pack 布局非常的灵活,它完全可以实现复杂的用户界面。通常对于复杂、古怪的界面,大多是可以分解为水平排列和垂直排列,Pack 布局可以实现水平排列和垂直排列,可将多个容器进行组合,进而开发出更复杂的界面。

使用 Pack 布局进行界面开发,首先要做的事情是将程序界面进行分解,分解成水平排列的容器和垂直排列的容器,有时候还要容器嵌套容器,然后使用多个 Pack 布局的容器将它们组合在一起。

2、Grid 布局管理器

Tkinter 后来引入的 Grid 布局简单易用,管理组件也很方便。Grid 把组件空间分解成一个网格进行维护,按照行、列的方式排列组件,组件位置由其所在的行号和列号决定。行号相同而列号不同的几个组件会被依次上下排列,列号相同而行号不同的几个组件会依次左右排列。

在多数场景下,Grid 是最好用的布局方式。Grid 布局的过程就是为各个组件指定行号和列号的过程,不需要为每个网格都指定大小,Grid 布局会自动为它们设置合适的大小。

容器调用组件的 grid() 方法就进行 Grid 布局,在调用 grid() 方法时可传入多个选项,该方法支持的 ipadx、ipady、padx、pady 与pack() 方法的这些选项相同。grid() 方法还额外增加了下面这些方法:

(1)、column:指定将组件放入哪列。第一列的索引为0.

(2)、columnspan:指定组件横跨多少列。

(3)、row:指定组件放入哪行。第一行的索引为0.

(4)、rowspan:指定组件横跨多少行。 (5)、sticky:有点类似于 pack() 方法的 anchor 选项,同样支持 N(北,代表上)、E(东,代表右)、S(南,代表下)、W(西,代表左)、NW(西北,代表左上)、NE(东北,右上)、SW(西南,左下)、SE(东南,右下)、CENTER(中,默认值)等这些值

下面代码使用 Grid 布局来实现一个计算器界面。

from tkinter import *
class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建一个输入组件
        e = Entry(relief=SUNKEN, font=('Courier New', 24), width=25)
        # 对该输入组件使用 pack 布局,放在容器(或者窗口)顶部
        e.pack(side=TOP, pady=10)
        p = Frame(self.master)
        p.pack(side=TOP)
        # 定义字符串元组
        names = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                 '+', '-', '*', '/', '.', '=')
        # 遍历字符串元组
        for i in range(len(names)):
            # 创建 Button,将 Button 组件放入 p 容器中
            b = Button(p, text=names[i], font=('Verdana', 20), width=6)
            b.grid(row=i // 4, column=i % 4)

root = Tk()
root.title("Grid布局")
App(root)
root.mainloop()

  

上面代码中使用了两个布局管理器进行嵌套,先使用 Pack 布局管理两个组件:Entry(输入组件)和 Frame(容器),这两个组件会按照从上到下的方式排列。接下来使用 Grid 布局管理 Frame 容器中的 16 个按钮,分别将 16 个按钮放入不同的行、不同的列。运行代码可看到如图六所示的界面。

                           图六   使用Grid布局实现计数器界面

3、Place 布局管理器

Place 布局也叫“绝对布局”,要求程序员显式指定每个组件的绝对位置相对于其他组件的位置。要使用 Place 布局,只要调用相应组件的 place() 方法即可。该方法支持的选项如下:

(1)、x:指定组件的 X 坐标。x 为0代表位于最左边。

(2)、y:指定组件的 Y 坐标。y 为0代表位于最右边。

(3)、relx:指定组件的 X 坐标,以父容器总宽度为单位 1,该值应该在 0.0~1.0之间,其中0.0位于窗口最左边,1.0位于窗口最右边,0.5 位于窗口中间。

(4)、rely:指定组件的 Y 坐标,以父容器总高度为 1,该值应该在 0.0~1.0 之间,其中 0.0位于窗口最上边,1.0 位于窗口最下边,0.5位于窗口中间。

(5)、width:指定组件的宽度,以 pixel 为单位。

(6)、height:指定组件的高度,以 pixel 为单位。

(7)、relwidth:指定组件的宽度,以父容器总宽度为单位 1,该值应该在 0.0~1.0 之间,其中 1.0 代表整个窗口宽度,0.5代表窗口的一半宽度。

(8)、relheight:指定组件的高度,以父容器总高度为单位 1,该值应该在 0.1~1.0 之间,其中 1.0 代表整个窗口高度,0.5 代表窗口的一半高度。

(9)、bordermode:该属性支持 “inside”或“outside” 属性值,用于指定当设置组件的宽度、高度时是否计算该组件的边框宽度。

当使用 Place 布局管理容器中的组件时,需要设置组件的 x、y 或 relx、rely 选项,Tkinter 容器内的坐标系统的原点(0, 0)在左上角,其中 X 轴向右延伸,Y 轴向下延伸。如果通过 x、y 指定坐标,单位就是 pixel(像素);如果通过 relx、rely 指定坐标,则以整个父容器的宽度、高度为1。下面代码使用 Place 进行布局,并动态计算各 Label 的大小和位置,并通过 place() 方法设置各 Label 的大小和位置。代码如下:

from tkinter import *
import random
class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 定义字符串元组
        books = ('Python 入门', 'Python 初级', 'Python 进阶', 'Python 高级', 'Python 核心')
        for i in range(len(books)):
            # 生成三个随机数
            ct = [random.randrange(256) for _ in range(3)]
            grayness = int(round(0.299*ct[0] + 0.587*ct[1] + 0.114*ct[2]))
            # 将元组中的三个随机数格式化成十六进制数,转换成颜色格式
            bg_color = "#%02x%02x%02x" % tuple(ct)
            # 创建 Label,设置背景色和前景色
            lb = Label(root, text=books[i], fg='White' if grayness < 125 else 'Black',
                       bg=bg_color)
            # 使用 place() 设置该 Label 的大小和位置
            lb.place(x = 20, y = 36 + i*36, width=180, height=30)
root = Tk()
root.title('Place 布局')
# 设置窗口的大小和位置
# width * height + x_offset + y_offset
root.geometry("250x250+30+30")
App(root)
root.mainloop()

  

上面代码中的 lb.place() 行代码调用了 Label 的 place() 方法进行布局。调用 place() 方法时设置了x坐标、y坐标、width宽度、height高度四个选项,通过这四个选项即可控制各 Label 的位置和大小。

在代码中使用了随机数计算 Label 组件的背景色,并根据背景色的灰度值来计算 Label 组件的前景色。如果 grayness 小于 125,则说明背景色较深,前景色使用白色;否则说明背景色较浅,前景色使用黑色。运行这段代码看到如七所示的界面。

                图七   使用Place布局

四、 事件处理

前面设置的各种组件中还不能响应任何操作,这是因为这些组件没有绑定任何事件处理的缘故。

1、 简单的事件处理

简单的事件处理可通过 command 选项来绑定,该选项绑定一个函数或方法,当用户单击指定按钮时,通过该 command 选项绑定的函数或方法就会被触发。下面代码为按钮的 command 绑定事件处理方法:

from tkinter import *
import random
class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        self.label = Label(self.master, width=20)
        self.label['font'] = ('Microsoft Yahei', 20)
        self.label['bg'] = 'deeppink'
        self.label.pack()
        bn = Button(self.master, text="单击", command=self.change)
        bn.pack()

    # 定义事件处理方法
    def change(self):
        self.label['text'] = "Python 编程入门"
        # 生成三个随机数
        ct = [random.randrange(256) for _ in range(3)]
        grayness = int(round(0.299*ct[0] + 0.587*ct[1] + 0.114*ct[2]))
        # 将元组中的三个随机数格式化成十六进制数,转换成颜色格式
        bg_colot = "#%02x%02x%02x" % tuple(ct)
        self.label['bg'] = bg_colot
        self.label['fg'] = 'black' if grayness > 125 else 'white'
root = Tk()
root.title("简单事件处理")
App(root)
root.mainloop()

  

上面代码中,在 initWidgets() 方法中为 Button 的 command 选项指定为 self.change,这表示当该按钮被单击时,就会触发当前对象的 change() 方法,该 change() 方法会改变界面上 Label 的文本和背景色。运行程序,单击图形界面上的“单击”按钮,就会看到如图八所示的界面。

                                          图八   使用command绑定事件处理

2、事件绑定

前面的事件绑定方法很简单,但是有局限性:

(1)、不能为具体事件(如鼠标移动、按键等)绑定事件处理方法。

(2)、程序中不能获取事件相关信息。

为此,Python 有更灵活的事件绑定方式,所有 Widget 组件都提供了一个 bind() 方法,该方法可以为“任意”事件绑定事件处理方法。下面示例是一个为按钮的单击、双击事件绑定事件处理的方法,代码如下:

from tkinter import *
class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        self.show = Label(self.master, width=30, bg='white', font=('times', 20))
        self.show.pack()
        bn = Button(self.master, text='单击或双击')
        bn.pack(fill=BOTH, expand=YES)
        # 为左键单击事件绑定处理方法
        bn.bind('<Button-1>', self.one)
        # 为左键双击事件绑定处理方法
        bn.bind('<Double-1>', self.double)

    def one(self, event):
        self.show['text'] = "左键单击:%s" % event.widget['text']
    def double(self, event):
        print("左键双击,退出程序:", event.widget['text'])
        import sys; sys.exit()

root = Tk()
root.title('简单绑定')
App(root)
root.mainloop()

  

下面这两行代码在上面的代码段中为 Button 按钮绑定了单击、双击事件的处理方法:

bn.bind('<Button-1>', self.one)
bn.bind('<Double-1>', self.double)

当单击按钮时相应的调用 self.one 方法来处理,双击是调用 self.double 方法来处理。在接下来定义的的事件处理方法中,都可定义一个 event 参数,该参数是传给事件处理方法的事件对象,在上面的代码段示例了通过事件来获取事件源的方式,即通过 event.widget 来获取。对于鼠标事件来说,鼠标相对当前组件的位置可通过 event 对象中的 x 和 y 属性来获取(获取方法:event.x,event.y)。

运行上面的这段代码,并且单击界面上的“单击或双击”按钮后的结果如图九所示,当双击按钮就退出程序。

                    图九 为单击、双击事件绑定事件处理方法

由上例可知,Tkinter 是直接使用字符串来代表事件类型的,比如 <Button-1>代表鼠标左键单击事件、<Double-1>代表鼠标左键双击事件。Tkinter 事件的字符串大致是按照下面的格式书写的:

<modifier-type-detail>

其中 type 是事件字符串的关键部分,用来描述事件的种类,如鼠标、键盘事件等;modifier 代表的是事件的修饰部分,如单击、双击等;detail 用于指定事件的详情,如鼠标左键、右键、滚轮等。Tkinter 支持各种鼠标、键盘事件如下表所示。

事件 简介
<Button-detail> 鼠标单击,detail指定哪个鼠标按键单击。左键是<Button-1>、中键是<Button-2>、右键是<Button-3>、向上滚动是<Button-4>、向下滚动是<Button-5>
<modifier-Motion> 鼠标在组件上的移动事件,modifier是鼠标按键。按住鼠标左键移动是<B1-Motion>,按住中键移动是<B2-Motion>,按住右键移动是<B3-Motion>
<ButtonRelease-detail> 鼠标释放事件,detail是指释放哪个鼠标键。释放左键是<ButtonRelease-1>、中键<ButtonRelease-2>、右键<ButtonRelease-3>
<Double-Button-detail><Double-detail> 双击向上滚轮是<Double-4>,双击向下滚轮是<Double-5>
<Enter> 鼠标进入组件的事件。注意,这个不是按下键盘上的回车键,键盘上的回车键事件是<Return>
<Leave> 鼠标移动事件
<FocusIn> 组件及其包含的子组件获得焦点
<FocusOut> 组件及其包含的子组件失去焦点
<Return> 按下回车键的事件。还可以为所有按钮绑定事件处理方法。特殊的键位名称包括:Cancel、BackSpace、Tab、Return(回车)、Shift_L(左Shift,只写Shift代表任意Shift)、Control_L(左Ctrl,只写Control代表任意Ctrl)、Alt_L(左Alt,只写Alt代表任意Alt)、Pause、Caps_Lock、Escape、Prior(Page Up)、Next(Page Down)、End、Home、Left、Up、Right、Down、Print、Insert、Delete、F1~F12、Num_Lock、Scroll_Lock
<Key> 键盘上任意键的单击事件,程序可通过event获取用户单击了哪个键
a 键盘上指定键被单击的事件。比如'a'代表a键被单击,'b'代表b键被单击,不要尖括号,......
<Shift-Up> 在Shift键被按下时按Up键(上键),类似的有<Shift-Left><Shift-Down><Alt-Up><Control-Up>
<Configure> 组件大小、位置改变的事件。组件改变之后的大小、位置可通过event的width、height、x、y获取

下面代码为鼠标移动事件绑定事件处理方法,代码如下所示:

from tkinter import *
class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        lb = Label(self.master, width=30, height=3)
        lb.config(bg='lightgreen', font=('Times', 12))
        # 为绿枝移动事件绑定事件处理方法
        lb.bind('<Motion>', self.motion)
        # 为按住左键时的鼠标移动事件绑定事件处理方法
        lb.bind('<B1-Motion>', self.press_motion)
        lb.pack()
        # 下面创建的 self.show 是实例变量,变量类型是 Label() 类型控件
        self.show = Label(self.master, width=28, height=1)
        self.show.config(bg='pink', font=('Courier New', 12))
        self.show.pack()

    def motion(self, event):
        # 调用实例变量的 text 属性
        self.show['text'] = "鼠标移动到:(%s %s)" % (event.x, event.y)
        return

    def press_motion(self, event):
        self.show['text'] = "按住鼠标的位置:(%s %s)" % (event.x, event.y)
        return

root = Tk()
root.title('鼠标事件')
App(root)
root.mainloop()

  

在上面的代码段中,为鼠标移动(<Motion>)事件绑定的处理方法是下面这行代码:

lb.bind('<Motion>', self.motion)

此时鼠标在 lb 组件上移动时将会不断触发 motion()方法。

另外,为鼠标按住左键时鼠标移动(<B1-Motion>)事件绑定的事件处理方法是下面这行代码:

lb.bind('<B1-Motion>', self.press_motion)

此时按住鼠标左键在 lb 组件上移动时将会不断触发press_motion()方法。

运行这段代码,可看到如图十一所示的界面,在该界面中,图1是鼠标直接在 lb 组件上移动的结果,图2是按住鼠标左键在 lb 组件上移动的结果。

                             图十一 鼠标在组件上移动、按下左键移动的结果

下面代码利用绑定事件处理方法实现一个简单的、真正意义的计算器,代码如下:

from tkinter import *
class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()
        self.expr = None

    def initWidgets(self):
        # 创建一个输入组件
        self.show = Label(relief=SUNKEN, font=('Courier New', 24),
                          width=25, bg='white', anchor=E)
        # 对该输入组件使用 Pack 布局,放在容器顶部
        self.show.pack(side=TOP, pady=10)
        p = Frame(self.master)
        p.pack(side=TOP)
        # 定义字符串元组
        names = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                 '+', '-', '*', '/', '.', '=')
        # 遍历字符串元组
        for i in range(len(names)):
            # 创建 Button,将 Button 放入 p 组件中
            b = Button(p, text=names[i], font=('Verdana', 20), width=6)
            b.grid(row= i // 4, column= i % 4)
            # 为鼠标左键的单击事件绑定事件处理方法
            b.bind('<Button-1>', self.click)
            # 为鼠标左键的双击事件绑定事件处理方法
            if b['text'] == '=': b.bind('<Double-1>', self.clean)

    def click(self, event):
        # 单击数字或小数点
        if (event.widget['text'] in ('0', '1', '2', '3', '4', '5',
                                      '6', '7', '8', '9', '.')):
            self.show['text'] = self.show['text'] + event.widget['text']    # 1, 1+1
        # 如果单击的是运算符
        elif (event.widget['text'] in ('+', '-', '*', '/')):
            # 连接运算符
            self.show['text'] = self.show['text'] + event.widget['text']
            self.expr = self.show['text']
        elif (event.widget['text'] == '=' and self.expr is not None):
            self.expr = self.show['text']
            print(self.expr)
            # 使用 eval 函数计算表达式值
            self.show['text'] = self.expr + "=" + str(eval(self.expr))
            self.expr = None

    # 双击 “=” 按钮时,清空计算结果,将表达式设为 None
    def clean(self, event):
        self.expr = None
        self.show['text'] = ''

def cal():
    """计算器主程序"""
    root = Tk()
    root.title("计算器")
    App(root)
    root.mainloop()

if __name__ == '__main__':
    cal()

在这段代码中,为所有按钮的单击事件绑定了处理方法的是下面这行代码,用于处理计算功能:

b.bind('<Button-1>', self.click)

此外,下面这行代码还为双击“=”等号按钮绑定了事件处理方法,在处理方法中实现了清空计算结果,重新开始计算。

if b['text'] == '=': b.bind('<Double-1>', self.clean)

上面这段计算器代码的运行结果如图十二所示。

                                   图十二   简单计算器界面

猜你喜欢

转载自www.cnblogs.com/Micro0623/p/12175642.html