创建线程安全的Toplevel小部件

时间:2013-04-05 22:12:46

标签: python python-2.7 thread-safety tkinter

我正在尝试学习如何使用线程模块。我按照这里的说明进行操作:http://effbot.org/zone/tkinter-threads.htm

我希望测试脚本会:

  1. 每两秒打印一次“计数”
  2. 显示弹出对话窗口(也是每2秒)
  3. 应该允许弹出窗口累积(如果我暂时不点击“确定”,应该有 多个弹出窗口)
  4. 但是,当我运行此脚本时,它会冻结主窗口并在一段时间后崩溃。我想我没有正确实现线程模块。

    有人可以看看并指出我做错了吗?

    这是我到目前为止所尝试的内容:

    from Tkinter import *
    import thread
    import Queue
    import time
    
    class TestApp:
        def __init__(self, parent):
            self.super_Parent = parent
            self.main_container = Frame(parent)
            self.main_container.pack()
            self.top_frame = Frame(self.main_container)
            self.top_frame.pack(side=TOP)
            self.bottom_frame = Frame(self.main_container)
            self.bottom_frame.pack(side=TOP)
            self.text_box = Text(self.top_frame)
            self.text_box.config(height=20, width=20)
            self.text_box.pack()
            self.queue = Queue.Queue()
            self.update_me()
    
        def show_popup(self):
            self.my_popup = Toplevel(self.main_container)
            self.my_popup.geometry('100x100')
            self.popup_label = Label(self.my_popup, text="Hello!")
            self.popup_label.pack(side=TOP)
            self.pop_button = Button(self.my_popup, text="OK", command=self.my_popup.destroy)
            self.pop_button.pack(side=TOP)
    
        def write(self, line):
            self.queue.put(line)
    
        def update_me(self):
            try:
                while 1:
                    line = self.queue.get_nowait()
                    if line is None:
                        self.text_box.delete(1.0, END)
                    else:
                        self.text_box.insert(END, str(line))
                    self.text_box.see(END)
                    self.text_box.update_idletasks()
            except Queue.Empty:
                pass
            self.text_box.after(100, self.update_me)
    
    def pipeToWidget(input, widget):
        widget.write(input)
    
    def start_thread():
        thread.start_new(start_test, (widget,))
    
    def start_test(widget):
        count = 0
        while True:
            pipeToWidget(str(count) + "\n", widget)
            count += 1
            time.sleep(2)
            widget.show_popup()
    
    root = Tk()
    widget = TestApp(root)
    start_button = Button(widget.bottom_frame, command=start_thread)
    start_button.configure(text="Start Test")
    start_button.pack(side=LEFT)
    root.title("Testing Thread Module")
    root.mainloop()
    

1 个答案:

答案 0 :(得分:3)

我无法重现你的问题,但我明白为什么会这样。

您正在使用queue将消息从后台线程传递到主线程以更新text_box,这是正确的。但是你也从后台线程调用widget.show_popup(),这意味着它在后台线程中创建并显示一个新的Toplevel。那是正确。

所有UI代码必须在每个顶级窗口的相同线程(并非所有UI代码)中运行,所有UI代码期间。在某些平台上,你可能会在自己的线程中运行每个窗口(甚至自由线程化所有东西),但这不是假设可以工作,肯定会崩溃或做不正确的事情一些平台。 (此外,单个UI线程必须是某些平台上的初始线程,但这与此无关。)


因此,要解决此问题,您需要执行相同的舞蹈来创建用于更新文本框的弹出窗口。

显而易见的方法是将widget.show_popup()移到update_me()的循环中。如果您希望在文本框更新后2秒发生,只需将self.top_frame.after(2000, self.show_popup)添加到方法。

但是我猜你正试图教会自己如何拥有多个独立的更新机制,所以告诉你“只使用一个更新队列”可能不是一个好的答案。在这种情况下,只需创建两个队列,并为每个队列提供单独的更新方法。然后,执行pipeToWidget,睡眠2秒,然后pipeToPopup


另一种方法是使用mtTkinter。它基本上完成了你正在做的事情,但是让它自动化,将每个实际的Tk GUI调用推送到一个队列,以便稍后由主循环运行。当然,你的对象本身必须是线程安全的,这也意味着你必须处理来自一个线程的GUI调用与来自另一个线程的调用交错。但只要这些都不是问题(而且它们似乎不在你的情况下),它就像魔术一样。


如果你想知道为什么这在Win7上为你冻结和/或崩溃而在OS X 10.8上对我没有...好吧,你真的需要看看乱七八糟的Tcl,C和Python代码,以及每个东西是如何构建的。并且,除非它是简单的(比如你的Tk构建不是自由线程的),否则它不会告诉你太多。代码不应该起作用,如果它似乎对我有用......那可能只是意味着它每次都会起作用,直到我职业生涯中最重要的演示,此时它会失败。