【问题标题】:Python Tkinter Text Widget with Auto & Custom Scroll具有自动和自定义滚动的 Python Tkinter 文本小部件
【发布时间】:2012-02-09 15:24:46
【问题描述】:

我编写了一个简单的基于 Tkinter 的 Python 应用程序,它从串行连接中读取文本并将其添加到窗口中,特别是一个文本框。

经过大量调整和一些非常奇怪的例外,这行得通。然后我通过这样做添加了自动滚动:

self.text.insert(END, str(parsed_line))
self.text.yview(END)

这些行在一个线程中运行。线程在从串行连接读取时阻塞,拆分行,然后将所有行添加到小部件。

这也有效。然后我想让用户滚动应该禁用自动滚动,直到用户滚动回底部。

我发现了这个 Stop Text widget from scrolling when content is changed 这似乎是相关的。特别是,我尝试了 DuckAssasin 评论中的代码:

if self.myWidgetScrollbar.get() == 1.0:
    self.myWidget.yview(END)

我还尝试了.get()[1],这实际上是我想要的元素(底部位置)。但是,这会崩溃并出现以下异常:

Traceback (most recent call last):
  File "transformer-gui.py", line 119, in run
    pos = self.scrollbar.get()[1]
  File "C:\Python26\lib\lib-tk\Tkinter.py", line 2809, in get
    return self._getdoubles(self.tk.call(self._w, 'get'))
  File "C:\Python26\lib\lib-tk\Tkinter.py", line 1028, in _getdoubles
    return tuple(map(getdouble, self.tk.splitlist(string)))
ValueError: invalid literal for float(): None

似乎 tkinter 某处返回 None ,然后被解析为浮点数。我在某处读过,例如如果请求的位置不可见,则文本的 index 方法有时会返回 None。

希望有人能帮我解决这个问题!

[编辑]

好的,我已经组装了一个演示脚本,可以在我的 Win XP 机器上重现这个问题:

import re,sys,time
from Tkinter import *
import Tkinter
import threading
import traceback


class ReaderThread(threading.Thread): 
    def __init__(self, text, scrollbar):
        print "Thread init"
        threading.Thread.__init__(self) 
        self.text = text
        self.scrollbar = scrollbar
        self.running = True

    def stop(self):
        print "Stopping thread"
        running = False

    def run(self):
        print "Thread started"
        time.sleep(5)
        i = 1
        try:
            while(self.running):
                # emulating delay when reading from serial interface
                time.sleep(0.05)
                line = "the quick brown fox jumps over the lazy dog\n"

                curIndex = "1.0"
                lowerEdge = 1.0
                pos = 1.0

                # get cur position
                pos = self.scrollbar.get()[1]

                # Disable scrollbar
                self.text.configure(yscrollcommand=None, state=NORMAL)

                # Add to text window
                self.text.insert(END, str(line))
                startIndex = repr(i) + ".0"
                curIndex = repr(i) + ".end"

                # Perform colorization
                if i % 6 == 0:
                    self.text.tag_add("warn", startIndex, curIndex)
                elif i % 6 == 1:
                    self.text.tag_add("debug", startIndex, curIndex)                            
                elif i % 6 == 2:
                    self.text.tag_add("info", startIndex, curIndex)                         
                elif i % 6 == 3:
                    self.text.tag_add("error", startIndex, curIndex)                            
                elif i % 6 == 4:
                    self.text.tag_add("fatal", startIndex, curIndex)                            
                i = i + 1

                # Enable scrollbar
                self.text.configure(yscrollcommand=self.scrollbar.set, state=DISABLED)

                # Auto scroll down to the end if scroll bar was at the bottom before
                # Otherwise allow customer scrolling                        

                if pos == 1.0:
                    self.text.yview(END)

                #if(lowerEdge == 1.0):
                #   print "is lower edge!"
                #self.text.see(curIndex)
                #else:
                #   print "Customer scrolling", lowerEdge

                # Get current scrollbar position before inserting
                #(upperEdge, lowerEdge) = self.scrollbar.get()
                #print upperEdge, lowerEdge

                #self.text.update_idletasks()
        except Exception as e:
            traceback.print_exc(file=sys.stdout)
            print "Exception in receiver thread, stopping..."
            pass
        print "Thread stopped"


