Python Tkinter Spinbox Validate Fail - python

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

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

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.

Related

How to get integer input for different variables from a single tkinter Entry widget?

I’m writing a code for an ecosystem simulation in which the user can adjust initial values as e.g. population size, fertility, and so on. Instead of writing a method for each variable I would like to use a general method that can be called for each variable. I also want to exclude non-integers as input and to restrict the acceptable value to a certain range.
However, the last requirement fails in my code as the range limits for each variable affect each other. Basically because I cannot add arguments to the entry.bind method. How to solve this?
Below, you'll find a simplified version of my code in which the problem happens.
import tkinter as tk
class Window():
def __init__(self):
tk.Label(master, text ='Fox number').grid(row=0,column=0)
tk.Label(master, text ='Hare number').grid(row=1,column=0)
self.fox_entry=tk.Entry(master, width=5)
self.fox_entry.grid(row=0, column=1)
self.hare_entry=tk.Entry(master, width=5)
self.hare_entry.grid(row=1, column=1)
class Ecosystem():
def __init__(self):
self.foxnumber = 10
self.harenumber = 100
def initiate(self):
def input_user(entry,value, minval, maxval):
self.value = value
self.inputvalue = value
self.minval = minval
self.maxval = maxval
def get_value(event):
try:
self.inputvalue = int(entry.get())
print(self.inputvalue)
except ValueError:
entry.delete(0,'end')
entry.insert(0,self.value)
self.inputvalue = self.value
if self.inputvalue not in range(self.minval, self.maxval+1):
entry.delete(0,'end')
entry.insert(0,self.value)
self.inputvalue = self.value
print(self.inputvalue)
entry.bind('<Return>', get_value)
value = self.inputvalue
return value
my_win.fox_entry.insert(0,self.foxnumber)
self.foxnumber = input_user(my_win.fox_entry,self.foxnumber, 0, 50)
my_win.hare_entry.insert(0,self.harenumber)
self.harenumber = input_user(my_win.hare_entry,self.harenumber, 0, 200)
# more variables will added later on
master = tk.Tk()
my_win = Window()
my_ecosystem = Ecosystem()
my_ecosystem.initiate()
master.mainloop()

Can't close tkinter root window after modalPane closed

