1

I am designing an application using PyQt5. There are a lot of plots and I use Matplotlib to manage them.

Currently, I'm trying to use Qthread so that the Main Loop does not freeze for too long.

I used This exemple: How to use a Qthread to update a Matplotlib figure with PyQt?.

But, as the autor mentioned it, in the first solution, the threads I create never stop.

My question is: How can I stop it when the replot function is done in the Plotter() object ?

I do not like the use of Terminate() in this code.

Also the fact that the QThread is finished will allow me to set a PushButton able again using a connection like self.thread.finished.connect(my_function).

Here are some simplified portions of my code:

Class figure to creates the figures

# Figures

class My_figure(FigureCanvas):

    send_fig = QtCore.pyqtSignal(Axes, name="send_fig")

    def __init__(self, fig_size, fig_move, parent=None):
        self.fig = Figure(figsize=fig_size)
        self.axes = self.fig.add_subplot(111)

        FigureCanvas.__init__(self, self.fig)
        self.setParent(parent)
        self.move(fig_move[0], fig_move[1])

    def update_plot(self, axes):
        self.axes = axes
        self.draw()

Functions in my main dialog

#  graphs creation
    def new_graph(self, name):

        setattr(self, name + '_my_fig', [])

        # créer les graphs, axes, canvas, etc.
        graph_cat = ['bar_', 'pie_','line_']
        graph_element = ['_figure', '_canvas', '_axes', '_plotter', '_thread']
        fig_dim = [[0, (6.8, 4.3), (6, 3.5)],[0, 0, 30]]
        compt = 0
        for g_c in graph_cat:

            getattr(self, name + '_my_fig').append([])

            setattr(self, g_c + name + '_my_fig', My_figure((3,3),[0,0], parent = getattr(self, g_c + name)))
            getattr(self, name + '_my_fig')[-1].append(getattr(self, g_c + name + '_my_fig'))
            # Couple thread / worker
            setattr(self, g_c + name + graph_element[3], None)
            setattr(self, g_c + name + graph_element[4], None)
            getattr(self, name + '_my_fig')[-1].append(getattr(self, g_c + name + graph_element[3]))
            getattr(self, name + '_my_fig')[-1].append(getattr(self, g_c + name + graph_element[4]))

            compt += 1
# thread and worker creation + start
        for my_fig in self.tot_my_fig:
            print(my_fig)
            # if there is already a thread running, kill it first
            if my_fig[1] != None and my_fig[1].isRunning():
                print('termiante')
                my_fig [1].terminate()

            my_fig[1] = QtCore.QThread()
            my_fig[2] = Plotter()

            self.send_fig.connect(my_fig[2].replot)
            my_fig[2].return_fig.connect(my_fig[0].update_plot)
            my_fig[1].finished.connect(self.ppp)
            #move to thread and start
            my_fig[2].moveToThread(my_fig[1])
            my_fig[1].start()
            # start the plotting
            self.send_fig.emit(my_fig[0].axes)

Workers's class

# Worker

class Plotter(QtCore.QObject):

    return_fig = QtCore.pyqtSignal(Axes)

    @QtCore.pyqtSlot(Axes)
    def replot(self, axes):  # A slot takes no params
        axes.clear()
        # do some random task
        data = np.random.rand(1000,1000)
        axes.plot(data.mean(axis=1))
        self.return_fig.emit(axes)

Edit:

I found the answer, thank to https://stackoverflow.com/a/6789205/12016306

I just needed to send a signal when the worker is done and connect this signal to the quit() method of the thread. Like this:

class Worker(QObject):

    finished = pyqtSignal()

    def do_the_work(self):
        # do the long task and even the plotting
        self.finished.emit()

class MainWindow(QMainWindow):

    def __init__(self):
        #Normal init goes here

        #construction of the thread and worker
        self.worker = Worker()
        self.thread = Qthread()
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.do_the_work)
        self.worker.finished.connect(self.thread.quit)
        self.thread.finished.connect(self.print_when_finished)
        self.thread.start() # You can start it whenever you want. Put it in a function to start the worker only after a certain action.
Francky380
  • 233
  • 1
  • 12
  • I would be very surprised if you were able to run matplotlib outside the main thread. Note how the linked solution does not let matplotlib do anything outside the main thread. – ImportanceOfBeingErnest Dec 09 '19 at 23:17
  • @ImportanceOfBeingErnest You are right, I can't. I edited my question a bit so as you can see, everything that is related to matplotlib happens inside the main class. I based my model on your previous answer on the post I quote. Your second solution does not really fit with my application since it uses only one thread and one worker and I need one thread per figure and I have about 13 figures so.... – Francky380 Dec 09 '19 at 23:30
  • I think it should work with many threads and workers as well. – ImportanceOfBeingErnest Dec 09 '19 at 23:39
  • Oh okay, i'll try it and keep you inform. – Francky380 Dec 10 '19 at 15:41
  • It seems that the thread never ends too :/ – Francky380 Dec 10 '19 at 18:56

1 Answers1

1

What I do is create a plot thread and pass the figure and axis I want to plot on to the thread. When you plot again before one finishes, both threads can run at the same time and they both finish but the latest plot is the one that shows.

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
import random
import time


from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.pyplot as plt


class MplFigure(object):
    def __init__(self,parent):
        self.figure=plt.figure(facecolor=(.195,.195,.195))
        self.canvas=FigureCanvas(self.figure)


class plotThread(QThread):
    finished=pyqtSignal()
    def __init__(self,main_figure,ax1,x,y=[]):
        super(plotThread, self).__init__()
        self.main_figure=main_figure
        self.ax1=ax1
        self.x=x
        self.y=y

    def run(self):
        self.ax1.clear()

        line,=self.ax1.plot(self.x)
        #line.set_ydata(self.y)
        self.ax1.draw_artist(line)
        self.main_figure.canvas.draw()
        time.sleep(2)
        self.finished.emit()

WINDOW_SIZE=900,600

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.initUI()

    def initUI(self):
        self.setGeometry(300,300,*WINDOW_SIZE)
        layout=QVBoxLayout()
        view=QGraphicsView()
        view.setLayout(layout)
        self.main_figure=MplFigure(self)
        layout.addWidget(self.main_figure.canvas)
        self.setCentralWidget(view)
        self.ax1=self.main_figure.figure.add_subplot(1,1,1)
        self.btn=QPushButton('Plot Random')
        layout.addWidget(self.btn)
        self.btn.clicked.connect(self.plot_threaded)
        self.show()

    def plot_threaded(self):
        #self.btn.setEnabled(False)
        rand=random.sample(range(1,20),10)
        self.worker=plotThread(self.main_figure,self.ax1,rand)
        self.worker.finished.connect(self.enable_button)
        self.worker.start()

    def enable_button(self):
        #self.btn.setEnabled(True)
        print('Thread finished')


if __name__=='__main__':
    app=QApplication([])
    window=MainWindow()
    app.exec_()
    sys.exit()
SixenseMan
  • 145
  • 2
  • 4
  • Your solution seems to work, even though I am trying to use a worker/thread model instead of reimplementing the run() function of the thread. Thank for your help ! – Francky380 Jan 07 '20 at 18:59
  • I implemented a worker.thread model, thank to you. I putted the draw() function in the worker. Unfortunately, it does not really solve my problem. My app still freeze for a long time while updating the plots. Still, I know how t terminate properly a thread now. – Francky380 Jan 07 '20 at 21:58