wxPython: A Wizard Tutorial

In this article, we will be looking at wxPython's Wizard widget. No, it has nothing to do with Dumbledore or Gandalf. Instead, it is that dialog that you'll see when you run an installer or set up a template. Sometimes you'll even see them used for setting up mail merge. We will cover two examples in this tutorial: one that is fairly simple and another that is slightly more complex. Let's get started!

Note: The code in this article was adapted from the wxPython Demo application

A Simple Wizard

When you need to use a wizard in wxPython, you'll want to import it in a special way. Instead of just importing wx, you will have to do this: import wx.wizard. Also note that there are two primary types of wizard pages: WizardPageSimple and PyWizardPage. The former is the easiest, so we'll use that in our simple example. Here's the code:

import wx
import wx.wizard as wiz

########################################################################
class TitledPage(wiz.WizardPageSimple):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent, title):
        """Constructor"""
        wiz.WizardPageSimple.__init__(self, parent)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(sizer)
        
        title = wx.StaticText(self, -1, title)
        title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD))
        sizer.Add(title, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
        sizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.ALL, 5)
        
#----------------------------------------------------------------------
def main():
    """"""
    wizard = wx.wizard.Wizard(None, -1, "Simple Wizard")
    page1 = TitledPage(wizard, "Page 1")
    page2 = TitledPage(wizard, "Page 2")
    page3 = TitledPage(wizard, "Page 3")
    page4 = TitledPage(wizard, "Page 4")
    
    wx.wizard.WizardPageSimple.Chain(page1, page2)
    wx.wizard.WizardPageSimple.Chain(page2, page3)
    wx.wizard.WizardPageSimple.Chain(page3, page4)
    wizard.FitToPage(page1)
    
    wizard.RunWizard(page1)
    
    wizard.Destroy()

#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    main()
    app.MainLoop()

That's a fair bit of code. Let's take it a part and see if we can figure it out. First off, we import wx and the wizard which we rename "wiz" to save on keystrokes. Next, we create a TitledPage class that subclasses WizardPageSimple. This class will be the basis for all the pages in our wizard. It basically just defines a page that has a centered title in 18 point font with a line underneath.

In the main function we find the real meat. Here we create the wizard using the following syntax: wx.wizard.Wizard(None, -1, "Simple Wizard"). This gives the wizard a parent of None, an id and a title. Then we create four pages which are instances of the TitledPage class that we mentioned earlier. Finally, we use wx.wizard.WizardPageSimple.Chain to chain the pages together. This allows us to use a couple of automatically generated buttons to page forwards and backwards through the pages. The last couple of lines of code will run the wizard and when the user is done, will destroy the wizard. Pretty simple, right? Now let's move on to the more advanced example.

Using PyWizardPage

In this section, we will create a subclass of PyWizardPage. We will also have a WizardPageSimple subclass so that we can mix and match the two to create a series of different pages. Let's just jump to the code so you can see it for yourself!

import images
import wx
import wx.wizard as wiz

########################################################################
class TitledPage(wiz.WizardPageSimple):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent, title):
        """Constructor"""
        wiz.WizardPageSimple.__init__(self, parent)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer = sizer
        self.SetSizer(sizer)
        
        title = wx.StaticText(self, -1, title)
        title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD))
        sizer.Add(title, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
        sizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.ALL, 5)

