Welcome

Running Threads In wxPython

wxPython is a powerful cross-platform GUI toolkit for the Python language. In this post you will learn how to start a long background task from a wxPython GUI with a progress-bar and a stop button.

GUI programming is almost always based on the event-driven paradigm. This is also true when using wxPython. In this paradigm, your responsibility as a programmer (in addition to building the layout of the GUI) is to write event handlers. Each event handler is a function which is responsible for handling some type of event. The event handlers are executed sequentially by wxPython (or any other GUI framework) in a single thread which means that while each event handler is running, the user can not interact with the program. So, in order to make a user friendly and responsive user interface your event handlers should do their job very quickly.

Sometimes we need to do some heavy processing in our GUI application. In such cases we should do the long processing in a separate thread.

In cases like these we will probably want to add the following features:

  1. Disabling the button that starts the long operation while it is running
  2. Allowing the user to stop the long operation with a stop button
  3. Showing the progress in a progress-bar
  4. Once the long operation is completed, updating the GUI and enabling the “start processing” button. These operations should not be done in the worker thread but in the main GUI thread, otherwise some bad thing can happen.

All of these features which are mentioned above adds some complexity into the code. Luckily you can find here a simple example which contains all of these feature and to copy-paste it into your project.

The Example

The following example demonstrate a GUI allowing computing the factorial of a number.

Factorial, in mathematics, the product of all positive integers less than or equal to a given positive integer and denoted by that integer and an exclamation point. Thus, factorial seven is written 7!, meaning 1 × 2 × 3 × 4 × 5 × 6 × 7. Factorial zero is defined as equal to 1.

https://www.britannica.com/science/factorial

Computing factorial in Python can be implemented as following:

def factorial(num: int) -> int:
    res = 1
    for i in range(2, num):
        res *= i
    return res

Bad Implementation Without Threads

The bad implementation in action

# import wxPython
import wx
# import IntCtrl which is a textbox that accepts only
# integer number
from wx.lib.intctrl import IntCtrl


class MainFrame(wx.Frame):
    def __init__(self, *args, **kw):
        """Construct the frame"""

        # Ensure the parent's __init__ is called
        super(MainFrame, self).__init__(*args, **kw)

        # Create a panel in the frame
        pnl = wx.Panel(self)

        # Create a sizer to stack the child widgets vertically
        sizer = wx.BoxSizer(wx.VERTICAL)
        pnl.SetSizer(sizer)

        # Add a caption: Input Number
        label = wx.StaticText(pnl, label="Input Number:")
        sizer.Add(label, 0, wx.EXPAND)

        # Add a textbox that accepts only integer number
        self.input_number = IntCtrl(pnl)
        sizer.Add(self.input_number, 0, wx.EXPAND)

        # Add a "Compute Factorial" button
        # that calls on_compute_button when clicked
        self.compute_button = \
            wx.Button(pnl, label='Compute Factorial')
        sizer.Add(self.compute_button, 0, wx.EXPAND)
        self.compute_button.Bind(wx.EVT_BUTTON,
                                 self.on_compute_button)

        # Add a textbox for displaying the result of the
        # computation
        self.output_textbox = \
            wx.TextCtrl(pnl, style=wx.TE_MULTILINE)
        font1 = \
            wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL,
                    False, u'Consolas')
        self.output_textbox.SetFont(font1)
        sizer.Add(self.output_textbox, 1, wx.EXPAND)

    def on_compute_button(self, event):
        """
        This function handles the click event of the
        "Compute Factorial" button.
        It computes the factorial of the input and display it
        """

        # Compute the factorial of self.input_number
        res = 1
        for i in range(2, self.input_number.GetValue()):
            res *= i

        # Convert the result into string
        line = str(res)

        # Divide the result into line of 50 characters
        digits_per_line = 50
        res = '\r\n'.join(
            [line[i:i + digits_per_line]
             for i in range(0, len(line), digits_per_line)])

        # Display the result in self.output_textbox
        self.output_textbox.SetValue(res)


if __name__ == '__main__':
    # Create a wx application
    app = wx.App()
    # Create the demo window
    frm = MainFrame(None, 
                    title='Compute Factorial - No Threads ')
    # Show the window
    frm.Show()
    # Start wx main loop
    app.MainLoop()

Good Implementation With A Worker Thread

The good implementation in action

# import wxPython
import wx
# import IntCtrl which is a textbox that accepts only
# integer number
from wx.lib.intctrl import IntCtrl
# for creating thread
from threading import Thread


