Progress frame is unresponsive although it runs in a separate thread

Hi,

I’m trying to run a long running task. While the user waits for the long running task to be completed, a separate small frame with a gauge on it is displayed. As the long running task is executed, the gauge bar is updated.

But the problem is, although I display the mini frame and update the gauge on a separate thread, the mini frame and gauge is unresponsive. Instead, the main frame is responsive.

What I would like to achieve is: when the user presses the “Start” button, the mini frame should be displayed modal, the gauge should be updated, and the main frame should be unclickable.

Please help,

Thanks
Best Regards

import wx
import time
import threading
import wx.lib.agw.pygauge as PG

class MyForm(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Main Frame",
                          style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX,
                          size=wx.Size(400, 200))
        self.panel = wx.Panel(self)
        self.button = wx.Button(self.panel, -1, "Start", size=(60, 30))

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.button, 0, wx.ALIGN_CENTER)
        self.panel.SetSizer(sizer)
        self.panel.Layout()

        self.button.Bind(wx.EVT_BUTTON, self.EvtButton, self.button)


    def EvtButton(self, event):
        WorkerThread(self)

    def print_10(self, prg_frame):
        for i in range(10):
            prg_frame.gauge.SetValue(i)
            prg_frame.gauge.Refresh()
            print(i)
            time.sleep(1)
        prg_frame.Destroy()

class ProgressFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Progress Frame",
                          style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX,
                          size=wx.Size(300, 100))
        self.progress_panel = wx.Panel(self)
        self.gauge = PG.PyGauge(self.progress_panel, -1, size=(100,25),style=wx.GA_HORIZONTAL)
        self.gauge.SetRange(10)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.gauge, 0, wx.ALIGN_CENTER)
        self.progress_panel.SetSizer(sizer)

class WorkerThread(threading.Thread):
    def __init__(self, the_frame):
        threading.Thread.__init__(self)
        self.frame = the_frame

        self.start()

    def run(self):
        print("here")
        self.prg_frame = ProgressFrame()
        self.prg_frame.Show()
        self.frame.print_10(self.prg_frame)


if __name__ == "__main__":
    app = wx.App(False)
    frame = MyForm()
    frame.Show()
    app.MainLoop()

Hi Steve,

When I need to run an operation in a child thread I like to separate the responsibilities so that only the main thread updates the GUI components and the child worker thread communicates with the main thread using events.

I have modified your code so that the gauge is in a modal dialog which prevents interaction with the Frame while it is displayed. The dialog binds two custom events for ‘progress’ and ‘worker ended’ messages. The event handler for the progress messages updates the gauge and the event handler for the worker ended message does a join() on the thread and closes the dialog.

The worker thread creates the events during its operation and passes them to AddPendingEvent().

It’s not really necessary to use 2 separate event types in this case, as the progress event handler could close the dialog when a particular value is received. However, I thought it would be useful to see how a child thread could send different types of event.


import wx
import time
import threading
import wx.lib.agw.pygauge as PG

evt_progress_type = wx.NewEventType()
evt_worker_ended_type = wx.NewEventType()

EVT_PROGRESS = wx.PyEventBinder(evt_progress_type,  1)
EVT_WORKER_ENDED = wx.PyEventBinder(evt_worker_ended_type, 1)

class ProgressEvent(wx.PyCommandEvent):
    def __init__(self, value):
        wx.PyCommandEvent.__init__(self, evt_progress_type, 1)
        self.value = value

class WorkerEndedEvent(wx.PyCommandEvent):
    def __init__(self):
        wx.PyCommandEvent.__init__(self, evt_worker_ended_type, 1)


class MyForm(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Main Frame",
                          style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX,
                          size=wx.Size(400, 200))
        self.panel = wx.Panel(self)
        self.button = wx.Button(self.panel, -1, "Start", size=(60, 30))

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.button, 0, wx.ALIGN_CENTER)
        self.panel.SetSizer(sizer)
        self.panel.Layout()

        self.button.Bind(wx.EVT_BUTTON, self.EvtButton, self.button)

        self.prg_dlg = None


    def EvtButton(self, event):
        self.prg_dlg = ProgressDialog()
        self.prg_dlg.ShowModal()
        self.prg_dlg.Destroy()


class ProgressDialog(wx.Dialog):
    def __init__(self):
        wx.Dialog.__init__(self, None, wx.ID_ANY, "Progress Dialog",
                           style=wx.DEFAULT_DIALOG_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX,
                           size=wx.Size(300, 100))
        self.progress_panel = wx.Panel(self)
        self.gauge = PG.PyGauge(self.progress_panel, -1, size=(100,25),style=wx.GA_HORIZONTAL)
        self.gauge.SetRange(10)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.gauge, 0, wx.ALIGN_CENTER)
        self.progress_panel.SetSizer(sizer)
        sizer.Layout()

        self.wx_app = wx.GetApp()
        self.wx_app.Bind(EVT_PROGRESS, self.OnProgress)
        self.wx_app.Bind(EVT_WORKER_ENDED, self.OnWorkerEnded)

        self.thread = WorkerThread()
        self.thread.start()


    def OnProgress(self, event):
        print(event.value)
        self.gauge.SetValue(event.value)
        self.gauge.Refresh()


    def OnWorkerEnded(self, event):
        print("Worker ended.")
        self.thread.join()
        self.thread = None
        self.EndModal(wx.ID_CLOSE)


class WorkerThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.wx_app = wx.GetApp()

    def run(self):
        for i in range(10):
            evt = ProgressEvent(i)
            self.wx_app.AddPendingEvent(evt)
            time.sleep(0.5)
        evt = WorkerEndedEvent()
        self.wx_app.AddPendingEvent(evt)


if __name__ == "__main__":
    app = wx.App(False)
    frame = MyForm()
    frame.Show()
    app.MainLoop()

Tested on Python 3.10.6 | wxPython 4.2.0 gtk3 (phoenix) wxWidgets 3.2.0 | Linux Mint 21.1

EDITED: to call the dialog’s Destroy() method in MyForm.EvtButton() method and also change the attribute’s name from self.prg_frame to self.prg_dlg.

1 Like

Hello Richard,

Thank you. I had never used event messaging to keep GUI responsive when running a long task. I have always used mixture of threading and pubsub messaging. Your code helped a lot.

One thing I noticed, if I comment out time.sleep(0.5), the GUI is still unresponsive, the gauge doesn’t progress as desired. So I had to make the program “sleep” at each loop, so that the user sees all steps of gauge progress properly. Strange.

Hi Steve,

One thing to bear in mind about pubsub is that the code in the subscriber(s) is run by the thread in which the pub.sendMessage() method is called. If the subscriber code then directly modifies the GUI components, that will be performed in the child thread and not in the main thread as it should be.

The issue with not seeing the progress without the time.sleep() comes down to what you consider to be a “long running task” and how many steps it involves. If the task is completed very quickly then seeing the gauge move is not very helpful. It may be better to have the WorkerEndedEvent trigger the display of a message to inform the user that it has completed.

If this program was using a different control to the PyGauge, I would try calling its Update() method in ProgressDialog.OnProgress(). Unfortunately, the author of the control decided to override the base class’s Update() method to do something completely different! However we can get round that by calling super(PG.PyGauge, self.gauge).Update() instead. I tried it with the sleep() call commented out using 10 steps and it was still too quick to see what was happening. If the number of steps is increased to 1000 (say), then it does make the progress visible. I’m sure that coding purists would frown on calling super() from outside the class, but it’s really a workaround for a bad design choice.

import wx
import threading
import wx.lib.agw.pygauge as PG

evt_progress_type = wx.NewEventType()
evt_worker_ended_type = wx.NewEventType()

EVT_PROGRESS = wx.PyEventBinder(evt_progress_type,  1)
EVT_WORKER_ENDED = wx.PyEventBinder(evt_worker_ended_type, 1)

class ProgressEvent(wx.PyCommandEvent):
    def __init__(self, value):
        wx.PyCommandEvent.__init__(self, evt_progress_type, 1)
        self.value = value

class WorkerEndedEvent(wx.PyCommandEvent):
    def __init__(self):
        wx.PyCommandEvent.__init__(self, evt_worker_ended_type, 1)


class MyForm(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Main Frame",
                          style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX,
                          size=wx.Size(400, 200))
        self.panel = wx.Panel(self)
        self.button = wx.Button(self.panel, -1, "Start", size=(60, 30))

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.button, 0, wx.ALIGN_CENTER)
        self.panel.SetSizer(sizer)
        self.panel.Layout()

        self.Bind(wx.EVT_CLOSE,  self.OnClose)
        self.Bind(wx.EVT_BUTTON, self.EvtButton, self.button)

        self.prg_frame = None


    def EvtButton(self, event):
        self.prg_frame = ProgressDialog()
        self.prg_frame.ShowModal()
        print("Dialog closed", threading.active_count())
        self.prg_frame.Destroy()


    def OnClose(self, event):
        print("OnClose")
        self.Destroy()

MAX_VALUE = 1000


class ProgressDialog(wx.Dialog):
    def __init__(self):
        wx.Dialog.__init__(self, None, wx.ID_ANY, "Progress Dialog",
                           style=wx.DEFAULT_DIALOG_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX,
                           size=wx.Size(300, 100))
        self.progress_panel = wx.Panel(self)
        self.gauge = PG.PyGauge(self.progress_panel, -1, size=(100,25),style=wx.GA_HORIZONTAL)
        self.gauge.SetRange(MAX_VALUE)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.gauge, 0, wx.ALIGN_CENTER)
        self.progress_panel.SetSizer(sizer)
        sizer.Layout()

        self.wx_app = wx.GetApp()
        self.wx_app.Bind(EVT_PROGRESS, self.OnProgress)
        self.wx_app.Bind(EVT_WORKER_ENDED, self.OnWorkerEnded)

        self.thread = WorkerThread()
        print("Starting child thread", threading.active_count())
        self.thread.start()


    def OnProgress(self, event):
        print(event.value, threading.active_count())
        self.gauge.SetValue(event.value)
        self.gauge.Refresh()
        super(PG.PyGauge, self.gauge).Update()


    def OnWorkerEnded(self, event):
        print("Worker ended", self.thread.is_alive())
        self.thread.join()
        self.thread = None
        self.EndModal(wx.ID_CLOSE)


class WorkerThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.wx_app = wx.GetApp()

    def run(self):
        for i in range(MAX_VALUE):
            evt = ProgressEvent(i)
            self.wx_app.AddPendingEvent(evt)
            # time.sleep(0.5)
        evt = WorkerEndedEvent()
        self.wx_app.AddPendingEvent(evt)


if __name__ == "__main__":
    app = wx.App(False)
    frame = MyForm()
    frame.Show()
    app.MainLoop()
    print("MainLoop ended")

1 Like

well, to prop up the responsiveness of the GUI put the long running stuff on a different core (that’s what they are for anyway) :rofl: