Menüauswahl abfragen

Fragen zu Tkinter.
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hier eine etwas entflochtene und ausführbare Variante des Beitrages:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import partial

try:
    # Tkinter for Python 2.xx
    import Tkinter as tk
except ImportError:
    # Tkinter for Python 3.xx
    import tkinter as tk

APP_TITLE = "Menu Abfragen"
APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 400
APP_HEIGHT = 300


class Application(tk.Frame):

    def __init__(self, master):
        self.master = master
        tk.Frame.__init__(self, master)

        self.obj_canvas_master = tk.Canvas(master, width=500, height=300,
            relief="sunken", bd=3)
        self.obj_canvas_master.pack()

        self.obj_frame = tk.Frame(self.obj_canvas_master, background="blue",
            bd=6, relief="groove", width=300, height=200, padx=3, pady=3)
            
        self.obj_canvas_master.create_window(0, 0, window=self.obj_frame,
            anchor="nw", tags="self.frame")

        # attach popup to canvas
        self.obj_frame.bind("<Button-1>", self.popup)
        
        # create a popup menu
        mainmenu = tk.Menu(self.obj_frame)
        menu = tk.Menu(mainmenu, tearoff=0, relief="raised", bd=3,
            activeforeground="red", activebackground="yellow")
        mainmenu.add_command(label="Spieler neu", command=self.hello)
        
        submenu_player_edit = tk.Menu(menu, tearoff=0)
        submenu_player_edit.add_command(label="Name ändern", command=self.hello)
        submenu_team_choice = tk.Menu(submenu_player_edit, tearoff=0)

        for i in range(1, 17):
            '''
            #~~ Variante OP
            #   Funktioniert: Typischer gewöhnungsbedürftige 'lambda' Syntax 
            submenu_team_choice.add_command(label="Team " + str(i), 
                command=lambda i=i: self.auswahl(str(i)))
                
            #~~ Variante sanfu
            #   Funktioniert nicht: Hier wird immer der letzte Team-Index
            #   als Argument übergeben 
            submenu_team_choice.add_command(label="Team {}".format(i),
                command=lambda: self.auswahl(i))
            '''
            #~~ Variante Sirius3, __deets__ (und wuf)
            #   Funktioniert: Ein etwas mehr verständlicherer 'partial' Syntax
            submenu_team_choice.add_command(label="Team {}".format(i),
            command=partial(self.auswahl, i))
            
        submenu_player_edit.add_cascade(label="Team wählen", menu=submenu_team_choice)
        submenu_player_edit.add_command(label="Handycap wählen", command=self.hello)
        menu.add_cascade(label="Spieler anpassen", menu=submenu_player_edit)

        submenu_player_move = tk.Menu(menu, tearoff=0)
        submenu_player_move.add_command(label="hoch", command=self.hello)
        submenu_player_move.add_command(label="runter", command=self.hello)
        menu.add_cascade(label="Spieler verschieben", menu=submenu_player_move,
            state=tk.DISABLED)

        submenu_player_delete = tk.Menu(menu, tearoff=0)
        submenu_player_delete.add_command(label="ja", command=self.hello)
        submenu_player_delete.add_command(label="nein", command=self.hello)
        menu.add_cascade(label="Spieler löschen", menu=submenu_player_delete)

        menu.add_separator()
        menu.add_command(label="Statistik", command=self.hello)
        menu.add_separator()
        menu.add_command(label="abbrechen")

        self.menu = menu
        
    def popup(self, event):
        self.menu.post(event.x_root, event.y_root)
        #self.menu.post(100, 100)
    
    def hello(self):
        print("hello!")
       
    def auswahl(self, number):
        print("Dies ist Team {}".format(number))

           
def main():
    app_win = tk.Tk()
    app_win.title(APP_TITLE)
    app_win.geometry("+{}+{}".format(APP_XPOS, APP_YPOS))
    app_win.geometry("{}x{}".format(APP_WIDTH, APP_HEIGHT))
        
    Application(app_win).pack(fill='both', expand=True)
    
    app_win.mainloop()

 
