Make wxPython editable ListCtrl accept only numbers from user

222 views Asked by At

I want to make editable ListCtrl which accept only numbers from user . I have this code :

            import wx
            import wx.lib.mixins.listctrl  as  listmix
            class EditableListCtrl(wx.ListCtrl, listmix.TextEditMixin):
                ''' TextEditMixin allows any column to be edited. '''

                #----------------------------------------------------------------------
                def __init__(self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition,
                             size=wx.DefaultSize, style=0):
                    """Constructor"""
                    wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
                    listmix.TextEditMixin.__init__(self)
                def OpenEditor(self, col, row):                    
                           # '''Enable the editor for the  column 2(year)'''
                    if col == 2 :
                        self._editing = (col, row)
                        listmix.TextEditMixin.OpenEditor(self, col, row)
            ########################################################################
            class MyPanel(wx.Panel):
                """"""

                #----------------------------------------------------------------------
                def __init__(self, parent):
                    """Constructor"""
                    wx.Panel.__init__(self, parent)

                    rows = [("Ford", "Taurus", "1996", "Blue"),
                            ("Nissan", "370Z", "2010", "Green"),
                            ("Porche", "911", "2009", "Red")
                            ]
                    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, "Year")
                    self.list_ctrl.InsertColumn(3, "Color")

                    index = 0
                    for row in rows:
                        self.list_ctrl.InsertStringItem(index, row[0])
                        self.list_ctrl.SetStringItem(index, 1, row[1])
                        self.list_ctrl.SetStringItem(index, 2, row[2])
                        self.list_ctrl.SetStringItem(index, 3, row[3])
                        index += 1
                    self.list_ctrl.Bind(wx.EVT_LIST_END_LABEL_EDIT, self.OnUpdate)
                    sizer = wx.BoxSizer(wx.VERTICAL)
                    sizer.Add(self.list_ctrl, 0, wx.ALL|wx.EXPAND, 5)
                    self.SetSizer(sizer)

                def OnUpdate(self, event):
                    row_id = event.GetIndex() #Get the current row
                    col_id = event.GetColumn () #Get the current column
                    new_data = event.GetLabel() #Get the changed data
                    item = self.list_ctrl.GetItem(row_id, col_id)
                    OldData= item .GetText()
                   
                    try :
                        new_data_int = int(new_data)#check if user enter number or not

                    except: #if not , add the old data again
        
                       self.list_ctrl.SetStringItem(row_id,col_id,OldData)

            ########################################################################
            class MyFrame(wx.Frame):
                """"""

                #----------------------------------------------------------------------
                def __init__(self):
                    """Constructor"""
                    wx.Frame.__init__(self, None, wx.ID_ANY, "Editable List Control")
                    panel = MyPanel(self)
                    self.Show()

            #----------------------------------------------------------------------
            if __name__ == "__main__":
                app = wx.App(False)
                frame = MyFrame()
                app.MainLoop() 

But when I try to add the old data again :

self.list_ctrl.SetStringItem(row_id,col_id,OldData)

ListCtrl save the change from user (ListCtrl does not add the old data) , what can I do to make ListCtrl add the old data OR is there another way to Make wxPython editable ListCtrl accept only numbers from user?

Edit : I used Veto() And It is worked Thank you for your nice answers.

