Search code examples
pythonpython-3.xwxpython

How to derive wxPython custom control from TextCtrl


For me the wxPython wx.lib.masked.NumCtrl has some really odd quirks. Hence, I wanted to create my own NumberCtrl derived from wx.TextCtrl that simply skips over non-numeric input. Side note: the downside is that ctrl.SetValue() only accepts strings.

The below class fails due to non-matching arguments (or number of arguments) in the call to the super class constructor.

class NumberCtrl(wx.TextCtrl):

    def __init__(self, parent, id=wx.ID_ANY, label=None):
        wx.TextCtrl.__init__(self, parent=parent, id=id, label=label)
        self.Bind(wx.EVT_CHAR, lambda event: self.force_numeric(event))

    def force_numeric(self, event):
        raw_value = self.GetValue().strip()
        keycode = event.GetKeyCode()
        if keycode < 255:
            if chr(keycode).isdigit() or chr(keycode) == '.' and '.' not in raw_value:
                event.Skip()

However, using just the bare basic arguments works.

def __init__(self, parent):
    wx.TextCtrl.__init__(self, parent=parent)

I don't understand what is going on here.


Solution

  • Try simply using the explicit init values for a TextCtrl.

    I've replaced label with value and added allowing for `backspace´ (keycode 8 on my setup) in the events validation.

    Edited to change the default value of value to string not None

    class NumberCtrl(wx.TextCtrl):
    
        def __init__(self, parent, id=wx.ID_ANY, value="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name=wx.TextCtrlNameStr):
            wx.TextCtrl.__init__(self, parent=parent, id=id, value=value)
            self.Bind(wx.EVT_CHAR, lambda event: self.force_numeric(event))
    
        def force_numeric(self, event):
            raw_value = self.GetValue().strip()
            keycode = event.GetKeyCode()
            print(keycode) 
            if keycode < 255:
                if chr(keycode).isdigit() or keycode == 8 or chr(keycode) == '.' and '.' not in raw_value:
                    event.Skip()
    

    Edit - Working code (on Linux wxpython 4.1.1)

    import wx
    
    class MyPanel(wx.Panel):
    
        def __init__(self, parent, flags=0):
            super().__init__(parent)
    
            # Panel 1 (contains button that takes up entire panel)
            self.pnl1 = wx.Panel(self)
            self.btn1 = wx.Button(self.pnl1, label="Panel2", size=(250,75))
            self.btn1.Bind(wx.EVT_BUTTON, self.show_pnl2)
    
            vbox1 = wx.BoxSizer(wx.VERTICAL)
            vbox1.Add(self.btn1)
            self.pnl1.SetSizer(vbox1)
    
            # Panel 2 (contains button that takes up entire panel)
            self.pnl2 = wx.Panel(self)
            self.btn2 = wx.Button(self.pnl2, label="Panel1", size=(75,250))
            self.num = NumberCtrl(self.pnl2, wx.ID_ANY, value="1", size=(75,250))
            self.btn2.Bind(wx.EVT_BUTTON, self.show_pnl1)
    
            vbox2 = wx.BoxSizer(wx.VERTICAL)
            vbox2.Add(self.btn2)
            vbox2.Add(self.num)
            self.pnl2.SetSizer(vbox2)
    
            # Main panel (displays either Panel 1 or Panel 2)
            vbox = wx.BoxSizer(wx.VERTICAL)
            vbox.Add(self.pnl1, flag=flags)
            vbox.Add(self.pnl2, flag=flags)
            self.SetSizer(vbox)
    
            # Start off with panel 1 showing and panel 2 hidden
            self.Show()
            self.pnl1.Hide()
    
        def show_pnl1(self, event):
            self.pnl2.Hide()
            self.pnl1.Show()
            self.Layout()
            self.Fit()
    
        def show_pnl2(self, event):
            self.pnl1.Hide()
            self.pnl2.Show()
            self.Layout()
            self.Fit()
    
    class NumberCtrl(wx.TextCtrl):
    
        def __init__(self, parent, id=wx.ID_ANY, value="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name=wx.TextCtrlNameStr):
            wx.TextCtrl.__init__(self, parent=parent, id=id, value=value)
            self.Bind(wx.EVT_CHAR, lambda event: self.force_numeric(event))
    
        def force_numeric(self, event):
            raw_value = self.GetValue().strip()
            keycode = event.GetKeyCode()
            print(keycode) 
            if keycode < 255:
                if chr(keycode).isdigit() or keycode == 8 or chr(keycode) == '.' and '.' not in raw_value:
                    event.Skip()
    
    class PanelSwitchExample(wx.Frame):
        """Represents application window that includes switchable panel"""
    
        def __init__(self, flags=0):
            super().__init__(None)
    
            vbox = wx.BoxSizer(wx.VERTICAL)
            vbox.Add(MyPanel(self, flags))
            self.SetSizer(vbox)
            self.Show()
    
    if __name__ == "__main__":
        app = wx.App()
    
        PanelSwitchExample()
        app.MainLoop()