如何从Tkinter按钮调用异步函数?
我来帮你拆解一下你遇到的两个错误,然后给出可行的解决办法~
首先说你碰到的第一个错误:当你用timer = tk.Button(root, text='Timer', command=wait())时,程序会在创建按钮的瞬间就执行wait(),而wait()是个异步协程函数,执行后返回的是一个协程对象,但你没有对它做await处理,所以就会抛出“coroutine 'wait' was never awaited”的错误。而且Tkinter的command参数需要的是一个无参的可调用对象(比如一个不带括号的函数名),你直接写wait()相当于把协程对象传给了command,这本身就不符合要求。
然后是第二个错误:用lambda: asyncio.run(wait())时,为什么会报错“asyncio.run() cannot be called from a running event loop”?因为你最后用asyncio.run(gui())启动了一个asyncio事件循环,而gui()里的root.mainloop()是阻塞式的,这个事件循环会一直处于运行状态。而asyncio.run()的本质是创建一个新的事件循环并运行它,它不允许在已经有事件循环运行的情况下被调用,所以就触发了这个错误。
那正确的做法是什么呢?核心思路是:在已运行的asyncio事件循环中提交协程任务,而不是创建新的事件循环。具体可以这么改:
步骤1:修改按钮的回调逻辑
不要用asyncio.run(),而是获取当前正在运行的asyncio事件循环,然后用loop.create_task()把异步协程提交到这个循环中执行。
步骤2:让Tkinter的主循环和asyncio事件循环共存
原来的root.mainloop()是阻塞式的,会把asyncio的事件循环挂起,我们可以改成用root.update()定期刷新Tkinter的界面,同时用await asyncio.sleep(0.01)给asyncio事件循环留出处理任务的时间。
完整的修正代码
import asyncio import time import tkinter as tk async def sleep(): await asyncio.sleep(1) async def wait(): start = time.time() await sleep() print(f'Elapsed: {time.time() - start}') async def gui(): root = tk.Tk() # 获取当前正在运行的asyncio事件循环 loop = asyncio.get_running_loop() def button_callback(): # 把wait协程提交到已有事件循环中执行 loop.create_task(wait()) timer = tk.Button(root, text='Timer', command=button_callback) timer.pack() # 让Tkinter界面和asyncio事件循环同时运行 while root.winfo_exists(): root.update() await asyncio.sleep(0.01) async def main(): # 先执行一次wait()(可选,根据你的需求调整) await wait() # 启动GUI逻辑 await gui() # 只启动一次asyncio事件循环 asyncio.run(main())
额外说明
你原来代码最后连续调用asyncio.run(main())和asyncio.run(gui())也是有问题的,因为asyncio.run()会一直运行直到传入的协程执行完毕,所以main()执行完才会启动gui(),而且两次调用会创建两个独立的事件循环。修正后的代码只调用一次asyncio.run(main()),把所有逻辑都整合到同一个事件循环中,这样就不会有冲突了。
备注:内容来源于stack exchange,提问作者Psionman

