Frame-Handling eines komplexeren GUIs

Fragen zu Tkinter.
Antworten
Flamez
User
Beiträge: 11
Registriert: Sonntag 14. Juni 2015, 18:43

Hallo zusammen,

seit ich an einem relativ umfangreichen GUI arbeite kamen mir einige Fragen, die ich noch nicht zufriedenstellend lösen konnte. Aktuell besitzt mein GUI 6 Frames plus Root Window. Zu beginn, als ich erst 1 oder 2 Frames hatte, habe ich für jeden Frame eine create, show und hide Methode implementiert. Das hat sich für mich als durchaus praktikabel herausgestellt, wodurch ich auch an dieser Vorgehensweise festgehalten habe. Nun allerdings, mit 6 Frames, ist meine GUI-Klasse ziemlich zugemüllt mit Methoden und man scrollt mittlerweile schon wie wild durch die create-Methoden. Meine Intention bei den Klassen war folgende. Will ich zum Beispiel einen Frame verstecken bzw. ausblenden, verwende ich die jeweilige forget-Methode des Frame-Layouts. Da sich die jedoch während der Entwicklung durchaus mal ändert und ich nicht jede Stelle einzeln ändern will, habe ich das ganze einfach in eine separate Methode der GUI-Klasse gepackt. So auch alle anderen Methodenarten.

Die Frage die ich mir dabei stelle ist die, ob das auf dauer zielführend ist? Ich merke schon jetzt, dass es immer umständlicher wird. Wie handelt man am besten so viele Frames und ist es sinnvoll die immer ein und auszublenden? Ich könnte zum Beispiel auch alle hide und show-Methoden zentral zusammenpacken und anhand der Parameter, die ich mitgebe die entsprechende Aktion durchführen. Quasi ein Sammler/Handler, wie auch immer. Noch dazu weiß ich auch gar nicht, wann ich alle erstellen soll. Aktuell erzeuge ich einen Frame beim erstmaligen Aufruf. Also eventgesteuert im Ablauf der GUI. Ich könnte mir auch vorstellen, bei Programmstart alle erzeugen zu lassen und dann einfach ein- oder ausblenden zu lassen.

Ich finde leider keine Beispiele oder Erklärungen, wie man komplexere GUIs organisiert.
BlackJack

@Flamez: Also mir stellt sich ja als erstes mal die Frage warum Du Frames ein- und ausblendest. Ist an sich schon mal eher ungewöhnlich.

Und `create()`-Methoden? Für alle 6 Frames in *einer* Klasse? Wieviele Klassen und Widgets hast Du denn?
Flamez
User
Beiträge: 11
Registriert: Sonntag 14. Juni 2015, 18:43

Das ein- und ausblenden war meine Idee, da ich es nicht besser wusste. :wink: In Beispielen wurde das immer so gemacht. In meinem Fall habe ich einen Login-Frame, dort kann ich User und Passwort eingeben. Nach dem (erfolgreichen) Login wird dann der Login-Frame ausgeblendet und der nächste eingeblendet. Mag sein, dass die Vorgehensweise nicht sinnvoll ist. Wie gesagt, ich wusste es nicht besser. Deswegen auch der Thread hier.
Ich habe eine GUI Klasse die das alles beinhaltet. Solche Dinge wie z. B. einen Parser habe ich in separate Klassen.
BlackJack

@Flamez: Ein-/Ausblenden wird üblicherweise nur bei Assistenten (a.k.a. „Wizards“) gemacht, wenn man den Benutzer Schritt für Schritt durch einen komplexeren Vorgang leiten möchte. Falls der Benutzer beliebig zwischen den ”Seiten” wechseln können darf, würde sich ein `ttk.Notebook` anbieten. Ansonsten ist das übliche vorgehen alle Seiten als Frames übereinander in einer Grid-Zelle zu ”stapeln” und mit der `tkraise()`-Methode jeweils das gewünschte nach ”oben”/”vorne” zu holen, damit es angezeigt wird.

