Seite 1 von 1

Trennen Logik/GUI -> wie aufrufen?

Verfasst: Montag 27. Januar 2020, 15:00
von Quendolyn
Hallo geschätzte Forum-User,

Ich arbeite daran in einer kleinen tkinter GUI einen Kalender darzustellen, in dem man die Monate über Buttons durch klicken kann.
Nach anfänglichem Durcheinander habe ich dabei auf klassenoriente Programmierung gewechselt und versucht, die Logik von der GUI zu trennen.
Dabei habe ich nun ein Problem, da ich von der GUI aus nicht mehr meine Funktionen der Klasse 'darüber' aufrufen kann.

(File "...", line 43, in init_widgets
self.month_heading = Label(self.calendar_month_frame, text=GUI_Handler.get_text_for_month_heading())
NameError: name 'GUI_Handler' is not defined)

Ich habe den stark gekürzten Code unten dazukopiert, damit man (hoffentlich) mein Problem verstehen kann.
Wie sieht der richtige Aufruf der Funktion nun aus, wenn ich dabei bleiben möchte die Logik aus der GUI raus zu halten?
(In der Praxis halte ich die Klassen jeweils in eigenen Files, falls das irgendetwas ändert, was ich nicht glaube.)

Code: Alles auswählen

from tkinter import *
from datetime import date #eigentlich in timehandler

class Time_Handler(object):
    MONTHS = {1:{'Name':'Jänner','Days':31}, 2: {'Name':'Februar', 'Days': 28}, 3: {'Name':'März', 'Days':31}, 4: {'Name':'April', 'Days': 30}, 5: {'Name':'Mai','Days': 31}, 6: {'Name':'Juni', 'Days': 30},
                          7: {'Name':'Juli','Days':31},8:{'Name':'August','Days':31}, 9: {'Name':'September','Days':30}, 10:{'Name':'Oktober','Days': 31}, 11:{'Name':'November','Days': 30}, 12:{'Name':'Dezember','Days': 31}}
    
    def __init__(self):
        self.month = date.today().month
        self.year = date.today().year

        self.shown_month=self.month
        self.shown_year=self.year
        
    def next_month(self):
        self.shown_month +=1
        if self.shown_month==13:
            self.shown_month=1
            self.shown_year +=1

    def former_month(self):
        self.shown_month -=1
        if self.shown_month ==0:
            self.shown_month=12
            self.shown_year -=1
    
class GUI_Handler():
    def __init__(self):
        time_handling = Time_Handler()
        root = Tk()
        app = Input_Window(root)
        root.mainloop()

    def next_month(self):
        time.next_month()
        app.month_heading['text']=time.get_text_for_month_heading()

    def former_month(self):
        time.former_month()
        app.month_heading['text']=time_handling.get_text_for_month_heading()

    def get_text_for_month_heading():
        return Time_Handler.MONTHS[time_handling.shown_month]['Name'] + ' ' + str(time_handling.shown_year)        
        
class Input_Window(Frame):
    def __init__(self, parent):
        super(Input_Window, self).__init__(parent)
        self.parent = parent
        
        self.active=False
        self.grid(row=0,column=0)
        self.init_widgets()

    def quit(self):
        sys.exit()                
            
    def init_widgets(self):
        self.calendar_month_frame=Frame(self)
        self.calendar_month_frame.grid(column=0,row=0) 
        
        self.month_heading = Label(self.calendar_month_frame, text=GUI_Handler.get_text_for_month_heading())
        self.button_former_month = Button(self.calendar_month_frame,text='<',command=GUI_Handler.former_month)
        self.button_next_month = Button(self.calendar_month_frame,text='>',command=GUI_Handler.next_month)

        self.button_former_month.grid(row=0,column=0)
        self.month_heading.grid(row=0,column=1)
        self.button_next_month.grid(row=0,column=2)

x=GUI_Handler()        


Liebe und hoffnungsvolle Grüße

Re: Trennen Logik/GUI -> wie aufrufen?

Verfasst: Montag 27. Januar 2020, 16:08
von __deets__
Die commands sind falsch - du übergibst da ja Klassenmethoden. Nicht eine „bound method“ wie du sie mir self.meine_methode bekämst. Mit der entkopplung von GUI und Logik hat das ja erstmal nichts zu tun. Der Methode fehlt dann auch das self.

Und wenn das nicht nur ein Spielbeispiel sein soll, solltest du die ganze Datumsrechnerei mit dem datetime Modul machen.

