Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I ensure my ttk.Entry's invalid state isn't cleared when it loses focus?

I would like to set or clear the invalid state flag of a ttk.Entry whenever the contents change. I'm doing this by connecting a StringVar to the entry, and in a trace() callback, calling state(['valid']) or state(['!invalid']). The flag is correctly set by my callback, but then, whenever the focus leaves the text entry, it's cleared! How can I avoid or work around this?

I want to set or clear the flag because I can change the visual style based on the state flags. I don't want to disallow the user from typing anything invalid; I want them to be free to type whatever they want, and see immediately if it's valid or not. I want to use the invalid flag specifically, not the alternate flag, not only because invalid is the more logical choice, but also because I'm already using the alternate flag for something else.

I'm not using the built-in validation capabilities of this widget, because, according to the Tk docs, if I invoke the validation command whenever the text is edited (-validate equal to 'keys' or 'all'),

The entry will be prevalidated prior to each edit ... If prevalidation fails, the edit is rejected.

Like I said before, I don't want that. I want what -validate equal to 'none' is supposed to do:

validation will only occur when specifically requested by the validate widget command.

Great, so in theory all I have to do is never call validate(). Unfortunately, the invalid flag is being cleared anyway. I can reproduce this unexpected and unwanted behavior in Python's interactive mode:

>>> import tkinter as tk
>>> from tkinter import ttk
>>> win = tk.Tk()
>>> entry = ttk.Entry(win)
>>> entry.pack()
>>> entry['validate']
'none'
>>> entry.state()
()
>>> entry.state(['invalid'])
('!invalid',)
>>> entry.state()
('invalid',)

So far, so good. (I'm using Python 3 in this example, but I get the same results with Python 2.) Now I change focus to and from my entry box, and:

>>> entry.state()
()

Why is it getting cleared, when -validate is 'none', not 'focus' or 'all'? Is there anything I can do to use the invalid state for my purposes?

I see this same behavior with both Python 3.4.2 and 2.7.9, using Tcl/Tk version 8.6, on Linux.

like image 656
Dan Getz Avatar asked May 19 '15 22:05

Dan Getz


1 Answers

Add your own binding to <FocusOut> which calls your validation function and resets the state.

Here's a complete working example. If the entry widget contains the word "invalid", the state will be changed to "invalid". You can then click out of the widget to see that the state remains invalid:

try:
    import Tkinter as tk
    import ttk
except ImportError:
    import tkinter as tk
    from tkinter import ttk

class Example(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        # give invalid entries a red background
        style = ttk.Style()
        style.map("TEntry",  background=[('invalid', "red")])

        self.entryVar = tk.StringVar()
        self.entry = ttk.Entry(self, textvariable=self.entryVar)

        # this label will show the current state, updated 
        # every second.
        self.label = tk.Label(self, anchor="w")
        self.after_idle(self.updateLabel)

        # layout the widgets
        self.entry.pack(side="top", fill="x")
        self.label.pack(side="bottom", fill="x")

        # add trace on the variable to do custom validation
        self.entryVar.trace("w", self.validate)

        # set up bindings to also do the validation when we gain
        # or lose focus
        self.entry.bind("<FocusIn>", self.validate)
        self.entry.bind("<FocusOut>", self.validate)

    def updateLabel(self):
        '''Display the current entry widget state'''
        state = str(self.entry.state())
        self.label.configure(text=state)
        self.after(1000, self.updateLabel)

    def validate(self, *args):
        '''Validate the widget contents'''
        value = self.entryVar.get()
        if "invalid" in value:
            self.entry.state(["invalid"])
        else:
            self.entry.state(["!invalid"])

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()
like image 123
Bryan Oakley Avatar answered Sep 21 '22 12:09

Bryan Oakley