11、事件和绑定
其实GUI程序是一种事件导向的应用程序设计,事件的来源可能是用户单击鼠标、键盘输入或是Widget状态改变。tkinter提供一些机制让我们可以针对这些事件做做更进一步的处理,这些处理的方式称为事件处理程序。
11-1 Widget的command参数
在前面介绍了许多Widget控件,许多Widget的构造方法内可以看到command参数,例如,功能按钮(Button)、数值滚动条(Scale)等。其实这就是一个Widget的事件绑定的概念,当按钮事件发生、当数值滚动条值改变......就可以通过command=callback,设计callback函数,这个callback函数就是事件处理程序。
下列程序是对前面概念的复习。
当单击功能按钮或是选择复选框时,窗口下方会做出所执行的动作,所利用的就是Widget控件构造方法内的command参数。
python
from tkinter import *
def pythonClicked():
if varPython.get():
lab.config(text="Select Python")
else:
lab.config(text="Unselect Python")
def javaClicked():
if varJava.get():
lab.config(text="Selected Java")
else:
lab.config(text="Unselected Java")
def buttonClicked():
lab.config(text="Button Clicked")
root = Tk()
root.title("ch11_1")
root.geometry("300x180")
btn = Button(root, text="Click me", command=buttonClicked)
btn.pack(anchor=W)
varPython = BooleanVar()
cbnPython = Checkbutton(root, text="Python", variable=varPython,
command=pythonClicked)
cbnPython.pack(anchor=W)
varJava = BooleanVar()
cbnJava = Checkbutton(root, text="Java", variable=varJava,
command=javaClicked)
cbnJava.pack(anchor=W)
lab = Label(root, bg="yellow", fg="blue",
height=2, width=12,
font="Times 16 bold")
lab.pack()
root.mainloop()
执行结果

11-2 事件绑定
在tkinter应用程序中最后一个指令是mainloop(),这个方法是让程序进入事件等待循环,除了如11-1节的Widget控件状态可以调用相对应的事件处理程序外,tkinter也提供了为事件绑定处理程序的机制。它的语法格式如下。
widget.bind(event, handler)
上述绑定语法中Widget是事件的来源,可以是root窗口对象,或是任意的Widget控件,例如,功能按钮、选项按钮、复选框......handler是事件处理程序。
鼠标相关的事件如下表所示。
|---------------------|-----------------------------------------|
| 鼠标事件 | 说明 |
| <Button-1> | 单击鼠标左键,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <Button-2> | 单击鼠标中键(鼠标含三个键),鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <Button-3> | 单击鼠标右键,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <Button-4> | 鼠标滑轮向上滚动,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <Button-5> | 鼠标滑轮向下滚动,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <Motion> | 鼠标移动,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <B1-Motion> | 拖拽,按住鼠标左键再移动鼠标,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <B2-Motion> | 拖拽,按住鼠标中键再移动鼠标,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <B3-Motion> | 拖拽,按住鼠标右键再移动鼠标,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <ButtonRelease-1> | 放开鼠标左键,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <ButtonRelease-2> | 放开鼠标中键,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <ButtonRelease-3> | 放开鼠标右键,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <Double-Button-1> | 连按鼠标左键,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <Double-Button-2> | 连按鼠标中键,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <Double-Button-3> | 连按鼠标右键,鼠标光标相对控件位置会被存入事件对象的x和y变量 |
| <Enter> | 鼠标光标进入Widget控件 |
| <Leave> | 鼠标光标离开Widget控件 |
[ ]
键盘相关的事件如下表所示。
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 键盘事件 | 说明 |
| <Focusln> | 键盘焦点进入Widget控件 |
| <FocusOut> | 键盘焦点离开Widget控件 |
| <Return> | 按下Enter键,键盘所有键都可以被绑定,例如,Cancel、BackSpace、Tab、Shift、Ctrl、Alt、End、Esc、Next(Page Down)、Prior(Page Up)、Home、End、Right、Left、Up、Down、F1 ~ F12、Scroll Lock、Num Lock |
| <Key> | 按下某键盘键,键值会被存储在event对象中传递 |
| <Shift-Up> | 按住Shift键时按下Up键 |
| <Alt-Up> | 按住Alt键时按下Up键 |
| <Ctrl-Up> | 按住Ctrl键时按下Up键 |
控件相关事件如下表所示。
|---------------|-------------------------------------------------|
| 控件事件 | 说明 |
| <Configure> | 更改widget控件的大小和位置,新控件大小的width与height会储存在event对象内 |
了解了以上事件绑定后,其实我们已经可以试着学习自我设计事件绑定处理程序,同时将事件处理程序与一般事件绑在一起。我们从先前的学习中可以知道,单击功能按钮时可以执行某个动作,所使用的是在Button()内增加command参数,然后单击功能按钮时让程序执行command所指定的方法。
其实设计功能按钮程序时,若是在Button()内省略command参数,所产生的影响是单击功能按钮是没有动作。然后我们可以使用本节的知识重新让单击功能按钮有动作产生,假设功能按钮对象是btn,可以使用下列方式建立单击与事件的绑定。
btn.bind("<Button-1>", event handler)
下面程序实例:重新设计程序ch11_1.py,使用事件绑定方式让单击Click me按钮后可以列出"Button clicked"字符串。对这个程序而言,功能按钮就是bind()方法的事件来源。所以第24行用btn.bind()建立绑定工作。
python
from tkinter import *
def pythonClicked():
if varPython.get():
lab.config(text="Select Python")
else:
lab.config(text="Unselect Python")
def javaClicked():
if varJava.get():
lab.config(text="Selected Java")
else:
lab.config(text="Unselected Java")
def buttonClicked(event):
lab.config(text="Button Clicked")
root = Tk()
root.title("ch11_1")
root.geometry("300x180")
btn = Button(root, text="Click me")
btn.pack(anchor=W)
btn.bind("<Button-1>", buttonClicked) # 单击Click me绑定buttonClicked
varPython = BooleanVar()
cbnPython = Checkbutton(root, text="Python", variable=varPython,
command=pythonClicked)
cbnPython.pack(anchor=W)
varJava = BooleanVar()
cbnJava = Checkbutton(root, text="Java", variable=varJava,
command=javaClicked)
cbnJava.pack(anchor=W)
lab = Label(root, bg="yellow", fg="blue",
height=2, width=12,
font="Times 16 bold")
lab.pack()
root.mainloop()
11-2-1 鼠标绑定的基本应用
下面程序实例:鼠标事件的基本应用,这个程序在执行时会建立300 x 180大小的窗口,当单击鼠标左键时,在Python Shell窗口中会列出单击事件时的坐标。
python
from tkinter import *
def callback(event):
print("clicked at", event.x, event.y)
root = Tk()
root.title("ch11_2")
frame = Frame(root, width=300, height=180)
frame.bind("<Button-1>", callback)
frame.pack()
root.mainloop()
执行结果

