Search code examples
pythonpyqtsignalsqspinbox

When QSpinBox changed in PyQt5, can I send a signal selectively?


I faced very difficult situation during the project. The function signal flow was described below, so I hope this picture help you understand what I want to solve it. I have a QSpinBox object(self.variable) and when its value was changed, the func_3 will be called through valuechanged signal. There are two ways of changing the QSpinBox value. One is simply click or edting the value of QspinBox directly, and the other is to call func_1 which is supposed to change QspinBox value. And when the value of QSpinbox is changed, the func_3 will be called in both situation. The problem is "I don't want to call func_3 when it is situation2". I want to call func_3, only when QSpinbox on GUI is directly changed not through func_1.

Is their any simple or noble ways to solve this problem? I hope many PyQt or Python experts's brilliant ideas. Thanks

enter image description here


Solution

  • There are at least four ways to do so.

    Block signals

    Use blockSignals() before calling setValue():

        block = self.spinBox.blockSignals(True)
        self.spinBox.setValue(newValue)
        self.spinBox.blockSignals(block)
    

    Restoring the block variable is required, because signals might have been blocked somewhere else, and it's important to restore the previous state.

    The more appropriate way to do so, though, is through QSignalBlocker, which is safer (the blocking is restored anyway even in case of exceptions):

        with QSignalBlocker(self.spinBox):
            self.spinBox.setValue(newValue)
    

    The main drawback of this approach is that it blocks all signals and connections indiscriminately: there are cases for which signals must be emitted for other uses, or a signal is connected to more than one function, with one of them requiring the signal anyway for proper behavior.

    A typical case is with complex widgets; for instance, if you block the signals of a item model while changing its values, the views linked to it will never know about the changes and won't update properly.

    Disconnect the function/slot

    You can temporarily disconnect the signal from the function before calling setValue() and reconnect it afterwards:

        self.spinBox.valueChanged.connect(self.valueChangedFunc)
    
    def setSpinValue(self, value):
        self.spinBox.valueChanged.disconnect(self.valueChangedFunc)
        self.spinBox.setValue(value)
        self.spinBox.valueChanged.connect(self.valueChangedFunc)
    

    In this way you can target a specific function, while retaining any other behavior related to the signal.

    The main issue with this is that in case you used a lambda (and didn't keep a reference for it), you cannot disconnect from it. You can only globally disconnect the signal (ie. self.spinBox.valueChanged.disconnect()), but reconnecting it might be a problem, especially if the lambda was based on temporary, local variables.

    Use a flag check

    For simple, specific cases, you can set a flag (possibly as instance attribute) before setting the value, and check it in the connected function:

    class MyClass(QWidget):
        spinBoxChanging = False
    
        # ...
        def setSpinValue(self, value):
            self.spinBoxChanging = True
            self.spinBox.setValue(value)
            self.spinBoxChanging = False
    
        def valueChangedFunc(self, value):
            if self.spinBoxChanging:
                return
            # go on...
    

    You have to be careful, though: the flag must be always restored, and you have to remember that behavior in case you call that function from somewhere else (but still want the signal to propagate as expected). Careful exception handling should be implemented too (see the above aspect of QSignalBlocker).

    Use a subclass with a custom signal

    You can create a custom QSpinBox subclass with a similar signal that is directly connected to valueChanged, and emit only when setValue() is explicitly called using an override.
    This works similarly to solution #2, but, instead of disconnecting the function, we disconnect the custom signal and reconnect it afterwards.

    class MySpinBox(QSpinBox):
        valueHasChanged = pyqtSignal(int)
        def __init__(self, *args, **kwargs)
            super().__init__(*args, **kwargs)
            self.valueChanged.connect(self.valueHasChanged)
    
        def setValue(self, value, emit=False):
            if not emit:
                self.valueChanged.disconnect(self.valueHasChanged)
            super().setValue(value)
            if not emit:
                self.valueChanged.connect(self.valueHasChanged)
    
    
    class MyClass(QWidget):
        def __init__(self):
            # ...
            self.spinBox = MySpinBox()
            self.spinBox.valueHasChanged(self.doSomething)
    
        def setSpinValue(self, value):
            self.spinBox.setValue(value)
    

    With the above, you can still "force" the signal emission by explicitly calling setValue(value, True).