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

img_sample_one.png

   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

source.zip


Additional Information

Link :

- - - - -

https://wiki.wxpython.org/TitleIndex

https://docs.wxpython.org/

https://stackoverflow.com/questions/73947332/make-wxpython-editable-listctrl-accept-only-numbers-from-user

https://discuss.wxpython.org/t/a-validating-editable-list-control-extension-update/36184


Thanks to

J. Healey (EditableListCtrlExtension.py coding), 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....

How to create a validating editable list control extension (Phoenix) (last edited 2023-11-28 09:31:07 by Ecco)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.