不要在后台线程中操作 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,我发现目前的实现不支持这种场景。