Falls die Erstellung der Frames nicht so teuer ist, dass sich für den Benutzer unzumutbare Wartezeiten ergeben, würde ich alle am Anfang erzeugen. Das ist auch nötig wenn man eine GUI haben möchte, deren Grösse sich am Inhalt des grössten Frames orientiert, damit die Fenstergrösse sich im Laufe des Programms nicht ändert.

Sechs Frames, die jeweils eine eigene ”Seite” darstellen in einer Klasse ist zu viel. Da würde man eher die ”Wizward”-Funktionalität in eine Klasse herausziehen (oder ein `ttk.Notebook` verwenden) und je eine Klasse für eine ”Seite” schreiben. Die Wizard-Klasse könnte man dann auch so generisch schreiben, oder eine Basisklasse heraus ziehen, dass man sie in anderen Programmen wiederverwenden kann. Andere GUI-Rahmenwerke haben so etwas schon in der Grundausstattung (`gtk.Assistant`, `QtGui.QWizard`, `wx.wizard.Wizard`).
Flamez
User
Beiträge: 11
Registriert: Sonntag 14. Juni 2015, 18:43

ttk.Notebook verwende ich sogar, da ein Teil der Frames als Tabs dargestellt sind. Ich habe 3 Hauptframes, einer davon enthält den Tab-Frame mit der ttk.Notebook und weiteren Frames dann als Tabs. Da ich Framewechsel nur über Buttons realisiert habe wie "Login", "Zurück", etc. kann der Benutzer nicht beliebig wechseln. Ich weiß immer von wo nach wo er will mit dem Betätigen eines Buttons. Ansonsten habe ich schon Tabs. Ob sich das evtl. sinnvoll in mehrere Klassen zerlegen lässt muss ich mal prüfen, immerhin habe ich nun eine Anregung. Ich denke auch, dass ich erst mal die Umstellung vollziehe, alle Frames zu Beginn erstellen zu lassen, zu stapeln und dann mit tkraise() hervorzuholen. Alleine schon, dass ich noch eine andere Vorgehensweise kennenlerne. Was die Fenstergröße angeht, die habe ich mit resizable(False, False) deaktiviert. So muss ich mir um die Skalierbarkeit erst mal keine Gedanken machen.
BlackJack

@Flamez: Es ging mir bei der Grössenänderung nicht darum das der Benutzer das macht, sondern das sich die Fenstergrösse ändert weil die Frames ja je nach Inhalt verschiedene grössen haben, und sich somit die Fenstergrösse beim Wechseln des Frames ändert wenn man die nicht stapelt und damit das Fenster so gross wie der grösste Frame ist.
Timme
User
Beiträge: 4
Registriert: Dienstag 22. März 2016, 18:58

Moin, ich bin neu hier und fange gerade erst mit Python an.
ich hab glaub ich ein ähnliches Problem, ich versuche 4 frames in einem fenster hin und her zu schalten.
Momentan ist das Script ncoh ziemlich verbugt evtl. hat ja jemand einen Tip wie ich das ganze realisieren bzw besser machen kann ;-)
LG Timme

Code: Alles auswählen

from Tkinter import *

global content,mframe
content = "Int"

def mainframe(parent,content):
    if 1==1:
        mframe=Frame(parent,
                 width=260,
                 height=240,
                 bd=0,
                 bg="black")
        mframe.place(x=0,
                 y=0)
        if content == "Int":
            intframe(mframe)
        elif content == "Col1":
            col1frame(mframe)
        elif content == "Col2":
            col2frame(mframe)
        else:
            tempframe(mframe)
    else:
        mframe.place_forget()

def menueframe(parent):
    mmenue = Frame(parent,
                   height=240,
                   width=60,
                   bd=0,
                   bg="black")
    intbutton = Button(mmenue,
                       text="Int",
                       font=15,
                       bd=1,
                       bg="black",
                       fg="white",
                       command= clearmframe(mframe,"Int"))
    col1button = Button(mmenue,
                       text="Col1",
                       font=15,
                       bd=1,
                       bg="black",
                       fg="white",
                       command= clearmframe(mframe,"Col1"))
    col2button = Button(mmenue,
                       text="Col2",
                       font=15,
                       bd=1,
                       bg="black",
                       fg="white")
    tempbutton = Button(mmenue,
                       text="Temp",
                       font=15,
                       bd=1,
                       bg="Black",
                       fg="White",
                       command=main.quit)
    mmenue.place(x=260)
    intbutton.place(y=0,
                    height=60,
                    width=60)
    col1button.place(y=60,
                    height=60,
                    width=60)
    col2button.place(y=120,
                    height=60,
                    width=60)
    tempbutton.place(y=180,
                    height=60,
                    width=60)





