Back to Home

Threads in wxPython

Python · wxPython · Threads

Threads in wxPython

Original author: Mike
  • Transfer
When writing programs in Python using the graphical interface, sometimes you have to start various long-term processing of any data, while in most cases the interface will be blocked and the user will see the program frozen. To avoid this, it is necessary to run our task in a parallel thread or process. In this article, we will look at how to do this in wxPython using the Threading module.

Thread safe wxPython methods


There are three methods for working with threads in wxPython. If you do not use them, then when updating the interface, Python programs may freeze. To avoid this, you must use thread safe methods: wx.PostEvent, wx.CallAfter, and wx.CallLater. According to Robin Dunn (creator of wxPython) wx.CallAfter uses wx.PostEvent to send an event to an application object. The application will have a handler for this event and will respond to it according to the algorithm laid down. As far as I understand, wx.CallLater calls wx.CallAfter with the given time parameter, so that it knows how much to wait for it before sending the event.

Robin Dunn also noted that Global Interpreter Lock (GIL) will not allow more than one thread to run simultaneously, which may limit the number of processor cores used. On the other hand, he also said that wxPython is freed from the GIL by calling the wx library's API functions, so other threads can work at the same time. In other words, performance may change when using threads on multi-core machines. Discussion of this issue may be interesting and not clear ...
Note. perev. - For a more complete acquaintance with GIL, I ask here .

Our three methods can be divided into levels of abstraction, wx.CallLater is at the very top, then comes wx.CallAfter, and wx.PostEvent is at the lowest level. In the following examples, you will see how to use wx.CallAfter and wx.PostEvent in WxPython programs.

wxPython, Threading, wx.CallAfter and PubSub


On the wxPython mailing list, you'll see experts tell other users to use wx.CallAfter in conjunction with PubSub to exchange messages between the application and the threads. In the following example, we will demonstrate this:
Copy Source | Copy HTML
  1. import time
  2. import wx
  3.  
  4. from threading import Thread
  5. from wx.lib.pubsub import Publisher
  6.  
  7. ########################################################################
  8. class TestThread(Thread):
  9.     """Test Worker Thread Class."""
  10.  
  11.     #----------------------------------------------------------------------
  12.     def __init__(self):
  13.         """Init Worker Thread Class."""
  14.         Thread.__init__(self)
  15.         self.start() # start the thread
  16.  
  17.     #----------------------------------------------------------------------
  18.     def run(self):
  19.         """Run Worker Thread."""
  20.         # This is the code executing in the new thread.
  21.         for i in range(6):
  22.             time.sleep(10)
  23.             wx.CallAfter(self.postTime, i)
  24.         time.sleep(5)
  25.         wx.CallAfter(Publisher().sendMessage, "update", "Thread finished!")
  26.  
  27.     #----------------------------------------------------------------------
  28.     def postTime(self, amt):
  29.         """
            Send time to GUI
            """
  30.         amtOfTime = (amt + 1) * 10
  31.         Publisher().sendMessage("update", amtOfTime)
  32.  
  33. ########################################################################
  34. class MyForm(wx.Frame):
  35.  
  36.     #----------------------------------------------------------------------
  37.     def __init__(self):
  38.         wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial")
  39.  
  40.         # Add a panel so it looks the correct on all platforms
  41.         panel = wx.Panel(self, wx.ID_ANY)
  42.         self.displayLbl = wx.StaticText(panel, label="Amount of time since thread started goes here")
  43.         self.btn = btn = wx.Button(panel, label="Start Thread")
  44.  
  45.         btn.Bind(wx.EVT_BUTTON, self.onButton)
  46.  
  47.         sizer = wx.BoxSizer(wx.VERTICAL)
  48.         sizer.Add(self.displayLbl,  0, wx.ALL|wx.CENTER, 5)
  49.         sizer.Add(btn,  0, wx.ALL|wx.CENTER, 5)
  50.         panel.SetSizer(sizer)
  51.  
  52.         # create a pubsub receiver
  53.         Publisher().subscribe(self.updateDisplay, "update")
  54.  
  55.     #----------------------------------------------------------------------
  56.     def onButton(self, event):
  57.         """
            Runs the thread
            """
  58.         TestThread()
  59.         self.displayLbl.SetLabel("Thread started!")
  60.         btn = event.GetEventObject()
  61.         btn.Disable()
  62.  
  63.     #----------------------------------------------------------------------
  64.     def updateDisplay(self, msg):
  65.         """
            Receives data from thread and updates the display
            """
  66.         t = msg.data
  67.         if isinstance(t, int):
  68.             self.displayLbl.SetLabel("Time since thread started: %s seconds" % t)
  69.         else:
  70.             self.displayLbl.SetLabel("%s" % t)
  71.             self.btn.Enable()
  72.  
  73. #----------------------------------------------------------------------
  74. # Run the program
  75. if __name__ == "__main__":
  76.     app = wx.PySimpleApp()
  77.     frame = MyForm().Show()
  78.     app.MainLoop()

