Events, bindings and callbacks
6.8 Putting events to work
6.8.2 Data verification
An important part of a GUI, which performs data entry, is verifying appropriate input values.
This area can consume a considerable amount of time and effort for the programmer. There are several approaches to validating input, but we will not attempt to cover all of them here.
Pmw EntryField widgets provide built-in validation routines for common entryfield types such as dates, times and numeric fields. Using these facilities can save you a considerable amount of time. Here is a simple example of using Pmw validation:
import time, string from Tkinter import * import Pmw
class EntryValidation:
def __init__(self, master):
now = time.localtime(time.time()) self._date = Pmw.EntryField(master,
labelpos = 'w', label_text = 'Date (mm/dd/yy):', value = '%d/%d/%d' % (now[1], now[2], now[0]), validate = {'validator':'date',
'format':'mdy', 'separator':'/'})
4
5
Figure 6.2 Validating entry fields (Example_6_7.py)
Example_6_7.py
1
112
C H A P T E R 6 E V E N T S , B I N D I NG S A N D C A L L B A C K S self._time = Pmw.EntryField(master,labelpos = 'w', label_text = 'Time (24hr clock):', value = '8:00:00',
validate = {'validator':'time',
'min':'00:00:00', 'max':'23:59:59', 'minstrict':0, 'maxstrict':0}) self._real = Pmw.EntryField(master,
labelpos = 'w',value = '127.2',
label_text = 'Real (50.0 to 1099.0):', validate = {'validator':'real',
'min':50, 'max':1099, 'minstrict':0},
modifiedcommand = self.valueChanged) self._ssn = Pmw.EntryField(master,
labelpos = 'w', label_text = 'Social Security #:', validate = self.validateSSN, value = '')
fields = (self._date, self._time, self._real, self._ssn) for field in fields:
field.pack(fill='x', expand=1, padx=12, pady=8) Pmw.alignlabels(fields)
self._date.component('entry').focus_set() def valueChanged(self):
print 'Value changed, value is', self._real.get() def validateSSN(self, contents):
result = -1
if '-' in contents:
ssnf = string.split(contents, '-') try: elif len(contents) == 9:
result = 1 return result if __name__ == '__main__':
root = Tk()
root.option_add('*Font', 'Verdana 10 bold')
root.option_add('*EntryField.Entry.Font', 'Courier 10') root.option_add('*EntryField.errorbackground', 'yellow') Pmw.initialise(root, useTkOptionDb=1)
root.title('Pmw EntryField Validation')
quit = Button(root, text='Quit', command=root.destroy) quit.pack(side = 'bottom')
P U T T I N G E V E N T S T O W O R K
113
Code comments
The date field uses the built-in date validator, specifying the format of the data and the separators:
validate = {'validator':'date',
'format':'mdy', 'separator':'/'})
The time field sets maximum and minimum options along with minstrict and maxstrict:
validate = {'validator':'time',
'min':'00:00:00', 'max':'23:59:59', 'minstrict':0, 'maxstrict':0})
Setting minstrict and maxstrict to False (zero) allows values outside of the min and max range to be set. The background will be colored to indicate an error. If they are set to True, values outside the range cannot be input.
The Social Security field uses a user-supplied validator:
validate = self.validateSSN, value = '')
Pmw provides a convenience method to align labels. This helps to reduce the need to set up additional formatting in the geometry managers.
Pmw.alignlabels(fields)
self._date.component('entry').focus_set()
It is always a good idea to set input focus to the first editable field in a data-entry screen.
The validateSSN method is simple; it looks for three groups or characters separated by dashes.
Since the entry is cumulative, the string.split call will fail until the third group has been entered.
We set the Tk options database to override fonts and colors in all components used in the Pmw widgets.
root.option_add('*Font', 'Verdana 10 bold')
root.option_add('*EntryField.Entry.Font', 'Courier 10') root.option_add('*EntryField.errorbackground', 'yellow') Pmw.initialise(root, useTkOptionDb=1)
This construct will be seen in many examples. However, this is a less-frequently used option to Pmw.initialise to force the use of the Tk option database.
Running Example_6_7 displays a screen similar to figure 6.2. Notice how the date and Social Security fields have a shaded background to indicate that they contain an invalid format.
Although validation of this kind is provided automatically by the Pmw Entryfield wid-get, it has some drawbacks.
1 There is no indication of the actual validation error. The user is required to determine the cause of the error himself.
2 Data which is valid, when complete, is indicated as being in error as it is being entered (the Social Security field in figure 6.2 is a good example).
1
114
C H A P T E R 6 E V E N T S , B I N D I NG S A N D C A L L B A C K S3 Where validation requires complex calculations and access to servers and databases, etc,.
the processing load can be high. This could be a source of performance problems in cer-tain environments.
To circumvent these and other problems you may use alternative approaches. Of course, your application may not use Pmw widgets, so yet another approach may be required.
Personally, I prefer not to use the built-in validation in Pmw widgets. If the action of formatting the content of the widget requires a redraw, you may observe annoying display glitches, particularly if the system is heavily loaded; these may distract the user. The following method avoids these problems.
To avoid validating every keystroke (which is how the Pmw EntryField manages data input), we will arrange for validation to be done in the following cases:
1 When the user moves the mouse pointer out of the current field.
2 When the focus is moved from the field using the TAB key.
3 When the ENTER key is pressed.
Validating this way means that you don’t get false errors as an input string is built up. In figure 6.3, for example, entering 192.311.40.10 would only raise a validation error when the field was left or if RETURN was pressed, thereby reducing operator confusion and CPU overhead..
import string
from Tkinter import * from validation import *
Example_6_8.py
Note
Figure 6.3 Data verification:
error dialogs
P U T T I N G E V E N T S T O W O R K
115
class EntryValidation:
def __init__(self, master):
self._ignoreEvent = 0
self._ipAddrV = self._crdprtV = self._lnameV = '' frame = Frame(master)
Label(frame, text=' ').grid(row=0, column=0,sticky=W) Label(frame, text=' ').grid(row=0, column=3,sticky=W)
self._ipaddr = self.createField(frame, width=15, row=0, col=2, label='IP Address:', valid=self.validate, enter=self.activate)
self._crdprt = self.createField(frame, width=8, row=1, col=2, label='Card - Port:', valid=self.validate, enter=self.activate)
self._lname = self.createField(frame, width=20, row=2, col=2, label='Logical Name:', valid=self.validate, enter=self.activate)
self._wDict = {self._ipaddr: ('_ipAddrV', validIP), self._crdprt: ('_crdprtV', validCP), self._lname: ('_lnameV', validLName) } frame.pack(side=TOP, padx=15, pady=15)
def createField(self, master, label='', text='', width=1, valid=None, enter=None, row=0, col=0):
Label(master, text=label).grid(row=row, column=col-1, sticky=W) id = Entry(master, text=text, width=width, takefocus=1)
id.bind('<Any-Leave>', valid) id.bind('<FocusOut>', valid) id.bind('<Return>', enter)
id.grid(row=row, column=col, sticky=W) return id
def activate(self, event):
print '<Return>: value is', event.widget.get() def validate(self, event):
var, validator = self._wDict[event.widget]
nValue, replace, valid = validator(currentValue) if replace:
116
C H A P T E R 6 E V E N T S , B I N D I NG S A N D C A L L B A C K S root.option_add('*Font', 'Verdana 10 bold')root.option_add('*Entry.Font', 'Courier 10') root.title('Entry Validation')
top = EntryValidation(root)
quit = Button(root, text='Quit', command=root.destroy) quit.pack(side = 'bottom')
root.mainloop()
Code comments
The grid geometry manager sometimes needs a little help to lay out a screen. We use an empty first and last column in this example:
Label(frame, text=' ').grid(row=0, column=0,sticky=W) Label(frame, text=' ').grid(row=0, column=3,sticky=W)
You cannot use the Grid manager’s minsize option if the column (or row) is empty;
you have to use the technique shown here. As an alternative, you can pack the gridded widget inside a Frame and use padding to add space at the sides.
Since we are using native Tkinter widgets, we have to create a Label and Entry widget for each row of the form and place them in the appropriate columns. We use the createField method to do this.
We create a dictionary to define a variable used to store the contents of each widget.
self._wDict = {self._ipaddr: ('_ipAddrV', validIP), self._crdprt: ('_crdprtV', validCP), self._lname: ('_lnameV', validLName) }
Using the dictionary enables us to use bindings to a single event-handler with multiple validators, which simplifies the code.
The bindings for validation are when the cursor leaves the widget and when focus is lost (tab-bing out of the field). We also bind the activate function called when the ENTER key is pressed.
id.bind('<Any-Leave>', valid) id.bind('<FocusOut>', valid) id.bind('<Return>', enter)
One of the complications of using this type of validation scheme is that whenever a field loses focus, its validator is called—including when we return to a field to allow the user to correct an error. We provide a mechanism to ignore one event:
if self._ignoreEvent:
self._ignoreEvent = 0
We get the variable and validator for the widget creating the event:
var, validator = self._wDict[event.widget]
nValue, replace, valid = validator(currentValue)
and call the validator to check the widget’s contents—possibly editing the content, as appropriate.
P U T T I N G E V E N T S T O W O R K
117
Finally, we react to the result of validation, setting the widget’s content. In the case of a vali-dation error, we reset focus to the widget. Here we set the flag to ignore the resulting focus event:
self._ignoreEvent = 1