how to prevent the arrow of a Qscrollbar from calling twice the same function?

179 Views Asked by At

I've made a pyqt window to display signal in a matplotlib figure with a QScrollBar to see different part of the signal. My issue is when I plot 100 signal, the arrow of the QScrollBar calls twice the function that is called on a valueChanged signal.

When I put only 10 signals (self.Sigs_dict = np.random.rand(10,105*self.Fs)) I pass only once in the update_plot function where I print time info:

first 1572956286.183867
set 0.0 

But If I increase the number of signal to 100 (self.Sigs_dict = np.random.rand(100,105*self.Fs)) I pass twice in the update_plot function. I then get :

first 1572956174.959317
set 0.009981632232666016
first 1572956175.6289513
set 0.0

I don't understand why it is happening and how I can get ride of that issue.

Here is the minimal example of my issue:

from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
import matplotlib
matplotlib.use('Qt5Agg')
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import numpy as np
import time

class Viewer(QMainWindow):
    def __init__(self, parent=None):
        super(Viewer, self).__init__()
        self.parent = parent
        #######################################
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        self.mainVBOX_param_scene = QVBoxLayout()
        self.mascene = plot(self)

        self.paramPlotV = QVBoxLayout()
        self.horizontalSliders  = QScrollBar(Qt.Horizontal)
        self.horizontalSliders.valueChanged.connect(self.update_plot)
        self.horizontalSliders.setMinimum(0)
        self.horizontalSliders.setMaximum(1)
        self.horizontalSliders.setPageStep(1)

        self.paramPlotV.addWidget(self.horizontalSliders)

        self.mainVBOX_param_scene.addWidget(self.mascene)
        self.mainVBOX_param_scene.addLayout(self.paramPlotV)

        self.centralWidget.setLayout(self.mainVBOX_param_scene)

        self.Fs = 1024
        self.Sigs_dict = np.random.rand(100,105*self.Fs)
        self.t = np.arange(self.Sigs_dict.shape[1])/self.Fs
        self.update()

    def updateslider(self):
        self.horizontalSliders.setMinimum(0)
        self.horizontalSliders.setMaximum(np.ceil(self.t[-1]/int(10))-1)

    def update_plot(self):
        t = time.time()
        print('first',t)
        self.mascene.update_set_data()
        print('set',time.time() - t)

    def update(self):
        self.updateslider()
        self.mascene.modify_sigs()
        self.mascene.update()


class plot(QGraphicsView):
    def __init__(self, parent=None):
        super(plot, self).__init__(parent)
        self.parent = parent
        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)
        self.figure = plt.figure(facecolor='white')#Figure()
        self.canvas = FigureCanvas(self.figure)
        self.widget = QWidget()
        self.widget.setLayout(QVBoxLayout())
        self.scroll = QScrollArea(self.widget)
        self.scroll.setWidget(self.canvas)

        layout = QVBoxLayout()
        layout.addWidget(self.scroll)
        self.setLayout(layout)

    def modify_sigs(self):
        self.Sigs_dict = self.parent.Sigs_dict
        self.t = self.parent.t
        self.Fs= self.parent.Fs

    def update_set_data(self):
        ts, te = self.get_ts_te()
        t = self.t[ts:te]
        for i,line in enumerate(self.Lines):
            line.set_data(t, (self.Sigs_dict[i, ts:te]) + i)
        self.axes.set_xlim((ts/ self.Fs, ts / self.Fs + 10 ))
        self.canvas.draw_idle()


    def update(self):
        self.figure.clear()
        plt.figure(self.figure.number)
        self.axes = plt.subplot(1, 1, 1)
        ts, te = self.get_ts_te()

        self.Lines = []
        for i in range(self.Sigs_dict.shape[0]):
            line, = plt.plot(self.t[ts:te], (self.Sigs_dict[i,ts:te]-np.mean(self.Sigs_dict[i,ts:te]))+i)
            self.Lines.append(line)

        self.canvas.setGeometry(0, 0, self.parent.width()-100, (self.parent.height()-100))
        self.canvas.draw_idle()

    def get_ts_te(self):
        win_num = self.parent.horizontalSliders.value()
        ts = int(10 * (win_num) * self.Fs)
        te = ts + int(10 * self.Fs)
        if te > len(self.t):
            diff = te - len(self.t)
            ts = ts - diff
            te = len(self.t)
        return ts, te

def main():
    app = QApplication(sys.argv)
    ex = Viewer(app)
    ex.show()
    sys.exit(app.exec())


if __name__ == '__main__':
    main()
2

There are 2 best solutions below

0
On BEST ANSWER

I found a way to counter the issue. I add self.horizontalSliders.setEnabled(False) before the matplotlib figure update and I set it to True when the update is finished. So I have:

def update_plot(self):
    self.horizontalSliders.setEnabled(False)
    t = time.time()
    print('first',t, self.horizontalSliders.value())
    self.mascene.update_set_data()
    print('set',time.time() - t)
    self.horizontalSliders.setEnabled(True)

I don't know if it is the right way to do it but it works

6
On

I see this occur when you try to move the slider more than one increment. As soon as the value changes 1 increment you get an update, and then another when when the value changes again. Because the drawing time for 100 signals is noticeably slower than for 10, the GUI response is slower, thus the slider tends to give multiple updates instead of jumping to the final value.

The simple fix for this is to change your connected signal to

self.horizontalSliders.sliderReleased.connect(self.update_plot)

The sliderReleased signal will assure your done moving the slider before doing your update.

The downside to this approach is that single mouse click on the arrows aren't captured, only a drag of the bar. If you want to handle both while preventing multiple updates, you'll need to create state variable to decide which mode you are in. You can look at the sliderPressed signal to see if the user is dragging the slider. If they are, look at the sliderReleased signal for completion. If not, use the valueChanged signal.

BTW... QT's signals ans slots are great but this type of signal recursion is very common and you often need to put in extra checks to prevent multiple calls to expensive redraw functions, etc..