为什么从lambda调用tkSimpleDialog.askstring时Tkinter会卡住?
我正在开发一个图形界面应用程序,用来模拟写论文。在这个应用中,用户可以创建一个新主题,然后在这个主题下添加笔记。目前,我有两种创建新主题的方法:一种是通过菜单中的下拉选项,另一种是通过主屏幕上的按钮。这个按钮一开始的文本是“新主题”。当用户点击这个按钮时,程序会创建一个新主题,询问用户用 tkSimpleDialog.askstring
来命名这个主题,然后把按钮的文本改成主题的名字和这个主题下笔记的数量。按钮的功能也会变成添加笔记到这个主题。
在开发这个程序时,我首先确认了菜单命令可以正常工作。它成功调用了 askstring
,弹出了一个我想要的输入窗口。然而,当我添加了按钮命令后,调用 askstring
就失败了,即使是通过菜单命令调用也是如此。应该弹出的对话框窗口变成了白色,程序也卡住了。如果我把按钮命令注释掉,它又能正常工作。如果我把菜单命令注释掉,程序又会卡住。
这是我添加菜单命令的代码:
TopicBtn.menu.add_command(label="New Topic", underline=0,
command=self.newTopic)
这是 newTopic() 的代码:
def newTopic(self, button=None):
""" Create a new topic. If a Button object is passed, associate that Button
with the new topic. Otherwise, create a new Button for the topic. """
topicPrompt = "What would you like to call your new topic?"
topicName = tkSimpleDialog.askstring("New Topic", topicPrompt)
if topicName in self.topics.keys():
print "Error: topic already exists"
else:
newTopic = {}
newTopic["name"] = topicName
newTopic["notes"] = []
newTopic["button"] = self.newTopicButton(newTopic, button)
self.topics[topicName] = newTopic
self.addToTopicLists(newTopic)
这是 newTopicButton() 的代码:
def newTopicButton(self, topic, button=None):
""" If a Button object is passed, change its text to display the topic name.
Otherwise, create and grid a new Button with the topic name. """
if button is None:
button = Button(self.topicFrame)
index = len(self.topics)
button.grid(row=index/self.TOPICS_PER_ROW, column=(index %
self.TOPICS_PER_ROW), sticky=NSEW, padx=10, pady=10)
else:
button.unbind("<Button-1>")
buttonText = "%s\n0 notes" % topic["name"]
button.config(text=buttonText)
button.config(command=(lambda s=self, t=topic: s.addNoteToTopic(t)))
return button
最后,这是按钮命令的代码:
for col in range(self.TOPICS_PER_ROW):
button = Button(self.topicFrame, text="New Topic")
button.bind("<Button-1>", (lambda e, s=self: s.newTopic(e.widget)))
button.grid(row=0, column=col, sticky=NSEW, padx=10, pady=10)
有没有人知道为什么把 lambda 表达式绑定到按钮上会导致 askstring
卡住?
编辑:感谢大家的评论。这是一个最小的示例,展示了这个问题:
from Tkinter import *
import tkSimpleDialog
class Min():
def __init__(self, master=None):
root = master
frame = Frame(root)
frame.pack()
button = Button(frame, text="askstring")
button.bind("<Button-1>", (lambda e, s=self: s.newLabel()))
button.grid()
def newLabel(self):
label = tkSimpleDialog.askstring("New Label", "What should the label be?")
print label
root = Tk()
m = Min(root)
root.mainloop()
注意,从 button.bind("<Button-1>", (lambda e, s=self: s.newLabel()))
切换到 button = Button(frame, text="askstring", command=(lambda s=self: s.newLabel()))
可以解决这个bug(但这样就无法获取被点击的按钮的引用)。我觉得问题可能和将事件作为 lambda 的输入之一有关。
2 个回答
我找到了一种解决方法。从我做的简单测试来看,问题出在单独调用绑定函数,这样就把事件当作输入传给了lambda函数。如果有人能解释为什么会这样,我会更倾向于接受他们的答案,不过现在我先接受这个。
这个解决方法是,不要使用单独的绑定函数,而是创建一个按钮数组,然后把数组中正确的元素作为参数传给lambda函数(你不能直接传按钮本身,因为按钮是在包含lambda函数的那一行创建的)。
下面是代码:
from Tkinter import *
import tkSimpleDialog
class Min():
def __init__(self, master=None):
root = master
frame = Frame(root)
frame.pack()
buttons = [None] * 2
for i in range (2):
buttons[i] = Button(frame, text="askstring",
command=(lambda s=self, var=i: s.newLabel(buttons[var])))
buttons[i].grid()
def newLabel(self, button):
label = tkSimpleDialog.askstring("New Label", "What should the label be?")
button.config(text=label)
print label
root = Tk()
m = Min(root)
root.mainloop()
你遇到的问题是因为你使用的对话框里调用了 wait_window
这个函数(虽然你自己没有调用,但实现这个对话框的代码里有)。比如,下面的代码在点击两次按钮后就会出现这个问题:
import Tkinter
def test(event=None):
tl = Tkinter.Toplevel()
tl.wait_window(tl)
root = Tkinter.Tk()
btn = Tkinter.Button(text=u'hi')
btn.bind('<Button-1>', test)
btn.pack(padx=10, pady=10)
root.mainloop()
这个 wait_window
的调用实际上和 update
命令的作用差不多,它是一个典型的例子,说明为什么调用 update
是不好的做法。因为它和正在处理的 <Button-1>
事件发生了冲突,导致程序卡住。问题在于,你必须接受 wait_window
被使用的事实,因为它是对话框代码的一部分。显然,如果你绑定到 <ButtonRelease-1>
事件,这种冲突就不会发生。你也可以在按钮中使用 command
参数,这样也能正常工作。
最后,我建议你根据想要实现的效果,以更简洁的方式来创建按钮:
for i in range(X):
btn = Tkinter.Button(text=u'%d' % i)
btn['command'] = lambda button=btn: some_callback(button)