Search code examples
pythonmultithreadinguser-interfacethread-safetywxpython

wxpython CallAfter not changing GUI


I needed a start and stop button for reading data continuously with a while loop, so I used the first example in here https://wiki.wxpython.org/LongRunningTasks. Then I wanted to change a graphic in continuous as well but I couldn't do it so to simulate that I tried to just change a TextCtrl. So I used wx.CallAfter to write on the TextCtrl, and I see that 'internaly' it changes but it doesn´t update the frame. I also tried using the Update() and Refresh() but that didn't work either. So I have no idea what to do and I've search all the places I could and don't know what to do anymore, so any help is appreciated!

import nidaqmx
import wx
import wxmplot
import time
from threading import Thread
from numpy import mean, std
from nidaqmx.constants import AcquisitionType, DataTransferActiveTransferMode, TerminalConfiguration


ID_START = wx.NewId()
ID_STOP = wx.NewId()
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

myEVT = wx.NewEventType()
EVT = wx.PyEventBinder(myEVT, 1)

class MyEvent(wx.PyCommandEvent):
    def __init__(self, evtType, id):
        wx.PyCommandEvent.__init__(self, evtType, id)
        myVal = None

    def SetMyVal(self, val):
        self.myVal = val

    def GetMyVal(self):
        return self.myVal

