Python Tkinter text widget with auto and custom scroll

I wrote a simple Tkinter-based Python application that reads text from a serial connection and adds it to a window, in particular text with an extension.

After a lot of tweaks and some very strange exceptions, this works. Then I added autoscrolling by doing the following:

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

These lines are executed in a thread. The thread blocks reading from the serial connection, breaks the lines, and then adds all the lines to the widget.

This also works. Then I wanted to allow the user to scroll, which should turn off auto-scrolling until the user scrolls down the page.

I found this to Leave the β€œStop Text” widget while scrolling the page while changing the content, which seems to be related. Especially, I tried the code from DuckAssasin's comment:

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

I also tried .get()[1] , which is actually the element I want (bottom position). However, this results in a failure with the following exception:

 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 

It seems that tkinter is returning None somewhere, which is then parsed as a float. I read somewhere that, for example, the index method of text that is tagged sometimes returns None if the requested location is not displayed.

Hope anyone can help me with this problem!

[EDIT]

Ok, I put together a demo script that can reproduce this problem on my Win XP machine:

 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) 

I skipped this scipt and started to scroll up and down, and after a while I get many different exceptions, such as:

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

Hope this helps you help me :)

Thanks,

/ J

+6
source share
3 answers

OK,

based on valuable suggestions noob oddy I was able to rewrite the sample script using the Tkinter.generate_event() method to generate an asynchronous event and a queue for transmitting information.

Each time a line is read from a stream (which is simulated by a constant line and a delay), I add a line to the queue (since passing objects to the event method is not supported by AFAIK), and then creates a new event.

The event callback method extracts a message from the queue and adds it to the text. This works because this method is called from mainloop Tkinter and therefore it cannot interfere with other tasks.

Here is the script:

 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) 

Thanks to everyone who contributed to your help!

+2
source

It's hard to say what is happening, but did you think you were using the queue?

 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() 
+2
source

Regarding your demo script.

You are making GUI stuff from a thread other than a GUI. This is causing problems.

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

+2
source

Source: https://habr.com/ru/post/905903/


All Articles