Текстовый виджет Python Tkinter с автоматической и пользовательской прокруткой

Я написал простое приложение Python на основе Tkinter, которое читает текст из последовательного соединения и добавляет его в окно, в частности текст расширен.

после многих настроек и некоторых очень странных исключений это работает. Затем я добавил автопрокрутку, сделав это:

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

эти строки выполняются в потоке. Поток блокирует чтение последовательного соединения, разбивает строки и добавляет все строки в виджет.

это работает, тоже. Тогда я хотел чтобы позволить пользователю прокручивать, который должен отключить автоматическую прокрутку, пока пользователь не прокручивается обратно вниз.

Я нашел это остановить текстовый виджет от прокрутки при изменении содержимого что, похоже, связано. Тем более, я попробовал код из комментария 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:Python26liblib-tkTkinter.py", line 2809, in get
    return self._getdoubles(self.tk.call(self._w, 'get'))
  File "C:Python26liblib-tkTkinter.py", line 1028, in _getdoubles
    return tuple(map(getdouble, self.tk.splitlist(string)))
ValueError: invalid literal for float(): None

кажется, что tkinter somewhere возвращает None, который затем анализируется как поплавок. Я где-то читал, что, например, индексный метод текста иногда не возвращается, если запрошенное местоположение не видно.

надеюсь, кто-нибудь может помочь мне с этой проблемой!

[EDIT]

хорошо, я собрал демо-скрипт, который может воспроизвести эту проблему на моей машине 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 dogn"

                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 запустить и начать прокручивать вверх и вниз, и через некоторое время я получаю много всегда разных исключений, таких как:

.sourcetesttoolsdevice-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:Python26liblib-tkTkinter.py", line 1202, in configure
Stopping thread
    return self._configure('configure', cnf, kw)
  File "C:Python26liblib-tkTkinter.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

.sourcetesttoolsdevice-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:Python26liblib-tkTkinter.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

.sourcetesttoolsdevice-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:Python26liblib-tkTkinter.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

.sourcetesttoolsdevice-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:Python26liblib-tkTkinter.py", line 2809, in get
    return self._getdoubles(self.tk.call(self._w, 'get'))
  File "C:Python26liblib-tkTkinter.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

.sourcetesttoolsdevice-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:Python26liblib-tkTkinter.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

3 ответов


трудно сказать, что на самом деле происходит, но вы рассматривали возможность использования очереди?

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

относительно вашего демо-скрипта.

Вы делаете GUI вещи из потока без GUI. Это приводит к проблемам.

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


ОК

основываясь на ценных предложениях нуба Одди, я смог переписать пример сценария, используя Tkinter.generate_event() метод для создания асинхронного события и очереди для передачи информации.

каждый раз, когда строка считывается из потока (который моделируется постоянной строкой и задержкой), я добавляю строку в очередь (потому что передача объектов в метод события не поддерживается AFAIK), а затем создаю новое событие.

обратный вызов события метод извлекает сообщение из очереди и добавляет его к расширенному тексту. Это работает, потому что этот метод вызывается из Tkinter mainloop, поэтому он не может мешать другим заданиям.

вот этот скрипт:

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)     

еще раз спасибо всем, кто внес свой вклад в вашу помощь!