ProgressBar and Status Bar not changing in macOS

Hello guys, how are you?

I’m having a little issue in macOS Catalina.
My app have a Status Bar (showing the file that in process) and a progress bar.
But they don’t change over time, it freezes.

The app do what it have to do.

Thanks,
Marcelo

Please create a small runnable example that shows the problem.

The wx.ProgressDialog is working fine in my app on Catalina. So
yeah, creating a small example may help you figure out what the
issue is, or will make it easier for us to see it.

David

Hi guys,

Here are the files. I have the main ui in a .py file and all the actions in another.
I tried to upload here, but as a new user, I wasn’t able to do it.

Here is a WeTransfer link: https://we.tl/t-wqHcI6fruq

But here is a paste from the code (a button activate it):

def copy_thread_start(self, event):
    import threading
    th = threading.Thread(target=self.copy_folder, args=(event,))
    th.start()

def copy_folder(self, event):
    file = "Arquivo"
    for i in range(1000):
        for j in range(1000):
            file = file + str(i)
            self.SetStatusText(f"Copying file {j}")
            total = 1000
            progress = 0
            self.pg_bar.SetValue(0)
            for k in range(1000):
                progress += k
                self.pg_bar.SetValue(int((progress / total) * 100))

    self.SetStatusText("Copy done!")

Thanks,
Marcelo

That paste looks like only part of your code, but I already see a problem: it seems you are making UI/wxPython calls from a secondary thread. This won’t work. You have to make any UI / wxPython calls from the main thread. If you need to communicate with the UI from a secondary thread, you can use wx.CallAfter(). See https://www.blog.pythonlibrary.org/2010/05/22/wxpython-and-threads/ for more details.

Hi Marcelo,

Your sample was too large, with too many red herrings. I’ve
attached a 34-line runnable example of your issue. It’s way
simpler to figure out a problem with a much smaller example.

Scott may well be right about your code, but I see things a
little differently based on my example. I was able to reproduce
the problem you described on Catalina with no threads in the
code. (I did not see the same issue on Windows, only on Mac. But
your original code worked on Windows for me as well.)

What I saw was that there was no “progress” occurring when the
app ran. The “Gauge.SetValue()” call was being executed, but the
UI was not updating until the progress bar reached 100%, that is,
when the for loop was complete. This seemed like a screen
refreshing issue to me. I tried calls to Refresh() and Update(),
but they didn’t seem to do the trick. Adding a wx.Yield() call
did. (wx.Yield lets the OS know that it should process
instructions that have built up in the queue., if I understand it
correctly.) If you comment out the wx.Yield() line, you see the
problem of the UI not updating as the data is incremented on
Catalina.

Hope that helps,

David

progress_bar_2.py (860 Bytes)

Hi David and Scott, thanks for the help!

I’ve tried something, adding a thread system, but even if wx.Yield() it’s still not working.
Here’s the code:

import os
import wx
import time
from threading import *

# Define Buttons
ID_START = 100
ID_STOP = 101

# Define notification event for thread completion
EVT_RESULT_ID = 100


def EVT_RESULT(win, func):
    """Define Result Event."""
    win.Connect(-1, -1, EVT_RESULT_ID, func)


class ResultEvent(wx.PyEvent):
    """Simple event to carry arbitrary result data."""

    def __init__(self, data):
        """Init Result Event."""
        wx.PyEvent.__init__(self)
        self.SetEventType(EVT_RESULT_ID)
        self.data = data


class ThreadTest(Thread):
    def __init__(self, main_window):
        """Init Worker Thread Class."""
        Thread.__init__(self)
        self._main_window = main_window
        self.want_abort = 0
        self.start()

    def run(self):
        self._main_window.pg_bar.SetValue(0)
        for k in range(10, 110, 10):
            if self.want_abort:
                # Use a result of None to acknowledge the abort
                wx.PostEvent(self._main_window, ResultEvent(None))
                return

            self._main_window.pg_bar.SetValue(k)
            print(k)
            wx.Yield()
            time.sleep(1)
        wx.PostEvent(self._main_window, ResultEvent(1))

    def abort(self):
        """abort worker thread."""
        # Method for use by main thread to signal an abort
        self.want_abort = 1


class CopierFrame(wx.Frame):
    def __init__(self, parent):
        # Define the main Frame
        wx.Frame.__init__(self, parent, wx.ID_ANY, "Gauge Test", size=(600, 400),
                          style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE)
        self.SetBackgroundColour(wx.WHITE)

        self.pg_bar = wx.Gauge(self, wx.ID_ANY, 100, (10, 10), (550, 15))

        btn = wx.Button(self, ID_START, "Push Me", (10, 30))
        btn2 = wx.Button(self, ID_STOP, "Stop", (250, 30))
        btn.Bind(wx.EVT_BUTTON, self.start_copy, id=ID_START)
        btn2.Bind(wx.EVT_BUTTON, self.stop_copy, id=ID_STOP)

        # Set up event handler for any worker thread results
        EVT_RESULT(self, self.on_result)

        # No threads at start
        self.thread = None

    def start_copy(self, event):
        if not self.thread:
            self.thread = ThreadTest(self)

    def stop_copy(self, event):
        """Stop any task."""
        # Flag the worker thread to stop if running
        if self.thread:
            self.thread.abort()

    def on_result(self, event):
        if event.data is None:
            # Thread aborted (using our convention of None return)
            print("Aborted")
        else:
            print("Finished")
        # In either event, the worker is done
        self.thread = None


class main(wx.App):

    def OnInit(self):
        frm = CopierFrame(None)
        frm.Show()
        return True


app = main()
app.MainLoop()

You are still calling UI functions from the worker thread, see the tagged lines below:

    def run(self):
        self._main_window.pg_bar.SetValue(0)         ## UI
        for k in range(10, 110, 10):
            if self.want_abort:
                # Use a result of None to acknowledge the abort
                wx.PostEvent(self._main_window, ResultEvent(None))
                return

            self._main_window.pg_bar.SetValue(k)    ## UI
            print(k)
            wx.Yield()                              ## UI
            time.sleep(1)
        wx.PostEvent(self._main_window, ResultEvent(1))

Firstly, wx.Yield should never be called from a non-UI thread. It sits in a loop dispatching any pending events to their event handlers, and all of that needs to be done in the UI thread.

Next, the self._main_window.pg_bar is a UI object, so you should not call any of its methods from a non-UI thread, other than a few exceptions.

If you want to go the PostEvent route then you should reorganize things such that all UI method calls are happening in the bound event handler. In your example that would mean pulling them all out of ThreadTest.run and moving them into CopierFrame.on_result.

It may seem like it’s too simple to be any good, but using wx.CallAfter would actually simplify your example quite a bit. It’s implementation essentially works via posted events as well, except in this case it puts the callable and parameters into an event object and posts the event. When received by the handler it simply calls the given callable, passing the given args and/or kwargs. In a CallAfter approach I would suggest passing a callback (like self.on_result) to the ThreadTest constructor, and then in ThreadTest.run you would do something like wx.CallAfter(self.callback, k), and then you do not need the ResultEvent at all.

Hi guys, thanks again for the help!
I’ve ajusted the code, keeping everything of the UI in the UI thread.
All working now!

Thanks again!

Hey Robin–slightly off topic, but read your last blog post and would love to get in touch directly. Could you send me a DM? I can’t find your email anywhere on this site, or even a way to contact you directly as you mention. Thanks!