Interrupting a GUI function call? (wxWindows Specifically)

David Bolen db3l at fitlinxx.com
Fri May 25 06:54:39 CEST 2001


dframe at tampabay.rr.com (Daniel Frame) writes:

> Basically, I've constructed a GUI with wxPython which runs a very long
> function when a 'START' button is pressed.
> 
> Of course, the GUI is 'locked up' until this function completes
> itself.  I was wanting to add a 'STOP' button to my application which
> would halt this other function if the user got tired of waiting for it
> to complete.
> 
> Is it possible to accomplish this without resorting to using threads?
> 
> If so, How can I have my running function check occaisonally to see if
> the stop button was pressed?
> 
> I've heard of wxYield, but I can't seem to  implement it properly.

There's really three possibilities I would think of - threading,
wxYield, or chunking up your processing in a wxEVT_IDLE handler.  I've
given a small sample of each below.

I personally like threads for this sort of thing (I think they leave
the UI the most responsive), but there's no hard and fast rule.  So
whichever you're more comfortable with.

Threading really isn't that hard - you can just start up a thread to
do your processing, and have it send an event to your main GUI thread
when it has completed.  While it is working, it can check an event
object or some other flag variable to indicate that it should give up
and stop.

For a simple sort of example, here's a small bit of code that presents
a simple (really dumb visually) frame, and uses a worker thread to
simulate some processing (that takes 10s, resulting in a value of 10),
while permitting it to be aborted.  This could be extrapolated to
doing any sort of lengthy processing and returning any sort of result.
Intermediate events could be generated during the processing to give
some indication to the main GUI thread of how things were proceeding
(perhaps updating something like a gauge or some other visual
indicator).

    import time
    from threading import *
    from wxPython.wx import *

    # Button definitions
    ID_START = wxNewId()
    ID_STOP  = wxNewId()

    # Define notification event for thread completion

    EVT_RESULT_ID = wxNewId()    

    def EVT_RESULT(win, func):
        win.Connect(-1, -1, EVT_RESULT_ID, func)

    class ResultEvent(wxPyEvent):
        """Simple event to carry arbitrary result data"""

        def __init__(self, data):
            wxPyEvent.__init__(self)
            self.SetEventType(EVT_RESULT_ID)
            self.data = data

    # Thread class that executes processing

    class WorkerThread(Thread):

        def __init__(self, notify_window):
            Thread.__init__(self)
            self._notify_window = notify_window
            self._want_abort = 0
            # This starts the thread running on creation, but you could
            # also make the GUI thread responsible for calling this
            self.start()

        def run(self):
            # This is the code executing in the new thread. Simulation of
            # a long process (well, 10s here) as a simple loop - you will
            # need to structure your processing so that you periodically
            # peek at the abort variable
            for i in range(10):
                time.sleep(1)
                if self._want_abort:
                    # Use a result of None to acknowledge the abort (of
                    # course you can use whatever you'd like or even
                    # a separate event type)
                    wxPostEvent(self._notify_window,ResultEvent(None))
                    return
            # Here's where the result would be returned (this is an
            # example fixed result of the number 10, but it could be
            # any Python object)
            wxPostEvent(self._notify_window,ResultEvent(10))

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

    # GUI Frame class that spins off the worker thread

    class MainFrame(wxFrame):
        def __init__(self, parent, id):
            wxFrame.__init__(self,parent,id,'Thread Test')

            # Dumb sample frame with two buttons
            wxButton(self,ID_START,'Start',pos=(0,0))
            wxButton(self,ID_STOP,'Stop',pos=(0,50))
            self.status = wxStaticText(self,-1,'',pos=(0,100))

            EVT_BUTTON(self,ID_START,self.OnStart)
            EVT_BUTTON(self,ID_STOP,self.OnStop)

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

            # And indicate we don't have a worker thread yet
            self.worker = None

        def OnStart(self, event):
            # Trigger the worker thread unless it's already busy
            if not self.worker:
                self.status.SetLabel('Starting computation')
                self.worker = WorkerThread(self)

        def OnStop(self, event):
            # Flag the worker thread to stop if running
            if self.worker:
                self.status.SetLabel('Trying to abort computation')
                self.worker.abort()

        def OnResult(self, event):
            if event.data is None:
                # Thread aborted (using our convention of None return)
                self.status.SetLabel('Computation aborted')
            else:
                # Process results here
                self.status.SetLabel('Computation Result: %s' % event.data)
            # In either event, the worker is done
            self.worker = None

    class MainApp(wxApp):

        def OnInit(self):
            self.frame = MainFrame(NULL,-1)
            self.frame.Show(true)
            self.SetTopWindow(self.frame)
            return true

    if __name__ == '__main__':

        app = MainApp(0)
        app.MainLoop()

Oh, and if you're concerned with hanging on an exit if your thread
doesn't terminate for some reason, just add a "self.setDaemon(1)" to
the __init__ and Python won't wait for it to terminate.

The second approach, using wxYield, should be fine too - just add a
call to wxYield() somewhere within the computation code such that it
executes periodically.  At that point, any pending window events will
be dispatched (permitting the window to refresh, process button
presses, etc...).  Then, it's similar to the above in that you set a
flag so that when the original code gets control after the wxYield()
returns it knows to stop processing.

