簡體   English   中英

當用戶單擊 tkinter 根 window 關閉框時,如何從 asyncio 事件循環 run_forever() 干凈地退出我的 python3 應用程序?

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM