3

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.

S818
  • 391
  • 3
  • 15

3 Answers3

3

The problem is representation of floats in computer memory wikipedia.

Add to your code from math import isclose and change button check on self.button.setEnabled(not isclose(0, value, abs_tol=1e-9))

Other solution, without math is use abs and compare value to some very small number. self.button.setEnabled(abs(value) > 1e-9)

Grzegorz Bokota
  • 1,736
  • 11
  • 19
  • Thank you for pointing me at `isclose` which I didn't know of! For this and because your answer is totally valid I upvoted it, but I accepted John's answer because it doesn't require importing math and seems slightly more elegant to me. – S818 Aug 01 '19 at 13:07
  • I think that going through conversion to string is not optimal solution. I think that importing math generate smaler overhead than converting string to float. I add to response second variant without `math` library. – Grzegorz Bokota Aug 01 '19 at 21:03
  • I'd add that, since the math module is part of the SL (and many basic math functions of python actually use it under the hood), the difference in importing it is negligible, especially as opposed to importing a *huge* module like Qt is. – musicamante Oct 02 '21 at 18:56
2

I agree with what Grzegorz says, as, conceptually speaking, using math is a proper (and IMHO actually more elegant) way to do so.

Another alternative is to use the round function, since we know the precision based on the decimals property of the spinbox:

    self.button.setEnabled(round(value, self.spin.decimals()))

This solution a middle-way between the given ones, but I believe it could also be a more valid one too:

  • it does use math instead of string conversion;
  • it does not require the math module; importing it is not such an overhead (especially as opposed to Qt modules and considering that even some built-in functions actually use it anyway), but one might prefer to avoid it;
  • it's more readable than any use of 1e-9;
  • it still considers the actually displayed value (since the low-level implementation is probably the same), so -0.00 is still returned as -0.00, but it also equals to 0 (see negative zero in python);
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • 2
    Elegance is of course debatable. But there is a software engineering principle at play here: separation of concerns. Qt is the framework that displays the number, which internally is most likely a floating-point value, that's true. But then, in order to display it, it converts that number to a string and rounds it accordingly. Since the Python code is supposed to react to what the user actually sees on the screen, we should leave the string conversion to Qt. As that is really Qt's *concern*. And it violates that principle to replicate, no matter how elegantly, what we think Qt is doing. – john-hen Oct 03 '21 at 23:10
  • Yes, your answer is also very elegant, so: +1. To clarify my sense of elegance in this case: I don't generally avoid `math`. I use it happily in modules where I actually want to implement math-y calculations, but it bugged me to import it in my gui module where its only purpose would have been to fix Qt's unintuitive behaviour. Even if the math-import is not that much of an overhead, (probably true), I'd rather take the actual overhead and work with the string representation. You may ascribe to me compulsive orderliness, but I'd prefer to be called SoC-compliant as John kindly elucidated! :-) – S818 Oct 04 '21 at 14:32
  • @S818 the Qt behavior might seem unintuitive, but I believe it's actually correct and consistent: while `setValue` adjusts the value based on the decimals, the single step doesn't have that "limitation" (nor it could, since it should accept the step change even if the decimal variation is not visible). Unfortunately, the float representation is *always* an issue, especially with UI elements, and some compromise is always required at some point of the implementation. – musicamante Oct 04 '21 at 15:16
  • @JohnHennig I agree with your point, but the fact is that the value of the double spinbox *can* be different due to the behavior of the step change (for instance, the program could allow to change the decimals, but keep the step precision even if the variation doesn't affect the visible difference). It could be considered a "bug", but, while forcing (on the Qt side of the implementation) the step to change based on the change of decimals might become much complex, implementing a system that can "fix" the step based on the decimals is easily doable if required. – musicamante Oct 04 '21 at 15:26
  • @musicamante: That's a good point. I haven't actually tested what you describe. And rather than a "bug" I would call it a "leaky abstraction", but that's equally bad and also a violation of software engineering principles (in that case, by the Qt framework). If that applies, in a given scenario, we may have to fix it on the Python side. – john-hen Oct 04 '21 at 19:54
  • @JohnHennig Indeed you're right, and yes, this question certainly deserves more votes considering the average PyQt questions; which is an interesting topic, I think that the low quality of most posts is due to the assumption that, since PyQt makes things "easier" since it's using python, most beginners are not patient enough to carefully study it as they should, and their questions reflect that lack of experience not only in the language/toolkit, but also in "programmer thinking", which brings to questions that lack research, insufficient/extended/unreproducible examples, bad practices, etc. – musicamante Oct 05 '21 at 12:47
  • @JohnHennig Unfortunately, I've also seen similar patterns in some tutorials (which is even worse): their authors are not experienced enough, they just scratch the surface without realizing how deep and complex Qt is, they see a result that seems valid to them, and believe they can teach it. Which brings back *more* bad quality questions. Back to the topic, yes, the basic behavior is not optimal, but I think it's still a good compromise: standard widgets should always provide basic features and "common" behavior (even if not always optimal like this), extending or "fixing" is *our* job ;-) – musicamante Oct 05 '21 at 12:56
1

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.

john-hen
  • 4,410
  • 2
  • 23
  • 40