Statusbar Update während eine externe Klasse ausgeführt wird.

Fragen zu Tkinter.
Antworten
kitsab
User
Beiträge: 10
Registriert: Mittwoch 25. März 2020, 22:14

Hallo,

bin relativ neu bei der TKinter Verwendung.

Ich nutze einen Button um eine Klasse eines imporierten Moduls auszuführen:

GUI Modul:
...

Code: Alles auswählen

class Window:
      .....
     def init_main_window(self)
           ......
           CalcButton = Button(self, text="Kalkulation", command=lambda: self.runCalcButton())
           self.sbar = Label(self, textvariable=self.statusvar, relief=SUNKEN, anchor="w")
           ......

      def runCalcButton(self):
            R1, R2, R3, Inf, Warn, Err = IBRouteCalc.runthis()

    def update_bar(self):
        sleep(1)
        if MQCounter > 0 or IBProgCount > 0:
            self.statusvar.set("IB Progress: " + str(IBProgCount) + "Abfragen " + str(MQCounter))
            self.sbar.update()

if __name__ == "__main__":
    root = Tk()
    root.geometry("400x600")
    app = Window(root)
    app.update_bar()
    root.mainloop()
______________________________
Kalkulationsmodul:

Code: Alles auswählen

IBRouteCalc.py:
MQCounter = 0
IBProgCounter = 0
class IBRouteCalc:
    def runthis(IB_File):
           Ausführung meherer def's (einer dieser def's steigert die beiden Counter)

Ich dachte, ich könnte nun während der Ausführung der Klasse aus der IBRouteCalc.py auf die beiden Counter zugreifen und sobald die größer 0 sind sekündlich einen Fortschritt in der Statusbar aktulaisieren könnte. Dem ist aber nicht so.

Ich bekomme hin, dass sich der Status zumindest partiell so anzeigen lässt:

Code: Alles auswählen

      def runCalcButton(self):
            self.statusvar.set("In Progress")
            self.sbar.update()
            R1, R2, R3, Inf, Warn, Err = IBRouteCalc.runthis()
            self.statusvar.set("Ready")
            self.sbar.update()
Allerdings vermute ich, dass ich aktiv die werte aus der Kalkulationsklasse an die GUI senden muss, während der Ausführung der externen Klasse sind anscheinend die Updates bzw die Ausführung der GUI pausiert.

Wie stelle ich es an, dass ich aus einer Klasse während der Laufzeit Fortschrittsinformationen in der GUI erhalte?

Danke für Anregungen

Viele Grüße

Kitsab
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Variablennamen und Funktionen schreibt man komplett klein.
Statt Strings per + zusammenzustückeln, benutzt man Formatstrings.
GUIs und Berechnungen im Hintergrund sind extrem komplexe Dinge. Sowas wie sleep darf in einer GUI nicht vorkommen, und längerlaufende Prozesse müssen in einem seperaten Thread laufen. Aus diesem Thread darf man aber die GUI nicht ändern, so dass man eine Queue für die Übertragung der Information braucht, die man regelmäßig in der GUI-Klasse abfragen muss.
Dazu gibt es hier im Forum etliche Beiträge. Einfach mal suchen.
kitsab
User
Beiträge: 10
Registriert: Mittwoch 25. März 2020, 22:14

Hallo,

ja ich weiß ich bin Anfänger und habe mir Python anhand von Online Guides selber beigebracht. Mein Programmierstiel ist alles andere als sauber und ich danke für die Hinweise wie man das sauberer umsetzen könnte.
Was die Suche angeht bin/war ich mir nicht sicher welche Suchbegriffe ich verwenden sollte um das gewünschte Ergebnis zu erhalten.
Ich hab schon in google nach "Do something during Tkinter mainloop" gesucht aber irgendwie war ich damit auf dem Holzweg.
Durch weitere Recherche habe ich zumindest einen Teilerfolg Errungen indem ich meine Funktion so umgeschrieben habe:

Code: Alles auswählen

    def update_bar(self):
        self.counter += 1
        self.statusvar.set(str(self.counter))
        self.after(1000, func=lambda: self.update_bar())
Damit zählt die Statusbar dann zumindest schonmal im Sekundentakt hoch und da ich im def __init__ die Funktion aufrufe.
Nun muss ich nur noch die Updates aus meinem anderen Modul hinbekommen und die Dauerupdates mit einem True/False Schalter versehen, damit die Updates nur zum gewünschten Zeitpunkt laufen.

