Dinamicamente la creazione di un menu in Tkinter. (espressioni lambda?)
Domanda
Ho un MenuButton, che se cliccato dovrebbe visualizzare un menu contenente una sequenza specifica di stringhe. Esattamente quello che le stringhe sono in quella sequenza, non sappiamo fino al runtime, in modo che il menu che si apre deve essere generata in quel momento. Ecco quello che ho:
class para_frame(Frame):
def __init__(self, para=None, *args, **kwargs):
# ...
# menu button for adding tags that already exist in other para's
self.add_tag_mb = Menubutton(self, text='Add tags...')
# this menu needs to re-create itself every time it's clicked
self.add_tag_menu = Menu(self.add_tag_mb,
tearoff=0,
postcommand = self.build_add_tag_menu)
self.add_tag_mb['menu'] = self.add_tag_menu
# ...
def build_add_tag_menu(self):
self.add_tag_menu.delete(0, END) # clear whatever was in the menu before
all_tags = self.get_article().all_tags()
# we don't want the menu to include tags that already in this para
menu_tags = [tag for tag in all_tags if tag not in self.para.tags]
if menu_tags:
for tag in menu_tags:
def new_command():
self.add_tag(tag)
self.add_tag_menu.add_command(label = tag,
command = new_command)
else:
self.add_tag_menu.add_command(label = "<No tags>")
La parte importante è la roba in "se menu_tags:" - menu_tags Supponiamo che l 'elenco [ 'pila', 'over', 'flusso']. Poi quello che voglio fare è effettivamente questo:
self.add_tag_menu.add_command(label = 'stack', command = add_tag_stack)
self.add_tag_menu.add_command(label = 'over', command = add_tag_over)
self.add_tag_menu.add_command(label = 'flow', command = add_tag_flow)
dove add_tag_stack () è definito come:
def add_tag_stack():
self.add_tag('stack')
e così via.
Il problema è che il 'tag' variabile assume 'pila' il valore e quindi il valore 'over' e così via, e che non venga valutato fino new_command si chiama, a quel punto il 'tag' variabile è solo 'flusso'. Così il tag che viene aggiunto è sempre l'ultimo sul menu, non importa ciò che l'utente fa clic su.
Mi è stato originariamente usando un lambda, e ho pensato che forse definire esplicitamente la funzione di cui sopra potrebbe funzionare meglio. In entrambi i casi il problema si verifica. Ho provato con una copia del 'tag' variabile (sia con "current_tag = tag" o utilizzando il modulo di copia), ma che non lo risolve. Io non so perché.
La mia mente sta cominciando a vagare verso cose come "eval" ma sto sperando che qualcuno possa pensare ad un modo intelligente che non coinvolge queste cose orribili.
Molto grazie!
(In caso è rilevante, Tkinter .__ version__ restituisce '$ Revision: 67083 $. E sto usando Python 2.6.1 su Windows XP)
Soluzione
Prima di tutto, il problema non ha nulla a che fare con Tkinter; è meglio se si riduce verso il basso per un semplice pezzo di codice che illustrano il problema, in modo da poter sperimentare con più facilmente. Ecco una versione semplificata di quello che stai facendo, che ho sperimentato con. Sto sostituendo un dict al posto del menù, per rendere più facile scrivere un piccolo banco di prova.
items = ["stack", "over", "flow"]
map = { }
for item in items:
def new_command():
print(item)
map[item] = new_command
map["stack"]()
map["over"]()
map["flow"]()
Ora, quando eseguiamo questo, come hai detto, otteniamo:
flow
flow
flow
Il problema qui è la nozione di Python di portata. In particolare, la dichiarazione for
non introduce un nuovo livello di portata, né una nuova associazione per item
; quindi viene aggiornata la stessa variabile item
ogni iterazione del ciclo, e tutte le funzioni new_command()
riferiamo a quello stesso elemento.
Quello che dovete fare è introdurre un nuovo livello di portata, con una nuova associazione, per ognuno dei item
s. Il modo più semplice per farlo è quello di avvolgerla in una nuova definizione di funzione:
for item in items:
def item_command(name):
def new_command():
print(name)
return new_command
map[item] = item_command(item)
Ora, se si sostituisce che nel programma precedente, si ottiene il risultato desiderato:
stack
over
flow
Altri suggerimenti
Questo genere di cose è piuttosto un problema comune in Tkinter, credo.
Prova questo (al momento opportuno):
def new_command(tag=tag):
self.add_tag(tag)
Ho avuto un errore simile. è stato mostrato solo l'ultimo elemento della lista. Fisso impostando
command=lambda x=i: f(x)
Si noti la x=i
dopo lambda
. Questa assegnazione rende la variabile locale i
andare a destra nella funzione f(x)
del vostro command
. Speranza, questo semplice esempio aiuterà:
# Using lambda keyword to create a dynamic menu.
import tkinter as tk
def f(x):
print(x)
root = tk.Tk()
menubar = tk.Menu(root)
root.configure(menu=menubar)
menu = tk.Menu(menubar, tearoff=False)
l = ['one', 'two', 'three']
for i in l:
menu.add_command(label=i, command=lambda x=i: f(x))
menubar.add_cascade(label='File', menu=menu)
root.mainloop()