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?
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
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.
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()
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
Entry is designed to only accept integers by implementing 'validatecommand'
User enters 1234567
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
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.
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.
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()
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
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()
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()
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.
I need to implement an entry box that accepts only a range of DoubleVar values. I have referenced this question that was asked How to only allow certain parameters within an entry field on Tkinter, but I want the user to be notified (using a change in font colour or anything) while they are entering the values. I've read the documentation but this is something that I haven't come across.
I'm new to Tkinter, so please excuse me if this sounds very stupid
You can bind KeyRelease event of the Entry to a callback and check whether the input value is valid and within the required range, then update the foreground color of the Entry accordingly:
import tkinter as tk
root = tk.Tk()
def check_value(entry, min_value, max_value):
try:
value = float(entry.get().strip())
valid = min_value <= value <= max_value
except ValueError:
valid = False
entry.config(fg='black' if valid else 'red')
return valid # in case you want the checking result somewhere else
entry = tk.Entry(root)
entry.pack()
entry.bind('<KeyRelease>', lambda e: check_value(e.widget, 10, 20))
root.mainloop()
Use the validatecommand option for the Entry.
Some code fragments to show how it works:
root = tk.Tk()
vcmd = root.register(is_number)
e = ttk.Entry(pressf, justify='right', validate='key', validatecommand=(vcmd, '%P'))
def is_number(data):
"""Validate the contents of an entry widget as a float."""
if data == '':
return True
try:
rv = float(data)
if rv < 0:
return False
except ValueError:
return False
return True
This basically calls the validation function on every keypress. Only if the validation succeeds is the character added to the entry.
You can find a complete working example here.
Edit
Above is the "canonical" example of a validator. It allows or disallows characters into the Entry.
But you can also use it in other ways.
For example, you can always return True, but e.g. change the text color of the Entry to red if the value is not within the limits you want.
I've written a GUI for a script that does some geometrical calculations. Certain ranges of values break the computation (e.g. find the intersection of two shapes that don't intersect.) I raise exceptions in those cases. I'd like to prevent the user from adjusting the spinbox value beyond the point where exceptions are raised.
I've tried overwriting the validator method for the QDoubleSpinBox. This works great when I manually enter values with the keyboard. But, it doesn't prevent me from clicking the up and down arrows.
How I can limit the ability of the user to run-up the values outside of the acceptable range?
Note: The actual some_complicated_function involves the values from 5 different spinboxes.
from PyQt4 import QtCore, QtGui
import sys
def some_complicated_function(val_a):
if val_a + 3 < 10:
return True
else:
raise Exception("Giant number!")
class SpinBoxSpecial(QtGui.QDoubleSpinBox):
def validate(self, value, pos):
# print float(value)
try:
some_complicated_function(float(value))
print "yup"
return QtGui.QValidator.Acceptable, QtGui.QValidator.Acceptable
except:
print "nope"
return QtGui.QValidator.Invalid, QtGui.QValidator.Invalid
a = QtGui.QApplication(sys.argv)
w = QtGui.QMainWindow()
w.resize(320, 100)
w.setWindowTitle("PyQT Python Widget!")
spinbox = SpinBoxSpecial(w)
spinbox.move(20, 20)
spinbox.CorrectionMode = QtGui.QAbstractSpinBox.CorrectToPreviousValue
w.show()
sys.exit(a.exec_())
Edit:
The basic ask is: I want to call a function when the value of a spinbox changes (via mouse or keyboard). If that function throws an exception, I want the value of the spinbox to revert to what it was.
Here is a simple way to dynamically set the range on a spinbox:
class SpinBoxSpecial(QtGui.QDoubleSpinBox):
def __init__(self, parent=None):
super(SpinBoxSpecial, self).__init__(parent)
self._last = self.value()
self.valueChanged.connect(self.handleValueChanged)
def handleValueChanged(self, value):
try:
some_complicated_function(float(value))
print "yup", value
self._last = value
except:
print "nope", value
if value > self._last:
self.setMaximum(self._last)
else:
self.setMinimum(self._last)
EDIT:
Just realized the above won't work correctly if a value is typed in directly, because it could fix the min/max too early. So maybe this would be better:
def handleValueChanged(self, value):
try:
some_complicated_function(float(value))
print "yup", value
self._last = value
except:
print "nope", value
self.setValue(self._last)
I'm trying to make a simple calculator that has buttons of numbers 0-9, plus, minus, clear and equals. I have the gui and the functionality of the buttons, but my calculator calculates further than 999. Any ideas?
-I have attempted to stop it calculating further than 999, if you look at line 45-53.
here is my code:
from tkinter import *
class Calculator(Frame):
def frame(this, side):
w = Frame(this)
w.pack(side=side, expand=YES, fill=BOTH)
return w
def button(this, root, side, text, command=None):
w = Button(root, text=text, command=command)
w.pack(side=side, expand=YES, fill=BOTH)
return w
need_clr = False
def digit(self, digit):
if self.need_clr:
self.display.set('')
self.need_clr = False
self.display.set(self.display.get() + digit)
def sign(self):
need_clr = False
cont = self.display.get()
if len(cont) > 0 and cont[0] == '-':
self.display.set(cont[1:])
else:
self.display.set('-' + cont)
def oper(self, op):
self.display.set(self.display.get() + ' ' + op + ' ')
self.need_clr = False
def calc(self):
try:
self.display.set(eval(self.display.get()))
self.need_clr = True
except:
showerror('Operation Error', 'Illegal Operation')
self.display.set('')
self.need_clr = False
def equals(self):
try:
result = eval(self.display.get())
if result >= 1000:
result (calc)
except:
results("ERROR")
display.delete(0, END)
display.insert(0, display)
def __init__(self):
Frame.__init__(self)
self.option_add('*Font', 'Dotum 15')
self.pack(expand=YES, fill=BOTH)
self.master.title('Simple Calculator')
self.display = StringVar()
e = Entry(self, relief=SUNKEN, textvariable=self.display)
e.pack(side=TOP, expand=YES, fill=BOTH)
for key in ("123", "456", "789"):
keyF = self.frame(TOP)
for char in key:
self.button(keyF, LEFT, char,
lambda c=char: self.digit(c))
keyF = self.frame(TOP)
self.button(keyF, LEFT, '0', lambda ch='0': self.digit(ch))
opsF = self.frame(TOP)
for char in "+-=":
if char == '=':
btn = self.button(opsF, LEFT, char, self.calc)
else:
btn = self.button(opsF, LEFT, char,
lambda w=self, s=char: w.oper(s))
clearF = self.frame(BOTTOM)
self.button(clearF, LEFT, 'Clr', lambda w=self.display: w.set(''))
if __name__ == '__main__':
Calculator().mainloop()
Your big problem is that you've tried to fix this in a method called equals that you never call anywhere in your code. So, obviously this won't do anything.
If you look at where your = button is defined, it does this:
opsF = self.frame(TOP)
for char in "+-=":
if char == '=':
btn = self.button(opsF, LEFT, char, self.calc)
So, it calls the calc method. You have to change calc to influence what it does; adding some completely separate method that never gets called anywhere won't help.
If you change this to call self.equals instead of self.calc, that solves this first problem… but of course you'll have a whole new set of problems, because most of the code in equals makes no sense, as Joran Beasley explains. Look at the working code in calc as a model for how to do things in equals.
However, a better design than copying/pasting/editing calc would be to modify it to call some new validate_result method, like this:
def calc(self):
try:
self.display.set(self.validate_result(eval(self.display.get())))
self.need_clr = True
except:
showerror('Operation Error', 'Illegal Operation')
self.display.set('')
self.need_clr = False
Now, you just need to write validate_result as a function that works on numbers and either returns the number (or returns a modified number, if you want) or raises an exception. It doesn't need to duplicate all the work that calc does, it can just let calc do all that stuff. For example:
def validate_result(self, result):
if result >= 1000:
raise ValueError('result too big!')
else:
return result
I assume you are talking about
def equals(self):
try:
result = eval(self.display.get()) # <-- this is some risky business
if result >= 1000: #I think you want to check less than but its not clear, this is greater than or equal
result (calc) #result is a number you cannot do 5(some_argument)
#^^^^^^^^^^^ this line should probably end up as an error message ...
#you probably need an else here to handle if the number is too big
else:
raise ValueError("Value Too Large!!!")
except:
results("ERROR") #not sure what this line is doing ...
display.delete(0, END)
display.insert(0, display)
there are several problems I commented ... perhaps one will solve your issue
but I think really abarnert nailed your problem so I would go with that ...