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')
등등.
문제는 'tag' 변수가 'stack' 값과 'over' 값 등을 취하고 new_command가 호출될 때까지 평가되지 않는다는 것입니다. 이때 변수 'tag'는 ' 흐름'.따라서 추가되는 태그는 사용자가 무엇을 클릭하든 상관없이 항상 메뉴의 마지막 태그입니다.
나는 원래 람다를 사용하고 있었고 위와 같이 함수를 명시적으로 정의하는 것이 더 나을 것이라고 생각했습니다.어느 쪽이든 문제가 발생합니다.변수 'tag'의 복사본을 사용해 보았지만("current_tag = tag"를 사용하거나 복사 모듈을 사용하여) 문제가 해결되지 않습니다.왜 그런지 잘 모르겠습니다.
내 마음은 "평가"와 같은 쪽으로 방황하기 시작했지만 누군가가 그런 끔찍한 일을 포함하지 않는 영리한 방법을 생각해 낼 수 있기를 바랍니다.
정말 감사합니다!
(관련된 경우 Tkinter.__version__은 '$Revision:67083 $' 그리고 저는 Windows XP에서 Python 2.6.1을 사용하고 있습니다.)
해결책
우선, 당신의 문제는 Tkinter와 아무 관련이 없습니다.문제를 보여주는 간단한 코드 조각으로 줄여서 더 쉽게 실험해 볼 수 있도록 하는 것이 가장 좋습니다.다음은 제가 실험한 작업의 단순화된 버전입니다.작은 테스트 사례를 쉽게 작성할 수 있도록 메뉴 대신 사전을 대체하고 있습니다.
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()