class Transformer:
    def __init__(self):
        pass

    def start(self):
        """starts to read linewise from self.in_stream and parses the read lines"""
        count = 1
        root = Tk()
        root.title("Tkinter Auto-Scrolling Test")
        topPane = PanedWindow(root, orient=HORIZONTAL)
        topPane.pack(side=TOP, fill=X)
        lowerPane = PanedWindow(root, orient=VERTICAL)

        scrollbar = Scrollbar(root)
        scrollbar.pack(side=RIGHT, fill=Y)
        text = Text(wrap=WORD, yscrollcommand=scrollbar.set)
        scrollbar.config(command=text.yview)
        # Color definition for log levels
        text.tag_config("debug",foreground="gray50")
        text.tag_config("info",foreground="green")
        text.tag_config("warn",foreground="orange")
        text.tag_config("error",foreground="red")
        text.tag_config("fatal",foreground="#8B008B")
        # set default color
        text.config(background="black", foreground="gray");
        text.pack(expand=YES, fill=BOTH)        

        lowerPane.add(text)
        lowerPane.pack(expand=YES, fill=BOTH)

        t = ReaderThread(text, scrollbar)
        print "Starting thread"
        t.start()

        try:
            root.mainloop()
        except Exception as e:
            print "Exception in window manager: ", e

        t.stop()
        t.join()


if __name__ == "__main__":
    try:
        trans = Transformer()
        trans.start()
    except Exception as e:
        print "Error: ", e
        sys.exit(1)     

我让这个 scipt 运行并开始上下滚动,一段时间后我得到了很多总是不同的异常,例如:

.\source\testtools\device-log-transformer>python tkinter-autoscroll.py
Thread init
Starting thread
Thread started
Traceback (most recent call last):
  File "tkinter-autoscroll.py", line 59, in run
    self.text.configure(yscrollcommand=self.scrollbar.set, state=DISABLED)
  File "C:\Python26\lib\lib-tk\Tkinter.py", line 1202, in configure
Stopping thread
    return self._configure('configure', cnf, kw)
  File "C:\Python26\lib\lib-tk\Tkinter.py", line 1193, in _configure
    self.tk.call(_flatten((self._w, cmd)) + self._options(cnf))
TclError: invalid command name ".14762592"
Exception in receiver thread, stopping...
Thread stopped

.\source\testtools\device-log-transformer>python tkinter-autoscroll.py
Thread init
Starting thread
Thread started
Stopping thread
Traceback (most recent call last):
  File "tkinter-autoscroll.py", line 35, in run
    pos = self.scrollbar.get()[1]
  File "C:\Python26\lib\lib-tk\Tkinter.py", line 2809, in get
    return self._getdoubles(self.tk.call(self._w, 'get'))
TclError: invalid command name ".14762512"
Exception in receiver thread, stopping...
Thread stopped

.\source\testtools\device-log-transformer>python tkinter-autoscroll.py
Thread init
Starting thread
Thread started
Traceback (most recent call last):
  File "tkinter-autoscroll.py", line 65, in run
    self.text.yview(END)
  File "C:\Python26\lib\lib-tk\Tkinter.py", line 3156, in yview
    self.tk.call((self._w, 'yview') + what)
Stopping threadTclError: invalid command name ".14762592"

 Exception in receiver thread, stopping...
Thread stopped

.\source\testtools\device-log-transformer>python tkinter-autoscroll.py
Thread init
Starting thread
Thread started
Traceback (most recent call last):
  File "tkinter-autoscroll.py", line 35, in run
    pos = self.scrollbar.get()[1]
  File "C:\Python26\lib\lib-tk\Tkinter.py", line 2809, in get
    return self._getdoubles(self.tk.call(self._w, 'get'))
  File "C:\Python26\lib\lib-tk\Tkinter.py", line 1028, in _getdoubles
    return tuple(map(getdouble, self.tk.splitlist(string)))
ValueError: invalid literal for float(): None
Exception in receiver thread, stopping...
Thread stopped
Stopping thread

.\source\testtools\device-log-transformer>python tkinter-autoscroll.py
Thread init
Starting thread
Thread started
Traceback (most recent call last):
  File "tkinter-autoscroll.py", line 53, in run
    self.text.tag_add("error", startIndex, curIndex)
  File "C:\Python26\lib\lib-tk\Tkinter.py", line 3057, in tag_add
    (self._w, 'tag', 'add', tagName, index1) + args)
TclError: bad option "261.0": must be bbox, cget, compare, configure, count, debug, delete, dlineinfo, dump, edit, get, image, index, insert, mark, pe
er, replace, scan, search, see, tag, window, xview, or yview
Exception in receiver thread, stopping...
Thread stopped

我希望这可以帮助你帮助我:)

谢谢,

/J

