sssbbbaaa
sssbbbaaa

Reputation: 244

Call method to the main thread 'from a sub thread'

I am making a data acquisition program that communicates with a measurement device. The status of the device needs to be checked periodically (e.g., every 0.1 sec) to see if acquisition is done. Furthermore, the program must have the 'abort' method because acquisition sometime takes longer than few minutes. Thus I need to use multi-threading.

I attached the flow-chart and the example code. But I have no idea how I call the main-thread to execute a method from the sub-thread.


python 3.7.2 wxpython 4.0.6


Flow Chart

import wx
import time
from threading import Thread

class TestFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="Test Frame")
        panel = wx.Panel(self)

        self.Btn1 = wx.Button(panel, label="Start Measurement")
        self.Btn1.Bind(wx.EVT_BUTTON, self.OnStart)
        self.Btn2 = wx.Button(panel, label="Abort Measurement")
        self.Btn2.Bind(wx.EVT_BUTTON, self.OnAbort)
        self.Btn2.Enable(False)

        self.DoneFlag = False
        self.SubThread = Thread(target=self.Check, daemon=True)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.Btn1, 0, wx.EXPAND)
        sizer.Add(self.Btn2, 0, wx.EXPAND)
        panel.SetSizer(sizer)

    def OnStart(self, event):
        # self.N is the number of data points
        self.N = 0
        # self.N_max is the number of data points that is going to be acquired
        self.N_max = int(input("How many data points do yo want? (greater than 1) : ")) 
        self.DoneFlag = False
        self.Btn1.Enable(False)
        self.Btn2.Enable(True)
        self.Start()

    def OnAbort(self, event):
        self.DoneFlag = True

    def Start(self):
        self.SubThread.start()

    def Done(self):
        if self.DoneFlag is True:
            self.Finish()
        elif self.DoneFlag is False:
            self.Start()

    def Finish(self):
        print("Measurement done (N = {})\n".format(self.N))
        self.Btn1.Enable(True)
        self.Btn2.Enable(False)

    def Check(self):
        # In the actual program, this method communicates with a data acquisition device to check its status
        # For example,
        # "RunningStatus" is True when the device is still running (acquisition has not been done yet),
        #                 is False when the device is in idle state (acquisition has done) 
        #
        #     [Structure of the actual program]
        #     while True:
        #         RunningStatus = GetStatusFromDevice()
        #         if RunningStatus is False or self.DoneFlag is True:
        #             break
        #         else:
        #             time.sleep(0.1)
    
        # In below code, it just waits 3 seconds then assumes the acqusition is done
        t = time.time()
        time.sleep(1)
        for i in range(3):
            if self.DoneFlag is True:
                break
            print("{} sec left".format(int(5-time.time()+t)))
            time.sleep(1)

        # Proceed to the next steps after the acquisition is done.
        if self.DoneFlag is False:
            self.N += 1
            print("Data acquired (N = {})\n".format(self.N))
            if self.N == self.N_max:
                self.DoneFlag = True
        self.Done() # This method should be excuted in the main thread
        
if __name__ == "__main__":
    app = wx.App()
    frame = TestFrame()
    frame.Show()
    app.MainLoop()

Upvotes: 0

Views: 512

Answers (1)

Rolf of Saxony
Rolf of Saxony

Reputation: 22443

When using a GUI it is not recommended to call GUI functions from another thread, see: https://docs.wxwidgets.org/trunk/overview_thread.html

One of your options, is to use events to keep track of what is going on.
One function creates and dispatches an event when something happens or to denote progress for example, whilst another function listens for and reacts to a specific event.
So, just like pubsub but native.
Here, I use one event to post information about progress and another for results but with different targets.
It certainly will not be an exact fit for your scenario but should give enough information to craft a solution of your own.

import time
import wx
from threading import Thread

import wx.lib.newevent
progress_event, EVT_PROGRESS_EVENT = wx.lib.newevent.NewEvent()
results_event, EVT_RESULTS_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.parent = parent
        self.btn = wx.Button(panel,label='Stop Measurements', 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=30)

        #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)

    def OnProgress(self, event):
        self.progress.SetValue(event.count)
        #or for indeterminate progress
        #self.progress.Pulse()
        if event.result != 0:
            evt = results_event(result=event.result)
            #Send back result to main frame
            try:
                wx.PostEvent(self.parent, evt)
            except:
                pass

    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.start()    # start the thread

    def run(self):
        curr_loop = 0
        while self.stopthread == False:
            curr_loop += 1
        # Send a result every 3 seconds for test purposes
            if curr_loop < 30:
                time.sleep(0.1)
                evt = progress_event(count=curr_loop,result=0)
                #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:
                curr_loop = 0
                evt = progress_event(count=curr_loop,result=time.time())
                #Send back current count for the progress bar
                try:
                    wx.PostEvent(self.parent, evt)
                except: # The parent frame has probably been destroyed
                    self.terminate()

    def terminate(self):
        evt = progress_event(count=0,result="Measurements Ended")
        try:
            wx.PostEvent(self.parent, evt)
        except:
            pass
        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 Measurements', 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))
        #Bind to the result event issued by the thread
        self.Bind(EVT_RESULTS_EVENT, self.OnResult)

    def Thread_Frame(self, event):
        self.thread_count += 1
        frame = ThreadFrame(title='Measurement Task '+str(self.thread_count), parent=self)

    def AddText(self,event):
        self.text_count += 1
        txt = "Gui is still active " + str(self.text_count)+"\n"
        self.txt.write(txt)

    def OnResult(self,event):
        txt = "Result received " + str(event.result)+"\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

Upvotes: 1

Related Questions