I would like to share the solution I have arrived at below. I discovered that when a string/substring in the ttk.Entry
textfield is selected, tkinter will default to performing the validation in 2 steps. (1) Treat the selected string/substring as the first edit to be done. (2) Treat the keypressed entry as the second edit that is to be done. So, validatecommand
will be called twice. I have also added some comments in the script.
import tkinter as tk # python 3.x
import tkinter.ttk as ttk # python 3.x
class Example(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
# %P = value of the entry if the edit is allowed
# %S = the text string being inserted or deleted, if any
# %s = value of entry prior to editing
vcmd = (self.register(self.onValidate),'%P', '%S', '%s')
self.text = tk.StringVar()
self.entry = ttk.Entry(self, validate="key", validatecommand=vcmd)
self.entry.pack(side="top", fill="x")
def onValidate(self, P, S, s):
# Disallow anything but '0123456789.+-'
selected = None
print('\nP={}, S={}, s={}'.format(P, S, s) )
try:
if S in '0123456789.+-' or float(S):
if self.entry.selection_present():
print('With Selection')
selected = self.entry.selection_get()
print('selected = ', selected )
# Notes:
# - When .selection_present()=True, I discovered that
# tkinter will return by default:
# P = s w/o 'selected'
# S = 'selected' and not the keypressed
# s = value of entry prior to editing.
# - I should "return True" so that tkinter will trigger method
# self.onValidate() again. This time,
# P = value of the entry if the keypress edit is allowed.
# S = the key pressed
# s = P from previous attempt.
# As such satisfy .selection_present()=False.
return True
else:
print('No Selection')
try:
float(P); print('True')
return True
except ValueError:
print(ValueError, 'float({}) False'.format(P))
return False
else:
print('S in 0123456789.+- False')
return False
except ValueError:
print('Try with Exception')
print(ValueError, 'float({}) False'.format(P))
return False
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Update:
The script below shows an improved algorithm to ONLY allow float type entries (including those with exponent) in a tkinter Entry
widget. Please use this.
Advantages:
- This algorithm allows float type numbers and its exponents to be entered to a tkinter Entry widget.
- This algorithm avoids the
.selection_present()
method given that
it uses %d
which is an inherent callback substitution code of
validatecommand
. %d
has values to indicate scenarios associated to deletion(or selection), insertion, and others (i.e. "focus in", "focus out", or "changes in textvariable values".
- The scenarios considered in this algorithm are more encompassing than my first algorithm. (Do alert me if you notice any relevant scenario being left out. Thank you.).
Improved Algorithm:
import tkinter as tk # python 3.x
import tkinter.ttk as ttk # python 3.x
class Example(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
# %d = Type of action (1=insert, 0=delete, -1 for others)
# %P = value of the entry if the edit is allowed
# %S = the text string being inserted or deleted, if any
vcmd = (self.register(self.onValidate_Float), '%d','%P','%S')
self.entry = ttk.Entry(self, validate="key", validatecommand=vcmd)
self.entry.pack(side="top", fill="x")
def onValidate_Float(self, d, P, S):
'''Allow only float type insertions (including exponents).
Notes: 1. The culminated insertions can fail to convert to a float.
This scenario occurs ONLY when the exponent entry is not
completed, i.e. when 'e-' and 'e+' are supplied only.
2. User of this method should remember that when they bind a
handle and '<KeyPress-Return>' to the tkinter Entry widget,
the handle can encounter error associated with processing
"float(numeric)" when numeric=1.4e- or 1.4e+ (as example).
3. I discovered that validatecommand uses %d to determine
the type of actions it take. As such, it is useful to
structure validatecommand instructions according to scenarios
d='0', d='1' and d='-1'.
'''
def _checkDecimal(P):
'''Return True when decimal does not occur in exponent.'''
decimal_index = P.find('.')
exponent_index = P.find('e')
if exponent_index > decimal_index:
return True
else:
return False
print('\nd={}, P={}, S={}'.format(d, P, S) )
if d == '0': #Delete Selection or keypress "Delete"/"Backspace"
print('Allow delete action regardless of keypress.')
return True
elif d == '1': #Insert keypress
print('d==1, Insert keypress.')
try:
if S in '0123456789e.+-':
float(P); print('True')
return True
else:
print('False')
return False
except ValueError:
print('float({}) ValueError.'.format(P))
if P.count('e')>1: return False
if P.count('e.')>0: return False
if P.count('-e')>0: return False
if P.count('+e')>0: return False
if P.find('e') == 0: return False
if P.count('.')>1: return False
if P[0]=="-":
print('P[0]=="-"')
if P.count("e-")>=1:
print('P.count("e-")>=1')
if P.count("-")>2: return False
if P.count("+")>0: return False
if not _checkDecimal(P): return False
elif P.count("e+")>=1:
print('P.count("e+")>=1')
if P.count("+")>1: return False
if P.count("-")>1: return False
if not _checkDecimal(P): return False
else:
print('no e- or e+')
if P.find('.') == 1: return False #disallow '-.'
if P.find('+') >= 1: return False #disallow '-+'
if P.find('-') >= 1: return False #disallow '--'
if P.count("-")>1: return False
if P.count("+")>1: return False
elif P[0]=="+":
print('P[0]=="+"')
if P.count("e-")>=1:
print('P.count("e-")>=1')
if P.count("-")>1: return False
if P.count("+")>1: return False
if not _checkDecimal(P): return False
elif P.count("e+")>=1:
print('P.count("e+")>=1')
if P.count("+")>2: return False
if P.count("-")>0: return False
if not _checkDecimal(P): return False
else:
print('no e- or e+')
if P.find('.') == 1: return False #disallow '+.'
if P.find('+') >= 1: return False #disallow '++'
if P.find('-') >= 1: return False #disallow '+-'
if P.count("-")>1: return False
if P.count("+")>1: return False
else:
print('P[0] is a number')
if P.count("e-")>=1:
print('P.count("e-")>=1')
if P.count("-")>1: return False
if P.count("+")>0 : return False
if not _checkDecimal(P): return False
elif P.count("e+")>=1:
print('P.count("e+")>=1')
if P.count("+")>1: return False
if P.count("-")>0: return False
if not _checkDecimal(P): return False
else:
print('no e- or e+')
if P.count("-")>0: return False
if P.count("+")>0: return False
return True #True for all other insertion exceptions.
elif d == '-1': #During focus in, focus out, or textvariable changes
print('d==-1, During focus in, focus out, or textvariable changes')
return True
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()