# janela para ler canais
class NewTaskWindow(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(1000,500))
        self.parent = parent
        self.title = title
        self.task_panel = wx.Panel(self, wx.ID_ANY, size = (1000,500))

        self.canais = ['Canal', 'Dev1/ai0','Dev1/ai1','Dev1/ai2','Dev1/ai3','Dev1/ai4','Dev1/ai5',
                       'Dev1/ai6','Dev1/ai7','Dev1/ai8','Dev1/ai9','Dev1/ai10','Dev1/ai11',
                       'Dev1/ai12','Dev1/ai13','Dev1/ai14','Dev1/ai15']

        self.cfgs = [TerminalConfiguration.DEFAULT,
                     TerminalConfiguration.DEFAULT,
                     TerminalConfiguration.DIFFERENTIAL,
                     TerminalConfiguration.NRSE,
                     TerminalConfiguration.PSEUDODIFFERENTIAL,
                     TerminalConfiguration.RSE]

        self.configs = ['Modo', 'DEFAULT', 'DIFFERENTIAL', 'NRSE', 'PSEUDODIFFERENTIAL', 'RSE']
        self.config1 = TerminalConfiguration.DEFAULT
        self.config2 = TerminalConfiguration.DEFAULT
        self.config3 = TerminalConfiguration.DEFAULT
        self.config4 = TerminalConfiguration.DEFAULT
        self.chan1 = 'canal1'
        self.chan2 = 'canal2'
        self.chan3 = 'canal3'
        self.chan4 = 'canal4'

        wx.Button(self.task_panel, ID_START, 'Start', pos=(10,10))
        wx.Button(self.task_panel, ID_STOP, 'Stop', pos=(10,90))
        self.status = wx.StaticText(self.task_panel, -1, '', pos=(800,0))

        self.lista_canais_1 = wx.ComboBox(self.task_panel, id=1, pos=(106,10), 
                                     size=(100,80), choices=self.canais, style=wx.CB_READONLY)
        self.lista_canais_1.SetSelection(0)

        self.lista_canais_2 = wx.ComboBox(self.task_panel, id=2, pos=(106,40), 
                                     size=(100,80), choices=self.canais, style=wx.CB_READONLY)
        self.lista_canais_2.SetSelection(0)

        self.lista_canais_3 = wx.ComboBox(self.task_panel, id=3, pos=(106,70), 
                                     size=(100,80), choices=self.canais, style=wx.CB_READONLY)
        self.lista_canais_3.SetSelection(0)

        self.lista_canais_4 = wx.ComboBox(self.task_panel, id=4, pos=(106,100), 
                                     size=(100,80), choices=self.canais, style=wx.CB_READONLY)
        self.lista_canais_4.SetSelection(0)

        self.lista_configs_1 = wx.ComboBox(self.task_panel, id=5, pos=(216,10), 
                                     size=(100,80), choices=self.configs, style= wx.CB_READONLY)
        self.lista_configs_1.SetSelection(0)

        self.lista_configs_2 = wx.ComboBox(self.task_panel, id=6, pos=(216,40), 
                                     size=(100,80), choices=self.configs, style= wx.CB_READONLY)
        self.lista_configs_2.SetSelection(0)

        self.lista_configs_3 = wx.ComboBox(self.task_panel, id=7, pos=(216,70), 
                                     size=(100,80), choices=self.configs, style= wx.CB_READONLY)
        self.lista_configs_3.SetSelection(0)

        self.lista_configs_4 = wx.ComboBox(self.task_panel, id=8, pos=(216,100), 
                                     size=(100,80), choices=self.configs, style= wx.CB_READONLY)
        self.lista_configs_4.SetSelection(0)

        self.check1 = wx.CheckBox(self.task_panel, wx.ID_ANY, "ON", pos=(330,10), size=(40,20))
        self.check2 = wx.CheckBox(self.task_panel, wx.ID_ANY, "ON", pos=(330,40), size=(40,20))
        self.check3 = wx.CheckBox(self.task_panel, wx.ID_ANY, "ON", pos=(330,70), size=(40,20))
        self.check4 = wx.CheckBox(self.task_panel, wx.ID_ANY, "ON", pos=(330,100), size=(40,20))

        self.input_pontos = wx.TextCtrl(self.task_panel , wx.ID_ANY, style = wx.TE_PROCESS_ENTER,
                                        value = "0", size =(90,20), pos = (10,70))

        self.media1_text = wx.TextCtrl(self.task_panel , wx.ID_ANY, style = wx.TE_READONLY,
                                        size =(90,20), pos = (420,12)) #<<<<-----------------------
        self.media2_text = wx.TextCtrl(self.task_panel , wx.ID_ANY, style = wx.TE_READONLY,
                                        size =(90,20), pos = (420,42))
        self.media3_text = wx.TextCtrl(self.task_panel , wx.ID_ANY, style = wx.TE_READONLY,
                                        size =(90,20), pos = (420,72))
        self.media4_text = wx.TextCtrl(self.task_panel , wx.ID_ANY, style = wx.TE_READONLY,
                                        size =(90,20), pos = (420,102))
        self.desvio1_text = wx.TextCtrl(self.task_panel , wx.ID_ANY, style = wx.TE_READONLY,
                                        size =(90,20), pos = (560,12))
        self.desvio2_text = wx.TextCtrl(self.task_panel , wx.ID_ANY, style = wx.TE_READONLY,
                                        size =(90,20), pos = (560,42))
        self.desvio3_text = wx.TextCtrl(self.task_panel , wx.ID_ANY, style = wx.TE_READONLY,
                                        size =(90,20), pos = (560,72))
        self.desvio4_text = wx.TextCtrl(self.task_panel , wx.ID_ANY, style = wx.TE_READONLY,
                                        size =(90,20), pos = (560,102))

        wx.StaticText(self.task_panel, wx.ID_ANY, "Nº Pontos:", (10,50))
        wx.StaticText(self.task_panel, wx.ID_ANY, "Média:", (380,12))
        wx.StaticText(self.task_panel, wx.ID_ANY, "Média:", (380,42))
        wx.StaticText(self.task_panel, wx.ID_ANY, "Média:", (380,72))
        wx.StaticText(self.task_panel, wx.ID_ANY, "Média:", (380,102))
        wx.StaticText(self.task_panel, wx.ID_ANY, "Desvio: ", (520,12))
        wx.StaticText(self.task_panel, wx.ID_ANY, "Desvio: ", (520,42))
        wx.StaticText(self.task_panel, wx.ID_ANY, "Desvio: ", (520,72))
        wx.StaticText(self.task_panel, wx.ID_ANY, "Desvio: ", (520,102))

        self.Bind(wx.EVT_COMBOBOX, self.SelectChannel, self.lista_canais_1)
        self.Bind(wx.EVT_COMBOBOX, self.SelectChannel, self.lista_canais_2)
        self.Bind(wx.EVT_COMBOBOX, self.SelectChannel, self.lista_canais_3)
        self.Bind(wx.EVT_COMBOBOX, self.SelectChannel, self.lista_canais_4)
        self.Bind(wx.EVT_COMBOBOX, self.SelectConfig, self.lista_configs_1)
        self.Bind(wx.EVT_COMBOBOX, self.SelectConfig, self.lista_configs_2)
        self.Bind(wx.EVT_COMBOBOX, self.SelectConfig, self.lista_configs_3)
        self.Bind(wx.EVT_COMBOBOX, self.SelectConfig, self.lista_configs_4)
        self.Bind(wx.EVT_BUTTON, self.OnStart, id=ID_START)
        self.Bind(wx.EVT_BUTTON, self.OnStop, id=ID_STOP)

        EVT_RESULT(self,self.OnResult)
        self.worker = None
        self.Bind(EVT, self.UpdateMedia1)


    def OnStart(self, event):                                #<<<<<<<<<<<--------------
        """Start Computation."""
        # Trigger the worker thread unless it's already busy
        global pontos, checks, chans, configz
        pontos=int(self.input_pontos.GetValue())
        checks = [self.check1.GetValue(), self.check2.GetValue(),
                  self.check3.GetValue(), self.check4.GetValue()]
        chans = [self.chan1, self.chan2, self.chan3, self.chan4]
        configz = [self.config1, self.config2, self.config3, self.config4]
        #self.media1_text.write('adsdsad')
        if len(chans) == len(set(chans)):
            self.graph_panel = wxmplot.plotpanel.PlotPanel(self.task_panel, pos=(0,150),
                                                           size=(500,350))
            self.graph_panel.plot([0],[0])
            if not self.worker:
                self.status.SetLabel('Starting computation')
                self.worker = WorkerThread(self, self.parent, self.title, 1)
        else:
            self.ErroCanais()

    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

    def UpdateMedia1(self, x):                # <<<<<<<----------------------
        self.media1_text.write(x)
        print(self.media1_text.GetValue()) 
        print(wx.IsMainThread())

    def ErroCanais(self):
        chan_erro_frame = wx.Frame(self, wx.ID_ANY, "ERRO", size=(200,100))
        chan_erro_panel = wx.Panel(chan_erro_frame, wx.ID_ANY, size=(200,100))
        wx.StaticText(chan_erro_panel, wx.ID_ANY, "CANAIS IGUAIS", (30,20))
        chan_erro_frame.Show()

    def SelectChannel(self, event):
        if event.GetId() == 1:
            if event.GetString() == 'Canal':
                self.chan1 = 'canal1'
            else:
                self.chan1 = event.GetString()
        elif event.GetId() == 2:
            if event.GetString() == 'Canal':
                self.chan2 = 'canal2'
            else:
                self.chan2 = event.GetString()
        elif event.GetId() == 3:
            if event.GetString() == 'Canal':
                self.chan3 = 'canal3'
            else:
                self.chan3 = event.GetString()
        elif event.GetId() == 4:
            if event.GetString() == 'Canal':
                self.chan4 = 'canal4'
            else:
                self.chan4 = event.GetString()

    def SelectConfig(self, event):
        if event.GetId() == 5:
            self.config1 = self.cfgs[event.GetSelection()]
        elif event.GetId() == 6:
            self.config2 = self.cfgs[event.GetSelection()]
        elif event.GetId() == 7:
            self.config3 = self.cfgs[event.GetSelection()]
        elif event.GetId() == 8:
            self.config4 = self.cfgs[event.GetSelection()]

