如何使“嵌套多线程”在 tkinter 中工作? [运行时错误:主线程不在主循环中]

2023-12-24

正如我在评论中提到的,背后的想法是这样的:

其上有一个根和一个按钮,一旦单击按钮,将首先弹出一个启动屏幕,同时顶层的元素已准备就绪,但根屏幕不会被冻结。 有什么办法可以实现这一点吗?提前致谢!

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 对象,最好避免这种情况。)

Call 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 框架事实上的标准。例如,有Dispatcher.BeginInvoke()在 WPF 中,QTimer::singleShot()在 处,gdk_threads_add_idle_full()在 Gtk 中,Activity.runOnUiThread()在安卓等

以下是涵盖 OP 场景的另一个示例,显示了启动窗口。该模式与上面相同。

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()在第二个(嵌套)后台线程中会导致问题。通过查看来源 https://github.com/python/cpython/blob/3.11/Modules/_tkinter.c#L1220,我发现目前的实现不支持这种场景。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何使“嵌套多线程”在 tkinter 中工作? [运行时错误:主线程不在主循环中] 的相关文章

随机推荐