wxPython: Using wx.Timers

This past couple of weeks, I’ve seen multiple people ask about timers and how they work either on the wxPython mailing list or on their IRC channel. So I decided it was high time I wrote a couple of example scripts to show how they’re used. I will cover two examples, the first of which will only have one timer and the second will have two. Robin Dunn contacted me with some improvements to the examples, so I have added a refactored example of each to this article.

My first example is super simple. It has only one button that starts and stops a timer. Let’s take a look at the code:

import wx
import time
 
class MyForm(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Timer Tutorial 1", 
                                   size=(500,500))
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.update, self.timer)

        self.toggleBtn = wx.Button(panel, wx.ID_ANY, "Start")
        self.toggleBtn.Bind(wx.EVT_BUTTON, self.onToggle)

    def onToggle(self, event):
        btnLabel = self.toggleBtn.GetLabel()
        if btnLabel == "Start":
            print "starting timer..."
            self.timer.Start(1000)
            self.toggleBtn.SetLabel("Stop")
        else:
            print "timer stopped!"
            self.timer.Stop()
            self.toggleBtn.SetLabel("Start")
            
    def update(self, event):
        print "\nupdated: ",
        print time.ctime()
 
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

As you can see, I only import two modules: wx and time. I use the time module to post the time that the wx.Timer event fires on. The two main things to pay attention to here is how to bind the timer to an event and the event handler itself. For this example to work, you have to bind the frame to the timer event. I tried binding the timer (i.e. self.timer.Bind) but that didn’t work. So the logical thing to do was ask Robin Dunn what was going on. He said that if the parent of the timer is the frame, then the frame is the only object that will receive the timer’s events unless you derive wx.Timer and override it’s Notify method. Makes sense to me.

Regardless, let’s look at the my event handler. In it I grab the button’s label and then use a conditional IF statement to decide if I want to start or stop the timer as well as what to label the button. In this way, I can have just one function that toggles the button and the timer’s state. The part to take note of are the methods Start and Stop. They are what control the timer.

In one of my real life applications, I have a timer execute every so often to check my email. I discovered that if I shut my program down without stopping the timer, the program would basically become a zombie process. Thus, you need to make sure that you stop all your timers when your program is closed or destroyed.

Before we get to my next example, let’s take a look at refactoring this one. Here’s the code. Can you tell what’s different?

import wx
import time
 
class MyForm(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Timer Tutorial 1", size=(500,500))
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.update, self.timer)

        self.toggleBtn = wx.Button(panel, wx.ID_ANY, "Start")
        self.toggleBtn.Bind(wx.EVT_BUTTON, self.onToggle)

    def onToggle(self, event):        
        if self.timer.IsRunning():
            self.timer.Stop()
            self.toggleBtn.SetLabel("Start")
            print "timer stopped!"
        else:
            print "starting timer..."
            self.timer.Start(1000)
            self.toggleBtn.SetLabel("Stop")
            
    def update(self, event):
        print "\nupdated: ",
        print time.ctime()
 
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

As you can see, I’ve changed the event handler to check if the timer is running or not rather than looking at the button’s label. This saves us one line, but it’s a little cleaner and shows how to accomplish the same thing in a slightly different way.

Now let’s look at my second example:

import wx
import time

TIMER_ID1 = 2000
TIMER_ID2 = 2001

class MyForm(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Timer Tutorial 2")
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        
        self.timer = wx.Timer(self, id=TIMER_ID1)
        self.Bind(wx.EVT_TIMER, self.update, self.timer)
        self.timer2 = wx.Timer(self, id=TIMER_ID2)
        self.Bind(wx.EVT_TIMER, self.update, self.timer2)

        self.toggleBtn = wx.Button(panel, wx.ID_ANY, "Start Timer 1")
        self.toggleBtn.Bind(wx.EVT_BUTTON, self.onStartTimerOne)
        self.toggleBtn2 = wx.Button(panel, wx.ID_ANY, "Start Timer 2")
        self.toggleBtn2.Bind(wx.EVT_BUTTON, self.onStartTimerOne)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.toggleBtn, 0, wx.ALL|wx.CENTER, 5)
        sizer.Add(self.toggleBtn2, 0, wx.ALL|wx.CENTER, 5)
        panel.SetSizer(sizer)        
        
    def onStartTimerOne(self, event):
        buttonObj = event.GetEventObject()
        btnLabel = buttonObj.GetLabel()
        timerNum = int(btnLabel[-1:])
        print timerNum
                    
        if btnLabel == "Start Timer %s" % timerNum:
            if timerNum == 1:
                print "starting timer 1..."
                self.timer.Start(1000)
            else:
                print "starting timer 2..."
                self.timer2.Start(3000)
            buttonObj.SetLabel("Stop Timer %s" % timerNum)
        else:
            if timerNum == 1:
                self.timer.Stop()
                print "timer 1 stopped!"
            else:
                self.timer2.Stop()
                print "timer 2 stopped!"
            buttonObj.SetLabel("Start Timer %s" % timerNum)
            
    def update(self, event):
        timerId = event.GetId()
        if timerId == TIMER_ID1:
            print "\ntimer 1 updated: ",
        else:
            print "\ntimer 2 updated: ", 
        print time.ctime()
 
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

