如何在Tkinter中运行线程并等待其完成

63 阅读2分钟

在Tkinter应用程序中,我想打开一个新的顶级窗口,并在其中显示测试结果。这个顶级窗口也是所有测试的调用者。我希望在测试期间不冻结窗口,而是在主线程中显示一个加载动画。我尝试使用线程来解决这个问题,但它不起作用,因为窗口会在测试完成前一直处于冻结状态。

  1. 解决方案 为了解决这个问题,我们需要将测试逻辑移到一个新的线程中,并使用队列来在主线程和新线程之间通信。主线程负责显示加载动画和从队列中获取测试结果,而新线程负责运行测试并将结果放入队列中。

代码例子:

import tkinter as tk
import threading
import queue

class TestDialog(tk.Toplevel):

    def __init__(self, parent, tester, url):
        super().__init__(parent)
        self.parent = parent
        self.webtester = tester

        self.__initComponents()

        self.run(url)

        self.wait_window(self)

    def __initComponents(self):
        self.transient(self.parent)

        frame = tk.Frame(self)

        self._tarea = tk.Text(frame, state='disabled', wrap='none', width=55, height=25)

        vsb = tk.Scrollbar(frame, orient=tk.VERTICAL, command=self._tarea.yview)
        self._tarea.configure(yscrollcommand=vsb.set)

        self._tarea.grid(row=1, column=0, columnspan=4, sticky="NSEW", padx=3, pady=3)
        vsb.grid(row=1, column=4, sticky='NS', pady=3)
        frame.grid(row=0, column=0, sticky=tk.NSEW)

        frame.columnconfigure(0, weight=2)
        frame.rowconfigure(1, weight=1)

        window = self.winfo_toplevel()
        window.columnconfigure(0, weight=1)
        window.rowconfigure(0, weight=1)

        self.bind("<Escape>", self.close)

        self.protocol("WM_DELETE_WINDOW", self.close)
        self.grab_set()

    def appendLine(self, msg):
        self._tarea['state'] = 'normal'
        self._tarea.insert("end", msg + '\n')
        self._tarea['state'] = 'disabled'

    def run(self, url):

        self.appendLine("Running test #1...")

        outqueue = queue.Queue()
        thr = threading.Thread(target=self.run_tests, args=(url, outqueue))
        thr.start()
        self.after(250, self.update, outqueue)

    def run_tests(self, url, outqueue):
        outqueue.put("Running test #1...")
        self.webtester.urlopen(url)
        outqueue.put("Running test #2")
        self.webtester.test2()
        outqueue.put(sentinel)

    def update(self, outqueue):
        try:
            msg = outqueue.get_nowait()
            if msg is not sentinel:
                self.appendLine(msg)
                self.after(250, self.update, outqueue)
            else:
                # By not calling after here, we allow update to truly end
                pass
        except queue.Empty:
            self.after(250, self.update, outqueue)

    def close(self, event=None):
        self.parent.setBackgroundScheme(DataTreeView.S_DEFAULT)
        self.parent.focus_set()
        self.destroy()

testDialog = TestDialog(self.parent, self._webtester, url)

在上面的代码中,我们在TestDialog类中定义了run()方法来运行测试。在run()方法中,我们创建了一个队列outqueue,并创建一个新的线程thr来运行测试。我们还使用after()方法每250毫秒调用update()方法,以从队列中获取测试结果并更新窗口。

在run_tests()方法中,我们将测试结果放入队列中。当所有测试都完成后,我们将sentinel放入队列中,以指示update()方法停止获取结果。

在update()方法中,我们将测试结果追加到窗口中的文本框中。当队列中没有更多结果时,我们将停止调用update()方法。

通过这种方法,我们可以在主