Des Weiteren solltest du das Modell-Objekt nicht in der GUI instanziieren. Sondern als Argument im Konstruktor übergeben. Das nennt sich etwas hochtrabend „dependency injection“ und erlaubt, die GUI zb auch mit einem Mock zu instantiieren. Oder das Modell woanders zu nutzen und vorzubereiten/damit zu arbeiten, bevor es in der GUI verwandt wird.

Re: Trennen Logik/GUI -> wie aufrufen?

Verfasst: Montag 27. Januar 2020, 16:17
von Sirius3
*-Importe sind böse, weil sie viele Namen unkontrolliert in den eigenen Namensraum kippen und damit nicht mehr klar ist, woher was kommt.
__init__ ist dazu da eine Instanz zu initialisieren, nicht dass sie ewig läuft. Das mainloop gehört da nicht rein.
Zum Rechnen mit Datum gibt es datetime.date, das Du ja auch schon importierst. Das brauchst Du in Time_Handler nicht nachprogrammieren. Für allgemeinere Kalender-Aufgaben gibt es das Modul calendar (z.B. wieviele Tage ein Monat hat, sogar mit Berücksichtigung von Schaltjahren).
Den Nutzen der Klasse `GUI_Handler` sehe ich gerade nicht. Das ist auch keine Klasse, da `self` fehlt, bzw. nicht benutzt wird. Da fliegt Dir das Programm mit NameErrors um die Ohren.

Das was in `Input_Window.init_widgets` steht, kann genausogut in __init__ stehen. Und dass Du da Methoden von GUI_Handler verwendest, ist ein Fehler, aber wie schon gesagt, den Sinn von GUI_Handler sehe ich nicht.
`sys.exit` hat in einem sauberen Programm nichts verloren.

x ist dann ein schlechter Name für ein GUI_Handler und dass mit der Instanz nichts gemacht wird, zeigt auch wieder den Design-Fehler dieser Klasse.

Re: Trennen Logik/GUI -> wie aufrufen?

Verfasst: Montag 27. Januar 2020, 16:58
von __blackjack__
@Quendolyn: Um auf Methoden zugerifen zu können brauchst Du nicht die Klasse sondern ein Exemplar was mit dieser Klasse erstellt wurde. Du versuchst in einigen Methoden auf ein `time` zuzugreifen das nirgends definiert wurde und in anderen auf ein `time_handling` das nur lokal in `GUI_Handler.__init__()` definiert ist, also ausserhalb der Funktion nicht sichtbar ist. Bei den anderen Klassen machst Du es doch in der `__init__()` richtig und bindest Dinge auf die von anderen Methoden aus zugegriffen werden soll an das Objekt selbst. Eben damit man über das Objekt in den Methoden darauf zugreifen kann.

Sternchen-Importe sind Böse™. Damit holt man sich bei `tkinter` hunderte von Namen ins Modul von denen nur ein kleiner Bruchteil verwendet wird. Und nicht nur Namen die in `tkinter` definiert werden sondern auch welche die dieses Modul seinerseits von woanders importiert. Das macht es am Ende sehr undurchsichtig welcher Namen woher kommt.

Handler ist hier überall ein komischer Namenszusatz der keinen Erkenntnisgewinn bringt. Die `Time_Handler`-Klasse hat auch gar nichts mit Zeit zu tun, sondern speichert einen Monat als Zustand. Beziehungsweise zwei. Wobei mir nicht klar ist warum zwei, weil auf `month` und `year` nirgends zugegriffen wird. Und `shown_month` und `shown_year` sollten IMHO das `shown_*` nicht enthalten, weil was damit gemacht wird — in einer GUI angezeigt, ausgedruckt, … ist nicht wirklich der Aufgabenbereich von dieser Klasse.

Das mit `MONTHS` ist auch nicht gut gelöst. Wörterbücher die immer die gleichen Schlüssel haben sind keine Wörterbücher sondern Objekte. Also ein eigener Typ, beispielsweise mit `collections.namedtupel`. Allerdings ist die Anzahl der Tage pro Monat für den Februar nicht fest, es gibt ja Schaltjahre. Und `datetime.datetime` kann damit letztlich schon umgehen — wenn man vom 1. März dieses Jahres einen Tag abzieht, bekommt man korrekt den 29. Februar und nicht den 28.:

Code: Alles auswählen

In [115]: datetime.date(2020, 3, 1) - datetime.timedelta(days=1)                
Out[115]: datetime.date(2020, 2, 29)
Was man leicht berechnen kann, sollte man nicht speichern. Bleiben also nur die Monatsnamen übrig. Die man auch aus `date`-Objekten per Zeichenkettenformatierung bekommen kann. Sogar nicht nur für Österreich, sondern international für die im System eingestellte Region wenn man das `locale`-Modul verwendet.

