Methodenübergabe bei GUI-Klassen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
NPC
User
Beiträge: 11
Registriert: Dienstag 8. Januar 2019, 17:51

Dienstag 8. Januar 2019, 19:38

Hallo Forum,

Das ist meine erste Frage in einem Forum. Wenn ich mich unklar (oder ungeschickt ausdrücke) gebt mir bitte bescheid :).
Ich hoffe ich habe nicht einfach eine mögliche Lösung in einem der anderen Themen übersehen.
Hoffentlich wirkt die Frage auch nicht dumm, wenn man sich besser mit der Funktionsweise von Python auskennt.

Zu meinem Problem:

Ich habe versucht mir in Python und tkinter eine art Arbeitsmappe zu schreiben. Dabei ist jede Seite ein Canvas. Ein kleines "Menü" ermöglicht mir das "blättern" druch diese. Das ganze wird aus der Klasse PageMap (erbt von Frame) heraus "verwaltet". Ich merke mir die aktuell gezeigte Seite durch self.pages[self.current]. Wenn zu einer Seite gewechselt wird, wird das aktuelle Canvas mit pack_forget() aus dem Frame pack entfernt und die neue Seite mit pack eingefügt.

Ist es möglich, dass ich die Methoden direkt an PageMap auffrufe und diese an self.pages[self.current] weiter gegeben werden ohne für jede methode von Canvas eine übergabe Methode bei PageMap zu schreiben. Oder wie sähe ein schönerer Weg aus?

Das soll erstmal ein funktionierender Prototyp werden. Wenn es klappt versuche ich nach bestem Gewissen den Code zu verbessern.
Bitte teilt mir auch unschöne Codestellen mit (sonst lerne ichs ja nicht ;)).

Code: Alles auswählen