As with the threading case, since all events go through during the
wxYield() you need to protect against trying to run the same operation
twice.

Here's the equivalent of the above but placing the computation right
inside the main window class.  Note that one difference is that unlike
with threading, the responsiveness of your GUI is now directly related
to how frequently you call wxYield, so you may have delays refreshing
your window dependent on that frequency.  You should notice that this is
a bit more sluggish with its frequency of a wxYield() each second.

    import time
    from wxPython.wx import *

    # Button definitions
    ID_START = wxNewId()
    ID_STOP  = wxNewId()

    # GUI Frame class that spins off the worker thread

    class MainFrame(wxFrame):
        def __init__(self, parent, id):
            wxFrame.__init__(self,parent,id,'wxYield Test')

            # Dumb sample frame with two buttons
            wxButton(self,ID_START,'Start',pos=(0,0))
            wxButton(self,ID_STOP,'Stop',pos=(0,50))
            self.status = wxStaticText(self,-1,'',pos=(0,100))

            EVT_BUTTON(self,ID_START,self.OnStart)
            EVT_BUTTON(self,ID_STOP,self.OnStop)

            # Indicate we aren't working on it yet
            self.working = 0

        def OnStart(self, event):
            # Start the processing - this simulates a loop - you need to call
            # wxYield at some periodic interval.
            if not self.working:
                self.status.SetLabel('Starting Computation')
                self.working = 1
                self.need_abort = 0

                for i in range(10):
                    time.sleep(1)
                    wxYield()
                    if self.need_abort:
                        self.status.SetLabel('Computation aborted')
                        break
                else:
                    # Here's where you would process the result
                    # Note you should only do this if not aborted.
                    self.status.SetLabel('Computation Completed')

                # In either event, we aren't running any more
                self.working = 0

        def OnStop(self, event):
            if self.working:
                self.status.SetLabel('Trying to abort computation')
                self.need_abort = 1

    class MainApp(wxApp):

        def OnInit(self):
            self.frame = MainFrame(NULL,-1)
            self.frame.Show(true)
            self.SetTopWindow(self.frame)
            return true

    if __name__ == '__main__':

        app = MainApp(0)
        app.MainLoop()


And finally, you can do your work within an idle handler.  In this
case, you let wxPython generate an IDLE event whenever it has
completed processing normal user events, and then you perform a
"chunk" of your processing in each such case.  This can be a little
tricker depending on your algorithm since you have to be able to
perform the work in discrete pieces.  Inside your IDLE handler, you
request that it be called again if you aren't done, but you want to
make sure that each pass through the handler doesn't take too long.
Effectively, each event is similar to the gap between wxYield() calls
in the previous example, and your GUI responsiveness will be subject
to that latency just as with the wxYield() case.

I'm also not sure you can remove an idle handler once established (or
at least I think I had problems with that in the past), so the code
below just establishes it once and the handler only does work if it's
in the midst of a computation.

    import time
    from wxPython.wx import *

    # Button definitions
    ID_START = wxNewId()
    ID_STOP  = wxNewId()

    # GUI Frame class that spins off the worker thread

    class MainFrame(wxFrame):
        def __init__(self, parent, id):
            wxFrame.__init__(self,parent,id,'Idle Test')

            # Dumb sample frame with two buttons
            wxButton(self,ID_START,'Start',pos=(0,0))
            wxButton(self,ID_STOP,'Stop',pos=(0,50))
            self.status = wxStaticText(self,-1,'',pos=(0,100))

            EVT_BUTTON(self,ID_START,self.OnStart)
            EVT_BUTTON(self,ID_STOP,self.OnStop)
            EVT_IDLE(self,self.OnIdle)

            # Indicate we aren't working on it yet
            self.working = 0

        def OnStart(self, event):
            # Set up for processing and trigger idle event
            if not self.working:
                self.status.SetLabel('Starting Computation')
                self.count = 0
                self.working = 1
                self.need_abort = 0

        def OnIdle(self, event):
            if self.working:
                # This is where the processing takes place, one bit at a time
                if self.need_abort:
                    self.status.SetLabel('Computation aborted')
                else:
                    self.count = self.count + 1
                    time.sleep(1)
                    if self.count < 10:
                        # Still more work to do so request another event
                        event.RequestMore()
                        return
                    else:
                        self.status.SetLabel('Computation completed')

                # Reaching here is an abort or completion - end in either case
                self.working = 0

        def OnStop(self, event):
            if self.working:
                self.status.SetLabel('Trying to abort computation')
                self.need_abort = 1

    class MainApp(wxApp):

        def OnInit(self):
            self.frame = MainFrame(NULL,-1)
            self.frame.Show(true)
            self.SetTopWindow(self.frame)
            return true

    if __name__ == '__main__':

        app = MainApp(0)
        app.MainLoop()


--
-- David
-- 
/-----------------------------------------------------------------------\
 \               David Bolen            \   E-mail: db3l at fitlinxx.com  /
  |             FitLinxx, Inc.            \  Phone: (203) 708-5192    |
 /  860 Canal Street, Stamford, CT  06902   \  Fax: (203) 316-5150     \
\-----------------------------------------------------------------------/



More information about the Python-list mailing list