Interactively validating Entry widget content in tkinter
Asked Answered
L

11

116

What is the recommended technique for interactively validating content in a tkinter Entry widget?

I've read the posts about using validate=True and validatecommand=command, and it appears that these features are limited by the fact that they get cleared if the validatecommand command updates the Entry widget's value.

Given this behavior, should we bind on the KeyPress, Cut, and Paste events and monitor/update our Entry widget's value through these events? (And other related events that I might have missed?)

Or should we forget interactive validation altogether and only validate on FocusOut events?

Loughlin answered 10/11, 2010 at 1:24 Comment(0)
I
288

The correct answer is, use the validatecommand attribute of the widget. Unfortunately this feature is severely under-documented in the Tkinter world, though it is quite sufficiently documented in the Tk world. Even though it's not documented well, it has everything you need to do validation without resorting to bindings or tracing variables, or modifying the widget from within the validation procedure.

The trick is to know that you can have Tkinter pass in special values to your validate command. These values give you all the information you need to know to decide on whether the data is valid or not: the value prior to the edit, the value after the edit if the edit is valid, and several other bits of information. To use these, though, you need to do a little voodoo to get this information passed to your validate command.

Note: it's important that the validation command returns either True or False. Anything else will cause the validation to be turned off for the widget.

Here's an example that only allows lowercase. It also prints the values of all of the special values for illustrative purposes. They aren't all necessary; you rarely need more than one or two.

import tkinter as tk  # python 3.x
# import Tkinter as tk # python 2.x

