Dinámicamente la creación de un menú en Tkinter. (expresiones lambda)?
Pregunta
Tengo un menubutton, que cuando se hace clic deberían mostrar un menú que contiene una secuencia específica de cadenas. Exactamente lo que las cadenas están en esa secuencia, no sabemos hasta que el tiempo de ejecución, por lo que el menú que aparece se debe generar en ese momento. Aquí es lo que tengo:
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>")
Lo importante es lo que hay debajo "si menu_tags:" - menu_tags supongo que es la lista [ 'pila', 'más', 'flujo']. Entonces lo que yo quiero hacer es efectivamente esto:
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)
donde add_tag_stack () se define como:
def add_tag_stack():
self.add_tag('stack')
y así sucesivamente.
El problema es, la 'etiqueta' variable toma 'pila' el valor y luego el valor 'más' y así sucesivamente, y que no consigue evalúa hasta new_command se llama, en cuyo punto la 'etiqueta' variable es simplemente 'flujo'. Por lo que la etiqueta que se agrega es siempre el último en el menú, no importa lo que hace clic en el usuario.
Yo estaba originalmente usando una lambda, y pensé que tal vez definir explícitamente la función que el anterior podría funcionar mejor. De cualquier manera el problema se produce. He intentado usar una copia de la 'etiqueta' variable (ya sea con "current_tag = tag" o utilizando el módulo de copia) pero eso no lo resuelve. No estoy seguro de por qué.
Mi mente está empezando a vagar hacia cosas como "eval" pero espero que alguien pueda pensar en una manera inteligente que no implica cosas tan horribles.
Muchas gracias!
(En caso de que sea relevante, Tkinter .__ version__ '$ Revision: 67083 $' retornos. Y estoy usando Python 2.6.1 en Windows XP)
Solución
En primer lugar, el problema no tiene nada que ver con Tkinter; que es mejor si la reduce a un simple trozo de código que muestra su problema, por lo que puede experimentar con ella más fácilmente. Aquí hay una versión simplificada de lo que está haciendo que he experimentado con. Estoy sustituyendo un diccionario en lugar del menú, para hacer más fácil para escribir un pequeño caso de prueba.
items = ["stack", "over", "flow"]
map = { }
for item in items:
def new_command():
print(item)
map[item] = new_command
map["stack"]()
map["over"]()
map["flow"]()
Ahora, cuando ejecutamos esto, como usted ha dicho, obtenemos:
flow
flow
flow
El problema aquí es la noción del alcance de Python. En particular, la declaración for
no introduce un nuevo nivel de alcance, ni una nueva unión para item
; por lo que es la actualización de la misma variable item
cada vez a través del bucle, y todas las funciones new_command()
estamos refiriendo a ese mismo artículo.
Lo que hay que hacer es introducir un nuevo nivel de alcance, con una nueva unión, para cada uno de los item
s. La forma más sencilla de hacerlo es a lo envuelve en una nueva definición de función:
for item in items:
def item_command(name):
def new_command():
print(name)
return new_command
map[item] = item_command(item)
Ahora, si se sustituye de que en el programa anterior, se obtiene el resultado deseado:
stack
over
flow
Otros consejos
Ese tipo de cosas es bastante un problema común en Tkinter, creo.
Probar (en el punto apropiado):
def new_command(tag=tag):
self.add_tag(tag)
Yo tenía un error similar. Sólo se muestra el último elemento de la lista. Determinada estableciendo
command=lambda x=i: f(x)
Tenga en cuenta el x=i
después lambda
. Esta asignación hace que su variable local i
ir a la derecha en la función de su f(x)
command
. Esperanza, este sencillo ejemplo ayudará a:
# 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()