Hello all,
I’ve refrained from asking here as I was trying to figure this out myself, and while I have come up with a working solution, it feels so complicated and unoptimised; I’m sure there must be a better way.
The ttk.Spinbox widgets in my application are used to enter a monetary value, and I’m trying to implement the following behaviours:
-
values the user can enter should be restricted to amounts in the ones, tens or hundreds of pounds (or euros, dollars, dollarydoos, etc.) and may include a decimal point followed by the number of pence (or cents). Users may also enter 0.00
-
if the user enters an incomplete amount, i.e an amount that does not include the decimal point and pence, the value should be completed for them upon the widget losing focus, or the user pressing enter
-
if the user enters one of a subset of amounts that are essentially equal to zero, the value of the widget will be set to 0.00
I feel like this is pretty standard behaviour that similar programs should exhibit, so I feel like there must be a more standard way to achieve it. My own solution is composed of two parts, first a validation handler that checks the hypothetical ‘new’ value against a list of regular expressions, and returns true if it finds a match. This is the only way I could get the widget to check the users input upon each keypress and in real time while allowing all valid additions and disallowing all invalid ones…
def rateValHandler(new_value, current_value, event_type):
valid = False
patterns = ['^0.00$', '^\d{,3}$', '^\d{,3}\.$', '^\d{,3}\.\d{1}$', '^\d{,3}\.\d{2}$']
try:
if new_value[0] == '0' and new_value[1] != '.':
return valid
except IndexError:
pass
if event_type == 'forced':
for i in patterns:
if match(i, current_value):
valid = True
if event_type == 'key':
for i in patterns:
if match(i, new_value):
valid = True
if event_type == 'focus':
pass
return valid
This part of the code ensures both that no invalid characters are entered, and that all valid characters entered are in the right position (e.g a decimal point can only be new_value[i] for i in (1, 2, 3), if new_value[0] == ‘0’ then new_value[1] == ‘.’, etc.) however it doesn’t handle the auto-completion of entries upon a <KeyPress-Return> or <FocusOut> event.
For that, I bound to a <KeyPress-Return> and <FocusOut> event on all widgets with the bindtag ‘TSpinbox’ an event handler that I called rateDecimalCompletion…
def rateDecimalCompletion(e):
value = e.widget.get()
### a subset of values are equal to 0.00 ###
for i in ['', '.', '.0', '.00', '0', '0.', '0.0']:
if value == i:
e.widget.set('0.00')
e.widget.icursor('end')
return
### a decimal point followed by one digit is bookended by zeros (0); a decimal point followed by two digits is prefixed with a
# zero; a value with only one digit after the decimal point is appended with a zero. A decimal point on it's own is handled
# by the previous block of code, and a single digit followed by a decimal point is handled by the block of code after this one.
# Notice that a subset of the values that would be matched by the regex expressions in this code are handled instead by the
# previous block which, upon ascertaining that the value of 'value' is part of that set, sets the value of the spinbox widget to
# '0.00' and returns to the main routine - meaning this snippet never handles any value that is a member of that set ###
if match('^\.\d$', value):
e.widget.set('0' + value + '0')
if match('^\.\d{2}$', value):
e.widget.set('0' + value)
if match('^\d{1,3}\.\d$', value):
e.widget.set(value + '0')
### values ending in a decimal point are appended with two zeros (00); like the above code snippet, values handled by the initial
# code snippet will never make it this far, e.g '0.0' and '0.' ###
elif value.endswith('.'):
e.widget.set(value + '00')
elif len(value) < 4 and '.' not in value:
e.widget.set(value + '.00')
e.widget.icursor('end')
So basically it all works okay, there is no value that rateValHandler will allow to be entered into the spinbox that is a) not valid in the context of the program or b) that the auto completion code in rateDecimalCompletion cannot complete in the event that the user hits enter or tabs out of the widget before entering a fully formed monetary value.
I can’t say I’m proud of this hodgepodge, and I’m sure there must be an easier way. If anyone can give me any advice it would be much appreciated!