2012-01-13 79 views
6

我編寫了一個簡單的基於Tkinter的Python應用程序,該應用程序從串行連接中讀取文本並將其添加到窗口中,特別是文本窗口。Python Tkinter Text Widget with Auto&Custom Scroll

經過很多調整和一些非常奇怪的例外,這個工程。然後我添加了自動滾動功能:

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然後被解析爲一個浮點數。我在某處讀過,例如如果請求的位置不可見,文本的索引方法有時會返回無。

希望有人能幫我解決這個問題!

[編輯]

好吧,我已經組建了一個演示腳本,可以重現這個問題我的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)  

我讓這個素文字的運行,並開始上下滾動和一段時間後,我得到了很多總是不同的例外,如:

.\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

+0

您是否確定'self.scrollbar'實際上是對滾動條小部件的引用? 'get'永遠不應該返回None。在最壞的情況下,它應該返回'(0.0,0.0,0.0,0.0)'。 – 2012-01-13 17:22:36

+0

是的,我確定'selfs.scrollbar'是正確的參考。然而,我並沒有說'get()'確實返回了'None',我只是說在調用堆棧的某個地方,Tkinter的確如此(正如您從追蹤中看到的那樣:ValueError:float '我不確定這是否與Tkinter內部處理方法調用有關,據我瞭解,它創建了一種任務發送到Tkinter主線程,然後異步處理。調用'update_idletask'但是這會導致整個系統在一段時間後掛起 – jaw 2012-01-16 09:35:47

回答

2

OK,

基於由小白oddy提出寶貴的建議,我能夠通過使用Tkinter.generate_event()方法來產生異步事件和隊列傳遞信息重寫示例腳本。

每次從流中讀取一行(由常量字符串和延遲模擬),我將行追加到隊列中(因爲不支持將事件傳遞給事件方法AFAIK),然後創建一個新事件。

事件回調方法從隊列中檢索消息並將其添加到文本窗口。這是有效的,因爲這種方法是從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)  

感謝大家誰貢獻您的幫助!

+0

除了'ReaderThread'中的數據生成外,我使用了上面的確切腳本,它實際上是串行接口的輸入流。不幸的是,它仍然崩潰。比以前少頻繁但仍然會崩潰。所以我在調用'self.root.event_generate'後插入了一個延遲(0.02s)。它稍微好一點,但它仍然崩潰:'壞窗口名稱/標識符「40034472set」' – jaw 2012-01-26 13:22:57

+0

哦,只是通知你,我剛剛有一個新的「錯誤信息」。實際上,tcl85.dll中的python.exe崩潰了。這也是隨機發生的。總結一下:我認爲(如果我沒有做錯什麼),'event_generate'方法似乎不夠穩定,不能從單獨的線程使用。 – jaw 2012-01-27 07:21:55

2

這很難說究竟發生了什麼事情,但你有沒有考慮過使用一個隊列?

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

我認爲這個隊列不是問題,因爲我有一個線程正在讀取一個流,然後插入它,然後等待新的數據到來。唯一可能會幫助的是輪詢延遲,但是頻率更高,輸出更遲。 – jaw 2012-01-19 16:09:47

+0

啊,好的,我明白了!在這個例子中,'self.after()'不是一個Python內置的定時器,但是一個Tkinter函數,所以這意味着我需要使用輪詢?這是恕我直言,我想避免某種反模式。 – jaw 2012-01-20 08:30:37

2

關於您的演示腳本。

你正在從非GUI線程做GUI的東西。這往往會造成問題。

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

+0

感謝您的提示,但我已閱讀它。我沒有得到區別。我的腳本和這個例子中的「GUI線程」實際上是主線程,因爲你調用'root.mainloop()',然後在內部執行GUI任務。然後,您至少需要一個其他線程與Tkinter進行交互。這是由我的情況和一個計時器線程的例子中的線程完成的。但是我從線程的角度看並沒有什麼不同。 – jaw 2012-01-20 08:23:24

+0

對不起,我把我的第二個回覆寄給了錯誤的帖子。它會讓答案和評論混淆;)。所以,只是爲了(相同的)記錄再次評論: – jaw 2012-01-20 11:56:31

+0

啊,好的,我明白了!在這個例子中,'self.after()'不是一個Python內置的定時器,而是一個Tkinter函數。那麼這意味着我需要使用輪詢?這是恕我直言,某種我想避免的反模式。 – jaw 2012-01-20 11:56:50