Search code examples
pythonuser-interfacewxpython

Unable to add custom Sizer as parent to its children widgets in wxPython


I am currently trying to make a GUI in wxPython. The layout is a panel on the right side of the GUI with 3 StaticBoxSizers. One of them is much more complex than the others, with the ability to add more elements to it by clicking a button, so I decided to break up the GUI file and create a custom class for it to try and make the code a bit more clear.

The current initialiser of the class looks like this:

class MonitorBox(wx.StaticBoxSizer):
    def __init__(self, monitors, pins, devices, *args, **kw):
        super().__init__(*args, **kw)
        self.monitors = monitors
        self.pin_choices = pins
        self.dev_choices = devices
        self.InitUI()

    def InitUI(self):
        monList = wx.FlexGridSizer(cols=4, vgap=5, hgap=5)
        add_monitor = wx.Button(self, wx.ID_ANY, "Add Monitor")
        add_monitor.Bind(wx.EVT_BUTTON, self.on_add_button)

        # create list headings
        nametxt = wx.StaticText(self, wx.ID_ANY, "Name:")
        devtxt = wx.StaticText(self, wx.ID_ANY, "Device:")
        pintxt = wx.StaticText(self, wx.ID_ANY, "Pin:")
        blnktxt = wx.StaticText(self, wx.ID_ANY, "")

        # Add Headings to sizer
        monList.AddMany([nametxt, devtxt, pintxt, blnktxt])
        self.monitor_rows = {}  # dictionary to store references to all the widgets

        # Create a row for each item in the mons dictionary
        for monitor, dev in self.mons.items():
            del_mon_button = wx.Button(self, wx.ID_ANY, '-', name=monitor, style=wx.BU_EXACTFIT)
            del_mon_button.Bind(wx.EVT_BUTTON, self.on_del_monitor)
            mon_name = wx.TextCtrl(self, wx.ID_ANY, monitor,
                                   style=wx.TE_PROCESS_ENTER,
                                   name=monitor)
            dev_name = wx.ComboBox(self, wx.ID_ANY, choices=self.dev_choices,
                                   style=wx.CB_READONLY, value=dev[0],
                                   name=monitor)
            pin_name = wx.ComboBox(self, wx.ID_ANY, choices=self.pin_choices[dev[0]],
                                   style=wx.CB_READONLY, value=dev[1],
                                   name=monitor)

            dev_name.Bind(wx.EVT_COMBOBOX, self.on_dev_choice)
            pin_name.Bind(wx.EVT_COMBOBOX, self.on_pin_choice)
            self.monitor_rows[monitor] = [mon_name, dev_name, pin_name, del_mon_button]
            monList.AddMany(self.monitor_rows[monitor])
        # TODO: make sure all the combo boxes are the same size
        self.monList = monList
        self.Add(monList, 1, wx.ALL, 5)
        self.Add(add_monitor, 0, wx.ALL, 5)

However when I try and run the GUI I get an error:

add_monitor = wx.Button(self, wx.ID_ANY, "Add Monitor")
TypeError: Button(): arguments did not match any overloaded call:
  overload 1: too many arguments
  overload 2: argument 1 has unexpected type 'MonitorBox'

I believe this is because I am trying to use my custom class as the parent for all the widgets inside it, but I am confused as to why this is happenning?

I am very new to creating GUIs (this is my first project) but I am currently in the process of going through and fixing all the references to parents etc. So it is hard for me to know the correct terms to search to see if others have had this issue.

My original code was just the InitUI(self) function called from the main wx.Frame of the GUI and creating monBox as a StaticBoxSizer which adds all the children widgets too directly. As mentioned at the start the reason I wanted to split this up is because there are a number of event handlers and additional diaglog boxes created by this panel that I wanted to keep in a seperate file to help keep the code readable.

The original code (not split up into a separate class) generates the Monitors panel that can be seen in the figure:

GUI


Solution

  • The problem is that you are deriving your MonitorBox class from wx.StaticBoxSizer and a wx.Button can only be a child of a window (wx.Frame, wx.Panel, etc) not a sizer (wx.StaticBoxSizer).

    If you want to separate the code then your MonitorBox class should come from wx.Panel. See the code (and its comments) below for a way to do this.

    import wx
    
    class MonitorBox(wx.Panel):
        """
        This is the class with all the widgets in your MonitorBox class.
        You need to arrange the widget in a sizer here. 
        """
        def __init__(self, parent):
            super().__init__(parent=parent)
    
            #### Parent panel
            # No need to define a parent panel because the class itself is already a
            # panel. So self refers to a panel.
    
            #### Widgets
            # Here self is a panel not a sizer like in your question. So no problem
            # adding the button.
            self.add_monitor = wx.Button(self, wx.ID_ANY, "Add Monitor")
            #### Sizers
            self.sizer = wx.BoxSizer()
            #### Add widgets to sizer
            self.sizer.Add(self.add_monitor, flag=wx.CENTER)
            #### Add sizer to panel
            self.SetSizer(self.sizer)
            self.Fit()
    
    class Main(wx.Frame):
        def __init__(self):
            super().__init__(None, title='Panel 3 comes from a different class',
                             size=(500, 500))
            #### Parent panel
            self.panel = wx.Panel(self)
    
            #### Some widgets
            ## The Static Box widget
            self.monitorsBox = wx.StaticBox(self.panel, label="Monitors")
    
            self.panelB = wx.Panel(self.panel, size=(200, 100))
            self.panelB.SetBackgroundColour((0, 0, 255))
            self.panelR = wx.Panel(self.panel, size=(200, 100))
            self.panelR.SetBackgroundColour((255, 0, 0))
            ## Add panel 3 as a child of the wx.StaticBox!!!
            self.panel3 = MonitorBox(self.monitorsBox)
    
            #### Sizers
            ## Main sizer
            self.sizer = wx.FlexGridSizer(3, 1, 5, 5)
            ## Sizers for the static box
            # The static box sizer
            self.monitorsBoxSizer = wx.StaticBoxSizer(self.monitorsBox, wx.VERTICAL)
            # Sizer for the content of the static box
            self.monitorsBoxSizerContent = wx.BoxSizer()
    
            #### Add widgets to sizers
            ## Static box content
            self.monitorsBoxSizerContent.Add(self.panel3, flag=wx.EXPAND)
            self.monitorsBoxSizer.Add(self.monitorsBoxSizerContent)
            ## Add everything to the main sizer
            self.sizer.Add(self.panelB, flag=wx.EXPAND|wx.ALL, border=10)
            self.sizer.Add(self.panelR, flag=wx.EXPAND|wx.ALL, border=10)
            self.sizer.Add(self.monitorsBoxSizer, flag=wx.EXPAND|wx.ALL, border=10)
    
            #### Add sizer to self.panel
            self.panel.SetSizer(self.sizer)
            self.panel.Fit()
    
    if __name__ == '__main__':
        app = wx.App()                            
        frame = Main()
        frame.Show()  
        app.MainLoop()
    

    Most probably in the original code the main class was derived from wx.Frame, right? If not, ignore the rest. That is why it worked. Because you were making the button a child of a frame not a sizer.