I am trying to limit the number of characters that can be input in a list of Entry widgets. I tried using the following:
def character_limit(entry_text):
if len(entry_text.get()) > 0:
entry_text.set(entry_text.get()[:10])
player_names = []
for i in range(num_players):
player_names.append(tk.StringVar())
player_names[i].trace("w", lambda *args: character_limit(player_names[i]))
player_name_entry = tk.Entry(top, textvariable=player_names[i])
player_name_entry.grid(row=i, column=0)
But this only limits the last Entry widget. How can I fix this?
The looping problem is a very commonly seen problem and to fix it, you have to store the current value of the iteration within the lambda itself:
def character_limit(*args, entry_text):
# if len(entry_text.get()) > 0: Can remove this line as it seems to not really be doing anything
entry_text.set(entry_text.get()[:10])
for i in range(num_players):
...
player_names[i].trace("w", lambda *args, i=i: character_limit(entry_text=player_names[i]))
The reason you use *args is because, trace passes 3 arguments to the function itself, that we don't need mostly.
But a more better method to do this will be to use validation for the entry widgets as this will prevent you needing to create a StringVar and trace its activity unnecessarily:
def character_limit(inp):
if len(inp) > 10:
return False
return True
player_names = []
vcmd = (root.register(character_limit), '%P')
for i in range(num_players):
player_name_entry = Entry(root, validate='key', validatecommand=vcmd)
player_name_entry.grid(row=i, column=0)
player_names.append(player_name_entry)
Read:
tkinter creating buttons in for loop passing command arguments
Interactively validating Entry widget content in tkinter
The problem is not related to the widget. Variable i is not local to the lambda functions, so the last value of i is used for every function.
To create local variables change your lambda into:
player_names[i].trace("w", lambda *args, n=i: character_limit(player_names[n]))
For a good description see https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result
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'm using Python 3.8.1 with tkinter version 8.6.
I have a GUI class, Pressureinput, which takes in input for a pressure sensor simulator. I want the entry to be in units of kPa (native units of the sensor) but I also want the user to know what the psi equivalent is. So, when the user updates the kpa value, I want the psi value to update, but I don't want the user to be able to update the psi value manually. I'm using an entry box for both. They start with a default of 242 kPa.
I'm trying to use validate="focusout" to trigger an event after the kpa entry box loses focus.
Here's my code so you can see what I'm trying to do. Basically, if they enter anything that's not a positive, even integer, I want it to automatically round the value in the entry box and then I also want it to update the psi equivalent.
I realize the method I'm using with my pressurevalid function won't work because the entrybox objects, kpa and psi are immutable and it won't change the original objects.
Note that I've set up the StringVar variables psitext and kpatext. Every time I try to use them in my pressurevalid function, however, I get errors saying they don't exist.
Everything else I've tried ends up with errors that won't run, and I think this at least illustrates what I want to do:
import tkinter as tkGUI
#global constants for conversion
global psi2kpa
global kpa2psi
psi2kpa = 6.894757
kpa2psi = 1 / psi2kpa
class Pressureinput(tkGUI.Frame):
def __init__(self,parent):
tkGUI.Frame.__init__(self,parent)
self.parent = parent
self.initialize()
def initialize(self):
kpatext = tkGUI.StringVar()
psitext = tkGUI.StringVar()
self.IDlabel = tkGUI.Label(self,text="Sensor ID (hex):")
self.IDlabel.grid(row=0, column=0)
self.ID = tkGUI.Entry(self)
self.ID.insert(0,"AABBCCDD")
self.ID.grid(row=0, column=1)
self.kpalabel = tkGUI.Label(self,text="Pressure (kPa):")
self.kpalabel.grid(row=1, column=0)
self.kpa = tkGUI.Entry(self)
self.kpa.insert(0,242)
self.kpa.grid(row=1, column=1)
self.psilabel = tkGUI.Label(self,text="Pressure (PSI):")
self.psilabel.grid(row=2, column=0)
self.psi = tkGUI.Entry(self, textvariable=psitext)
self.psi.insert(0,float(self.kpa.get())*kpa2psi)
self.psi.grid(row=2, column=1)
self.psi.config(state='disabled') #state = 'normal' to restore
vpressure = self.register(self.pressurevalid(self.kpa,self.psi))
self.kpa = tkGUI.Entry(self, textvariable=kpatext, validate="focusout", validatecommand=vpressure)
self.sendbutton = tkGUI.Button(self,text="Send Transmission",state="disabled",command=self.send_data)
self.sendbutton.grid(row=9,columnspan=2)
def pressurevalid(self,kpa,psi):
if len(kpa.get()) < 1:
kpa.delete(0,tkGUI.END)
kpa.insert(0,"0");
elif 2*int(round(float(kpa.get())) / 2) != int(kpa.get()):
kpa.delete(0,tkGUI.END)
kpa.insert(0,2 * int(round(float(kpa.get()))) / 2)
psi.config(state='normal')
psi.delete(0,tkGUI.END)
psi.insert(0,float(kpa.get())*kpa2psi)
psi.config(state='disabled')
return True
def send_data(self):
ID = int(self.ID.get(),16)
pressure = int(self.kpa.get())
if pressure >= 510:
pressure = 255
else:
pressure = int(round(pressure/2))
sendstring = str(ID) + "," + str(function_code) + "," + str(pressure)
print (sendstring)
Since you are using a StringVar for the entries, you can set up a trace on the variable to call a function whenever the value changes. This will constantly keep the value updated rather than waiting for a focus-out event.
First, you need to convert the variables into attributes of the class rather than making them local variables:
self.kpatext = tkGUI.StringVar()
self.psitext = tkGUI.StringVar()
You'll also have to adjust other places which reference these variables:
self.psi = tkGUI.Entry(..., textvariable=self.psitext, ...)
self.kpa = tkGUI.Entry(..., textvariable=self.kpatext, ...)
Next, set up a trace on self.kpatext right after you create the variables:
self.kpatext.trace("w", self.update_psi)
And finally, write the method self.update_psi. The following code will set the PSI to an empty string if the current value of kPa isn't able to be converted.
def update_psi(self, *args):
try:
psi = int(self.kpatext.get())*kpa2psi
self.psitext.set(psi)
except Exception as e:
self.psitext.set("")
For more information on what the arguments to the trace function are, see What are the arguments to Tkinter variable trace method callbacks?. In this example we don't need them, but the function still must accept them.
Note, your code defines self.kpa twice -- once without using textvariable and once with. I don't understand why you're doing that given that the second one is never added to the screen with pack/place/grid. My solution works under the assumption that the original self.kpa is the one that you intend to use.
I would like to sort numbers when I clicked sort numbers Radiobutton. I already achieve this by calling a function when the Radiobutton is clicked. however, i couldn't sort numbers without calling a function.
this is my code
R1=Radiobutton(root,text="Sort Student Numbers",value=1)
R1.pack(anchor=W)
R2=Radiobutton(root,text="Sort Student Names",value=2)
R2.pack(anchor=W)
with open("student.json", "r"") as f:
data = json.load(f)
for d in data["student"]:
if value == 1:
data["student"].sort(key = lambda d: d["Numbers"])
elif value == 2:
data["student"].sort(key = lambda d: d["Names"])
label_1 = Label(frame , text="Name: %s" %(d["Names"]))
label_1.pack()
label_2 = Label(frame , text="Student Numbers: %d" %(d["Numbers"]))
label_2.pack()
if I say for example R1=Radiobutton(root,text="Sort Student Numbers",value=1, command = sorted_numbers(1)) everything works fine but the reason I don't want to use function calling is I would have to create 3 functions to achieve what I want. thanks
One way to solve this problem is to tie these radio buttons to a shared tkinter variable instance. When a radio button is selected, the value of the variable will be set to the value of the radio button, and then you can use that value in your code.
I haven't had time to test this code, but I have copied your code and modified it in a way that should work. This code assumes that you are importing everything from tkinter, using the line from tkinter import *; otherwise, you will need to do something like from tkinter import IntVar. There are several types of tkinter variable subclasses (IntVar, BooleanVar, etc.), and each has the methods get and set, which behave exactly as you'd expect (as demonstrated below).
# This is the variable that will store the value of the currently selected radio button
sort_value = IntVar()
# For each radio button, assign sort_value to the keyword parameter "variable"
R1=Radiobutton(root,text="Sort Student Numbers",variable=sort_value,value=1)
R1.pack(anchor=W)
R2=Radiobutton(root,text="Sort Student Names",variable=sort_value,value=2)
R2.pack(anchor=W)
with open("student.json", "r") as f:
data = json.load(f)
for d in data["student"]:
# sort_value is an IntVar, so sort_value.get returns a Python int
if sort_value.get() == 1:
data["student"].sort(key = lambda d: d["Numbers"])
elif sort_value.get() == 2:
data["student"].sort(key = lambda d: d["Names"])
label_1 = Label(frame , text="Name: %s" %(d["Names"]))
label_1.pack()
label_2 = Label(frame , text="Student Numbers: %d" %(d["Numbers"]))
label_2.pack()
Edit: Like Nae pointed out in the comments, you can also initialize the variable to a default value like this:
sort_value = IntVar(value=1)
Otherwise, its default value will be 0. I believe that by setting it to 1, this will also cause the radio button whose value is 1 to be selected by default.
I hope this helps.
I'm trying to make use of this excellent answer by Bryan Oakley, but to no avail (https://stackoverflow.com/a/4140988/5060127)...
I would like to use the same method to verify Spinbox values. I have defined from_ and to values for spinboxes, but user can still type most anything in them... it should be validated that only values within the from_-to range are possible to be inputted by the user, and only integers at that.
Here's the code that shows how far I've got...
try:
from Tkinter import *
except ImportError:
from tkinter import *
class GUI:
def __init__(self):
# root window of the whole program
self.root = Tk()
self.root.title('ImageSound')
# registering validation command
vldt_ifnum_cmd = (self.root.register(self.ValidateIfNum),'%s', '%S')
# creating a spinbox
harm_count = Spinbox(self.root, from_=1, to=128, width=5, justify='right', validate='all', validatecommand=vldt_ifnum_cmd)
harm_count.delete(0,'end')
harm_count.insert(0,8)
harm_count.pack(padx=10, pady=10)
def ValidateIfNum(self, s, S):
# disallow anything but numbers
valid = S.isdigit()
if not valid:
self.root.bell()
return valid
if __name__ == '__main__':
mainwindow = GUI()
mainloop()
I think I found the problem. Validator function is called initially with S='' and your condition S.isdigit() returns False and function is not called anymore. But after I updated condition to valid = S == '' or S.isdigit() it started to work as expected.
Of course you'll probably want some more sophisticated condition (e.g. checking if value is within range), but it looks like empty string has to pass (at least initial) validation.
I have done it! Both integer-only input and range-checking that takes widget's from_ and to values into account is working! It perhaps looks a bit hacky, but it's working! Here's the code for anyone interested:
try:
from Tkinter import *
except ImportError:
from tkinter import *
class GUI:
def __init__(self):
# root window of the whole program
self.root = Tk()
self.root.title('ImageSound')
# registering validation command
vldt_ifnum_cmd = (self.root.register(self.ValidateIfNum),'%P', '%S', '%W')
# creating a spinbox
harm_count = Spinbox(self.root, from_=1, to=128, width=5, justify='right', validate='all', validatecommand=vldt_ifnum_cmd)
harm_count.insert(0,8)
harm_count.delete(1,'end')
harm_count.pack(padx=10, pady=10)
def ValidateIfNum(self, user_input, new_value, widget_name):
# disallow anything but numbers in the input
valid = new_value == '' or new_value.isdigit()
# now that we've ensured the input is only integers, range checking!
if valid:
# get minimum and maximum values of the widget to be validated
minval = int(self.root.nametowidget(widget_name).config('from')[4])
maxval = int(self.root.nametowidget(widget_name).config('to')[4])
# check if it's in range
if int(user_input) not in range (minval, maxval):
valid = False
if not valid:
self.root.bell()
return valid
if __name__ == '__main__':
mainwindow = GUI()
mainloop()
One thing that I noticed isn't quite working is if you select the whole text in the spinbox, and paste something wrong, like text. That breaks validation completely. Ugh.
I've came up with a solution that works for any Entry widget and thus SpinBox's as well. It uses validatecommand to ensure only the correct values are entered. A blank entry is temporarily validate but on FocusOut it changes back to the last valid value.
intvalidate.py
import tkinter as tk
def int_validate(entry_widget, limits=(None, None)):
"""
Validates an entry_widget so that only integers within a specified range may be entered
:param entry_widget: The tkinter.Entry widget to validate
:param limits: The limits of the integer. It is given as a (min, max) tuple
:return: None
"""
num_str = entry_widget.get()
current = None if (not _is_int(num_str)) else int(num_str)
check = _NumberCheck(entry_widget, limits[0], limits[1], current=current)
entry_widget.config(validate='all')
entry_widget.config(validatecommand=check.vcmd)
entry_widget.bind('<FocusOut>', lambda event: _validate(entry_widget, check))
_validate(entry_widget, check)
def _is_int(num_str):
"""
Returns whether or not a given string is an integer
:param num_str: The string to test
:return: Whether or not the string is an integer
"""
try:
int(num_str)
return True
except ValueError:
return False
def _validate(entry, num_check):
"""
Validates an entry so if there is invalid text in it it will be replaced by the last valid text
:param entry: The entry widget
:param num_check: The _NumberCheck instance that keeps track of the last valid number
:return: None
"""
if not _is_int(entry.get()):
entry.delete(0, tk.END)
entry.insert(0, str(num_check.last_valid))
class _NumberCheck:
"""
Class used for validating entry widgets, self.vcmd is provided as the validatecommand
"""
def __init__(self, parent, min_, max_, current):
self.parent = parent
self.low = min_
self.high = max_
self.vcmd = parent.register(self.in_integer_range), '%d', '%P'
if _NumberCheck.in_range(0, min_, max_):
self.last_valid = 0
else:
self.last_valid = min_
if current is not None:
self.last_valid = current
def in_integer_range(self, type_, after_text):
"""
Validates an entry to make sure the correct text is being inputted
:param type_: 0 for deletion, 1 for insertion, -1 for focus in
:param after_text: The text that the entry will display if validated
:return:
"""
if type_ == '-1':
if _is_int(after_text):
self.last_valid = int(after_text)
# Delete Action, always okay, if valid number save it
elif type_ == '0':
try:
num = int(after_text)
self.last_valid = num
except ValueError:
pass
return True
# Insert Action, okay based on ranges, if valid save num
elif type_ == '1':
try:
num = int(after_text)
except ValueError:
if self.can_be_negative() and after_text == '-':
return True
return False
if self.is_valid_range(num):
self.last_valid = num
return True
return False
return False
def can_be_negative(self):
"""
Tests whether this given entry widget can have a negative number
:return: Whether or not the entry can have a negative number
"""
return (self.low is None) or (self.low < 0)
def is_valid_range(self, num):
"""
Tests whether the given number is valid for this entry widgets range
:param num: The number to range test
:return: Whether or not the number is in range
"""
return _NumberCheck.in_range(num, self.low, self.high)
#staticmethod
def in_range(num, low, high):
"""
Tests whether or not a number is within a specified range inclusive
:param num: The number to test if its in the range
:param low: The minimum of the range
:param high: The maximum of the range
:return: Whether or not the number is in the range
"""
if (low is not None) and (num < low):
return False
if (high is not None) and (num > high):
return False
return True
It's used as such
import tkinter as tk
from tkinter import ttk
from intvalidate import int_validate
if __name__ == '__main__':
root = tk.Tk()
var = tk.DoubleVar()
widget = ttk.Spinbox(root, textvariable=var, justify=tk.CENTER, from_=0, to_=10)
widget.pack(padx=10, pady=10)
int_validate(widget, limits=(0, 10))
root.mainloop()