簡體   English   中英

如何使“嵌套多線程”在 tkinter 中工作? [RuntimeError: 主線程不在主循環中]

[英]How to make "nested multithreading" work in tkinter? [RuntimeError: main thread is not in main loop]

正如我在評論中提到的,背后的想法是這樣的:

它上面有一個根和一個按鈕,一旦單擊按鈕,將首先彈出一個啟動畫面,同時頂層的元素准備就緒,但根屏幕不會凍結。 有什么辦法可以做到這一點? 提前致謝!

from tkinter import *
import customtkinter
import threading

def splash_screen():
    global splash_screen
    splash_screen = Tk()

    label = customtkinter.CTkLabel(splash_screen, text="PLEASE WAIT...")
    label.pack(pady=30, padx=30)


def initiate():
    # get elements of the toplevel
    pass

def toplevel():
    # second main window after root
    pass

def func1():
    # to avoid root freezing
    threading.Thread(target=func2).start()

def func2():
    thread = threading.Thread(target=initiate)
    thread.start()

    splash_screen()
    # wait until toplevel is ready
    thread.join()
    splash_screen.destroy()

    toplevel()


root = customtkinter.CTk()

button = customtkinter.CTkButton(root, command=func1)
button.pack(pady=10, padx=10)

root.mainloop()

追溯:

Exception in Tkinter callback
Traceback (most recent call last):
  File "D:\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "D:\Python311\Lib\tkinter\__init__.py", line 861, in callit
    func(*args)
  File "D:\Python311\Lib\site-packages\customtkinter\windows\widgets\scaling\scaling_tracker.py", line 178, in check_dpi_scaling
    if window.winfo_exists() and not window.state() == "iconic":
       ^^^^^^^^^^^^^^^^^^^^^
  File "D:\Python311\Lib\tkinter\__init__.py", line 1139, in winfo_exists
    self.tk.call('winfo', 'exists', self._w))
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: main thread is not in main loop

不要在后台線程中操作 UI 對象。(即使tkinter在某種程度上允許在后台線程中操作 UI 對象,這與其他 UI 框架不同,但最好避免這種情況。)

在后台線程中調用after_idle()與主線程進行交互,如下例所示。 使用此模式,您不需要嵌套線程。

import time
import threading
import tkinter

worker_thread = None
def worker_entry():
    def update_status(v):
        # This will run in the main UI thread.
        status_text.delete(1.0, 'end')
        status_text.insert('end', v)
    for i in range(30):
        root.after_idle(update_status, f'i:{i}')
        time.sleep(0.1)
    def join_worker():
        # This will run in the main UI thread.
        global worker_thread
        worker_thread.join()
        worker_thread = None
        start_worker_button['state'] = 'normal'
    root.after_idle(join_worker)
def on_start_worker():
    global worker_thread
    worker_thread = threading.Thread(target=worker_entry)
    worker_thread.start()
    start_worker_button['state'] = 'disabled'

root = tkinter.Tk()
start_worker_button = tkinter.Button(root, text='start worker',
    command=on_start_worker)
start_worker_button.pack()  
status_text = tkinter.Text(root)
status_text.pack()

root.mainloop()

附帶說明一下,上述模式是 UI 框架的事實標准。 比如WPF中的Dispatcher.BeginInvoke() ,Qt中的QTimer::singleShot() ,Gtk中的gdk_threads_add_idle_full() ,Android中的Activity.runOnUiThread()等。

以下是涵蓋 OP 場景的另一個示例,顯示了啟動畫面 window。模式與上述相同。

import time
import threading
import tkinter

worker_thread = None
def worker_entry():
    def update_status(v):
        text = splash_win.status_text
        text.delete(1.0, 'end')
        text.insert('end', v)
    for i in range(30):
        root.after_idle(update_status, f'i:{i}')
        time.sleep(0.1)
    def join_worker():
        global worker_thread, splash_win
        worker_thread.join()
        worker_thread = None
        splash_win.destroy()
        splash_win = None
        start_worker_button['state'] = 'normal'
    root.after_idle(join_worker)
def on_start_worker():
    global worker_thread, splash_win
    splash_win = win = tkinter.Toplevel(root)
    splash_win.status_text = text = tkinter.Text(win)
    text.pack()
    worker_thread = threading.Thread(target=worker_entry)
    worker_thread.start()
    start_worker_button['state'] = 'disabled'

root = tkinter.Tk()
start_worker_button = tkinter.Button(root, text='start worker',
    command=on_start_worker)
start_worker_button.pack()  

root.mainloop()

另一方面,我試驗了一個類似於其中一個 OP 的場景,其中在第一個后台線程中創建了第二個 Tk 根(Tcl 解釋器),並發現在第二個(嵌套的)后台線程中調用after_idle()會導致問題。 通過查看源碼,發現目前的實現並不支持這種場景。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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