Menüauswahl abfragen

Fragen zu Tkinter.
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Hallo,

ich bin mit Python recht neu unterwegs. Habe mich auch schon ca. 2 Wochen durch diverse Schulungsunterlagen gekämpft und versuche mich jetzt über ein paar Beispiele an ein Praxisprojekt ranzuarbeiten.

Aktuell hänge ich bei der Menüabfrage. Den grundsätzlichen Aufbau meine ich verstanden zu haben. Das Menü steht weitestgehend. Allerdings finde ich keine Lösung für ein spezielles Problem und hoffe, dass mir hier jemand helfen kann.

Ziel beim aktuellen Programmteil ist es, über Menü ein Team auswählen zu können. Mit der Auswahl sollen dann ein paar Parameter (Farbe, Teamzugehörigkeit, usw.) für den aktuellen Spieler gesetzt werden.
Hinweis: Um die Oberfläche schlank zu halten und nicht zu überfrachten, werden auch noch einige anderen Auswahlpunkte über das selbe Menü gesteuert, so dass separate List- oder Kombiboxen nicht wirklich gewünscht sind.

Mit jedem Submenüpunkt kann ja über Command eine Funktion aufgerufen werden. Ich möchte im Menü jedoch wie beschrieben eine Auswahl gleichartiger Auswahlpunkte (z.B. Teams) anbieten. Dies habe ich erstmal durch eine For-Schleife gelöst, welche zum Menüpunkt den Zähler mit anhängt. Später wird hier ggf. der Zähler einfach nur noch als Index für Listeneinträge genutzt. Die Anzahl der Menüpunkte soll dynamisch erzeugt werden können. Ich könnte jetzt für jeden einzelnen Auswahlpunkt eine eigene Funktion erstellen. Dies macht jedoch keinen Sinn, da der Zähler auch der einzige Parameter ist, den ich in der aufzurufenden Funktion wieder nutzen möchte bzw. danach auswerte. Ich habe bisher nichts gefunden, um den ausgewählten Menüpunkt als Parameter an eine Funktion mit übergeben zu können bzw. diesen Auswahlpunkt überhaupt ermitteln zu können.
Lösungsansätze über Optionsbutton finde ich optisch nicht besonders schön. Ich gehe auch davon aus, dass man den ausgewählten Menüpunkt irgendwie ermitteln und als Parameter mit übergeben kann.

Hat vielleicht jemand eine Idee, wie ich das anstellen könnte?

Schon mal im Voraus vielen Dank für mögliche Lösungsansätze.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Klingt danach als ob du functools.partial suchst. Damit erzeugst du die gewünschten Funktionen on-demand sozusagen.
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Erstmal besten Dank an deets ...

Lösung war dann doch einfacher als ich dachte mit lambda-Funktion umzusetzen. Relevant ist die Zeile mit der lambda-Funktion. Der entscheidende Haken lag hier noch an der fehlenden Zuweisung i=i.
In der Funktion auswahl könnte man nun die Auswahl auswerten oder mit dem Index Aktionen durchführen.
Für alle, die es interessiert das Testlisting ... (allerdings nur in einer Testversion ohne offiziellen Pythonstyle).
Hinweis: Die Maus müsste noch an einen konkreten Menübutton gebunden werden, zum Testen reicht es aber.

Code: Alles auswählen

#from tkinter import *
from tkinter import Canvas
import tkinter as tk
root = tk.Tk()

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

def popup(event):
    menu.post(event.x_root, event.y_root)
#    menu.post(100, 100)

# attach popup to canvas
obj_frame.bind("<Button-1>", popup)

def hello():
    print("hello!")
    
def auswahl(number):
    print("Dies ist Team " + number)
    
# create a popup menu
mainmenu = tk.Menu(obj_frame)
menu = tk.Menu(mainmenu, tearoff=0, relief="raised", bd=3, activeforeground="red", activebackground="yellow")
mainmenu.add_command(label="Spieler neu", command=hello)
submenu_player_edit = tk.Menu(menu, tearoff=0)
submenu_player_edit.add_command(label="Name ändern", command=hello)
submenu_team_choice = tk.Menu(submenu_player_edit, tearoff=0)
for i in range(1, 17):
#    submenu_team_choice.add_command(label="Team " + str(i), command=hello)
    submenu_team_choice.add_command(label="Team " + str(i), command = lambda i=i: auswahl(str(i)))
