为什么从lambda调用tkSimpleDialog.askstring时Tkinter会卡住?

2 投票
2 回答
1898 浏览
提问于 2025-04-17 14:15

我正在开发一个图形界面应用程序,用来模拟写论文。在这个应用中,用户可以创建一个新主题,然后在这个主题下添加笔记。目前,我有两种创建新主题的方法:一种是通过菜单中的下拉选项,另一种是通过主屏幕上的按钮。这个按钮一开始的文本是“新主题”。当用户点击这个按钮时,程序会创建一个新主题,询问用户用 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 个回答

0

我找到了一种解决方法。从我做的简单测试来看,问题出在单独调用绑定函数,这样就把事件当作输入传给了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()
1

你遇到的问题是因为你使用的对话框里调用了 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)

撰写回答