*Ein Tip was ich als Suchbegriff für die oben gestellt Frage eingeben könnte wäre nett.

Mit freundlichen Grüßen

Kitsab
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@kitsab: Noch ein paar Rückmeldungen zu den gezeigten Quelltextausschnitten:

Warum gibt es `init_main_window()`? Die `Window`-Klasse hat doch eine `__init__()`-Methode. Da gehört die Initialisierung rein.

In der Methode wird das Objekt selbst an die erzeugten Widget-Objekte als `master` übergeben, `Window` erbt aber gar nicht von einer Klasse die das ermöglichen würde *und* bekommt ein `Tk`-Objekt beim erstellen übergeben. Das ist extrem schräg und dürfte so überhaupt gar nicht funktionieren.

Beide gezeigten ``lambda``-Ausdrücke sind unnötig. Es wird ja nur eine Methode mit den gleichen Argumenten aufgerufen, welche die anonyme Funktion übergeben bekommt (in beiden Fällen gar keine). Da kann und sollte man einfach direkt die Methode übergeben, weil diese Indirektion keinen Sinn machen.

Das es im GUI-Modul offenbar globale Namen `MQCounter` und `IBProgCount` gibt, die es ebenfalls noch mal global im Kalkulationsmodul gibt, sieht verdächtig falsch aus. Also mal davon abgesehen, dass es überhaupt solche globalen Variablen gibt.

`root` und `app` sind auch globale Variablen in dem Modul. Das Hauptrprogramm gehört in eine Funktion.

Die `update()`-Methode sollte man nicht selbst aufrufen. Die ist nicht ganz ungefährlich und in der Regel ein Zeichen, dass man gegen das Rahmenwerk programmiert und nicht mit dem Rahmenwerk.

Das Kalkulationsmodul und `IBRouteCalc` sind falsch. Verwende keine globalen Variablen und keine Klassen die überhaupt gar keine Klassen sind. Klassen sind nicht einfach ein Namensraum für Funktionen. Das ist die Aufgabe von Modulen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
kitsab
User
Beiträge: 10
Registriert: Mittwoch 25. März 2020, 22:14

Hallo,

sorry, es hat ewig gedauert, dass ich wieder zum Programmieren gekommen bin, ich habe diese Woche am Programm weiter gemacht, mit Klassen habe ich noch zu Kämpfen. Aber danke schon mal für die Tips.

Ich habe meine erstes Programm Modul Classe IBRoute nun richtig als Klasse programmiert, jetzt nehme ich mir nochmals das Window vor.

Viele Grüße

Kitsab
kitsab
User
Beiträge: 10
Registriert: Mittwoch 25. März 2020, 22:14

Hallo,

ich hab nun doch noch weiter gemacht, und die Klasse angepasst. Ich habe mir Python komplett selbst beigebracht mit Online Tutorials, daher das "Unwissen" bei vielen Dingen. Ich suche mir einfach oft Beispiele und versuche das dann nachzubauen.

Nun sieht es so aus (Ausschnitte):
Gui File:

Code: Alles auswählen

import tkinter
import requests
from tkinter import *
from tkinter import messagebox as tkMessageBox
from IBroute import IBRouteCalc
from tkinter import filedialog as fd

class mainwindow(Tk):

    def __init__(self, title, geometry):
        super().__init__()
        self.UpdateRate = 1000
        self.title(title)
        self.geometry(geometry)
        self.UpdateRate = 1000
        self.IBrunSession = IBRouteCalc()
        self.counter = 0
        self.statusvar = StringVar()
        self.statusvar.set("Bereit")
        self.update_bar()
	.... Buttons, Textefelder, Labels, Menu etc (funktionert auch wie gewünscht)
	
      def update_bar(self):
        if self.IBrunSession.IBProgCount > 0:
            self.statusvar.set("IB Progress: " + str(self.IBrunSession.IBProgCount))
        self.after(1000, func=lambda: self.update_bar())                # wenn ich hier kein lambda verwende bricht dasScript mit dem Fehler 					 
                                                                                                                  exzessive Wiedeholung ab
        
      def runcalcbutton(self):
        Frame.clipboard_clear(self)
        ReturnArr = IBRouteCalc.runthis(self.IBrunSession) # <---- exzessiver Prozess aus einer Klasse in einem anderen Phyton Script
  
       ..... anderes Zeug   

if __name__ == "__main__":
    mainwindow = mainwindow("IBRouteGUI", "400x400")
    mainwindow.mainloop()

IBRouteCalc in seperatem File (Klasse)

Code: Alles auswählen

import ... Zeug

class IBRouteCalc:
   def __init__(self)
         self.IBProgCount = 0
         .... viele andere Variablen aus der Klasse
         
   def runthis(self):
        self.ErrorHandling()
        self.loadconfig()
        if self.SysName != "None" and self.SysPSI == "None":
            self.Config.update({"KompColumn": 0})
            self.Config.update({"KompCSV": self.Config["SysCSVSourceFile"]})
        if self.IBFile != "None":
            self.Config.update({"IBSourceFile": str.replace(self.IBFile, "/", "\\")})
            print("Using manuallyT selected datasource: " + str(self.IBFile))
        if self.SingleSystem == True:
            self.debug.update({"SingleSystem": True})
            self.DebugSingleSystem = {self.SysNo: [self.SysName, self.SysZip, 'dummy', 'dummy', 'dummy', self.SysCity, self.SysPSI]}
        # self.DebugSingleSystem = {"PC3434XR03": ["Pristina", "89518", 'dummy', 'dummy', 'dummy', "Heidenheim", "XMM567"]}
        self.Debugloop()
        self.Load_IBList()
        self.Debugloop()
        self.Load_Kompetenz()
        self.Load_SysFile()
        self.Load_IBFile()
        self.Load_TechFile()
        self.Create_KompDic()
        self.OutputSystemsFiles()
        self.Prepare_Compdic()
        self.ReadTechDrive()
        self.IB_Tech_calc()
        self.Output_override()
        self.ReadTechDrive()
        self.Output_result()
        self.calc_statistic()
        self.DebugLoop2()
        self.SysPSI = "None"
        self.SysCity = "None"
        self.SysName = "None"
        self.SingleSystem = False
        self.SysNo = "None"
        self.SysZip = "None"
        return self.Results()

Beide Python Scripts funktionieren an sich gut.

Nur die Statusanzeige der Variable IBProgCount funktioniert nicht wie ich es gerne hätte.

Diese zeigt zu Anfang, wenn ich das Programm lade "Bereit" in der Statusbar an.
und wenn der Prozess IBRouteCalc.runthis(self.IBrunSession) ausführe ändert sich zur Laufzeit nichts, wenn der Prozess beendet ist, springt der Zähler in der Statusbar umgehend auf 1865.
Der Prozess kann bis zu 15-20 Minuten dauern, weil viele Daten über Internet Abfragen eingeholt werden.
Mein Wunsche wäre die Statusbar zur Laufzeit des IBRouteCalc Prozesses zu aktulaisieren.
Muss ich hier Funktionen wie Subprocess oder Threading in betracht ziehen. Es sieht mir so aus, als würde Python hier eine Stapelabarbeitung durchführen und die Aktualisierung erst nach Ende des Prozesses anzeigen.
Führe ich den Prozess IBRouteCalc in der eigenen Klasse als Main Window aus (if __name__ == "__main__":) dann erscheint die Ausgabe auf der Script-Befehlszeile und der Zähler wird laufend aktualisiert.

Danke und viele Grüße

Kitsab
Benutzeravatar
sparrow
User
Beiträge: 4144
Registriert: Freitag 17. April 2009, 10:28

Unabhängig von deinem akuten Problem Hinweise zu deinem Quelltext, warum ich gerade nicht tiefer in dein Programm einsteige:

Verwene keine * Importe.
Damit holt man sich eine Vielzahl von Namen in den Namensraum, die unter Umständen unbemerkt kollidieren und dadurch unerwartetes Verhalten hervorrufen. Zudem ist es für den Leser (und das bist auch du) nicht mehr möglich nachvollziehen, woher ein Name überhaupt kommt.
Gerade bei tkinter reden wir hier von hunderten Namen.
Verwende also statt "from tkinter import *" das übliche "import tkinter as tk" und greife per "tk.name" auf die entsprechenden Namen zu.

