8.3.2 事件处理

GUI 应用程序的核心是对各种交互事件的处理程序。应用程序一般在完成建立图形界面 等初始化工作后都会进入一个事件循环,等待事件发生并触发相应的事件处理程序。Tkinter 程序通过 mainloop 方法进入事件循环,而事件与相应事件处理程序之间是通过绑定建立关联 的。

最常见的绑定形式是针对构件实例的:

<构件实例>.bind(<事件描述符>,<事件处理程序>)

其语义是:若针对<构件实例>发生了与<事件描述符>相匹配的事件,则调用<事件处理程序>。 调用事件处理程序时,系统会传递一个 Event 类的对象作为实际参数,该对象描述了所发生 事件的详细信息。

事件处理程序一般都是用户自定义的函数。这种函数在应用程序中定义,但不由应用程 序调用,而是由系统调用,所以一般称为回调(callback)函数。

GUI 应用程序经常封装为类,在这种情况下,事件处理程序常常定义为应用程序类的方 法。我们将在 8.4.1 中通过例子详细介绍这种做法。

先看一个处理鼠标点击事件的例子:

【程序 8.6】eg8_6.py

from Tkinter import *
def callback(event):
    print "clicked at", event.x, event.y
root = Tk()
f = Frame(root, width=100, height=100) 
f.bind("<Button-1>", callback) 
f.pack()
root.mainloop()

本程序在根窗口中添加了一个框架构件,然后把框架构件与<Button-1>事件进行了绑定, 对应<Button-1>事件的回调函数是 callback,意思是每当在框架中点击鼠标左键时,都将触发 callback 执行。系统执行 callback 时,将一个描述事件的 Event 类对象作为参数传递给该函数, 该函数从事件对象参数中提取点击位置信息并在控制台输出类似“clicked at 44 63”的信息。

键盘事件与焦点

当图形界面中存在许多构件时,如果是用鼠标直接点击某个窗口或构件,程序自然就知

道要操作哪个构件。但如果是按一下键盘,应该由哪个构件做出响应呢?GUI 引入了“焦点” 概念:图形界面中有唯一焦点,任何时刻只能有一个构件占有焦点,键盘事件总是发送到当 前占有焦点的构件。焦点的位置可以通过构件的 focus_set()方法来设置,也可以用键盘上的 Tab 键来轮转。因此,键盘事件处理比鼠标事件处理多了一个设置焦点的步骤,如下例所示:

【程序 8.7】eg8_7.py

from Tkinter import *
def printInfo(event):
    print "pressed", event.char
root = Tk()
b = Button(root,text = 'Press any key') 
b.bind('<Key>',printInfo)
b.focus_set() 
b.pack() 
root.mainloop()

