Search code examples
pythonwxpythonwxwidgets

Problems sizing a ScrolledPanel when adding/removing child widgets


The following application contains a scrolled panel within a frame. The scrolled panel contains a row of buttons to which a user can add/remove rows. The problem I'm running into is two-fold:

  1. Adding a row causes the scroll panel to disappear. Adding enough rows to exceed the height of the surrounding frame causes the rows to run off the frame.
  2. Removing a row does not resize the surrounding scroll panel

I've looked at several of the other ScrolledPanel topics but can't seem to find one that quite matches this issue.

import wx
from wx.combo import OwnerDrawnComboBox as ComboBox
from wx.lib.scrolledpanel import ScrolledPanel

class Choice(wx.Panel):
    def __init__(self, parent, allow_delete=True):
        wx.Panel.__init__(self, parent)        

        self.colChoice = ComboBox(self, choices=['a','b','c'], style=wx.CB_READONLY)
        self.colChoice.Select(0)
        if allow_delete:
            self.minus_button = wx.Button(self, label='-', size=(30,-1))
            self.minus_button.Bind(wx.EVT_BUTTON, lambda event: self.Parent.on_remove_choice(event,self))              
        self.plus_button = wx.Button(self, label='+', size=(30,-1))   
        self.plus_button.Bind(wx.EVT_BUTTON, lambda event: self.Parent.on_add_choice(event,self))     

        colSizer = wx.BoxSizer(wx.HORIZONTAL)
        colSizer.Add(self.colChoice, 1, wx.EXPAND|wx.ALL,1)
        colSizer.AddSpacer((5,-1))
        colSizer.Add(self.plus_button, 0, wx.EXPAND|wx.ALL,1)        
        if allow_delete:
            colSizer.AddSpacer((5,-1))
            colSizer.Add(self.minus_button, 0, wx.EXPAND)
        else:
            colSizer.AddSpacer((5,-1))
            colSizer.Add(wx.StaticText(self), 0,wx.EXPAND)
        self.SetSizerAndFit(colSizer)

class ChoicePanel(ScrolledPanel):
    def __init__(self,parent):
        ScrolledPanel.__init__(self, parent, -1)

        self.panel_sizer = wx.BoxSizer( wx.VERTICAL )
        self.choices = []
        c = Choice(self, False)
        self.panel_sizer.Add(c, 0, wx.EXPAND)
        self.choices.append(c)

        self.SetSizer(self.panel_sizer)
        self.SetAutoLayout(1)
        self.SetupScrolling(False,True)

    def on_add_choice(self,event,selected_filter):
        self.choices.append(Choice(self, True))
        self.panel_sizer.Add(self.choices[-1], 0, wx.EXPAND|wx.BOTTOM|wx.LEFT|wx.RIGHT, 5)
        self.SetupScrolling(False,True)
        self.panel_sizer.SetMinSize(self.panel_sizer.GetMinSize())
        self.SetSizerAndFit(self.panel_sizer)        
        self.SetAutoLayout(1)
        self.Refresh()
        self.Layout()      

    def on_remove_choice(self,event,selected_filter):
        i = self.choices.index(selected_filter)
        self.choices.remove(selected_filter)
        self.panel_sizer.Remove(selected_filter)
        selected_filter.Destroy()
        self.SetupScrolling(False,len(self.choices) < 3 )  
        self.Refresh()
        self.Layout()          

class TestFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, -1)
        self.sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.test_panel = ChoicePanel(self)
        self.sizer.Add(wx.StaticText(self, -1, "Some text here:"), 0, wx.CENTER|wx.ALL, 4)
        self.sizer.AddSpacer((4,-1))        
        self.sizer.Add(self.test_panel, 1, wx.CENTER|wx.ALL, 4)
        self.SetSizer(self.sizer)
        self.Layout()

if __name__=='__main__':
    app = wx.PySimpleApp()
    f = TestFrame()
    f.Show()
    app.MainLoop()    

Solution

  • I had to add wx.EXPAND to a few sizers, and then the magic in the add and remove methods... personally I'd opt for the scroll bars to always be shown, since the way it is now I accidentally clicked the remove button after the scroll bars show up. Also PySimpleApp is deprecated so I changed that to App:

    import wx
    from wx.combo import OwnerDrawnComboBox as ComboBox
    from wx.lib.scrolledpanel import ScrolledPanel
    
    class Choice(wx.Panel):
        def __init__(self, parent, allow_delete=True):
            wx.Panel.__init__(self, parent)        
    
            self.colChoice = ComboBox(self, choices=['a','b','c'], style=wx.CB_READONLY)
            self.colChoice.Select(0)
            if allow_delete:
                self.minus_button = wx.Button(self, label='-', size=(30,-1))
                self.minus_button.Bind(wx.EVT_BUTTON, lambda event: self.Parent.on_remove_choice(event,self))              
            self.plus_button = wx.Button(self, label='+', size=(30,-1))   
            self.plus_button.Bind(wx.EVT_BUTTON, lambda event: self.Parent.on_add_choice(event,self))     
    
            colSizer = wx.BoxSizer(wx.HORIZONTAL)
            colSizer.Add(self.colChoice, 1, wx.EXPAND|wx.ALL,1)
            colSizer.AddSpacer((5,-1))
            colSizer.Add(self.plus_button, 0, wx.EXPAND|wx.ALL,1)        
            if allow_delete:
                colSizer.AddSpacer((5,-1))
                colSizer.Add(self.minus_button, 0, wx.EXPAND)
            else:
                colSizer.AddSpacer((5,-1))
                colSizer.Add(wx.StaticText(self), 0,wx.EXPAND)
            self.SetSizerAndFit(colSizer)
    
    class ChoicePanel(ScrolledPanel):
        def __init__(self,parent):
            ScrolledPanel.__init__(self, parent, -1)
    
            self.panel_sizer = wx.BoxSizer( wx.VERTICAL )
            self.choices = []
            c = Choice(self, False)
            self.panel_sizer.Add(c, 0, wx.EXPAND)
            self.choices.append(c)
    
            self.SetSizer(self.panel_sizer)
            self.SetAutoLayout(1)
            self.SetupScrolling(False,True)
    
        def on_add_choice(self,event,selected_filter):
            self.choices.append(Choice(self, True))
            self.panel_sizer.Add(self.choices[-1], 0, wx.EXPAND|wx.BOTTOM|wx.LEFT|wx.RIGHT, 5)
            self.panel_sizer.Layout()
            self.Fit()
            self.GetParent().Layout()
    
        def on_remove_choice(self,event,selected_filter):
            i = self.choices.index(selected_filter)
            self.choices.remove(selected_filter)
            self.panel_sizer.Remove(selected_filter)
            selected_filter.Destroy()
            self.panel_sizer.Layout()
            self.Fit()
            self.GetParent().Layout()
    
    class TestFrame(wx.Frame):
        def __init__(self):
            wx.Frame.__init__(self, None, -1)
            self.sizer = wx.BoxSizer(wx.HORIZONTAL)
            panel = wx.Panel(self, -1)
            self.test_panel = ChoicePanel(panel)
            self.sizer.Add(wx.StaticText(panel, -1, "Some text here:"), 1, wx.EXPAND|wx.CENTER|wx.ALL, 4)
            self.sizer.AddSpacer((4,-1))        
            self.sizer.Add(self.test_panel, 1, wx.EXPAND|wx.CENTER|wx.ALL, 4)
            panel.SetSizer(self.sizer)
            panel.Layout()
    
    if __name__=='__main__':
        app = wx.App(False)
        f = TestFrame()
        f.Show()
        app.MainLoop()