Seite 1 von 1

GUI update

Verfasst: Mittwoch 1. September 2010, 11:08
von RedSharky
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()

Re: GUI update

Verfasst: Mittwoch 1. September 2010, 13:01
von 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.

Re: GUI update

Verfasst: Mittwoch 1. September 2010, 13:19
von yipyip
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

Re: GUI update

Verfasst: Mittwoch 1. September 2010, 13:31
von RedSharky
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()

Re: GUI update

Verfasst: Mittwoch 1. September 2010, 13:47
von yipyip
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

Re: GUI update

Verfasst: Mittwoch 1. September 2010, 13:50
von RedSharky
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?

Re: GUI update

Verfasst: Mittwoch 1. September 2010, 14:48
von 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.

Re: GUI update

Verfasst: Mittwoch 1. September 2010, 21:08
von problembär
RedSharky hat geschrieben:after() ist da glaub ich nicht das Wahre
Doch, siehe z.B. hier.

Re: GUI update

Verfasst: Freitag 3. September 2010, 11:27
von RedSharky
@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?

Re: GUI update

Verfasst: Freitag 3. September 2010, 11:58
von RedSharky
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()

Re: GUI update

Verfasst: Freitag 3. September 2010, 12:16
von 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.

Re: GUI update

Verfasst: Samstag 4. September 2010, 00:21
von 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ß

Re: GUI update

Verfasst: Samstag 4. September 2010, 07:26
von 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.

Re: GUI update

Verfasst: Samstag 4. September 2010, 18:04
von 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.

Re: GUI update

Verfasst: Samstag 4. September 2010, 20:33
von Leonidas
Das kannst du mit multiprocessing aber schon einfacher haben...

Re: GUI update

Verfasst: Samstag 4. September 2010, 23:50
von DasIch
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.

Re: GUI update

Verfasst: Sonntag 5. September 2010, 09:02
von RedSharky
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.

Re: GUI update

Verfasst: Sonntag 5. September 2010, 09:37
von 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.

Re: GUI update

Verfasst: Sonntag 5. September 2010, 11:12
von yipyip
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