class CanMenu(Canvas):

    x = 0
    active = 0

    def __init__(self, parent, height, *args, **kwargs):
        Canvas.__init__(self, parent, height=height, *args, **kwargs)
        self.height = height
        self.parent = parent
        self.add_but()

    def add(self, number, name=""):
        self.delete("+")

        if name == "":
            name = "Page " + str(number)
        width = max(len(name) * 10 + 4, 30)

        rec_name = "rec_" + str(number)
        txt_name = "txt_" + str(number)

        self.create_rectangle(self.x, 0, self.x + width, self.height + 2, outline="#000000", tag=rec_name)
        self.create_text(self.x + width//2, self.height//2 + 1, text=name, tag=txt_name)
        self.tag_bind(rec_name, "<1>", lambda event: self.parent.open_page(number))
        self.tag_bind(txt_name, "<1>", lambda event: self.parent.open_page(number))

        self.x += width
        self.activate(number)
        self.add_but()

    def activate(self, number):
        new_tag = "rec_" + str(number)
        old_tag = "rec_" + str(self.active)

        self.itemconfig(old_tag, fill="#FFFFFF")
        self.itemconfig(new_tag, fill="#0040FF")
        self.active = number
        self.update()

    def add_but(self):
        self.create_rectangle(self.x, 0, self.x + self.height + 2, self.height + 2, fill="#FFBF00", tag="+")
        self.create_text(self.x + (self.height+2)//2, self.height//2 + 1, tag="+", text="+")
        self.tag_bind("+", "<1>", lambda event: self.parent.add_page())

Code: Alles auswählen

class PageMap(Frame):

    current = 0
    pages = []

    def __init__(self, parent, *args, **kwargs):
        Frame.__init__(self, parent, *args, **kwargs)
        self.menu = CanMenu(self, bg="#FFFFFF", height=16)
        self.menu.pack(fill=X)

    def add_page(self, name=""):
        if self.pages != []:
            self.pages[self.current].pack_forget()
        self.current = len(self.pages)
        self.pages.append(Canvas(self, bg="#FFFFFF"))
        self.pages[self.current].pack(fill=BOTH)

        self.menu.add(self.current, name)

    def open_page(self, x):
        if x < len(self.pages):
            self.pages[self.current].pack_forget()
            self.current = x
            self.pages[self.current].pack(fill=BOTH)
            self.menu.activate(self.current)

    def page_number(self):
        return self.current

    def page(self):
        return self.pages[self.current]

    def drop_page(self, number):
        if len(self.pages) == 1:
            self.add_page()
        elif number == self.current:
            to_open = (number + 1) % (len(self.pages)-1)
            self.open_page(to_open)
        self.pages.remove(number)
zum kleinen Testen:

Code: Alles auswählen

 if __name__ == "__main__":
    def add():
        page = map.page()
        page.create_rectangle(0, 100, 100, 0, fill="#FF0000")
        # hier würde mit funktionierender Lösung "map.create_rectangle(...)" statt die beiden oberen Zeilen stehen 

    tk = Tk()
    tk.geometry("500x500")
    map = PageMap(tk)
    map.place(relx=0, rely=0, relwidth=1, relheight=0.8)
    map.add_page()
    but = Button(tk, text="Markierung", command=add)
    but.place(relx=0, rely=0.9, relwidth=1)
    tk.mainloop()
Danke schonmal im Voraus :)
Benutzeravatar
__blackjack__
User
Beiträge: 3350
Registriert: Samstag 2. Juni 2018, 10:21

Mittwoch 9. Januar 2019, 11:46

@NPC: Vorbemerkung: Du verwendest Klassenattribute falsch. Auf Klassen gehören keine Variablen, denn das ist globaler Zustand. Alles was keine Konstante ist, sollte in der jeweiligen `__init__()` eingeführt werden, und Konstanten schreibt man per Konvention KOMPLETT_GROSS.

Das Hauptprogramm sollte auch ein einer Funktion stehen, ebenfalls um globale Variablen zu vermeiden, die üblicherweise `main()` heisst.

Abkürzungen bei Namen vermeiden. Also `button` statt `but`.

`map` ist der Name einer eingebauten Funktion, den sollte man nicht an etwas anderes binden.

Falls ganz oben eine Sternchen-Import aus `tkinter` steht: das ist Böse™. Damit holt man sich fast 200 Namen in das Modul von denen nur ein Bruchteil tatsächlich benötigt wird. Es ist schwerer nachzuvollziehen wo was her kommt, und es besteht die Gefahr von Namenskollisionen.

Zur Frage: Man würde auf `PageMap` die entsprechenden Methoden implementieren, die dann den jeweiligen Aufruf auf dem `menu`-Attribut machen. Wenn Dir das zu viel Schreibarbeit ist, wäre zu überlegen warum Du das überhaupt so machen willst.
“I am Dyslexic of Borg, Your Ass will be Laminated” -- unknown
NPC
User
Beiträge: 11
Registriert: Dienstag 8. Januar 2019, 17:51

Mittwoch 9. Januar 2019, 12:17

Hey,

erstmal danke __blackjack__.
Das kleine Ding zum Testen benutze ich nur während des schreibens
(Die Klasse ist eine von mehreren GUI-Klassen für ein anders Programm und ich will nicht immer alles ausführen müssen um zu sehen obs klappt :)).
Keine Sorge ich benutze nicht from tkinter import * sondern habe alles mit from tkinter import Button, ... eingegeben (ist das besser oder immernoch böse?).

Zu den Klassenattributen:
Kannst du mir genauer erklären was du mit "Auf Klassen gehören keine Variablen, denn das ist globaler Zustand" meinst. Heißt das in die Klasse kommen nur Konstanten?
Wenn die Attribute in der __init__ nicht verwendet werden, warum darf ich sie dann nicht schon darüber schreiben? (also warum ist das Falsch/Unschön?)
Konstanten habe ich (meines Wissens) in dem Programm erstmal keine oder bezieht sich das auf attribute wie self.name?

Zur Frage:
Hatte kurz überlegt das so zu machen. War mir dann aber zu viel aufwand. Weist du ob ich vor den Methodenaufruf generell einen Dekorator stellen kann? Damit könnte ich (wahrscheinlich zwar etwas unsauber) den Aufruf weiterleiten oder ist das eher eine schlechte idee bzw. unmöglich?

Ich hatte gehofft das so machen zu können, dass ich PageMap dann wie ein Canvas benutzen kann.
Benutzeravatar
__blackjack__
User
Beiträge: 3350
Registriert: Samstag 2. Juni 2018, 10:21

