实时更新Tkinter文本组件而非在类结束后
我现在有点麻烦,因为我在一个保密的机器上写这个,无法复制粘贴到这里。作为一个新手,我的做法可能有点不寻常。
我用Tkinter写了一个图形界面,里面有几个按钮。每个按钮都连接到一个类,这个类实际上运行一个简短的脚本。当点击按钮时,我会初始化一个叫做 log_window
的类,它其实就是一个Tkinter的文本小部件。接着,我创建一个全局变量,把 log
连接到我刚创建的 log_window
,在脚本运行时,我把 sys.stdout/stderr
重定向到 log
(我为此专门创建了一个写入方法)。一切看起来都没问题,但 log_window
的文本小部件在我重定向的输出更新之前,直到调用它的类完成后才会更新。不过,如果我在类里面直接使用 print
,它会按调用的顺序打印出来。
举个例子
import Tkinter
from Tkinter import *
import time
class log_window:
def __init__(self,master):
self.textframe = Tkinter.Frame(master)
self.text = Text(self.textframe)
self.text.pack()
self.textframe.pack()
def write(self,text):
self.text.insert(END,text)
class some_func1: # This effectively waits 5 seconds then prints both lines at once
def __init__(self,master):
log.write("some text")
time.sleep(5)
log.write("some text")
class some_func2: # This prints the first object, waits 5 seconds, then prints the second
def __init__(self,master):
print "some text"
time.sleep(5)
print "some text"
if __name__ == '__main__':
global log
root = Tk()
log = log_window(root)
root.after(100,some_func1, root)
root.after(100,some_func2, root)
root.mainloop()
抱歉我的例子可能有点不清楚,但我觉得它表达了我的意思。我做的重定向是通过 Popen
和一些系统调用,但这些不是问题的关键,所以我只强调了我认为是问题的核心部分。
3 个回答
你需要在 self.text.insert(END,text)
之后加上 self.text.update()
来更新你的小部件内容。
当你调用 sleep
的时候,你的整个图形界面(GUI)会卡住。你要记住,图形界面是通过一个事件循环来运行的,这个循环是一个无限循环,包裹着你所有的代码。事件循环负责在你改变界面元素时重新绘制它们。当有事件发生时,它会在这个循环中调用你的代码,所以只要你的代码在运行,事件循环就无法继续循环。
你有几种选择。一个方法是在向界面元素添加文本后调用 update_idletasks
。这样可以让事件循环处理“空闲”事件——也就是在程序没有其他事情做的时候安排执行的任务。重新绘制屏幕就是其中一个事件,还有其他的事件。
另一个选择是把你的函数放在一个线程或者单独的进程中运行。因为 Tkinter 不是线程安全的,所以这些线程或进程不能直接和图形界面沟通。它们需要把消息放到一个队列里,然后你的主线程(图形界面线程)必须去检查这个队列,取出消息。把这个代码集成到你的日志类里是很简单的,使用事件循环可以轻松地检查队列——只需要写一个方法来从队列中取出消息并插入到界面元素中,然后用 after
方法在几百毫秒后再次调用自己。
我对Tkinter的并发处理不是很了解,但通过尝试发现,如果在每次调用log.write
之后加上
master.update_idletasks()
,它就会按时更新。你可以给log加一个.flush()
的方法来实现这个功能(就像文件处理一样),或者你也可以让log.write
在写入后自己调用这个方法。