submenu_player_edit.add_cascade(label="Team wählen", menu=submenu_team_choice)
submenu_player_edit.add_command(label="Handycap wählen", command=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=hello)
submenu_player_move.add_command(label="runter", command=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=hello)
submenu_player_delete.add_command(label="nein", command=hello)
menu.add_cascade(label="Spieler löschen", menu=submenu_player_delete)
menu.add_separator()
menu.add_command(label="Statistik", command=hello)
menu.add_separator()
menu.add_command(label="abbrechen")
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@suk: genau dafür gibt es `partial`: `command=partial(auswahl, str(i))`
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich würde das so machen:

Code: Alles auswählen

def auswahl(number):
    print("Dies ist Team", number)

# ...

for i in range(1, 17):
    submenu_team_choice.add_command(label="Team {}".format(i), command=lambda: auswahl(i))
Wobei auswahl() ja hoffentlich mehr macht und das print() nur ein Platzhalter ist...
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@snafu: und das ist genau der Weg, der nicht funktioniert.Die Variablen, die in einem Lamda-Ausdruck verwendet werden, gehören zum umbegenden Namespace. Alle command-Aufrufe verwenden also den Wert den das `i` zuletzt hat, 17. Besser `partial` verwenden.
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Danke für die Tipps.
Mal unabhängig von den Hinweisen ... Mit der Variablenzuweisung in der Lambda-Anweisung funktioniert der Aufruf der Funktion mit dem entsprechenden Index.
Gibt es einen Vorteil für die Nutzung von partial bzw. gibt es einen Grund, warum man partial statt lambda nehmen sollte?
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@suk: genau aus dem Grund, dass Du den Umweg über i=i gehen mußt, oder dass snafu das vergessen hat, und es deshalb falsch ist; und partial ist einfach einfacher zu verstehen.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

partial Objekte erlauben einfachere Introspektion: https://docs.python.org/2/library/functools.html

Und du kannst es auch mit positionalen Argumenten aufrufen: partial(f,i) statt lambda: i=i: f(i). Finde ich partial auch ästethischer.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

In welchem Zusammenhang tritt das Problem mit lambda auf? Habe es mal unter Python 3 getestet:

Code: Alles auswählen

def call_func(f):
    return f()

def main():
    for i in range(5):
        call_func(lambda: print(i))

if __name__ == '__main__':
    main()
Hier wird erwartungsgemäß ausgegeben:

Code: Alles auswählen

0
1
2
3
4
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Naja du rufst ja gleich auf. Dann ist i eben wie erwartet. Pack die in eine Liste und ruf sie dann auf.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Was genau soll ich in eine Liste stecken? Die fünf Zahlen? Dann verhält es sich weiterhin wie erwartet. Ich tu doch eigentlich das Gleiche wie der OP.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nein, die 5 callables. Das Problem ist, das der Closure/ABschluss über die Namen passiert. Nicht über die Werte. Da du aber nicht erst alle callables erzeugst, sondern immer nur einen, und den dann gleich aufrufst, sieht der auch den Wert, den du erwartest. Denn der ist ja noch nicht neu gebunden.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ja richtig, das macht der OP doch auch, oder übersehe ich was? Wenn man die Lambdas in eine Liste steckt, dann ist doch klar, dass sie sich den Wert holen, der gerade aktuell ist. Ich habe das Gefühl, wir reden über ein Problem, das gar nicht existiert...

Nochmal anders gesagt: Lambdas arbeiten mit den Werten, die zum Zeitpunkt ihres Aufrufs aktuell sind. Dies ist nicht zwingend der Wert, wenn der Bezeichner übergeben wird, denn der Wert kann sich zwischenzeitlich geändert haben. Wenn ich also i an ein Lambda binde und das Lambda erst später aufrufe, nachdem ich i verändert habe, dann wird das Lambda mit dem dann aktuellen i aufgerufen, so wie jede andere Funktion es zum Zeitpunkt ihres Aufrufs ebenfalls tun würde. Kann sein, dass das für manche verwirrend ist, aber ich finde es konsequent.
narpfel
User
Beiträge: 643
Registriert: Freitag 20. Oktober 2017, 16:10

@snafu: Die `lambda`s werden erst ausgeführt, wenn ein Menüpunkt ausgewählt wird, also wenn alle `i`s den selben Wert haben.
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: 17711
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: 6731
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: 6731
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?
Antworten