【问题讨论】:

  • 您确定self.scrollbar 实际上是对滚动条小部件的引用吗? get 永远不应该返回 None。在最坏的情况下,它应该返回(0.0, 0.0, 0.0, 0.0)
  • 是的,我确信selfs.scrollbar 是正确的参考。然而,我并没有说get() 确实返回了None,我只是说在调用堆栈中的某个地方,Tkinter 确实如此(正如你从回溯中看到的ValueError: invalid literal for float(): None。我不确定这是否有什么与Tkinter如何在内部处理方法调用有关。据我了解,它创建了一种任务,该任务被发送到Tkinter主线程并随后被异步处理。我也尝试调用update_idletask但这会导致整个系统一段时间后挂起。

标签: python text scrollbar tkinter autoscroll


【解决方案1】:

很难说到底发生了什么,但您是否考虑过使用队列?

from Tkinter import *
import time, Queue, thread

def simulate_input(queue):
    for i in range(100):
        info = time.time()
        queue.put(info)
        time.sleep(0.5)

class Demo:
    def __init__(self, root, dataQueue):
        self.root = root
        self.dataQueue = dataQueue

        self.text = Text(self.root, height=10)
        self.scroller = Scrollbar(self.root, command=self.text.yview)
        self.text.config(yscrollcommand=self.scroller.set)
        self.text.tag_config('newline', background='green')
        self.scroller.pack(side='right', fill='y')
        self.text.pack(fill='both', expand=1)

        self.root.after_idle(self.poll)

    def poll(self):
        try:
            data = self.dataQueue.get_nowait()
        except Queue.Empty:
            pass
        else:
            self.text.tag_remove('newline', '1.0', 'end')
            position = self.scroller.get()
            self.text.insert('end', '%s\n' %(data), 'newline')            
            if (position[1] == 1.0):
                self.text.see('end')
        self.root.after(1000, self.poll)

q = Queue.Queue()
root = Tk()
app = Demo(root, q)

worker = thread.start_new_thread(simulate_input, (q,))
root.mainloop()

【讨论】:

  • 我认为队列不是问题,因为我有一个线程正在从流中读取,然后将其插入,然后等待新数据到来。唯一可能有帮助的是轮询延迟。但频率较高,输出滞后。
  • 啊,好的,我明白了!在此示例中,self.after() 不是 Python 内置的计时器,而是 Tkinter 函数。所以这意味着,我需要使用轮询?恕我直言,这是我想避免的某种反模式。
【解决方案2】:

关于您的演示脚本。

您正在从非 GUI 线程执行 GUI 工作。这往往会导致问题。

见:http://www.effbot.org/zone/tkinter-threads.htm

【讨论】:

  • 感谢您的提示,但我已经阅读了。我不明白其中的区别。我的脚本和本示例中的“GUI 线程”实际上是主线程,因为您调用 root.mainloop(),然后它会在内部执行 GUI 任务。然后,您至少需要一个其他线程来与 Tkinter 交互。在我的例子中,这是由一个线程完成的,在这个例子中是由一个计时器线程完成的。但从线程的角度来看,我看不出有什么不同。
  • 抱歉,我的第二条回复发错了。它与答案和 cmets 混淆了;)。所以,只是为了记录(相同的)再次评论:
  • 啊,好的,我明白了!在这个例子中,'self.after()' 不是 Python 内置的计时器,而是一个 Tkinter 函数。所以这意味着,我需要使用轮询?恕我直言,这是我想避免的某种反模式。
  • “也就是说,我需要使用轮询?恕我直言,这是我想避免的某种反模式。”如果这些想法中的任何一个在您的上下文中有意义,那么可能还有另一种方法。否则,我不确定。 (1) groups.google.com/group/comp.lang.python/msg/0b2f3081b1726783 (2) stackoverflow.com/questions/7141509/… (3) groups.google.com/group/comp.lang.python/msg/f98af3be3747827a
  • @ebeb:轮询是一个完全可以接受的解决方案,不要因为教条的原因而放弃这个概念。你已经有一个无限循环在运行——事件循环——它可能正在工作,否则它什么都不做。
【解决方案3】:

好的,

根据 noob oddy 的宝贵建议,我能够重写示例脚本,使用 Tkinter.generate_event() 方法生成异步事件和传递信息的队列。

每次从流中读取一行(由常量字符串和延迟模拟)时,我都会将该行附加到队列中(因为 AFAIK 不支持将对象传递给事件方法),然后创建一个新的事件。

事件回调方法从队列中检索消息并将其添加到 Text widged。这是有效的,因为这个方法是从 Tkinter 主循环中调用的,因此它不会干扰其他作业。

这是脚本:

import re,sys,time
from Tkinter import *
import Tkinter
import threading
import traceback
import Queue


