Search code examples
pythonprecisionpyside2qdoublespinbox

Why is the precision of a QDoubleSpinBox sometimes higher than its decimals property allows?


In PySide2, if I create a QDoubleSpinBox and explicitly set its decimals-property to 2 and its singleStep-property to 0.1 and then change its value via the up/down buttons, the printed value sometimes has a much higher precision.

Why is that so?

While it may not necessarily be a problem for calculations being done with this value, it can affect the user experience:

In my case I want to disable a QPushButton if the spinbox-value is 0.0 and enable it otherwise. In case of the spinbox-value becoming something just really close to 0, the button (as well as the spinbox' down-arrow) don't get disabled although the readable number inside the spinbox says "0,00".


What I do at the moment to fix the user experience is:

int(self.spin.value()*100) > 0

instead of:

self.spin.value() > 0.0

Is this really the best way to do it?


Here is a small example to reproduce the unwanted behaviour:

import sys
from PySide2 import QtWidgets

class Window(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)
        self.spin = QtWidgets.QDoubleSpinBox(self)
        self.spin.setDecimals(2)
        self.spin.setSingleStep(0.1)
        self.button = QtWidgets.QPushButton(self)
        self.button.move(0, 40)
        self.spin.valueChanged.connect(self.on_spin_value_change)
        self.show()

    def on_spin_value_change(self, value):
        print(value)
        self.button.setEnabled(value > 0.0)


app = QtWidgets.QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())

This is an example output:

enter image description here


Edit: No, my question does not simply come down to "how to compare floats in Python". I asked for a way to fix the user experience of QDoubleSpinBoxes and for an explanation why the internal representation of the value is diffrent from the visible. Yes, choosing a different way to compare floats can solve the problem, but using another overloaded signal is the better solution for me - a solution I wouldn't have been told if my question wouldn't have been Qt/PySide-specific.


Solution

  • The better way to do this ("best", I don't know) is to connect the slot on_spin_value_change to the (overloaded) valueChanged signal that passes the value parameter as the actual string, and not as a floating-point number.

    Note how Qt actually defines two signals with different signatures. The one you used passes the value as a number and has this (C++) signature:

    void QDoubleSpinBox::valueChanged(double d)
    

    Whereas the other one passes the value as a string:

    void QDoubleSpinBox::valueChanged(const QString &text)
    

    (Edited to add: Meanwhile, more recent Qt 5 versions, and Pyside2 as well, have deprecated that second, overloaded signal in favor of the more aptly named textChanged.)

    In order to connect to the latter, not the first, use this syntax (in Python):

    self.spin.valueChanged[str].connect(self.on_spin_value_change)
    

    Then alter the condition in the slot, leveraging the fact that it's now receiving a string:

    self.button.setEnabled(float(value) > 0)
    

    This will work because float('0.0…') evaluates to true zero for any number of zeros in the string representation. Alternatively, we could also use string operations to evaluate the condition.