下面是Python Shell示范输出界面。
bash
PS E:\code\python\GuiTkinter\ch11> python .\ch11.py
clicked at 151 64
clicked at 191 108
在程序第3行绑定的事件处理程序中必须留意,callback(event)需有参数event,event名称可以自定义,这时因为事件会传递事件对象给此事件处理程序。
下面程序实例:移动鼠标时可以在窗口右下方看到鼠标目前坐标。
python
from tkinter import *
def mouseMotion(event):
x = event.x
y = event.y
text = "Mouse location - x:{}, y:{}".format(x, y)
var.set(text)
root = Tk()
root.title("ch11_2_1")
root.geometry("300x180")
x, y = 0, 0
var = StringVar()
text = "Mouse location - x:{}, y:{}".format(x, y)
var.set(text)
lab = Label(root, textvariable=var) # 建立标签
lab.pack(anchor=S, side=RIGHT, padx=10, pady=10)
root.bind("<Motion>", mouseMotion)
root.mainloop()
执行结果

下面这个程序在执行时,如果鼠标光标进入Exit功能按钮,会在黄色底的标签区域显示"鼠标进入Exit功能按钮",如果鼠标光标在离开Exit功能按钮,会在黄色底的标签区域显示"鼠标离开Exit功能按钮",如果单击Exit按钮,程序结束。
python
from tkinter import *
def enter(event):
x.set("Enter the button area.")
def leave(event):
x.set("Leave the button area.")
root = Tk()
root.title("ch11_3")
root.geometry("300x180")
btn = Button(root, text="Exit", command=root.destroy)
btn.pack(pady=30)
btn.bind("<Enter>", enter) # 进入绑定enter
btn.bind("<Leave>", leave) # 离开绑定leave
x = StringVar()
lab = Label(root, textvariable=x,
bg="yellow", fg="blue",
height=4, width=45,
font="Times 12 bold")
lab.pack(pady=30)
root.mainloop()
执行结果

11-2-2 键盘绑定的基本应用
下面程序实例:这是一个测试键盘绑定的程序,在执行时会出现窗口,若是按Esc键,将出现对话框询问是否离开,单击"是"按钮可以离开程序,单击"否"按钮程序继续。
python
from tkinter import *
from tkinter import messagebox
def leave(event):
ret = messagebox.askyesno("ch11_4", "是否离开?")
if ret == True:
root.destroy()
else:
return
root = Tk()
root.title("ch11_4")
root.bind("<Escape>", leave)
lab = Label(root, text="测试Esc键",
bg="yellow", fg="blue",
height=4, width=15,
font=('Times', 20, 'bold'))
lab.pack(padx=30, pady=30)
root.mainloop()
执行结果

