Long running tasks with theading and event
Zeilennummern ein/ausschalten
1 import time
2 from threading import *
3 import wx
4
5 # Button definitions
6 ID_START = wx.NewId()
7 ID_STOP = wx.NewId()
8
9 # Define notification event for thread completion
10 EVT_RESULT_ID = wx.NewId()
11
12 def EVT_RESULT(win, func):
13 """Define Result Event."""
14 win.Connect(-1, -1, EVT_RESULT_ID, func)
15
16 class ResultEvent(wx.PyEvent):
17 """Simple event to carry arbitrary result data."""
18 def __init__(self, data):
19 """Init Result Event."""
20 wx.PyEvent.__init__(self)
21 self.SetEventType(EVT_RESULT_ID)
22 self.data = data
23
24 # Thread class that executes processing
25 class WorkerThread(Thread):
26 """Worker Thread Class."""
27 def __init__(self, notify_window):
28 """Init Worker Thread Class."""
29 Thread.__init__(self)
30 self._notify_window = notify_window
31 self._want_abort = 0
32 # This starts the thread running on creation, but you could
33 # also make the GUI thread responsible for calling this
34 self.start()
35
36 def run(self):
37 """Run Worker Thread."""
38 # This is the code executing in the new thread. Simulation of
39 # a long process (well, 10s here) as a simple loop - you will
40 # need to structure your processing so that you periodically
41 # peek at the abort variable
42 for i in range(10):
43 time.sleep(1)
44 if self._want_abort:
45 # Use a result of None to acknowledge the abort (of
46 # course you can use whatever you'd like or even
47 # a separate event type)
48 wx.PostEvent(self._notify_window, ResultEvent(None))
49 return
50 # Here's where the result would be returned (this is an
51 # example fixed result of the number 10, but it could be
52 # any Python object)
53 wx.PostEvent(self._notify_window, ResultEvent(10))
54
55 def abort(self):
56 """abort worker thread."""
57 # Method for use by main thread to signal an abort
58 self._want_abort = 1
59
60 # GUI Frame class that spins off the worker thread
61 class MainFrame(wx.Frame):
62 """Class MainFrame."""
63 def __init__(self, parent, id):
64 """Create the MainFrame."""
65 wx.Frame.__init__(self, parent, id, 'Thread Test')
66
67 # Dumb sample frame with two buttons
68 wx.Button(self, ID_START, 'Start', pos=(0,0))
69 wx.Button(self, ID_STOP, 'Stop', pos=(0,50))
70 self.status = wx.StaticText(self, -1, '', pos=(0,100))
71
72 self.Bind(wx.EVT_BUTTON, self.OnStart, id=ID_START)
73 self.Bind(wx.EVT_BUTTON, self.OnStop, id=ID_STOP)
74
75 # Set up event handler for any worker thread results
76 EVT_RESULT(self,self.OnResult)
77
78 # And indicate we don't have a worker thread yet
79 self.worker = None
80
81 def OnStart(self, event):
82 """Start Computation."""
83 # Trigger the worker thread unless it's already busy
84 if not self.worker:
85 self.status.SetLabel('Starting computation')
86 self.worker = WorkerThread(self)
87
88 def OnStop(self, event):
89 """Stop Computation."""
90 # Flag the worker thread to stop if running
91 if self.worker:
92 self.status.SetLabel('Trying to abort computation')
93 self.worker.abort()
94
95 def OnResult(self, event):
96 """Show Result status."""
97 if event.data is None:
98 # Thread aborted (using our convention of None return)
99 self.status.SetLabel('Computation aborted')
100 else:
101 # Process results here
102 self.status.SetLabel('Computation Result: %s' % event.data)
103 # In either event, the worker is done
104 self.worker = None
105
106 class MainApp(wx.App):
107 """Class Main App."""
108 def OnInit(self):
109 """Init Main App."""
110 self.frame = MainFrame(None, -1)
111 self.frame.Show(True)
112 self.SetTopWindow(self.frame)
113 return True
114
115 if __name__ == '__main__':
116 app = MainApp(0)
117 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.
Zeilennummern ein/ausschalten
1 import time
2 import wx
3
4 # Button definitions
5 ID_START = wx.NewId()
6 ID_STOP = wx.NewId()
7
8 # GUI Frame class that spins off the worker thread
9 class MainFrame(wx.Frame):
10 """Class MainFrame."""
11 def __init__(self, parent, id):
12 """Create the MainFrame."""
13 wx.Frame.__init__(self, parent, id, 'wxYield Test')
14
15 # Dumb sample frame with two buttons
16 wx.Button(self, ID_START, 'Start', pos=(0,0))
17 wx.Button(self, ID_STOP, 'Stop', pos=(0,50))
18 self.status = wx.StaticText(self, -1, '', pos=(0,100))
19
20 self.Bind (wx.EVT_BUTTON, self.OnStart, id=ID_START)
21 self.Bind (wx.EVT_BUTTON, self.OnStop, id=ID_STOP)
22
23 # Indicate we aren't working on it yet
24 self.working = 0
25
26 def OnStart(self, event):
27 """Start Computation."""
28 # Start the processing - this simulates a loop - you need to call
29 # wx.Yield at some periodic interval.
30 if not self.working:
31 self.status.SetLabel('Starting Computation')
32 self.working = 1
33 self.need_abort = 0
34
35 for i in range(10):
36 time.sleep(1)
37 wx.Yield()
38 if self.need_abort:
39 self.status.SetLabel('Computation aborted')
40 break
41 else:
42 # Here's where you would process the result
43 # Note you should only do this if not aborted.
44 self.status.SetLabel('Computation Completed')
45
46 # In either event, we aren't running any more
47 self.working = 0
48
49 def OnStop(self, event):
50 """Stop Computation."""
51 if self.working:
52 self.status.SetLabel('Trying to abort computation')
53 self.need_abort = 1
54
55 class MainApp(wx.App):
56 """Class Main App."""
57 def OnInit(self):
58 """Init Main App."""
59 self.frame = MainFrame(None,-1)
60 self.frame.Show(True)
61 self.SetTopWindow(self.frame)
62 return True
63
64 if __name__ == '__main__':
65 app = MainApp(0)
66 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. [Actually, you can use the Disconnect method to remove an event handler binding, although there is no real need to do so as there is very little overhead if you use a guard condition as in the code below. —Robin]
Zeilennummern ein/ausschalten
1 import time
2 import wx
3
4 # Button definitions
5 ID_START = wx.NewId()
6 ID_STOP = wx.NewId()
7
8 # GUI Frame class that spins off the worker thread
9 class MainFrame(wx.Frame):
10 """Class MainFrame."""
11 def __init__(self, parent, id):
12 """Create the MainFrame."""
13 wx.Frame.__init__(self, parent, id, 'Idle Test')
14
15 # Dumb sample frame with two buttons
16 wx.Button(self, ID_START, 'Start',p os=(0,0))
17 wx.Button(self, ID_STOP, 'Stop', pos=(0,50))
18 self.status = wx.StaticText(self, -1, '', pos=(0,100))
19
20 self.Bind (wx.EVT_BUTTON, self.OnStart, id=ID_START)
21 self.Bind (wx.EVT_BUTTON, self.OnStop, id=ID_STOP)
22 self.Bind (wx.EVT_IDLE, self.OnIdle)
23
24 # Indicate we aren't working on it yet
25 self.working = 0
26
27 def OnStart(self, event):
28 """Start Computation."""
29 # Set up for processing and trigger idle event
30 if not self.working:
31 self.status.SetLabel('Starting Computation')
32 self.count = 0
33 self.working = 1
34 self.need_abort = 0
35
36 def OnIdle(self, event):
37 """Idle Handler."""
38 if self.working:
39 # This is where the processing takes place, one bit at a time
40 if self.need_abort:
41 self.status.SetLabel('Computation aborted')
42 else:
43 self.count = self.count + 1
44 time.sleep(1)
45 if self.count < 10:
46 # Still more work to do so request another event
47 event.RequestMore()
48 return
49 else:
50 self.status.SetLabel('Computation completed')
51
52 # Reaching here is an abort or completion - end in either case
53 self.working = 0
54
55 def OnStop(self, event):
56 """Stop Computation."""
57 if self.working:
58 self.status.SetLabel('Trying to abort computation')
59 self.need_abort = 1
60
61 class MainApp(wx.App):
62 """Class Main App."""
63 def OnInit(self):
64 """Init Main App."""
65 self.frame = MainFrame(None, -1)
66 self.frame.Show(True)
67 self.SetTopWindow(self.frame)
68 return True
69
70 if __name__ == '__main__':
71 app = MainApp(0)
72 app.MainLoop()