I've cloned a class called ListBoxChoice found on the web found (adding some needed features) below:
from Tkinter import *
class ListBoxChoice(object):
def __init__(self, master=None, title=None, message=None,\
list=[]):
self.master = master
self.value = None
self.list = list[:]
self.modalPane = Toplevel(self.master)
self.modalPane.transient(self.master)
self.modalPane.grab_set()
self.modalPane.bind("<Return>", self._choose)
self.modalPane.bind("<Escape>", self._cancel)
if title:
self.modalPane.title(title)
if message:
Label(self.modalPane, text=message).pack(padx=5, pady=5)
listFrame = Frame(self.modalPane)
listFrame.pack(side=TOP, padx=5, pady=5)
scrollBar = Scrollbar(listFrame)
scrollBar.pack(side=RIGHT, fill=Y)
# get the largest value of the 'list' to set the width
widthOfList = 0
for k in list:
if len(str(k)) > widthOfList:
widthOfList = len(str(k))
# now pad some space to back of the widthOfList
widthOfList = widthOfList + 2
self.listBox = Listbox(listFrame, selectmode=SINGLE,\
width=widthOfList)
self.listBox.pack(side=LEFT, fill=Y)
scrollBar.config(command=self.listBox.yview)
self.listBox.config(yscrollcommand=scrollBar.set)
self.list.sort()
for item in self.list:
self.listBox.insert(END, item)
buttonFrame = Frame(self.modalPane)
buttonFrame.pack(side=BOTTOM)
chooseButton = Button(buttonFrame, text="Choose",\
command=self._choose)
chooseButton.pack()
cancelButton = Button(buttonFrame, text="Cancel",\
command=self._cancel)
cancelButton.pack(side=RIGHT)
def _choose(self, event=None):
try:
firstIndex = self.listBox.curselection()[0]
self.value = self.list[int(firstIndex)]
except IndexError:
self.value = None
self.modalPane.destroy()
def _cancel(self, event=None):
self.modalPane.destroy()
def returnValue(self):
self.master.wait_window(self.modalPane)
return self.value
if __name__ == '__main__':
import random
root = Tk()
returnValue = True
list = [random.randint(1,100) for x in range(50)]
while returnValue:
returnValue = ListBoxChoice(root, "Number Picking",\
"Pick one of these crazy random numbers",\
list).returnValue()
print returnValue
Now this example says to do something like this:
results = ListBoxChoice(root, list=listOfItems).returnValue().
What I'm trying to do is provide a list of values from which the user selects a single value. The window should close before I use the results from the selected value. Here is that code:
from tkinter import Tk, Label
form ListBoxChoice import ListBoxChoice
...
eventList = ["20190120","20190127","20190203"]
root = Tk()
root.withdraw() # This causes the ListBoxChoice object not to appear
selectValue = ListBoxChoice(root, title="Event",\
message="Pick Event", list=eventList).returnValue()
root.wait_window() # Modal Pane/window closes but not the root
print("selectValue:", selectValue)
A root window is placed behind the modalPane (Toplevel). I have to close that window before the calling process continues. So there is a block in place.
I've tried to put a sleep(1.01) command above but had no impact.
How do I get the ListBoxChoice to close once the selection has been made
before my print statement of the selectValue? For it is at that point I want to use the results to plot data.
If I don't use root.wait_winow(), it is only when the plot is closed (end of the process) that the ListBoxChoice box close as well.
Suggestions?
Slightly updated
Here's a version of the ListBoxChoice class which I think works the way you desire. I've updated my previous answer slightly so the class is now defined in a separate module named listboxchoice.py. This didn't change anything I could see when I tested—it other words it still seems to work—but I wanted to more closely simulate the way you said you're using it the comments.
It still uses wait_window() because doing so is required to give tkinter's mandatory event-processing-loop the opportunity to run (since mainloop() isn't called anywhere). There's some good background material in the article Dialog Windows about programming tkiner dialogs you might find useful. The added root.withdraw() call eliminates the issue of not being able to close it because it's not there. This is fine since there's no need to have the empty window being displayed anyway.
test_lbc.py
import random
try:
import Tkinter as tk # Python 2
except ModuleNotFoundError:
import tkinter as tk # Python 3
from listboxchoice import ListBoxChoice
root = tk.Tk()
root.withdraw() # Hide root window.
values = [random.randint(1, 100) for _ in range(50)]
choice = None
while choice is None:
choice = ListBoxChoice(root, "Number Picking",
"Pick one of these crazy random numbers",
values).returnValue()
print('choice: {}'.format(choice))
listboxchoice.py
""" ListBoxChoice widget to display a list of values and allow user to
choose one of them.
"""
try:
import Tkinter as tk # Python 2
except ModuleNotFoundError:
import tkinter as tk # Python 3
class ListBoxChoice(object):
def __init__(self, master=None, title=None, message=None, values=None):
self.master = master
self.value = None
if values is None: # Avoid use of mutable default argument value.
raise RuntimeError('No values argument provided.')
self.values = values[:] # Create copy.
self.modalPane = tk.Toplevel(self.master, takefocus=True)
self.modalPane.bind("<Return>", self._choose)
self.modalPane.bind("<Escape>", self._cancel)
if title:
self.modalPane.title(title)
if message:
tk.Label(self.modalPane, text=message).pack(padx=5, pady=5)
listFrame = tk.Frame(self.modalPane)
listFrame.pack(side=tk.TOP, padx=5, pady=5)
scrollBar = tk.Scrollbar(listFrame)
scrollBar.pack(side=tk.RIGHT, fill=tk.Y)
# Get length the largest value in 'values'.
widthOfList = max(len(str(value)) for value in values)
widthOfList += 2 # Add some padding.
self.listBox = tk.Listbox(listFrame, selectmode=tk.SINGLE, width=widthOfList)
self.listBox.pack(side=tk.LEFT, fill=tk.Y)
scrollBar.config(command=self.listBox.yview)
self.listBox.config(yscrollcommand=scrollBar.set)
self.values.sort()
for item in self.values:
self.listBox.insert(tk.END, item)
buttonFrame = tk.Frame(self.modalPane)
buttonFrame.pack(side=tk.BOTTOM)
chooseButton = tk.Button(buttonFrame, text="Choose", command=self._choose)
chooseButton.pack()
cancelButton = tk.Button(buttonFrame, text="Cancel", command=self._cancel)
cancelButton.pack(side=tk.RIGHT)
def _choose(self, event=None):
try:
firstIndex = self.listBox.curselection()[0]
self.value = self.values[int(firstIndex)]
except IndexError:
self.value = None
self.modalPane.destroy()
def _cancel(self, event=None):
self.modalPane.destroy()
def returnValue(self):
self.master.wait_window(self.modalPane)
return self.value

How to only allow certain parameters within an entry field on Tkinter