class MainFrame(wx.Frame):
    def __init__(self, *args, **kw):
        """Construct the frame"""

        # Ensure the parent's __init__ is called
        super(MainFrame, self).__init__(*args, **kw)

        # Create a panel in the frame
        pnl = wx.Panel(self)

        # Create a sizer to stack the child widgets vertically
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        pnl.SetSizer(self.sizer)

        # Add a caption: Input Number
        label = wx.StaticText(pnl, label="Input Number:")
        self.sizer.Add(label, 0, wx.EXPAND)

        # Add a textbox that accepts only integer number
        self.input_number = IntCtrl(pnl)
        self.sizer.Add(self.input_number, 0, wx.EXPAND)

        # Add a "Compute Factorial" button
        # that calls on_compute_button when clicked
        self.compute_button = \
            wx.Button(pnl, label='Compute Factorial')
        self.sizer.Add(self.compute_button, 0, wx.EXPAND)
        self.compute_button.Bind(wx.EVT_BUTTON,
                                 self.on_compute_button)

        # Add an hidden stop button that calls
        # on_stop_button when clicked
        # It will be displayed during processing
        self.stop_button = wx.Button(pnl, label='Stop')
        self.sizer.Add(self.stop_button, 0, wx.EXPAND)
        self.stop_button.Hide()
        self.stop_button.Bind(wx.EVT_BUTTON,
                              self.on_stop_button)
        self.stop_flag = False

        # Add an hidden progress bar
        # It will be displayed during processing
        self.progress_bar = wx.Gauge(pnl,
                                     style=wx.GA_HORIZONTAL)
        self.progress_bar.Hide()
        self.sizer.Add(self.progress_bar, 0, wx.EXPAND)

        # Add a textbox for displaying the result
        # of the computation
        self.output_textbox = \
            wx.TextCtrl(pnl, style=wx.TE_MULTILINE)
        font1 = wx.Font(10, wx.MODERN, wx.NORMAL,
                        wx.NORMAL, False, u'Consolas')
        self.output_textbox.SetFont(font1)
        self.sizer.Add(self.output_textbox, 1, wx.EXPAND)

        # Bind event handler for EVT_COMPUTATION_COMPLETED
        self.EVT_COMPUTATION_COMPLETED = wx.Window.NewControlId()
        self.Connect(-1, -1, self.EVT_COMPUTATION_STOPPED,
                     self.on_computation_completed)

        # Bind event handler for EVT_COMPUTATION_STOPPED
        self.EVT_COMPUTATION_STOPPED = wx.Window.NewControlId()
        self.Connect(-1, -1, self.EVT_COMPUTATION_STOPPED,
                     self.on_computation_stopped)

        # Bind event handler for EVT_PROGRESS
        self.EVT_PROGRESS = wx.Window.NewControlId()
        self.Connect(-1, -1, self.EVT_PROGRESS,
                     self.on_progress)

        # Create a timer fot updating the progress
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
        self.update_progress = True

    # timer event handler
    def on_timer(self, event):
        # signal to the thread to update the progress bar
        self.update_progress = True

    def on_compute_button(self, event):
        """
        This function handles the click event of the
        "Compute Factorial" button.
        It starts a thread that computes the factorial
        of the input and display it
        """

        # update the GUI:
        #   1. hide the compute button
        #   2. display the stop button and the progress bar
        #   3. start a timer for updating the progress bar
        self.set_display_mode(is_running=True)

        # stop_flag is used for stopping the thread when
        # clicking on stop button
        self.stop_flag = False

        # get the input number from the text box
        input_number = self.input_number.GetValue()

        # initialize the progress bar
        self.progress_bar.SetRange(input_number)
        self.progress_bar.SetValue(0)

        # start the thread
        Thread(target=self.compute_factorial_thread,
               args=(input_number,)).start()

    def set_display_mode(self, is_running):
        """
        Update the GUI according the current mode
        """
        if is_running:
            self.compute_button.Hide()
            self.stop_button.Show()
            self.progress_bar.Show()
            self.timer.Start(500)
        else:
            self.compute_button.Show()
            self.stop_button.Hide()
            self.progress_bar.Hide()
            self.timer.Stop()
        self.sizer.Layout()

    # event handler for the stop button
    def on_stop_button(self, event):
        # signal to the thread to stop
        self.stop_flag = True

    def post_event(self, event_id, data=None):
        """
        Post an event of a specific type with
        some data argument
        """
        event = wx.PyEvent()
        event.SetEventType(event_id)
        event.data = data
        wx.PostEvent(self, event)

    def compute_factorial_thread(self, input_num):
        """
        thread that compute factorial
        """
        res = 1
        # computation loop
        for i in range(2, input_num):
            res *= i
            # if the update progress flag is on
            if self.update_progress:
                # turn off the update progress flag
                self.update_progress = False
                # update the progress bar
                self.post_event(self.EVT_PROGRESS, i)
            # if the stop flag is on
            if self.stop_flag:
                # update the GUI thread that the
                # computation was stopped
                self.post_event(self.EVT_COMPUTATION_STOPPED)
                # quit the thread
                return

        # update the progress bar to 100%
        self.post_event(self.EVT_PROGRESS, input_num-1)

        # Convert the result into string
        line = str(res)

        # Divide the result into line of 50 characters
        digits_per_line = 50
        res = '\r\n'.join(
            [line[i:i + digits_per_line]
             for i in range(0, len(line), digits_per_line)])

        # update the GUI thread with the result of the
        # computation
        event = wx.PyEvent()
        event.SetEventType(self.EVT_COMPUTATION_COMPLETED)
        event.data = res
        wx.PostEvent(self, event)

    # this event handler is called when the thread is
    # completed
    def on_computation_completed(self, event):
        # Display the result in self.output_textbox
        self.output_textbox.SetValue(event.data)
        # return the GUI to the initial mode
        self.set_display_mode(is_running=False)

    # this event handler is called when the thread is
    # stopped
    def on_computation_stopped(self, event):
        # Write "Sopped" in self.output_textbox
        self.output_textbox.SetValue("Stopped")
        # return the GUI to the initial mode
        self.set_display_mode(is_running=False)

    # this event handler is called for updating the
    # progress bar
    def on_progress(self, event):
        self.progress_bar.SetValue(event.data)


if __name__ == '__main__':
    # Create a wx application
    app = wx.App()
    # Create the demo window
    frm = MainFrame(
        None, title='Compute Factorial - Worker Thread ')
    # Show the window
    frm.Show()
    # Start wx main loop
    app.MainLoop()


	

Leave a Reply

Your email address will not be published. Required fields are marked *

Close Bitnami banner
Bitnami