6

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.

Dan Getz
  • 8,774
  • 6
  • 30
  • 64
  • As your entry is supposed to be linked to a StringVar, why don't you set a valid attribute on the var, and then adjust the display of the entry depending on the StringVar value state. – Arthur Vaïsse May 20 '15 at 07:30
  • @ArthurVaïsse I don't understand how and when you think I should adjust the entry's display from your comment. Maybe you could elaborate in an answer? – Dan Getz May 20 '15 at 18:23

3 Answers3

4

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()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • This works for me if I replace the style's `background=` with `fieldbackground=`. Is that a typo, or are the style attributes not standardized? – Dan Getz May 21 '15 at 15:59
  • Also, do you know why binding `` and `` are necessary? This seems to work fine, but understanding what's really happening would help me to trust that this kind of solution will always work right. Am I misunderstanding the man page's description of built-in validation? – Dan Getz May 21 '15 at 16:10
  • @DanGetz: I do not know why they are necessary. My guess is that the ttk widget has some internal functionality that is not configurable. I didn't see any focus related bindings on the TButton class so I don't know what is causing the state to change. I don't currently have tcl/tk source code on my box to look. Sadly, the ttk widgets are a little under-documented. – Bryan Oakley May 21 '15 at 16:14
  • Alright, style attributes like `background` and `fieldbackground` are unfortunately **not** standardized, according to [this other question](http://stackoverflow.com/q/17635905). That must be why the code works with one attribute for you and with another attribute for me. – Dan Getz May 21 '15 at 19:02
3

Why it happens

In the Tk source file generic/ttk/ttkEntry.c, EntryRevalidate() is always run on focus events. This calls EntryValidateChange(), which, when it notices that validation on focus events is "turned off", returns a result indicating that the current value is valid to EntryRevalidate(), which accordingly clears the invalid flag.

So, as it's currently implemented, the invalid flag does not persist through focus events. "No revalidation" really means "instead of doing revalidation, clear the invalid flag".

A solution

If you can't beat them, join them. Use validate='focus' with a validatecommand that returns whether or not the text is valid, as if you had wanted to check this on focus events all along. And continue to set the invalid flag when something really changes. For example:

class MyEntry(ttk.Entry):

    def __init__(self, master):
        self.var = tk.StringVar(master)
        super().__init__(master,
                         textvariable=self.var,
                         validate='focus',
                         validatecommand=self.is_valid)
        self.var.trace('w', self.revalidate)
        self.revalidate()

    def revalidate(self, *args):
        self.state(['!invalid' if self.is_valid()
                    else 'invalid'])

    def is_valid(self, *args):
        # ... return if current text is valid ...

Another (similar) solution

You could instead simply persist the state flag across focus events:

class ValidationDisabledEntry(ttk.Entry):
    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            validate='focus',
            validatecommand=lambda *a: 'invalid' not in self.state(),
            **kwargs)

and then set or clear the invalid flag whenever you wish, without worrying about focus events clearing it. Setting validate and validatecommand as above should (and seem to) implement the behavior that I had thought validate='none' would get me.

Dan Getz
  • 8,774
  • 6
  • 30
  • 64
-2

Here is a solution to your problem. You may probably want to adapt it to your need but it may give you an idea :

import tkinter as tk
from tkinter import ttk

class ValidatingEntry(ttk.Entry):

    COLOR_VALID = "#99ff99"
    COLOR_INVALID="#ff9999"

    def __init__(self, master, *args, **kwargs):
        self.stringVar = tk.StringVar()

        tk.Entry.__init__(self, master, *args, textvariable = self.stringVar, **kwargs)

        self.validatingFunction = None

        self.bind("<FocusOut>",self.validation)

    def validation(self, event):

        if self.validatingFunction != None:
            if self.validatingFunction():
                self['bg']=ValidatingEntry.COLOR_VALID
            else:
                self['bg']=ValidatingEntry.COLOR_INVALID
        else:
            print("no evaluation possible for the content of this entry")


if __name__ == "__main__":
    app = tk.Tk()

    entry = ValidatingEntry(app)
    entry.validatingFunction = lambda : 'foo' in entry.stringVar.get()
    entry.pack()

    entry2 = ValidatingEntry(app)
    entry2.validatingFunction = lambda : 'bar' in entry2.stringVar.get()
    entry2.pack()


    app.mainloop()

It is probably possible to modify the class to get a pattern parameter and use it to check the StringVar content match the pattern as a regexp for example. But this is not anymore TK related.

Hope it helps. Arthur.

Arthur Vaïsse
  • 1,551
  • 1
  • 13
  • 26
  • 1
    This solution doesn't use a ttk entry, and the question is explicitly about ttk entry widgets. – Bryan Oakley May 21 '15 at 10:56
  • Hum just had to replace `tk.Entry` in the class by `ttk.Entry` as ttk widgets are extension of tk's ones. Thanks to pointed out the mistake @Bryan Oakley ^^ NB : I don't think this deserve a downvote but okay... – Arthur Vaïsse May 21 '15 at 12:25
  • You are missing the point. Ttk entry widgets have a completely different way of managing and visualizing state. The question is specifically about this different way of managing state. Your answer isn't a solution to the problem, it's a workaround. It may be a valid workaround, but it doesn't address the actual question. – Bryan Oakley May 21 '15 at 12:35
  • My understanding of the question was "I'm tring to achieve something, but [I'm not using the built-in validation capabilities of this widget] as it do not act as I want. Do someone know how to make it work ?` He so asked in a Stack Overflow Fashion a concise question that show he try something and propose an approach, incomplete and the limits. I so propose another approach relying on the standard attributes of the widget. I may be wrong. But this is my opinion. Anyway, thanks you for your reply Bryan. – Arthur Vaïsse May 21 '15 at 12:53
  • 1
    This only works because you're not using `ttk`. You're calling `tk.Entry.__init__`, so you're really getting the behavior of a `tkinter.Entry`, not a `tkinter.ttk.Entry`. If you "fix" that other line of the code, it won't work any more, because setting the background color of a ttk entry can't be done the same way as for a normal tk entry. – Dan Getz May 21 '15 at 15:54