Tkinter Spinbox range validation Tkinter Spinbox range validation tkinter tkinter

Tkinter Spinbox range validation


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 validif __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 tkdef 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 Falsedef _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 tkfrom tkinter import ttkfrom intvalidate import int_validateif __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()