wxPython: Adding and Removing Widgets Dynamically

Posted by Mike on May 5th, 2012 filed in Python, wxPython

I keep seeing people asking about how to add or remove widgets after they’ve already started their wxPython application. This is actually something that’s really easy to do, so I decided it was time to write a simple tutorial on the subject. I have had to do this myself from time to time depending on what kind of user was accessing my program so I could show slightly different options. Anyway, let’s get started!

I decided to make this really simple. All this application will do is allow the user to add or remove buttons. The following script will create a window similar to the one at the beginning of this article. If you press the “add” button a few times, you should see something like this:

As you can see, you end up with more buttons! Now let’s take a moment and read the code. I’ll explain it as soon as you get done reading it.

import wx
 
########################################################################
class MyPanel(wx.Panel):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent)
        self.number_of_buttons = 0
        self.frame = parent
 
        self.mainSizer = wx.BoxSizer(wx.VERTICAL)
        controlSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.widgetSizer = wx.BoxSizer(wx.VERTICAL)
 
        self.addButton = wx.Button(self, label="Add")
        self.addButton.Bind(wx.EVT_BUTTON, self.onAddWidget)
        controlSizer.Add(self.addButton, 0, wx.CENTER|wx.ALL, 5)
 
        self.removeButton = wx.Button(self, label="Remove")
        self.removeButton.Bind(wx.EVT_BUTTON, self.onRemoveWidget)
        controlSizer.Add(self.removeButton, 0, wx.CENTER|wx.ALL, 5)
 
        self.mainSizer.Add(controlSizer, 0, wx.CENTER)
        self.mainSizer.Add(self.widgetSizer, 0, wx.CENTER|wx.ALL, 10)
 
        self.SetSizer(self.mainSizer)
 
    #----------------------------------------------------------------------
    def onAddWidget(self, event):
        """"""
        self.number_of_buttons += 1
        label = "Button %s" %  self.number_of_buttons
        name = "button%s" % self.number_of_buttons
        new_button = wx.Button(self, label=label, name=name)
        self.widgetSizer.Add(new_button, 0, wx.ALL, 5)
        self.frame.fSizer.Layout()
        self.frame.Fit()
 
    #----------------------------------------------------------------------
    def onRemoveWidget(self, event):
        """"""
        if self.widgetSizer.GetChildren():
            self.widgetSizer.Hide(self.number_of_buttons-1)
            self.widgetSizer.Remove(self.number_of_buttons-1)
            self.number_of_buttons -= 1
            self.frame.fSizer.Layout()
            self.frame.Fit()
 
########################################################################
class MyFrame(wx.Frame):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, parent=None, title="Add / Remove Buttons")
        self.fSizer = wx.BoxSizer(wx.VERTICAL)
        panel = MyPanel(self)
        self.fSizer.Add(panel, 1, wx.EXPAND)
        self.SetSizer(self.fSizer)
        self.Fit()
        self.Show()
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = MyFrame()
    app.MainLoop()

I think this is pretty straight forward code, so we’ll just focus on the important bits. The first topic I’m going to point out is that I call the Frame’s Fit() method right before I show it. I normally avoid using Fit, but I was having trouble getting the Frame to change size appropriately whenever I added or removed the buttons and Fit fixed that issue for me. I should note that Fit always tries to make the widgets fit the container and sometimes it ends up doing it in ways I don’t like.

Anyway, the other bit in the onAddWidget and onRemoveWidget methods. You normally want to call Layout on the container object to make it update and layout the controls whenever you add or remove a widget. Oddly enough, it seems that Fit does that automatically, so those Layout() calls that you see in the code above can actually be removed. I tried removing the Fit ones to see if Layout was enough, but when you do that, the frame doesn’t update its size, so Fit seems to be required in this case. Now, if you happened to be adding or removing widgets in such a way that it wouldn’t effect the frame’s overall size, I think Layout would be enough.

Finally, as a side note, you sometimes use Layout() at the end of a Freeze / Thaw update as well.

Alright, that’s it! Now you too should be able to add or remove widgets after your application is running too. I hope you learned something new.

Print Friendly

  • Joaquin Abian

    Mike, why do you “normally avoid using Fit” ?  

  • Tormod Landet

    I normally do

    panel.Freeze()
    # modify …
    sizer.Layout()
    panel.Refresh()
    panel.Update()
    panel.Thaw()

    Seems to work OK, but it could be that your way is better?

  • driscollis

    I’m not completely sure, but I think Layout() calls Update and possibly Refresh itself. Try just calling Layout and see if that does everything you need.

  • Bill

    I’m running under Windows and I’m finding I need to do something like this to switch panels in and out without too much grief:

    Before the panel __init__ does anything (besides call wx.Pane.__init__) I do a Hide().

    This prevents the Layout operation during construction to appear and look like a flicker. (I’m using wxFormBuilder generated Python code for the panel layouts.)

    The other approach I took was to use fSizer.Detach() instead of .Remove().

    My particular app doesn’t need to call Fit() since I’ll be setting a large enough MinSize on my frame to take care of it.

  • Dan

    I’ve been struggling with this problem for most of the day and I couldn’t deduce why when I was removing widgets dynamically they would stay visible (sometimes they disappear but then show up when the frame is resized with the mouse). The widgets would lose their functionality, but there they were. From your code the secret is to hide the widget first, then remove it. Doesn’t seem very intuitive. Is this “just the way its done” or is there a good reason for this?

    (btw, your tutorials are great for a hack like myself)

  • driscollis

    @Dan – I’m not sure why that is. I think I have done a sizer.Remove(child) call and had it work in some of my older code, but I couldn’t get it to work in my latest code. However, either way you do it, you almost always need the Layout() command at the end to show the new widgets and make sure that the screen is redrawn so that the old widgets don’t stay onscreen. You could ask on the wxPython user’s group about this behavior too though.

  • Bill

    Do all of your control containers (panel, tabs, whatever) have the TAB TRAVERSAL window style? *stabs in dark*

  • driscollis

    The code in my article should work cross-platform. I wonder if Mac is having trouble detecting the new buttons for some reason. I think you can ask on either group since there’s plenty of Mac guys on the regular list or you can cross-post to both.

  • Nitin

    how to insert a button and a label in a single cell

  • siwei

    Hi Mike,

    I tried your code and it worked very well. But I wonder how could I actually use the generated buttons. Since they are added through a function, I don’t know how to bind these buttons to new events and getValue() method seems not working as well.

  • http://www.blog.pythonlibrary.org/ Mike Driscoll

    You would want to do the binding when you create the widget in the “onAddWidget” method. You could bind all the widgets to the same event and use something like “event.GetEventObject()” to get a reference to the button. See this other tutorial for more info: http://www.blog.pythonlibrary.org/2011/09/20/wxpython-binding-multiple-widgets-to-the-same-handler/

  • siwei

    I actually forget to change name of the handler when i pasted it. Finally, the code now works .Thank you so much.