简体   繁体   中英

PyQt: signal emitted twice when calculations are too long

I use two widgets: a QSpinBox and a QLineEdit . valueChanged slot of the QSpinBox widget is connected to the update function. This function consist of a time-consuming processing (a loop with calculations or a time.sleep() call) and a QLineEdit.setText() call. At the beginning, i thought it worked as expected but I noticed that the signal seems to be emitted twice when the calculations takes a long time.

Bellow is the code:

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

Another version of 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))

There is no real mystery here. If you click a spin-box button, the value will increase by a single step. But if you press and hold down the button, it will increase the values continually. In order to tell the difference between a click and a press/hold, a timer is used. Presumably, the threshold is around half a second. So if you insert a small additional delay, a click may be interpreted as a short press/hold, and so the spin-box will increment by two steps instead of one.

UPDATE :

One way to work around this behaviour is by doing the processing in a worker thread, so that the delay is eliminated. The main problem with this is avoiding too much lag between spin-box value changes and line-edit updates. If you press and hold the spin-box button, a large number of signal events could be queued by the worker thread. A simplistic approach would wait until the spin-box button was released before handling all those queued signals - but that would result in a long delay whilst each value was processed separately. A better approach is to compress the events, so that only the most recent signal is handled. This will still be somewhat laggy, but if the processing time is not too long, it should result in acceptable behaviour.

Here is a demo that implements this approach:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM