Текстовый виджет 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:\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, который затем анализируется как float. Я где-то читал, что, например, метод индекса текста, который видится, иногда возвращает 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
Я надеюсь, что это поможет вам помочь мне:)
Спасибо,
/ Дж
3 ответа
В порядке,
основываясь на ценных предложениях Нуба Одди, я смог переписать пример сценария с помощью 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)
Еще раз спасибо всем, кто внес свой вклад в вашу помощь!
Относительно вашего демо-сценария.
Вы делаете вещи с графическим интерфейсом из потока без графического интерфейса. Это имеет тенденцию вызывать проблемы.
Трудно сказать, что на самом деле происходит, но рассматривали ли вы вопрос об использовании очереди?
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()