Mehrere Tkinter - Code bleibt stecken

Fragen zu Tkinter.
Antworten
tobi45f
User
Beiträge: 24
Registriert: Montag 22. Februar 2021, 14:31

Hallo zusammen,

ich habe ein Problem, mit der Ausführung meines Codes, was mit den Fenstern zusammenhängt, da der Code hängen bleibt. Ich habe ein Fenster (FensterResult), welches meine Daten anzeigt. Über Buttons wird eine neue Berechnung (Calculate) angestoßen und dessen Ergebnisse (Dataset) werden angezeigt. Die Berechnung greift auf ein anderes Programm zu und liest dort die selektierten Objekte aus. Das Problem: Wenn die Objekte müssen in der "richtigen Reihenfolge" selektiert werden. Da hier viel Fehlerpotenzial herrscht wollte ich die selektierten Objekte über ein kleines Fenster (FensterKnoten) anzeigen lassen mit der Möglichkeit diese zu tauschen.

Ohne die Anzeige von FensterKnoten läuft das Programm, wie es soll. Ich drücke berechnen - er berechnet und zeigt die Werte an.

Sobald ich aber ein zweites tkinter Fenster anzeigen lasse, dann bleibt er stecken. Wenn ich dann aber das andere Fenster AUCH schließe, in dem die Ergebnisse angezeigt werden, dann wird die Berechnung ausgeführt aber logischerweise nicht mehr angezeigt ;)

Irgendwas ist da mit den mainloops falsch. Allerdings weiß ich nicht, wie man es anders/besser macht - ich weiß auch leider nicht, welche Begriffe ich da am besten bei der Suche verwende!?

Kann mir hier jemand bitte weiterhelfen?

Der Code sollte so lauffähig sein. Wenn man in der def Calculate die entsprechenden Zeilen auskommentiert, dann läuft es, nur eben ohne die Abfrage.

Gruß Tobias

Code: Alles auswählen

import tkinter as tk

class Dataset:
    def __init__(self, data, root, column):
        self.data = data
        self.obj = []
        self.root = root    
        self.column = column    
    
        row = 0
        for x in range(0, len(self.data)-2):
            label = tk.Label(self.root, text=str(round(self.data[x],2)))
            label.grid(row = row, column = self.column)
            row+=1

            self.obj.append(label)
              

        btn = tk.Button(self.root, text='löschen', command=self.Delete)
        btn.grid(row=row, column = self.column)        
        self.obj.append(btn)
        row += 1

    def Delete(self):
        for o in self.obj:
            o.destroy()

class FensterResult:
    def __init__(self):
        self.calcs = []
        self.column = 2

    def Show(self):
        self.root = tk.Tk()
        self.root.title('Ergebnisvorschau')

        #NVP Daten
        rows = ["NVP", "S_K''min", "Netzimp.winkel", "UW Sammelschiene", 
        "S_K''max", "I_K''", "I_p", "I_a", "delta U NVP", "delta U_max Netz", "U/U_n max Netz", "E-Spulenkapazität",
        "I_E abgelesen", "I_E Planung", "I_E Bestand"]
        
        for x in range(len(rows)):
            tk.Label(self.root, text=rows[x]).grid(row=x, column=1)

        tk.Button(self.root, text='Neue Berechnung', command=self.Berechnung, height=1, width = 15
        ).grid(row=len(rows), column=1, pady=4, padx = 5)
        
        tk.Button(self.root, text='Speichern', command=self.Speichern, height=1, width = 15
        ).grid(row=len(rows)+1, column=1, pady=4, padx = 5)

        tk.Button(self.root, text='Beenden', command=self.Ok, height=1, width = 15
        ).grid(row=len(rows)+2, column=1, pady=4, padx = 5)
        
        self.root.mainloop()
    

    def Berechnung(self):
        calc = Calculate()
        if calc != 0:
            dataset = Dataset(calc, self.root, self.column)        
            self.column += 1
            self.calcs.append(dataset)
        
    def Speichern(self):
        print("save")


    def Ok(self):
        self.abbruch = False
        self.Quit()   

    def Quit(self):
       self.root.destroy()

