When I'm throwing together prototype code, I don't want to spend extra time setting up parameters, or chasing down problems from entering parameters wrongly. So this is a general purpose, fairly easy to use, GUI data input helper, that hopefully takes care of most of the data entry whoopsies that frustrate just getting the damned target code working.
I really had trouble with deciding whether each entry line should be an object in its own right. The code shown here takes that approach, but as a drawback the field widths are fixed so that they line up nicely, and there's an update callback. In an earlier version, the top level widget built everything from individual Label
, Entry
and Button
widgets, which took care of a lined-up layout automatically and didn't need the update callback, but was a bit untidier around keeping the corresponding elements together, and needed closures for trace callbacks to check the right data.
I changed whether I had lists of keys and entry lines, or a dict
of entries keyed on the keys several times, neither seems really tidy, though the lists implementation I have now is not too bad.
Anyhow, it runs. Have I missed a better way of designing it that would have me feeling better about it? It has a __name__ == '__main__'
, so run it and have a play.
""" class to use as a general purpose GUI input widget
and a few convenience functions for validating data inputs
"""
import tkinter as tki
import tkinter.messagebox as tkm
class MyLabelEntry(tki.Frame):
""" a combination of label, entry and help button, for validated gui entry of data"""
def __init__(self, parent, text='label', data='', conv=None, update=None):
""" text optional used to label the entry
data optional used to initialise the Entry box
uses empty string if none supplied
conv optional conversion function, which also validates the entry
note that int and float can be used
return string unchanged if omitted or None
return object if entry is valid
its __doc__ string is available as help
if the docstring starts with [x], x is put on the help button
(have to work on tooltips sometime!)
raise ValueError if the entry is invalid
The err from ValueError is saved for the help button
so more info can be given about failure to validate
"""
tki.Frame.__init__(self, master=parent)
self.update = update
# do the properties
self.err = ''
self.value = None
# do the label
self.label = tki.Label(self, text=text, width=15)
self.label.grid(row=0, column=0)
# do the conversion function
self.conv = conv
cdoc = conv.__doc__ # easier to type
if conv:
# we have a conversion function specified
# is it one of the builtins?
if conv == int:
help_face = 'i'
self.conv_help = 'builtin int() function'
elif conv == float:
help_face = 'f'
self.conv_help = 'builtin float() function'
else:
# neither of those, so does it have a docstring?
if cdoc:
# yes, does it start with a help_face?
face_end = cdoc.find(']')
if (cdoc[0] == '[') and (face_end != -1) and (face_end < 6):
help_face = cdoc[1:face_end]
else:
help_face = '?'
# is the help prompt truncated in the docstring?
help_end = cdoc.find('[end_help]')
if help_end != -1:
self.conv_help = cdoc[:help_end]
else:
self.conv_help = cdoc
else:
self.conv_help = 'no documentation\nfor this conversion'
help_face = '?'
else:
self.conv = str
help_face = '='
self.conv_help = 'unmodified string'
# do the entry
self.var = tki.StringVar()
self.entry = tki.Entry(self, textvariable=self.var, width = 15)
self.entry.grid(row=0, column=1)
self.var.trace('w', self._changed)
# do the help button
self.help_but = tki.Button(self, text = help_face, command=self._show_help,
width=5, takefocus=0) # don't take part in tab-focus
self.help_but.grid(row=0, column=2)
# initialise it, which triggers the trace, _change and validation
self.var.set(str(data))
def _show_help(self):
tkm.showinfo('conversion information', '{}\n{}'.format(self.conv_help, self.err))
def _changed(self, *args):
ent_val = self.var.get()
try:
self.value = self.conv(ent_val)
self.entry.config(bg='white')
self.err = ''
self.valid = True
except ValueError as err:
self.value = None
self.entry.config(bg='orange')
self.err = err
self.valid = False
self.update()
class GUI_inputs(tki.LabelFrame):
""" A GUI data input convenience class"""
def __init__(self, parent, text='Input Widget', execute=None):
""" initialise with text for the LabelFrame
if execute is defined, create an 'executeute' button in the frame
greyed out until all entries are valid
"""
tki.LabelFrame.__init__(self, master=parent, text=text)
# we have a list of entries
self.entries = [] # the data entry widgets
self.keys = [] # the keys, both lists will stay in the same order
self.row = 0
# if there's a execute supplied, put up a button for it, on the last row
self.execute_func = execute
if execute:
self.execute_but = tki.Button(self, text='execute',
command=self.execute_func,
state=tki.DISABLED)
self.execute_but.grid(row=99, column=0)
def add(self, key, disp_name='', data='', conv=None):
""" add a new line to the input widget
key required key for the entry, must be unique
other arguments follow from MyLabelEntry() usage above
"""
if key in self.keys:
raise ValueError('duplicate key name >>>{}<<<'.format(key))
if not disp_name:
disp_name = str(key)
mle = MyLabelEntry(self, disp_name, data, conv, self.update)
mle.grid(row=self.row)
self.row += 1
self.entries.append(mle)
self.keys.append(key)
def update(self):
""" called when something has changed"""
# only need to worry about this when there's a execute button to grey-out
if self.execute_func:
# get the valid properties of each entry
valids = [x.valid for x in self.entries]
if all(valids):
self.execute_but.config(state=tki.NORMAL)
else:
self.execute_but.config(state=tki.DISABLED)
def get(self):
""" return a dict of the results"""
output = dict(zip(self.keys, [x.value for x in self.entries]))
return output
def intfloat(x):
"""[if] a float accepted, truncated to an integer for return"""
return int(float(x))
def int16(x):
"""[16] a base16 (hexadecimal) integer"""
return int(x, base=16)
def float_pair(x):
"""[f,f] Two floats seperated by a comma
all three elements are required
[end_help]
example non-trivial conversion function
not all of docstring intended to be displayed as help
throw two different types of ValueError, one from split, one from float
return a list of the values
"""
fields = x.split(',')
if len(fields) != 2:
raise ValueError('need two fields seperated by one comma')
output = []
for field in fields: # no need to try:, ValueError will burn through
output.append(float(field))
return output
if __name__ == '__main__':
def execute_func():
print('executing with')
print( ml.get(), basic.get())
def cryptic_conv(x):
# no docstring for this conversion function
return int(x)
root = tki.Tk()
basic = GUI_inputs(root, 'basic')
basic.pack()
basic.add('key 1')
basic.add('key 2')
ml = GUI_inputs(root, 'more flexible', execute=execute_func)
ml.pack()
ml.add('we')
ml.add('we1', conv=int)
ml.add('we2', 'disp4we2', data=999)
ml.add('pair', 'f_pair','',float_pair)
ml.add('cryp', 'no doc string', 6, cryptic_conv)
root.mainloop()