class WorkerThread(Thread,NewTaskWindow):
    """Worker Thread Class."""
    def __init__(self, notify_window, parent, title, value):
        """Init Worker Thread Class."""
        Thread.__init__(self)
        NewTaskWindow.__init__(self, parent, title)
        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
        global pontos, checks, chans, configz
        #chann1, chann2, chann3, chann4 = "","","",""
        #chanz = [chann1, chann2, chann3, chann4]
        #task = nidaqmx.Task()
        #for i in range(4):
            #if checks[i]==True:
                #pass
#                task.ai_channels.add_ai_voltage_chan(chans[i],
#                                                     terminal_config=configz[i])
#                task.timing.cfg_samp_clk_timing(rate=10123, 
#                                                sample_mode=AcquisitionType.CONTINUOUS, 
#                                                samps_per_chan=10000)
#                chanz[i] = nidaqmx._task_modules.channels.ai_channel.AIChannel(task._handle,
#                                                                               chans[i])
#                chanz[i].ai_data_xfer_mech = DataTransferActiveTransferMode.INTERRUPT
        #data = []
        pontos = 10
        for i in range(pontos):
            #p = []
            #r = task.read(1)
            #p.append(r)
            #data.append(mean(p))
            #event = MyEvent(myEVT, self.GetId())
            #event.SetMyVal(i)
            #self.GetEventHandler().ProcessEvent(event)
            wx.CallAfter(self.UpdateMedia1, str(i))            #<<<<<<------------------
            time.sleep(1)
            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(None))
                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