def clearmframe(f,next):
    f.place_forget()
    mainframe(main,next)

def intframe(mframe):
    intlab= Label(mframe,
              text="Intensity",
              bg="black",
              fg="white",
              font="20")
    intlab.place(x=55,
             y=10,
             width=150,
             height=50)
    intfad= Scale(mframe,
              from_=100,
              to=0,
              font=15,
              bg="black",
              fg ="white",
              bd="1",
              troughcolor="green",
              highlightcolor="black",
              activebackground="black",
              highlightbackground="black",
              highlightthickness="1",
              width=40,
              length=150)
    intfad.place(x=80,
                 y=80)

def col1frame(mframe):
    intbutton = Button(mframe,
                       text="Int",
                       font=15,
                       height=60,
                       width=60,
                       bd=0,
                       bg="black",
                       fg="white")
    intbutton.place(y=100,
                    x=100)
    return

def col2frame(mframe):
    return


def tempframe(mframe):
    return



main = Tk()
main.configure(width=320,height=240,bg="Black")
main.overrideredirect(1)
mainframe(main,content)
menueframe(main)
main.mainloop()
BlackJack

@Timme: Das ist ein ziemliches Durcheinander. Lass Sternchen-Importe bleiben, vergiss dass es ``global`` gibt, am besten auch gleich die `place()`-Methode, nenne Funktionen nach den *Tätikeiten* die sie ausführen und nicht nach passiven ”Dingen”, und beschäftige Dich mit objektorientierter Programmiergung. Auf Modulebene gehört nur Code der Konstanten, Funktionen, oder Klassen definiert und Funktionen und Methoden sollten auf keine Werte (ausser Konstanten) zugreifen die nicht als Argument übergeben wurden. Das Hauptprogramm gehört auch in eine Funktion.
Timme
User
Beiträge: 4
Registriert: Dienstag 22. März 2016, 18:58

Hi und danke für die schnelle Antwort,
wie gesagt ich steige gerade erst in Python ein, bis jetzt hab ich nur kleiner scripts in anderen sprachen geschrieben und auch das eher stümperhaft ;-) objektorientiert war da bis jetzt noch kein wirkliches Thema für mich und ich hab ein wenig angst das die verschachtelung mit klassen das ganze nur unnötig kompliziert macht...

Ich beschreib mal was ich am ende haben möchte,
Eine 320x240px (rpi Touchscreen 2,8") große Oberfläche die kommandos/daten per tcp/ip sendet und empfängt
um Licht und Heizung zu steuern.
Dafür brauche ich 4 Seiten mit Widgets, damit alles auch über den kleinen Bildschirm bedienbar ist
Ach ja es ist wichtig das, dass Menu auf der rechten Seite ist damit der Platz optimal ausgenutzt ist daher fällt tkk.notebook wohl leider aus,oder?

Evtl. hast du da ja n paar Beispiele oder kannst mir den groben weg dahin beschreiben ;-)
LG Timme
BlackJack

@Timme: Klassen machen es einfacher, darum wurden die ja erfunden, damit man nicht ein Riesenskript mit globalem Zustand hat wo am Ende keiner mehr durchsteigt, inklusive dem Autor selber wenn er da nach ein paar Monaten wieder rein schaut. Und spätestens bei GUI-Programmierung wird es in Python schwer ohne Klassen, aber trotzdem nachvollziehbar und sauber zu programmieren. Da `ttk.Notebook` wegen der Platzierung der Reiter nicht in Frage kommt, kann man sich beispielsweise selbst so eine Klasse schreiben mit Schaltflächen rechts zum wechseln der Seiten.