本程序创建了一个按钮构件,该按钮与按任意键事件<Key>进行绑定,事件处理程序是 回调函数 printInfo。此程序的 b.focus_set()语句将按钮设为键盘焦点,从而按下任何键都会由 按钮响应,并触发 printInfo 函数来处理事件,处理过程是显示按下的键的字符。读者可以思 考一下:本例中绑定的是<Key>事件,运行时如果输入上档键(如@#$%^&之类)会出现什 么结果呢?

绑定到多个事件 一个构件可以响应多种事件,例如下面这个程序同时响应鼠标和键盘事件:

【程序 8.8】eg8_8.py

from Tkinter import *
def callback1(event):
    print "pressed", event.char
def callback2(event): f.focus_set()
    print "clicked at", event.x, event.y
root = Tk()
f = Frame(root, width=100, height=100) 
f.bind("<Key>", callback1) 
f.bind("<Button-1>", callback2) 
f.pack()
root.mainloop()

此程序在根窗口中创建一个框架构件,并为框架构件同时绑定了任意键事件<Key>和鼠 标左键事件<Button-1>。运行此程序,先在框架中点击鼠标,从而触发 callback2 函数的执行, 该函数又将框架设置为键盘焦点。此后,按下任何键都将触发 callback1 函数的执行,其功能 是显示所按的字符。运行此程序后如果没有在框架中先点击鼠标,则框架未获得焦点,也就 不会对键盘事件进行处理。

当构件绑定的多个事件之间具有“特殊与一般”的关系,总是调用最“近”的事件处理 程序。例如,如果将某构件与任意键事件<Key>绑定,相应事件处理程序是 h1,又与回车键 事件<Return>绑定,相应事件处理程序是 h2,那么当按下回车键时,处理此事件的将是 h2。

绑定层次

前面三个例子中都是针对某个构件实例进行事件绑定,称为“实例绑定”。实例绑定只对 该构件实例有效,对其他实例——即使是同类型的构件——是无效的。除了实例绑定,Tkinter 还提供了其他事件绑定方式。实际上,Tkinter 中共有不同层次的四种绑定方法:

  • 实例绑定:绑定只对特定构件实例有效,用构件实例的 bind 方法实现。

  • 类绑定:绑定针对构件类,故对该类的所有实例有效,可用任何构件实例的 bind_class 方法实现。例如,为使 Button 类的所有实例都以同样方式响应回车键事件,可执行:

    root.bind_class("Button","<Return>",callback)
    
  • 窗口绑定:绑定对窗口(根窗口或顶层窗口)中的所有构件有效。用窗口的 bind 方 法实现,例如为使窗口中所有构件都以同样方式响应鼠标右键点击事件,可执行:

    root.bind('<Button-3>',callback)
    
  • 应用程序绑定:绑定对应用程序中的所有构件都有效。用任一构件实例的 bind_all 方法实现。例如,很多应用程序在运行时可以随时按下 F1 键以使用户得到帮助信 息,这可以通过建立 F1 键的应用程序绑定来实现:

    root.bind_all('<F1>',printHelp)
    

下面这个例子演示了事件传递与绑定层次结合所带来的后果:

【程序 8.9】eg8_9.py

from Tkinter import *
def printInstance(event):
    print 'Instance:',event.keycode
def printToplevel(event):
    print 'Toplevel:',event.keycode
def printClass(event):
    print 'Class:',event.keycode
def printApp(event):
    print 'Application:',event.keycode
root = Tk()
b = Button(root,text = 'Press Return') 
b.bind('&lt;Return&gt;',printInstance) 
b.winfo_toplevel().bind('&lt;Return&gt;',printToplevel) 
root.bind_class('Button','&lt;Return&gt;',printClass) 
root.bind_all('&lt;Return&gt;',printApp)
b.pack() 
b.focus_set() 
root.mainloop()

本程序中定义了四个层次的事件绑定,运行此程序并按下回车键,将得到如图 8.24 所示 的输出。这是因为<Return>事件首先被拥有焦点的按钮实例 b 捕获,并执行 printInstance 函 数。此后,<Return>事件还将向 b 的各级上层传递,从而依次被 b 所属的 Button 类、b 所属 的顶层窗口 root、b 所属的应用程序这三个层次捕获,分别导致 printClass、printTopleve 和 printApp 三个函数的执行。

图 8.24 多层绑定

关于程序 8.9 还有几点要说明:(1)程序中的 b.winfo_toplevel()方法返回 b 所属的顶层构 件,本例中即根窗口 root;(2)对程序代码与输出结果进行比较后可看出,事件的传递层次 与程序中绑定语句的次序没有关系;(3)类绑定与应用程序绑定可以通过任何构件来设置, 因此将上面程序中的 root.bind_class 和 root.bind_all 改成 b.bind_class 和 b.bind_all,结果也是 一样的。

协议处理

用过 Word 的读者都知道,如果编辑了文档还没有保存就去关闭程序窗口,Word 会弹出 一个对话框,询问用户是否要保存当前文档。如果我们希望利用事件绑定到事件处理程序来 实现这种功能,就面临一个问题:“关闭窗口”并不属于前面介绍过的事件类型,因此无法用 事件绑定来处理。

为此,Tkinter 提供了一种称为“协议处理”的机制,用于应用程序处理来自操作系统窗 口管理器的协议消息。处理过程是这样的:当用户企图关闭窗口,操作系统的窗口管理器就 会生成一条 WM_DELETE_WINDOW 的协议消息并发送给应用程序,应用程序再调用相应的 处理程序来处理这条消息。

窗口构件有一个称为 protocol 的方法,用于定义对协议消息的处理程序:

<窗口构件>.protocol("WM_DELETE_WINDOW",<处理程序>)

其中窗口构件可以是根窗口或顶层窗口,处理程序是函数或方法。如此定义之后,当用户试 图关闭窗口时,我们自己的处理程序就会接管控制。处理程序可以弹出一个消息框询问用户 是否要保存当前数据,或者干脆忽略关闭窗口的请求。处理完毕之后,可以在处理程序中完 成关闭窗口的操作,方法是调用窗口的 destroy 方法。例如:

【程序 8.10】eg8_10.py

from Tkinter import * 
from tkMessageBox import *
def callback():
    if askokcancel("Quit","Do you really wish to quit?"): 
        root.destroy()
root = Tk() 
root.protocol("WM_DELETE_WINDOW", callback) 
root.mainloop()

虚拟事件

我们也可以自定义新的事件类型,称为虚拟事件。虚拟事件的形式是<<事件名称>>,可 利用构件的 event_add 方法来创建。例如,如果想为构件 w 创建一个新事件<<MyEvent>>, 该事件由鼠标右键或键盘上的 Pause 键触发,则执行下列语句:

w.event_add("<<MyEvent>>","<Button-3>","<KeyPress-Pause>")

此后就可以像系统定义的事件一样使用了。例如:

w.bind("<<MyEvent>>",myHandler)

在构件 w 上点击右键或按下 Pause 键都会触发函数 myHandler。