Search code examples
pythonmultithreadingwxpython

Python thread not listening to flag


I'm using a slightly edited example from here. The start button in the GUI starts a thread, the stop button stops it. I've added an x = x+1 and a print(x) to the loop, that works too. But if I try to bring x to the GUI, the stop button stops working. I don't understand why. Can you explain it to me?

The code is below. Running it like this will give you the result after stopping the thread. Uncommenting line 48 will give you the result every 1 second, but the stop button will stop working.

import time
from threading import *
import wx

# Button definitions
ID_START = wx.NewId()
ID_STOP = wx.NewId()

# Define notification event for thread completion
EVT_RESULT_ID = wx.NewId()

def EVT_RESULT(win, func):
    """Define Result Event."""
    win.Connect(-1, -1, EVT_RESULT_ID, func)

class ResultEvent(wx.PyEvent):
    """Simple event to carry arbitrary result data."""
    def __init__(self, data):
        """Init Result Event."""
        wx.PyEvent.__init__(self)
        self.SetEventType(EVT_RESULT_ID)
        self.data = data
        pass

# Thread class that executes processing
class WorkerThread(Thread):
    """Worker Thread Class."""
    def __init__(self, notify_window):
        """Init Worker Thread Class."""
        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):
        """Run Worker Thread."""
        # 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
        x=0
        for i in range(10):
            x = x + 1
            time.sleep(1)
            print(x)
            #wx.PostEvent(self._notify_window, ResultEvent(x))
            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)
                wx.PostEvent(self._notify_window, ResultEvent('ended with ' + str(x)))
                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)
        wx.PostEvent(self._notify_window, ResultEvent(10))

    def abort(self):
        """abort worker thread."""
        # 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(wx.Frame):
    """Class MainFrame."""
    def __init__(self, parent, id):
        """Create the MainFrame."""
        wx.Frame.__init__(self, parent, id, 'Thread Test')

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

        self.Bind(wx.EVT_BUTTON, self.OnStart, id=ID_START)
        self.Bind(wx.EVT_BUTTON, self.OnStop, id=ID_STOP)

        # 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):
        """Start Computation."""
        # 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):
        """Stop Computation."""
        # 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):
        """Show Result status."""
        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(wx.App):
    """Class Main App."""
    def OnInit(self):
        """Init Main App."""
        self.frame = MainFrame(None, -1)
        self.frame.Show(True)
        self.SetTopWindow(self.frame)
        return True

if __name__ == '__main__':
    app = MainApp(0)
    app.MainLoop()

Solution

  • You have found the classic example but it is now very dated. Either look for a modern example or pick the bones out of the following:

    import time
    import wx
    from threading import Thread
    
    import wx.lib.newevent
    progress_event, EVT_PROGRESS_EVENT = wx.lib.newevent.NewEvent()
    
    class ThreadFrame(wx.Frame):
    
        def __init__(self, title, parent=None):
            wx.Frame.__init__(self, parent=parent, title=title)
            panel = wx.Panel(self)
            self.btn = wx.Button(panel,label='Stop Long running process', size=(200,30), pos=(10,10))
            self.btn.Bind(wx.EVT_BUTTON, self.OnExit)
            self.progress = wx.Gauge(panel,size=(240,10), pos=(10,50), range=240)
    
            #Bind to the progress event issued by the thread
            self.Bind(EVT_PROGRESS_EVENT, self.OnProgress)
            #Bind to Exit on frame close
            self.Bind(wx.EVT_CLOSE, self.OnExit)
            self.Show()
    
            self.mythread = TestThread(self)
            #Enable the GUI to be responsive by briefly returning control to the main App
            while self.mythread.isAlive():
                time.sleep(0.1)
                wx.GetApp().Yield()
                continue
    
            try:
                self.OnExit(None)
            except:
                pass
    
        def OnProgress(self, event):
            self.progress.SetValue(event.count)
            #or for indeterminate progress
            #self.progress.Pulse()
    
        def OnExit(self, event):
            if self.mythread.isAlive():
                self.mythread.terminate() # Shutdown the thread
                self.mythread.join() # Wait for it to finish
            self.Destroy()
    
    class TestThread(Thread):
        def __init__(self,parent_target):
            Thread.__init__(self)
            self.parent = parent_target
            self.stopthread = False
            self.time = time.time()
            self.start()    # start the thread
    
        def run(self):
            # A loop that will run for 2 minutes then terminate
            while self.stopthread == False:
                curr_loop = int(time.time() - self.time)
                if curr_loop < 240:
                    time.sleep(0.1)
                    evt = progress_event(count=curr_loop)
                    #Send back current count for the progress bar
                    try:
                        wx.PostEvent(self.parent, evt)
                    except: # The parent frame has probably been destroyed
                        self.terminate()
                else:
                    self.terminate()
    
        def terminate(self):
            self.stopthread = True
    
    class MyPanel(wx.Panel):
    
        def __init__(self, parent):
            wx.Panel.__init__(self, parent)
            self.text_count = 0
            self.thread_count = 0
            self.parent=parent
            btn = wx.Button(self, wx.ID_ANY, label='Start Long running process', size=(200,30), pos=(10,10))
            btn.Bind(wx.EVT_BUTTON, self.Thread_Frame)
            btn2 = wx.Button(self, wx.ID_ANY, label='Is the GUI still active?', size=(200,30), pos=(10,50))
            btn2.Bind(wx.EVT_BUTTON, self.AddText)
            self.txt = wx.TextCtrl(self, wx.ID_ANY, style= wx.TE_MULTILINE, pos=(10,90),size=(400,100))
    
        def Thread_Frame(self, event):
            self.thread_count += 1
            frame = ThreadFrame(title='Threaded Task '+str(self.thread_count), parent=self.parent)
    
        def AddText(self,event):
            self.text_count += 1
            txt = "Gui is still active " + str(self.text_count)+"\n"
            self.txt.write(txt)
    
    class MainFrame(wx.Frame):
    
        def __init__(self):
            wx.Frame.__init__(self, None, title='Main Frame', size=(600,400))
            panel = MyPanel(self)
            self.Show()
    
    
    if __name__ == '__main__':
        app = wx.App(False)
        frame = MainFrame()
        app.MainLoop()
    

    enter image description here