Tkinter Best Practices?

Fragen zu Tkinter.
Antworten
Vetsch
User
Beiträge: 7
Registriert: Sonntag 10. Juni 2018, 11:00

Grüße miteinander,

mal eine kurze Frage. Ich habe schon einige Beispiele gesehen, in denen Klassen für Tkinter GUIs folgendermaßen initialisiert werden:

Code: Alles auswählen

class Test( tkinter.Tk ):
    def __init__( self ):
        tkinter.Tk.__init__( self )

oder

class Canvas( tk.Canvas ):
    self.app_win = app_win
    tk.Canvas.__init__( self, app_win ... )
Allerdings funktioniert dasselbe ja auch mit einem Konstrukt wie:

Code: Alles auswählen

class Test( object ):
    def __init__( self ):
        self.root = tk.Tk()
Bei der zweiten Variante sehe ich, dass ich nochmal auf ein Objekt zugreifen muss anstatt dies im Laufe der weiteren Klasse über 'self' zu tun. Weiterhin scheint die erste Variante auf den ersten Blick etwas eleganter.

Gilt aber die zweite Variante als No-Go?

Meine zweite Frage ist, ob es sinnig ist, die eigenen Funktionen für das bewegen von Fenster etc. nachzubilden, um ein etwas anderes Look & Feel in Tk zu bekommen. Beispielsweise:

Code: Alles auswählen

import sys

if sys.version_info.major == 3:
    import tkinter as tk
    import tkinter.font as tkF
    from tkinter import ttk
else:
    import Tkinter as tk
    import Tkinter as ttk

class GOUS( object ):
    def __init__( self ):
        self.root = tk.Tk()
        # Setup Dimensions of Window
        self.root.overrideredirect( True )
        self.width = self.root.winfo_screenwidth()
        self.height = self.root.winfo_screenheight()
        self.width = self.width - ( self.width  / 3 )
        self.height = self.height - ( self.height / 3 )
        top = self.root.winfo_toplevel()
        top.rowconfigure( 0, weight=1 )
        top.columnconfigure( 0, weight=1 )
        self.root.geometry( "{}x{}+{}+{}".format( int( self.width ), int( self.height ), \
                                             int( ( self.root.winfo_screenwidth() - self.width ) / 2 ), \
                                             int( ( self.root.winfo_screenheight() - self.height ) / 2 ) ) )
        self.root.rowconfigure( 0, weight=1 )
        self.root.columnconfigure( 0, weight=1 )
        # AppCanvas
        self.AppCanvas = tk.Canvas( self.root, height=self.height, width=self.width )
        self.AppCanvas.grid( ipadx=0, ipady=0, padx=0, pady=0, sticky="news" )
        self.AppCanvas.configure( bg="#2b2b2b", relief="ridge", highlightthickness=0 )
        # Setup Default Bindings
        self.root.bind( "<KeyPress-F11>", self.toggle_fullscreen )
        self.root.bind( "<KeyPress-Escape>", self.exit_event )
        self.AppCanvas.bind( "<Configure>", self.resize_win )
        # Default Values
        self.queued_exit = False
        self.force_no_exit = False
        self.root.after( 800,self.check_exit )
        self.fullscreen = False
        # Test for MenuBar
        self.testbar = GOUSMenu( self.AppCanvas, self.root, "menu_bar" )
        self.root.mainloop()

    def exit_event( self, event=False ):
        self.root.bell()
        self.queued_exit = True

    def check_exit( self ):
        if self.queued_exit and not self.force_no_exit:
            self.root.destroy()
        self.root.after( 800, self.check_exit )

    def resize_win( self, event ):
        xscale = float( event.width ) / float( self.AppCanvas.configure( 'width' )[-1] )
        yscale = float( event.height ) / float( self.AppCanvas.configure( 'height' )[-1] )
        self.AppCanvas.configure( width=event.width, height=event.height )

    def toggle_fullscreen( self, event ):
        self.fullscreen = not self.fullscreen
        self.root.overrideredirect( not self.fullscreen )
        self.root.attributes( '-fullscreen', self.fullscreen )

