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.
Related
I have a tkinter application that searches through a list of about 100000 wordlist when user types into the Entry widget (using trace with write callback to capture change in Entry variable).
I want to implement sort of a delay in order to NOT invoke the trace callback (to search the entire 100k wordlist) at EVERY keystroke (as the user might still be typing and it can become rather jerky/slow to invoke the callback function for each keystroke), rather I want to employ some sort of a min time to wait for additional input/keystroke AND/OR a max time since the first key was pressed BEFORE invoking the trace callback function.
I tried implementing a sleep but that is just a blocking call and does not achieve the desired affect. Here is some sample code where entering the string 'password' will invoke the callback (since this is literally just checking against the string 'password', it is super fast, yet in my app I loop over 100k word list for each keystroke which becomes slow). Thank You!
import tkinter as tk
from tkinter import ttk
class App(tk.Tk):
SUCCESS = 'Success.TLabel'
def __init__(self):
super().__init__()
self.title('Enter <password>')
self.geometry("200x120")
self.passwordVariable = tk.StringVar()
self.passwordVariable.trace('w', self.validate)
password_entry = ttk.Entry(
self, textvariable=self.passwordVariable) #, show='*'
password_entry.grid(column=0, row=1)
password_entry.focus()
self.message_label = ttk.Label(self)
self.message_label.grid(column=0, row=0)
def set_message(self, message, type=None):
self.message_label['text'] = message
if type:
self.message_label['style'] = type
def validate(self, *args):
confirm_password = self.passwordVariable.get()
if confirm_password == "password":
self.set_message(
"Success: The new password looks good!", self.SUCCESS)
return
if confirm_password.startswith("pas"):
self.set_message('Warning: Keep entering the password')
if __name__ == "__main__":
app = App()
app.mainloop()
I tried to understand what your current code does so I can implement the function here, but I've had no luck. Hopefully you being the author can implement this example onto your code.
The idea here is to schedule a callback to run after x seconds and if it is already scheduled, then cancel it. Sort of like a timer, if you think about it.
from tkinter import *
root = Tk()
SECONDS_TO_WAIT = 1
rep = None
def typing(*args):
global rep
if rep is None:
writing.config(text='Typing...')
else:
root.after_cancel(rep) # if already scheduled, then cancel it
rep = root.after(SECONDS_TO_WAIT*1000, caller)
def caller(*args):
global rep
writing.config(text='Not typing')
rep = None # Set it to None if `caller` GETS executed
var = StringVar()
entry = Entry(root,textvariable=var)
entry.pack(padx=10,pady=10)
entry.focus_force()
writing = Label(root,text='Not typing')
writing.pack()
var.trace('w',typing)
root.mainloop()
This will execute typing each time the entry widget is edited/written to. And according to the conditions inside the function, caller gets executed.
I have a reimplemented comboBox that performs dynamic search and autocompletion (code isn't mine). The problem is when I type something, that doesn't match any value in combobox list and press enter - I receive an empty string. But I wish to receive instead an old value, that was in combobox before I started to type other value. Could anybody help me with that?
Also I want to ask the meaning of 2 strings in ExtendedComboBox class (as long as code isn't mine):
inside function on_completer_activated there is expression if text: ; I can't understand what does it mean, because I always write the whole expression (like if text == True: or something like that)
I don't understand the meaning of [str] in line self.activated[str].emit(self.itemText(index)). I have never seen this kind of construction in pyqt when something in square brackets comes directly after a signal.
code:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class ExtendedComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
super(ExtendedComboBox, self).__init__(parent)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setEditable(True)
# add a filter model to filter matching items
self.pFilterModel = QtCore.QSortFilterProxyModel(self)
self.pFilterModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.pFilterModel.setSourceModel(self.model())
# add a completer, which uses the filter model
self.completer = QtWidgets.QCompleter(self.pFilterModel, self)
# always show all (filtered) completions
self.completer.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion)
self.setCompleter(self.completer)
# connect signals
self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
self.completer.activated.connect(self.on_completer_activated)
# on selection of an item from the completer, select the corresponding item from combobox
def on_completer_activated(self, text):
if text:
index = self.findText(text)
self.setCurrentIndex(index)
self.activated[str].emit(self.itemText(index))
# on model change, update the models of the filter and completer as well
def setModel(self, model):
super(ExtendedComboBox, self).setModel(model)
self.pFilterModel.setSourceModel(model)
self.completer.setModel(self.pFilterModel)
# on model column change, update the model column of the filter and completer as well
def setModelColumn(self, column):
self.completer.setCompletionColumn(column)
self.pFilterModel.setFilterKeyColumn(column)
super(ExtendedComboBox, self).setModelColumn(column)
class ComboBox_Model(QtCore.QAbstractListModel):
def __init__(self, data_list = [], parent = None):
super(ComboBox_Model, self).__init__()
self.data_list = data_list
def rowCount(self, parent):
return len(self.data_list)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
value = self.data_list[row]
return value
if role == QtCore.Qt.EditRole:
row = index.row()
value = self.data_list[row]
return value
class Mainwindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.combobox = ExtendedComboBox()
self.layout_1 = QtWidgets.QHBoxLayout()
self.layout_1.addWidget(self.combobox)
self.setLayout(self.layout_1)
data = ['some text to display', 'other text to display', 'different text']
self.model = ComboBox_Model(data)
self.combobox.setModel(self.model)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
application = Mainwindow()
application.show()
sys.exit(app.exec())
When a combobox is set as editable, by default allows insertion of non existing items at the bottom of the current model when pressing return. Since the model used in that code is not editable, when pressing return with unrecognized text the combobox is unable to add the new item (and select it), which results in setting the index to -1.
You can connect to the embedded QLineEdit returnPressed signal and check whether the current index is valid or not; this is possible because the signal is also previously connected to the combobox insertion, so when you receive the signal the combo has already tried to add the new item and eventually set the (possibly) invalid index.
In order to store the previous index, just connect to the currentIndexChanged() and save it as long as it's greater or equal to 0.
class ExtendedComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
# ...
self.lineEdit().returnPressed.connect(self.returnPressed)
self.currentIndexChanged.connect(self.storePreviousIndex)
self.previousIndex = self.currentIndex()
def storePreviousIndex(self, index):
if index >= 0:
self.previousIndex = index
def returnPressed(self):
if self.currentIndex() < 0 or self.currentText() != self.itemText(self.currentIndex()):
self.setCurrentIndex(self.previousIndex)
Note that the second comparison in returnPressed is to add compatibility to the default internal model, in case setModel() is not called and the insertion policy is NoInsert.
About the two final questions:
the if statement checks if the condition is true or not, or, if you want, the condition is not false, as in "not nothing" (aka, False, 0, None); you can do some experiments with simple statements to better understand: if True:, if 1:, if 'something': will all result as valid conditions, while if False:, if 0: or if '': not.
some signals have multiple signatures for their arguments, meaning that the same signal can be emitted more than once, each time with different types of arguments; for example the activated signal of QComboBox is emitted twice, the first time as int with the new current index, then with the new current text; whenever you want to connect to (or emit) an overload that is not the default one, you need to specify the signature in brackets. In the case above, the signal is explicitly emitted for the str signature only (I don't know why the int was not, though). Note that overloaded signals are being gradually removed in Qt (in fact, the [str] signature of activated() is considered obsolete since Qt 5.14).
from tkinter import *
from tkinter.ttk import Combobox
v1=[]
root = Tk()
root.geometry('500x500')
frame1=Frame(root,bg='#80c1ff',bd=5)
frame1.place(relx=0.5,rely=0.1,relwidth=0.75,relheight=0.1,anchor='n')
lower_frame=Frame(root,bg='#80c1ff',bd=10)
lower_frame.place(relx=0.5,rely=0.25,relwidth=0.75,relheight=0.6,anchor='n')
v=[]
def maincombo():
Types=["MA","MM","MI","SYS","IN"]
combo1=Combobox(frame1,values=Types)
combo1.place(relx=0.05,rely=0.25)
combo2=Combobox(frame1,values=v)
combo2.bind('<<ComboboxSelected>>', combofill)
combo2.place(relx=0.45,rely=0.25)
def combofill():
if combo1.get()=="MA":
v=[1,2,3,45]
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
if combo1.get()=="MM":
v=[5,6,7,8,9]
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
maincombo()
root.mainloop()
I want to populate the one combobox based on selection of other combobox I,e types.But failed to do so with simple functions.
Looking at you code, most of what you need is already there. The changes I have made are as follows:
Bound to combo1 rather than combo2 (as combo1 is the one you want to monitor)
Set combo1 and combo2 as global variables (so they can be used in the combofill method)
Set the combofill method to accept the event arg (it would raise a TypeError otherwise)
Use the .config method on combo2 rather than creating a new one each time
Set combo2 to be empty when neither "MA" or "MM" are selected
Here is my implementation of that:
from tkinter import *
from tkinter.ttk import Combobox
v1=[]
root = Tk()
root.geometry('500x500')
frame1=Frame(root,bg='#80c1ff',bd=5)
frame1.place(relx=0.5,rely=0.1,relwidth=0.75,relheight=0.1,anchor='n')
lower_frame=Frame(root,bg='#80c1ff',bd=10)
lower_frame.place(relx=0.5,rely=0.25,relwidth=0.75,relheight=0.6,anchor='n')
v=[]
def maincombo():
global combo1, combo2
Types=["MA","MM","MI","SYS","IN"]
combo1=Combobox(frame1,values=Types)
combo1.place(relx=0.05,rely=0.25)
combo1.bind('<<ComboboxSelected>>', combofill)
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
def combofill(event):
if combo1.get()=="MA":
v=[1,2,3,45]
elif combo1.get()=="MM":
v=[5,6,7,8,9]
else:
v=[]
combo2.config(values=v)
maincombo()
root.mainloop()
A couple other ideas for potential future consideration:
I would recommend using the grid manager rather than the place manager as it will stop widgets overlapping, etc. (on my system, combo2 slightly covers combo1)
Use a dictionary rather than if ... v=... elif ... v= ... and then use the get method so you can give the default argument. For example:
v={"MA": [1,2,3,45],
"MM": [5,6,7,8,9]}. \
get(combo1.get(), [])
EDIT:
Responding to the question in the comments, the following is my implementation of how to make a "toggle combobox" using comma-separated values as requested.
As the combobox has already overwritten the value of the text area when our <<ComboboxSelected>> binding is called, I had to add a text variable trace so we could keep track of the previous value of the text area (and therefore append the new value, etc.). I am pretty sure that explanation is completely inadequate so: if in doubt, look at the code!
from tkinter import *
from tkinter.ttk import Combobox
root = Tk()
def log_last():
global last, cur
last = cur
cur = tx.get()
def append_tx(event):
if last:
v = last.split(",")
else:
v = []
v = list(filter(None, v))
if cur in v:
v.remove(cur)
else:
v.append(cur)
tx.set(",".join(v))
combo.selection_clear()
combo.icursor("end")
last, cur = "", ""
tx = StringVar()
combo = Combobox(root, textvariable=tx, values=list(range(10)))
combo.pack()
combo.bind("<<ComboboxSelected>>", append_tx)
tx.trace("w", lambda a, b, c: log_last())
root.mainloop()
I am writing a program in Python 3.6 using Tkinter where a customer has multiple(11) entry fields. I want these entry fields to only accept integers and also be able to define the maximum amount of characters.
I already have a function that does this. But this function only works for one entry field. I have tried entering variables with calling the function so it changes another entry field for example. I was not able to do this.
This is the function I have that works with 1 entry field.
def limitMuntgeld(self, *args):
value = self.invoerM.get()
if len(value) > 5:
self.invoerM.set(value[:5])
if value.lower() in "abcdefghijklmnopqrstuvwxyz-=[];/":
self.invoerM.set(value[:0])
This is the example entry field code that works with the function
self.invoerMuntgeld = Entry(self, font=('Arial', 14), textvariable=self.invoerM)
This is combined with a trace on the entry field posted below.
self.invoerM = StringVar()
self.invoerM.trace('w', self.limitMuntgeld)
I have also tried it with vcmd and validatecommand. However, no good results.
My endresult would be one function working with all entry fields. If anyone has any suggestions, I am all ears!
The proper way to do entry validation is with the validatecommand option rather than using trace. With the validation feature built into the widget you don't need a reference to the widget itself (though you can use it if you want).
When the validatecommand is run, you can have it pass in what the new value will be if the input is valid. You only need to check this value and then return True or False, without having to know which widget it applies to.
For example:
import tkinter as tk
def validate_input(new_value):
valid = new_value .isdigit() and len(new_value) <= 5
return valid
root = tk.Tk()
validate = root.register(validate_input)
for i in range(10):
entry = tk.Entry(root, validate="key", validatecommand=(validate, "%P"))
entry.pack(side="top", fill="x")
root.mainloop()
For information about what %P represents, and what else can be used as arguments to the command, see this question: Interactively validating Entry widget content in tkinter
I am currently learning the tkinter basics and I'm building a small, super-simple program to test my knowledge on some of the most basic widgets.
I am having a problem with validation and an entry, possibly because of my lack of understanding in the matter... This poses three questions:
1 - How to do what was done here: https://stackoverflow.com/a/4140988/2828287 without the class part. Just doing it when the script runs.
2 - What are all those self. and .self doing there? Which ones are there because that is a class, and which ones are there because of the validating method itself??
3 - What's wrong in my code? based in this explanation >> http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html
from tkinter import *
from tkinter import ttk
# function that should take the '%d' replacer and only validate if the user didn't delete
def isOkay(self, why):
if why == 0:
return False
else:
return True
okay = entry.register(isOkay) # didn't understand why I had to do this, but did it anyway...
entry = ttk.Entry(mainframe, validate="key", validatecommand=(okay, '%d'))
# the mainframe above is a ttk.Frame that contains all the widgets, and is the only child of the usual root [ = Tk()]
entry.grid(column=1,row=10) # already have lots of stuff on upper rows
The error I'm getting goes like this:
"NameError: name 'entry' is not defined"
I've tried to change the order of things, but there's always one of these errors.. It points to the line where I do the .register() stuff
--EDITED CODE--
This doesn't throw me an error, but still allows me to delete...
def isOkay(why):
if (why == 0):
return False
else:
return True
okay = (**root**.register(isOkay), "%d")
entry = ttk.Entry(mainframe, validate="key", validatecommand=okay)
entry.grid(column=1,row=10)
(where the 'root' part is written between ** **, does it have to be the root? Or it can be any parent of the widget that is going to use that? Or it has to be the immediate parent of it?
for instance, I have:
root >> mainframe >> entry. Does it have to be root, mainframe, or could be both?)
All usages of self are due to the use of classes. They have absolutely nothing to do with the validation. Nothing at all.
Here's an example without using classes, and without the long comment describing the validation function:
import Tkinter as tk
def OnValidate(d, i, P, s, S, v, V, W):
print "OnValidate:"
print "d='%s'" % d
print "i='%s'" % i
print "P='%s'" % P
print "s='%s'" % s
print "S='%s'" % S
print "v='%s'" % v
print "V='%s'" % V
print "W='%s'" % W
# only allow if the string is lowercase
return (S.lower() == S)
root = tk.Tk()
vcmd = (root.register(OnValidate),
'%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
entry = tk.Entry(root, validate="key",
validatecommand=vcmd)
entry.pack()
root.mainloop()
Note: the point of registering a command is to create a bridge between the underlying tcl/tk engine and the python library. In essence it creates a tcl command that calls the OnValidate function, giving it the supplied arguments. This is necessary because tkinter failed to provide a suitable interface to the input validation features of tk. You don't need to do this step if you don't want all of the fancy variables (%d, %i, etc).
The error NameError: name 'entry' is not defined is because you are using entry before you define what entry is. One of the benefits of using classes is that it allows you define methods further down in the file than where you use them. By using a procedural style you are forced to define functions before they are used*.
* technically speaking, you always have to define functions before they are used. In the case of using classes, you don't actually use the methods of a class until after you've created the instance of the class. The instance isn't actually created until very near the end of the file, which lets you define the code well before you use it.