Menüauswahl abfragen

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

Sonntag 17. Dezember 2017, 02:21

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: 3510
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 17. Dezember 2017, 11:48

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

Freitag 22. Dezember 2017, 00:11

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: 8437
Registriert: Sonntag 21. Oktober 2012, 17:20

Freitag 22. Dezember 2017, 08:55

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

Freitag 22. Dezember 2017, 09:46

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...
shcol (Repo | Doc | PyPi)
Sirius3
User
Beiträge: 8437
Registriert: Sonntag 21. Oktober 2012, 17:20

Freitag 22. Dezember 2017, 10:21

@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

Freitag 22. Dezember 2017, 19:12

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: 8437
Registriert: Sonntag 21. Oktober 2012, 17:20

Freitag 22. Dezember 2017, 19:35

@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: 3510
Registriert: Mittwoch 14. Oktober 2015, 14:29

Freitag 22. Dezember 2017, 19:38

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: 5537
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Freitag 22. Dezember 2017, 23:47

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
shcol (Repo | Doc | PyPi)
__deets__
User
Beiträge: 3510
Registriert: Mittwoch 14. Oktober 2015, 14:29

Samstag 23. Dezember 2017, 00:25

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: 5537
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Samstag 23. Dezember 2017, 08:47

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.
shcol (Repo | Doc | PyPi)
__deets__
User
Beiträge: 3510
Registriert: Mittwoch 14. Oktober 2015, 14:29

Samstag 23. Dezember 2017, 09:18

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: 5537
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Samstag 23. Dezember 2017, 09:46

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.
shcol (Repo | Doc | PyPi)
narpfel
User
Beiträge: 230
Registriert: Freitag 20. Oktober 2017, 16:10

Samstag 23. Dezember 2017, 10:27

@snafu: Die `lambda`s werden erst ausgeführt, wenn ein Menüpunkt ausgewählt wird, also wenn alle `i`s den selben Wert haben.
Antworten