Search code examples
pythonpython-3.xpyqt5qthreadpyqtgraph

Python - Change plot parameters in QThread


I'm looking for a solution to solve the following issue: My program starts with a plot of all data, later when I start a function a worker is plotting the same graph according to it times. So there are two lines, first a red one that shows how the plot will look like, later the plot that follows the first graph, done by a worker.

Unfortunately, the second plot is very thin. I've created a variable called "plotsize" in my example. This can change the first plot, but I have no idea how to change the characteristics of the second one within the threading with a worker. I'm using QThread.

Here my example codes, two data. Name of the GUI file is just GUI.py

#GUI.py
from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(739, 532)
        MainWindow.setStyleSheet("")
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.WidgetPlot = PlotWidget(self.centralwidget)
        self.WidgetPlot.setGeometry(QtCore.QRect(100, 40, 541, 341))
        self.WidgetPlot.setObjectName("WidgetPlot")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(330, 420, 93, 28))
        self.pushButton.setObjectName("pushButton")
        MainWindow.setCentralWidget(self.centralwidget)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Main Window"))
        self.pushButton.setText(_translate("MainWindow", "Start"))

from pyqtgraph import PlotWidget

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

And here the script:

#PROGRAM/SCRIPT
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication
import sys
import GUI
import datetime
import pyqtgraph
import time
plotsize = 2

# =============================================================================
# Threading for not freezing the GUI while running
# =============================================================================
class Worker(QtCore.QObject):
    progress = QtCore.pyqtSignal(int)
    finished = QtCore.pyqtSignal()
    widgetplot = QtCore.pyqtSignal(list, list)
    

    def __init__(self):
        super().__init__()


    def start(self):
        self._run()


    def _run(self):
        self._count = 0
        self.x = plot_time[:0]
        self.y = plot_value[:0]
        self.widgetplot.emit(self.x, self.y)
        self._start_time = datetime.datetime.now()
        while 0 <= self._count < 100:
            self._count_prev = self._count
            QtCore.QThread.usleep(10000)
            self._diff = datetime.datetime.now() - self._start_time
            
            self._count = int((self._diff.total_seconds() * 10))
            if(self._count != self._count_prev):
                print(self._count)
                self.x = plot_time[:self._count]
                self.y = plot_value[:self._count]
                self.widgetplot.emit(self.x, self.y)


class my_class(QtWidgets.QMainWindow, GUI.Ui_MainWindow):
    def __init__(self, parent=None):
        super(my_class, self).__init__(parent)
        self.setupUi(self)
        self.thread = QtCore.QThread(self)
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.start)
        self.worker.finished.connect(self.thread.quit)
        self.worker.widgetplot.connect(self.WidgetPlot.plot)
        self.pushButton.clicked.connect(self.my_function)

        self.WidgetPlot.setMouseEnabled(x=False, y=False)
        font=QtGui.QFont()
        font.setPixelSize(20)
        font.setBold(True)
        self.WidgetPlot.getAxis("bottom").setTickFont(font)
        self.WidgetPlot.getAxis("left").setTickFont(font)


    def my_function(self):
        global plot_time
        plot_time = []
        global plot_value
        plot_value = []
        for i in range(100):
            plot_time.append(i)
            plot_value.append(i)
        self.start()

        
    def preview_plot(self):
        self.WidgetPlot.clear()
        self.WidgetPlot.setXRange(0, 105, padding=0)
        self.WidgetPlot.setYRange(0, 105, padding=0)
        self.preview_line = self.WidgetPlot.plot(plot_time, plot_value, pen=pyqtgraph.mkPen('r', width=plotsize))

        
    def start(self):
        self.preview_plot()
        self.thread.start()    
    


def main():
    app = QApplication(sys.argv)
    form = my_class()
    form.show()
    app.exec_()
    
    
if __name__ == '__main__':

Many Thanks in advance! Andrew


