| 1 | """ |
| 2 | This demonstrates a simple use of delayedresult: get/compute |
| 3 | something that takes a long time, without hanging the GUI while this |
| 4 | is taking place. |
| 5 | |
| 6 | The top button runs a small GUI that uses wx.lib.delayedresult.startWorker |
| 7 | to wrap a long-running function into a separate thread. Just click |
| 8 | Get, and move the slider, and click Get and Abort a few times, and |
| 9 | observe that GUI responds. The key functions to look for in the code |
| 10 | are startWorker() and __handleResult(). |
| 11 | |
| 12 | The second button runs the same GUI, but without delayedresult. Click |
| 13 | Get: now the get/compute is taking place in main thread, so the GUI |
| 14 | does not respond to user actions until worker function returns, it's |
| 15 | not even possible to Abort. |
| 16 | """ |
| 17 | |
| 18 | import wx |
| 19 | import wx.lib.delayedresult as delayedresult |
| 20 | |
| 21 | |
| 22 | class FrameSimpleDelayedBase(wx.Frame): |
| 23 | def __init__(self, *args, **kwds): |
| 24 | wx.Frame.__init__(self, *args, **kwds) |
| 25 | pnl = wx.Panel(self) |
| 26 | self.checkboxUseDelayed = wx.CheckBox(pnl, -1, "Using delayedresult") |
| 27 | self.buttonGet = wx.Button(pnl, -1, "Get") |
| 28 | self.buttonAbort = wx.Button(pnl, -1, "Abort") |
| 29 | self.slider = wx.Slider(pnl, -1, 0, 0, 10, size=(100,-1), |
| 30 | style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS) |
| 31 | self.textCtrlResult = wx.TextCtrl(pnl, -1, "", style=wx.TE_READONLY) |
| 32 | |
| 33 | self.checkboxUseDelayed.SetValue(1) |
| 34 | self.checkboxUseDelayed.Enable(False) |
| 35 | self.buttonAbort.Enable(False) |
| 36 | |
| 37 | vsizer = wx.BoxSizer(wx.VERTICAL) |
| 38 | hsizer = wx.BoxSizer(wx.HORIZONTAL) |
| 39 | vsizer.Add(self.checkboxUseDelayed, 0, wx.ALL, 10) |
| 40 | hsizer.Add(self.buttonGet, 0, wx.ALL, 5) |
| 41 | hsizer.Add(self.buttonAbort, 0, wx.ALL, 5) |
| 42 | hsizer.Add(self.slider, 0, wx.ALL, 5) |
| 43 | hsizer.Add(self.textCtrlResult, 0, wx.ALL, 5) |
| 44 | vsizer.Add(hsizer, 0, wx.ALL, 5) |
| 45 | pnl.SetSizer(vsizer) |
| 46 | vsizer.SetSizeHints(self) |
| 47 | |
| 48 | self.Bind(wx.EVT_BUTTON, self.handleGet, self.buttonGet) |
| 49 | self.Bind(wx.EVT_BUTTON, self.handleAbort, self.buttonAbort) |
| 50 | |
| 51 | |
| 52 | |
| 53 | |
| 54 | class FrameSimpleDelayed(FrameSimpleDelayedBase): |
| 55 | """This demos simplistic use of delayedresult module.""" |
| 56 | |
| 57 | def __init__(self, *args, **kwargs): |
| 58 | FrameSimpleDelayedBase.__init__(self, *args, **kwargs) |
| 59 | self.jobID = 0 |
| 60 | self.abortEvent = delayedresult.AbortEvent() |
| 61 | self.Bind(wx.EVT_CLOSE, self.handleClose) |
| 62 | |
| 63 | def setLog(self, log): |
| 64 | self.log = log |
| 65 | |
| 66 | def handleClose(self, event): |
| 67 | """Only needed because in demo, closing the window does not kill the |
| 68 | app, so worker thread continues and sends result to dead frame; normally |
| 69 | your app would exit so this would not happen.""" |
| 70 | if self.buttonAbort.IsEnabled(): |
| 71 | self.log( "Exiting: Aborting job %s" % self.jobID ) |
| 72 | self.abortEvent.set() |
| 73 | self.Destroy() |
| 74 | |
| 75 | def handleGet(self, event): |
| 76 | """Compute result in separate thread, doesn't affect GUI response.""" |
| 77 | self.buttonGet.Enable(False) |
| 78 | self.buttonAbort.Enable(True) |
| 79 | self.abortEvent.clear() |
| 80 | self.jobID += 1 |
| 81 | |
| 82 | self.log( "Starting job %s in producer thread: GUI remains responsive" |
| 83 | % self.jobID ) |
| 84 | delayedresult.startWorker(self._resultConsumer, self._resultProducer, |
| 85 | wargs=(self.jobID,self.abortEvent), jobID=self.jobID) |
| 86 | |
| 87 | |
| 88 | def _resultProducer(self, jobID, abortEvent): |
| 89 | """Pretend to be a complex worker function or something that takes |
| 90 | long time to run due to network access etc. GUI will freeze if this |
| 91 | method is not called in separate thread.""" |
| 92 | import time |
| 93 | count = 0 |
| 94 | while not abortEvent() and count < 50: |
| 95 | time.sleep(0.1) |
| 96 | count += 1 |
| 97 | return jobID |
| 98 | |
| 99 | |
| 100 | def handleAbort(self, event): |
| 101 | """Abort the result computation.""" |
| 102 | self.log( "Aborting result for job %s" % self.jobID ) |
| 103 | self.buttonGet.Enable(True) |
| 104 | self.buttonAbort.Enable(False) |
| 105 | self.abortEvent.set() |
| 106 | |
| 107 | |
| 108 | def _resultConsumer(self, delayedResult): |
| 109 | jobID = delayedResult.getJobID() |
| 110 | assert jobID == self.jobID |
| 111 | try: |
| 112 | result = delayedResult.get() |
| 113 | except Exception, exc: |
| 114 | self.log( "Result for job %s raised exception: %s" % (jobID, exc) ) |
| 115 | return |
| 116 | |
| 117 | # output result |
| 118 | self.log( "Got result for job %s: %s" % (jobID, result) ) |
| 119 | self.textCtrlResult.SetValue(str(result)) |
| 120 | |
| 121 | # get ready for next job: |
| 122 | self.buttonGet.Enable(True) |
| 123 | self.buttonAbort.Enable(False) |
| 124 | |
| 125 | |
| 126 | class FrameSimpleDirect(FrameSimpleDelayedBase): |
| 127 | """This does not use delayedresult so the GUI will freeze while |
| 128 | the GET is taking place.""" |
| 129 | |
| 130 | def __init__(self, *args, **kwargs): |
| 131 | self.jobID = 1 |
| 132 | FrameSimpleDelayedBase.__init__(self, *args, **kwargs) |
| 133 | self.checkboxUseDelayed.SetValue(False) |
| 134 | |
| 135 | def setLog(self, log): |
| 136 | self.log = log |
| 137 | |
| 138 | def handleGet(self, event): |
| 139 | """Use delayedresult, this will compute result in separate |
| 140 | thread, and will affect GUI response because a thread is not |
| 141 | used.""" |
| 142 | self.buttonGet.Enable(False) |
| 143 | self.buttonAbort.Enable(True) |
| 144 | |
| 145 | self.log( "Doing job %s without delayedresult (same as GUI thread): GUI hangs (for a while)" % self.jobID ) |
| 146 | result = self._resultProducer(self.jobID) |
| 147 | self._resultConsumer( result ) |
| 148 | |
| 149 | def _resultProducer(self, jobID): |
| 150 | """Pretend to be a complex worker function or something that takes |
| 151 | long time to run due to network access etc. GUI will freeze if this |
| 152 | method is not called in separate thread.""" |
| 153 | import time |
| 154 | time.sleep(5) |
| 155 | return jobID |
| 156 | |
| 157 | def handleAbort(self, event): |
| 158 | """can never be called""" |
| 159 | pass |
| 160 | |
| 161 | def _resultConsumer(self, result): |
| 162 | # output result |
| 163 | self.log( "Got result for job %s: %s" % (self.jobID, result) ) |
| 164 | self.textCtrlResult.SetValue(str(result)) |
| 165 | |
| 166 | # get ready for next job: |
| 167 | self.buttonGet.Enable(True) |
| 168 | self.buttonAbort.Enable(False) |
| 169 | self.jobID += 1 |
| 170 | |
| 171 | |
| 172 | #--------------------------------------------------------------------------- |
| 173 | #--------------------------------------------------------------------------- |
| 174 | |
| 175 | class TestPanel(wx.Panel): |
| 176 | def __init__(self, parent, log): |
| 177 | self.log = log |
| 178 | wx.Panel.__init__(self, parent, -1) |
| 179 | |
| 180 | vsizer = wx.BoxSizer(wx.VERTICAL) |
| 181 | b = wx.Button(self, -1, "Long-running function in separate thread") |
| 182 | vsizer.Add(b, 0, wx.ALL, 5) |
| 183 | self.Bind(wx.EVT_BUTTON, self.OnButton1, b) |
| 184 | |
| 185 | b = wx.Button(self, -1, "Long-running function in GUI thread") |
| 186 | vsizer.Add(b, 0, wx.ALL, 5) |
| 187 | self.Bind(wx.EVT_BUTTON, self.OnButton2, b) |
| 188 | |
| 189 | bdr = wx.BoxSizer() |
| 190 | bdr.Add(vsizer, 0, wx.ALL, 50) |
| 191 | self.SetSizer(bdr) |
| 192 | self.Layout() |
| 193 | |
| 194 | def OnButton1(self, evt): |
| 195 | frame = FrameSimpleDelayed(self, title="Long-running function in separate thread") |
| 196 | frame.setLog(self.log.WriteText) |
| 197 | frame.Show() |
| 198 | |
| 199 | def OnButton2(self, evt): |
| 200 | frame = FrameSimpleDirect(self, title="Long-running function in GUI thread") |
| 201 | frame.setLog(self.log.WriteText) |
| 202 | frame.Show() |
| 203 | |
| 204 | |
| 205 | #--------------------------------------------------------------------------- |
| 206 | |
| 207 | |
| 208 | def runTest(frame, nb, log): |
| 209 | win = TestPanel(nb, log) |
| 210 | return win |
| 211 | |
| 212 | |
| 213 | #--------------------------------------------------------------------------- |
| 214 | |
| 215 | |
| 216 | overview = __doc__ |
| 217 | |
| 218 | |
| 219 | if __name__ == '__main__': |
| 220 | import sys,os |
| 221 | import run |
| 222 | run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:]) |
| 223 | |
| 224 | |