7

I wrote a simple Tkinter based Python application that reads text from a serial connection and adds it to the window, specifically a text widged.

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

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

These lines run in a thread. The thread blocks on reading fromt the serial connection, splits lines and then adds all lines to the widget.

This works, too. Then I wanted to allow the user to scroll which should disable auto-scroll until the user scrolls back to the bottom.

I found this Stop Text widget from scrolling when content is changed 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 crashes 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 as if tkinter somewhere returns None which then is being parsed as a float. I read somewhere, that e.g. the index method of the text widged sometimes returnes None if the requested location is not visible.

Hopefully, anybody can help me out with this problem!

[EDIT]

Ok, I have assembled a demo script that can reproduce this issue 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 let this scipt run and start to scroll up and down and after some time I get a lot of always 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

I hope this helps you to help me :)

Thanks,

/J

Community
  • 1
  • 1
jaw
  • 932
  • 2
  • 10
  • 24
  • Are you absolutely certain that `self.scrollbar` actually is a reference to a scrollbar widget? `get` should never return None. At worst, it should return `(0.0, 0.0, 0.0, 0.0)`. – Bryan Oakley Jan 13 '12 at 17:22
  • Yes, I am sure that `selfs.scrollbar` is the correct reference. However, I did not say that `get()` did actually returned `None`, I just said that somewhere within the call stack, Tkinter did (as you can see from the traceback `ValueError: invalid literal for float(): None`. I'm not sure if this has something to do with how Tkinter internally handles method calls. As far as I understands it, it creates a kind of task that is send to the Tkinter mainthread and is asynchonously processed then. I also tried to call `update_idletask` but this causes the whole system to hang after a while. – jaw Jan 16 '12 at 09:35

3 Answers3

2

It's hard to tell what's really going on but have you considered using a 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()
noob oddy
  • 1,314
  • 8
  • 11
  • I think the queue is not the problem because I have one thread which is reading from a stream and then inserting it and then waiting until new data is coming. The only thing that might help would be the polling delay. But frequency is higher, the output is lagging. – jaw Jan 19 '12 at 16:09
  • Ahh, OK, I get it! In this example, `self.after()` is not a Python built in timer but a Tkinter function. So that means, I NEED to use polling? This is IMHO some kind of an anti-pattern I would like to avoid. – jaw Jan 20 '12 at 08:30
2

Regarding your demo script.

You're doing GUI stuff from the non-GUI thread. That tends to cause problems.

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

noob oddy
  • 1,314
  • 8
  • 11
  • Thanks for the hint but I already read it. And I don't get the difference. "The GUI thread" in both my script and this example is actually the main thread because you call `root.mainloop()` which then internally performs GUI tasks. Then, you need at least one other thread to interact with Tkinter. That is beeing done by a thread in my case and by a timer thread in case of the example. But I don't see a difference from the threading point of view. – jaw Jan 20 '12 at 08:23
  • Sorry, I postted my second reply to the wrong post. It gets confusing with answers and comments ;). So, just for the record that (same) comment again: – jaw Jan 20 '12 at 11:56
  • Ahh, OK, I get it! In this example, ´self.after()´ is not a Python built in timer but a Tkinter function. So that means, I NEED to use polling? This is IMHO some kind of an anti-pattern I would like to avoid. – jaw Jan 20 '12 at 11:56
  • "So that means, I NEED to use polling? This is IMHO some kind of an anti-pattern I would like to avoid." If any of these ideas make sense in your context, then there may be another way. Otherwise, I'm not sure. (1) http://groups.google.com/group/comp.lang.python/msg/0b2f3081b1726783 (2) http://stackoverflow.com/questions/7141509/tkinter-wait-for-item-in-queue (3) http://groups.google.com/group/comp.lang.python/msg/f98af3be3747827a – noob oddy Jan 20 '12 at 20:57
  • @ebeb: polling is a perfectly acceptable solution, don't discard the notion for dogmatic reasons. You already have an infinite loop running -- the event loop -- it might as well be doing work when it is otherwise not doing anything else. – Bryan Oakley Jan 21 '12 at 00:25
  • @ Bryan: Polling is Ok and probably necessary when you have something like a sensor and you never know when its value will change but it's never a desirable solution. Polling always has the disadvantage that you'll insert an artificial delay plus artificial work load. Either 1.) You'll wait/block even if there is data available or 2.) you run your infinite loop even if there's nothing to do. Now you can lower the delay to be more responsive but you get higher and constant CPU usage. If you lower the delay, you'll get less CPU usage but less responsiveness, too. – jaw Jan 23 '12 at 08:52
  • @nooboddy Thanks for you're reading suggestions. The seem to look interesting and might fit. Especially the `Tk.event_generate()` method look promising if it is really thread safe as mentioned in this post. It might be a bit clumsy to defined events for all methods you wanna call asynchronously but it seems to be the best match to what I want to achieve. – jaw Jan 23 '12 at 09:02
2

OK,

based on the valuable suggestions by noob oddy I was able to rewrite the example script by using the Tkinter.generate_event() method to generate asynchronous event and a queue to pass the information.

Every time a line is read from the stream (which is simulated by a constant string and a delay), I append the line to a queue (because passing objects to the event method is not supported AFAIK) and then create a new event.

The event callback method retrieves the message from the queue and adds it to the Text widged. This works because this method is called from the Tkinter mainloop an thus it cannot interfere with the other jobs.

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 again to everybody who contributed for your help!

jaw
  • 932
  • 2
  • 10
  • 24
  • I used the exact script as above except for the data generation in the `ReaderThread` which is really an input stream of a serial interface. Unfortunately, it still crashes. Less frequent than before but still, it does crash. So I inserted a delay (0.02s) after calling `self.root.event_generate`. It got slightly better but it's still crashing: `bad window name/identifier "40034472set"` – jaw Jan 26 '12 at 13:22
  • Oh, just to inform you, I just had a new "error message". Actually, the python.exe, being in tcl85.dll, crashed. This also happens randomly. To sum it up: I think (If I am not doing something wrong), the `event_generate` method seems not to be stable enough to be used from a separate thread. – jaw Jan 27 '12 at 07:21