If i want an entry box in Tkinter that only accepts floating point numbers that are greater than or equal to 0.0 and less than or equal to 1.0 how would i do that?
The proper way it to use tkinter's validate capabilities. But it's really a PIA to use.
dsgdfg has a good answer, but I can make that a lot neater, robust, and more dynamic:
import Tkinter as tk
class LimitedFloatEntry(tk.Entry):
'''A new type of Entry widget that allows you to set limits on the entry'''
def __init__(self, master=None, **kwargs):
self.var = tk.StringVar(master, 0)
self.var.trace('w', self.validate)
self.get = self.var.get
self.from_ = kwargs.pop('from_', 0)
self.to = kwargs.pop('to', 1)
self.old_value = 0
tk.Entry.__init__(self, master, textvariable=self.var, **kwargs)
def validate(self, *args):
try:
value = self.get()
# special case allows for an empty entry box
if value not in ('', '-') and not self.from_ <= float(value) <= self.to:
raise ValueError
self.old_value = value
except ValueError:
self.set(self.old_value)
def set(self, value):
self.delete(0, tk.END)
self.insert(0, str(value))
You use it just like an Entry widget, except now you have 'from_' and 'to' arguments to set the allowable range:
root = tk.Tk()
e1 = LimitedFloatEntry(root, from_=-2, to=5)
e1.pack()
root.mainloop()
If you want to call a button to check whether, this is a way to do it.
from tkinter import *
class GUI():
def __init__(self, root):
self.Entry_i = Entry(root, bd = 5)
self.test = StringVar()
Label_i = Label(root, textvariable = self.test)
Button_i = Button(root, text = "Go", command = self.floatornot)
self.Entry_i.grid()
Label_i.grid()
Button_i.grid()
mainloop()
def floatornot(self):
test = self.Entry_i.get()
if float(test) < 0 or float(test) > 1:
self.test.set("Good")
else:
self.test.set("")
root = Tk()
GUI(root)
The button will call the floatornot function. This will get the value of the entry and check if it okay or not. Depending on the result the value of the label will be changed.

How to tell user to only use integers if they input string in Python / Tkinter?

