2012-01-13 21 views
6

Ho scritto una semplice applicazione Python basata su Tkinter che legge il testo da una connessione seriale e lo aggiunge alla finestra, in particolare un testo widged.Python Tkinter Widget testo con scorrimento automatico e personalizzato

Dopo un sacco di modifiche e alcune eccezioni molto strane, questo funziona. Quindi ho aggiunto lo scorrimento automatico nel modo seguente:

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

Queste righe vengono eseguite in una discussione. Il thread si blocca durante la lettura dalla connessione seriale, divide le linee e quindi aggiunge tutte le linee al widget.

Anche questo funziona. Quindi volevo consentire all'utente di scorrere il quale dovrebbe disabilitare lo scorrimento automatico fino a quando l'utente non torna indietro.

Ho trovato questo Stop Text widget from scrolling when content is changed che sembra essere correlato. In particolare, ho provato il codice dal commento di DuckAssasin:

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

Ho provato anche .get()[1] che in realtà è l'elemento che voglio (posizione inferiore). Tuttavia, questo si blocca con la seguente eccezione:

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 

Sembra come se tkinter ritorna da qualche parte Nessuno che poi viene analizzato come un galleggiante. Ho letto da qualche parte, ad es. il metodo index del testo widged a volte restituisce None se la posizione richiesta non è visibile.

Speriamo che qualcuno possa darmi una mano con questo problema!

[EDIT]

Ok, ho assemblato uno script demo in grado di riprodurre il problema sulla mia macchina 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)  

ho lasciato questa corsa scipt e iniziare a scorrere verso l'alto e verso il basso e dopo qualche tempo ho un sacco di sempre diverse eccezioni come:

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

Spero che questo aiuta ad aiutarmi :)

Grazie,

/J

+0

Sei assolutamente sicuro che 'self.scrollbar' sia effettivamente un riferimento a un widget della barra di scorrimento? 'get' non dovrebbe mai restituire None. Nel peggiore dei casi, dovrebbe restituire '(0.0, 0.0, 0.0, 0.0)'. –

+0

Sì, sono sicuro che 'selfs.scrollbar' è il riferimento corretto. Tuttavia, non ho detto che 'get()' ha effettivamente restituito 'None', ho appena detto che da qualche parte all'interno dello stack delle chiamate, ha fatto Tkinter (come potete vedere dal traceback' ValueError: letterale non valido per float(): None 'Non sono sicuro se questo ha qualcosa a che fare con il modo in cui Tkinter gestisce internamente le chiamate ai metodi. Per quanto ne capisca, crea un tipo di compito che viene inviato al mainthread di Tkinter e viene elaborato in modo asincrono. per chiamare 'update_idletask' ma questo fa sì che l'intero sistema si blocchi dopo un po 'di tempo – jaw

risposta

2

OK,

sulla base dei preziosi suggerimenti da niubbo Oddy sono stato in grado di riscrivere lo script di esempio utilizzando il metodo Tkinter.generate_event() per generare eventi asincroni e una coda per passare le informazioni.

Ogni volta che una riga viene letta dallo stream (che viene simulata da una stringa costante e un ritardo), aggiungo la riga a una coda (poiché il passaggio di oggetti al metodo evento non è supportato AFAIK) e quindi crea un nuovo evento.

Il metodo di richiamata dell'evento recupera il messaggio dalla coda e lo aggiunge al testo visualizzato. Questo funziona perché questo metodo viene chiamato dal mainloop di Tkinter e quindi non può interferire con gli altri lavori.

Ecco lo 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)  

Grazie ancora a tutti coloro che hanno contribuito per il vostro aiuto!

+0

Ho usato lo script esatto come sopra tranne per la generazione dei dati nel 'ReaderThread' che è in realtà un flusso di input di un'interfaccia seriale. Sfortunatamente, si blocca ancora. Meno frequente di prima ma ancora, si blocca. Così ho inserito un ritardo (0.02s) dopo aver chiamato 'self.root.event_generate'. È leggermente migliorato ma si blocca ancora: 'nome/identificatore di finestra errato" 40034472set "' – jaw

+0

Oh, solo per informarti, ho appena avuto un nuovo "messaggio di errore". In realtà, il python.exe, che si trova in tcl85.dll, si è arrestato in modo anomalo. Questo succede anche casualmente. Per riassumere: Penso che (se non sto facendo qualcosa di sbagliato), il metodo 'event_generate' sembra non essere abbastanza stabile da essere usato da un thread separato. – jaw

2

E 'difficile dire che cosa sta realmente accadendo, ma avete pensato di utilizzare una coda?

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

Penso che la coda non sia il problema perché ho un thread che sta leggendo da uno stream e poi lo sto inserendo e aspettando che arrivino nuovi dati. L'unica cosa che potrebbe aiutare sarebbe il ritardo del polling, ma la frequenza è più alta, l'uscita è in ritardo – jaw

+0

Ah, OK, ho capito! In questo esempio, 'self.after()' non è un timer costruito in Python ma una funzione Tkinter. Questo significa che ho bisogno di usare il polling? IMHO una specie di anti-modello che vorrei evitare. – jaw

2

Per quanto riguarda lo script demo.

Stai facendo roba GUI dal thread non GUI. Questo tende a causare problemi.

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

+0

Grazie per il suggerimento ma l'ho già letto. E non ho la differenza. "Il thread della GUI" sia nel mio script sia in questo esempio è in realtà il thread principale perché si chiama 'root.mainloop()' che quindi esegue internamente le attività della GUI. Quindi, è necessario almeno un altro thread per interagire con Tkinter. Questo è stato fatto da un thread nel mio caso e da un thread del timer nel caso dell'esempio. Ma non vedo una differenza dal punto di vista del threading. – jaw

+0

Spiacente, ho postato la mia seconda risposta al post sbagliato. Diventa confuso con risposte e commenti;). Quindi, solo per il record che (lo stesso) commenta ancora: – jaw

+0

Ah, OK, ho capito! In questo esempio, self.after() 'non è un timer costruito in Python ma una funzione Tkinter. Quindi questo significa che ho bisogno di usare il polling? Questo è IMHO una specie di anti-modello che vorrei evitare. – jaw

Problemi correlati