Basic Entries
=============
.. figure:: ../figures/ent_basic.webp
:width: 142
:height: 24
:alt: basic tkinter entry string validation
:align: center
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
`_.
.. topic:: A Little Demo
.. figure:: ../figures/ent_demo.webp
:width: 327
:height: 233
:alt: demonstration tk validate
:align: center
If you are new to this run the validate_demo program. The original was
posted by Bryan Oakley in Stackoverflow, as the various inputs are added
we can see how the inputs change the values of the substitutes.
.. container:: toggle
.. container:: header
*Show/Hide Code* 00validate_demo.py
.. literalinclude:: ../examples/entry/00validate_demo.py
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.
.. figure:: ../figures/ent_str.webp
:width: 145
:height: 27
:alt: string tkinter entry validation
:align: center
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
...
.. topic:: String Validation Function
First check whether the first character is a capital
letter or not. Then there are two queries that check whether the input
is an alpha numeric character or one of a set of punctuation options.
Although the logic can be followed, the fact that at every query apart
from the first, has an elif or else query should raise a warning flag. As
it stands the validation function is doing its job but there is room
for improvement.
Without peeking see if you can improve on the validation function,
remember the first letter is capitalalised, thereafter characters, or some
prescribed punctuation. If at any time a character such as **%** can be
inserted then the validation is no longer working. After making your changes
test it with pylint, if the score is more than 9.5 and it works - well done!
.. container:: toggle
.. container:: header
*Show/Hide Code* 02entry_str.py
.. literalinclude:: ../examples/entry/02entry_str.py
:emphasize-lines: 25-34, 38
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.
.. container:: toggle
.. container:: header
*Show/Hide Code* 02entry_str_better.py
.. literalinclude:: ../examples/entry/02entry_str_better.py
:emphasize-lines: 28-39, 43
.. 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
.. topic:: Better String Validation Function
First ensure that the index is an integer to allow valid comparisons.
All the string validation queries are dependant on the fact that we are
inserting, so these are nested. The elif clauses were replaced by if
clauses, which at first glance may not seem logically equivalent, but
they work as the queries are independant from each other.
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.
.. container:: toggle
.. container:: header
*Show/Hide Code* 03entry_isnumeric.py
.. literalinclude:: ../examples/entry/03entry_isnumeric.py
:emphasize-lines: 24-34, 38
The try construct::
def is_okay(text):
print(text)
if text in("", "-"):
return True
try:
int(text)
except ValueError:
return False
return True
.. topic:: Integer Validation Function
Before checking that the input is an integer or not, first of all validate
the input required to write the first character when making an integer.
This can only be an integer, a minus sign or empty. Since we are looking
at the first character one could use either the text ``%P`` or the input
``%S``, but as we want to check whether the first character is empty or
not it is best to use text. The outcome
of either of these options is **True**, but it is not necessarily
**False** if it is not one of these options.
In the second part of the validation check whether it is an integer or not.
Once starting to insert a single integer either by itself or combined
with a minus sign, is accepted by the integer validation.
Both parts are independant, in that if it does not satisfy the first part
it could still be valid for the second part. Therefore use sequential
rather than nested queries.
.. container:: toggle
.. container:: header
*Show/Hide Code* 04entry_negative_integers.py
.. literalinclude:: ../examples/entry/04entry_negative_integers.py
:emphasize-lines: 24-30
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
.. topic:: Float Validation Function
The float validation runs along similar lines to the integer validation.
The main differences being that we are checking for a float, and there
are more options for the first character or two, as there can be a decimal
point or a minus sign and a decimal point. Otherwise it is just a copy
of the integer validation.
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 :ref:`09float_function.py` ).
.. _entry-float:
|
.. container:: toggle
.. container:: header
*Show/Hide Code* 05entry_float.py
.. literalinclude:: ../examples/entry/05entry_float.py
:emphasize-lines: 19-21, 36-42, 47