class MainWindow(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(500,500))
        self.CreateStatusBar() # A StatusBar in the bottom of the window

        self.panel = wx.Panel(self, wx.ID_ANY)
        new_task_button = wx.Button(self.panel, wx.ID_ANY, 'New Task', (10, 10))

        # Setting up the menu.
        filemenu= wx.Menu()
        # wx.ID_ABOUT and wx.ID_EXIT are standard ids provided by wxWidgets.
        menuAbout = filemenu.Append(wx.ID_ABOUT, "&About"," Information about this program")
        menuExit = filemenu.Append(wx.ID_EXIT,"E&xit"," Terminate the program")
        # Creating the menubar.
        menuBar = wx.MenuBar()
        menuBar.Append(filemenu,"&File") # Adding the "filemenu" to the MenuBar
        self.SetMenuBar(menuBar)  # Adding the MenuBar to the Frame content.

        self.v1_check = wx.CheckBox(self.panel, id=20, label="Valvula 1", pos=(20,50), size=(70,20))
        self.v2_check = wx.CheckBox(self.panel, id=21, label="Valvula 2", pos=(20,70), size=(70,20))
        self.v3_check = wx.CheckBox(self.panel, id=22, label="Valvula 3", pos=(20,90), size=(70,20))
        self.v4_check = wx.CheckBox(self.panel, id=23, label="Valvula 4", pos=(20,110), size=(70,20))
        self.v5_check = wx.CheckBox(self.panel, id=24, label="Porta 5", pos=(20,130), size=(70,20))
        self.v6_check = wx.CheckBox(self.panel, id=25, label="Porta 6", pos=(20,150), size=(70,20))
        self.v7_check = wx.CheckBox(self.panel, id=26, label="Porta 7", pos=(20,170), size=(70,20))
        self.v8_check = wx.CheckBox(self.panel, id=27, label="Porta 8", pos=(20,190), size=(70,20))
        self.v9_check = wx.CheckBox(self.panel, id=28, label="Porta 9", pos=(20,210), size=(70,20))
        self.v10_check = wx.CheckBox(self.panel, id=29, label="Porta 10", pos=(20,230), size=(70,20))

        # Set events.
        self.Bind(wx.EVT_BUTTON, self.OnNewTask, new_task_button)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v1_check)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v2_check)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v3_check)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v4_check)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v5_check)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v6_check)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v7_check)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v8_check)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v9_check)
        self.Bind(wx.EVT_CHECKBOX, self.AbrirFecharValvula, self.v10_check)
        self.Bind(wx.EVT_MENU, self.OnAbout, menuAbout)
        self.Bind(wx.EVT_MENU, self.OnExit, menuExit)

        self.Show(True)

    def AbrirFecharValvula(self, event):
        vchecksid=[20,21,22,23,24,25,26,27,28,29]
        vchecks = [self.v1_check,self.v2_check,self.v3_check,self.v4_check,self.v5_check,
                   self.v6_check,self.v7_check,self.v8_check,self.v9_check,self.v10_check]
        #num = str(vchecksid.index(event.GetId()))
        #porta = 'Dev1/port'+num+'/line1'
        for check in vchecks:
            if check.GetId()==event.GetId():
                #with nidaqmx.Task() as task:
                    #task.do_channels.add_do_chan(porta)
                    #task.write(check.GetValue())
                print(check.GetValue())

    def OnNewTask(self, event):
        newin = NewTaskWindow(self.panel, 'Task')
        newin.Show()

    def OnAbout(self,e):
        # A message dialog box with an OK button. wx.OK is a standard ID in wxWidgets.
        dlg = wx.MessageDialog( self, "A user interface with nidaqmx",
                               "About NIDAQMX GUI", wx.OK)
        dlg.ShowModal() # Show it
        dlg.Destroy() # finally destroy it when finished.

    def OnExit(self,e):
        self.Close(True)  # Close the frame.


# self.graph_panel.plot_many([(range(5),range(5)),(range(5),[0,0,0,0,0])])#[(x1,y1),(x2,y2)]    

if __name__=='__main__':
    app = wx.App(False)
    frame = MainWindow(None, "NIDAQMX GUI")
    frame.Centre()
    app.MainLoop()
    del app

I commented with <<<<<----- the parts of the code at cause. And you can run it to see for your selves - click New Task and then Start. So the CallAfter is calling to UpdateMedia1 method and it shows that it is in the main thread and that it is changing the value of the media1 textctrl but its not changing in the window and I don't understand why. I've also tried creating my own event and use PostEvent but that didn't work either. I know this is a long question so thanks for your time reading!!


Solution

  • Your WorkerThread class is derived from NewTaskWindow so when you create WorkerThread you also create a new invisible NewTaskWindow

    In

    wx.CallAfter(self.UpdateMedia1, str(i))

    the self refers to the invisible second frame and therefore its (invisible) text control is updated.

    Solution

    Don't inherit NewTaskWindow in WorkerThread and call the UpdateMedia1 of the visible frame object instead (you already gave it as parameter when creating WorkerThread).