Search code examples
pythonwxpythonwxwidgets

wxPython: frame class loads several commands AFTER it is called


I have a function that runs a few intensive commands, so I made a Spinner class, which is just a simple window that appears with a wx.Gauge widget that pulses during loading.

The problem is that, when called in Run, the window doesn't appear until several seconds after it was initialized - self.TriangulatePoints() actually finishes before the window appears. Indeed, if I don't comment out load.End() (which closes the window), the Spinner instance will appear and immediately disappear.

I assume this has something to do with threading, and the program continues to run while Spinner initiates. Is this the case? And if so, can you pause progression of Run() until the Spinner window appears?

It should also be noted that running time.sleep(n) after calling Spinner(...) does not change when in the program sequence it appears on screen.

def Run(self, event):

    gis.points_packed = False
    gis.triangulated = False

    load = Spinner(self, style=wx.DEFAULT_FRAME_STYLE & (~wx.CLOSE_BOX) & (~wx.MAXIMIZE_BOX) ^ (wx.RESIZE_BORDER) & (~wx.MINIMIZE_BOX))
    load.Update('Circle packing points...')

    gis.boundary(infile=gis.loaded_boundary)

    load.Pulse()

    self.GetPoints(None, show=False)

    load.Update("Triangulating nodes...")

    self.TriangulatePoints(None, show=True)

    load.End()

########################################################

class Spinner(wx.Frame):

    def __init__(self, *args, **kwds):
        super(Spinner, self).__init__(*args, **kwds)

        self.SetSize((300,80))
        self.SetTitle('Loading')

        process = "Loading..."
        self.font = wx.Font(pointSize = 12, family = wx.DEFAULT,
                   style = wx.NORMAL, weight = wx.BOLD,
                   faceName = 'Arial')

        self.process_txt = wx.StaticText(self, -1, process)
        self.process_txt.SetFont(self.font)

        self.progress = wx.Gauge(self, id=-1, range=100, pos=(10,30), size=(280,15), name="Loading")        
        self.Update(process)

        self.Centre()
        self.Show(True)

    def End(self):
        self.Close(True)

    def Update(self,txt):

        dc = wx.ScreenDC()
        dc.SetFont(self.font)

        tsize = dc.GetTextExtent(txt)
        self.process_txt.SetPosition((300/2-tsize[0]/2,10))

        self.process_txt.SetLabel(txt)
        self.progress.Pulse()

    def Pulse(self):
        self.progress.Pulse()

Solution

  • By adding wx.Yield() immediately after load.Update('...'), I was able to fix the problem.

    I found the solution through a post that Robin Dunn (@RobinDunn), one of the original authors of wxPython, wrote in a Google group:

    As Micah mentioned the various yield functions are essentially a nested event loop that reads and dispatches pending events from the event queue. When the queue is empty then the yield function returns.

    The reason [wx.Yield()] fixes the problem you are seeing is that your long running tasks are preventing control from returning to the main event loop and so the paint event for your custom widget will just sit in the queue until the long running task completes and control is allowed to return to the main loop. Adding the yield allows those events to be processed sooner, but you may still have problems when the long running task does finally run because any new events that need to be processed during that time (for example, the user clicks a Cancel button) will still have to wait until the LRT is finished.

    Another issue to watch out for when using a yield function is that it could lead to an unexpected recursion. For example you have a LRT that periodically calls yield so events can be processed, but one of the events that happens is one whose event handler starts the LRT again.

    So usually it is better to use some other way to prevent blocking of events while running a the LRT, such as breaking it up into chunks that are run from EVT_IDLE handlers, or using a thread.