The following script creates a ttk.Entry
widget that only accepts entries that can be converted to a float type. When I use the mouse pointer to select the typed entries, followed by pressing new numeric entries, I would like the new numeric entries to replace the selected entries. Presently that behavior does not occur. Instead, the new numeric entries will appear on the left of the selected numbers. How do I get the replacement behavior I require?
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
vcmd = (self.register(self.onValidate),'%P', '%S')
self.entry = ttk.Entry(self, validate="key", validatecommand=vcmd)
self.entry.pack(side="top", fill="x")
def onValidate(self, P, S):
# Disallow anything but '0123456789.+-'
if S in '0123456789.+-':
try:
float(P)
print('float(P) True')
return True
except ValueError:
print('float(P) False')
return False
else:
print('S in 0123456789.+- False')
return False
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Update: Using the .bind
method on '<ButtonRelease>'
event, I discovered that the selected typed entries in the ttk.Entry
widget can be obtained with the .selection_get()
method. However, I have yet to figure out how to link these approaches to get the desired behavior.
Append these sentences to the end of the __init__()
method.
self.entry.bind( '<ButtonRelease>', self.selecttext )
def selecttext(self, event):
try:
print( 'selection = ', self.entry.selection_get() )
except tk.TclError:
pass
What is happening is this: when you select a range of text and then press a key, tkinter must do two things: it must delete the selected text and then it must insert the new text.
The handler is first called for the delete. Because the delete causes the entry widget to be completely empty, and you can't convert an empty string to a float, your handler returns False
. This prevents the delete from happening.
Next, your handler is called for the insert. The old text is still there. You allow the insert, so you end up with the result that the selected text is not removed and the new text is inserted right before it.
The simplest solution is to allow for an empty string. You can then simply validate that a non-empty string can be converted to a float.
Example:
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
vcmd = (self.register(self.onValidate),'%P')
self.entry = ttk.Entry(self, validate="key", validatecommand=vcmd)
self.entry.pack(side="top", fill="x")
def onValidate(self, P):
if P.strip() == "":
# allow blank string
return True
try:
float(P)
return True
except:
return False
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
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:
.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".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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With