Search code examples
pythonwxpythonpython-multithreading

Thread Logging to TextCtrl


I am trying to print values from a thread into a TextCtrl window defined in a different class. The code posted is a stripped down version, it will increment a value in the background thread and then fail trying to print it to the TextCtrl window.

I have tried using pubsub, but couldn't find an example close enough to my code to "integrate".

Update

I have added my thread to its own class and now I am getting an error.

How do I use the post event to send data to the textCtrl box in the other class?

import wx 
import time
import threading

GlobalVar = False

#draws the GUI
class Mywin(wx.Frame): 
    def __init__(self, parent, title): 
    #Frame
       super(Mywin, self).__init__(parent, title = title,size = (500,300))  
       panel = wx.Panel(self) 
    #Constant stream toggle Button  
       self.tbtn =wx.ToggleButton(self, label="Constant Stream", pos=(20, 90))
       self.tbtn.Bind(wx.EVT_TOGGLEBUTTON, self.OnToggle)
    #Text window  
       self.logger = wx.TextCtrl(self, pos=(150,0), size=(400,100), style=wx.TE_MULTILINE | wx.TE_READONLY)
    #Draws and laysout the GUI
       panel.Layout()
       self.Centre() 
       self.Show() 
       self.Fit()

#=====================================================================
    def OnToggle(self,event):  #Toggle on, start background stream
        state = event.GetEventObject().GetValue()
        global GlobalVar

        if state == True:
            GlobalVar = True
            Stream.start()
            event.GetEventObject().SetLabel("Streaming")

        else:
            GlobalVar = False
            event.GetEventObject().SetLabel("Constant Stream")

class StreamThread(threading.Thread):
    def __init__(self, threadID, name):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name

    def run(self):
        global GlobalVar
        IncrementMe = 0
        while GlobalVar == True:
            wx.PostEvent(Mywin.logger.AppendText(str(IncrementMe)))
            print(IncrementMe)
            time.sleep(1)
            IncrementMe = IncrementMe + 1

Stream = StreamThread(1, "Stream_Thread")
Stream.daemon = True

app = wx.App() 
Mywin(None,  'Incrementer') 
app.MainLoop()
del app

Solution

  • Just to get this working and as you have already embraced a global variable, just make logger a global variable, rather than self.logger.
    Note self.logger.AppendText(IncrementMe) will have to become logger.AppendText(str(IncrementMe)) as appending an int will give you grief.
    Ideally, you should rewite this to have the thread as a Class, then you can use a wx.PostEvent to trigger actions in your MyWin by binding to the Event.
    See: https://wxpython.org/Phoenix/docs/html/events_overview.html https://www.blog.pythonlibrary.org/2010/05/22/wxpython-and-threads/

    Edit: Here is a pubsub script which, hopefully, will point you in the right direction.

    from threading import Thread
    import wx
    from wx.lib.pubsub import pub
    import time
    class WorkThread(Thread):
    
        def __init__(self):
            """Init Worker Thread Class."""
            Thread.__init__(self)
            self.stop_work_thread = 0
            self.start()  # start the thread
            self.val = 0
    
        def run(self):
            while True:
                if self.stop_work_thread == 1:
                    break
                time.sleep(1)
                self.val += 1
                wx.CallAfter(pub.sendMessage, "update", step=self.val)
            wx.CallAfter(pub.sendMessage, "finish")
            return
    
        def stop(self):
            self.stop_work_thread = 1
    
    class Progress(wx.Frame):
        def __init__(self, parent, title):
            super(Progress, self).__init__(parent, title = title,size = (500,300))
            self.panel = wx.Panel(self)
            self.start_btn = wx.Button(self.panel, label="Start", pos=(20,50))
            self.start_btn.Bind(wx.EVT_BUTTON, self.onStart)
            self.stop_btn =wx.ToggleButton(self.panel, label="Stop", pos=(20, 90))
            self.stop_btn.Bind(wx.EVT_TOGGLEBUTTON, self.onCancel)
            self.logger = wx.TextCtrl(self.panel, pos=(150,0), size=(200,200), style=wx.TE_MULTILINE | wx.TE_READONLY)
            self.stop_btn.Disable()
            self.Bind(wx.EVT_CLOSE, self.onExit)
    
        def onUpdate(self, step):
            step = str(step)+'\n'
            self.logger.AppendText(step)
    
        def onStart(self, event):
            self.logger.Clear()
            pub.subscribe(self.onUpdate, "update")
            pub.subscribe(self.onFinish, "finish")
            btn = event.GetEventObject()
            self.start_btn.Disable()
            self.work = WorkThread()
            Progress(self.panel,'Incrementer')
            self.stop_btn.Enable()
    
        def onCancel(self, event):
            """Cancel thread process"""
            try:
                self.work.stop()
                self.work.join()
            except:
                pass
    
        def onFinish(self):
            """thread process finished"""
            try:
                pub.unsubscribe(self.onUpdate, "update")
                pub.unsubscribe(self.onFinish, "finish")
            except:
                pass
            self.start_btn.Enable()
            self.stop_btn.Disable()
    
        def onExit(self, event):
            self.onCancel(None)
            self.onFinish()
            self.Destroy()
    
    app = wx.App()
    frame = Progress(None,'Incrementer')
    frame.Show(True)
    app.MainLoop()
    

    enter image description here