How to create a validating editable list control extension (Phoenix)
Keywords : ListCtrl, TextEditMixin, Extension, Validating.
Demonstrating :
Tested py3.8.10, wx4.x and Linux.
Tested py3.x, wx4.x and Win11.
Are you ready to use some samples ?
Test, modify, correct, complete, improve and share your discoveries !
Sample one
Latest version here : https://discuss.wxpython.org/t/a-validating-editable-list-control-extension-update/36184
1 import wx
2 import wx.lib.mixins.listctrl as listmix
3 import datetime
4
5 class EditableListCtrl(wx.ListCtrl, listmix.TextEditMixin):
6
7 def __init__(self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0):
8 wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
9 listmix.TextEditMixin.__init__(self)
10 #
11 # Validating Editable List Control Extension
12 #
13 # Author: J Healey
14 # Created: 8th October 2022
15 # Email: rolfofsaxony
16 #
17 # *** Requires datetime having been imported if using "date" or "time" checks ***
18 #
19 # Create a base dictionary entry for each type of test to apply when a column is edited
20 # This should be amended after this class has been created
21 # Erroneous input can be refused, retaining the original data, with a report or silently
22 # Erroneous input can be simply reported as not within parameters but retained
23 #
24 # Entries for integer and float, are a simple list of columns requiring those tests
25 # e.g. MyListCtrl.mixin_test["integer"] = [2,5] will perform integer tests on columns 2 and 5
26 # if they are edited
27 # MyListCtrl.mixin_test["float"] = [6] would perform a float test on column 6
28 #
29 # Range and Group are a dictionary of columns, each with a list of min/max or valid entries
30 # e.g. MyListCtrl.mixin_test["range"] = {4:["A","D"], 5:[1,9], 6:[1.0, 1.9]}
31 # e.g. MyListCtrl.mixin_test["group"] = {4:["A","B","E","P"]}
32 #
33 # Range and Group can be integers, floats or strings, the test is adjusted by including the
34 # column number in the "integer" or "float" tests.
35 # For strings you may add a column "case" test for "upper", "lower" etc
36 # e.g. MyListCtrl.mixin_test["integer"] = [5] combined with the range entry above would ensure
37 # an integer test for values between 1 and 9
38 # e.g. MyListCtrl.mixin_test["case"] = {4:["upper"]} combined with the range or group test above
39 # would ensure the input is converted to uppercase before the range or group test is
40 # applied
41 #
42 # Date is a dictionary item of columns, each with a list item containing the date format required
43 # e.g. ListCtrl_name.mixin_test["date"] = {2:['%Y/%m/%d'], 3:['%Y/%m/%d']}
44 # *** Remember %x for locale's date format ***
45 #
46 # Picking the appropriate datetime format means that a date check can be used for input of
47 # Month names %B, Day names %A, Years %Y, Week numbers %W etc
48 #
49 # Time is a dictionary item of columns, each with a list item containing the time format required
50 # e.g. ListCtrl_name.mixin_test["time"] = {2:['%H:%M:%S'], 3:['%M:%S.%f'], 4:['%H:%M'],}
51 # *** Remember %X for locale's time format ***
52 #
53 # Time may also have a null format {2:[]} using an empty list
54 # this will utilise a generic time format checking both hh:mm:ss and mm:ss (hh:mm) for a match
55 #
56 # Case is a dictionary item of columns, each with a list item containing a string function:
57 # "upper", "lower", "capitalize" or "title"
58 # that function will be applied to the string in the given column
59 #
60 # Report should be True or False allowing Reporting of Errors or silent operation
61 # report is global, not for individual columns
62 #
63 # Warn should be True or False
64 # warn overrides erroneous data being swapped back to the original data
65 # A warning is issued but the erroreous data is retained
66 # warn is global, not for individual columns
67 #
68 # Tips should be True or False
69 # if tips is True simple ToolTips are constructed depending on the test type and validating rules
70 # tips is global, not for individual columns
71 #
72 # Escape key will backout of an edit
73 #
74 self.mixin_test = {
75 "integer": [],
76 "float": [],
77 "time": {None:[],},
78 "date": {None:[],},
79 "range": {None:[],},
80 "group": {None:[],},
81 "case": {None:[],},
82 "report": True,
83 "warn": False,
84 "tips": True
85 }
86
87 def OpenEditor(self, col, row):
88 # Enable the editor for the column construct tooltip
89 listmix.TextEditMixin.OpenEditor(self, col, row)
90 self.col = col # column is used for type of validity check
91 self.OrigData = self.GetItemText(row, col) # original data to swap back in case of error
92 if self.mixin_test["tips"]:
93 tip = self.OnSetTip(tip="")
94 self.editor.SetToolTip(tip)
95 self.editor.Bind(wx.EVT_KEY_DOWN, self.OnEscape)
96
97 def OnEscape(self, event):
98 keycode = event.GetKeyCode()
99 if keycode == wx.WXK_ESCAPE:
100 self.CloseEditor(event=None, swap=True)
101 return
102 else:
103 event.Skip()
104
105 def OnSetTip(self, tip=""):
106 if self.col in self.mixin_test["integer"]:
107 tip += "Integer\n"
108 if self.col in self.mixin_test["float"]:
109 tip += "Float\n"
110 if self.col in self.mixin_test["date"]:
111 try:
112 format = self.mixin_test["date"][self.col][0]
113 format_ = datetime.datetime.today().strftime(format)
114 tip += "Date format "+format_
115 except Exception as e:
116 tip += "Date format definition missing "+str(e)
117 if self.col in self.mixin_test["time"]:
118 try:
119 format = self.mixin_test["time"][self.col][0]
120 format_ = datetime.datetime.today().strftime(format)
121 tip += "Time format "+format_
122 except Exception as e:
123 tip += "Time generic format hh:mm:ss or mm:ss"
124 if self.col in self.mixin_test["range"]:
125 try:
126 r_min = self.mixin_test["range"][self.col][0]
127 r_max = self.mixin_test["range"][self.col][1]
128 tip += "Range - Min: "+str(r_min)+" Max: "+str(r_max)+"\n"
129 except Exception as e:
130 tip += "Range definition missing "+str(e)
131 if self.col in self.mixin_test["group"]:
132 try:
133 tip += "Group: "+str(self.mixin_test["group"][self.col])+"\n"
134 except Exception as e:
135 tip += "Group definition missing "+str(e)
136 if self.col in self.mixin_test["case"]:
137 try:
138 tip += "Text Case "+str(self.mixin_test["case"][self.col])
139 except Exception as e:
140 tip += "Case definition missing "+str(e)
141
142 return tip
143
144 def OnRangeCheck(self, text):
145 head = mess = ""
146 swap = False
147
148 try:
149 r_min = self.mixin_test["range"][self.col][0]
150 r_max = self.mixin_test["range"][self.col][1]
151 except Exception as e:
152 head = "Range Missing - Error"
153 mess = "Error: "+str(e)+"\n"
154 swap = True
155 return head, mess, swap
156
157 try:
158 if self.col in self.mixin_test["float"]:
159 item = float(text)
160 head = "Float Range Test - Error"
161 elif self.col in self.mixin_test["integer"]:
162 item = int(text)
163 head = "Integer Range Test - Error"
164 else:
165 item = text
166 head = "Text Range Test - Error"
167 if item < r_min or item > r_max:
168 mess += text+" Out of Range: Min - "+str(r_min)+" Max - "+str(r_max)+"\n"
169 swap = True
170 except Exception as e:
171 head = "Range Test - Error"
172 mess += "Error: "+str(e)+"\n"
173 swap = True
174
175 return head, mess, swap
176
177 def OnDateCheck(self, text):
178 head = mess = ""
179 swap = False
180
181 try:
182 format = self.mixin_test["date"][self.col][0]
183 except Exception as e:
184 head = "Date Format Missing - Error"
185 mess = "Error: "+str(e)+"\n"
186 swap = True
187 return head, mess, swap
188
189 try:
190 datetime.datetime.strptime(text, format)
191 except Exception as e:
192 format_ = datetime.datetime.today().strftime(format)
193 head = "Date Test - Error"
194 mess = text+" does not match format "+format_+"\n"
195 swap = True
196
197 return head, mess, swap
198
199 def OnTimeCheck(self, text):
200 head = mess = ""
201 swap = False
202
203 try:
204 format = self.mixin_test["time"][self.col][0]
205 except Exception as e:
206 try:
207 datetime.datetime.strptime(text, '%H:%M:%S')
208 except Exception as e:
209 try:
210 datetime.datetime.strptime(text, '%M:%S')
211 except:
212 head = "Time Test - Error"
213 mess = "Generic Time format must be hh:mm:ss or mm:ss\n"
214 swap = True
215 return head, mess, swap
216
217 try:
218 datetime.datetime.strptime(text, format)
219 except Exception as e:
220 format_ = datetime.datetime.today().strftime(format)
221 head = "Time Test - Error"
222 mess = text+" does not match format "+format_+"\n"
223 swap = True
224
225 return head, mess, swap
226
227 def OnCaseCheck(self, text):
228 try:
229 format = self.mixin_test["case"][self.col][0]
230 except:
231 format = None
232 if format == "upper":
233 text = text.upper()
234 if format == "lower":
235 text = text.lower()
236 if format == "capitalize":
237 text = text.capitalize()
238 if format == "title":
239 text = text.title()
240 self.editor.SetValue(text)
241
242 return text
243
244 def OnGroupCheck(self, text):
245 head = mess = ""
246 swap = False
247
248 try:
249 tests = self.mixin_test["group"][self.col]
250 except Exception as e:
251 head = "Group Missing - Error"
252 mess = "Error: "+str(e)+"\n"
253 swap = True
254 return head, mess, swap
255
256 if text in tests:
257 pass
258 else:
259 head = "Group Test - Error"
260 mess = text+" Not in Group: "+str(tests)+"\n"
261 swap = True
262
263 return head, mess, swap
264
265 def CloseEditor(self, event=None, swap=False):
266 text = self.editor.GetValue()
267 warn = self.mixin_test["warn"]
268 report = self.mixin_test["report"]
269 if swap:
270 self.editor.Hide()
271 listmix.TextEditMixin.CloseEditor(self, event)
272 return
273 if warn:
274 report = False
275 # Integer Check
276 if self.col in self.mixin_test["integer"]:
277 try:
278 int(text)
279 except Exception as e:
280 head = "Integer Test - Error"
281 mess = "Not Integer\n"
282 swap = True
283 # Float Check
284 if self.col in self.mixin_test["float"]:
285 try:
286 float(text)
287 except Exception as e:
288 head = "Float Test - Error"
289 mess = str(e)+"\n"
290 swap = True
291 # Time check
292 if self.col in self.mixin_test["time"]:
293 head, mess, swap = self.OnTimeCheck(text)
294
295 # Date check
296 if self.col in self.mixin_test["date"]:
297 head, mess, swap = self.OnDateCheck(text)
298
299 # Case check
300 if self.col in self.mixin_test["case"]:
301 text = self.OnCaseCheck(text)
302
303 # Range check
304 if not swap and self.col in self.mixin_test["range"]:
305 head, mess, swap = self.OnRangeCheck(text)
306
307 # Group check
308 if not swap and self.col in self.mixin_test["group"]:
309 head, mess, swap = self.OnGroupCheck(text)
310
311 if warn and swap:
312 wx.MessageBox(mess + 'Invalid entry: ' + text + "\n", \
313 head, wx.OK | wx.ICON_ERROR)
314 elif swap: # Invalid data error swap back original data
315 self.editor.SetValue(self.OrigData)
316 if report:
317 wx.MessageBox(mess + 'Invalid entry: ' + text + "\nResetting to "+str(self.OrigData), \
318 head, wx.OK | wx.ICON_ERROR)
319
320 listmix.TextEditMixin.CloseEditor(self, event)
321
322
323 class MyPanel(wx.Panel):
324 def __init__(self, parent):
325 wx.Panel.__init__(self, parent)
326
327 rows = [("Ford", "Taurus", "1996/01/01", "Blue", "C", "1", "1.1", "12:32", "M"),
328 ("Nissan", "370Z", "2010/11/22", "Green", "B", "2", "1.8", "10:10", "F"),
329 ("Porche", "911", "2009/02/28", "Red", "A", "1", "1.3", "23:44", "F")
330 ]
331
332 self.list_ctrl = EditableListCtrl(self, style=wx.LC_REPORT)
333 self.list_ctrl.InsertColumn(0, "Make")
334 self.list_ctrl.InsertColumn(1, "Model")
335 self.list_ctrl.InsertColumn(2, "Date*")
336 self.list_ctrl.InsertColumn(3, "Text*")
337 self.list_ctrl.InsertColumn(4, "Range*")
338 self.list_ctrl.InsertColumn(5, "Integer*")
339 self.list_ctrl.InsertColumn(6, "Float*")
340 self.list_ctrl.InsertColumn(7, "Time*")
341 self.list_ctrl.InsertColumn(8, "Group*")
342 index = 0
343 for row in rows:
344 self.list_ctrl.InsertItem(index, row[0])
345 self.list_ctrl.SetItem(index, 1, row[1])
346 self.list_ctrl.SetItem(index, 2, row[2])
347 self.list_ctrl.SetItem(index, 3, row[3])
348 self.list_ctrl.SetItem(index, 4, row[4])
349 self.list_ctrl.SetItem(index, 5, row[5])
350 self.list_ctrl.SetItem(index, 6, row[6])
351 self.list_ctrl.SetItem(index, 7, row[7])
352 self.list_ctrl.SetItem(index, 8, row[8])
353 index += 1
354 sizer = wx.BoxSizer(wx.VERTICAL)
355 sizer.Add(self.list_ctrl, 0, wx.ALL|wx.EXPAND, 5)
356 self.SetSizer(sizer)
357 self.list_ctrl.Bind(wx.EVT_LIST_BEGIN_LABEL_EDIT, self.OnVetoItems)
358
359 # Here we note columns that require validation for the editablelistctrl
360 #
361 # column 2 should be a date with the format yyyy/mm/dd
362 # column 3 should be capitalised
363 # column 4 should be in a range from "A" to "D" and uppercase
364 # column 5 should be in a integer range from 1 to 9
365 # column 6 should be in a float range from 1.0 to 1.9
366 # column 7 should be in a time, format hh:mm
367 # column 8 should be in a group the M or F and upper case
368 self.list_ctrl.mixin_test["date"] = {2:['%Y/%m/%d']}
369 self.list_ctrl.mixin_test["integer"] = [5]
370 self.list_ctrl.mixin_test["float"] = [6]
371 self.list_ctrl.mixin_test["case"] = {3:["capitalize"], 4:["upper"], 8:["upper"]}
372 self.list_ctrl.mixin_test["range"] = {4:["A","D"], 5:[1,9], 6:[1.0, 1.9]}
373 self.list_ctrl.mixin_test["time"] = {7:['%H:%M']}
374 self.list_ctrl.mixin_test["group"] = {8:["M","F"]}
375 # This would be column 7 with a time format including micro seconds
376 #self.list_ctrl.mixin_test["time"] = {7:['%M:%S.%f']}
377 # This would be column 7 with a time format hh:mm:ss
378 #self.list_ctrl.mixin_test["time"] = {7:['%H:%M:%S']}
379 # This would be column 7 with a generic time format of either hh:mm:ss, hh:mm or mm:ss
380 #self.list_ctrl.mixin_test["time"] = {7:[]}
381 self.list_ctrl.mixin_test["report"] = True
382 #self.list_ctrl.mixin_test["warn"] = False
383 #self.list_ctrl.mixin_test["tips"] = False
384
385 def OnVetoItems(self, event):
386 # Enable editing only for columns 2 and above
387 if event.Column < 2:
388 event.Veto()
389 return
390
391 class MyFrame(wx.Frame):
392 def __init__(self):
393 wx.Frame.__init__(self, None, wx.ID_ANY, "Validating Editable List Control", size=(750, -1))
394 panel = MyPanel(self)
395 self.Show()
396
397 if __name__ == "__main__":
398 app = wx.App(False)
399 frame = MyFrame()
400 app.MainLoop()
Download source
Additional Information
Link :
- - - - -
https://wiki.wxpython.org/TitleIndex
https://discuss.wxpython.org/t/a-validating-editable-list-control-extension-update/36184
Thanks to
J. Healey (aka RolfofSaxony), the wxPython community...
About this page
Date(d/m/y) Person (bot) Comments :
15/01/23 - Ecco (Created page for wxPython Phoenix).
Comments
- blah, blah, blah....