PySimpleGUI教程3 - 事件逻辑速成
这一部分主要是来解决怎么“优雅”的解决事件问题。我们已经知道了PySimpleGUI使用事件循环来处理GUI事件,这里,我们考虑怎么把我们的功能代码和显示代码分离,以及解决多线程问题。
事件循环
我们用一个简单的例子来回顾一遍我们学过了什么。我们简单的来写一个计算器:
import PySimpleGUI as sg
progress = sg.ProgressBar(100)
num1_inp = sg.Input(size=5)
num2_inp = sg.Input(size=5)
result_txt = sg.Text()
add_btn = sg.Button('add')
layout = [
[num1_inp, num2_inp, result_txt],
[progress],
[add_btn]
]
window = sg.Window('slow calculator', layout)
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: break
if event == 'add':
num1 = int(num1_inp.get())
num2 = int(num2_inp.get())
result_txt.update(str(num1 + num2))
我们可以看到,确实可以正常运行:
但是很明显,这不是一个好的代码规范。我们把GUI代码和逻辑代码混写在一起,现在看起来代码还可以,等我们要实现的功能多了,代码可就要向“屎山”进化了。我们先把界面响应和功能实现分别抽离出来:
def add(a, b):
return a + b
def add_btn_click():
num1 = int(num1_inp.get())
num2 = int(num2_inp.get())
result_txt.update(add(num1, num2))
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: break
if event == 'add':
add_btn_click()
我们这次把功能代码 add() 和界面触发 add_btn_click() 单独写成一个方法,这样GUI和功能实现就不会相互干扰,你就可以专注不同部分的代码编写了。这时,功能代码完全可以单独抽到一个文件里编写,再也不用看着GUI代码头疼了。当然,这里的代码还可以做的更好:
import PySimpleGUI as sg
import api
def add_btn_click(value):
num1 = int(num1_inp.get())
num2 = int(num2_inp.get())
result_txt.update(api.add(num1, num2))
event_callbacks = {
'add': add_btn_click
}
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: break
if event in event_callbacks:
event_callbacks[event](value)
这次,我们把功能代码单独放在了外部文件 api.py 里。并且,添加了一个event_callbacks的map,以后只需要管理这个map就好了!
长时任务
接下来的问题也是编写GUI头疼的一大难题:长时任务。当你的功能代码很耗时的时候,GUI就会直接卡在那里…看下面的代码:
import time
def add(a, b):
time.sleep(5)
return a + b
假设我们的大聪明功能 add 非常耗时,算一次加法要5秒。按照之前的代码,界面就真的会卡住5秒…下面给两种解决方法:
多线程(不推荐)
一个常见的解决方案是多线程。这里需要注意的是, 谨慎使用多线程!! 看官方的解释:
当然了,这里的意思不是说不能用,只是说最好确保GUI线程是主线程。那么,直接用多线程的话,我们可以使用Window.write_event_value 函数。可以在新线程里调用这个函数来通知窗体事件。
多线程的话,我们需要让 add 函数通知主窗体一个事件,当完成运算时发送一个事件来通知结果:
def add(a, b, window):
time.sleep(5)
window.write_event_value("add_output", a+b)
write_event_value需要传入两个参,key和value。它们和 window.read() 获取的值基本是一样的。
接着GUI代码:
import PySimpleGUI as sg
import threading
import api
def add_btn_click(value):
num1 = int(num1_inp.get())
num2 = int(num2_inp.get())
threading.Thread(target=api.add, args=(num1, num2, window)).start()
def add_output(value):
result_txt.update(str(value['add_output']))
event_callbacks = {
'add': add_btn_click,
'add_output': add_output
}
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: break
if event in event_callbacks:
event_callbacks[event](value)
这里要额外添加一个 add_output 事件来接收值。是的,不推荐这种写法的原因是因为太麻烦了…官方本身有更好的封装。
perform_long_operation(推荐)
使用 window.perform_long_operation 可以更简单的解决问题。我们在事件循环里使用它。第一个参数是需要执行的函数,第二个参数是执行后需要触发的事件。对应函数的返回值会作为window.read()获得的value。和刚刚几乎一样的代码,只不过没有用多线程:
import PySimpleGUI as sg
import api
def add_btn_click(value):
num1 = int(num1_inp.get())
num2 = int(num2_inp.get())
window.perform_long_operation(lambda :api.add(num1, num2), 'add_output')
def add_output(value):
result_txt.update(str(value['add_output']))
event_callbacks = {
'add': add_btn_click,
'add_output': add_output
}
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: break
if event in event_callbacks:
event_callbacks[event](value)
不过这次,可以保留原来的add函数:
import time
def add(a, b):
time.sleep(5)
return a+b
这里注意,当需要让perform_long_operation触发带参方法时,需要使用lambda。直接按上文写法就好了。
偷懒写法(不推荐)
这里就直接把要更新的组件给传进新线程了,新线程里会直接调用组件的方法来修改值。下面的代码使用了perform_long_operation直接让add_btn_click在新进程里调用,这样连接收值的事件都不用了,直接在新线程里修改result_txt。当然不推荐这种写法,虽然我在测试里是可以这样成功运行,但官方貌似没有这种示例,所以可能有bug。这次是完整的代码:
import PySimpleGUI as sg
import api
progress = sg.ProgressBar(100)
num1_inp = sg.Input(size=5)
num2_inp = sg.Input(size=5)
result_txt = sg.Text()
add_btn = sg.Button('add')
layout = [
[num1_inp, num2_inp, result_txt],
[progress],
[add_btn]
]
window = sg.Window('slow calculator', layout)
def add_btn_click(value):
num1 = int(num1_inp.get())
num2 = int(num2_inp.get())
output = api.add(num1, num2)
result_txt.update(str(output))
event_callbacks = {
'add': add_btn_click
}
while True:
event, value = window.read()
if event == sg.WINDOW_CLOSED: break
if event in event_callbacks:
window.perform_long_operation(lambda :event_callbacks[event](value), '')
不过确实很方便就是了,目前我也没碰着过bug,写写代码自用应该没有什么问题。
进度条
最后以一个常见需求结束吧。进度条需要功能代码运行时实时提醒界面更新进度条的值。可以用write_event_value实现,也可以偷懒直接把进度条组件作为入参修改值。这里还是用偷懒写法了(因为真的懒…),所以直接在偷懒写法的版本上做修改了。
修改add方法,现在add时同时更新进度条:
import time
def add(a, b, progress_bar):
for i in range(100):
time.sleep(0.05)
progress_bar.update(i+1)
return a+b
把进组条组件传进去:
def add_btn_click(value):
num1 = int(num1_inp.get())
num2 = int(num2_inp.get())
output = api.add(num1, num2, progress)
result_txt.update(str(output))
现在进度条可以正常显示了:
|