This example uses the time module to create a fake process that runs for a long time. However, you can use something more useful instead. In a real example, I use a stream to open Adobe Reader and send a PDF to print. This operation may seem insignificant, but when I do not use threads, the print button in my application remains pressed while the document is sent to the printer and the interface remains frozen until the action is completed. Even a second or two are noteworthy to the user!

Anyway, let's see how it works. In our thread class (described below), we redefined the run method as we needed. This thread starts when we create it because we have “self.start ()” in the __init__ method. In the “run” method, in a loop, every 10 seconds, 6 times we call an update to our interface using wx.CallAfter and PubSub. When the cycle is completed, we send the final message to our application to inform the user about it.

Copy Source | Copy HTML
  1. ########################################################################
  2. class TestThread(Thread):
  3.     """Test Worker Thread Class."""
  4.  
  5.     #----------------------------------------------------------------------
  6.     def __init__(self):
  7.         """Init Worker Thread Class."""
  8.         Thread.__init__(self)
  9.         self.start() # start the thread
  10.  
  11.     #----------------------------------------------------------------------
  12.     def run(self):
  13.         """Run Worker Thread."""
  14.         # This is the code executing in the new thread.
  15.         for i in range(6):
  16.             time.sleep(10)
  17.             wx.CallAfter(self.postTime, i)
  18.         time.sleep(5)
  19.         wx.CallAfter(Publisher().sendMessage, "update", "Thread finished!")
  20.  
  21.     #----------------------------------------------------------------------
  22.     def postTime(self, amt):
  23.         """
            Send time to GUI
            """
  24.         amtOfTime = (amt + 1) * 10
  25.         Publisher().sendMessage("update", amtOfTime)


In our code, the thread is started using the button. At the same time, we make it inactive so as not to accidentally start additional threads, otherwise various messy messages from different threads would appear and this would only confuse us. Although it can be used to good use. You can show the PID of the stream to know which of them is who and display information in a creeping line to observe the operation of several streams at once.

The last interesting part of our code is PubSub, which accepts the event and the event handler:

Copy Source | Copy HTML
  1. def updateDisplay(self, msg):
  2.     """
        Receives data from thread and updates the display
        """
  3.     t = msg.data
  4.     if isinstance(t, int):
  5.         self.displayLbl.SetLabel("Time since thread started: %s seconds" % t)
  6.     else:
  7.         self.displayLbl.SetLabel("%s" % t)
  8.         self.btn.Enable()


So, we extract the message from the stream and update our interface. At the same time, we check the data type of the message to determine what exactly to show the user. Now let's go down one level and try to do the same with wx.PostEvent.

Streams and wx.PostEvent


The code below is based on this wxPython wiki example . This code is a bit more complicated than the one given earlier, but I think it’s not difficult to figure it out.

