Bizarrely I appear to have lost the ability to edit a previous post.
I forgot the ability to back out of an edit in the Validating Editable List Control Extension, this update rectifies that oversight, by adding the ability to use the Escape key to do that.
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
#
# *** 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
#
# Escape key will backout of an edit
#
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)
self.editor.Bind(wx.EVT_KEY_DOWN, self.OnEscape)
def OnEscape(self, event):
keycode = event.GetKeyCode()
if keycode == wx.WXK_ESCAPE:
self.CloseEditor(event=None, swap=True)
return
else:
event.Skip()
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, swap=False):
text = self.editor.GetValue()
warn = self.mixin_test["warn"]
report = self.mixin_test["report"]
if swap:
self.editor.Hide()
listmix.TextEditMixin.CloseEditor(self, event)
return
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()