Mittwoch 9. Januar 2019, 12:53

@NPC: Genau, auf Klassenebene sollten nur Konstanten stehen. Nichtveränderliche Werte die erst bei ”Bedarf” auf das Exemplar kopiert werden sind zumindest mal verwirrend und veränderliche Werte wie `PageMap.pages` sind tatsächlich falsch, weil diese Liste für alle Exemplare von `PageMap` gilt.

Klar kann man Methoden dekorieren, ich sehe aber nicht was das bringen soll. Du könntest einen Dekorator für die Klasse schreiben, der dann beispielsweise ein Klassenattribut mit einer Sequenz von Methodennamen abfragt und daraus dann entsprechende weiterleitende Methoden erstellt. Ist halt ein bisschen ”magic”.

Edit: Oder Du implementierst `__getattr__()`.
“I am Dyslexic of Borg, Your Ass will be Laminated” -- unknown
NPC
User
Beiträge: 11
Registriert: Dienstag 8. Januar 2019, 17:51

Mittwoch 9. Januar 2019, 13:33

Ah, danke jetzt weis ich was du meinst. Ja das werde ich ändern.

Das ist der Plan, den ich mit den Dekoratoren auch hatte. Meine Frage zielte eher darauf ab ob es eine Möglich keit gibt, mit nur einem Befehl, den Dekorator vor alle Methoden zu schreiben.

Danke für die schnellen Antworten
Sirius3
User
Beiträge: 9887
Registriert: Sonntag 21. Oktober 2012, 17:20

Mittwoch 9. Januar 2019, 13:59

Unschön ist es, da es Frame-Methoden und Canvas-Methoden gibt, die gleich heißen, und dann nicht klar ist, für was denn das nun gilt.
Wenn page ein Property wäre, was wäre so schlimm, wenn man `pagemap.page.create_oval` schreiben müßte, statt `pagemap.create_oval`? Für mich ist die erste Variante klarer.
NPC
User
Beiträge: 11
Registriert: Dienstag 8. Januar 2019, 17:51

Mittwoch 9. Januar 2019, 14:34

Hey,
Ich habs jetzt mittels `__getattr__()` hinbekommen :).
@__blackjack__ danke dafür :)

@Sirius3
Da hast du recht. Ich dachte nur, dass es vielleicht intuitiver wäre, wenn man es dann wie ein Canvas benutze kann.
Aber danke für den Hinweis :).

Die Lösung mit __getattr__ die ich jetzt geschrieben habe wäre:

Code: Alles auswählen

    def __getattr__(self, item):
        if item in dir(self.pages[self.current]):
            return getattr(self.pages[self.current], item)
        else:
            raise SyntaxError("Method '{}' is not defined".format(item))
Sirius3
User
Beiträge: 9887
Registriert: Sonntag 21. Oktober 2012, 17:20

Mittwoch 9. Januar 2019, 15:16

Statt des if`s einfach den AttributeError durchfallen lassen, den getattr wirft. Es ist kein SyntaxError!
Benutzeravatar
__blackjack__
User
Beiträge: 3350
Registriert: Samstag 2. Juni 2018, 10:21

Mittwoch 9. Januar 2019, 15:19

@NPC: Das ist viel zu kompliziert und auch ganz sicher kein `SyntaxError`. Lass den Test einfach weg. `getattr()` löst genau die richtige Ausnahme doch schon selbst aus wenn es das Attribut nicht gibt. Zudem müssen in `dir()` nicht alle Attribute enthalten sein. Zum Beispiel solche nicht die erst mit `__getattr__()` dynamisch erzeugt werden.
“I am Dyslexic of Borg, Your Ass will be Laminated” -- unknown
NPC
User
Beiträge: 11
Registriert: Dienstag 8. Januar 2019, 17:51

Mittwoch 9. Januar 2019, 16:34

Ok ich hab das mit dem SyntaxError jetzt raus genommen.
Da es wirklich nicht viel komplizierter ist das aktuelle Canvas auszulesen, werde ich wahrscheinlich auf diese Variante wechseln.
Danke an euch beide für die Antworten & die Hinweise :)
Antworten