class FensterKnoten:
    def __init__(self, s_name, s_node_id, nvp_name, nvp_node_id):
        self.s_node_id = s_node_id
        self.nvp_node_id = nvp_node_id
        self.s_name = s_name
        self.nvp_name = nvp_name
        self.switched = False
        self.abbruch = True

    def Show(self):
        self.root = tk.Tk()
        self.root.title('Auswahl')

        tk.Label(self.root, text="K1:").grid(row=0, column=0, padx=4, pady=4)
        tk.Label(self.root, text=self.s_name).grid(row=0, column=1, padx=4, pady=4)

        tk.Label(self.root, text="K2:").grid(row=1, column=0, padx=4, pady=4)
        tk.Label(self.root, text=self.nvp_name).grid(row=1, column=1, padx=4, pady=4)  

        tk.Button(self.root, text='berechnen', command=self.Ok).grid(row=3, column=0, padx=4, pady=4)
        tk.Button(self.root, text='tauschen', command=self.Switch).grid(row=3, column=1, padx=4, pady=4)

        self.root.mainloop()

    def Ok(self):
        self.abbruch = False
        self.Quit()

    def Quit(self):
       self.root.destroy()

    def Switch(self): 
        buffname = self.s_name
        buffid = self.s_node_id

        self.s_name = self.nvp_name
        self.s_node_id = self.nvp_node_id
        self.nvp_name = buffname
        self.nvp_node_id = buffid
        self.Quit()
        self.switched = True

def Calculate():    
    s_node_id = 123123
    nvp_node_id = 44444
    s_name = "abc"
    nvp_name = "blub"

    #Fenster anzeigen, welches die Auswahl anzeigt zur Prüfung, ob die beiden Knoten richtig (und nicht vertauscht) angegeben worden sind
    fstr = FensterKnoten(s_name, s_node_id, nvp_name, nvp_node_id)
    fstr.Show()
    #erneut anzeigen, wenn man die Knoten gewechselt hat oder die NAB-Anzahl falsch ist
    while fstr.switched:
        fstr = FensterKnoten(fstr.s_name, fstr.s_node_id, fstr.nvp_name, fstr.nvp_node_id)
        fstr.Show()  

    if fstr.abbruch:
        return 0

    #Berechnungsstuff

    return (123,234,345,456,57,76,6786,78,435,3453,4342,4444,5555,444,55,777,77)

if __name__ == "__main__":  
    fnstr = FensterResult()
    fnstr.Show()
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@tobi45f: „mainloops“ — Mehrzahl klingt schon falsch. Eine Hauptschleife heisst so weil es die in der Regel *ein mal* gibt.

Es darf nur *ein* `Tk`-Objekt geben. Da hängt der gesamte Tcl/Tk-Interpeter dran und halt auch die Hauptschleife. Zusätzliche Fenster kann man mit `tkinter.Toplevel` erstellen.

Der Code im ``if __name__ …`` sollte auch in einer Funktion stehen, damit `fnstr` nicht einfach so global vorhanden ist.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

`fnstr`? Hat Dir jemand die Vokale geklaut? Wir sind hier nicht beim Glücksrad, die Buchstaben sind nicht knapp und man muss auch keine Vokale extra kaufen. Namen sollten keine kryptischen Abkürzungen enthalten oder gar nur daraus bestehen.

Die Worte sollten auch in der richtigen Reihenfolge stehen. Falls Du nicht Yoda bist ist ein Fensterknoten etwas anderes als ein Knotenfenster.

Die `show()`-Methoden sind komisch. Die enthalten Code der in die `__init__()` gehört. Nach Ablauf der `__init__()` sollte ein Objekt in einem benutzbaren Zustand sein und dort werden auch alle Attribute angelegt, nicht in späteren Methodenaufrufen. Das `FensterResult` beispielsweise irgendwann ein `abbruch`-Attribut bekommt (das auch nirgends verwendet wird) ist sehr überraschend. Das sieht nach kopiertem Code aus. Die `Quit()`-Methode wird auch nur indirekt von `Ok()` verwendet. Beide Methoden können weg weil letztlich alle auf ein ``self.root.destroy()`` hinaus läuft. Die Methode kann man aber auch einfach direkt als `command` an die Schaltfläche übergeben.

``for i in range(len(sequence)):`` ist in Python ein „anti-pattern“. Man kann direkt über die Elemente von Sequenztypen iterieren, ohne den Umweg über einen Index. Sollte man zusätzlich eine laufende Zahl benötigen gibt es die `enumerate()`-Funktion. Besonder unsinnig ist das bei der Schleife wo zusätzlich zur Laufvariablen `x` auch noch `row` manuell hochgezählt wird. `x` und `row` haben in der Schleife immer den gleichen Wert.

