wxPython: How to Disable a Wizard’s Next Button

The other day someone was asking a lot of questions on StackOverflow about how to work with wizards in wxPython. You can read the two original questions here and here. The code we’ll be looking at in this example is what I used to answer the questions on Stack. The primary question was how to disable the Next in a wxPython wizard.


How do you disable the Wizard’s Next Button anyway?

wxwizard

The idea that the original person had when they posted the question was that they wanted the user to fill out two text controls before being able to continue. That means that we need to disable the Next button until both text widgets have something in them. I came up with an idea where I use a wx.Timer to check the text controls once a second to see if they have data in them. If they do, then the timer’s event handler will enable the Next button. Let’s take a look:

import wx
import wx.wizard

########################################################################
class WizardPage(wx.wizard.PyWizardPage):
    #----------------------------------------------------------------------
    def __init__(self, parent, title):
        wx.wizard.PyWizardPage.__init__(self, parent)
        self.next = None
        self.prev = None
        self.initializeUI(title)

    #----------------------------------------------------------------------
    def initializeUI(self, title):      
        # create grid layout manager    
        self.sizer = wx.GridBagSizer()
        self.SetSizerAndFit(self.sizer)

    #----------------------------------------------------------------------
    def addWidget(self, widget, pos, span): 
        self.sizer.Add(widget, pos, span, wx.EXPAND)

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

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

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

    #----------------------------------------------------------------------
    def GetNext(self):
        return self.next
    
########################################################################
class MyWizard(wx.wizard.Wizard):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.wizard.Wizard.__init__(self, None, -1, "Some Title")
        self.SetPageSize((500, 350))
        
        mypage1 = self.create_page1()
        
        forward_btn = self.FindWindowById(wx.ID_FORWARD) 
        forward_btn.Disable()
        
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.onUpdate, self.timer)
        self.timer.Start(1)
        
        self.RunWizard(mypage1)
    
    #----------------------------------------------------------------------
    def create_page1(self):
        page1 = WizardPage(self, "Page 1")
        d = wx.StaticText(page1, label="test")
        page1.addWidget(d, (2, 1), (1,5))
        
        self.text1 = wx.TextCtrl(page1)
        page1.addWidget(self.text1, (3,1), (1,5))
        
        self.text2 = wx.TextCtrl(page1)
        page1.addWidget(self.text2, (4,1), (1,5))
        
        page2 = WizardPage(self, "Page 2")
        page2.SetName("page2")
        self.text3 = wx.TextCtrl(page2)
        self.Bind(wx.wizard.EVT_WIZARD_PAGE_CHANGED, self.onPageChanged)
        page3 = WizardPage(self, "Page 3")
    
        # Set links
        page2.SetPrev(page1)
        page1.SetNext(page2)
        page3.SetPrev(page2)
        page2.SetNext(page3)
    
        return page1
    
    #----------------------------------------------------------------------
    def onPageChanged(self, event):
        """"""
        page = event.GetPage()
        
        if page.GetName() == "page2":
            self.text3.SetValue(self.text2.GetValue())
    
    #----------------------------------------------------------------------
    def onUpdate(self, event):
        """
        Enables the Next button if both text controls have values
        """
        value_one = self.text1.GetValue()
        value_two = self.text2.GetValue()
        if value_one and value_two:
            forward_btn = self.FindWindowById(wx.ID_FORWARD) 
            forward_btn.Enable()
            self.timer.Stop()
        
#----------------------------------------------------------------------
def main():
    """"""
    wizard = MyWizard()
    
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    main()
    app.MainLoop()

Let’s break this down a bit. The first class we’ll look at is MyWizard, which is where all the action is anyway. MyWizard is a subclass of wxPython’s Wizard class. In the __init__, we create a page and we find the Next button so we can disable it. Then we create and start our timer object while binding it to the onUpdate method. Finally, we run the wizard. When we create a wizard page, we instantiate the WizardPage class. That class is actually pretty self-explanatory. Anyway, we end up creating several widgets that we place on the wizard page. The only other interesting bit is in the onUpdate method. Here we check to see if the user has entered data into both of the text controls.

If they have, then we find the Next button, enable it and stop the timer. There is a potential bug here. What happens if the user goes and removes some content AFTER they have filled them both out? The Next button doesn’t disable itself again. Here’s an updated version of the onUpdate method that fixes that issue:

#----------------------------------------------------------------------
def onUpdate(self, event):
    """
    Enables the Next button if both text controls have values
    """
    value_one = self.text1.GetValue()
    value_two = self.text2.GetValue()
    forward_btn = self.FindWindowById(wx.ID_FORWARD) 
    if value_one and value_two:
        forward_btn.Enable()
    else:
        if forward_btn.IsEnabled():
            forward_btn.Disable()

Here we never stop the timer. Instead, the timer is constantly checking the values of the text controls and if it finds that one of them doesn’t have data AND the next button is enabled, the handler will disable the button.


Wrapping Up

Disabling the Next button in a wxPython wizard isn’t particularly hard, it’s just a bit convoluted. It would be nice if the API for the Wizard widget allowed a bit more access to the standard widgets it creates. However, now you know how to work with them and change their state. Use this knowledge wisely!


Related Reading