class ReaderThread(threading.Thread): 
    def __init__(self, root, queue):
        print "Thread init"
        threading.Thread.__init__(self) 
        self.root = root
        self.running = True
        self.q = queue

    def stop(self):
        print "Stopping thread"
        running = False

    def run(self):
        print "Thread started"
        time.sleep(5)

        try:
            while(self.running):
                # emulating delay when reading from serial interface
                time.sleep(0.05)
                curline = "the quick brown fox jumps over the lazy dog\n"

                try:
                    self.q.put(curline)
                    self.root.event_generate('<<AppendLine>>', when='tail')
                # If it failed, the window has been destoyed: over
                except TclError as e:
                    print e
                    break

        except Exception as e:
            traceback.print_exc(file=sys.stdout)
            print "Exception in receiver thread, stopping..."
            pass
        print "Thread stopped"


class Transformer:
    def __init__(self):
        self.q = Queue.Queue()
        self.lineIndex = 1
        pass

    def appendLine(self, event):
        line = self.q.get_nowait()

        if line == None:
            return

        i = self.lineIndex
        curIndex = "1.0"
        lowerEdge = 1.0
        pos = 1.0

        # get cur position
        pos = self.scrollbar.get()[1]

        # Disable scrollbar
        self.text.configure(yscrollcommand=None, state=NORMAL)

        # Add to text window
        self.text.insert(END, str(line))
        startIndex = repr(i) + ".0"
        curIndex = repr(i) + ".end"

        # Perform colorization
        if i % 6 == 0:
            self.text.tag_add("warn", startIndex, curIndex)
        elif i % 6 == 1:
            self.text.tag_add("debug", startIndex, curIndex)                            
        elif i % 6 == 2:
            self.text.tag_add("info", startIndex, curIndex)                         
        elif i % 6 == 3:
            self.text.tag_add("error", startIndex, curIndex)                            
        elif i % 6 == 4:
            self.text.tag_add("fatal", startIndex, curIndex)                            
        i = i + 1

        # Enable scrollbar
        self.text.configure(yscrollcommand=self.scrollbar.set, state=DISABLED)

        # Auto scroll down to the end if scroll bar was at the bottom before
        # Otherwise allow customer scrolling                        

        if pos == 1.0:
            self.text.yview(END)

        self.lineIndex = i

    def start(self):
        """starts to read linewise from self.in_stream and parses the read lines"""
        count = 1
        self.root = Tk()
        self.root.title("Tkinter Auto-Scrolling Test")#
        self.root.bind('<<AppendLine>>', self.appendLine)
        self.topPane = PanedWindow(self.root, orient=HORIZONTAL)
        self.topPane.pack(side=TOP, fill=X)
        self.lowerPane = PanedWindow(self.root, orient=VERTICAL)

        self.scrollbar = Scrollbar(self.root)
        self.scrollbar.pack(side=RIGHT, fill=Y)
        self.text = Text(wrap=WORD, yscrollcommand=self.scrollbar.set)
        self.scrollbar.config(command=self.text.yview)
        # Color definition for log levels
        self.text.tag_config("debug",foreground="gray50")
        self.text.tag_config("info",foreground="green")
        self.text.tag_config("warn",foreground="orange")
        self.text.tag_config("error",foreground="red")
        self.text.tag_config("fatal",foreground="#8B008B")
        # set default color
        self.text.config(background="black", foreground="gray");
        self.text.pack(expand=YES, fill=BOTH)       

        self.lowerPane.add(self.text)
        self.lowerPane.pack(expand=YES, fill=BOTH)

        t = ReaderThread(self.root, self.q)
        print "Starting thread"
        t.start()

        try:
            self.root.mainloop()
        except Exception as e:
            print "Exception in window manager: ", e

        t.stop()
        t.join()


if __name__ == "__main__":
    try:
        trans = Transformer()
        trans.start()
    except Exception as e:
        print "Error: ", e
        sys.exit(1)     

再次感谢所有为您提供帮助的人!

【讨论】:

  • 我使用了与上面完全相同的脚本,除了ReaderThread 中的数据生成,它实际上是串行接口的输入流。不幸的是,它仍然崩溃。不像以前那么频繁,但仍然会崩溃。所以我在调用self.root.event_generate 后插入了一个延迟(0.02 秒)。它稍微好一点,但仍然崩溃:bad window name/identifier "40034472set"
  • 哦,只是为了通知你,我刚刚收到一条新的“错误消息”。实际上,tcl85.dll 中的 python.exe 崩溃了。这也是随机发生的。总结一下:我认为(如果我没有做错什么),event_generate 方法似乎不够稳定,无法从单独的线程中使用。
猜你喜欢
  • 1970-01-01
  • 2014-02-07
  • 2021-11-28
  • 1970-01-01
  • 2015-11-09
  • 2023-02-10
  • 1970-01-01
  • 2018-08-21
  • 2015-08-20
相关资源
最近更新 更多