########################################################################
class UseAltBitmapPage(wiz.PyWizardPage):
    
    #----------------------------------------------------------------------
    def __init__(self, parent, title):
        wiz.PyWizardPage.__init__(self, parent)
        self.next = self.prev = None
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        
        title = wx.StaticText(self, label=title)
        title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD))
        self.sizer.Add(title)
        
        self.sizer.Add(wx.StaticText(self, -1, "This page uses a different bitmap"),
                       0, wx.ALL, 5)
        self.sizer.Layout()

    #----------------------------------------------------------------------
    def SetNext(self, next):
        self.next = next

    #----------------------------------------------------------------------
    def SetPrev(self, prev):
        self.prev = prev

    #----------------------------------------------------------------------
    def GetNext(self):
        return self.next

    #----------------------------------------------------------------------
    def GetPrev(self):
        return self.prev

    #----------------------------------------------------------------------
    def GetBitmap(self):
        # You usually wouldn't need to override this method
        # since you can set a non-default bitmap in the
        # wxWizardPageSimple constructor, but if you need to
        # dynamically change the bitmap based on the
        # contents of the wizard, or need to also change the
        # next/prev order then it can be done by overriding
        # GetBitmap.
        return images.WizTest2.GetBitmap()
    
#----------------------------------------------------------------------
def main():
    """"""
    wizard = wiz.Wizard(None, -1, "Dynamic Wizard", 
                        images.WizTest1.GetBitmap())
    page1 = TitledPage(wizard, "Page 1")
    page2 = TitledPage(wizard, "Page 2")
    page3 = TitledPage(wizard, "Page 3")
    page4 = UseAltBitmapPage(wizard, "Page 4")
    page5 = TitledPage(wizard, "Page 5")
        
    wizard.FitToPage(page1)
    page5.sizer.Add(wx.StaticText(page5, -1, "\nThis is the last page."))
    
    # Set the initial order of the pages
    page1.SetNext(page2)
    page2.SetPrev(page1)
    page2.SetNext(page3)
    page3.SetPrev(page2)
    page3.SetNext(page4)
    page4.SetPrev(page3)
    page4.SetNext(page5)
    page5.SetPrev(page4)

    wizard.GetPageAreaSizer().Add(page1)
    wizard.RunWizard(page1)
    wizard.Destroy()
    
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    main()
    app.MainLoop()

This code starts out in much the same way as the previous code did. In this example, we also import an images module that contains a couple PyEmbeddedImage objects that we will use to demonstrate how to add bitmaps to our wizard page. Anyway, the first class is exactly the same as the previous one. Next we create a UseAltBitmapPage class that is a subclass of the PyWizardPage. We have to override a few methods to make it work correctly, but they're pretty self-explanatory. This page will just be used to change the bitmap image of one page.

In the main function, we create a wizard in a slightly different way than we did previously:

wizard = wiz.Wizard(None, -1, "Dynamic Wizard", images.WizTest1.GetBitmap())

As you can see, this method allows us to add a bitmap that will appear along the left hand side of the wizard pages. Anyway, after that, we create five pages with four of them being instances of the TitledPage and one being an instance of a UseAltBitmapPage. We fit the wizard to page one and then we see something odd:

page5.sizer.Add(wx.StaticText(page5, -1, "\nThis is the last page."))

What does that do? Well, it's a silly way to append a widget to a page. To let the user know that they've reached the last page, we add a StaticText instance to it that explicitly tells that they have reached the end. The next few lines set up the order of the pages using SetNext and SetPrev. While these methods give you more granular control over the order of the pages, they're not as convenient as the WizardPageSimple.Chain method. The last few lines of code are the same as the previous example.

A Bonus Tip: How to Relabel the Wizard Buttons

While creating this article, someone actually asked how to change the button labels of the wizard on the official wxPython mailing list. So for completeness, we'll take Robin Dunn's solution and show how to change both the previous and the next button's labels.

prev_btn = self.FindWindowById(wx.ID_BACKWARD)
prev_btn.SetLabel("Foo")
next_btn = self.FindWindowById(wx.ID_FORWARD) 
next_btn.SetLabel("Bar")

Wrapping Up

Now you know how to create the two types of wizards that are included with wxPython. You also have learned a fun hack to change the labels of the buttons in the wizard. Let me know if you think I forgot something and I'll update the post or write a follow-up.

Further Reading

Source Code

Copyright © 2024 Mouse Vs Python | Powered by Pythonlibrary