if __name__ == '__main__':
    main()      
Gruss wuf :wink:
Take it easy Mates!
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@snafu: hier noch mal das Problem als Programm, mit dem es hoffentlich klar wird:

Code: Alles auswählen

from functools import partial
 
def define_functions1():
    result = []
    for i in range(5):
        result.append(lambda: print(i))
    return result

def define_functions2():
    result = []
    for i in range(5):
        result.append(partial(print, i))
    return result

def main():
    func1 = define_functions1()
    func2 = define_functions2()
    print("Ergebnis 1: ")
    for f in func1: f()
    print("Ergebnis 2: ")
    for f in func2: f()
    
if __name__ == '__main__':
    main()
und der Ausgabe:
[codebox=text file=Unbenannt.txt]
Ergebnis 1:
4
4
4
4
4
Ergebnis 2:
0
1
2
3
4
[/code]
Benutzeravatar
snafu
User
Beiträge: 6736
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Habe es jetzt begriffen, denke ich. Die GUI führt die Lambdas nicht unmittelbar aus. Dadurch gilt bei einem nachträglichen Aufruf nur der letzte Wert von i. Ähnlich wie es wäre, wenn man die Lambdas zuvor in eine Liste ablegt und die Liste anschließend durchläuft.
Benutzeravatar
snafu
User
Beiträge: 6736
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

__deets__ hat geschrieben:Das Problem ist, das der Closure/ABschluss über die Namen passiert. Nicht über die Werte.
Jetzt nochmal gelesen und nun auch verstanden. Hatte etwas länger gedauert bei mir.
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Das war bis hier recht ausführlich. Danke, konnte einiges Neues mitnehmen.
Als Fazit für mich ... beide Varianten sind möglich.
Vorteil lambda: direkter Zusammenbau der erforderlichen Funktion
Vorteil partial: deutlich einfacher zu verstehen, jedoch Import einer zusätzlichen Funktion erforderlich

Habe allerdings noch ein zweites Problem. Wie bekomme ich es hin, dass das Menü nicht gleich bei Auswahl geschlossen wird. Die Menüpunkte hoch und runter sollen ggf. mehrmals hintereinander ausgeführt werden. Da wäre es nervig, jedesmal den Menüpunkt neu aufrufen zu müssen.
Mein Ziel wäre es bei konsequenter Menüführung, das Menü bei einigen Auswahlpunkten offen zu halten oder nach Ausführung genau so wieder aufzumachen, dass der Benutzer sofort auf dem selben Menüpunkt steht.
Die Alternative wäre natürlich, die Mehrfachwiederholung in der Funktion selber einzubauen.
Hätte jemand eine Idee?
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das geht denke ich nicht. Du hast dir da eine Interaktion überlegt für die Menüs nicht gedacht sind. So etwas baut man in einen normalen Dialog.
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

wuf hat geschrieben:Hier eine etwas entflochtene und ausführbare Variante des Beitrages:

...
class Application(tk.Frame):
...
@wuf: Deine version sieht sehr aufgeräumt aus. Ich habe aber anscheinend noch nicht ganz verstanden, warum Du in die Klassendefinition tk.Frame aufgenommen hast. Ich hätte ja vermutet, dass an der Übergabe app_win auch die Funktion tk.Frame mit dranhängt.
Welche Aufgabe hat der Teil in der Klassendefinition?
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@suk: das nennt sich Vererbung und hat was mit Objektorientierung zu tun. Application erbt alle Eigenschaften von tk.Frame und bekommt noch ein paar mehr.
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

suk hat geschrieben:@wuf: Deine version sieht sehr aufgeräumt aus. Ich habe aber anscheinend noch nicht ganz verstanden, warum Du in die Klassendefinition tk.Frame aufgenommen hast. Ich hätte ja vermutet, dass an der Übergabe app_win auch die Funktion tk.Frame mit dranhängt:
Als Basis meiner Experimentierskripte verwende ich folgendes Template um einzelne im Beitrag gesammelte Codefetzen in eine ausführbare Struktur einzufügen. Der Klassenname 'Application' müsste eventuell eher in 'Container' umbenannt werden. Übergeben wird nur das Hauptfenstersobjekt ohne ein Frameobjekt übergeben.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
from functools import partial
 
