Динамическое создание меню в Tkinter.(лямбда-выражения?)

StackOverflow https://stackoverflow.com/questions/728356

  •  05-09-2019
  •  | 
  •  

Вопрос

У меня есть кнопка меню, при нажатии на которую должно отображаться меню, содержащее определенную последовательность строк.Какие именно строки находятся в этой последовательности, мы не знаем до момента выполнения, поэтому всплывающее меню должно быть сгенерировано в этот момент.Вот что у меня есть:

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>")

Важная часть — это то, что находится под «if Menu_tags:». Предположим, что Menu_tags — это список ['stack', 'over', 'flow'].Тогда то, что я хочу сделать, это эффективно:

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)

где add_tag_stack() определяется как:

def add_tag_stack():
    self.add_tag('stack')

и так далее.

Проблема в том, что переменная «тег» принимает значение «стек», а затем значение «более» и так далее, и она не оценивается до тех пор, пока не будет вызвана новая_команда, после чего переменная «тег» будет просто « поток'.Таким образом, добавляемый тег всегда оказывается последним в меню, независимо от того, на что нажимает пользователь.

Изначально я использовал лямбду и подумал, что, возможно, явное определение функции, как указано выше, могло бы работать лучше.В любом случае проблема возникает.Я пробовал использовать копию переменной «тег» (либо с помощью «current_tag = tag», либо с помощью модуля копирования), но это не решило проблему.Я не уверен, почему.

Мои мысли начинают блуждать по таким вещам, как «eval», но я надеюсь, что кто-нибудь сможет придумать умный способ, не связанный с такими ужасными вещами.

Большое спасибо!

(Если это актуально, Tkinter.__version__ возвращает '$Revision:67083 $' и я использую Python 2.6.1 в Windows XP.)

Это было полезно?

Решение

Прежде всего, ваша проблема не имеет ничего общего с Tkinter;Лучше всего, если вы сократите его до простого фрагмента кода, демонстрирующего вашу проблему, чтобы вам было легче с ней экспериментировать.Вот упрощенная версия того, что вы делаете, и с чем я экспериментировал.Я подставляю dict вместо меню, чтобы было проще написать небольшой тестовый пример.

items = ["stack", "over", "flow"]
map = { }

for item in items:
    def new_command():
        print(item)

    map[item] = new_command

map["stack"]()
map["over"]()
map["flow"]()

Теперь, когда мы выполним это, как вы сказали, мы получим:

flow
flow
flow

Проблема здесь в понятии области действия Python.В частности, for заявление не вводит ни новый уровень области видимости, ни новую привязку для item;так что он обновляется одинаково item переменную каждый раз в цикле, и все new_command() функции ссылаются на один и тот же элемент.

Что вам нужно сделать, так это ввести новый уровень области видимости с новой привязкой для каждого из itemс.Самый простой способ сделать это — обернуть его в новое определение функции:

for item in items:
    def item_command(name):
        def new_command():
            print(name)
        return new_command

    map[item] = item_command(item)

Теперь, если вы подставите это в предыдущую программу, вы получите желаемый результат:

stack
over
flow

Другие советы

Я думаю, что подобные вещи — довольно распространенная проблема в Tkinter.

Попробуйте это (в соответствующем месте):

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

У меня была аналогичная ошибка.Был показан только последний элемент в списке.Исправляется установкой

command=lambda x=i: f(x)

Обратите внимание x=i после lambda.Это присвоение делает вашу локальную переменную i иди прямо в f(x) функция вашего command.Надеюсь, этот простой пример поможет:

# 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()
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top