My code became Like this :

            import wx
            import wx.lib.mixins.listctrl  as  listmix
            class EditableListCtrl(wx.ListCtrl, listmix.TextEditMixin):
                ''' TextEditMixin allows any column to be edited. '''

                #----------------------------------------------------------------------
                def __init__(self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition,
                             size=wx.DefaultSize, style=0):
                    """Constructor"""
                    wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
                    listmix.TextEditMixin.__init__(self)
                def OpenEditor(self, col, row):                    
                           # '''Enable the editor for the  column 2(year)'''
                    if col == 2 :
                        self._editing = (col, row)
                        listmix.TextEditMixin.OpenEditor(self, col, row)
            ########################################################################
            class MyPanel(wx.Panel):
                """"""

                #----------------------------------------------------------------------
                def __init__(self, parent):
                    """Constructor"""
                    wx.Panel.__init__(self, parent)

                    rows = [("Ford", "Taurus", "1996", "Blue"),
                            ("Nissan", "370Z", "2010", "Green"),
                            ("Porche", "911", "2009", "Red")
                            ]
                    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, "Year")
                    self.list_ctrl.InsertColumn(3, "Color")

                    index = 0
                    for row in rows:
                        self.list_ctrl.InsertStringItem(index, row[0])
                        self.list_ctrl.SetStringItem(index, 1, row[1])
                        self.list_ctrl.SetStringItem(index, 2, row[2])
                        self.list_ctrl.SetStringItem(index, 3, row[3])
                        index += 1
                    self.list_ctrl.Bind(wx.EVT_LIST_END_LABEL_EDIT, self.OnUpdate)
                    sizer = wx.BoxSizer(wx.VERTICAL)
                    sizer.Add(self.list_ctrl, 0, wx.ALL|wx.EXPAND, 5)
                    self.SetSizer(sizer)

                def OnUpdate(self, event):
                    row_id = event.GetIndex() #Get the current row
                    col_id = event.GetColumn () #Get the current column
                    new_data = event.GetLabel() #Get the changed data
                    
                    
                   
                    try :
                        new_data_int = int(new_data)#check if user enter number or not
                        event.Skip()
                    except: #if not , Kill The Edit Event
        
                       event.Veto()

            ########################################################################
            class MyFrame(wx.Frame):
                """"""

                #----------------------------------------------------------------------
                def __init__(self):
                    """Constructor"""
                    wx.Frame.__init__(self, None, wx.ID_ANY, "Editable List Control")
                    panel = MyPanel(self)
                    self.Show()

            #----------------------------------------------------------------------
            if __name__ == "__main__":
                app = wx.App(False)
                frame = MyFrame()
                app.MainLoop() 
2

There are 2 answers

0
Rolf of Saxony On

You can make use of the CloseEditor function in the mixin to check for validity.
Although this version is simplistic and only works for the one item. You'd have to do some work if you wanted to extend it.

import wx
import wx.lib.mixins.listctrl as listmix

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)
        self.parent = parent
        self.OrigData = 0

    def OpenEditor(self, col, row):        
        # Enable the editor for the  column 2 (year)
        lc = self.parent.list_ctrl
        item = lc.GetItem(row, col)
        listmix.TextEditMixin.OpenEditor(self, col, row)
        self.Historic = self.OrigData
        curr_data = item.GetText()
        if not curr_data.isnumeric():
            self.OrigData = self.Historic
        else:
            self.OrigData = curr_data

    def CloseEditor(self, event=None):
        text = self.editor.GetValue()
        if not text.isnumeric():
            self.editor.SetValue(self.OrigData)
            wx.MessageBox('Non-Numeric entry ' + text+"\nResetting to "+str(self.OrigData), \
                          'Error', 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", "Blue"),
                ("Nissan", "370Z", "2010", "Green"),
                ("Porche", "911", "2009", "Red")
                ]

        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, "Year")
        self.list_ctrl.InsertColumn(3, "Color")

        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])
            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)

    def OnVetoItems(self, event):
        if event.Column != 2:
            event.Veto()
            return    

class MyFrame(wx.Frame):

    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Editable List Control")
        panel = MyPanel(self)
        self.Show()

if __name__ == "__main__":
    app = wx.App(False)
    frame = MyFrame()
    app.MainLoop() 

Edited to simplify the code, where I got confused by the fact that CloseEditor gets called twice, once for the control and again for a Focus event, which could result in non-numeric data still be written.

0
Rolf of Saxony On

Here's a slightly improved version that, depending on the column (defined by you), will validate:

  • Integer
  • Float
  • Date, you choose the format
  • Time, ditto
  • Range
  • Group
  • Text to upper, lower, capitalise and title

Columns with an asterisk* are editable

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:     rolfofsaxony
        # Created:    8th October 2022
        #
        #   *** 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() 

enter image description here enter image description here enter image description here