(The tkinter version of this can be found here)
If you have ever tried to use
You will notice that the call to change the label text to running doesn't seem work,the listbox does not update until after the sleep has finished, and the button locks in the down position.
Example of the problem
Example of adding a thread but still getting a error
To get around this locking up problem we can use threads.
If we use a thread on its own the gui will become responsive but if the button is pressed a few times in a row, the number sequence will get jumbled up because each time the button is pressed a new thread is started.
There will also be an error if the gui is closed while the threads are still working, the gui loop doesn't like separate threads calling it.
Example of a solution to the problem
To get this working correctly we can use wx's CallAfter to make the gui changes happen in the gui thread and so only one thread is running and multiple clicks are queued up we can use concurrent futures ThreadPoolExecutor.
Example of a solution to the problem using decorators
Decorators of CallAfter and ThreadPoolExecutor can be used as shown below, by using these any blocking method just needs to be decorated by
If you have ever tried to use
time.sleep
or any code that takes some time to run within your gui code, you will find it becomes unresponsive like in the following example.You will notice that the call to change the label text to running doesn't seem work,the listbox does not update until after the sleep has finished, and the button locks in the down position.
Example of the problem
import time import wx class MainFrame(wx.Frame): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.panel = wx.Panel(self) self.panel_sizer = wx.BoxSizer(wx.VERTICAL) self.panel.SetSizer(self.panel_sizer) self.label = wx.StaticText(self.panel, label='not running') self.panel_sizer.Add(self.label, 0, wx.ALL|wx.CENTER, 5) self.listbox = wx.ListBox(self.panel) self.panel_sizer.Add(self.listbox, 1, wx.ALL|wx.CENTER, 5) self.button = wx.Button(self.panel, label='blocking task') self.button.Bind(wx.EVT_BUTTON, self.on_button) self.panel_sizer.Add(self.button, 0, wx.ALL|wx.CENTER, 5) self.Layout() self.Show() def on_button(self, event): print('Button clicked') self.blocking_code() def blocking_code(self): self.label.SetLabel('running') for number in range(5): self.listbox.Append(str(number)) print(number) time.sleep(1) self.label.SetLabel('not running') if __name__ == '__main__': app = wx.App(False) main_frame = MainFrame(None) app.MainLoop()
Output:Button clicked
0
1
2
3
4
Example of adding a thread but still getting a error
To get around this locking up problem we can use threads.
If we use a thread on its own the gui will become responsive but if the button is pressed a few times in a row, the number sequence will get jumbled up because each time the button is pressed a new thread is started.
There will also be an error if the gui is closed while the threads are still working, the gui loop doesn't like separate threads calling it.
Error:RuntimeError: main thread is not in main loop
import threading class MainFrame(wx.Frame): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) .... .... def on_button(self, event): print('Button clicked') thread = threading.Thread(target=self.blocking_code) thread.start() def blocking_code(self): self.label.SetLabel('running')
Output:Button clicked
0
Button clicked
0
1
1
2
Button clicked
0
2
3
1
Error:Exception in thread Thread-2:
Traceback (most recent call last):
File "C:\Users\Dave\AppData\Local\Programs\Python\Python37\lib\threading.py", line 917, in _bootstrap_inner
self.run()
File "C:\Users\Dave\AppData\Local\Programs\Python\Python37\lib\threading.py", line 865, in run
self._target(*self._args, **self._kwargs)
File "C:\Users\Dave\Documents\Eclipse Workspace\Test\forum\wx_blocking.py", line 37, in blocking_code
self.listbox.Append(str(number))
RuntimeError: wrapped C/C++ object of type ListBox has been deleted
Example of a solution to the problem
To get this working correctly we can use wx's CallAfter to make the gui changes happen in the gui thread and so only one thread is running and multiple clicks are queued up we can use concurrent futures ThreadPoolExecutor.
import wx from concurrent import futures import time thread_pool_executor = futures.ThreadPoolExecutor(max_workers=1) class MainFrame(wx.Frame): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.panel = wx.Panel(self) self.panel_sizer = wx.BoxSizer(wx.VERTICAL) self.panel.SetSizer(self.panel_sizer) self.label = wx.StaticText(self.panel, label='not running') self.panel_sizer.Add(self.label, 0, wx.ALL | wx.CENTER, 5) self.listbox = wx.ListBox(self.panel) self.panel_sizer.Add(self.listbox, 1, wx.ALL | wx.CENTER, 5) self.button = wx.Button(self.panel, label='blocking task') self.button.Bind(wx.EVT_BUTTON, self.on_button) self.panel_sizer.Add(self.button, 0, wx.ALL | wx.CENTER, 5) self.Layout() self.Show() def on_button(self, event): print('Button clicked') thread_pool_executor.submit(self.blocking_code) def set_label_text(self, text=''): self.label.SetLabel(text) def listbox_insert(self, item): self.listbox.Append(item) print(item) def blocking_code(self): wx.CallAfter(self.set_label_text, 'running') for number in range(5): wx.CallAfter(self.listbox_insert, str(number)) time.sleep(1) wx.CallAfter(self.set_label_text, 'not running') if __name__ == '__main__': app = wx.App(False) main_frame = MainFrame(None) app.MainLoop()
Output:Button clicked
0
Button clicked
Button clicked
1
2
3
4
0
1
2
3
4
0
1
2
3
4
Example of a solution to the problem using decorators
Decorators of CallAfter and ThreadPoolExecutor can be used as shown below, by using these any blocking method just needs to be decorated by
submit_to_pool_executor
and any methods called from a separate thread just need decorating with wx_call_after
.import wx from concurrent import futures import time import functools thread_pool_executor = futures.ThreadPoolExecutor(max_workers=1) def wx_call_after(target): @functools.wraps(target) def wrapper(self, *args, **kwargs): args = (self,) + args wx.CallAfter(target, *args, **kwargs) return wrapper def submit_to_pool_executor(executor): '''Decorates a method to be sumbited to the passed in executor''' def decorator(target): @functools.wraps(target) def wrapper(*args, **kwargs): result = executor.submit(target, *args, **kwargs) result.add_done_callback(executor_done_call_back) return result return wrapper return decorator def executor_done_call_back(future): exception = future.exception() if exception: raise exception class MainFrame(wx.Frame): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.panel = wx.Panel(self) self.panel_sizer = wx.BoxSizer(wx.VERTICAL) self.panel.SetSizer(self.panel_sizer) self.label = wx.StaticText(self.panel, label='not running') self.panel_sizer.Add(self.label, 0, wx.ALL | wx.CENTER, 5) self.listbox = wx.ListBox(self.panel) self.panel_sizer.Add(self.listbox, 1, wx.ALL | wx.CENTER, 5) self.button = wx.Button(self.panel, label='blocking task') self.button.Bind(wx.EVT_BUTTON, self.on_button) self.panel_sizer.Add(self.button, 0, wx.ALL | wx.CENTER, 5) self.Layout() self.Show() def on_button(self, event): print('Button clicked') self.blocking_code() @wx_call_after def set_label_text(self, text=''): self.label.SetLabel(text) @wx_call_after def listbox_insert(self, item): self.listbox.Append(item) print(item) @submit_to_pool_executor(thread_pool_executor) def blocking_code(self): self.set_label_text('running') for number in range(5): self.listbox_insert(str(number)) time.sleep(1) self.set_label_text('not running') if __name__ == '__main__': app = wx.App(False) main_frame = MainFrame(None) app.MainLoop()Edit: improved
submit_to_pool_executor
previously errors in the threaded code would happen silently, errors will now be raised by the call back executor_done_call_back