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)

¿Fue útil?

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 items. 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()
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top