class GOUSMenu( object ):
    def __init__( self, canvas_win, root_window, menu_name, x=0, y=0, height=30, width=0, grab=True ):
        self.root = canvas_win
        self.window = root_window
        self.menu_name = menu_name
        self.width = width
        if self.width == 0:
            self.width = int( self.root.configure( 'width' )[-1] )
        self.height = height
        self.root.create_rectangle( x, y, self.width, self.height, fill="#2b5555", width=0, \
                                    tags=("{}.gous.top_wingrab".format( self.menu_name ), \
                                          "{}.gous.top_MenuBar".format( self.menu_name ) ) )
        self.root.create_line( 0, self.height - 1, self.width, self.height - 1, width=1, \
                               fill="#999999", tags=("{}.gous.top_Menu_separator".format( self.menu_name ), \
                                                     "{}.gous.gous.top_MenuBar".format( self.menu_name ) ) )
        # Move the Window
        self.menu_grabbed = False
        self.win_old_pos = [0,0]
        self.root.tag_bind( "{}.gous.top_wingrab".format( self.menu_name ), sequence="<ButtonPress-1>", func=self.grab_menu )
        self.root.tag_bind( "{}.gous.top_wingrab".format( self.menu_name ), sequence="<ButtonRelease-1>", func=self.release_menu )
        self.root.tag_bind( "{}.gous.top_wingrab".format( self.menu_name ), sequence="<Motion>", func=self.move_menu )

    def grab_menu( self, event ):
        self.menu_grabbed = True
        self.win_old_pos[0] = event.x_root
        self.win_old_pos[1] = event.y_root

    def release_menu( self, event ):
        self.menu_grabbed = False
        self.win_old_pos = [0,0]

    def trigger_exit( self, event ):
        self.window.event_generate( "<KeyPress-Escape>" )

    def move_menu( self, event ):
        if self.menu_grabbed:
            new_x = event.x_root - self.win_old_pos[0]
            new_y = event.y_root - self.win_old_pos[1]
            win_pos = self.window.geometry()
            win_pos = win_pos.replace( 'x', ' ' )
            win_pos = win_pos.replace( '+', ' ' )
            win_pos = win_pos.split( ' ' )
            self.window.geometry( "{}x{}+{}+{}".format( int( self.root.configure( 'width' )[-1] ), \
                                                        int( self.root.configure( 'height' )[-1] ), \
                                                        int( win_pos[2] ) + new_x, int( win_pos[3] ) + new_y ) )
            self.win_old_pos[0] = event.x_root
            self.win_old_pos[1] = event.y_root

if __name__ == '__main__':
    gous = GOUS()
Danke schonmal Vorab für eure Mühe.
Benutzeravatar
__blackjack__
User
Beiträge: 13061
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Vetsch: Die Frage ist was `Test` ist, *ist* das ein Hauptfenster oder *enthält* das ein Hauptfenster (aus OOP-Sicht, nicht grafisch gesprochen). Je nach dem macht man dann das eine oder das andere. Gleiches für `Canvas`, wobei das dann wahrscheinlich schon ein komischer Name ist, wenn es kein Canvas *ist*.

Vererbung ist eine `ist-ein(e)`-Beziehung. Und das würde ich bei GUIs im allgemeinen und Tk im besonderen nicht anders sehen als in jedem anderen Bereich bei OOP.

Es ist IMHO nicht sinnvoll bei irgendeinem GUI-Rahmenwerk ein anderes Look & Feel zu programmieren. Man sollte davon ausgehen das die sich so verhalten wie der Benutzer das auf der Plattform erwartet und konfiguriert hat, was ja auf jeder Plattform unterschiedlich sein kann, und da sollte man IMHO nicht dran rumpfuschen. Zumal man die Kontrolle sowieso nur innerhalb des Fensterrahmens hat. Sofern es einen Fensterrahmen gibt, nicht einmal das muss ja der Fall sein. Alles was ausserhalb der inneren Fläche von Fenstern passieren soll, würde ich maximal als geäusserte Wünsche vom Programm betrachten, denen eine externe Fensterverwaltung nachkommen kann, aber nicht muss.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist eines dieser Themen die man lang und breit diskutieren kann. Aber ich stehe auf dem Standpunkt: wenn es geht, vermeidet man Vererbung. Sie macht mehr Probleme als sie loest. Wenn dein Objekt am Anfang nur mit einem Canvas arbeitet, dann ist die Versuchung gross, es davon abzuleiten. Und dann ploetzlich kommt noch ein Frame drumrum, damit du einen Button darunter plazieren kannst. Ist das jetzt der Frame, oder immer noch der Canvas? Ich wuerde das immer ueber Komposition loesen, also einfach Member anlegen. Dann kannst du an deinen widget-Hierarchien rumschrauben, ohne das sich an den Typen etwas aendert.

