Tkinter:在事件发生后设置StringVar <Key> ,包括按下的键



每次在Text小部件中输入字符时,我都想获得该小部件的内容并从某个数字中减去其长度(基本上是"您还剩下x个字符"交易)。

但是StringVar()总是落后于一个事件。据我所知,这是因为事件是在字符输入到Text小部件之前处理的。这意味着,如果我在字段中有3个字符,我输入第4个字符,StringVar被更新,但仍然是3个字符长,然后当我输入第5个字符时,它更新为4。

有办法使两者保持一致吗?

下面是一些代码。我删除了不相关的部分。

def __init__(self, master):
    self.char_count = StringVar()
    self.char_count.set("140 chars left")
    self.post_tweet = Text(self.master)
    self.post_tweet.bind("<Key>", self.count)
    self.post_tweet.grid(...)
    self.char_count = Label(self.master, textvariable=self.foo)
    self.char_count.grid(...)
def count(self):
    self.x = len(self.post_tweet.get(1.0, END))
    self.char_count.set(str(140 - self.x))

一个简单的解决方案是在类绑定之后添加一个新的bindtag。这样,类绑定将在绑定之前触发。看到这个问题的答案如何绑定自事件在Tkinter文本小部件后,它将由文本小部件绑定?举个例子。这个答案使用条目小部件而不是文本小部件,但是这两个小部件之间的bindtags概念是相同的。只是要确保在适当的地方使用Text而不是Entry

另一个解决方案是绑定在KeyRelease上,因为默认绑定发生在KeyPress上。

下面的例子展示了如何使用bindtags:

import Tkinter as tk
class Example(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.post_tweet = tk.Text(self)
        bindtags = list(self.post_tweet.bindtags())
        bindtags.insert(2, "custom") # index 1 is where most default bindings live
        self.post_tweet.bindtags(tuple(bindtags))
        self.post_tweet.bind_class("custom", "<Key>", self.count)
        self.post_tweet.grid()
        self.char_count = tk.Label(self)
        self.char_count.grid()
    def count(self, event):
        current = len(self.post_tweet.get("1.0", "end-1c"))
        remaining = 140-current
        self.char_count.configure(text="%s characters remaining" % remaining)
if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(side="top", fill="both", expand=True)
    root.mainloop()

像Tk中的大多数事件一样,您的<Key>处理程序在事件被内置绑定处理之前(而不是之后)触发。例如,这允许您阻止正常处理的发生,或更改其所做的工作。

但是这意味着你不能访问新值(无论是通过StringVar,还是仅仅通过调用entry.get()),因为它还没有更新。


如果你使用的是Text,有一个虚拟事件<<Modified>>在"modified"标志改变后被触发。假设您没有将该标志用于其他目的(例如,在文本编辑器中,您可能希望使用它来表示"启用保存按钮"),您可以使用它来做您想做的事情:

def count(self, event=None):
    if not self.post_tweet.edit_modified():
        return
    self.post_tweet.edit_modified(False)
    self.x = len(self.post_tweet.get(1.0, END))
    self.char_count.set(str(140 - self.x))
# ...
self.post_tweet.bind("<<Modified>>", self.count)

通常,当你想要这样的东西时,你想要一个Entry而不是Text。它提供了一种更好的方法:验证。就像Tkinter中的所有基础知识一样,如果不阅读Tcl/Tk文档(这就是为什么Tkinter文档链接到它们),您将无法弄清楚这一点。实际上,即使是Tk文档也没有很好地描述验证。下面是它的工作原理:
def count(self, new_text):
    self.x = len(new_text)
    self.char_count.set(str(140 - self.x))
    return True
# ...
self.vcmd = self.master.register(self.count)
self.post_tweet = Edit(self.master, validate='key',
                       validatecommand=(self.vcmd, '%P'))

validatecommand可以接受一个包含0个或多个参数的列表来传递给函数。如果允许的话,%P参数获取该条目将具有的新值。有关详细信息,请参阅条目手册中的VALIDATION

如果您希望条目被拒绝(例如,如果您想实际阻止某人输入超过140个字符),只需返回False而不是True


顺便说一下,在Tk wiki上搜索Tkinter食谱是值得的。很有可能有人在TextEntry周围进行了包装,隐藏了您为使这些解决方案(或其他解决方案)工作而必须做的所有额外工作,因此您只需要编写适当的count方法。甚至可能是一个Text包装器,它添加了Entry样式的验证。

还有其他一些方法可以做到这一点,但它们都有缺点。

添加一个trace,将所有写操作挂接到您的小部件上的StringVar。这将被触发,任何写入变量。我保证,当您第一次尝试使用它进行验证时,您将遇到无限递归循环问题,然后您将在将来遇到其他更微妙的问题。通常的解决方案是创建一个哨兵标志,每次进入处理程序时检查它,以确保没有递归地执行它,然后在执行任何可能触发递归事件的操作时设置它。(对于上面的edit_modified示例,这是不必要的,因为我们可以忽略任何将标志设置为False的人,我们只将其设置为False,因此没有无限递归的危险。)

您可以从<Key>虚拟事件中获得新的字符(或多字符字符串)。但是,你用它做什么呢?你需要知道它将被添加到哪里,它将覆盖哪个字符,等等。如果您自己不做所有的工作来模拟Entry——或者更糟的是,模拟Text——编辑,那么这并不比只做len(entry.get()) + 1好。

最新更新