wxPython: Putting a Background Image on a Panel

Yesterday, I received a request to create a GUI with Tkinter or wxPython that had an image for the background with buttons on top. After looking at Tkinter, I discovered that it’s PhotoImage widget only supported two formats: gif and pgm (unless I installed the Python Imaging Library). Because of this, I decided to give wxPython a whirl. Here’s what I found out.

Using some of my Google-Fu, I found a thread on daniweb that seemed like it might work. I’ll reproduce that example here:

# create a background image on a wxPython panel
# and show a button on top of the image
 
import wx
 
class Panel1(wx.Panel):
    """class Panel1 creates a panel with an image on it, inherits wx.Panel"""
    def __init__(self, parent, id):
        # create the panel
        wx.Panel.__init__(self, parent, id)
        try:
            # pick an image file you have in the working 
            # folder you can load .jpg  .png  .bmp  or 
            # .gif files
            image_file = 'roses.jpg'
            bmp1 = wx.Image(
                image_file, 
                wx.BITMAP_TYPE_ANY).ConvertToBitmap()
            # image's upper left corner anchors at panel 
            # coordinates (0, 0)
            self.bitmap1 = wx.StaticBitmap(
                self, -1, bmp1, (0, 0))
            # show some image details
            str1 = "%s  %dx%d" % (image_file, bmp1.GetWidth(),
                                  bmp1.GetHeight()) 
            parent.SetTitle(str1)
        except IOError:
            print "Image file %s not found" % imageFile
            raise SystemExit
        
        # button goes on the image --> self.bitmap1 is the 
        # parent
        self.button1 = wx.Button(
            self.bitmap1, label='Button1', 
            pos=(8, 8))

app = wx.App(False)

# create a window/frame, no parent, -1 is default ID
# change the size of the frame to fit the backgound images
frame1 = wx.Frame(None, -1, "An image on a panel", 
   size=(350, 400))

# create the class instance
panel1 = Panel1(frame1, -1)
frame1.Show(True)
app.MainLoop()

My first thought when I saw this was something like: “This is probably bad”. Why would I think that? Well, the guy who posted this was using a wx.StaticBitmap for the parent of the button. The StaticBitmap widget is NOT a container widget like a Panel or Frame is, so I figured this was probably not a good idea. Thus, I asked Robin Dunn on the #wxPython IRC channel what he thought. He said that if I did it like the example above, I’d probably have tab traversal issues and such and he recommended that I use the EVT_ERASE_BACKGROUND event to do some custom drawing. Since Robin Dunn is the creator of wxPython, I ended up going this route and here is what is the code I ended up with by using his advice:

import wx

########################################################################
class MainPanel(wx.Panel):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
        self.frame = parent
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        
        for num in range(4):
            label = "Button %s" % num
            btn = wx.Button(self, label=label)
            sizer.Add(btn, 0, wx.ALL, 5)
        hSizer.Add((1,1), 1, wx.EXPAND)
        hSizer.Add(sizer, 0, wx.TOP, 100)
        hSizer.Add((1,1), 0, wx.ALL, 75)
        self.SetSizer(hSizer)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
    
    #----------------------------------------------------------------------
    def OnEraseBackground(self, evt):
        """
        Add a picture to the background
        """
        # yanked from ColourDB.py
        dc = evt.GetDC()
                
        if not dc:
            dc = wx.ClientDC(self)
            rect = self.GetUpdateRegion().GetBox()
            dc.SetClippingRect(rect)
        dc.Clear()
        bmp = wx.Bitmap("butterfly.jpg")
        dc.DrawBitmap(bmp, 0, 0)
        
    
########################################################################
class MainFrame(wx.Frame):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, None, size=(600,450))
        panel = MainPanel(self)        
        self.Center()
        
########################################################################
class Main(wx.App):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, redirect=False, filename=None):
        """Constructor"""
        wx.App.__init__(self, redirect, filename)
        dlg = MainFrame()
        dlg.Show()
        
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = Main()
    app.MainLoop()

Here’s an example screenshot using a fun butterfly picture I took over the summer for my background image:

The main piece of code to care about is the following:

def OnEraseBackground(self, evt):
    """
    Add a picture to the background
    """
    # yanked from ColourDB.py
    dc = evt.GetDC()
        
    if not dc:
        dc = wx.ClientDC(self)
        rect = self.GetUpdateRegion().GetBox()
        dc.SetClippingRect(rect)
    dc.Clear()
    bmp = wx.Bitmap("butterfly.jpg")
    dc.DrawBitmap(bmp, 0, 0)

I copied it from the ColourDB.py demo which is in the wxPython Demo and edited it a bit to make it work for my application. Basically, you just bind the panel to EVT_ERASE_BACKGROUND and in that handler, you grab the device context (DC), which in this case is the panel (I think). I call its Clear method mainly because in my real application I used an image with a transparency and it was letting the background bleed through. By clearing it, I got rid of the bleed. Anyway, the conditional checks to see if the dc is None or empty (I’m not quite sure which) and if not, it updates the region (or dirty area – which is any part of the application that was “damaged” by moving another window over it). Then I grab my image and use DrawBitmap to apply it to the background. It’s kind of funky and I don’t completely understand what’s going on, but it does work.

I also found another method that I didn’t try at this blog: http://www.5etdemi.com/blog/archives/2006/06/making-a-panel-with-a-background-in-wxpython/

Feel free to try them both out and see which one works the best for you. It’s kind of like Robin Dunn’s method in that it uses DCs too, but not the same type that I’m using.

Update 01/15/2014: The code in this article was originally tested using wxPython 2.8.12. It has been found than in 2.9+, you will need to remove the following line:

self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

If you do not, the image does not show up correctly. According to this StackOverflow answer, the reason is because the style, wx.BG_STYLE_CUSTOM, prevents the background from being erased.

You can also change the background style to wx.BG_STYLE_ERASE and that will work as well.