2013-04-05 40 views
1

我在学习如何使用线程模块。我跟着一起在这里说明:http://effbot.org/zone/tkinter-threads.htm创建线程安全的Toplevel小部件

我希望测试脚本:

  1. 打印出来的“计数”每两秒钟
  2. 显示弹出对话窗口(也每隔2秒)
  3. 的弹出窗口,应允许积累(如果我不点击“OK”了一段时间,应该有 多个弹出窗口)

Howev呃,当我运行这个脚本时,它会冻结主窗口,并在一段时间后崩溃。我想我没有正确实现线程模块。

有人请看看并指出我做错了什么?

这是我到目前为止已经试过:

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() 
+0

首先,作为['thread'](http://docs.python.org/2/library/thread.html)文档反复就是说,['threading'](HTTP://docs.python。 org/3/library/thread.html)模块“提供了一个更易于使用和更高级别的API”,您应该强烈考虑使用它。 – abarnert 2013-04-05 22:28:55

+0

同时......你确定这正是你正在运行的代码吗?当我运行它时(OS X 10.8.3与64位Apple Python 2.7.2),它似乎完全按照描述工作,没有冻结或崩溃。 – abarnert 2013-04-05 22:31:07

+0

@abarnert好的,我会阅读线程。另外,是的,我正在运行Win 7上的确切代码Python 2.7.3 – 2013-04-05 22:38:02

回答

3

我无法重现你的问题,但我可以看到,为什么会发生这种事。

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

所有用户界面代码必须运行在相同的线程 - 并非所有UI代码为每个顶级窗口,所有UI代码期间。在某些平台上,你可能会在自己的线程中运行每个窗口(甚至是自由线程化所有内容),但这不是假设可以工作,并且肯定会在某些平台上崩溃或做不正确的事情。 (同样,单UI线程必须在某些平台上的初始线程,但在这里不相关)。


因此,要解决这个问题,你需要做同样的舞蹈用于创建弹出窗口你为更新文本框做了什么。

明显的做法是将widget.show_popup()移动到update_me()的循环中。如果您希望在文本框更新后2秒内发生,请将self.top_frame.after(2000, self.show_popup)添加到该方法。

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


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


如果你想知道为什么这是在OS X 10.8冻结和/或Win7上崩溃了你,而不是我...好了,你真的需要考虑的Tcl,C乱七八糟的,和Python代码,以及每个东西的构建方式。而且,除非它是简单的东西(比如你的Tk版本不是免费线程的),反正它不会告诉你什么。该代码不应该工作,如果它似乎适用于我......这可能意味着它将每次都工作,直到我职业生涯中最重要的演示,在这一点上它会失败。

+0

是的,我试图围绕线程(ing)缠绕我的头,这对我来说是一个新概念。谢谢你的解释。将它移动到update_me()中的循环使其工作。 :) – 2013-04-05 22:53:10

+0

很酷。我也会看看mtTkinter。再次感谢。 – 2013-04-05 22:56:58

+0

@DirtyPenguin:如果你不确定每个代码运行哪个线程,有一个简单的方法来检查:在构建时给每个新的'threading.Thread'命名,并添加一行来打印'threading .current_thread().name'和你要在每个Tk调用之前调用的内容。然后,你会看到“背景:关于调用Toplevel()',你会知道,无论出于什么原因,你是从错误的线程来做它。 – abarnert 2013-04-05 23:03:16