Динамическое создание меню в Tkinter.(лямбда-выражения?)
Вопрос
У меня есть кнопка меню, при нажатии на которую должно отображаться меню, содержащее определенную последовательность строк.Какие именно строки находятся в этой последовательности, мы не знаем до момента выполнения, поэтому всплывающее меню должно быть сгенерировано в этот момент.Вот что у меня есть:
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()