Mehrere Frames Logik von GUI trennen

Fragen zu Tkinter.
Antworten
Benutzeravatar
Dennis89
User
Beiträge: 1173
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

und schon wieder benötige ich euere Hilfe.
Dieses mal habe ich mir überlegt wie ich ein Programm angehen würde, wenn ich mit Buttons zwischen verschiedenen Seiten wechseln will. Dabei soll man auf den einzelnen Seiten Werte eingeben oder Radiobuttons auswählen. Mit diesen Informationen soll gerechnet werden und auf der letzten Seite soll das Ergebnis erscheinen.

Das wechseln der Seiten funktioniert sehr gut, dank dieser Vorlage:
https://stackoverflow.com/questions/754 ... in-tkinter

Daraus ist dann mal dieser Code entstanden:

Code: Alles auswählen

import tkinter as tk
from functools import partial


class CalculationApp(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.result = None
        self.frames = {}
        for page in (Home, Entry, Result):
            page_name = page.__name__
            frame = page(parent=self, controller=self)
            frame.grid(row=0, column=0, sticky="nsew")
            self.frames[page_name] = frame
        self.show_page("Home")

    def show_page(self, page_name):
        self.frames[page_name].tkraise()

    def get_page(self, classname):
        for page in self.frames.values():
            if str(page.__class__.__name__) == classname:
                return page
        return None


class Home(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        headline = tk.Label(self, text="Hier gehts zur Berechnung")
        headline.grid(row=0, column=1)
        go_to_entry_page = tk.Button(
            self, text=">>", command=partial(self.controller.show_page, "Entry")
        )
        go_to_entry_page.grid(row=1, column=3)


class Entry(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.result = None
        values_for_calculation = [10, 30]
        description = tk.Label(self, text="Bitte Werte auswählen")
        description.grid(row=0, column=1)
        self.selected_button = tk.StringVar()
        for row_index, value in enumerate(values_for_calculation, 1):
            radio_button = tk.Radiobutton(
                self, text=value, value=value, variable=self.selected_button
            )
            radio_button.grid(row=row_index, column=0)
        tk.Button(
            self, text="Berechne", command=self.calculate_something
        ).grid(row=3, column=3)
        tk.Button(self, text="<<", command=partial(self.controller.show_page, "Home")).grid(
            row=3, column=0
        )

    def calculate_something(self):
        self.result = int(self.selected_button.get()) / 2
        self.controller.show_page('Result')


class Result(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.show_result = tk.Label(self, text='')
        self.show_result.grid(row=0, column=1)
        button = tk.Button(
            self, text="Startseite", command=partial(controller.show_page, "Home")
        )
        button.grid(row=1, column=1)
        self.update_label()

    def update_label(self):
        self.show_result.config(text=self.controller.get_page('Entry').result)


def main():
    root = tk.Tk()
    root.title("Berechnungsprogramm")
    app = CalculationApp(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Mein Problem ist, dass in der Klasse 'Entry' nach dem Aufrf von 'calculate_something' der Name 'self.result' nicht an den berechneten Wert gebunden wird, sondern weiterhin 'None' ist, wie in der __init__ angegeben. Was übersehe ich da?

Dann habe ich noch ein Problem, es gefällt mir gar nicht, dass ich die Berechnung in einer Klasse mache, die eigentlich die grafische Oberfläche repräsentiert. Zu dem gefällt mir auch nicht, das ich in einer Funktion, die "calculate" im Name hat eine andere grafische Oberfläche aufrufe.
Wie würde man den so ein Programm richtig strukturieren?
Es kann ja auch gut sein, dass man zwischen den Seitenwechsel umfangreichere Berechnungen vornehmen muss und eventuell Daten aus Datenbanken abrufen. Das passt meiner Meinung nach nicht in eine von meinen Klassen. Das wäre ja das trenne nvon Grafik und Logik, wie man es immer wieder liest. Jetzt ist der Tag bald rum und bevor ich mich mal wieder komplett verrenne bin ich auf eure Antworten sehr gespannt.

Vielen Dank schon einmal!

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17768
Registriert: Sonntag 21. Oktober 2012, 17:20

Warum glaubst Du, dass `result` `None` ist? Wie prüfst Du das? Du machst ja mit dem Ergebnis im gesamten Programm nichts.

Wenn Du Logik und GUI trennen willst, mußt Du eine Logik-Klasse implementieren, davon eine Instanz erzeugen, und diese an alle Frames übergeben, die dann damit arbeiten können.
Benutzeravatar
Dennis89
User
Beiträge: 1173
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

danke für deine Antwort.
Ich greife in der Klasse 'Result' mit der Funktion 'update_label' auf 'result zu. (Zumindest denke ich, dass ich das tue.)

Ach mist, aber gerade merke ich, dass 'update_label' ja nicht erst aufgerufen wird, wenn ich auf die Seite wechsle, sondern schon ganz am Anfang, wenn die die Frames instanziert werden.
So gehts:

Code: Alles auswählen

import tkinter as tk
from functools import partial


class CalculationApp(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.result = None
        self.frames = {}
        for page in (Home, Entry, Result):
            page_name = page.__name__
            frame = page(parent=self, controller=self)
            frame.grid(row=0, column=0, sticky="nsew")
            self.frames[page_name] = frame
        self.show_page("Home")

    def show_page(self, page_name):
        self.frames[page_name].tkraise()

    def get_page(self, classname):
        for page in self.frames.values():
            if str(page.__class__.__name__) == classname:
                return page
        return None


class Home(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        headline = tk.Label(self, text="Hier gehts zur Berechnung")
        headline.grid(row=0, column=1)
        go_to_entry_page = tk.Button(
            self, text=">>", command=partial(self.controller.show_page, "Entry")
        )
        go_to_entry_page.grid(row=1, column=3)


class Entry(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        values_for_calculation = [10, 30]
        description = tk.Label(self, text="Bitte Werte auswählen")
        description.grid(row=0, column=1)
        self.selected_button = tk.StringVar()
        for row_index, value in enumerate(values_for_calculation, 1):
            radio_button = tk.Radiobutton(
                self, text=value, value=value, variable=self.selected_button
            )
            radio_button.grid(row=row_index, column=0)
        tk.Button(
            self, text="Berechne", command=self.calculate_something
        ).grid(row=3, column=3)
        tk.Button(self, text="<<", command=partial(self.controller.show_page, "Home")).grid(
            row=3, column=0
        )

    def calculate_something(self):
        self.controller.get_page('Result').show_result.config(text=int(self.selected_button.get()) / 2)
        self.controller.show_page('Result')


class Result(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.show_result = tk.Label(self, text='')
        self.show_result.grid(row=0, column=1)
        button = tk.Button(
            self, text="Startseite", command=partial(controller.show_page, "Home")
        )
        button.grid(row=1, column=1)


def main():
    root = tk.Tk()
    root.title("Berechnungsprogramm")
    app = CalculationApp(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Danke für den Hinweis mit der Logik-Klasse, das werde ich dann mal versuchen.

Könnt ihr mir noch sagen, ob ich den Code, so wie er jetzt ist, um die Fenster zu wechseln so lassen kann? Ist da irgendwas drin, das man so nicht macht oder das zu Problemen führen könnte?

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13144
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Die `get_page()`-Methode sieht komisch aus. Warum gehst Du da die Werte des Wörterbuchs durch? Der Klassenname ist doch der *Schlüssel* über den man *direkt* auf die Seite zugreifen kann:

Code: Alles auswählen

    def get_page(self, page_name):
        return self.frames[page_name]
Hat zudem den Vorteil, dass man nicht auf einen speziellen Rückgabewert prüfen muss wenn die Seite nicht existiert — es wird dann eine Ausnahme ausgelöst.

Was mir an dem Entwurf nicht so gefällt ist das die Seiten über die Klassen identifiziert werden und das die Seiten ihre ”Kollegen” kennen müssen. Das mit den Klassen(namen) als Schlüsseln macht es unflexibel und so etwas wie eine Seite weiter/zurück ist üblicherweise Aufgabe der Klasse die die Seiten verwaltet. Ich würde mich da an Rahmenwerken orientieren, die so etwas schon fertig anbieten. Sowohl Gtk als auch Qt haben dafür schon was dessen API man sich anschauen kann.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Benutzeravatar
Dennis89
User
Beiträge: 1173
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo und auch dir danke für deine Antwort.

Deine Anmerkungen werde ich mir morgen vornehmen, für heute mache ich Schluss 😴

Ich habe aber gerade noch das mit der Logikklasse versucht und dabei ist jetzt folgender Code entstanden:

Code: Alles auswählen

import tkinter as tk
from functools import partial


class CalculationApp(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        calculator = Calculator()
        self.frames = {}
        self.home_page = Home(self, self)
        self.home_page.grid(row=0, column=0, sticky="nsew")
        self.frames['Home'] = self.home_page
        self.entry_page = Entry(self, self, calculator)
        self.entry_page.grid(row=0, column=0, sticky="nsew")
        self.frames['Entry'] = self.entry_page
        self.result_page = Result(self, self, calculator)
        self.result_page.grid(row=0, column=0, sticky="nsew")
        self.frames['Result'] = self.result_page
        self.show_page("Home")

    def show_page(self, page_name):
        self.frames[page_name].tkraise()

    def get_page(self, classname):
        for page in self.frames.values():
            if str(page.__class__.__name__) == classname:
                return page
        return None


class Home(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        headline = tk.Label(self, text="Hier gehts zur Berechnung")
        headline.grid(row=0, column=1)
        go_to_entry_page = tk.Button(
            self, text=">>", command=partial(self.controller.show_page, "Entry")
        )
        go_to_entry_page.grid(row=1, column=3)


class Entry(tk.Frame):
    def __init__(self, parent, controller, calculator):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.calculator = calculator
        values_for_calculation = [10, 30]
        description = tk.Label(self, text="Bitte Werte auswählen")
        description.grid(row=0, column=1)
        self.selected_button = tk.StringVar()
        for row_index, value in enumerate(values_for_calculation, 1):
            radio_button = tk.Radiobutton(
                self, text=value, value=value, variable=self.selected_button
            )
            radio_button.grid(row=row_index, column=0)
        tk.Button(
            self, text="Berechne", command=self.start_calculation
        ).grid(row=3, column=3)
        tk.Button(self, text="<<", command=partial(self.controller.show_page, "Home")).grid(
            row=3, column=0
        )

    def start_calculation(self):
        self.calculator.calculate(int(self.selected_button.get()))
        self.controller.get_page('Result').show_result.config(text=self.calculator.result)
        self.controller.show_page('Result')



class Result(tk.Frame):
    def __init__(self, parent, controller, calculator):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.calculator = calculator
        self.show_result = tk.Label(self, text='')
        self.show_result.grid(row=0, column=1)
        button = tk.Button(
            self, text="Startseite", command=partial(controller.show_page, "Home")
        )
        button.grid(row=1, column=1)


class Calculator:
    def __init__(self):
        self.result = None
        self.database = [2, 3, 5, 6]

    def calculate(self, user_choice):
        self.result = sum(number * user_choice for number in self.database)


def main():
    root = tk.Tk()
    root.title("Berechnungsprogramm")
    app = CalculationApp(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Das ich eine Funktion wie 'start_calculation' (vielleicht ein komischer Name) brauche, in der ich dann die Berechnung starte und das nächste Fenster anzeige, darum komme ich wohl nicht drum rum? Es sei denn, ich führe etwas aus, wenn ein Radiobutton angeklickt wurde, oder ein Eingabefeld befüllt wurde, so dass der Button für das Fenster wechseln wieder frei wäre.

Naja nur um meine letzten Gedanken für heute noch nieder geschrieben zu haben 😃

Ich kümmere mich dann morgen erst mal um den API-Hinweis.

Grüße und gute Nacht
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13144
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Wenn die Programmlogik eine Klasse ist, dann übergibt man das Objekt in der Regel der GUI und erstellt es nicht dort. Im konkreten Fall sehe ich aber hier keinen Sinn einer Klasse für die Logik wenn es auch eine einfache Funktion tut. `calculate()` kann das Ergebnis doch einfach als Ergebnis zurückgeben, und schon braucht es kein `result`-Attribut mehr. Und damit gibt es keinen Zustand mehr, der dort gekapselt wird und eine Klasse begründen würde. `Result` bekommt das Logik-Objekt übergeben, macht aber gar nichts damit.

Das alle Seiten zweimal `self` als Argument übergeben bekommen ist etwas schräg.

Wenn es im `tkinter`-Modul eine Konstante für einen Wert mit ”besonderer” Bedeutung gibt, sollte man die verwenden, statt den Wert als literale Zeichenkette zu schreiben. Dann weiss der Leser auf den ersten Blick, dass die Zeichenkette nicht willkürlich gewählt werden kann, und kann auch in der Dokumentation nach der Bedeutung und den anderen möglichen Werten schauen.

Die Seiten sollten eher keine Attribute von `CalculationApp` sein. Es wird darüber nicht drauf zugegriffen, denn die sind ja alle auch in `self.frames` eingetragen. Dort könnte man sie auch direkt in ein literales Wörterbuch schreiben, oder vielleicht besser, eine Methode zum hinzufügen schreiben, dann muss man die immer gleiche `grid()`-Zeile nur einmal schreiben.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Benutzeravatar
Dennis89
User
Beiträge: 1173
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,

danke für die weitere Hilfe.
__blackjack__ hat geschrieben: Donnerstag 7. Juli 2022, 00:07 @Dennis89: Wenn die Programmlogik eine Klasse ist, dann übergibt man das Objekt in der Regel der GUI und erstellt es nicht dort. Im konkreten Fall sehe ich aber hier keinen Sinn einer Klasse [..]
Okay, das Objekt wird jetzt in der 'main' erstellt. Hier macht die Klasse keinen Sinn, das stimmt. Ich möchte mir mit dem Code aber eine Art Vorlage schreiben. Wenn ich das mal benötige, dann kann die Logik umfangreicher sein und ich muss mir vielleicht auch etwas "merken", deswegen habe ich die Klasse gleich mit eingebaut.

__blackjack__ hat geschrieben: Donnerstag 7. Juli 2022, 00:07 Das alle Seiten zweimal `self` als Argument übergeben bekommen ist etwas schräg.
Habe ich geändert. Jetzt kann ich allerdings in der 'main' nicht mehr 'app.pack()' aufrufen. Da muss ich ehrlich sein, da fehlt es etwas am Verständnis.
__blackjack__ hat geschrieben: Donnerstag 7. Juli 2022, 00:07 Wenn es im `tkinter`-Modul eine Konstante für einen Wert mit ”besonderer” Bedeutung gibt, sollte man die verwenden, statt den Wert als literale Zeichenkette zu schreiben.
Damit meinst du sicherlich, das ich 'sticky' so angeben soll?
sticky=(tk.N, tk.S, tk.E, tk.W)

Habe ich geändert, allerdings beschwert sich PyCharm, er würde an dieser Stelle einen String erwarten 🤷‍♂️
__blackjack__ hat geschrieben: Donnerstag 7. Juli 2022, 00:07 Die Seiten sollten eher keine Attribute von `CalculationApp` sein. Es wird darüber nicht drauf zugegriffen, denn die sind ja alle auch in `self.frames` eingetragen. Dort könnte man sie auch direkt in ein literales Wörterbuch schreiben, oder vielleicht besser, eine Methode zum hinzufügen schreiben, dann muss man die immer gleiche `grid()`-Zeile nur einmal schreiben.
Das mit den Attributen habe ich geändert. Im ersten Beispiel hatte ich eine 'for'-Schleife, damit habe ich mir die gleichen 'grid'-Zeilen auch gespart. Jetzt ist das so geworden, weil eine Klasse jetzt noch ein zusätzliches Argument bekommt, 'calculator'. Dafür gibt es aber sicher auch eine Lösung, eventuell mit default-Argumenten?
__blackjack__ hat geschrieben: Donnerstag 7. Juli 2022, 00:07 Ich würde mich da an Rahmenwerken orientieren, die so etwas schon fertig anbieten. Sowohl Gtk als auch Qt haben dafür schon was dessen API man sich anschauen kann.
Ich habe mal durch die QT-Doku gestöbert. Bin mir aber nicht so richtig sicher, nach was ich genau suchen sollte. Meintest du ich sollte sowas nachbauen?
https://doc.qt.io/qtforpython/PySide6/Q ... .isVisible

Der Code sieht mit den Änderungen, die ich bis jetzt gemacht habe so aus:

Code: Alles auswählen

import tkinter as tk
from functools import partial


class CalculationApp(tk.Frame):
    def __init__(self, master, calculator):
        tk.Frame.__init__(self, master)
        self.calculator = calculator
        self.frames = {}
        home_page = Home(self)
        home_page.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
        entry_page = Entry(self, calculator)
        entry_page.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
        result_page = Result(self)
        result_page.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
        self.frames['Home'] = home_page
        self.frames['Entry'] = entry_page
        self.frames['Result'] = result_page
        self.show_page("Home")

    def show_page(self, page_name):
        self.frames[page_name].tkraise()

    def get_page(self, page_name):
        return self.frames[page_name]


class Home(tk.Frame):
    def __init__(self, controller):
        tk.Frame.__init__(self)
        self.controller = controller
        headline = tk.Label(self, text="Hier gehts zur Berechnung")
        headline.grid(row=0, column=1)
        go_to_entry_page = tk.Button(
            self, text=">>", command=partial(self.controller.show_page, "Entry")
        )
        go_to_entry_page.grid(row=1, column=3)


class Entry(tk.Frame):
    def __init__(self, controller, calculator):
        tk.Frame.__init__(self)
        self.controller = controller
        self.calculator = calculator
        values_for_calculation = [10, 30]
        description = tk.Label(self, text="Bitte Werte auswählen")
        description.grid(row=0, column=1)
        self.selected_button = tk.StringVar()
        for row_index, value in enumerate(values_for_calculation, 1):
            radio_button = tk.Radiobutton(
                self, text=value, value=value, variable=self.selected_button
            )
            radio_button.grid(row=row_index, column=0)
        tk.Button(
            self, text="Berechne", command=self.start_calculation
        ).grid(row=3, column=3)
        tk.Button(self, text="<<", command=partial(self.controller.show_page, "Home")).grid(
            row=3, column=0
        )

    def start_calculation(self):
        self.calculator.calculate(int(self.selected_button.get()))
        self.controller.get_page('Result').show_result.config(text=self.calculator.result)
        self.controller.show_page('Result')


class Result(tk.Frame):
    def __init__(self, controller):
        tk.Frame.__init__(self)
        self.controller = controller
        self.show_result = tk.Label(self, text='')
        self.show_result.grid(row=0, column=1)
        button = tk.Button(
            self, text="Startseite", command=partial(controller.show_page, "Home")
        )
        button.grid(row=1, column=1)


class Calculator:
    def __init__(self):
        self.result = None
        self.database = [2, 3, 5, 6]

    def calculate(self, user_choice):
        self.result = sum(number * user_choice for number in self.database)


def main():
    calculator = Calculator()
    root = tk.Tk()
    root.title("Berechnungsprogramm")
    app = CalculationApp(root, calculator)
    app.mainloop()


if __name__ == "__main__":
    main()
Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13144
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: `sticky` würde ich als ``sticky=tk.NSEW`` angeben. Weniger Tippartbeit, und PyCharm ist dann auch glücklicher, weil das eine Zeichenkette ist.

Ich meinte keine einzelne Methode auf einem `QWindow` sondern alles was `QWizard`/`QWizardPage` so bieten: https://doc.qt.io/qtforpython/PySide6/Q ... izard.html

Bei Gtk heisst das Konzept und die Klasse um den Benutzer durch mehrere Schritte/Seiten zu leiten `Assistant`.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Benutzeravatar
Dennis89
User
Beiträge: 1173
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Abend,
__blackjack__ hat geschrieben: Donnerstag 7. Juli 2022, 14:26 @Dennis89: `sticky` würde ich als ``sticky=tk.NSEW`` angeben. Weniger Tippartbeit, und PyCharm ist dann auch glücklicher, weil das eine Zeichenkette ist.
perfekt, jetzt bin nicht nur ich, sondern auch PyCharm glücklich 😃
__blackjack__ hat geschrieben: Donnerstag 7. Juli 2022, 14:26 Ich meinte keine einzelne Methode auf einem `QWindow` sondern alles was `QWizard`/`QWizardPage` so bieten: https://doc.qt.io/qtforpython/PySide6/Q ... izard.html
Wenn ich das mal ehrlich betrachte, dann denke ich, dass das eine Nummer zu hoch für mich ist. Ich "weis" jetzt wie ich mit Qt mehrere Seiten erstellen würde bzw. was ich da aufrufen müsste, aber wie ich das auf tkinter übertragen könnte, dazu fällt mir gerade überhaupt nichts ein. Noch nicht mal eine vernünftige Frage zum Einstieg.


Auf jeden Fall vielen Dank, dass ihr mir in kürzester Zeit wieder weiter geholfen habt! 😊


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1173
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

jetzt muss ich doch nochmal nachfragen. Sollte ich dann eine Klasse erstellen, die Methoden wie 'addPage', 'setPage', 'setButton' etc. hat? Und diese nutze ich dann um meine Seiten zu erstellen?

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Antworten