2009-04-08 14 views
6

Tengo un menú que, al hacer clic, debe mostrar un menú que contiene una secuencia específica de cadenas. Exactamente qué cadenas están en esa secuencia, no lo sabemos hasta el tiempo de ejecución, por lo que el menú que aparece debe generarse en ese momento. Aquí es lo que tengo:Creación dinámica de un menú en Tkinter. (¿expresiones lambda?)

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 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 que la variable 'etiqueta' toma el valor 'pila' y luego el valor 'sobre' y así sucesivamente, y no se evalúa hasta que se invoca new_command, en cuyo momento la variable 'etiqueta 'es solo' flujo '. Entonces, la etiqueta que se agrega siempre es la última en el menú, sin importar en qué haga clic el usuario.

Originalmente estaba usando una lambda, y pensé que quizás definir explícitamente la función anterior podría funcionar mejor. De cualquier manera, el problema ocurre. Intenté usar una copia de la variable 'etiqueta' (ya sea con "current_tag = tag" o usando el módulo de copia) pero eso no lo resuelve. No estoy seguro por qué.

Mi mente está empezando a vagar hacia cosas como "eval" pero espero que alguien pueda pensar de una manera inteligente que no involucre cosas tan horribles.

Muchas gracias!

(En caso de que sea relevante, Tkinter .__ version__ devuelve '$ Revision: 67083 $' y estoy usando Python 2.6.1 en Windows XP.)

Respuesta

6

En primer lugar, el problema no tiene nada que ver con Tkinter; es mejor si lo reduce a una simple pieza de código que demuestre su problema, para que pueda experimentar con él más fácilmente. Aquí hay una versión simplificada de lo que estás haciendo con lo que experimenté. Estoy sustituyendo un dict en lugar del menú, para que sea más fácil 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, se obtiene:

flow 
flow 
flow 

La cuestión 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 vinculación para item; por lo que está actualizando la misma variable item cada vez que pasa el ciclo, y todas las funciones new_command() hacen referencia al mismo elemento.

Lo que debe hacer es introducir un nuevo nivel de alcance, con un nuevo enlace, para cada uno de los item s.La forma más sencilla de hacerlo es a lo envuelve en una nueva definición de la 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 
+0

Bueno, pensé que podría haber una solución específica para Tkinter. Alguien que diga "No, no, no, la forma en que haces esto es la función especial Tkinter.somethingOrOther()" ¡Gracias por la ayuda! – MatrixFrog

+0

¡Ningún problema! Solo quería señalar que un buen primer paso es tratar de aislar un pequeño ejemplo del problema, para ver si se trata de un problema de lenguaje o de API. –

2

Ese tipo de cosas es un problema bastante común en Tkinter , Creo.

Prueba esto (en el punto apropiado):

def new_command(tag=tag): 
    self.add_tag(tag) 
+0

No sé exactamente por qué funciona, pero lo hace! Aunque me gusta la explicación detallada de la otra respuesta, usaré esta porque es un poco más corta. ¡Gracias! – MatrixFrog

+0

Creo que funciona debido al argumento predeterminado tag = tag en la lista de argumentos de funciones. Hacer esto crea el nombre 'etiqueta' dentro del alcance de la función, apuntando a la etiqueta de valor en el alcance del invocador. Corrígeme si me equivoco, actualmente estoy lidiando con preguntas de alcance similares. –

+0

Básicamente, el constructo 'for x in ..' es uno de los pocos lugares en python donde se rebota un nombre (x). Por lo tanto, debe crear un nuevo nombre que tenga el mismo valor que x en el punto apropiado. Haga esto con 'tag = tag', que se evalúa en la definición de la función y no más tarde en la ejecución de la función. –

0

que tenía una error similar Solo se mostró el último elemento en la lista. Determinada estableciendo

command=lambda x=i: f(x)

Nota del x=i después lambda. Esta asignación hace que su variable local i vaya directamente a la función f(x) de su command. Espero, este simple ejemplo ayudará:

# 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()