![](/img/trans.png)
[英]Cannot close a running event loop,when i call run_forever i can't close the loop
[英]How do I exit my python3 application cleanly from asyncio event loop run_forever() when user clicks tkinter root window close box?
我正在嘗試為我的 Raspberry Pi 4B 制作一個 python3 應用程序,並且我有 tkinter windows 工作正常,但需要添加異步處理以允許 tkinter 小部件在處理窗口小部件發起的異步操作時響應。
測試代碼使用 asyncio 和 tkinter。 但是,如果沒有 root.mainloop(),因為 asyncio loop.run_forever() 在最后被調用。 這個想法是,當用戶單擊主窗口的關閉框時,調用 RequestQuit() 以設置 quitRequested 標志,然后當控制返回事件循環時,root.after_idle(AfterIdle) 將導致調用 AfterIdle,其中標志被檢查,如果為真,事件循環停止,或者失敗,應用程序被 exit(0) 殺死。
當用戶單擊主 window 關閉框時,循環 WM_DELETE_WINDOW 協議協程 RequestQuit 不會被調用,因此 AfterIdle 協程永遠不會獲得退出標志,我必須通過退出 XQuartz 來終止應用程序。
我通過 MacOS X Big Sur 11.5.2 上的終端使用 ssh,連接到帶有 Python 3.7.3 的 Raspberry Pi 4B。
我在這里錯過了什么?
(為簡潔起見,我沒有在此處包含小部件或其處理程序或異步處理,因為它們不是手頭問題的一部分。)
from tkinter import *
from tkinter import messagebox
import aiotkinter
import asyncio
afterIdleProcessingIntervalMsec = 500 # Adjust for UI responsiveness here.
busyProcessing = False
quitRequested = False
def RequestQuit():
global quitRequested
global busyProcessing
if busyProcessing:
answer = messagebox.askquestion('Exit application', 'Do you really want to abort the ongoing processing?', icon='warning')
if answer == 'yes':
quitRequested = True
def AfterIdle():
global quitRequested
global loop
global root
if not quitRequested:
root.after(afterIdleProcessingIntervalMsec, AfterIdle)
else:
print("Destroying GUI at: ", time.time())
try:
loop.stop()
root.destroy()
except:
exit(0)
if __name__ == '__main__':
global root
global loop
asyncio.set_event_loop_policy(aiotkinter.TkinterEventLoopPolicy())
loop = asyncio.get_event_loop()
root = Tk()
root.protocol("WM_DELETE_WINDOW", RequestQuit)
root.after_idle(AfterIdle)
# Create and pack widgets here.
loop.run_forever()
你的程序不能工作的原因是沒有 Tk 事件循環,或者它的等價物。 沒有它,Tk 將不會處理事件; 不會運行 Tk 回調函數。 因此,您的程序不會響應 WM_DELETE_WINDOW 事件或任何其他事件。
幸運的是,Tk 可以用來執行相當於 asyncio.Task 的事件循環,而且這並不難。 基本概念是像這樣編寫 function,其中“w”是任何 tk 小部件:
async def new_tk_loop():
while some_boolean:
w.update()
await asyncio.sleep(sleep_interval_in_seconds)
當你准備好開始處理 tk 事件時,這個 function 應該被創建為 asyncio.Task,並且應該繼續運行直到你准備好停止這樣做。
這是一個 class,TkPod,我將其用作任何 Tk + asyncio 程序的基礎。 還有一個簡單的小演示程序,說明如何從另一個任務中關閉 Tk 循環。 如果您在 5 秒過去之前單擊“X”,程序將通過退出主循環 function 立即關閉。 5 秒后,程序將通過取消 mainloop 任務關閉。
我使用 0.05 秒的默認睡眠間隔,這似乎工作得很好。
退出這樣的程序時,需要考慮一些事情。
當您單擊主 window 上的“X”按鈕時,object 將其app_closing
變量設置為 false。 如果你需要做一些其他的清理工作,你可以繼承 Tk 並覆蓋方法close_app
。
退出主循環不會調用destroy
function。 如果您需要這樣做,則必須單獨進行。 class 是一個上下文管理器,因此您可以確保使用with
塊調用destroy
。
像任何異步任務一樣,可以取消mainloop
。 如果這樣做,則需要捕獲該異常以避免回溯。
#! python3.8
import asyncio
import tkinter as tk
class TkPod(tk.Tk):
def __init__(self, sleep_interval=0.05):
self.sleep_interval = sleep_interval
self.app_closing = False
self.loop = asyncio.get_event_loop()
super().__init__()
self.protocol("WM_DELETE_WINDOW", self.close_app)
# Globally suppress the Tk menu tear-off feature
# In the following line, "*tearOff" works as documented
# while "*tearoff" does not.
self.option_add("*tearOff", 0)
def __enter__(self):
return self
def __exit__(self, *_x):
self.destroy()
def close_app(self):
self.app_closing = True
# I don't know what the argument n is for.
# I include it here because pylint complains otherwise.
async def mainloop(self, _n=0):
while not self.app_closing:
self.update()
await asyncio.sleep(self.sleep_interval)
async def main():
async def die_in5s(t):
await asyncio.sleep(5.0)
t.cancel()
print("It's over...")
with TkPod() as root:
label = tk.Label(root, text="Hello")
label.grid()
t = asyncio.create_task(root.mainloop())
asyncio.create_task(die_in5s(t))
try:
await t
except asyncio.CancelledError:
pass
if __name__ == "__main__":
asyncio.run(main())
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.