如何防止 Qscrollbar 的箭头调用两次相同的函数?

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

我制作了一个 pyqt window 以在 matplotlib 图形中显示信号,并使用 QScrollBar 查看信号的不同部分。 我的问题是当我绘制 100 信号时,QScrollBar 的箭头调用了 valueChanged 信号上调用的函数的两倍。

当我只输入 10 个信号 (self.Sigs_dict = np.random.rand(10,105*self.Fs)) 时,我在打印时间信息的 update_plot 函数中只传递了一次:

first 1572956286.183867
set 0.0 

但是如果我将信号数量增加到 100 (self.Sigs_dict = np.random.rand(100,105*self.Fs)),我会在 update_plot 函数中传递两次。然后我得到:

first 1572956174.959317
set 0.009981632232666016
first 1572956175.6289513
set 0.0

我不明白为什么会这样,也不知道我该如何解决这个问题。

这是我的问题的最小示例:

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()

我发现当您尝试将滑块移动超过一个增量时会发生这种情况。一旦值改变 1 个增量,你就会得到一个更新,然后当值再次改变时另一个。因为 100 个信号的绘制时间明显比 10 个慢,GUI 响应更慢,因此滑块倾向于提供多次更新而不是跳转到最终值。

解决此问题的简单方法是将连接的信号更改为

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

sliderReleased 信号将确保您在执行更新之前完成移动滑块。

这种方法的缺点是鼠标单击箭头不会被捕获,只能拖动条。如果您想在防止多次更新的同时处理这两种情况,则需要创建状态变量来决定您处于哪种模式。您可以查看 sliderPressed 信号以查看用户是否正在拖动滑块。如果是,请查看 sliderReleased 完成信号。如果不是,请使用 valueChanged 信号。

顺便说一句...QT 的信号和插槽很棒,但是这种类型的信号递归很常见,您经常需要进行额外的检查以防止多次调用昂贵的重绘函数等。

我找到了解决这个问题的方法。 我在 matplotlib 图更新之前添加 self.horizontalSliders.setEnabled(False) 并在更新完成后将其设置为 True 。所以我有:

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)

我不知道这样做是否正确,但它有效