PyQt:当计算太长时信号发出两次

PyQt: signal emitted twice when calculations are too long

我使用了两个小部件:一个 QSpinBox 和一个 QLineEditQSpinBox 小部件的 valueChanged 插槽连接到 update 函数。这个函数由一个耗时的处理(一个带计算的循环或一个time.sleep()调用)和一个QLineEdit.setText()调用组成。一开始,我以为它按预期工作,但我注意到当计算时间很长时,信号似乎会发出两次。

下面是代码:

import time

from PyQt5.QtWidgets import QWidget, QSpinBox, QVBoxLayout, QLineEdit


class Window(QWidget):
    def __init__(self):
        # parent constructor
        super().__init__()

        # widgets
        self.spin_box = QSpinBox()
        self.line_edit = QLineEdit()

        # layout
        v_layout = QVBoxLayout()
        v_layout.addWidget(self.spin_box)
        v_layout.addWidget(self.line_edit)

        # signals-slot connections
        self.spin_box.valueChanged.connect(self.update)

        #
        self.setLayout(v_layout)
        self.show()

    def update(self, param_value):
        print('update')
        # time-consuming part
        time.sleep(0.5) # -> double increment
        #time.sleep(0.4) # -> works normally!
        self.line_edit.setText(str(param_value))


if __name__ == '__main__':
    from PyQt5.QtWidgets import QApplication
    import sys

    app = QApplication(sys.argv)
    win = Window()
    sys.exit(app.exec_())

update的另一个版本:

# alternative version, calculations in a loop instead of time.sleep()
# -> same behaviour
def update2(self, param_value):
    print('update2')
    for i in range(2000000): # -> double increment
        x = i**0.5 * i**0.2
    #for i in range(200000): # -> works normally!
    #    x = i**0.5 * i**0.2
    self.line_edit.setText(str(param_value))

这里没有真正的神秘之处。如果单击数字显示框按钮,值将增加一步。但是如果你按住按钮,它会不断增加值。为了区分点击和 press/hold,使用了一个计时器。据推测,阈值大约是半秒。因此,如果您插入一个小的额外延迟,点击可能会被解释为短 press/hold,因此旋转框将增加两步而不是一步。

更新:

解决此问题的一种方法是在工作线程中进行处理,从而消除延迟。这样做的主要问题是避免旋转框值更改和行编辑更新之间的过多滞后。如果按住旋转框按钮,工作线程可能会排队等待大量信号事件。一种简单的方法是等到旋转框按钮被释放后再处理所有这些排队的信号——但这会导致长时间延迟,同时每个值都被单独处理。更好的方法是压缩事件,以便只处理最近的信号。这仍然会 有点 滞后,但如果处理时间不是太长,它应该会导致可接受的行为。

这是一个实现这种方法的演示:

import sys, time
from PyQt5.QtWidgets import (
    QApplication, QWidget, QSpinBox, QVBoxLayout, QLineEdit,
    )
from PyQt5.QtCore import (
    pyqtSignal, pyqtSlot, Qt, QObject, QThread, QMetaObject,
    )

class Worker(QObject):
    valueUpdated = pyqtSignal(int)

    def __init__(self, func):
        super().__init__()
        self._value = None
        self._invoked = False
        self._func = func

    @pyqtSlot(int)
    def handleValueChanged(self, value):
        self._value = value
        if not self._invoked:
            self._invoked = True
            QMetaObject.invokeMethod(self, '_process', Qt.QueuedConnection)
            print('invoked')
        else:
            print('received:', value)

    @pyqtSlot()
    def _process(self):
        self._invoked = False
        self.valueUpdated.emit(self._func(self._value))

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.spin_box = QSpinBox()
        self.line_edit = QLineEdit()
        v_layout = QVBoxLayout()
        v_layout.addWidget(self.spin_box)
        v_layout.addWidget(self.line_edit)
        self.setLayout(v_layout)
        self.thread = QThread(self)
        self.worker = Worker(self.process)
        self.worker.moveToThread(self.thread)
        self.worker.valueUpdated.connect(self.update)
        self.spin_box.valueChanged.connect(self.worker.handleValueChanged)
        self.thread.start()
        self.show()

    def process(self, value):
        time.sleep(0.5)
        return value

    def update(self, param_value):
        self.line_edit.setText(str(param_value))

if __name__ == '__main__':

    app = QApplication(sys.argv)
    win = Window()
    sys.exit(app.exec_())