try:
    # Tkinter for Python 2.xx
    import Tkinter as tk
except ImportError:
    # Tkinter for Python 3.xx
    import tkinter as tk
 
APP_TITLE = "Template"
APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 400
APP_HEIGHT = 300
 
 
class Application(tk.Frame):
 
    def __init__(self, master):
        self.master = master
        tk.Frame.__init__(self, master)
 
           
def main():
    app_win = tk.Tk()
    app_win.title(APP_TITLE)
    app_win.geometry("+{}+{}".format(APP_XPOS, APP_YPOS))
    app_win.geometry("{}x{}".format(APP_WIDTH, APP_HEIGHT))
       
    Application(app_win).pack(fill='both', expand=True)
   
    app_win.mainloop()
 
 
if __name__ == '__main__':
    main()      
Gruss wuf :wink:
Take it easy Mates!
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich würde die die Vererbung von Frame loswerden. Sie ist unnötig und schlimmstenfalls verwirrend. Sie würde nur dann Sinn ergeben, wenn durch die Ableitung die eigentliche Darstellung innerhalb tkinters verändert würde. So wie man zb in Qt ein QWidget mit überladen der Paint Methode machen kann.

Tkinter kennt so etwas meiner Kenntnis nach nicht, höchstens könnte man ein Styling vornehmen - das kann man aber immer auch in dem umliegenden Code machen.
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

__deets__ hat geschrieben:Ich würde die die Vererbung von Frame loswerden. Sie ist unnötig und schlimmstenfalls verwirrend. Sie würde nur dann Sinn ergeben, wenn durch die Ableitung die eigentliche Darstellung innerhalb tkinters verändert würde. So wie man zb in Qt ein QWidget mit überladen der Paint Methode machen kann.
Wie würdest du dann das obige Template abändern, dass es deinen Vorstellungen entspricht?
Gruss wuf :wink:
Take it easy Mates!
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Einfach von object erben, und den Frame im Konstruktor erzeugen. Grob (auf dem Telefon getippt):

Code: Alles auswählen

class Application(object):

     der __init__(self, ...):
           self._frame = tk.Frame(....)
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

OK __deets__

Danke für deine Rückantwort. Habe mein Templates für Tkinter-Versuche wie folgt auf meine Bedürfnisse abgeändert:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import partial

try:
    # Tkinter for Python 2.xx
    import Tkinter as tk
except ImportError:
    # Tkinter for Python 3.xx
    import tkinter as tk

APP_TITLE = "NewTemplate_01"
APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 300
APP_HEIGHT = 200

        
class Application(tk.Tk):

    def __init__(self):
        
        tk.Tk.__init__(self)
        self.title(APP_TITLE)
        self.protocol("WM_DELETE_WINDOW", self.close_app)
        self.geometry("+{}+{}".format(APP_XPOS, APP_YPOS))
        self.geometry("{}x{}".format(APP_WIDTH, APP_HEIGHT))

        self.option_add("*Button.highlightThickness", 0)
        
        # Trial container-01
        self.app_container_01 = tk.Frame(self, bg='yellow', bd=0)
        self.app_container_01.pack(side='left', fill='both', expand=True)
        tk.Button(self.app_container_01, text='Container-01').pack(expand=True)

        # Trial container-02
        self.app_container_02 = tk.Frame(self, bg='steelblue', bd=0)
        self.app_container_02.pack(side='left', fill='both', expand=True)
        tk.Button(self.app_container_02, text='Container-02').pack(expand=True)
        
    def close_app(self):
        # Here do something before apps shutdown
        print("Good Bye!")
        self.destroy()
        
Application().mainloop()
P.S. Nicht auf der Telefon-Konsole getippt. Wünsche dir und allen Forum Mitgliedern noch alles Gute fürs 2018!

Gruss wuf :wink:
Take it easy Mates!
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Frohes neues zurück.