Solution

  • Based on what I have inferred from your request (i.e. you want just to use two different size for overlapping plot lines) I've rewritten your code with a series of improvements (in my humble opinion).

    At the very core of the problem, what I've done is to create a separate pyqtgraph.PlotDataItem curve, plot it on top of your preview_line and handle a different size for it (completely randomic, please adjust the size as you desire). Thus, it's all about using

    self.second_line.setData(x, y)
    

    instead of recreating it every time with self.WidgetPlot.plot(...)

    All the details are into the comments.

    #PROGRAM/SCRIPT
    from PyQt5 import QtCore, QtGui, QtWidgets
    from PyQt5.QtWidgets import QApplication
    import sys
    import GUI
    import datetime
    import pyqtgraph
    import time
    plotsize = 20
    
    # =============================================================================
    # Threading for not freezing the GUI while running
    # =============================================================================
    class Worker(QtCore.QThread):
        progress = QtCore.pyqtSignal(int)
        finished = QtCore.pyqtSignal()
        widgetplot = QtCore.pyqtSignal(list, list)
        
    
        def __init__(self, plot_time, plot_value):
            QtCore.QThread.__init__(self, objectName='WorkerThread')
    
            # AS LONG AS YOU DON'T MODIFY plot_time and plot_value INTO THIS THREAD, they are thread-safe. 
            # If you intend to modify them in the future (into this thread), you need to implement a mutex/lock system to avoid races
            # Note that lists are mutable: you're storing the reference to the actual object that will always be edited into the MAIN THREAD
            self.plot_time = plot_time
            self.plot_value = plot_value
    
        def run(self):
            # This is naturally a LOCAL variable
            # self._count = 0
            _count = 0
    
            # --- This is useless, it plots ([], [])
            # self.widgetplot.emit(self.x, self.y) 
            # self.x = plot_time[:0]
            # self.y = plot_value[:0]
            # -----------------------------------
            _start_time = datetime.datetime.now()
            while 0 <= _count < 100:
                # Use local variable!
                _count_prev = _count
                QtCore.QThread.usleep(10000)
                _diff = datetime.datetime.now() - _start_time
                
                _count = int((_diff.total_seconds() * 10))
                if(_count != _count_prev):
                    print(_count)
                    x = self.plot_time[:_count]
                    y = self.plot_value[:_count]
    
                    # Since plot_time and plot_value are managed by main thread, you would just need to emit _count variable.
                    # But I'll stick with your code
                    self.widgetplot.emit(x, y)
    
    
    class my_class(QtWidgets.QMainWindow, GUI.Ui_MainWindow):
        def __init__(self, parent=None):
            super(my_class, self).__init__(parent)
            self.setupUi(self)
            
            # In your version, the range is constant irrespective of the data being updated. It can be moved here
            self.WidgetPlot.setXRange(0, 105, padding=0)
            self.WidgetPlot.setYRange(0, 105, padding=0)
    
            # Create and store ONE line, you will change just the underlying data, not the object itself. WAY MORE efficient
            # Different pen with different plot size. Is this what you're seeking?
            self.second_line = pyqtgraph.PlotDataItem(pen=pyqtgraph.mkPen('w', width=plotsize*2))
            
            
            # Here class variable initialization goes
            self.plot_time = []
            self.plot_value = []
    
            # You don't need `moveToThread`. You can just subclass QThread 
            self.worker_thread = Worker(self.plot_time, self.plot_value)
    
            # I changed the name just to highlight the fact it is just an update and not a plot rebuilt
            self.worker_thread.widgetplot.connect(self.update_second_line_plot)
    
            self.pushButton.clicked.connect(self.my_function)
    
            self.WidgetPlot.setMouseEnabled(x=False, y=False)
            font=QtGui.QFont()
            font.setPixelSize(20)
            font.setBold(True)
            self.WidgetPlot.getAxis("bottom").setTickFont(font)
            self.WidgetPlot.getAxis("left").setTickFont(font)
    
    
        def my_function(self):
            # Use class variable instead, see the __init__
    
            # ...Not efficient
            # for i in range(100):
            #    plot_time.append(i)
            #    plot_value.append(i)
    
            # Better:
            _l = list(range(100))
            self.plot_time.extend(_l)
            self.plot_value.extend(_l)
            # DON'T DO self.plot_time = list(range(100)) --> it will recreate a new object, but this one is shared with the worker thread!
    
            self.start()
            
        def update_second_line_plot(self, plot_time, plot_value):
            # Just update the UNDERLYING data of your curve
            self.second_line.setData(plot_time, plot_value)
    
    
        def start(self):
            # First plot preview_plot. done ONCE
            self.WidgetPlot.plot(self.plot_time, self.plot_value, pen=pyqtgraph.mkPen('r', width=plotsize))
    
            # Add NOW the new line to be drawn ON TOP of the preview one
            self.WidgetPlot.addItem(self.second_line)
        
            # It automatically will do the job. You don't need any plotfunction
            self.worker_thread.start()    
        
    
    
    def main():
        app = QApplication(sys.argv)
        form = my_class()
        form.show()
        app.exec_()
        
        
    if __name__ == '__main__':
        main()
    

    The result:

    enter image description here