Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Threaded plotting with pyqtgraph



  • Dear all,

    I try to plot data from a thread and I have issues. I use pyqtgraph as plotter.

    • I use a worker (AcquisitionWorker in the code below) object run by a thread, through moveToThread process. This worker generates random data in order to be plotted. The data are stored in the main window, data generation process is protected by a QMutex.
    • I also have a timer in the main loop, for test.

    Both time-sharing techniques work well in my code (checked by some print).

    When I move the plot code in the timer of the main event loop, everything works. However, if I move this code to the worker thread, just close to the data generation code, the plot does not refresh well, and I get :

    QObject::startTimer: Timers cannot be started from another thread
    QObject::startTimer: Timers cannot be started from another thread
    QObject::startTimer: Timers cannot be started from another thread
    QObject::startTimer: Timers cannot be started from another thread
    QObject::startTimer: Timers cannot be started from another thread
    QObject::startTimer: Timers cannot be started from another thread
    

    Any Idea ?

    Working Code

    here in timer mode, please read comments of "runProc" method for Thread/Worker mode. Click on the "start" button to launch the worker thread.

    #!/opt/miniconda3/bin/python3.7
    
    from PyQt5.QtCore import QObject, QThread, pyqtSignal,QTimer,QMutex
    from PyQt5 import QtGui,QtWidgets
    import pyqtgraph as pg
    import random
    import numpy as np
    from datetime import datetime
    import time
    import sys
    
    class GenericAcquisitionWorker(QObject):
        progress = pyqtSignal()
    
        def __init__(self,*args,**kwargs):
            super().__init__(*args,**kwargs)
            self.mutex = QMutex()
            self.pleaseRunFlag = None
    
        def pleaseRun(self):
            if self.pleaseRunFlag == False:
                if self.mutex.tryLock():
                    self.mutex.unlock()
                else:
                    self.mutex.unlock()
                self.pleaseRunFlag = True
    
        def pleasePause(self):
            self.pleaseRunFlag = False
    
        def pleaseRunStatus(self):
            return self.pleaseRunFlag
    
        def run(self):
            while True:
                if self.mutex.tryLock():
                    if self.pleaseRunFlag == True :
                        self.runningProcess()
                        self.progress.emit()
                        self.mutex.unlock()
                else:
                    self.idlingProcess()
    
        def runningProcess(self): pass
    
        def idlingProcess(self):pass
    
    
    class AcquisitionWorker(GenericAcquisitionWorker):
        def __init__(self,parent,*args,**kwargs):
            super().__init__(*args,**kwargs)
            self.parent = parent
    
        def runningProcess(self):
            self.parent.runProc()
    
        def idlingProcess(self):
            time.sleep(0.1)
    
    
    def showThread(name):
        print("%s : %s Thread 0x%x"%(datetime.now().strftime("%H:%M:%S"),name,int(QThread.currentThreadId())))
    
    class Window(QtWidgets.QMainWindow):
        def __init__(self,*args,**kwargs):
            super().__init__(*args,**kwargs)
            self.buildGUILayout()
            self.buildPausedAcquisitionWorker()
            self.buildTimer()
            self.x = np.linspace(0,10,1000)
            self.y = np.linspace(0,10,1000)
            self.thePen = {'color':'r','width': 2}
    
        def workerProgress(self):
            self.GuiMainLayout['pbar'].setValue(random.randint(0, 100))
            showThread('Report')
            print("\n")
    
        def mainAppProgress(self):
            showThread('Timer')
            # DEACTIVATE CODE BELOW FOR TIMER-BASED PLOTTING
            if(self.acquisitionWorker.pleaseRunStatus()):
                self.GuiMainLayout['timePlot'].plot(x=self.x,y=self.y,  clear=True, _callSync='off')#,pen=self.thePen)
    
        def runProc(self):
            self.x = np.linspace(0,10,10000)
            self.y = np.random.rand(*self.x.shape)
            time.sleep(0.1)
            showThread('Acquisition Worker')
            # DEACTIVATE CODE BELOW FOR WORKER-THREAD-BASED PLOTTING
            #if(self.acquisitionWorker.pleaseRunStatus()):
            #   self.GuiMainLayout['timePlot'].plot(x=self.x,y=self.y,  clear=True, _callSync='off')#,pen=self.thePen)
    
    
    
        def buildPausedAcquisitionWorker(self):
            self.thread = QThread()
            self.acquisitionWorker = AcquisitionWorker(self)
            self.acquisitionWorker.moveToThread(self.thread)
            self.thread.started.connect(self.acquisitionWorker.run)
            self.acquisitionWorker.progress.connect(self.workerProgress)
            self.acquisitionWorker.pleasePause()
            self.thread.start()
    
    
    
    
    
        def buildTimer(self):
            self.timer = QTimer()
            self.timer.setInterval(100)
            self.timer.timeout.connect(self.mainAppProgress)
            self.timer.start()
    
        def buildGUILayout(self):
            self.GuiMainLayout={}
            # all individual widgets
            self.GuiMainLayout['label'] = QtWidgets.QLabel('test')
            self.GuiMainLayout['start'] = QtWidgets.QPushButton("Start")
            self.GuiMainLayout['start'].clicked.connect(self.signal_start)
            self.GuiMainLayout['stop'] = QtWidgets.QPushButton("Stop")
            self.GuiMainLayout['stop'].clicked.connect(self.signal_stop)
            self.GuiMainLayout['pbar'] = QtWidgets.QProgressBar()
            self.GuiMainLayout['pbar'].setGeometry(30, 40, 200, 25)
    
            pg.setConfigOption('antialias',True)
            #pg.setConfigOption('background', 'w')
            #pg.setConfigOption('foreground', 'k')
            pg.setConfigOption('useOpenGL',True)
            #pg.setConfigOption('leftButtonPan',False)
    
    
            self.GuiMainLayout['timePlot'] = pg.PlotWidget()
    
            # the layout itself
            self.GuiMainLayout['mainL'] = QtWidgets.QVBoxLayout()
            self.GuiMainLayout['mainL'].addWidget(self.GuiMainLayout['label'])
            self.GuiMainLayout['mainL'].addWidget(self.GuiMainLayout['timePlot'])
            self.GuiMainLayout['mainL'].addWidget(self.GuiMainLayout['start'])
            self.GuiMainLayout['mainL'].addWidget(self.GuiMainLayout['stop'])
            self.GuiMainLayout['mainL'].addWidget(self.GuiMainLayout['pbar'])
            self.GuiMainLayout['main'] = QtWidgets.QWidget()
            self.GuiMainLayout['main'].setLayout(self.GuiMainLayout['mainL'])
            # attribute the layout to this window
            self.setCentralWidget(self.GuiMainLayout['main'])
    
        def signal_start(self):
            self.acquisitionWorker.pleaseRun()
    
        def signal_stop(self):
            self.acquisitionWorker.pleasePause()
    
    app = QtWidgets.QApplication(sys.argv)
    win = Window()
    win.show()
    sys.exit(app.exec())
    

  • Lifetime Qt Champion

    Hi,

    Do not access GUI element from different threads. It's only allowed in the main thread aka GUI thread.



  • So there is no solution ? may it be possible to make "offscreen drawing" of a pyqtgraph in a thread, then blitting it in the event loop ? Is there information about offscreen drawing with Qt ? (I did not find).

    Thanks a lot,
    Mike


  • Lifetime Qt Champion

    You can check the dvg-pyqtgraph-threadsafe package. (I haven't used it though).


Log in to reply