Die Spaltennummerierung im `grid()` fängt bei 0 an.

`calcs` hiesse wohl besser `datasets`.

`Calculate()` gibt entweder eine 0 oder ein Tupel mit Werten zurück, was eine schräge API ist. Rückgabewerte sollte in der Regel immer den gleichen (Duck-)Typ haben. Wobei `None` als Wert für ”nichts” mit jedem anderen Typ ”kompatibel” ist.

Ohne die blockierenden `mainloops()` geht das mit dem Programmablauf auch nicht so ”linear” und um das *richtig* zu machen, muss man einiges beachten. Es würde sich also anbieten `tkinter.simpledialog.Dialog` als Basis für eigene modale Dialoge zu verwenden.

Es ist aber auch nicht wirklich sinnvoll nur wegen dem vertauschen einen neuen Dialog mit den vertauschten Daten zu öffnen. Besser wäre es einfach in dem bereits angezeigten Dialog die Daten zu vertauschen und anzuzeigen.

Das vertauschen von zwei Werten geht in Python einfacher als selbst temporäre Namen und drei Zuweisungen zu schreiben: ``a, b = b, a``.

In `Dataset` gehören `root` und `column` nicht zum Zustand, das sind einfach nur lokale Namen in der `__init__()`.

`root` ist zudem zu spezifisch. Es muss gar nicht das Hauptfenster sein, jedes Containerwidget würde hier gehen.

`obj` wird gar nicht an *ein* Objekt gebunden, sondern an eine Liste von Objekten. Hier ist der Name zu generisch, denn das sind nicht beliebige Objekte, sondern Widgets.

Das ``row += 1`` am Ende der `__init__()` ist Überflüssig.

`round()` ist nicht für die Anzeige gedacht, sondern wenn man tatsächlich mit gerundeten Werten weiterrechnen möchte. Für die Anzeige formatiert man die Zahl entsprechend.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from tkinter.simpledialog import Dialog


class Dataset:
    def __init__(self, master, column, values):
        self.values = values
        self.widgets = []

        row = 0
        for row, value in enumerate(self.values[:-2]):
            label = tk.Label(master, text=f"{value:0.2f}")
            label.grid(row=row, column=column)
            self.widgets.append(label)

        btn = tk.Button(master, text="löschen", command=self.delete)
        btn.grid(row=row + 1, column=column)
        self.widgets.append(btn)

    def delete(self):
        for widget in self.widgets:
            widget.destroy()


class KnotenFenster(Dialog):
    def __init__(self, parent, s_node, nvp_node):
        self.s_node = s_node
        self.nvp_node = nvp_node
        self.should_calculate = False
        Dialog.__init__(self, parent, "Auswahl")

    def update_display(self):
        self.s_name_label["text"] = self.s_node[0]
        self.nvp_name_label["text"] = self.nvp_node[0]

    def body(self, master):
        options = {"padx": 4, "pady": 4}

        tk.Label(master, text="K1:").grid(row=0, column=0, **options)
        self.s_name_label = tk.Label(master)
        self.s_name_label.grid(row=0, column=1, **options)

        tk.Label(master, text="K2:").grid(row=1, column=0, **options)
        self.nvp_name_label = tk.Label(master)
        self.nvp_name_label.grid(row=1, column=1, **options)

        self.update_display()

    def buttonbox(self):
        box = tk.Frame(self)
        options = {"side": tk.LEFT, "padx": 5, "pady": 5}
        tk.Button(self, text="berechnen", command=self.ok).pack(**options)
        tk.Button(self, text="tauschen", command=self.on_switch).pack(
            **options
        )
        box.pack()

    def apply(self):
        self.should_calculate = True

    def on_switch(self):
        self.s_node, self.nvp_node = self.nvp_node, self.s_node
        self.update_display()


