PySimpleGUI教程5 - 高级和自定义事件
PySimpleGUI中,一般只使用按钮点击触发事件。但有时我们希望监测一些自定义事件,例如用户自己编写的回调事件。又或者,还有一些其他高级事件,例如键盘输入、鼠标移动…该怎么办呢?下面给一些解决方案:
自定义事件
当我们需要手动触发事件循环时,可以使用 windows
的 write_event_value
方法(前面已经介绍过):
def write_event_value(self, key: Any, value: Any) -> None
Adds a key & value tuple to the queue that is used by threads to communicate with the window
Params:
key – The key that will be returned as the event when reading the window
value – The value that will be in the values dictionary
我们可以在其他线程触发自定义事件:
import PySimpleGUI as sg
import threading, time
def looping(window):
called = 0
while True:
called += 1
time.sleep(1)
window.write_event_value('loop_event', {'called': called})
txt = sg.Text('------', key='test')
layout = [[txt]]
window = sg.Window('test', layout, finalize=True)
threading.Thread(target=looping, args=(window,)).start()
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: exit()
print(event, value) # 打印所有触发的事件
可以看到打印结果如下:
loop_event {'loop_event': {'called': 1}}
loop_event {'loop_event': {'called': 2}}
loop_event {'loop_event': {'called': 3}}
loop_event {'loop_event': {'called': 4}}
loop_event {'loop_event': {'called': 5}}
...
控件交互事件
按官方文档,当控件的 enable_events
为 true
时,当该元素被交互时(例如单击,输入一个字符),则立即生成一个事件,导致 window.read()
调用返回。例如,对一个输入框,每当输入的字符更新时,都会触发事件。下面是示例代码:
import PySimpleGUI as sg
inp = sg.Input(enable_events=True, key='input')
layout = [[inp]]
window = sg.Window('test', layout)
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: exit()
print('EVENTS: ',event)
这时在每个在输入框里的键盘输入都会被检测到。
全局键盘事件
对于 window
组件可以开启 return_keyboard_events
对键盘进行全局监听。示例:
import PySimpleGUI as sg
button = sg.Button('test', key='test')
layout = [[button]]
window = sg.Window('test', layout, return_keyboard_events=True)
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: exit()
print(event)
这样就可以在event获取对应的keycode:
Shift_L:16
Shift_L:16
Escape:27
Control_L:17
1
Q
bind高级事件
然而有一些监听事件是PySimpleGUI做不到的。这时就需要调用组件的bind函数来绑定Tkinter的事件(因为PySimpleGUI是基于Tkinter开发的)。bind函数文档如下:
def bind(self, bind_string: str, key_modifier: str, propagate: bool = True) -> None
Used to add tkinter events to an Element. The tkinter specific data is in the Element’s member variable user_bind_event :param bind_string: The string tkinter expected in its bind function :type bind_string: (str) :param key_modifier: Additional data to be added to the element’s key when event is returned :type key_modifier: (str) :param propagate: If True then tkinter will be told to propagate the event to the element :type propagate: (bool)
Params:
bind_string – The string tkinter expected in its bind function
key_modifier – Additional data to be added to the element’s key when event is returned
propagate – If True then tkinter will be told to propagate the event to the element
这里, bind_string
填待绑定的tkinter事件名(event sequences)。python-course站点给出了一些示例:
Event | Description |
---|---|
<Button> | 表示鼠标按下的事件。特别的,<Button-1>表示鼠标左键,<Button-2>表示鼠标中键,<Button-3>表示鼠标右键。 |
<Motion> | 表示鼠标移动的事件。特别的,鼠标按下时,鼠标的左中右键被表示为 <B1-Motion>, <B2-Motion> and <B3-Motion>。 |
<ButtonRelease> | 表示鼠标松开的事件。特别的,<ButtonRelease-1>表示鼠标左键,<ButtonRelease-2>表示鼠标中键,<ButtonRelease-3>表示鼠标右键。 |
<Enter> | 表示鼠标移入控件的事件。 |
<Leave> | 表示鼠标移出控件的事件。 |
在这里查看完整一点的列表:https://python-course.eu/tkinter/events-and-binds-in-tkinter.php
最完整的tkinter事件列表可参见:https://manpages.debian.org/bullseye/tk8.6-doc/bind.3tk.en.html
key_modifier
用来填写给event返回的附加值。例如,一个按钮控件的 key
是 btn
,bind
函数里的 key_modifier
是_move
,那么 bind
事件触发时,在事件循环里获取的event值就是 btn_move
。
下面是一个完整的bind示例:
import PySimpleGUI as sg
button = sg.Button('TEST', key='test') # 必须设置 key
layout = [[button]]
window = sg.Window('test', layout, finalize=True) # 必须设置 finalize
button.bind('<Motion>', '_move') # 绑定鼠标移动的事件
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: exit()
print('EVENTS: ', event)
这里给按钮绑定了 <Motion>
事件,当鼠标在按钮上移动时,就会触发事件。因为 key_modifier
是 _move
,所以事件名为 test_move
。打印结果如下:
EVENTS: test_move
EVENTS: test_move
EVENTS: test_move
EVENTS: test_move
...
这里要注意的是,由于bind属于对组件的动态操作,所以调用时必须确保窗体构建完毕,因此需要给window
加上 finalize
。
也可以参考官方仓库里的issue:https://github.com/PySimpleGUI/PySimpleGUI/issues/2639
获取bind事件值
可惜,可以看出,在上面的示例中,我们在事件循环中只能获取事件名,但不能获取事件值。什么是事件值?一般来说,当一个事件触发时,我们还希望获得事件的具体情况。例如,当鼠标在控件上移动时,我们更希望知道鼠标的位置——不然只知道鼠标在移动有什么用?像这种事件的具体信息都会打包成一个事件值。
这时,我们可以使用控件的 user_bind_event
属性。当用户自定义的 bind
事件触发时,user_bind_event
会更新为本次事件的事件值。这里的事件值对象是tkinter里的 Event
类构造出的对象。其文档如下:
class Event: """Container for the properties of an event. Instances of this type are generated if one of the following events occurs: KeyPress, KeyRelease - for keyboard events ButtonPress, ButtonRelease, Motion, Enter, Leave, MouseWheel - for mouse events Visibility, Unmap, Map, Expose, FocusIn, FocusOut, Circulate, Colormap, Gravity, Reparent, Property, Destroy, Activate, Deactivate - for window events. If a callback function for one of these events is registered using bind, bind_all, bind_class, or tag_bind, the callback is called with an Event as first argument. It will have the following attributes (in braces are the event types for which the attribute is valid): serial - serial number of event num - mouse button pressed (ButtonPress, ButtonRelease) focus - whether the window has the focus (Enter, Leave) height - height of the exposed window (Configure, Expose) width - width of the exposed window (Configure, Expose) keycode - keycode of the pressed key (KeyPress, KeyRelease) state - state of the event as a number (ButtonPress, ButtonRelease, Enter, KeyPress, KeyRelease, Leave, Motion) state - state as a string (Visibility) time - when the event occurred x - x-position of the mouse y - y-position of the mouse x_root - x-position of the mouse on the screen (ButtonPress, ButtonRelease, KeyPress, KeyRelease, Motion) y_root - y-position of the mouse on the screen (ButtonPress, ButtonRelease, KeyPress, KeyRelease, Motion) char - pressed character (KeyPress, KeyRelease) send_event - see X/Windows documentation keysym - keysym of the event as a string (KeyPress, KeyRelease) keysym_num - keysym of the event as a number (KeyPress, KeyRelease) type - type of the event as a number widget - widget in which the event occurred delta - delta of wheel movement (MouseWheel) """
从文档中可以看出,可以通过 user_bind_event.x
和 user_bind_event.y
来获取鼠标当时的位置。看如下示例:
import PySimpleGUI as sg
button = sg.Button('test', key='test') # 必须设置 key
layout = [[button]]
window = sg.Window('test', layout, finalize=True) # 必须设置 finalize
button.bind('<Motion>', '_move') # 绑定鼠标移动的事件
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: exit()
if event == 'test_move':
print('Mouse at:', button.user_bind_event.x, button.user_bind_event.y)
鼠标在 button
上移动时, test_move
事件被触发,同时 button
的 user_bind_event
也会更新。此时便能获取鼠标的位置。输出如下:
Mouse at: 10 1
Mouse at: 11 1
Mouse at: 12 1
Mouse at: 13 1
Mouse at: 14 2
Mouse at: 15 3
...
例1 - 滚轮事件
我们用两个例子来说明如何自定义事件。第一个例子是一个文本控件,当鼠标滚轮在文本上上滚动时,文本显示UP,反之显示DOWN。
初始布局如下:
import PySimpleGUI as sg
txt = sg.Text('------', key='test')
layout = [[txt]]
window = sg.Window('test', layout, finalize=True)
通过查询上面的文档,我们可以知道滚轮事件的 bind_string
为 <MouseWheel>
,绑定即可:
txt.bind('<MouseWheel>', '_wheel')
接下来编写事件循环,并获取事件值对象,打印一下试试:
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: exit()
if event == 'test_wheel':
bind_event = txt.user_bind_event
print(bind_event)
可以看到 bind_event
结构如下:
<MouseWheel event send_event=True state=Mod1 delta=-120 x=19 y=9>
不难看出,使用 delta
属性可以获知滚轮的位移。正数就是向上滚,反之就是向下。因此,加入判断即可:
if bind_event.delta > 0:
txt.update('UP')
else:
txt.update('DOWN')
下面是完整代码:
import PySimpleGUI as sg
txt = sg.Text('------', key='test')
layout = [[txt]]
window = sg.Window('test', layout, finalize=True)
txt.bind('<MouseWheel>', '_wheel')
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: exit()
if event == 'test_wheel':
bind_event = txt.user_bind_event
if bind_event.delta > 0:
txt.update('UP')
else:
txt.update('DOWN')
运行效果正常:
例2 - 长按事件
接下来,我们为按钮编写一个长按事件,当按住按钮时,事件就一直被触发。GUI如下:
import PySimpleGUI as sg
import threading, time
button = sg.Button('test', key='test') # 必须设置 key
layout = [[button]]
window = sg.Window('test', layout, finalize=True) # 必须设置 finalize
虽然Tkinter里没有长按事件的定义,但我们可以监控鼠标按下和松开的事件。这里用到的 bind_string
是 <Button-1>
和 <ButtonRelease-1>
。由于按钮的默认事件就是ButtonRelease,所以我们不用bind这个事件:
# 绑定按钮按下的事件
button.bind('<Button-1>', '-down')
# button按钮松开的事件就是原生事件,无需绑定,其他元素需要
# button.bind('<ButtonRelease-1>', '-up')
通过这两个事件的触发,我们定义一个flag来表示按钮是否在按下状态。如果是,就反复触发按钮按下的事件。我们用额外的线程来实现:
test_pressed_flag = False
def trigger_callback(window):
while True:
time.sleep(0.1)
if test_pressed_flag:
window.write_event_value('test-pressed', {})
threading.Thread(target=trigger_callback, args=(window,)).start()
接着在事件循环中更新这个flag:
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: exit()
if event == 'test-down': # mouse down
test_pressed_flag = True
if event == 'test': # mouse up
test_pressed_flag = False
print(event)
现在 test-pressed
事件就可以被正常识别了!下面是打印结果:
test-down
test-pressed
test-pressed
test-pressed
test-pressed
test
完整代码:
import PySimpleGUI as sg
import threading, time
button = sg.Button('test', key='test') # 必须设置 key
layout = [[button]]
window = sg.Window('test', layout, finalize=True) # 必须设置 finalize
# 绑定按钮按下的事件
button.bind('<Button-1>', '-down')
# button按钮松开的事件就是原生事件,无需绑定,其他元素需要
# button.bind('<ButtonRelease-1>', '-up')
test_pressed_flag = False
def trigger_callback(window):
while True:
time.sleep(0.1)
if test_pressed_flag:
window.write_event_value('test-pressed', {})
threading.Thread(target=trigger_callback, args=(window,)).start()
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: exit()
if event == 'test-down': # mouse down
test_pressed_flag = True
if event == 'test': # mouse up
test_pressed_flag = False
print(event)