Auch hier hast du ja aber eine Vererbung drin, diesmal halt zu tk.Tk. Wozu? Einen solchen Schritt sollte man nur machen, wenn es notwendig ist. nicht nur, weil es zulässig ist.
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

OK __deets__

Dann hältst du diese Variante für besser?

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import partial

try:
    # Tkinter for Python 2.xx
    import Tkinter as tk
except ImportError:
    # Tkinter for Python 3.xx
    import tkinter as tk

APP_TITLE = "NewTemplate_01"
APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 300
APP_HEIGHT = 200

        
class Application(object):

    def __init__(self, app_win):
        self.app_win = app_win
        app_win.protocol("WM_DELETE_WINDOW", self.close_app)
        
        # Trial container-01
        self.app_container_01 = tk.Frame(app_win, bg='yellow', bd=0)
        self.app_container_01.pack(side='left', fill='both', expand=True)
        tk.Button(self.app_container_01, text='Container-01').pack(expand=True)

        # Trial container-02
        self.app_container_02 = tk.Frame(app_win, bg='steelblue', bd=0)
        self.app_container_02.pack(side='left', fill='both', expand=True)
        tk.Button(self.app_container_02, text='Container-02').pack(expand=True)
        
    def close_app(self):
        # Here do something before apps shutdown
        print("Good Bye!")
        self.app_win.destroy()

        
def main():
    app_win = tk.Tk()
    app_win.title(APP_TITLE)
    app_win.geometry("+{}+{}".format(APP_XPOS, APP_YPOS))
    app_win.geometry("{}x{}".format(APP_WIDTH, APP_HEIGHT))
    app_win.option_add("*Button.highlightThickness", 0)
    
    Application(app_win)
    
    app_win.mainloop()
 
 
if __name__ == '__main__':
    main()
Gruss wuf :wink:
Take it easy Mates!
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Definitiv besser.

Wobei ich sogar noch einen Schritt weiter gehen würde, und das app_win im Konstruktor definieren. Dann die Konstanten in die Klasse ziehen, und darauf mit self.TITLE etc zugreifen.

Die eigentlichen UI-Elemente (app_container etc) würde ich dann in einer Methode init_ui definieren, die per default leer ist.

Von dieser Klasse KANN man dann sinnvoll ableiten. Und sowohl Position, Name, Größe und UI nach Wunsch umgestalten. Damit bleibt dein Template imme gleich, und nur die variablen Anteile werden in der Ableitung definiert.

Last but not least käme noch eine mainloop Methode dazu, die einfach auf self.app-win.mainloop weiterleitet. Grund: von außen soll schon das Applications-Objekt die Schnittstelle darstellen. Wie es das tut ist (theoretisch) dann sein Problem. Faktisch ruft es natürlich den Tkinter mainloop auf.
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi __deets__

Habe versucht meine letzte Variante auf deine Ratschläge hin anzupassen. Hoffe dich richtig verstanden zu haben.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import partial

try:
    # Tkinter for Python 2.xx
    import Tkinter as tk
except ImportError:
    # Tkinter for Python 3.xx
    import tkinter as tk

APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 300
APP_HEIGHT = 200

        
class Application(object):
    APP_TITLE = "NewTemplate_01"
    
    def __init__(self):
        self.app_win = tk.Tk()
        self.app_win.protocol("WM_DELETE_WINDOW", self.close_app)
        self.app_win.title(self.APP_TITLE)
        self.app_win.geometry("+{}+{}".format(APP_XPOS, APP_YPOS))
        self.app_win.geometry("{}x{}".format(APP_WIDTH, APP_HEIGHT))
        self.app_win.option_add("*Button.highlightThickness", 0)
        
        self.init_ui()
        self.run_app()
        
    def init_ui(self):
        # Trial container-01
        self.app_container_01 = tk.Frame(self.app_win, bg='yellow', bd=0)
        self.app_container_01.pack(side='left', fill='both', expand=True)
        tk.Button(self.app_container_01, text='Container-01').pack(expand=True)

        # Trial container-02
        self.app_container_02 = tk.Frame(self.app_win, bg='steelblue', bd=0)
        self.app_container_02.pack(side='left', fill='both', expand=True)
        tk.Button(self.app_container_02, text='Container-02').pack(expand=True)
    
    def run_app(self):
        self.app_win.mainloop()
            
    def close_app(self):
        # Here do something before apps shutdown
        print("Good Bye!")
        self.app_win.destroy()

        