def calculate(parent):
    s_node_id = 123123
    nvp_node_id = 44444
    s_name = "abc"
    nvp_name = "blub"
    #
    # Fenster anzeigen, welches die Auswahl anzeigt zur Prüfung, ob die beiden
    # Knoten richtig (und nicht vertauscht) angegeben worden sind.
    #
    fenster = KnotenFenster(
        parent, (s_name, s_node_id), (nvp_name, nvp_node_id)
    )
    return (
        (
            123,
            234,
            345,
            456,
            57,
            76,
            6786,
            78,
            435,
            3453,
            4342,
            4444,
            5555,
            444,
            55,
            777,
            77,
        )
        if fenster.should_calculate
        else None
    )


class ErgebnisFenster:
    def __init__(self):
        self.datasets = []
        self.column = 0
        self.window = tk.Tk()
        self.window.title("Ergebnisvorschau")
        #
        # NVP Daten
        #
        rows = [
            "NVP",
            "S_K''min",
            "Netzimp.winkel",
            "UW Sammelschiene",
            "S_K''max",
            "I_K''",
            "I_p",
            "I_a",
            "delta U NVP",
            "delta U_max Netz",
            "U/U_n max Netz",
            "E-Spulenkapazität",
            "I_E abgelesen",
            "I_E Planung",
            "I_E Bestand",
        ]
        for row, text in enumerate(rows):
            tk.Label(self.window, text=text).grid(row=row, column=self.column)

        tk.Button(
            self.window,
            text="Neue Berechnung",
            command=self.berechne,
            width=15,
        ).grid(row=len(rows), column=self.column, pady=4, padx=5)

        tk.Button(
            self.window, text="Speichern", command=self.speichere, width=15
        ).grid(row=len(rows) + 1, column=self.column, pady=4, padx=5)

        tk.Button(
            self.window, text="Beenden", command=self.window.quit, width=15
        ).grid(row=len(rows) + 2, column=self.column, pady=4, padx=5)

        self.column += 1

    def show(self):
        self.window.mainloop()

    def berechne(self):
        values = calculate(self.window)
        if values != 0:
            dataset = Dataset(self.window, self.column, values)
            self.column += 1
            self.datasets.append(dataset)

    def speichere(self):
        print("save")


def main():
    fenster = ErgebnisFenster()
    fenster.show()


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
tobi45f
User
Beiträge: 24
Registriert: Montag 22. Februar 2021, 14:31

hi blackjack,

vielen Dank für deine mehr als ausführliche Antwort.
Ja einige Dinge sind stilistisch vermutlich fragwürdig, aber für einen Gelegenheitsprogrammierer, denke ich zumindest, verzeihbar ;)
die update_display Methode ist natürlich nett, die kannte ich nicht. Ich kam aucht nicht auf die Idee, dass es mit einem so einfachen Befehl getan ist. Bei C# und wpf ist das ja ein etwas größerer Aufriss mit MVVM/PropertyChanged.

Aber daraus kann ich ja nur lernen. An Unterstriche im Namen werde ich mich vermutlich nie gewöhnen können. Ich bin eher ein Freund von camel/PascalCase.
Deine Verbesserungen passen alles in allem, es läuft wunderbar! Allerdings frage ich mich, worauf der Command self.ok in der Klasse KnotenFenster beim berechnen-Button verweisen soll? Also es funktioniert interessanterweise - aber ich nehme an, es soll der command self.apply sein?! Oder war das gewollt? (nicht sarkastisch, ich würde denken, es ist ein Fehler aber dennoch läuft es korrekt.. sonderbar)

Grüße Tobias
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@tobi45f: `ok()` ist in der `Dialog`-Klasse definiert und die Methode ruft unter anderem `apply()` auf. Vorher auch noch `validate()` und zum Schluss `cancel()`, damit sich das Fenster dann auch schliesst/zerstört wird, und die Kontrolle vom Dialog wieder auf das Elternfenster davon übergeht.

Das schöne an Programmiersprachen bei denen man normalerweise den Quelltext weitergibt, ist ja, dass man da auch in sehr vielen Fällen einfach reinschauen kann was die Standardbibliothek macht:

Code: Alles auswählen

In [252]: tkinter.simpledialog.Dialog.ok??                                      
Signature: tkinter.simpledialog.Dialog.ok(self, event=None)
Docstring: <no docstring>
Source:   
    def ok(self, event=None):

        if not self.validate():
            self.initial_focus.focus_set() # put focus back
            return

        self.withdraw()
        self.update_idletasks()

        try:
            self.apply()
        finally:
            self.cancel()
File:      /usr/lib/python3.6/tkinter/simpledialog.py
Type:      function
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten