Advertisement
Guest User

Untitled

a guest
May 20th, 2019
75
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.65 KB | None | 0 0
  1. #! /usr/bin/env python
  2.  
  3. """Tools for running threads behind a wx.ProgressDialog."""
  4.  
  5. # Standard library imports.
  6. import sys
  7. import threading
  8. import time
  9.  
  10. # Third party imports.
  11. import wx
  12.  
  13. class ProgressThread(threading.Thread):
  14. """A thread that does the work represented by a wx.ProgressDialog.
  15.  
  16. It does the work and sends updates to the UI, so the end user can
  17. have some idea what the program is doing.
  18.  
  19. To use, subclass and override the do_work generator.
  20.  
  21. """
  22.  
  23. # GRIPE A wx.ProgressDialog cannot adjust its number of steps after
  24. # creation. However, being able to create the dialog before we know how
  25. # many steps there actually are is very convenient - some threads might not
  26. # actually know how many steps they involve until a lot of processing has
  27. # been done. Thus, we assume that there are one million steps in any thread,
  28. # then internally subdivide it based on the number of actual steps do_work()
  29. # calculates.
  30. _max_num_steps = 1000000
  31.  
  32. # Minimum time in milliseconds between attempts to update the progress
  33. # dialog. Set it to another value if you need to. If it's much lower,
  34. # you'll lock the main thread by flooding the dialog with update attempts.
  35. min_update_wait = 34
  36.  
  37. def __init__(self, parent, title, msg, *args, **kwargs):
  38. """Create a thread tied to a progress dialog.
  39.  
  40. The dialog is spawned when the thread starts running.
  41.  
  42. `parent` is the window this subtask belongs to.
  43.  
  44. `title` is the dialog's title.
  45.  
  46. `msg` is the dialog's starting message.
  47.  
  48. `args` and `kwargs` will be passed to self.do_work() as *args
  49. and **kwargs, allowing you to pass params to your background
  50. thread easily.
  51.  
  52. """
  53.  
  54. threading.Thread.__init__(self)
  55.  
  56. self._parent = parent
  57. self._dialog = wx.ProgressDialog(title, msg, self._max_num_steps,
  58. style=wx.PD_CAN_ABORT)
  59. self._dialog.Center()
  60. self._dialog.Pulse()
  61.  
  62. self._args = args
  63. self._kwargs = kwargs
  64.  
  65. self._cancel_signal = False
  66. self._num_steps = None
  67. self._cur_step = None
  68.  
  69. # The rate at which we step through the self._max_num_steps points
  70. # of progress available to us.
  71. self._stride = None
  72. # How far we are through the self._max_num_steps units in the progress
  73. # bar.
  74. self._cur_progress = None
  75.  
  76. self._update_timestamp = None
  77.  
  78. self._dialog.Bind(wx.EVT_BUTTON, self.handle_cancel_click)
  79.  
  80. def _close_dialog(self, retCode):
  81. """Close this thread's dialog safely.
  82.  
  83. `retCode` is required by ProgressDialog.EndModal(). I think it
  84. sets the result code returned by ShowModal(), but the docs don't
  85. actually make it clear.
  86.  
  87. """
  88.  
  89. wx.CallAfter(self._dialog.EndModal, retCode)
  90. wx.CallAfter(self._dialog.Destroy)
  91.  
  92. def _success_cleanup(self):
  93. """Close the progress dialog and call do_success."""
  94.  
  95. self.update_dialog(self._max_num_steps)
  96. self._close_dialog(wx.ID_OK)
  97.  
  98. self.do_success(self._parent)
  99.  
  100. def _cancel_cleanup(self):
  101. """Handle any post-cancellation cleanup.
  102.  
  103. We hide the dialog and call the implemented cleanup method.
  104.  
  105. """
  106.  
  107. self._close_dialog(wx.ID_CANCEL)
  108.  
  109. self.do_cancel(self._parent)
  110.  
  111. def set_num_steps(self, num_steps):
  112. """Set the number of steps in this thread to `num_steps`.
  113.  
  114. Sets self._cur_step to 0.
  115.  
  116. """
  117.  
  118. self._num_steps = num_steps
  119. self._cur_step = 0
  120.  
  121. self._stride = int(self._max_num_steps / self._num_steps)
  122. self._cur_progress = 0
  123.  
  124. def do_work(self, *args, **kwargs):
  125. """Generator that yields a summary of its next action as a str.
  126.  
  127. Override this to implement your thread's innards.
  128.  
  129. This is a good place to call self.set_num_steps() from.
  130.  
  131. """
  132.  
  133. raise Exception('This generator is not implemented.')
  134.  
  135. def run(self):
  136. """Run this thread."""
  137.  
  138. try:
  139. for next_step in self.do_work(*self._args, **self._kwargs):
  140. if self._cancel_signal:
  141. self._cancel_cleanup()
  142. return
  143.  
  144. self._cur_step += 1
  145. self._cur_progress += self._stride
  146. self.update_dialog(self._cur_progress, next_step)
  147.  
  148. self._success_cleanup()
  149. except Exception as exc:
  150. # GRIPE I'm not sure wx.ID_ABORT is a good way to say "we crashed".
  151. self._close_dialog(wx.ID_ABORT)
  152.  
  153. # Rethrow this exception in the GUI thread, so our handler can
  154. # deal with it.
  155. exc_info = sys.exc_info()
  156. def rethrow(exc_info):
  157. raise exc_info[1], None, exc_info[2]
  158. wx.CallAfter(rethrow, exc_info)
  159.  
  160. def update_dialog(self, step_num, msg=None):
  161. """Set this thread's progress to `step_num` and show `msg`.
  162.  
  163. If `msg` is not passed, the message is not updated.
  164.  
  165. If it has not been at least self.min_update_wait milliseconds
  166. since the last update to the dialog, the new update will be
  167. silently dropped.
  168.  
  169. Otherwise, we may flood the GUI thread with events, and cause
  170. everything to freeze until this thread is done running. That
  171. would defeat the purpose of running things in a thread.
  172.  
  173. This behavior is admittedly iffy - it might be better to look
  174. at how many updates are as yet unprocessed, and decide whether
  175. the current one should be sent based on that.
  176.  
  177. That's harder, though, and might run too slowly to be usable.
  178. This appears to work.
  179.  
  180. """
  181.  
  182. cur_timestamp = time.time() * 1000
  183. if self._update_timestamp is not None:
  184. if cur_timestamp - self._update_timestamp < self.min_update_wait:
  185. return
  186.  
  187. self._update_timestamp = cur_timestamp
  188.  
  189. args = [self._dialog.Update, step_num]
  190. if msg is not None:
  191. args.append(msg)
  192.  
  193. wx.CallAfter(*args)
  194.  
  195. def do_cancel(self):
  196. """Perform any cleanup needed to properly cancel this thread.
  197.  
  198. Subclasses should override this.
  199.  
  200. For computations without side effects, `pass` is sufficient.
  201. Operations that modify the environment should use this to ensure
  202. the environment is left in the initial state.
  203.  
  204. """
  205.  
  206. raise Exception('This method is not implemented.')
  207.  
  208. def handle_cancel_click(self, event):
  209. """Handle clicks on the dialog's Cancel button."""
  210.  
  211. self.cancel()
  212.  
  213. def cancel(self):
  214. """Ask this thread to stop running.
  215.  
  216. A canceled thread should leave the environment as it was before it
  217. started. It is up to self.do_cancel() to ensure this is true.
  218.  
  219. """
  220.  
  221. self._cancel_signal = True
  222.  
  223. class Counter(ProgressThread):
  224. """A thread that counts to n slowly.
  225.  
  226. This is meant as a simple example and test case - it has no use that
  227. I am aware of.
  228.  
  229. """
  230.  
  231. def do_work(self, n, cause_exception=False):
  232. """Count to `n` slowly.
  233.  
  234. `cause_exception` can be set to True if you want to test
  235. exception handling.
  236.  
  237. """
  238.  
  239. self.n = n
  240.  
  241. self.set_num_steps(n)
  242. for i in range(n):
  243. yield str(i + 1)
  244.  
  245. if cause_exception is True:
  246. raise Exception('I am crashing on purpose.')
  247.  
  248. def do_cancel(self, parent):
  249. """Explain what happened."""
  250.  
  251. dialog = wx.MessageDialog(parent, 'You stopped me! Why?',
  252. 'Not %s' % self.n)
  253. dialog.ShowModal()
  254.  
  255. def do_success(self, parent):
  256. """Display a dialog."""
  257.  
  258. dialog = wx.MessageDialog(parent, 'I counted to %s!' % self.n,
  259. 'I Did It')
  260. dialog.ShowModal()
  261.  
  262.  
  263. class ProgressFrame(wx.Frame):
  264. """Primitive demo of ProgressThread."""
  265.  
  266. def __init__(self, parent, id):
  267. """Create the ProgressFrame."""
  268.  
  269. wx.Frame.__init__(self, parent, id, 'Thread Test')
  270.  
  271. self.btn = wx.Button(self, wx.ID_ANY, 'Start', pos=(0,0))
  272.  
  273. self.btn.Bind(wx.EVT_BUTTON, self.OnStart)
  274.  
  275. def OnStart(self, event):
  276. """Handle clicks on the Start button."""
  277.  
  278. thread = Counter(self, 'Counting...', 'Initializing...', 1000000)
  279. thread.start()
  280.  
  281. class ProgressApp(wx.App):
  282. """App to demonstrate my ProgressThread."""
  283.  
  284. def OnInit(self):
  285. """Initialize app."""
  286.  
  287. self.frame = ProgressFrame(None, -1)
  288. self.frame.Show(True)
  289. self.frame.Center()
  290. self.SetTopWindow(self.frame)
  291.  
  292. return True
  293.  
  294. def main():
  295. """Simple demo of using ProgressThread."""
  296.  
  297. app = ProgressApp(0)
  298. app.MainLoop()
  299.  
  300. if __name__ == '__main__':
  301. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement