GUI update

Fragen zu Tkinter.
Antworten
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Hallo,
ich hab mal wieder ein Problemchen:

In folgendem Beispiel werden in einer Schleife aktuelle Werte erzeugt, die natürlich auch zeitnah über die netten tk/ttk-Widgets dargestellt werden sollen. Leider passiert das eben nicht. Eine Ausgabe mit print() funktioniert dagegen ohne verzögerung.

Könnte mir mal jemand die Logik des dahinterstehen Konzeptes verklickern? Und wie macht man es richtig? Ich möchte, dass die Variablenänderungen sofort an die Widgets weitergegeben werden.

Danke

Code: Alles auswählen

from tkinter import *
from tkinter import ttk
import time

def aloop():
	for i in range(5):
		t = time.asctime(time.localtime())
		alabel['text'] = t
		print(t)
		time.sleep(1)

root = Tk()

atext = StringVar()
atext = "---"

alabel = ttk.Label(root, text=atext)
alabel.pack()

abutton = ttk.Button(root, text="Info", command=aloop)
abutton.pack()

root.mainloop()
BlackJack

@RedSharky: Verwende statt der Schleife und `time.sleep()` die `after()`-Methode auf Widgets. In der Schleife kommt `Tkinter` ja nicht dazu seine Hauptschleife abzuarbeiten, weil statt der Deine Schleife läuft.
yipyip
User
Beiträge: 418
Registriert: Samstag 12. Juli 2008, 01:18

Neben der "after()"-Methode brauchst Du auch noch fuer das Label die "update()"-Methode, damit das Label auch wirklich neu gezeichnet wird.
Habe mal versucht, die von Dir gewuenschte Funktionalitaet nachzubilden.

Code: Alles auswählen

#!/usr/bin/env python3.1

import tkinter as tk
import time

class Gui(object):


  def __init__(self, counts=5, ms=1000):

    self.root = tk.Tk()
    self.svar = tk.StringVar()
    self.svar.set("---")
    self.label = tk.Label(textvariable=self.svar)
    self.label.pack()
    self.but = tk.Button(self.root, width=30, text="Info", command=self.loop_time)
    self.but.pack()
    self.state = ''
    self.counter = counts
    self.counts = counts
    self.ms = ms
    

  def loop_time(self):

    if not self.state == 'looping':
      self.state = 'looping'
      self.set_time()


  def set_time(self):

    #print(self.counter)
    self.svar.set(time.asctime(time.localtime()))
    self.label.update()
    self.counter -= 1
    if self.counter <= 0:
      self.state = ''
      self.label.after_cancel(self.id)
      self.counter = self.counts
    else:
      self.id = self.label.after(self.ms, self.set_time)
      
    
  def run(self):
    
    self.root.mainloop()


if __name__ == '__main__':

  Gui(50, 100).run()
:wink:
yipyip
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Hi,

ich hab jetzt mal probiert, das mit einem Thread zu lösen. Das ist aber nicht zu empfehlen, oder? Kommt mir irgendwie wie mit Kanonen auf Spatzen geschossen vor. An after() hab ich mich auch noch schwach erinnert, aber ich find ums Verrecken keine Doku dazu. Könnte mir da jemand aushelfen?
Danke für das Beispiel, werd ich mir mal anschauen.
Frage: Wieso geht das mit Tk nicht einfach so? Muss das so oder gibt's da irgendwelche Vorteile?

Code: Alles auswählen

from tkinter import *
from tkinter import ttk
import time
import threading

def aloop():
	#for i in range(5):
	while 1:
		t = time.asctime(time.localtime())
		alabel['text'] = t
		print(t)
		time.sleep(1)

def start_aloop():
	thrd = threading.Thread(target=aloop)
	thrd.start()
	
root = Tk()

atext = StringVar()
atext = "---"

alabel = ttk.Label(root, text=atext)
alabel.pack()

abutton = ttk.Button(root, text="Info", command=start_aloop)
abutton.pack()

root.mainloop()
yipyip
User
Beiträge: 418
Registriert: Samstag 12. Juli 2008, 01:18

Nee, nee, nee, das wird so nix (...und dann auch noch bei jedem Button-Click einen neuen Thread starten...). Beitraege zum Thema Threads und GUI gibt's hier genuegend. :-)

Zur "after()"-Methode: http://effbot.org/tkinterbook/widget.htm

:wink:
yipyip
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Ich muss noch mal nachfragen: Oben habe ich nur eine Art Minimalbeispiel angegeben.
Eigentlich ist die Situation viel kompilizierter: Ich habe ein Programm geschrieben, dass etliche Dateien einliest, bearbeitet und Details und Zwischenstände dazu ausgibt. Dazu sind for-Schleifen zwingend notwendig. Nur leider gibt es das Feedback erst, wenn alles beendet ist. Und das kann schon mal Minuten dauern.
after() ist da glaub ich nicht das Wahre; was nun?
BlackJack