Auch wenn man für eine feste Displaygrösse programmiert, macht es Sinn das Layout dem GUI-Rahmenwerk zu überlassen. Zum einen passt es sich dann automatisch an wenn man doch mal ein anderes Display verwendet, und man hat beispielsweise die Schaltflächen mit dem Menü immer genau so breit wie der breiteste Menüpunkt ist, ohne das man selber ”ausmessen” oder herumprobieren muss wieviel Platz das denn nun braucht. Und wenn zu wenig Platz übrig bleibt, dann ersetzt man einfach die Beschriftung(en) durch kürzere und das Menü nimmt automatisch weniger Platz ein.

GUI und Server verbinden bedeutet Socket-Programmierung oder ein Rahmenwerk für RPC, und nebenläufige Programmierung (`threading`-Modul). Du suchst Dir ja für den Anfang lustige Sachen aus. :-)

Ich würde ausserdem sagen, dass die GUI vielleicht nicht das ist womit man anfangen sollte, sondern die Geschäftslogik, also das was das Programm im Kern tun soll. Den Steuerungsteil, dann den Server, und dann am Ende die GUI da drauf setzen. Als abgeschlossene Teilprojekte.
Timme
User
Beiträge: 4
Registriert: Dienstag 22. März 2016, 18:58

:D ja ich weiß ist nicht unbedingt auf "Hallo Welt!" nivau aber man wächst ja an seinen Aufgaben :lol:
Tcp Socket sah jetzt nicht so aus als ob man dafür studieren müßte, aber da kann ich mich auch irren^^
Macht es den sin wenn ich das Gui auf 2 Hauptframes Menu/Content aufteile
und je nach auswahl die 4 oberflächen(content) neu erzeuge?
du hast mich da ziemlich verunsichert :roll: :wink:
BlackJack

@Timme: Socket-Programmierung fehlerfrei hinzubekommen ist nicht so einfach wie die vielen, einfachen, fehlerhaften Beispiele im Netz vermuten lassen. Ausserdem sollte man sich überlegen ob man noch ein neues Protokoll in die Welt setzen muss, und nicht lieber etwas vorhandenes verwendet.

Das mit dem neu erzeugen wurde ja schon weiter oben in diesem Thema geklärt: Es ist einfacher gleich am Anfang alle Frames zu erstellen und dann einfach nur noch zwischen denen umzuschalten.
Timme
User
Beiträge: 4
Registriert: Dienstag 22. März 2016, 18:58

kannst du mir für das erzeugen, stapel etc. mal n kurzes script beispiel schreiben, hab da schon nach gesucht aber nichts klärendes gefudnen :(
ich nutze übrigends python 2.7
ins threading lese ich mich gerade rein, ist ziemmlich interessant^^
was meinst du mit protokoll?
Das Protokoll ist doch TCP auf dem ich dann simple strings sende und empfange, oder meinst du was anderes?
BlackJack

@Timme: Du musst nur mehrere `Frame`-Exemplare mit `grid()` in die selbe Zelle setzen und kannst dann mit der `tkraise()`-Methode den jeweils gewünschten `Frame` in den Vorhergrund holen. Du musst dann nur noch dafür sorgen, dass die `Frame`\s an allen vier Seiten ”kleben”, damit sie alle den gesamten Platz des grössten `Frame`\s ausfüllen (oder den Platz des Fensters). Und dann müsste man das Grid noch so konfigurieren, dass diese Zelle mit dem Platz der von ”aussen” zur Verfügung gestellt wird, mit wächst (`rowconfigure()`- und `columnconfigure()`-Methoden auf dem Elternwidget).

Voraussetzung ist hier aber sowieso erst einmal objektorientierte Programmierung. Und da man das auch für die Geschäftslogik brauchen wird, und die normalerweise vor der GUI implementiert wird, dauert es wahrscheinlich noch eine Weile bis Du die GUI tatsächlich brauchst. :-)

TCP ist das Transportprotokoll. Du musst ja aber auch Deine Kommunikation auf Anwendungsebene irgendwie strukturieren. Simple Zeichenketten hört sich nicht gut an. Wie gesagt, bevor Du Dir selbst etwas bastelst, nimm etwas fertiges.
Antworten