To be honest, this second example is mostly the same as the first one. The main difference is that I have two buttons and two timer instances. I decided to be geeky and have both buttons bind to the same event handler. This is probably one of my better tricks. To find out which button called the event, you can use the event’s GetEventObject method. Then you can get the label off the button. If you’re a real nerd, you’ll notice that I could combine lines 32 and 33 into this one-liner:

btnLabel = event.GetEventObject().GetLabel()

I split that into two lines to make it easier to follow though. Next, I used some string slicing to grab the button’s label number so I would know which timer to stop or start. Then my program enters my nested IF statements where it checks the button label and then the timer number. Now you know how to start and stop multiple timers too.

Once again, Robin Dunn came up with a better way to do this second example, so let’s see what we came up with:

import wx
import time

class MyForm(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Timer Tutorial 2")
 
        # Add a panel so it looks the correct on all platforms
        panel = wx.Panel(self, wx.ID_ANY)
        
        self.timer = wx.Timer(self, wx.ID_ANY)
        self.Bind(wx.EVT_TIMER, self.update, self.timer)
        self.timer2 = wx.Timer(self, wx.ID_ANY)
        self.Bind(wx.EVT_TIMER, self.update, self.timer2)

        self.toggleBtn = wx.Button(panel, wx.ID_ANY, "Start Timer 1")
        self.toggleBtn.Bind(wx.EVT_BUTTON, self.onStartTimer)
        self.toggleBtn2 = wx.Button(panel, wx.ID_ANY, "Start Timer 2")
        self.toggleBtn2.Bind(wx.EVT_BUTTON, self.onStartTimer)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.toggleBtn, 0, wx.ALL|wx.CENTER, 5)
        sizer.Add(self.toggleBtn2, 0, wx.ALL|wx.CENTER, 5)
        panel.SetSizer(sizer)
        
        # Each value in the following dict is formatted as follows:
        # (timerNum, timerObj, secs between timer events)
        self.objDict = {self.toggleBtn: (1, self.timer, 1000),
                        self.toggleBtn2: (2, self.timer2, 3000)}
        
    def onStartTimer(self, event):
        btn = event.GetEventObject()
        timerNum, timer, secs = self.objDict[btn]
        if timer.IsRunning():
            timer.Stop()
            btn.SetLabel("Start Timer %s" % timerNum)
            print "timer %s stopped!" % timerNum
        else:
            print "starting timer %s..." % timerNum
            timer.Start(secs)
            btn.SetLabel("Stop Timer %s" % timerNum)
            
    def update(self, event):
        timerId = event.GetId()
        if timerId == self.timer.GetId():
            print "\ntimer 1 updated: ",
        else:
            print "\ntimer 2 updated: ", 
        print time.ctime()
 
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

In the “__init__” I added a dictionary that is keyed on the button objects. The values of the dictionary are the timer number, the timer object, and the number of seconds (technically milliseconds) between timer events. Next, I updated the button event handler to grab the button object from the event’s GetEventObject method and then extract the respective values using said object for the dict’s key. Then I can use the same trick I used in the other refactored example I detailed above, namely the checking of whether or not the timer is running.

This was tested on the following systems:

  • Windows Vista with wxPython 2.8.9.2 (msw-unicode) and Python 2.5.2.
  • Windows XP with wxPython 2.8.10.1 (msw-unicode) and Python 2.5.2

You can download my code below. Let me know if you test it on another OS and if you have any questions by posting in the comments section. My deepest gratitude goes out to Robin Dunn for his help with this tutorial and for making wxPython available in the first place.

Further Reading

Downloads

7 thoughts on “wxPython: Using wx.Timers”

  1. Hi Mike,
    Just to inform you that it worked on my XP SP2 and as I see it should work on any platform without problem
    Steve

  2. Hi Mike,
    Just to inform you that it worked on my XP SP2 and as I see it should work on any platform without problem
    Steve

  3. @ Steve,

    I agree. It should work on any platform, but I’ve been surprised before so I didn’t want to make any promises.

    – Mike

  4. @ Steve,

    I agree. It should work on any platform, but I’ve been surprised before so I didn’t want to make any promises.

    – Mike

  5. Thanks for the warning. I just wrote a timer like this last week, and didn’t know about the zombie process thing. I’ll have to watch out for it.

  6. Works just fine on Mac OS X 10.6.1 (Python 2.6.2), too. Thanks – this was clear and very helpful.

Comments are closed.