Namen schreibt man in Python klein_mit_unterstrich. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und die Namen von Klassen (PascalCase). Namen sollten sprechend sein, die von Funktionen die Tätigkeit beschreiben, die sie tun.
Dass du fast alle Namen in PascalCase schreibst, macht das Lesen für mich so unübersichtlich, dass ich nicht einsteige.
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Wie schon geschrieben, muß die Hauptschleife der GUI ständig laufen. Langlaufende Funktionsaufrufe sind nicht erlaubt, die müssen in einen Hintergrundprozess verschoben werden. Die falsch geschriebenen Variablen- und Methodennamen machen den Code schwierig zu lesen, auch 17 Funktionsaufrufe hintereinander sind nicht erfassbar für das menschliche Hirn. Benutze keine kryptischen Abkürzungen. Was ist ein Sys-No? oder ein IB? ein KombDic? Und warum ist das an anderer Stelle ein Compdic? Der Leser sollte nicht rätseln müssen.
Wenn man einen einzelnen Wert eines Wörterbuchs setzen will, benutzt man nicht update mit einem Wörterbuch mit nur einem einzigen Eintrag, sondern setzt einfach der Wert per Indexzugriff: `config["KompColumn"] = 0`
Wenn ein lambda-Ausdruck eh nur eine Funktion aufruft, dann übergibt man die Funktion direkt.
Man referenziert keine Methode über die Klasse: `self.IBrunSession.runthis()`
Genauso ist das auch bei str.replace.

Um Dir helfen zu können, brauchen wir einen für uns ausführbaren Code, der nur das Wesentliche enthält.
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ergänzend: das sind ja auch keine 17 Funktionsaufrufe sondern Methoden von denen keine Argumente entgegen nimmt und nur eine etwas zurück gibt. Zusammen mit dem Kommentar „... viele andere Variablen aus der Klasse“ sieht das so aus als wenn hier ”Funktionen” die auf einer grossen Menge globaler Variablen operieren, einfach in eine Gottklasse verschoben wurden. Das sollte man mit echten Funktionen und sinnvollen Klassen lösen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
kitsab
User
Beiträge: 10
Registriert: Mittwoch 25. März 2020, 22:14

Hallo,

danke für die bisherigen Ratschläge, ich habe das nun versucht nach euren Hinweisen umzusetzen.

GUI Klasse:

Code: Alles auswählen

from tkinter import Tk, Menu, StringVar, Button, Label, SUNKEN, BOTTOM, X, messagebox
from test import counter

class mainwindow(Tk):
    def __init__(self, title, geometry):
        super().__init__()
        self.counterinstance = counter()
        self.title(title)
        self.geometry(geometry)
        self.updaterate = 1000
        self.counter = 0
        self.statusvar = StringVar()
        self.statusvar.set("Bereit")
        self.update_bar()
        menu = Menu(self)
        file = Menu(menu)
        file.add_command(label="Exit", command=self.client_exit)
        menu.add_cascade(label="File", menu=file)
        self.statusvar = StringVar()
        self.statusvar.set("Bereit")
        self.sbar = Label(self, textvariable=self.statusvar, relief=SUNKEN, anchor="w")
        self.sbar.pack(side=BOTTOM, fill=X)
        quitbutton = Button(self, text="Quit", command=self.client_exit)
        quitbutton.place(x=50, y=20)
        singlesystembutton = Button(self, text="Single System", command=lambda: self.singlesystembutton())
        singlesystembutton.place(x=10, y=100)
        self.update_bar()

    def update_bar(self):
        print("test1")
        if self.counterinstance.count > 0:
            self.statusvar.set("IB Progress: " + str(self.ibrunsession.IBProgCount))
        self.after(1000, func=lambda: self.update_bar())

    def client_exit(self):
        self.quit()

    def singlesystembutton(self):
        self.counterinstance.process()
        print("Counter aktviert")

    def update_bar(self):
        if self.counterinstance.count > 0:
            self.statusvar.set("Fortschritt: " + str(self.counterinstance.count))
        self.after(1000, func=lambda: self.update_bar())


if __name__ == "__main__":
    mainwindow = mainwindow("StatusBarTest", "400x400")
    mainwindow.mainloop()

Klasse mit Rechenprozess:

Code: Alles auswählen

from time import sleep

class counter:
[code]
    def __init__(self):
        self.count = 0

    def process(self):
        for i in range(1,20):
            self.count += 1
            print(self.count)
            sleep(1)

if __name__ == "__main__":
    x = counter()
    x.process()
Ich hoffe, dass das nun nachvollziehbar ist. Die Statusbar zeigt erst nach Ablauf der Zeit den letzten Counterstand an, mein Wunsch wäre die Aktualisierung direkt zu sehen.

Danke für eure Tipps

Viele Grüße

Kitsab
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Klassen schreibt man mit großem Anfangsbuchstaben, daran erkennt man gleich was eine Klasse ist und das verhindert, dass Du die Klasse mainwindow mit deren Instanz überdeckst.
`update_bar` hast Du doppelt definiert, `client_exit` ist überflüssig, da Du gleich `quit` verwenden könntest. Alle Deine lambda-Ausdrücke sind unnötig, weil Du dort auch gleich die Funktionen, angeben könntest.