Copy Source | Copy HTML
  1. import time
  2. import wx
  3.  
  4. from threading import Thread
  5.  
  6. # Define notification event for thread completion
  7. EVT_RESULT_ID = wx.NewId()
  8.  
  9. def EVT_RESULT(win, func):
  10.     """Define Result Event."""
  11.     win.Connect(-1, -1, EVT_RESULT_ID, func)
  12.  
  13. class ResultEvent(wx.PyEvent):
  14.     """Simple event to carry arbitrary result data."""
  15.     def __init__(self, data):
  16.         """Init Result Event."""
  17.         wx.PyEvent.__init__(self)
  18.         self.SetEventType(EVT_RESULT_ID)
  19.         self.data = data
  20.  
  21. ########################################################################
  22. class TestThread(Thread):
  23.     """Test Worker Thread Class."""
  24.  
  25.     #----------------------------------------------------------------------
  26.     def __init__(self, wxObject):
  27.         """Init Worker Thread Class."""
  28.         Thread.__init__(self)
  29.         self.wxObject = wxObject
  30.         self.start() # start the thread
  31.  
  32.     #----------------------------------------------------------------------
  33.     def run(self):
  34.         """Run Worker Thread."""
  35.         # This is the code executing in the new thread.
  36.         for i in range(6):
  37.             time.sleep(10)
  38.             amtOfTime = (i + 1) * 10
  39.             wx.PostEvent(self.wxObject, ResultEvent(amtOfTime))
  40.         time.sleep(5)
  41.         wx.PostEvent(self.wxObject, ResultEvent("Thread finished!"))
  42.  
  43. ########################################################################
  44. class MyForm(wx.Frame):
  45.  
  46.     #----------------------------------------------------------------------
  47.     def __init__(self):
  48.         wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial")
  49.  
  50.         # Add a panel so it looks the correct on all platforms
  51.         panel = wx.Panel(self, wx.ID_ANY)
  52.         self.displayLbl = wx.StaticText(panel, label="Amount of time since thread started goes here")
  53.         self.btn = btn = wx.Button(panel, label="Start Thread")
  54.  
  55.         btn.Bind(wx.EVT_BUTTON, self.onButton)
  56.  
  57.         sizer = wx.BoxSizer(wx.VERTICAL)
  58.         sizer.Add(self.displayLbl,  0, wx.ALL|wx.CENTER, 5)
  59.         sizer.Add(btn,  0, wx.ALL|wx.CENTER, 5)
  60.         panel.SetSizer(sizer)
  61.  
  62.         # Set up event handler for any worker thread results
  63.         EVT_RESULT(self, self.updateDisplay)
  64.  
  65.     #----------------------------------------------------------------------
  66.     def onButton(self, event):
  67.         """
            Runs the thread
            """
  68.         TestThread(self)
  69.         self.displayLbl.SetLabel("Thread started!")
  70.         btn = event.GetEventObject()
  71.         btn.Disable()
  72.  
  73.     #----------------------------------------------------------------------
  74.     def updateDisplay(self, msg):
  75.         """
            Receives data from thread and updates the display
            """
  76.         t = msg.data
  77.         if isinstance(t, int):
  78.             self.displayLbl.SetLabel("Time since thread started: %s seconds" % t)
  79.         else:
  80.             self.displayLbl.SetLabel("%s" % t)
  81.             self.btn.Enable()
  82.  
  83. #----------------------------------------------------------------------
  84. # Run the program
  85. if __name__ == "__main__":
  86.     app = wx.PySimpleApp()
  87.     frame = MyForm().Show()
  88.     app.MainLoop()


Let's take it a bit. The first three parts are the most confusing for me:

Copy Source | Copy HTML
  1. # Define notification event for thread completion
  2. EVT_RESULT_ID = wx.NewId()
  3.  
  4. def EVT_RESULT(win, func):
  5.     """Define Result Event."""
  6.     win.Connect(-1, -1, EVT_RESULT_ID, func)
  7.  
  8. class ResultEvent(wx.PyEvent):
  9.     """Simple event to carry arbitrary result data."""
  10.     def __init__(self, data):
  11.         """Init Result Event."""
  12.         wx.PyEvent.__init__(self)
  13.         self.SetEventType(EVT_RESULT_ID)
  14.         self.data = data


EVT_RESULT_ID is the key here, which seems to bind the EVT_RESULT function to the ResultEvent class. Using the EVT_RESULT function, we set up the event handler that our stream generates. The wx.PostEvent function routes events from the stream to the ResultEvent class, which are then processed by the previously installed event handler.

Our TestThread class works almost the same as we did before, except that we used wx.PostEvent instead of PubSub. The code of our event handler updating the interface has not changed.

Conclusion


I hope you now know how to use the basic flow methods in your wxPython programs. There are several other methods for working with threads, but they are not covered in this article, for example wx.Yield or Queues. Fortunately, wxPython wiki covers these topics pretty well, so be sure to check out the links below if you are interested in these methods.

Additional material


Read Next