Динамическое создание меню в Tkinter. (лямбда-выражения?) - PullRequest
6 голосов
/ 08 апреля 2009

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

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

и т. Д.

Проблема в том, что переменная 'tag' принимает значение 'stack', а затем значение 'over' и т. Д., И она не оценивается, пока не будет вызвана команда new_command, после чего переменная 'tag' это просто «поток». Таким образом, добавляемый тег всегда является последним в меню, независимо от того, на что нажимает пользователь.

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

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

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

(В случае необходимости Tkinter .__ version__ возвращает '$ Revision: 67083 $', и я использую Python 2.6.1 в Windows XP.)

Ответы [ 3 ]

7 голосов
/ 08 апреля 2009

Прежде всего, ваша проблема не имеет ничего общего с 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 s. Самый простой способ сделать это - обернуть его в новое определение функции:

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

    map[item] = item_command(item)

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

stack
over
flow
2 голосов
/ 08 апреля 2009

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

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

def new_command(tag=tag):
    self.add_tag(tag)
0 голосов
/ 14 декабря 2017

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

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()
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...