def main():    
    Application()
 
 
if __name__ == '__main__':
    main()      

Gruss wuf :wink:
Take it easy Mates!
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@__deets__: da wäre ich anderer Meinung. »Application« ist eine Tk-Instanz. Klarer Fall von Vererbung. So wie es jetzt ist, hat man ja immer die Indirektion über app_win (ist das Saarländisch für Apfelwein?), die meiner Meinung nach unnötig und nur umständlich ist. Ein Template ist auch nicht dafür da, dass man davon ableitet, sondern dass man es an die eigenen Bedürfnisse anpasst und erweitert. Eine »init_ui«-Methode ist auch überflüssig, da sie ja nur das, was eigentlich in __init__ stehen sollte, nochmal verstreut (__init__ ist kein Konstruktor, den gibt es auch und der heißt __new__). Die Variante davor hatte auch ihren Charme, alles in einen Frame zu packen, das vereinfacht das Umschreiben, falls aus der Tk-Instanz doch mal nur ein TopLevel wird.

@wuf: __init__ ist nur dazu da, ein Objekt zu initialisieren. Der Mainloop gehört da definitiv nicht rein. Das sieht man auch an »main«, in dem nur ein verkümmertes Objekt erzeugt wird, mit dem aber sonst nichts passiert. Die feste Vorgabe der Fenstergröße ist normalerweise ungeschickt. Laß doch das Fenster seine passende Größe und vor allem die Position selbst finden, das sollte ein Programm nicht selbst entscheiden.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

@sirius3: zu Beginn war es ja noch nicht mal eine Tk-Instanz, die Indirektion also auch gegeben. Und eine Ableitung sollte eine funktionale Erweiterung der Basisklasse darstellen. Wozu letztere auch in der gedacht sein muss. Da die Tk Klasse das nicht vorsieht, genauso wenig wie der Frame, wird es also reduziert auf eine reine zu-Dekoration von Methoden und Zustand zweier ansonsten völlig getrennter Funktionalitäten. Das ist in meinen Augen keine wünschenswerter Architektur.

Die Argumentation bezüglich des Templates ist allerdings richtig. Ich habe da selbst im Laufe der Diskussion drüber nachgedacht. Ohne eigenes Modul ist das überkandidelt. Die Aufteilung in verschiedene Methoden, in denen die UI aufgebaut wird, empfinde ich allerdings trotzdem (je nach Umfang) sinnvoll, weil der Aufbau der Widget Hierarchie IMHO oft sehr hässlich & repetitiv ist. Das einfach überlesen zu können und sich stattdessen auf die Anmeldung zb von callbacks und timern konzentrieren zu können finde ich durchaus sinnvoll. Für eine Handvoll Widgets lohnt sich das aber eher nicht.

Und was vorher vereinfacht war, sehe ich nicht. Die Ableitung anzufassen vs den Typ eines Members zu ändern - das ist doch gehupft wie gesprungen.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@__deets__: wie meist Du das, dass die Tk Klasse eine Vererbung nicht vorsieht? Aus einem leeren Fenster wird ein Fenster mit Inhalt. Typische Erweiterung der Funktionalität. Ein Widget-Hierarchie sollte man auch gar nicht in einem Hauptfenster aufbauen. Wenn man eine Klasse mit Aufgaben überhäuft, dann hilft es auch nichts, eine weitere Methode hinzuzufügen. Es gilt weiterhin die Konvention, dass alle Attribute in __init__ angelegt werden müssen. Statt dessen sollte man Teile in weitere Klassen auslagern, die dann typischerweise von Frame erben, damit man die Teile flexibel zu einem Fenster zusammenbauen kann.
Antworten