2012-01-13 18 views
6

Escribí una aplicación Python basada en Tkinter simple que lee texto de una conexión en serie y lo agrega a la ventana, específicamente un texto con ancho de carro.Widget de texto de Python Tkinter con desplazamiento automático y personalizado

Después de muchos ajustes y algunas excepciones muy extrañas, esto funciona. Luego agregué el desplazamiento automático al hacer esto:

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

Estas líneas se ejecutan en un hilo. El hilo bloquea la lectura desde la conexión en serie, divide las líneas y luego agrega todas las líneas al widget.

Esto también funciona. Luego, quería permitir que el usuario se desplace, lo que debería deshabilitar el desplazamiento automático hasta que el usuario se desplace hacia atrás hasta la parte inferior.

He encontrado Stop Text widget from scrolling when content is changed que parece estar relacionado. Sobre todo, probé el código de comentario de DuckAssasin:

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

También probé .get()[1] que en realidad es el elemento que quiero (posición inferior). Sin embargo, esto choca con la siguiente excepción:

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 

Parece como si en algún lugar tkinter vuelve Ninguno que a su vez está siendo analizado como un flotador. Leí en alguna parte, que p. el método de índice del texto con ancho retorna a veces Ninguno si la ubicación solicitada no es visible.

¡Espero que alguien pueda ayudarme con este problema!

[EDIT]

Ok, he montado un script de demostración que puede reproducir este problema en mi máquina Windows 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)  

que vamos a esta carrera scipt y comenzar a desplazarse hacia arriba y hacia abajo y después de algún tiempo consigo una gran cantidad de diferentes siempre excepciones tales como:

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

espero que esto le ayuda a mí :)

012 ayudas

Gracias,

/J

+0

¿Está absolutamente seguro de que 'self.scrollbar' en realidad es una referencia a un widget de barra de desplazamiento? 'get' nunca debe devolver None. En el peor, debería devolver '(0.0, 0.0, 0.0, 0.0)'. –

+0

Sí, estoy seguro de que 'selfs.scrollbar' es la referencia correcta. Sin embargo, no dije que 'get()' realmente devolvió 'None', solo dije que en algún lugar dentro de la pila de llamadas, Tkinter lo hizo (como se puede ver en el traceback' ValueError: literal inválido para float(): None '.No estoy seguro de si esto tiene algo que ver con la forma en que Tkinter maneja internamente las llamadas a métodos. Por lo que yo entiendo, crea un tipo de tarea que se envía al hilo principal de Tkinter y luego se procesa de manera asíncrona. para llamar a 'update_idletask', pero esto hace que todo el sistema se cuelgue después de un tiempo. – jaw

Respuesta

2

OK,

basado en las sugerencias valiosas por oddy novato que fue capaz de volver a escribir el guión ejemplo, utilizando el método Tkinter.generate_event() para generar eventos asíncronos y una cola para pasar la información.

Cada vez que se lee una línea de la secuencia (que se simula mediante una cadena constante y un retraso), anexo la línea a una cola (porque pasar objetos al método de evento no es compatible con AFAIK) y luego creo un nuevo evento.

El método de devolución de llamada de evento recupera el mensaje de la cola y lo agrega al Texto con ancho de banda. Esto funciona porque este método se llama desde Tkinter mainloop y por lo tanto no puede interferir con los otros trabajos.

Aquí está la secuencia de comandos:

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)  

Gracias de nuevo a todos los que contribuyeron para su ayuda!

+0

Utilicé la secuencia de comandos exacta como la anterior, excepto la generación de datos en 'ReaderThread', que en realidad es una secuencia de entrada de una interfaz en serie. Desafortunadamente, todavía se cuelga. Menos frecuente que antes, pero aún así, falla. Entonces inserté un retraso (0.02s) después de llamar 'self.root.event_generate'. Se puso un poco mejor, pero aún se cuelga: 'nombre/identificador de ventana incorrecto" 40034472set "' – jaw

+0

Oh, solo para informarle, acabo de recibir un nuevo "mensaje de error". En realidad, python.exe, que se encuentra en tcl85.dll, se bloqueó. Esto también ocurre al azar. Para resumir: creo (si no estoy haciendo algo mal), el método 'event_generate' parece no ser lo suficientemente estable como para ser utilizado desde un hilo separado. – jaw

2

Es difícil saber lo que realmente está pasando pero ¿ha considerado el uso de una cola?

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

Creo que la cola no es el problema porque tengo un hilo que lee un flujo y luego lo inserta y luego espera hasta que lleguen nuevos datos. Lo único que podría ayudar sería la demora de sondeo. Pero la frecuencia es más alta, la salida es más lenta. – jaw

+0

¡Ahh, está bien, lo entiendo! En este ejemplo, 'self.after()' no es un temporizador construido en Python, sino una función de Tkinter. ¿Eso significa que NECESITO usar sondeo? Esto es En mi humilde opinión, algún tipo de antipatrón que quisiera evitar. – jaw

2

En cuanto a la secuencia de comandos de demostración.

Está haciendo cosas de GUI desde el hilo que no es GUI. Eso tiende a causar problemas.

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

+0

Gracias por la pista pero ya lo leí. Y no entiendo la diferencia. "El hilo de la interfaz gráfica de usuario" tanto en mi script como en este ejemplo es en realidad el hilo principal porque se llama a 'root.mainloop()', que luego ejecuta internamente las tareas de la GUI. Entonces, necesitas al menos otro hilo para interactuar con Tkinter. Eso está siendo hecho por un hilo en mi caso y por un hilo de temporizador en el caso del ejemplo. Pero no veo una diferencia desde el punto de vista de enhebrado. – jaw

+0

Lo siento, posteé mi segunda respuesta a la publicación incorrecta. Se vuelve confuso con respuestas y comentarios;). Entonces, solo para el registro que (mismo) comento de nuevo: – jaw

+0

Ahh, OK, lo entiendo! En este ejemplo, self.after() 'no es un temporizador construido en Python, sino una función Tkinter. Entonces eso significa, NECESITO usar encuestas? Esto es en mi humilde opinión una especie de antipatrón que me gustaría evitar. – jaw

Cuestiones relacionadas