Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

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

Is there a way to keep the two in line?

Here's some code. I removed irrelevant 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))
like image 610
Rory Byrne Avatar asked Apr 02 '13 18:04

Rory Byrne


2 Answers

A simple solution is to add a new bindtag after the class binding. That way the class binding will fire before your binding. See this answer to the question How to bind self events in Tkinter Text widget after it will binded by Text widget? for an example. That answer uses an entry widget rather than a text widget, but the concept of bindtags is identical between those two widgets. Just be sure to use Text rather than Entry where appropriate.

Another solution is to bind on KeyRelease, since the default bindings happen on KeyPress.

Here's an example showing how to do it 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()
like image 101
Bryan Oakley Avatar answered Nov 16 '22 21:11

Bryan Oakley


Like most events in Tk, your <Key> handler is fired before the event is processed by the built-in bindings, rather than after. This allows you to, for example, prevent the normal processing from happening, or change what it does.

But this means that you can't access the new value (whether via a StringVar, or just by calling entry.get()), because it hasn't been updated yet.


If you're using Text, there's a virtual event <<Modified>> that gets fired after the "modified" flag changes. Assuming you weren't using that flag for another purpose (e.g., in a text editor, you might want to use it to mean "enable the Save button"), you can use it to do 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 an Entry rather than a Text. Which provides a much nicer way to do this: validation. As with everything beyond the basics in Tkinter, there's no way you're going to figure this out without reading the Tcl/Tk docs (which is why the Tkinter docs link to them). And really, even the Tk docs don't describe validation very well. But here's 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'))

The validatecommand can take a list of 0 or more arguments to pass to the function. The %P argument gets the new value the entry will have if you allow it. See VALIDATION in the Entry manpage for more details.

If you want the entry to be rejected (e.g., if you want to actually block someone from entering more than 140 characters), just return False instead of True.


By the way, it's worth looking over the Tk wiki and searching for Tkinter recipes on ActiveState. It's a good bet someone's got wrappers around Text and Entry that hide all the extra stuff you have to do to make these solutions (or others) work so you just have to write the appropriate count method. There might even be a Text wrapper that adds Entry-style validation.

There are a few other ways you could do this, but they all have downsides.

Add a trace to hook all writes to a StringVar attached to your widget. This will get fired by any writes to the variable. I guarantee that you will get the infinite-recursive-loop problem the first time you try to use it for validation, and then you'll run into other more subtle problems in the future. The usual solution is to create a sentinel flag, which you check every time you come into the handler to make sure you're not doing it recursively, and then set while you're doing anything that can trigger a recursive event. (That wasn't 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's no danger of infinite recursion.)

You can get the new char (or multi-char string) out of the <Key> virtual event. But then, what do you do with it? You need to know where it's going to be added, which character(s) it's going to be overwriting, etc. If you don't do all the work to simulate Entry—or, worse, Text—editing yourself, this is no better than just doing len(entry.get()) + 1.

like image 39
abarnert Avatar answered Nov 16 '22 22:11

abarnert