Der einzige Grund, warum man ableitet (im Kontext von GUIs, aber auch allgemein) ist fuer mich, wenn das zum erreichen einer Funktionalitaet *notwendig* ist. ZB wenn du ein QWidget-Objekt fuer das Qt-Framework erstellen willst, dann *musst* du die paint-Methode ueberladen, und damit macht es dann auch Sinn ein MeinWidget(QWidget) einzufuehren.
Benutzeravatar
__blackjack__
User
Beiträge: 13061
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@__deets__: Wenn um den `Canvas` ein `Frame` kommt, würde ich von `Frame` erben und den `Canvas` zum Attribut machen. Sonst kann man das Objekt ja nicht in ein anderes Container-Widget einfügen. Das wäre/ist für mich ein Grund von Widgets zu erben — das man sie in andere Widgets per Layoutmanager anordnen kann.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

@__blackjack__: ich sehe jetzt nicht, wo das einen funktionalen Unterschied macht.

Code: Alles auswählen

class MitVererbung(Canvas):
     def __init__(self, parent, ...):
            super().__init__(parent)
             ...
vs

Code: Alles auswählen

class OhneVererbung(object):
     def __init__(self, parent, ...):
            self.canvas = Canvas(parent)
             ...
Und durch die Vererbung hat man zusaetzlich noch die ganzen Methoden und anderen Eigenschaften geerbt, mit denen es zu Kollisionen kommen kann. Auch hier sehe ich jetzt nicht, wo das ernsthaft irgendwelche Vorteile bringt. Abgesehen davon, dass ich eh dazu neigen wuerde, die Widget-Hierarchie unabhaengig von einem Objekt zu bauen, das damit arbeitet - so, wie man das mit pyuic ja auch tut:

Code: Alles auswählen

def setup_gui(self):
       ...
       self.spielfeld = Canvas(...)
       ...
       self.radar = Canvas(...)

def __init__(self):
      self.setup_gui()
      self._spielfeld_darsteller = SpielfeldDarsteller(self.spielfeld)
Und natuerlich gegebenenfalls auch noch diverse andere Abhaengikeiten, die der SpielfeldDarsteller so haben koennte, wie zB die Buttons mit denen der Ausschnitt oder das Zoomlevel ausgewaehlt wird.

Aber wie gesagt, das ist auch sehr stark Geschmackssache.
Benutzeravatar
__blackjack__
User
Beiträge: 13061
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Der Unterschied fängt da wo man versucht das einzubauen:

Code: Alles auswählen

    mit_vererbung = MitVererbung(parent)
    mit_vererbung.pack()  # oder .grid()

# vs:

    ohne_vererbung = OhneVererbung(parent)
    # Wie layouten???
Da muss man dann wissen das es ein `canvas`-Attribut gibt, oder dem einen allgemeinen Namen geben wie `widget` den man dann überall verwendet.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Stimmt. An das pack habe ich da nicht gedacht. Denn

Code: Alles auswählen

 ohne_vererbung = OhneVererbung(parent)
 ohne_vererbung.canvas.pack()
