Someone on StackOverflow asked about allowing only integer input, using a wx.EditableListCtrl and it occurred to me that it would be useful to not only restrict input but define allowable formats, to a degree as well.
Below is what I came up with.
Obviously it’s malleable to your own requirements, as there is no change to the underlying EditableListCtrl.
I trust someone will find it useful.
Issues on a postcard please.
Regards
Rolf
I would have posted an attachment but I’m not permitted, as I forgot my old login and had to create a new one.
import wx
import wx.lib.mixins.listctrl as listmix
import datetime
class EditableListCtrl(wx.ListCtrl, listmix.TextEditMixin):
def __init__(self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0):
wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
listmix.TextEditMixin.__init__(self)
#
# Validating Editable List Control Extension
#
# Author: J Healey
# Created: 8th October 2022
# Email: <rolfofsaxony@gmx.com>
#
# *** Requires datetime having been imported if using "date" or "time" checks ***
#
# Create a base dictionary entry for each type of test to apply when a column is edited
# This should be amended after this class has been created
# Erroneous input can be refused, retaining the original data, with a report or silently
# Erroneous input can be simply reported as not within parameters but retained
#
# Entries for integer and float, are a simple list of columns requiring those tests
# e.g. MyListCtrl.mixin_test["integer"] = [2,5] will perform integer tests on columns 2 and 5
# if they are edited
# MyListCtrl.mixin_test["float"] = [6] would perform a float test on column 6
#
# Range and Group are a dictionary of columns, each with a list of min/max or valid entries
# e.g. MyListCtrl.mixin_test["range"] = {4:["A","D"], 5:[1,9], 6:[1.0, 1.9]}
# e.g. MyListCtrl.mixin_test["group"] = {4:["A","B","E","P"]}
#
# Range and Group can be integers, floats or strings, the test is adjusted by including the
# column number in the "integer" or "float" tests.
# For strings you may add a column "case" test for "upper", "lower" etc
# e.g. MyListCtrl.mixin_test["integer"] = [5] combined with the range entry above would ensure
# an integer test for values between 1 and 9
# e.g. MyListCtrl.mixin_test["case"] = {4:["upper"]} combined with the range or group test above
# would ensure the input is converted to uppercase before the range or group test is
# applied
#
# Date is a dictionary item of columns, each with a list item containing the date format required
# e.g. ListCtrl_name.mixin_test["date"] = {2:['%Y/%m/%d'], 3:['%Y/%m/%d']}
# *** Remember %x for locale's date format ***
#
# Picking the appropriate datetime format means that a date check can be used for input of
# Month names %B, Day names %A, Years %Y, Week numbers %W etc
#
# Time is a dictionary item of columns, each with a list item containing the time format required
# e.g. ListCtrl_name.mixin_test["time"] = {2:['%H:%M:%S'], 3:['%M:%S.%f'], 4:['%H:%M'],}
# *** Remember %X for locale's time format ***
#
# Time may also have a null format {2:[]} using an empty list
# this will utilise a generic time format checking both hh:mm:ss and mm:ss (hh:mm) for a match
#
# Case is a dictionary item of columns, each with a list item containing a string function:
# "upper", "lower", "capitalize" or "title"
# that function will be applied to the string in the given column
#
# Report should be True or False allowing Reporting of Errors or silent operation
# report is global, not for individual columns
#
# Warn should be True or False
# warn overrides erroneous data being swapped back to the original data
# A warning is issued but the erroreous data is retained
# warn is global, not for individual columns
#
# Tips should be True or False
# if tips is True simple ToolTips are constructed depending on the test type and validating rules
# tips is global, not for individual columns
#
self.mixin_test = {
"integer": [],
"float": [],
"time": {None:[],},
"date": {None:[],},
"range": {None:[],},
"group": {None:[],},
"case": {None:[],},
"report": True,
"warn": False,
"tips": True
}
def OpenEditor(self, col, row):
# Enable the editor for the column construct tooltip
listmix.TextEditMixin.OpenEditor(self, col, row)
self.col = col # column is used for type of validity check
self.OrigData = self.GetItemText(row, col) # original data to swap back in case of error
if self.mixin_test["tips"]:
tip = self.OnSetTip(tip="")
self.editor.SetToolTip(tip)
def OnSetTip(self, tip=""):
if self.col in self.mixin_test["integer"]:
tip += "Integer\n"
if self.col in self.mixin_test["float"]:
tip += "Float\n"
if self.col in self.mixin_test["date"]:
try:
format = self.mixin_test["date"][self.col][0]
format_ = datetime.datetime.today().strftime(format)
tip += "Date format "+format_
except Exception as e:
tip += "Date format definition missing "+str(e)
if self.col in self.mixin_test["time"]:
try:
format = self.mixin_test["time"][self.col][0]
format_ = datetime.datetime.today().strftime(format)
tip += "Time format "+format_
except Exception as e:
tip += "Time generic format hh:mm:ss or mm:ss"
if self.col in self.mixin_test["range"]:
try:
r_min = self.mixin_test["range"][self.col][0]
r_max = self.mixin_test["range"][self.col][1]
tip += "Range - Min: "+str(r_min)+" Max: "+str(r_max)+"\n"
except Exception as e:
tip += "Range definition missing "+str(e)
if self.col in self.mixin_test["group"]:
try:
tip += "Group: "+str(self.mixin_test["group"][self.col])+"\n"
except Exception as e:
tip += "Group definition missing "+str(e)
if self.col in self.mixin_test["case"]:
try:
tip += "Text Case "+str(self.mixin_test["case"][self.col])
except Exception as e:
tip += "Case definition missing "+str(e)
return tip
def OnRangeCheck(self, text):
head = mess = ""
swap = False
try:
r_min = self.mixin_test["range"][self.col][0]
r_max = self.mixin_test["range"][self.col][1]
except Exception as e:
head = "Range Missing - Error"
mess = "Error: "+str(e)+"\n"
swap = True
return head, mess, swap
try:
if self.col in self.mixin_test["float"]:
item = float(text)
head = "Float Range Test - Error"
elif self.col in self.mixin_test["integer"]:
item = int(text)
head = "Integer Range Test - Error"
else:
item = text
head = "Text Range Test - Error"
if item < r_min or item > r_max:
mess += text+" Out of Range: Min - "+str(r_min)+" Max - "+str(r_max)+"\n"
swap = True
except Exception as e:
head = "Range Test - Error"
mess += "Error: "+str(e)+"\n"
swap = True
return head, mess, swap
def OnDateCheck(self, text):
head = mess = ""
swap = False
try:
format = self.mixin_test["date"][self.col][0]
except Exception as e:
head = "Date Format Missing - Error"
mess = "Error: "+str(e)+"\n"
swap = True
return head, mess, swap
try:
datetime.datetime.strptime(text, format)
except Exception as e:
format_ = datetime.datetime.today().strftime(format)
head = "Date Test - Error"
mess = text+" does not match format "+format_+"\n"
swap = True
return head, mess, swap
def OnTimeCheck(self, text):
head = mess = ""
swap = False
try:
format = self.mixin_test["time"][self.col][0]
except Exception as e:
try:
datetime.datetime.strptime(text, '%H:%M:%S')
except Exception as e:
try:
datetime.datetime.strptime(text, '%M:%S')
except:
head = "Time Test - Error"
mess = "Generic Time format must be hh:mm:ss or mm:ss\n"
swap = True
return head, mess, swap
try:
datetime.datetime.strptime(text, format)
except Exception as e:
format_ = datetime.datetime.today().strftime(format)
head = "Time Test - Error"
mess = text+" does not match format "+format_+"\n"
swap = True
return head, mess, swap
def OnCaseCheck(self, text):
try:
format = self.mixin_test["case"][self.col][0]
except:
format = None
if format == "upper":
text = text.upper()
if format == "lower":
text = text.lower()
if format == "capitalize":
text = text.capitalize()
if format == "title":
text = text.title()
self.editor.SetValue(text)
return text
def OnGroupCheck(self, text):
head = mess = ""
swap = False
try:
tests = self.mixin_test["group"][self.col]
except Exception as e:
head = "Group Missing - Error"
mess = "Error: "+str(e)+"\n"
swap = True
return head, mess, swap
if text in tests:
pass
else:
head = "Group Test - Error"
mess = text+" Not in Group: "+str(tests)+"\n"
swap = True
return head, mess, swap
def CloseEditor(self, event=None):
text = self.editor.GetValue()
swap = False
warn = self.mixin_test["warn"]
report = self.mixin_test["report"]
if warn:
report = False
# Integer Check
if self.col in self.mixin_test["integer"]:
try:
int(text)
except Exception as e:
head = "Integer Test - Error"
mess = "Not Integer\n"
swap = True
# Float Check
if self.col in self.mixin_test["float"]:
try:
float(text)
except Exception as e:
head = "Float Test - Error"
mess = str(e)+"\n"
swap = True
# Time check
if self.col in self.mixin_test["time"]:
head, mess, swap = self.OnTimeCheck(text)
# Date check
if self.col in self.mixin_test["date"]:
head, mess, swap = self.OnDateCheck(text)
# Case check
if self.col in self.mixin_test["case"]:
text = self.OnCaseCheck(text)
# Range check
if not swap and self.col in self.mixin_test["range"]:
head, mess, swap = self.OnRangeCheck(text)
# Group check
if not swap and self.col in self.mixin_test["group"]:
head, mess, swap = self.OnGroupCheck(text)
if warn and swap:
wx.MessageBox(mess + 'Invalid entry: ' + text + "\n", \
head, wx.OK | wx.ICON_ERROR)
elif swap: # Invalid data error swap back original data
self.editor.SetValue(self.OrigData)
if report:
wx.MessageBox(mess + 'Invalid entry: ' + text + "\nResetting to "+str(self.OrigData), \
head, wx.OK | wx.ICON_ERROR)
listmix.TextEditMixin.CloseEditor(self, event)
class MyPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
rows = [("Ford", "Taurus", "1996/01/01", "Blue", "C", "1", "1.1", "12:32", "M"),
("Nissan", "370Z", "2010/11/22", "Green", "B", "2", "1.8", "10:10", "F"),
("Porche", "911", "2009/02/28", "Red", "A", "1", "1.3", "23:44", "F")
]
self.list_ctrl = EditableListCtrl(self, style=wx.LC_REPORT)
self.list_ctrl.InsertColumn(0, "Make")
self.list_ctrl.InsertColumn(1, "Model")
self.list_ctrl.InsertColumn(2, "Date*")
self.list_ctrl.InsertColumn(3, "Text*")
self.list_ctrl.InsertColumn(4, "Range*")
self.list_ctrl.InsertColumn(5, "Integer*")
self.list_ctrl.InsertColumn(6, "Float*")
self.list_ctrl.InsertColumn(7, "Time*")
self.list_ctrl.InsertColumn(8, "Group*")
index = 0
for row in rows:
self.list_ctrl.InsertItem(index, row[0])
self.list_ctrl.SetItem(index, 1, row[1])
self.list_ctrl.SetItem(index, 2, row[2])
self.list_ctrl.SetItem(index, 3, row[3])
self.list_ctrl.SetItem(index, 4, row[4])
self.list_ctrl.SetItem(index, 5, row[5])
self.list_ctrl.SetItem(index, 6, row[6])
self.list_ctrl.SetItem(index, 7, row[7])
self.list_ctrl.SetItem(index, 8, row[8])
index += 1
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.list_ctrl, 0, wx.ALL|wx.EXPAND, 5)
self.SetSizer(sizer)
self.list_ctrl.Bind(wx.EVT_LIST_BEGIN_LABEL_EDIT, self.OnVetoItems)
# Here we note columns that require validation for the editablelistctrl
#
# column 2 should be a date with the format yyyy/mm/dd
# column 3 should be capitalised
# column 4 should be in a range from "A" to "D" and uppercase
# column 5 should be in a integer range from 1 to 9
# column 6 should be in a float range from 1.0 to 1.9
# column 7 should be in a time, format hh:mm
# column 8 should be in a group the M or F and upper case
self.list_ctrl.mixin_test["date"] = {2:['%Y/%m/%d']}
self.list_ctrl.mixin_test["integer"] = [5]
self.list_ctrl.mixin_test["float"] = [6]
self.list_ctrl.mixin_test["case"] = {3:["capitalize"], 4:["upper"], 8:["upper"]}
self.list_ctrl.mixin_test["range"] = {4:["A","D"], 5:[1,9], 6:[1.0, 1.9]}
self.list_ctrl.mixin_test["time"] = {7:['%H:%M']}
self.list_ctrl.mixin_test["group"] = {8:["M","F"]}
# This would be column 7 with a time format including micro seconds
#self.list_ctrl.mixin_test["time"] = {7:['%M:%S.%f']}
# This would be column 7 with a time format hh:mm:ss
#self.list_ctrl.mixin_test["time"] = {7:['%H:%M:%S']}
# This would be column 7 with a generic time format of either hh:mm:ss, hh:mm or mm:ss
#self.list_ctrl.mixin_test["time"] = {7:[]}
self.list_ctrl.mixin_test["report"] = True
#self.list_ctrl.mixin_test["warn"] = False
#self.list_ctrl.mixin_test["tips"] = False
def OnVetoItems(self, event):
# Enable editing only for columns 2 and above
if event.Column < 2:
event.Veto()
return
class MyFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "Validating Editable List Control", size=(750, -1))
panel = MyPanel(self)
self.Show()
if __name__ == "__main__":
app = wx.App(False)
frame = MyFrame()
app.MainLoop()