下面程序实例:这个程序在执行时用作绑定事件key,整个程序执行时会将所按a...z键打印出来。这个程序第4行使用了比较少使用的repr()函数,这个函数会将参数处理成字符串。
python
from tkinter import *
def key(event): # 处理键盘按a...z事件
print("按了 " + repr(event.char) + " 键") # 输出按键字符
root = Tk()
root.title("ch11_5")
root.bind("<Key>", key) # <Key>键绑定Key函数
root.mainloop()
执行结果

下面是按键的结果
python
PS E:\code\python\GuiTkinter\ch11> python .\ch11.py
按了 'a' 键
按了 'w' 键
11-2-3 键盘与鼠标事件绑定的陷阱
我们在第8章学习了框架Frame的观念,框架本身是一个Widget控件,在使用框架时需特别小心获得焦点的概念,当事件绑定与Frame有关时,必须在Frame获得焦点时,键盘绑定才可生效。
下面程序实例:键盘与鼠标绑定Frame对象的应用。
python
from tkinter import *
def key(event): # 处理键盘按a...z事件
print("按了 " + repr(event.char) + " 键") # 输出按键字符
def coordXY(event): # 列出鼠标坐标
frame.focus_set()
print("鼠标坐标: ", event.x, event.y)
root = Tk()
root.title("ch11_6")
frame = Frame(root, width=100, height=100)
frame.bind("<Key>", key) # frame对象的<Key>绑定key
frame.bind("Button-1", coordXY) # frame对象单击绑定corrdXY
frame.pack()
frame.focus_set()
root.mainloop()
执行结果 这个程序在执行时必须将鼠标光标放在窗口内,同时吸纳有鼠标单击,这是第7行同时使用frame.focus_set()让Widget控件frame获得焦点,然后按键才可以动作。

不知道为啥,只有按键有打印,鼠标单击没反应。
至于ch11_5程序在一开始时即可执行,原因是此程序是在root窗口执行绑定,在程序被启动时窗口已经获得焦点。
11-3 取消绑定
取消绑定obj的方法如下。
obj.unbind("<xxx>") # <xxx>是绑定方式
下面程序实例:这是一个tkinter按钮程序,在tkinter按钮下方有复选框bind/unbind。如果勾选这个复选框,相当于有绑定,在单击tkinter按钮时Python Shell会列出字符串"I like tkinter"。如果没有选择这个复选框,相当于没有绑定,在单击tkinter按钮时Python Shell没有任何动作产生。
python
from tkinter import *
def buttonClicked(event):
print("I like tkinter")
def toggle(onoff):
if var.get() == True:
onoff.bind("<Button-1>", buttonClicked)
else:
onoff.unbind("<Button-1>")
root = Tk()
root.title("ch11_7")
root.geometry("300x180")
btn = Button(root, text="tkinter")
btn.pack(anchor=W, padx=10, pady=10)
var = BooleanVar()
cbtn = Checkbutton(root, text="bind/unbind", variable=var,
command=lambda:toggle(btn))
cbtn.pack(anchor=W, padx=10)
root.mainloop()
执行结果

当按钮与复选框绑定时,单击tkinter按钮会在Python Shell窗口中打印"I like tkinter"字符串。

11-4 一个事件绑定多个事件处理程序
之前程序中使用bind()方法时可以绑定一个事件处理程序,tkinter也允许我们执行一个事件绑定多个事件处理程序,同样时使用bind()方法,但是新增加的事件处理程序需要在bind()方法内增加参数add="+"。
下面程序实例:一个单击功能按钮动作,会有两个事件处理程序做出响应。
python
from tkinter import *
def btnClicked1():
print("Command event handler. I like tkinter.")
def btnClicked2(event):
print("Binding event handler. I like tkinter.")
root = Tk()
root.title("ch11_8")
root.geometry("300x180")
btn = Button(root, text="tkinter",
command=btnClicked1)
btn.pack(anchor=W, padx=10, pady=10)
btn.bind("<Button-1>", btnClicked2, add="+") # 添加事件处理程序
root.mainloop()
执行结果

若单击tkinter功能按钮,可以在python shell窗口中看到执行两个事件处理程序的结果。

从上述我们也发现了当单击按钮事件发生时,程序会先执行bind()绑定的程序,然后再执行Button()内command指定的程序。
11-5 Protocols
Protocols可以翻译为通信协议,在tkinter内可以解释为窗口管理程序(windows Manager)与应用程序(Application)之间的通信协议。tkinter也支持使用绑定概念更改此通信协议
下面程序实例:单击通信协议(Protocols)内容窗口右上角的 × 按钮可以关闭窗口,它的名称是WM_DELETE_WINDOW。这个程序会修改此协议,改为单击此按钮后增加Messagebox,询问"结束或取消",若是单击"确定"按钮才会结束此程序。
python
from tkinter import *
from tkinter import messagebox
def callback():
ret = messagebox.askokcancel("OKCANCEL", "结束或取消?")
if ret == True:
root.destroy()
else:
return
root = Tk()
root.title("ch11_9")
root.geometry("300x180")
root.protocol("WM_DELETE_WINDOW", callback)
root.mainloop()
执行结果
