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()