Den gleichen Zeitpunkt zweimal zu erzeugen (`today()`) und dann jeweils nur einen Teil davon zu verwenden ist ein Fehler, weil es dann passieren kann, das zwei Werte bekommen kann, die nicht mehr zusammengehören, weil zwischen den beiden Aufrufen ja Zeit vergeht.

Ich würde aus dem Monatsobjekt was `Time_Handling` eigentlich ist, auch einen Werttyp machen, also einen der nicht verändert werden sollte, sondern wo man immer neue Exemplare erstellt. Wie bei Objekten aus `datetime`.

Das `parent`-Attribut ist redundant weil sich da die Widget-Klassen schon selbst kümmern und das übergeordnete Objekt unter dem Attribut `master` zur Verfügung stellen. Du verwendest da auch überhaupt nicht. `active` wird auch nirgends verwendet.

Widgets layouten sich nicht selbst, das macht der Aufrufer.

Die `init_widgets()`-Methode macht keinen Sinn. Das gehört in die `__init__()`.

Die ”Trennung” von `Input_Window` und `GUI_Handler` macht keinen Sinn weil die sich gegenseitig brauchen. Keines von beiden ist durch eine andere Klasse austauschbar, damit sind die so eng gekoppelt das sie in einer Klasse gehören. Aus `Input_Window` wird auch gar nicht versucht auf ein `GUI_Handler`-Objekt zuzugreifen sondern es wird versucht die Methoden auf der Klasse aufzurufen. Was falsch ist, weil es keine Klassenmethoden sind.

Die `quit()`-Methode wird nirgends aufgerufen. Wenn sie es würde, würde sie nicht funktionieren weil `sys` nicht definiert ist. Und `sys.exit()` wäre hier auch viel zu hart, weil man damit den Prozess abwürgt, statt mit der `quit()`-Methode auf Widgets die GUI-Hauptschleife zu verlassen.

`__init__()` initialisiert ein Objekt in einen benutzbaren Zustand und kehrt dann zum Aufrufer zurück damit der etwas mit dem initialisierten Objekt machen kann. In der `__init__()` sollte also keine GUI-Hauptschleife laufen bis das gesamte Programm beendet ist.

Man braucht nicht explizit von `object` zu erben und `super()` braucht keine Argumente.

Nahezu ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
from datetime import date as Date, timedelta as TimeDelta
import locale
import tkinter as tk


class Month:
    def __init__(self, year, month):
        self._date = Date(year, month, 1)

    @property
    def year(self):
        return self._date.year
    
    @property
    def month(self):
        return self._date.month

    @property
    def name(self):
        return format(self._date, "%B")
    
    @property
    def days(self):
        return (self.next() - TimeDelta(days=1)).day
    
    def next(self):
        return self.from_date(self._date + TimeDelta(days=32))

    def previous(self):
        return self.from_date(self._date - TimeDelta(days=1))

    @classmethod
    def from_date(cls, date=None):
        if date is None:
            date = Date.today()
        return cls(date.year, date.month)


class InputWindow(tk.Frame):
    def __init__(self, parent, month):
        super().__init__(parent)
        self.month = month

        self.calendar_month_frame = tk.Frame(self)
        self.calendar_month_frame.grid(column=0, row=0)

        self.month_heading = tk.Label(self.calendar_month_frame)
        self.button_former_month = tk.Button(
            self.calendar_month_frame,
            text='<',
            command=self.goto_previous_month,
        )
        self.button_next_month = tk.Button(
            self.calendar_month_frame, text='>', command=self.goto_next_month
        )

        self.button_former_month.grid(row=0, column=0)
        self.month_heading.grid(row=0, column=1)
        self.button_next_month.grid(row=0, column=2)

        self.update_display()

    def update_display(self):
        self.month_heading["text"] = f"{self.month.name} {self.month.year}"
    
    def goto_next_month(self):
        self.month = self.month.next()
        self.update_display()
    
    def goto_previous_month(self):
        self.month = self.month.previous()
        self.update_display()


def main():
    locale.setlocale(locale.LC_ALL, "")
    root = tk.Tk()
    window = InputWindow(root, Month.from_date())
    window.grid(row=0, column=0)
    root.mainloop()


if __name__ == "__main__":
    main()
Wobei man vielleicht erst in das `calendar`-Modul aus der Standardbibliothek schauen sollte, bevor man hier das Rad neu erfindet.