waere dann wieder Demeter-Law-verletzend :(

Ich wuerde dann eher den schon angesprochenen setup_gui-Weg gehen.
Vetsch
User
Beiträge: 7
Registriert: Sonntag 10. Juni 2018, 11:00

__blackjack__ hat geschrieben: Donnerstag 21. Juni 2018, 16:14

Code: Alles auswählen

    mit_vererbung = MitVererbung(parent)
    mit_vererbung.pack()  # oder .grid()

# vs:

    ohne_vererbung = OhneVererbung(parent)
    # Wie layouten???
Hier muss ich dann doch mal fragen. Ich musste eben Demeter-Law suchen, daher kann ich dazu noch nicht viel sagen.

Prinzipiell ist Tkinter mein zugrunde liegendes Framework. In was für einem Fall sollte ich denn nicht davon ausgehen können, dass die Elemente welche ich baue, sich nicht bereits bewusst darüber sind, wo diese sich befinden? Die Ausnahme bildet selbstverständlich, wenn ich Widgets für den allgemeinen Gebrauch anfertigen möchte.

Aber wenn ich eine Klasse bilde, die eindeutig und exklusiv für ein bestimmtes Menü zuständig ist, und ein Menü verändert in (aus meiner Sicht) seltenen Fällen seine Position (Skalierung bei Größenanpassung des Elternrahmens ausgenommen), kann ich doch der Klasse es überlassen durch eine einfache Schnittstelle, sich selbst zu packen. Oder täusche ich mich?

Code: Alles auswählen

class MeinKnopf( object ):
    def __init__( self, elternfenster, width, height ):
        self.canvas = tk.Canvas( elternfenster, width=width, height=height )

    def show( self ):
        self.canvas.pack()
Allerdings sehe ich hier auch, dass man die einheitliche Schnittstelle für Tk Klassen natürlich zunichte macht. Wie oben erwähnt, weiß ich nicht wie oft das vorkommt, wenn jemand seine eigene Grafische Oberfläche bastelt.
Benutzeravatar
__blackjack__
User
Beiträge: 13061
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Vetsch: Ich würde in jedem Fall davon ausgehen das ein Element nicht weiss wo es sich befindet. Und auch nicht ob es mit `grid()` oder `pack()` angeordnet wird. Das ist nicht seine Aufgabe. Dazu müsste es ja den Aufbau des Widgets kennen in das es eingebaut wird.

Zum `MeinKnopf` beispielsweise: Den kannst Du dann nur in einem Containerwidget verwenden in dem alles andere auch per `pack()` und an der oberen Seite angeordnet ist. Wenn Du jetzt aber in dem Containerwidget auf `grid()` wechseln möchtest, musst Du plötzlich den Code von ``MeinKnopf` und vielleicht auch noch allen anderen Klassen von denen Exemplare in dem Container stecken anfassen um Code zu ändern der mit dem Erscheinungsbild des jeweiligen Widgets *selbst* gar nichts zu tun hat.

Ich kann mich hier irren, aber Deine Beispiele hinterlassen bei mir so ein bisschen den Eindruck von Sachen die man überhaupt gar nicht machen möchte, jedenfalls nicht regelmässig und in normalem Code. Tk hat bereits Menüs und Knöpfe und Layoutmanager.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Vetsch
User
Beiträge: 7
Registriert: Sonntag 10. Juni 2018, 11:00

Danke dir __blackjack__, diese Auslegung habe ich verstanden und kann ich so auch akzeptieren.
__blackjack__ hat geschrieben: Freitag 22. Juni 2018, 09:52 Ich kann mich hier irren, aber Deine Beispiele hinterlassen bei mir so ein bisschen den Eindruck von Sachen die man überhaupt gar nicht machen möchte, jedenfalls nicht regelmässig und in normalem Code. Tk hat bereits Menüs und Knöpfe und Layoutmanager.
Hierzu kann ich nur sagen, dass du mehr als Wahrscheinlich Recht hast. In dem was ich aktuell tue, fixiere ich bestimmte Elemente manuell anstatt .grid oder .pack zu verwenden. Es kann durchaus sein, dass meine Designvorstellung über die Standard Tkinter oder ttk Elemente realisiert werden kann.

Für mich war es erstmal einfacher, dass über Canvas zu versuchen. Im ersten Moment ^^ Bis mir auffiel, was für ein Aufwand das ist und wie der Code schon jetzt aussieht.

Ich schaue mir sonst nochmal die Möglichkeiten an, die Standard ttk Elemente zu nutzen. Und Ihr habt mir schon betreffend meiner Frage zu den zwei verschiedenen Instanzierungsvarianten geholfen, danke dafür.
Antworten