💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
### 8.3.2 事件处理 GUI 应用程序的核心是对各种交互事件的处理程序。应用程序一般在完成建立图形界面 等初始化工作后都会进入一个事件循环,等待事件发生并触发相应的事件处理程序。Tkinter 程序通过 mainloop 方法进入事件循环,而事件与相应事件处理程序之间是通过绑定建立关联 的。 最常见的绑定形式是针对构件实例的: ``` <构件实例>.bind(<事件描述符>,<事件处理程序>) ``` 其语义是:若针对&lt;构件实例&gt;发生了与&lt;事件描述符&gt;相匹配的事件,则调用&lt;事件处理程序&gt;。 调用事件处理程序时,系统会传递一个 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() ``` 本程序在根窗口中添加了一个框架构件,然后把框架构件与&lt;Button-1&gt;事件进行了绑定, 对应&lt;Button-1&gt;事件的回调函数是 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() ``` 本程序创建了一个按钮构件,该按钮与按任意键事件&lt;Key&gt;进行绑定,事件处理程序是 回调函数 printInfo。此程序的 b.focus_set()语句将按钮设为键盘焦点,从而按下任何键都会由 按钮响应,并触发 printInfo 函数来处理事件,处理过程是显示按下的键的字符。读者可以思 考一下:本例中绑定的是&lt;Key&gt;事件,运行时如果输入上档键(如@#$%^&之类)会出现什 么结果呢? 绑定到多个事件 一个构件可以响应多种事件,例如下面这个程序同时响应鼠标和键盘事件: 【程序 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() ``` 此程序在根窗口中创建一个框架构件,并为框架构件同时绑定了任意键事件&lt;Key&gt;和鼠 标左键事件&lt;Button-1&gt;。运行此程序,先在框架中点击鼠标,从而触发 callback2 函数的执行, 该函数又将框架设置为键盘焦点。此后,按下任何键都将触发 callback1 函数的执行,其功能 是显示所按的字符。运行此程序后如果没有在框架中先点击鼠标,则框架未获得焦点,也就 不会对键盘事件进行处理。 当构件绑定的多个事件之间具有“特殊与一般”的关系,总是调用最“近”的事件处理 程序。例如,如果将某构件与任意键事件&lt;Key&gt;绑定,相应事件处理程序是 h1,又与回车键 事件&lt;Return&gt;绑定,相应事件处理程序是 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 所示 的输出。这是因为&lt;Return&gt;事件首先被拥有焦点的按钮实例 b 捕获,并执行 printInstance 函 数。此后,&lt;Return&gt;事件还将向 b 的各级上层传递,从而依次被 b 所属的 Button 类、b 所属 的顶层窗口 root、b 所属的应用程序这三个层次捕获,分别导致 printClass、printTopleve 和 printApp 三个函数的执行。 ![](https://box.kancloud.cn/2016-02-22_56cafce65ccd8.png) 图 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() ``` 虚拟事件 我们也可以自定义新的事件类型,称为虚拟事件。虚拟事件的形式是&lt;&lt;事件名称&gt;&gt;,可 利用构件的 event_add 方法来创建。例如,如果想为构件 w 创建一个新事件&lt;&lt;MyEvent&gt;&gt;, 该事件由鼠标右键或键盘上的 Pause 键触发,则执行下列语句: ``` w.event_add("<<MyEvent>>","<Button-3>","<KeyPress-Pause>") ``` 此后就可以像系统定义的事件一样使用了。例如: ``` w.bind("<<MyEvent>>",myHandler) ``` 在构件 w 上点击右键或按下 Pause 键都会触发函数 myHandler。