@RedSharky: Logik und GUI komplett trennen, Logik in einem eigenen Thread laufen lassen, und mit der GUI über eine `Queue.Queue` kommunizieren, die periodisch mit `after()` abgefragt wird.
problembär

RedSharky hat geschrieben:after() ist da glaub ich nicht das Wahre
Doch, siehe z.B. hier.
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

@RedSharky: Logik und GUI komplett trennen, Logik in einem eigenen Thread laufen lassen, und mit der GUI über eine `Queue.Queue` kommunizieren, die periodisch mit `after()` abgefragt wird.
Frage: Wenn ich das so mache, muss ich dann die Funktion, die after() aufruft und mit get() die Queue abfragt zwingend in einen eigenen Thread stecken, da ja get() sonst wieder alles blockiert?! Ich habe nämlich den Eindruck. Brauche ich also zwei Threads, einen für die Logik (Sender, put()) und einen für das Update des GUI (Empfänger, get())? Oder kann man sich für get() den Thread sparen?
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Hmm, schein auch so zu klappen. Vergesst meinen letzten Post.

Hier ein Beispiel, Kritik erwünscht:

Code: Alles auswählen

from tkinter import *
from time import *
import threading
import queue

#------------------------------------------------------------------------------

class Counting(threading.Thread):
    '''
    Just counting...
    '''
    def __init__(self, label, t, q):
        threading.Thread.__init__(self)       
        self.jobqueue = q # get job queue for sending commands
        self.t = t 

       
    def run(self):
        while 1:# and self.mb.entrycget(1,"label")=="Stop":
            sleep(0.1) # wait a second
            self.t += 1 # count down
            print(self.t)
            
            # send to job queue !!!
            self.jobqueue.put(("time_label",self.t))
                       

class AppGUI:
    '''
    Application GUI. 
    '''
    def __init__(self, master):
        self.t = 0
        
        #--- GUI design -------------------------------------------------------
        self.label = Label(master,font=("Arial","30"))
        self.label.pack()
        self.label.config(text=str(self.t))
        
       
        #--- Job queue --------------------------------------------------------
        self.q = queue.Queue() # Make job queue (Queue)
        offswitch = threading.Event() # Make offswitch (Event)
        
        self.label.after(100,self.label_update)
        
        cd = Counting(self.label, self.t, self.q)
        cd.start()
        
        
    def label_update(self):
        print("label update")
        job = self.q.get() #get job form queue!!!
        
        if job[0] == "time_label":
            self.label.config(text=str(job[1]))
        else:
            print("Unknown job:", job)
            
        self.label.after(100,self.label_update)
       
      
   
def main():
    
    root=Tk()
    root.title("Counting")
    root.wm_geometry('170x50+200+200')
    root.resizable(False,False)
    root.deiconify()
    app=AppGUI(root)  
    root.mainloop()


if __name__ == '__main__':
    main()
BlackJack

@RedSharky: Der `Queue.get()`-Aufruf blockiert die GUI. Entweder Du verwendest die nicht-blockierende Variante, oder Du testest vorher, ob etwas in der Queue steckt.

Sternchen-Importe sind immer noch Böse™. :-)

Die Namen `t` und `q` sind etwas zu kurz.

Du übertreibst es ein *bisschen* mit Ausrufezeichen in Kommentaren. ;-) Und Kommentare sollten auch zum Quelltext passen. "wait a second" wo *keine* Sekunde gewartet wird, oder "count down" wo *hoch* gezählt wird, sind irgendwie nicht so toll. Wobei an den beiden Stellen der Quelltext eigentlich selbst schon genug aussagt.

Bei `Counting.run()` würde ich ``while True:`` schreiben. Und der auskommentierte Teil der Bedingung begeht wahrscheinlich den Fehler um den es hier geht: Zugriff auf die GUI von einem anderen Thread als dem, in dem die Hauptschleife der GUI läuft.
problembär

RedSharky hat geschrieben:Hier ein Beispiel, Kritik erwünscht
Ich sehe immer noch nicht, wozu Du da Threads brauchen solltest:

Code: Alles auswählen

import tkinter as tk

class Counter:

    def __init__(self):
        self.t = 0

    def increaseT(self):
        self.t += 1

