Why does wx.Yield() not work?

Asked this question on stackoverflow, will synchronize with this thread for users who only use groups.
https://stackoverflow.com/questions/48762163/why-does-wx-yield-not-work

I am well aware/familiar how to handle LongRunningTasks in wxPython (using threading.Thread is just working fine). But I always wondered, why wx.Yield() and its siblings are not working (or how they shall be used properly).

Attached a (not so) minimal example, tested with 4.0.0a2 msw (phoenix):

from __future__ import print_function
from time import sleep
from datetime import datetime
import wx

def long_running(handler):
    for i in range(10):
        thetxt = '{0}: {1}'.format(str(datetime.now()), str(i))
        sleep(1) # using this as drop-in for something which is blocking
        wx.SafeYield()
        wx.CallAfter(handler, thetxt)

class tst_frm(wx.Frame):
    def __init__(self, *args, **kwds):
        wx.Frame.__init__(self, *args, **kwds)
        self.btn = wx.Button(self, -1, 'Click to Status')
        self.btn.Bind(wx.EVT_BUTTON, self.on_btn)
       
    def on_btn(self, evt):
        the_txt = '{0}: EVT_BUTTON'.format(str(datetime.now()))
        self.update_prog(the_txt)
       
    def update_prog(self, update_txt):
        """Handler for task update (``str``)."""
        print(update_txt)
       
if __name__ == '__main__':
    app = wx.App(redirect=False)
    frm = tst_frm(None, -1, 'test_long')
    frm.Show()
   
    handler = frm
   
    long_running(handler.update_prog)
   
    app.MainLoop()

The button clicks are being registered, but neither the wx.CallAfter events nor the button events are being processed until after long_running has completed.

My questions:

  • wx.Yield should allow to process events which are piling up?
  • Can this example being made work with wx.Yield and if yes, how?
  • If not, What is the explanation why it does not work?

I did not run your code, but I noticed these two:
- SafeYield is supposed not to deliver user input events like button clicks; try Yield instead if you need these
- wx.CallAfter(handler, thetxt) will call your frame, but that does not have a __call__ method

Regards,
Dietmar

I did not run your code, but I noticed these two:

  • SafeYield is supposed not to deliver user input events like button
    clicks; try Yield instead if you need these

Changing wx.SafeYield() to wx.Yield() gives the same result (or better said will not lead to the EVT_BUTTON events be processed (these will be processed after long_running has finished)

  • wx.CallAfter(handler, thetxt) will call your frame, but that does
    not have a call method

handler in long_running is not the frame, but the update_prog method of the frame, this should be fine

You should definitely try the snippet (should work as copy/paste) so that we are on the same page.

···

On Tuesday, February 13, 2018 at 11:18:05 AM UTC+1, Dietmar Schwertberger wrote:

Try this. It waits to start the long_running function until after the mainloop has started.

from future import print_function

from time import sleep

from datetime import datetime

import wx

def long_running(handler):

for i in range(10):

thetxt = ‘{0}: {1}’.format(str(datetime.now()), str(i))

sleep(1) # using this as drop-in for something which is blocking

wx.CallAfter(handler, thetxt)

wx.Yield()

class tst_frm(wx.Frame):

def init(self, *args, **kwds):

wx.Frame.init(self, *args, **kwds)

self.btn = wx.Button(self, -1, ‘Click to Status’)

self.btn.Bind(wx.EVT_BUTTON, self.on_btn)

wx.CallAfter(long_running, self.update_prog)

def on_btn(self, evt):

the_txt = ‘{0}: EVT_BUTTON’.format(str(datetime.now()))

self.update_prog(the_txt)

def update_prog(self, update_txt):

“”“Handler for task update (str).”“”

print(update_txt)

if name == ‘main’:

app = wx.App(redirect=False)

frm = tst_frm(None, -1, ‘test_long’)

frm.Show()

handler = frm

long_running(handler.update_prog)

app.MainLoop()

···

On Tuesday, February 13, 2018 at 6:26:39 AM UTC-6, nepix32 wrote:

On Tuesday, February 13, 2018 at 11:18:05 AM UTC+1, Dietmar Schwertberger wrote:

I did not run your code, but I noticed these two:

  • SafeYield is supposed not to deliver user input events like button
    clicks; try Yield instead if you need these

Changing wx.SafeYield() to wx.Yield() gives the same result (or better said will not lead to the EVT_BUTTON events be processed (these will be processed after long_running has finished)

  • wx.CallAfter(handler, thetxt) will call your frame, but that does
    not have a call method

handler in long_running is not the frame, but the update_prog method of the frame, this should be fine

You should definitely try the snippet (should work as copy/paste) so that we are on the same page.

Robert White wrote:

Try this. It waits to start the `long_running` function until after
the mainloop has started.

This is the Right Answer. Remember, all of the setup you're doing
basically just sends a bunch of window messages, including frm.Show.
None of those messages can get processed -- and hence the application
can't get property initialized -- until your app gets into its main loop
to drain the messages. You are assuming in long_running that the
application and its main loop are up and running, but they're not. That
won't happen until long_running returns.

···

--
Tim Roberts, timr@probo.com
Providenza & Boekelheide, Inc.

Hello Robert, hello Tim,

yes that was indeed the right answer. I was not taking the fact seriously enough that the main loop was not started yet (mistake by my part because I had observed cases where the GUI was reacting even if there was no call to app.MainLoop() at all).

I slightly changed the Code by Robert and kept the call to long_running outside of the frame class.

So the verdict is: wx.Yield() works but only if used carefully/properly.

from future import print_function
from time import sleep
from datetime import datetime
import wx

def long_running(handler):
for i in range(10):
thetxt = ‘{0}: {1}’.format(str(datetime.now()), str(i))
sleep(1) # using this as drop-in for something which is blocking
wx.CallAfter(handler, thetxt)
wx.Yield()

class tst_frm(wx.Frame):
def init(self, *args, **kwds):
wx.Frame.init(self, *args, **kwds)
self.btn = wx.Button(self, -1, ‘Click to Status’)
self.btn.Bind(wx.EVT_BUTTON, self.on_btn)

def on_btn(self, evt):
    the_txt = '{0}: EVT_BUTTON'.format(str(datetime.now()))
    self.update_prog(the_txt)

def update_prog(self, update_txt):
    """Handler for task update (``str``)."""
    print(update_txt)

if name == ‘main’:
app = wx.App(redirect=False)
frm = tst_frm(None, -1, ‘test_long’)
frm.Show()

handler = frm

wx.CallLater(1500, long_running, handler.update_prog)

app.MainLoop()