Basic Entries#
As a start create an Entry with validation that checks whether the input is
alphabetic or not. We require both options validate and validatecommand.
Validate is using 'key' which means that after every keyboard entry the
callback function is_okay runs and checks the input, Entry then displays
this input if allowed. The validatecommand needs to know the callback
function and options being used, in this case '%P' which
is the allowed text. As the input is entered the contents of '%P'
increases with each correct keystroke, which is shown in the print output.
The callback function is_okay needs to be registered using the hook
vcmd before it can be used. As root is the parent widget it is used for
registration in this example, later on we can use other widgets such as a
Frame:
from tkinter import Tk
from tkinter.ttk import Entry, Style
root = Tk()
s = Style()
s.theme_use('default')
def is_okay(text):
print(text)
if text.isalpha():
return True
else:
return False
vcmd = root.register(is_okay)
ent0 = Entry(root, validate='key', validatecommand=(vcmd, '%P'))
ent0.pack()
root.mainloop()
Note
When creating the callback function the first few examples will
have explicit else: clauses to show the True and False return logic.
This is stylistically wrong, but I found it helped me in formulating the
conditions.
Try using different characters, numbers, punctuation marks and a space or two - anything other than characters will not show, The operation of validation is immediate so all the user requires to know is what is allowed. Now try to correct the first letter.
When judging whether an input is correct or not and can be validated, we need to use more criteria than just whether the text is acceptable or not, to this end tkinter (tcl/tk) has provided eight special substitutions to help validating command, to start with concentrate on the four that we shall be using, %P, %S, %i and %d (text if allowed, input, index and action). Find the official documentation here Link to the Widget Specifications and Validation.
When dealing with strings the first character is normally capitalised,
thereafter characters, numbers and simple punctuation, so
we need to know the current indexed position before the current input,
'%S', can be accurately checked.
The first character is restricted to a capital letter, but if wrong it needs
to be corrected, in fact the input should be able to be corrected anywhere.
Sometimes the normal if ... elif clauses can give unexpected
results, so think in terms of progressive queries such that the next query
is only true if all previous queries are satisfied. In general start with
the widest ranging query and progressively narrow it down to the last query.
It also makes sense to deal with the queries in the order that they are displayed, particularly when they have disparate conditions such as the first and subsequent characters of strings. Here it is often useful to have consequetive queries rather than nest them.
One complication is to find the right criterion for the query. Are we checking the complete text or only the input, unfortunately there is no hard and fast rule, other than if it works it probably is right.
In this example use both text and input:
def is_okay(text, input): # %P, %S
if input.isupper() and len(text)==1:
return True
elif input in (",", ".", "'", " ") and len(text)>1:
return True
elif input.isalnum() and len(text)>1:
return True
else:
return False
....
ent0 = Entry(root, validate='key', validatecommand=(vcmd, '%P', '%S'))
....
Add a check for an empty input to enable corrections by the user. Do not use a single nested set of queries, as the the first character has a different condition to the following characters:
def is_okay(input, action):
....
elif text == "":
return True
...
Show/Hide Code 02entry_str.py
"""basic entry for string allowing additional characters,
the logic in the validation function will be improved
"""
from tkinter import Tk
from tkinter.ttk import Entry, Style, Label
root = Tk()
style = Style()
style.theme_use('default')
def is_okay(text, input):
""" validation function - demo purposes only
Parameters
----------
text : str
text if allowed
input : str
current input
Returns
-------
boolean
"""
if input.isupper() and len(text) == 1:
return True
elif input in (",", ".", "'", " ") and len(text) > 1:
return True
elif input.isalnum() and len(text) > 1:
return True
elif text == "":
return True
else:
return False
vcmd = root.register(is_okay)
lab = Label(root, text= 'String input, starts with Capital letter')
lab.pack(padx=10, pady=10)
ent0 = Entry(root, validate='key', validatecommand=(vcmd, '%P', '%S'))
ent0.pack(padx=10)
root.mainloop()
Test this out, is it alright or not? We can delete the first letter which has to be a capital, we can add different letters and figures, plus the special characters. Did you try deleting - went well didn't it until the second position. It is possible to change the first character before other characters were added - a case of the Eric Morecombe's - all the right notes just not necessarily in the right order as in Andre Preview. Once the input becomes longer deleting the first character is no longer possible.
If we are honest there was no real need to test the length of %P, it's
better to use the built in function %i, also we are not using %P elsewhere.
We need a better test for deleting, try using %d the action option. Use
it to test that we are inserting, apply all the normal criteria, then
change the last else to return True which allows deletions. This
option highlights the fact that the query can have two correct answers, just
that we tackle the first answer by a nested set of queries to check the input,
while the second option shows we are not inserting but deleting.
Show/Hide Code 02entry_str_better.py
"""basic entry for string allowing additional characters, made better
but not the best
"""
from tkinter import Tk
from tkinter.ttk import Entry, Style, Label
root = Tk()
style = Style()
style.theme_use('default')
def is_okay(index, action, input):
""" validation function
Parameters
----------
index : str
index
action : str
action
input : str
current input
Returns
-------
boolean
"""
index = int(index)
if action == '1':
if input.isupper() and index == 0:
return True
if input in (",", ".", "'", " ") and index > 0:
return True
if input.isalnum() and index > 0:
return True
else:
return False
else:
return True
vcmd = root.register(is_okay)
lab = Label(root, text= 'String input, starts with Capital letter')
lab.pack(padx=10, pady=10)
ent0 = Entry(root, validate='key', validatecommand=(vcmd, '%i', '%d', '%S'))
ent0.pack(padx=10)
root.mainloop()
Note
All Validating Substitutes are Strings
Whenever using action %d or index %i use quoted numbers, or change the substitute variable to an integer.
It is better in that the expected behaviour is happening, but stylistically it isn't optimal:
index = int(index)
if action == '1':
if input.isupper() and index == 0:
return True
if input in (",", ".", "'", " ") and index > 0:
return True
if input.isalnum() and index > 0:
return True
else:
return False
else:
return True
Integer Entry#
Integer entry in some ways is simpler than a set of characters. There are no spaces, punctuation or letters, therefore the entire entry can only be integers with maybe a minus at the beginning.
First change the callback function, using only '%P'. Now make a test on whether the input is an integer or not:
def is_okay(text):
if int(text):
return True
else:
return False
.....
ent0 = Entry(root, validate='key', validatecommand=(vcmd, '%P'))
Test this and you should find that neither a minus sign is allowed nor a
correction can be made. Change the second line of the callback function to
test against isdigit:
if text.isdigit():
It is just as restrictive as int, but notice anything different? You
should notice that no warnings were generated when a decimal point was tried,
which allows validation process to continue. We still have the problem of
trying to change the first integer, use a similar solution to that
for characters.
It would also be useful to have a method that prevents input beyond upper
and lower limits, so try range to provide the limits:
if text.isnumeric(): # int(inp):
## do not use range, change 1 to 11 and test ##
if int(text) in range(1, 63):
return True
else:
return False
return True
elif text == "":
return True
else:
return False
As it stands this is only applicable to positive integers, and our test for an empty first entry is tacked on. The limits work but the user does not know the reason why an input is not accepted - the simplest solution is to provide labels showing the limits - an alternative solution would provide feedback.
Now change the lower range value from 1 to 11, no input is allowed. To overcome this problem one needs to either check the input and use limits only after the entry is completed or separate out the limit checking as will be done later see 07layout_integer.py :ref: Layout Integer.
Now enable minus integers:
elif text in ("", "-"):
Only using "%P" fails since isnumeric does not recognise negative numbers,
(by the way it fails with isdigit and isdecimal as well). As seen before,
using int() with the if conditional construct produces errors that stops
validating. This can be solved by using a try and except construct
with int() for negative integers, only use an if construct with
isnumeric and positive integers.
Show/Hide Code 03entry_isnumeric.py
"""basic entry for positive integers, with validation,
optional range checking
"""
from tkinter import Tk
from tkinter.ttk import Entry, Style, Label
root = Tk()
style = Style()
style.theme_use('default')
def is_okay(text):
""" validation function
Parameters
----------
text : str
text if allowed
Returns
-------
boolean
"""
print(text)
if text.isnumeric(): # int(inp):
## do not use range, change 1 to 11 and test ##
if int(text) in range(1, 63):
return True
else:
return False
return True
elif text == "":
return True
else:
return False
vcmd = root.register(is_okay)
lab = Label(root, text= 'Integer input, a range exists')
lab.pack(padx=10, pady=10)
ent0 = Entry(root, validate="key", validatecommand=(vcmd, "%P"))
ent0.pack(padx=10)
root.mainloop()
The try construct:
def is_okay(text):
print(text)
if text in("", "-"):
return True
try:
int(text)
except ValueError:
return False
return True
Show/Hide Code 04entry_negative_integers.py
"""basic entry for integer, validation, allows negative numbers,
no range checking
"""
from tkinter import Tk
from tkinter.ttk import Entry, Style, Label
root = Tk()
style = Style()
style.theme_use('default')
def is_okay(text):
""" validation function
Parameters
----------
text : str
text if allowed
Returns
-------
boolean
"""
print(text)
if text in("", "-"):
return True
try:
int(text)
except ValueError:
return False
return True
vcmd = root.register(is_okay)
lab = Label(root, text= 'Integer input, negative allowed, no limit')
lab.pack(padx=10, pady=10)
ent0 = Entry(root, validate="key", validatecommand=(vcmd, "%P"))
ent0.pack(padx=10)
root.mainloop()
Float Entry#
Float entry should be similar to integer, with negative numbers and decimal points. As suspected, unless special provision is made for these two inputs, it will create an error and not be read into the entry. To be consistent "-." is also a special case.
As there is no equivalent to isdigit for testing float we need to use
float() within a try construct, (otherwise the validation stops
working if an error is generated):
def is_okay(text):
print(text)
if text in ("", "-", ".", "-."):
return True
try:
float(text)
except ValueError:
return False
return True
It may be useful to make the entry font bold, even though Entry is a themed widget, changes to the font are made directly in its options rather than using Style. This also applies to the themed Combobox and Spinbox:
from tkinter import Tk, font
.....
def_font = font.nametofont('TkHeadingFont')
font_family = def_font.actual()['family']
font_size = def_font.actual()['size'] + 2
......
ent0 = Entry(....font=(font_family,font_size,'bold'))
.....
If limits are required then it is best to separate these from the validation, and have a labelframe to enclose the limit labels and entry (see later 09float_function.py ).
Show/Hide Code 05entry_float.py
"""basic entry for float, correction, allows negative input,
optional range checking
Parameters
----------
None
Returns
-------
None
"""
from tkinter import Tk, font
from tkinter.ttk import Entry, Style, Label
root = Tk()
style = Style()
style.theme_use('default')
def_font = font.nametofont('TkDefaultFont')
font_family = def_font.actual()['family']
font_size = def_font.actual()['size'] + 2
def is_okay(text):
""" validation function
Parameters
----------
text : str
text if allowed
Returns
-------
boolean
"""
print(text)
if text in ("", "-", ".", "-."):
return True
try:
float(text)
except ValueError:
return False
return True
vcmd = root.register(is_okay)
lab = Label(root, text= 'Float input, negative allowed, no limit')
lab.pack(padx=10, pady=10)
ent0 = Entry(root, validate="key", validatecommand=(vcmd, '%P'),
font=(font_family, font_size, 'bold'))
ent0.pack(padx=10)
root.mainloop()