class AppGUI:

    def __init__(self):

        self.root=tk.Tk()
        self.root.title("Counting")
        self.root.wm_geometry('170x50+200+200')
        self.root.resizable(False, False)
        self.root.deiconify()
 
        self.c = Counter()
       
        self.label = tk.Label(self.root, font = ("Arial", "30"))
        self.label.pack()
        self.label.config(text = str(self.c.t))
       
        self.root.after(100, self.label_update)
        self.root.mainloop()
       
    def label_update(self):
        self.c.increaseT()
        print("label update")
        self.label.config(text = str(self.c.t))
        self.root.after(100, self.label_update)
       
def main():
   app = AppGUI()  

if __name__ == '__main__':
    main()
Gruß
BlackJack

@problembär: Das richtige Problem ist etwas komplizierter als ein einfacher Counter. Da braucht man schon einen Thread zu. Man will ja nicht einen lang laufenden Algorithmus so umschreiben, dass er immer nur Stückweise mit `after()` läuft und dadurch Zeit verschenkt.
problembär

BlackJack hat geschrieben:@problembär: Das richtige Problem ist etwas komplizierter als ein einfacher Counter. Da braucht man schon einen Thread zu. Man will ja nicht einen lang laufenden Algorithmus so umschreiben, dass er immer nur Stückweise mit `after()` läuft und dadurch Zeit verschenkt.
Ok, das seh' ich ein.

Irgendwie ist mir bei Threads immer ein bißchen unwohl, ob das auch stabil genug läuft. Wenn das wirklich zwei ganz getrennte Sachen sind, die gleichzeitig laufen sollen, nehm' ich meistens lieber einen zweiten Prozeß, mit dem ich über "subprocess" kommuniziere. Das geht (auch) ganz gut.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Das kannst du mit multiprocessing aber schon einfacher haben...
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

problembär hat geschrieben:Irgendwie ist mir bei Threads immer ein bißchen unwohl, ob das auch stabil genug läuft. Wenn das wirklich zwei ganz getrennte Sachen sind, die gleichzeitig laufen sollen, nehm' ich meistens lieber einen zweiten Prozeß, mit dem ich über "subprocess" kommuniziere. Das geht (auch) ganz gut.
Du willst ZeroMQ nutzen.
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

So, ich bins nochmal.

Wie muss ich es machen, wenn ich nicht nur ein Widget updaten möchte, sondern viele, ca. 20. Soll ich dann für jedes Widget einen eigene Queue einrichten und an jedes Wiget ein after()->get() dranhängen, oder geht das irgendwie eleganter. Ist es überhaupt wichtig, an welches Widget man das after() koppelt? Eigentlich müsste das doch egal sein. Wie macht ihr denn sowas? Oder brauch man generell nur eine Queue, oder in diesem Fall tatsächlich 20? Irgendwie verstehe ich das Prinzip nicht. Ich könnte zwar alles einzeln per Hand und zu Fuß machen, aber das kann doch nicht richtig sein!

Ich möchte doch nur eine Tk-Oberfläche, die mir zeitnah ein Feedback über meine ganzen Schleifen, Dateiladenvorgänge, usw anzeigt.

Hätte jemand ein Beispiel? Mehrere Widgets, die aus komplett unanhängigen Threads aktualisiert werden - einfach erweiterbar.

Ich versuche mir gerade eine Minimalanwendung stricken, die dann nach Belieben ausbauen kann. Bisher ist es leider so, dass ich eine Idee habe und anfange zu programmieren, dann aber immer an einen Punkt komme, an dem ich nicht weiterkomme, weil irgendetwas, was mit print() noch ausgegeben werden konnte, ums Verrecken nicht über die Widgets dargestellt werden möchte und gerne mal die ganze Applikation wegreißt. Deshalb auch der Plan, ein funktionierendes Grundgerüst zu haben, als Frustrationsprävention. Leider habe ich sowas noch nirgendwo gesehen.
BlackJack

@RedSharky: Auf welchem Widget man das `after()` aufruft ist in der Regel nicht so wichtig. Wieviele Widget pro Queue Du brauchst, hängt IMHO vom Anwendungsfall ab. Du kannst eine für alle haben, eine für jedes Widget, aber auch irgendwas dazwischen. Was halt am meisten Sinn macht.

Wobei 20 Widgets die gleichzeitig aktualisiert werden, schon ein wenig unübersichtlich klingt.
yipyip
User
Beiträge: 418
Registriert: Samstag 12. Juli 2008, 01:18

Da mir bis jetzt noch niemand gesagt hat, daß ich dort Unsinn verzapft habe:

http://www.python-forum.de/viewtopic.ph ... 41&start=0
und
http://www.python-forum.de/viewtopic.ph ... 90&start=0

sollten ganz hilfreich sein.

:wink:
yipyip
Antworten