Du brauchst einen Thread, der im Hintergrund läuft.
kitsab
User
Beiträge: 10
Registriert: Mittwoch 25. März 2020, 22:14

Hallo,

leider weiß ich nicht genau wie ich den Tip umsetzen soll, ich habe es nun mal mit Threading versucht, aber das resultat ist das selbe:

Gui Klasse:

Code: Alles auswählen

from tkinter import Tk, Menu, StringVar, Button, Label, SUNKEN, BOTTOM, X, messagebox
from test import Counter
import threading

# def counter_process():
#     x = counter()
#     threading.Thread(x.process())

class Mainwindow(Tk):

    def __init__(self, title, geometry):
        super().__init__()
        self.counterinstance = Counter()
        self.counterthread = None
        self.title(title)
        self.geometry(geometry)
        self.updaterate = 1000
        self.counter = 0
        self.statusvar = StringVar()
        self.statusvar.set("Bereit")
        menu = Menu(self)
        file = Menu(menu)
        file.add_command(label="Exit", command=self.client_exit)
        menu.add_cascade(label="File", menu=file)
        self.statusvar = StringVar()
        self.statusvar.set("Bereit")
        self.sbar = Label(self, textvariable=self.statusvar, relief=SUNKEN, anchor="w")
        self.sbar.pack(side=BOTTOM, fill=X)
        quitbutton = Button(self, text="Quit", command=self.client_exit)
        quitbutton.place(x=50, y=20)
        # singlesystembutton = Button(self, text="Single System", command=lambda: self.singlesystembutton())
        singlesystembutton = Button(self, text="Single System", command=self.singlesystembutton)
        singlesystembutton.place(x=10, y=100)
        self.update_bar()

    def update_bar(self):
        if self.counterinstance.count > 0:
            self.statusvar.set("IB Progress: " + str(self.counterinstance.count))
        self.after(1000, func=lambda: self.update_bar())      #hier benötige ich das Lamba, ohne das exzessive Wiederholung und 	Programm steigt aus

    def client_exit(self):
        quit()

    def singlesystembutton(self):
        self.counterthread = threading.Thread(self.counterinstance.process())
        self.counterthread.start()
        self.counterthread.join()

if __name__ == "__main__":
    def window():
        window = Mainwindow("StatusBarTest", "400x400")
        window.mainloop()
    t1 = threading.Thread(target=window)
    t1.start()
    t1.join()
Prozess Klasse:

Code: Alles auswählen

from time import sleep

class Counter:
    def __init__(self):
        self.count = 0

    def process(self):
        for i in range(1,10):
            self.count += 1
            print(self.count)
            sleep(1)

if __name__ == "__main__":
    x = counter()
    x.process()
Danke für eure Hilfe.

Viele Grüße

Kitsab
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du brauchst da immer noch kein lambda. Ein simples self.update_bar reicht. OHNE Klammern!

Und du sollst nicht die GUI in einen Thread packen. Das geht nicht gut, je nach OS. Sondern einen extra Thread, der die Arbeit macht. Und via einer queue zb den Fortschritt an das update_bar vermeldet.
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Man definiert keine Funktionen innerhalb eines if. Der einzige Befehl innerhalb des if __name__...-Blocks ist `main()`.
Die GUI muss im Hauptthread laufen, dafür einen extra Thread zu starten, und den Hauptthread lahmzulegen ist eh Unsinn.
In `singlesystembutton`: target bei Thread muß eine Funktion sein, und nicht der Rückgabewert der Funktion, die man starten möchte, so blockiert ja wieder die Ereignisschleife bis die Berechnung zu Ende ist. Und auch das Warten auf das Ende des Threads ist kontraproduktiv, wenn man doch etwas parallel ausführen will.
kitsab
User
Beiträge: 10
Registriert: Mittwoch 25. März 2020, 22:14

Hallo,

endlich geschafft, danke für die Hilfe.

def singlesystembutton(self):
self.counterinstance.count = 0
t1 = threading.Thread(target=self.counterinstance.process) #hier lag der Fehler ich hatte Urspürnglich eine Klammer hinter .process())
t1.start()

den Prozess für die GUI habe ich korrigiert, der einzige Thread ist die Funktion.

Viele Grüße

Kitsab
Antworten