class Example(tk.Frame):

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

        # valid percent substitutions (from the Tk entry man page)
        # note: you only have to register the ones you need; this
        # example registers them all for illustrative purposes
        #
        # %d = Type of action (1=insert, 0=delete, -1 for others)
        # %i = index of char string to be inserted/deleted, or -1
        # %P = value of the entry if the edit is allowed
        # %s = value of entry prior to editing
        # %S = the text string being inserted or deleted, if any
        # %v = the type of validation that is currently set
        # %V = the type of validation that triggered the callback
        #      (key, focusin, focusout, forced)
        # %W = the tk name of the widget

        vcmd = (self.register(self.onValidate),
                '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
        self.entry = tk.Entry(self, validate="key", validatecommand=vcmd)
        self.text = tk.Text(self, height=10, width=40)
        self.entry.pack(side="top", fill="x")
        self.text.pack(side="bottom", fill="both", expand=True)

    def onValidate(self, d, i, P, s, S, v, V, W):
        self.text.delete("1.0", "end")
        self.text.insert("end","OnValidate:\n")
        self.text.insert("end","d='%s'\n" % d)
        self.text.insert("end","i='%s'\n" % i)
        self.text.insert("end","P='%s'\n" % P)
        self.text.insert("end","s='%s'\n" % s)
        self.text.insert("end","S='%s'\n" % S)
        self.text.insert("end","v='%s'\n" % v)
        self.text.insert("end","V='%s'\n" % V)
        self.text.insert("end","W='%s'\n" % W)

        # Disallow anything but lowercase letters
        if S == S.lower():
            return True
        else:
            self.bell()
            return False

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()

For more information about what happens under the hood when you call the register method, see Why is calling register() required for tkinter input validation?

For the canonical documentation see the Validation section of the Tcl/Tk Entry man page

Inkster answered 10/11, 2010 at 3:29 Comment(19)
This is the right way to do it. It addresses the problems I found when I tried to get jmeyer10's answer working. This one example provides superior documentation to validate compared to what I can find elsewhere. I wish I could give this 5 votes.Eat
WOW! I agree with Steven - this is the type of reply that deserves more than one vote. You should write a book on Tkinter (and you've already posted enough solutions to make that a multi-volume series). Thank you!!!Loughlin
As a side note: I'm blown away by the power of Tkinter. Its gotten a bad wrap for many years, but with the new support for native themes (ttk) and explanations of its hidden power powers by experts like Bryan Oakley, this GUI framework can hold its own against the likes of wxPython, pyQT, and others.Loughlin
Thanks for the example. It's worth noting that the validatecommand MUST return a boolean (only True and False). If not, the validation will be removed.Precarious
self.text = tk.Text(self, height=10, width=40) I have a question about this code 7 years later~ why the component of self can be constructed with the parent MASTER as self itself???? Shouldn't it like this self.text = tk.Text(parent, height=10, width=40)Laurielaurier
@MMMMMCCLXXVII: because self is a frame and the text widget is a child of that frame. parent is the parent of self.Inkster
@BryanOakley I see!! Another question. If I re-write your code tk.Frame.__init__(self, parent) to super(Example,self).__init__(parent), it will throw an error TypeError: super() argument 1 must be type, not classobj. Why is that? How to use super in your case?Laurielaurier
@MMMMMCCLXXVII: stackoverflow isn't a discussion site. If you have questions, please click the "Ask Question" button.Inkster
The example works flawless. However I fail to integrate this into my code. I get AttributeError: 'MyClass' object has no attribute 'register'. Well the error is for sure linked to that 'self'. When I dump it in the example it says "... instance at 0x..." while in mycode it is just ",.. object at 0x...". So seem that voodoo-part of the code only work for code in instances and not for 'static' code. Gosh I guess need do an other round in learning about OOP in Python.Impinge
@Nadu: no, there's no special voodoo. register is a common method on all widgets. In this example self is a Frame widget. You can use the register method of any widget (eg: root.register(...))Inkster
I think this page should be brought to the fore.Ascarid
"severely under-documented in the Tkinter world". LOL — like almost all of the rest of the Tkiinter world.Immeasurable
@Rightleg That page doesn't exist anymore. Archived version: web.archive.org/web/20190423043443/http://infohost.nmt.edu/tcc/…Shulock
Will we have to add validation to each widget if we wanted validation for multiple entry widgets?Kimber
@CoolCloud: I'm not sure what you mean. You have to configure the validation options for each widget, though you could probably use the option database to specify defaults. You can also create your own custom Entry class that does it automatically.Inkster
@BryanOakley Oh rather than applying validation to all class, i think making a class with validation is better, Thanks !Kimber
How can the '%W' option be used? Anyway to map the name to a tkinter widget variable (which could be useful)?Immeasurable
@martineau: you can use the common nametowidget method to convert the name to the actual widget object.Inkster
Bryan: Thank you. Eventually on my own I found some info on that method — knew something like nametowidget() existed but couldn't remember its name nor exactly how to use it.Immeasurable
L
29

After studying and experimenting with Bryan's code, I produced a minimal version of input validation. The following code will put up an Entry box and only accept numeric digits.

from tkinter import *

root = Tk()

def testVal(inStr,acttyp):
    if acttyp == '1': #insert
        if not inStr.isdigit():
            return False
    return True

entry = Entry(root, validate="key")
entry['validatecommand'] = (entry.register(testVal),'%P','%d')
entry.pack()

root.mainloop()

Perhaps I should add that I am still learning Python and I will gladly accept any and all comments/suggestions.

Lorikeet answered 22/2, 2016 at 13:16 Comment(3)
Generally people use entry.configure(validatecommand=...) and write test_val instead of testVal, but this is a good, simple example.Micro
Wouldn't you need to also allow a decimal point? ".".isdigit()FalseImmeasurable
@Immeasurable The answerer intended this code to only accept "numeric digits". This intentionally doesn't include the ".". "." is not a numeric digit. The code wasn't meant to accept all numbers, it was meant to accept "numeric digits".Twister
E
14

Use a Tkinter.StringVar to track the value of the Entry widget. You can validate the value of the StringVar by setting a trace on it.

Here's a short working program that accepts only valid floats in the Entry widget.

try:
    from tkinter import *
except ImportError:
    from Tkinter import *  # Python 2


root = Tk()
sv = StringVar()

def validate_float(var):
    new_value = var.get()
    try:
        new_value == '' or float(new_value)
        validate_float.old_value = new_value
    except:
        var.set(validate_float.old_value)

validate_float.old_value = ''  # Define function attribute.

# trace wants a callback with nearly useless parameters, fixing with lambda.
sv.trace('w', lambda nm, idx, mode, var=sv: validate_float(var))
ent = Entry(root, textvariable=sv)
ent.pack()
ent.focus_set()

root.mainloop()

Eat answered 10/11, 2010 at 2:47 Comment(8)
Thanks for your post. I enjoyed seeing the Tkinter StringVar .trace() method in use.Loughlin
any idea why I could possibly get this error? "NameError: name 'validate' is not defined"Pyemia
@ArmenSanoyan: It's because validate is not defined in this snippet (and that should be corrected).Immeasurable
That's a really great idea in some cases but what about fixing the indicated issue around the validate variable?Roccoroch
@Wolf: See update I made since answer's author does not appear interested in fixing it themselves…Immeasurable
Thanks, @martineau, for taking up the cause.Roccoroch
@Wolf: While fixing things here I noticed some other deficiencies and decided to post an answer of my own to address them.Immeasurable
@Immeasurable Thanks for letting me know. Posting this implementation of the core idea makes sense.Roccoroch
S
8

Bryan's answer is correct, however no one mentioned the 'invalidcommand' attribute of the tkinter widget.

A good explanation is here: http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html

Text copy/pasted in case of broken link

The Entry widget also supports an invalidcommand option that specifies a callback function that is called whenever the validatecommand returns False. This command may modify the text in the widget by using the .set() method on the widget's associated textvariable. Setting up this option works the same as setting up the validatecommand. You must use the .register() method to wrap your Python function; this method returns the name of the wrapped function as a string. Then you will pass as the value of the invalidcommand option either that string, or as the first element of a tuple containing substitution codes.

Note: There is only one thing that I cannot figure out how to do: If you add validation to an entry, and the user selects a portion of the text and types a new value, there is no way to capture the original value and reset the entry. Here's an example

  1. Entry is designed to only accept integers by implementing 'validatecommand'
  2. User enters 1234567
  3. User selects '345' and presses 'j'. This is registered as two actions: deletion of '345', and insertion of 'j'. Tkinter ignores the deletion and acts only on the insertion of 'j'. 'validatecommand' returns False, and the values passed to the 'invalidcommand' function are as follows: %d=1, %i=2, %P=12j67, %s=1267, %S=j
  4. If the code does not implement an 'invalidcommand' function, the 'validatecommand' function will reject the 'j' and the result will be 1267. If the code does implement an 'invalidcommand' function, there is no way to recover the original 1234567.
Staurolite answered 20/11, 2018 at 22:39 Comment(0)
I
7

Define a function returning a boolean that indicates whether the input is valid.
Register it as a Tcl callback, and pass the callback name to the widget as a validatecommand.

For example:

import tkinter as tk


def validator(P):
    """Validates the input.

    Args:
        P (int): the value the text would have after the change.

    Returns:
        bool: True if the input is digit-only or empty, and False otherwise.
    """

    return P.isdigit() or P == ""


root = tk.Tk()

entry = tk.Entry(root)
entry.configure(
    validate="key",
    validatecommand=(
        root.register(validator),
        "%P",
    ),
)
entry.grid()

root.mainloop()

Reference.

Isoelectronic answered 16/6, 2019 at 7:46 Comment(0)
C
5

While studying Bryan Oakley's answer, something told me that a far more general solution could be developed. The following example introduces a mode enumeration, a type dictionary, and a setup function for validation purposes. See line 48 for example usage and a demonstration of its simplicity.

#! /usr/bin/env python3
# https://stackoverflow.com/questions/4140437
import enum
import inspect
import tkinter
from tkinter.constants import *


Mode = enum.Enum('Mode', 'none key focus focusin focusout all')
CAST = dict(d=int, i=int, P=str, s=str, S=str,
            v=Mode.__getitem__, V=Mode.__getitem__, W=str)


def on_validate(widget, mode, validator):
    # http://www.tcl.tk/man/tcl/TkCmd/ttk_entry.htm#M39
    if mode not in Mode:
        raise ValueError('mode not recognized')
    parameters = inspect.signature(validator).parameters
    if not set(parameters).issubset(CAST):
        raise ValueError('validator arguments not recognized')
    casts = tuple(map(CAST.__getitem__, parameters))
    widget.configure(validate=mode.name, validatecommand=[widget.register(
        lambda *args: bool(validator(*(cast(arg) for cast, arg in zip(
            casts, args)))))]+['%' + parameter for parameter in parameters])


class Example(tkinter.Frame):

    @classmethod
    def main(cls):
        tkinter.NoDefaultRoot()
        root = tkinter.Tk()
        root.title('Validation Example')
        cls(root).grid(sticky=NSEW)
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)
        root.mainloop()

    def __init__(self, master, **kw):
        super().__init__(master, **kw)
        self.entry = tkinter.Entry(self)
        self.text = tkinter.Text(self, height=15, width=50,
                                 wrap=WORD, state=DISABLED)
        self.entry.grid(row=0, column=0, sticky=NSEW)
        self.text.grid(row=1, column=0, sticky=NSEW)
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(0, weight=1)
        on_validate(self.entry, Mode.key, self.validator)

    def validator(self, d, i, P, s, S, v, V, W):
        self.text['state'] = NORMAL
        self.text.delete(1.0, END)
        self.text.insert(END, 'd = {!r}\ni = {!r}\nP = {!r}\ns = {!r}\n'
                              'S = {!r}\nv = {!r}\nV = {!r}\nW = {!r}'
                         .format(d, i, P, s, S, v, V, W))
        self.text['state'] = DISABLED
        return not S.isupper()


if __name__ == '__main__':
    Example.main()
Condolence answered 11/5, 2016 at 20:6 Comment(0)
A
4
import tkinter
tk=tkinter.Tk()
def only_numeric_input(e):
    #this is allowing all numeric input
    if e.isdigit():
        return True
    #this will allow backspace to work
    elif e=="":
        return True
    else:
        return False
#this will make the entry widget on root window
e1=tkinter.Entry(tk)
#arranging entry widget on screen
e1.grid(row=0,column=0)
c=tk.register(only_numeric_input)
e1.configure(validate="key",validatecommand=(c,'%P'))
tk.mainloop()
#very usefull for making app like calci
Aloud answered 31/1, 2019 at 20:48 Comment(2)
Hi, welcome to Stack Overflow. "Code-only" answers are frowned upon, especially when answering a question that already has many answers. Please be sure to add some additional insight into why the response you're providing is somehow substantive and not simply echoing what's already been vetted by the original poster.Boater
@Demian Wolf I liked your improved version of the original answer, but I had to roll it back. Please, consider posting it as an answer of your own (you can find it in the revision history).Strobotron
S
4

This code can help if you want to set both just digits and max characters.

from tkinter import *

root = Tk()

def validate(P):
    if len(P) == 0 or len(P) <= 10 and P.isdigit():  # 10 characters
        return True
    else:
        return False

ent = Entry(root, validate="key", validatecommand=(root.register(validate), '%P'))
ent.pack()

root.mainloop()
Selfstyled answered 23/3, 2021 at 9:20 Comment(0)
I
4

Here's an improved version of @Steven Rumbalski's answer of validating the Entry widgets value by tracing changes to a StringVar — which I have already debugged and improved to some degree by editing it in place.

The version below puts everything into a StringVar subclass to encapsulates what's going on better and, more importantly allow multiple independent instances of it to exist at the same time without interfering with each other — a potential problem with his implementation because it utilizes function attributes instead of instance attributes, which are essentially the same thing as global variables and can lead to problems in such a scenario.

try:
    from tkinter import *
except ImportError:
    from Tkinter import *  # Python 2


class ValidateFloatVar(StringVar):
    """StringVar subclass that only allows valid float values to be put in it."""

    def __init__(self, master=None, value=None, name=None):
        StringVar.__init__(self, master, value, name)
        self._old_value = self.get()
        self.trace('w', self._validate)

    def _validate(self, *_):
        new_value = self.get()
        try:
            new_value == '' or float(new_value)
            self._old_value = new_value
        except ValueError:
            StringVar.set(self, self._old_value)


root = Tk()
ent = Entry(root, textvariable=ValidateFloatVar(value=42.0))
ent.pack()
ent.focus_set()
ent.icursor(END)

root.mainloop()

Immeasurable answered 14/8, 2021 at 1:34 Comment(2)
Extending it to work with multiple instances is an important improvement, the original “solution” would introduce a massive problem (bad code structure) when fixing another one.Roccoroch
@Wolf: Obviously I agree. I felt the original author's idea of using tkinter's StringVar tracing capabilities to do the validation has merit, but the code in the implementation that was posted needed some work to be really be viable.Immeasurable
R
1

Responding to orionrobert's problem of dealing with simple validation upon substitutions of text through selection, instead of separate deletions or insertions:

A substitution of selected text is processed as a deletion followed by an insertion. This may lead to problems, for example, when the deletion should move the cursor to the left, while a substitution should move the cursor to the right. Fortunately, these two processes are executed immediately after one another. Hence, we can differentiate between a deletion by itself and a deletion directly followed by an insertion due to a substitution because the latter has does not change the idle flag between deletion and insertion.

This is exploited using a substitutionFlag and a Widget.after_idle(). after_idle() executes the lambda-function at the end of the event queue:

class ValidatedEntry(Entry):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.tclValidate = (self.register(self.validate), '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
        # attach the registered validation function to this spinbox
        self.config(validate = "all", validatecommand = self.tclValidate)

    def validate(self, type, index, result, prior, indelText, currentValidationMode, reason, widgetName):

        if typeOfAction == "0":
            # set a flag that can be checked by the insertion validation for being part of the substitution
            self.substitutionFlag = True
            # store desired data
            self.priorBeforeDeletion = prior
            self.indexBeforeDeletion = index
            # reset the flag after idle
            self.after_idle(lambda: setattr(self, "substitutionFlag", False))

            # normal deletion validation
            pass

        elif typeOfAction == "1":

            # if this is a substitution, everything is shifted left by a deletion, so undo this by using the previous prior
            if self.substitutionFlag:
                # restore desired data to what it was during validation of the deletion
                prior = self.priorBeforeDeletion
                index = self.indexBeforeDeletion

                # optional (often not required) additional behavior upon substitution
                pass

            else:
                # normal insertion validation
                pass

        return True

Of course, after a substitution, while validating the deletion part, one still won’t know whether an insert will follow. Luckily however, with: .set(), .icursor(), .index(SEL_FIRST), .index(SEL_LAST), .index(INSERT), we can achieve most desired behavior retrospectively (since the combination of our new substitutionFlag with an insertion is a new unique and final event.

Regularize answered 20/2, 2019 at 21:16 Comment(0)
Y
0
entry_text1 = StringVar()
XYZ=Entry( root, width=10,textvariable=entry_text1 ,  
font=font3)
XYZ.grid(column=1,row=1,padx=10)

try:
    XYZ=float(entry_text1.get())
    return True
except ValueError:        
    XYZ.delete(0, END)       
    XYZ.insert(0,0)
    tkinter.messagebox.showinfo("not pass",  "please enter only 
    number with '.' or only number") 
    return False
Yaw answered 29/12, 2023 at 20:21 Comment(1)
check entry widget for only floats with "try-except", if not pass then pop up windows warns the usersYaw

© 2022 - 2024 — McMap. All rights reserved.