Tkinter: set StringVar after the <Key> event, including the key pressed

Every time a character is entered into the Text widget, I want to get the contents of this widget and subtract its length from a certain number (basically, “you have x characters on the left”).

But StringVar() always a single event. This is from what I am collecting, because the event is processed before the character is entered in the Text widget. This means that if I have 3 characters in the field and I enter the 4th, StringVar updated, but still 3 characters, and then it is updated to 4 when entering the 5th character.

Is there a way to keep two lines?

Here is the code. I deleted the unnecessary parts.

 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)) 
+6
source share
2 answers

A simple solution is to add a new bindtag after class binding. This way the class binding will fire before your binding. See this answer to the question. How do I tie myself in a Tkinter text widget after it is bound by a text widget? for example. This answer uses an input widget, not a text widget, but the concept of bindtags is identical between the two widgets. Just remember to use Text , not Entry , where necessary.

Another solution is binding to KeyRelease, since the default bindings occur on KeyPress.

Here is an example showing how to do this with 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() 
+4
source

Like most events in Tk, your <Key> handler fires before the event handles inline bindings, not after. This allows, for example, to prevent normal processing or to change what it does.

But this means that you cannot access the new value (via StringVar or simply by calling entry.get() ) because it has not been updated yet.


If you use Text , a virtual <<Modified>> event appears, which is fired after changing the “changed” flag. Assuming that you are not using this flag for another purpose (for example, in a text editor you can use it to indicate “enable the Save button”), you can use it to accomplish exactly what you want:

 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) 

Usually, when you want something like this, you want Entry , not Text . This provides a much better way to do this: validation. Like everything that is outside the basics of Tkinter, you won’t be able to figure this out without reading the Tcl / Tk docs (so the Docs link to them is Tkinter). Indeed, even Tk docs do not describe validation very well. But here is how it works:

 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 can accept a list of 0 or more arguments to jump to a function. The argument %P gets the new value that the entry will have if you enable it. See VALIDATION in the entry man page for more details.

If you want the entry to be rejected (for example, if you want to block any of more than 140 characters), just return False instead of True .


By the way, it's worth taking a look at the Tk wiki and finding Tkinter recipes on ActiveState . Someone has a good bet for wrappers around Text and Entry that hide all the extra things you have to do for these solutions (or others) to work, so you just need to write the appropriate count method. There may even be a Text wrapper that adds Entry -style validation.

There are several other ways to do this, but they all have flaws.

Add trace to intercept all the entries in StringVar attached to your widget. This will be triggered by any entry in the variable. I guarantee that you will get a problem with an infinite recursive outline the first time you use it for verification, and then you will encounter other more subtle problems in the future. The usual solution is to create a watchdog flag that you check every time you come to the handler to make sure you don't execute it recursively, and then set when you do something that might trigger a recursive event. (This was not necessary for the edit_modified example above, because we could just ignore anyone setting the flag to False , and we only set it to False , so there is no danger of infinite recursion.)

You can get a new char (or multi-char string) from the virtual <Key> event. But then, what will you do with it? You need to know where it will be added, what character it will rewrite, etc. If you don’t do all the work of modeling Entry or, worse, Text eat yourself, this is no better than just len(entry.get()) + 1 .

+3
source

All Articles