0

I am having an issue validating spinbox input. I have a workaround below that seems to work; however, it's awkward. Assuming this isn't a bug, is there a correct way to do this? I am using Anaconda Python 3.6 (tk 8.6) on Windows 10.

The issue is that validate is set to None if you return False from the validation function when the value in the spinbox entry is between to and from. This only occurs when clicking the up or down buttons and not when directly editing the text.

import tkinter as tk

class SpinboxGui:

    def __init__(self):
        self.root = tk.Tk()
        vcmd = (self.root.register(self.validate_spin), '%W', '%P')
        self.spin = tk.Spinbox(self.root, from_=0, to=50000)
        self.spin.config(validate="key", validatecommand=vcmd)
        self.spin.pack()

    def validate_spin(self, name, nv):
        try:
            print(nv)
            n = int(nv)
        except:
            return False
        if n <= 15000:
            return True
        return False

if __name__ == "__main__":
    SpinboxGui()
    tk.mainloop()

To reproduce, highlight 0 and type 149999. Then click up a few times. Note that the validation command stops being called. Output is:

01
014
0149
01499
014999
0149999
15000
15001

Now, according to the docs, using textVariable and validateCommand together is dangerous; indeed, I have crashed Python/Tkinter in more ways than one. However, in this case, it doesn't matter whether you use textVariable or not; the problem is the same.

One possible solution might be to edit the to and from options in the validation function. Even if this works, it's somewhat problematic for me because I'm syncing spinbox values to an embedded Matplotlib plot. I would need to compute to and from and convert units for each Matplotlib Artist and spinbox.

Since you can't edit the textVariable in the validation function, what I came up with is the following. Maybe someone can improve on this.

def __init__(self):
    # http://stackoverflow.com/a/4140988/675216
    vcmd= (self.root.register(self.validate_spin), '%W', '%P')
    # Rest of code left out
    self.spin.config(validate="key", validatecommand=vcmd)
    self.spin.bind("<<ResetValidate>>", self.on_reset_validate)

def on_reset_validate(self, event):
    # Turn validate back on and set textVariable
    self.spin.config(validate="key")

def validate_spin(self, name, nv):
    # Do validation ...
    if not valid:
        self.spin.event_generate("<<ResetValidate>>", when="tail")
    return valid
Todd
  • 475
  • 4
  • 15
  • This statement is false: _"The issue is that validate is set to None if you return False from the validation function..."_ - the validation function will not be set to `None` simply because you return `False`. In fact, the only valid return values are `True` and `False`. Please read and follow the advice here: [How to create a Minimal, Complete, and Verifiable example](http://www.stackoverflow.com/help/mcve) – Bryan Oakley Mar 11 '17 at 00:47
  • It is considerable time and effort to reproduce a problem. It's not that I'm too lazy to do it; I thought this might be the intended behavior of the spinbox. I didn't want to invest the time hunting down a non-existent problem. But now that I'm informed, here it is. – Todd Mar 11 '17 at 14:43
  • Ur code is fine. I just entered 149999 and then click next and get 150000 – toyota Supra Oct 31 '22 at 18:50

2 Answers2

1

After struggling with the validation mechanism in spinbox, I gave up on it. Maybe it works the way it was intended to, but I think it is counter-intuitive that it only gets called once. My application uses spinbox to update a matplotlib graph, and I need the data to be an integer in a specified range. I needed the code to catch non-integer entries as well as out-of-range integers. The solution I came up with was to use key bindings instead of the validation mechanism to achieve the desired result. Here is the relevant part of the code:

class IntSpinbox(ttk.Frame):

    def __init__(self, parent, **kwargs):
        ttk.Frame.__init__(self,
                   parent,
                   borderwidth=kwargs.get('frameborderwidth', 2),
                   relief=kwargs.get('framerelief', tk.GROOVE))
        self.valuestr = tk.StringVar()
        self.valuestr2 = tk.StringVar()
        self.minvalue = kwargs.get('minvalue', 0)
        self.maxvalue = kwargs.get('maxvalue', 99)
        self.initval = kwargs.get('initvalue', self.minvalue)
        self.valuestr.set(str(self.initval))
        self.valuestr2.set(str(self.initval))
        self.label = ttk.Label(self,
                   text=kwargs.get('labeltext', 'No label'),
                   anchor='w',
                   width=kwargs.get('labelwidth', 20))
        self.spinbox = tk.Spinbox(self,
                   from_=self.minvalue,
                   to=self.maxvalue,
                   increment=1,
                   textvariable=self.valuestr)
        self.spinbox.bind('<Return>', self.updateSpinbox)
        self.spinbox.bind('<FocusOut>', self.updateSpinbox)
        self.spinbox.bind('<FocusIn>', self.storeSpinbox)
        self.spinbox.bind('<Button-1>', self.storeSpinbox)
        self.spinbox.bind('<Button-2>', self.storeSpinbox)

        self.label.pack(side=tk.TOP, fill=tk.X, expand=True, padx=5)
        self.spinbox.pack(side=tk.BOTTOM, fill=tk.X, expand=True, padx=2, pady=5)
        self.onChange = kwargs.get('onchange', self.doNothing)

    def storeSpinbox(self, event):
        tmpstr = self.valuestr.get()
        try:
            tmpval = int(tmpstr)
        except:
            tmpval = -1000
        if tmpval < self.minvalue:
            tmpval = self.minvalue
        elif tmpval > self.maxvalue:
            tmpval  = self.maxvalue
        self.valuestr2.set(str(tmpval))        

    def updateSpinbox(self, event):
        tmpstr = self.valuestr.get()
        try:
            tmpval = int(tmpstr)
        except:
            tmpstr = self.valuestr2.get()
            self.valuestr.set(tmpstr)
            return
        if tmpval < self.minvalue:
            tmpval = self.minvalue
        elif tmpval > self.maxvalue:
            tmpval  = self.maxvalue
        tmpstr = str(tmpval)
        self.valuestr.set(tmpstr)
        self.valuestr2.set(tmpstr)
        self.onChange()

    def doNothing(self):
        pass

    def getValue(self):
        tmpstr = self.valuestr.get()
        return(int(tmpstr))

    def setValue(self, value):
        self.valuestr.set(str(value))
O.A.
  • 11
  • 1
  • There's probably numerous ways around it. The question is if it's behaving as intended or we're just doing something wrong. – Todd May 29 '17 at 22:19
0

I'm probably late to the party, but I'll leave it here in case someone needs it. What I did in a similar situation is to do everything I need in the callback for the writing of the variable I linked to the spinbox. Something like:

import Tkinter as tk

root = tk.Tk()
my_var = tk.IntVar() # or whatever you need
spin = tk.Spinbox(root, from_=0, to=100, textvariable=my_var)
spin.pack()

def do_whatever_I_need(*args):
    # here I can access the Spinbox value using spin.get()
    # I can do whatever check I 

my_var.trace('w', whatever) #'w' for "after writing"

The callback created by the trace method calls the given function whith two arguments: the callback mode ('w', in this case) and the variable name (it's some internal tkinter identifyer I've never used). This is why the signature for do_wahtever_I_need is *args.

Puff
  • 503
  • 1
  • 4
  • 14