What would be the best way to give an error and tell the user to only input numbers if they type letters as an input? Code that doesn't work:
if self.localid_entry.get() == int(self.localid_entry.get():
self.answer_label['text'] = "Use numbers only for I.D."
The variable is obtained in Tkinter with:
self.localid2_entry = ttk.Entry(self, width=5)
self.localid2_entry.grid(column=3, row=2)
The best solution is to use the validation feature to only allow integers so you don't have to worry about validation after the user is done.
See https://stackoverflow.com/a/4140988/7432 for an example that allows only letters. Converting that to allow only integers is trivial.
Bryan has the correct answer, but using tkinter's validation system is pretty bulky. I prefer to use a trace on the variable to check. For instance, I can make a new type of Entry that only accepts digits:
class Prox(ttk.Entry):
'''A Entry widget that only accepts digits'''
def __init__(self, master=None, **kwargs):
self.var = tk.StringVar(master)
self.var.trace('w', self.validate)
ttk.Entry.__init__(self, master, textvariable=self.var, **kwargs)
self.get, self.set = self.var.get, self.var.set
def validate(self, *args):
value = self.get()
if not value.isdigit():
self.set(''.join(x for x in value if x.isdigit()))
You would use it just like an Entry widget:
self.localid2_entry = Prox(self, width=5)
self.localid2_entry.grid(column=3, row=2)
Something like this:
try:
i = int(self.localid_entry.get())
except ValueError:
#Handle the exception
print 'Please enter an integer'
""" Below code restricts the ttk.Entry widget to receive type 'str'. """
import tkinter as tk
from tkinter import ttk
def is_type_int(*args):
item = var.get()
try:
item_type = type(int(item))
if item_type == type(int(1)):
print(item)
print(item_type)
except:
ent.delete(0, tk.END)
root = tk.Tk()
root.geometry("300x300")
var = tk.StringVar()
ent = ttk.Entry(root, textvariable=var)
ent.pack(pady=20)
var.trace("w", is_type_int)
root.mainloop()

Tkinter calculator fails to update label

I tried making a simple integer sign calculator using tkinter. It has a class with two different functions. The second function is supposed to be initiated when the user presses the "Enter" button. When I run the code the window comes up just as it is supposed to. But when I type and hit "Enter" the second function fails to run and does not update the label. I want for it to update as either "This number is positive.", "This number is 0.", or "This number is negative." Instead it remains blank.
I doubt it is relevant, but I made this program in PyCharm Community Edition 5.0.4, and I am using Python 3.5 (32-bit).
import tkinter
class IntegerSign:
def __init__(self):
self.window = tkinter.Tk()
self.window.title("Integer Sign Calculator")
self.window.geometry("300x150")
self.number_frame = tkinter.Frame(self.window)
self.solution_frame = tkinter.Frame(self.window)
self.button_frame = tkinter.Frame(self.window)
self.number_label = tkinter.Label(self.number_frame, text="Enter an integer:")
self.number_entry = tkinter.Entry(self.number_frame, width=10)
self.number_label.pack(side='left')
self.number_entry.pack(side='left')
self.statement = tkinter.StringVar()
self.solution_label = tkinter.Label(self.solution_frame, textvariable=self.statement)
self.statement = tkinter.Label(self.solution_frame, textvariable=self.statement)
self.solution_label.pack(side='left')
self.calc_button = tkinter.Button(self.button_frame, text='Enter', command=self.calc_answer)
self.quit_button = tkinter.Button(self.button_frame, text='Quit', command=self.window.destroy)
self.calc_button.pack(side='left')
self.quit_button.pack(side='left')
self.number_frame.pack()
self.solution_frame.pack()
self.button_frame.pack()
tkinter.mainloop()
def calc_answer(self):
self.number = int(self.number_entry.get())
self.statement = tkinter.StringVar()
if self.number > 0:
self.statement = "This number is positive."
elif self.number == 0:
self.statement = "This number is 0."
else:
self.statement = "This number is negative."
IntegerSign()
The first problem: in your constructor you initialize a variable named self.statement to a StringVar and then initialize again to a Label. After that second initialization, you have no way of accessing the first object. You need to use two different names.
The second problem: in your event handler, calc_answer, you create a new object named self.statement, but instead you need to set a new value into the old one (see docs). Here is a modified version of your program that works as intended:
import tkinter
class IntegerSign:
def __init__(self):
self.window = tkinter.Tk()
self.window.title("Integer Sign Calculator")
self.window.geometry("300x150")
self.number_frame = tkinter.Frame(self.window)
self.solution_frame = tkinter.Frame(self.window)
self.button_frame = tkinter.Frame(self.window)
self.number_label = tkinter.Label(self.number_frame, text="Enter an integer:")
self.number_entry = tkinter.Entry(self.number_frame, width=10)
self.number_label.pack(side='left')
self.number_entry.pack(side='left')
self.solution_string = tkinter.StringVar()
self.solution_label = tkinter.Label(self.solution_frame, textvariable=self.solution_string)
self.statement = tkinter.Label(self.solution_frame, textvariable=self.solution_string)
self.solution_label.pack(side='left')
self.calc_button = tkinter.Button(self.button_frame, text='Enter', command=self.calc_answer)
self.quit_button = tkinter.Button(self.button_frame, text='Quit', command=self.window.destroy)
self.calc_button.pack(side='left')
self.quit_button.pack(side='left')
self.number_frame.pack()
self.solution_frame.pack()
self.button_frame.pack()
tkinter.mainloop()
def calc_answer(self):
self.number = int(self.number_entry.get())
if self.number > 0:
self.solution_string.set("This number is positive.")
elif self.number == 0:
self.solution_string.set("This number is 0.")
else:
self.solution_string.set("This number is negative.")
IntegerSign()
This code works but contains a bad practice that I recommend you fix. The function tkinter.mainloop() is essentially an infinite loop, and you have placed it inside the constructor. Thus the constructor won't return the way a constructor is normally supposed to. Take that statement out of the __init__ function and put it at the end, after the call to IntegerSign, and make this a pattern to be used in the future.
To set the value of a StringVar you need to use the set method.
Right now all you did was re-assign the variable. You can also set a stringvar's (default) value by giving it a value when you first initialize it. e.g. - var = tk.StringVar(value="some value")
Edit: Didn't see that you also set self.statement to be the label widget... This would work if you used the method all the way at the bottom of this answer, and disregarded (optionally) stringvar's entirely. But, when you do this you can think of it as sticky notes. You stuck a sticky note that says "this variable holds this value", then you re-assigned the variable put another sticky note over the previous one that says "it now holds this value" as a really loose visual analogy.
>>> import tkinter as tk
>>> root = tk.Tk()
>>> statement = tk.StringVar()
>>> type(statement)
>>> <class 'tkinter.StringVar'>
>>> statement = "This number is positive"
>>> type(statement)
>>> <class 'str'>
>>> statement = tk.StringVar()
>>> statement.set("This number is positive")
>>> statement.get()
'This number is positive'
>>> type(statement)
>>> <class 'tkinter.StringVar'>
Alternatively you could just change the labels text by doing label_widget['text'] = 'new_text'

Categories

Resources