5

I use a QML Spinbox but I have trouble to use floats in it. If I write something like value: 5.0 , it will be displayed as 5 , so as an int instead of a float.

Do you have any idea of how to proceed ?

Thanks a lot and have a good day !

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Ed Nio
  • 593
  • 2
  • 11
  • 27
  • 6
    It's right there in the doc you linked : https://doc.qt.io/qt-5/qml-qtquick-controls2-spinbox.html#custom-values – GrecKo Apr 14 '17 at 08:03
  • Thanks ! But I would like to know if there no simple way to do it, because I couldn't find doublespinbox or something like that. – Ed Nio Apr 14 '17 at 08:08
  • 1
    Suggestion for DoubleSpinBox: https://bugreports.qt.io/browse/QTBUG-67349 – Mitch Mar 27 '18 at 12:04

4 Answers4

11

You can create a Spinbox with custom texts

DoubleSpinBox.qml

import QtQuick 2.0

import QtQuick.Controls 2.1

Item {
    property int decimals: 2
    property real realValue: 0.0
    property real realFrom: 0.0
    property real realTo: 100.0
    property real realStepSize: 1.0

    SpinBox{
        property real factor: Math.pow(10, decimals)
        id: spinbox
        stepSize: realStepSize*factor
        value: realValue*factor
        to : realTo*factor
        from : realFrom*factor
        validator: DoubleValidator {
            bottom: Math.min(spinbox.from, spinbox.to)*spinbox.factor
            top:  Math.max(spinbox.from, spinbox.to)*spinbox.factor
        }

        textFromValue: function(value, locale) {
            return parseFloat(value*1.0/factor).toFixed(decimals);
        }

    }
}

Example:

DoubleSpinBox{
    realValue: 5.0
    realStepSize: 0.01
}

enter image description here

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thanks a lot, this is simple and nice ! :) – Ed Nio Apr 14 '17 at 08:17
  • 2
    Basically it is the same, as the second example of GrecKo's link, just not so good, as you forgot many parts: `valueFromText` so someone can enter a text, and it is validated. And you provide no way, to read out the value as you hide it, by (I can't se any reason for it) encapsulating the `SpinBox` in an `Item`. I can't see how you write back the current value to your `Item`. – derM - not here for BOT dreams Apr 14 '17 at 08:26
  • I'd propose to use exactly the second example of the link, already posted by @GrecKo. Once again: https://doc.qt.io/qt-5/qml-qtquick-controls2-spinbox.html#custom-values – derM - not here for BOT dreams Apr 14 '17 at 08:28
  • @derM It is not the same example, if you test the code you will see that you do not get 5.0 but 5, place it inside an item to hide the property factor. – eyllanesc Apr 14 '17 at 08:31
  • I tested the code, and it works fine. Indeed it is no real *float point*-Spinbox but a *fixed point*, and *fixed point* is appropriately handled by storing it internaly as `int`. The only thing that could be improved is a way, to initially set a `realValue` - but this would require a more complex double binding mechanism, that would break the scope of an example. (You need to determine whether the change was internally or externally...) But it should be rather simple for a programmer, to set it up with having in mind, that a value of 100 equals 1.00 if you have two decimals set. – derM - not here for BOT dreams Apr 14 '17 at 09:13
  • Therfore the only thing you need to take care of, is that the output is right (e.g. shows 1.00 instead of 100 on the screen) and gives you a way to read out the right output (for convenience) – derM - not here for BOT dreams Apr 14 '17 at 09:13
  • @derM indeed there are some mistakes, but encapsulation is needed to have same API as SpinBox. I posted an improved version – ceztko Dec 06 '17 at 00:32
8

There is a limit in the current SpinBox (Controls 2.0 - 2.4) in that it only accepts a to/from range of +/- 0x7FFF FFFF (which can't even handle an unsigned int, BTW). For reals this has the (possibly serious) implication that for every digit you need after the decimal point, you lose a digit before the decimal. For example if you need 6 digit decimal precision, your maximum possible value is 2147.483647. This affects both the proposed solutions in this answer so far.

If you try to set the to or from beyond those int limits (eg. by multiplying by the factor like in the suggestions here), you will get very "strange" behavior which can be quite baffling. If you try to exceed those limits directly (eg. to: 0xFFFFFFFF) you get an error.

See (and vote for :) ) QTBUG-67349

I realize this is not an answer to the question, but hopefully it can save someone some hours of frustration like I had.

The only workaround I can come up with so far is to simply use a text edit field with a DoubleValidator. I will try to come back here with a simplified example.

EDIT:

Here is my version of a DoubleSpinBox (docs). The code is too long to paste here (IMHO) and I'd rather not maintain multiple versions.

Briefly, the idea is to bypass the SpinBox value entirely and just use the base Controls.2 SpinBox for the buttons and overall look/feel. This means it doesn't need to be customized per theme (Fusion/Material/etc).

Unfortunately this involves re-implementing all the event handlers (button presses/repeats, wheel scroll, text edit), and a bit of other chicanery. But (so far) I think it beats re-implementing the whole thing from scratch with custom buttons/etc while trying to match all the different themes.

It also adds a few options not available in the standard SpinBox... because, why not? :)

Default theme Fusion theme Material theme

Maxim Paperno
  • 4,485
  • 2
  • 18
  • 22
2

This is a @eyllanesc fixed version. All other missing properties must be aliased. Please report me bugs, if you find.

DoubleSpinBox.qml

import QtQuick 2.0
import QtQuick.Controls 2.2
import QmlUtils 1.0

Item
{
    id: doublespinbox
    width: 140
    height: 40
    property int decimals: 1
    property alias value: valuePreview.value
    property real from: 0
    property real to: 99
    property real stepSize: 1
    property alias editable: spinbox.editable
    property alias font: spinbox.font

    SpinBox
    {
        id: spinbox
        property bool init: false
        property real factor: Math.pow(10, decimals)

        function setValue(preview)
        {
            init = true
            value = preview.value * factor
            init = false
            preview.value = value / factor
        }

        DoubleValuePreview
        {
            id: valuePreview
            onValuePreview: spinbox.setValue(preview)
        }

        anchors.fill: parent
        editable: true
        stepSize: doublespinbox.stepSize * factor
        to : doublespinbox.to * factor
        from : doublespinbox.from * factor

        onValueChanged:
        {
            if (init)
                return

            valuePreview.setValueDirect(value / factor)
        }

        validator: DoubleValidator
        {
            bottom: Math.min(spinbox.from, spinbox.to)
            top: Math.max(spinbox.from, spinbox.to)
        }

        textFromValue: function(value, locale)
        {
            return Number(value / factor).toLocaleString(locale, 'f', doublespinbox.decimals)
        }

        valueFromText: function(text, locale)
        {
            doublespinbox.value = Number.fromLocaleString(locale, text)
            return doublespinbox.value * factor
        }
    }
}

QmlValuePreview.h

#pragma once

#include <qobject.h>
#include <qqmlengine.h>

class QDoubleValueArg : public QObject
{
    Q_OBJECT
public:
    QDoubleValueArg(double value) : Value(value)
    {
        QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
    }
public:
    Q_PROPERTY(double value MEMBER Value)
public:
    double Value;
};

class QmlDoubleValuePreview : public QObject
{
    Q_OBJECT
public:
    using QObject::QObject;
public:
    Q_PROPERTY(double value READ getValue WRITE setValue NOTIFY valueChanged)
public:
    Q_INVOKABLE void setValueDirect(double value)
    {
        if (m_value == value)
            return;

        m_value = value;
        emit valueChanged();
    }
public:
    inline double getValue() const { return m_value; }
    inline void setValue(double value)
    {
        if (m_value == value)
            return;

        QDoubleValueArg arg(value);
        emit valuePreview(&arg);
        if (m_value == arg.Value)
            return;

        m_value = arg.Value;
        emit valueChanged();
    }
signals:
    void valueChanged();
    void valuePreview(QDoubleValueArg *preview);
private:
    double m_value = 0;
};

Registered with:

#define URI "QmlUtils"
#define VERSION_MAJOR 1
#define VERSION_MINOR 0

void registerTypes()
{
    qRegisterMetaType<QDoubleValueArg *>("QDoubleValueArg *");
    qmlRegisterType<QmlDoubleValuePreview>(URI, VERSION_MAJOR, VERSION_MINOR, "DoubleValuePreview");
}
ceztko
  • 14,736
  • 5
  • 58
  • 73
  • I think, because of precision loss this might result in a binding loop for some values. It would be better to not have the same API, but a robust instead. – derM - not here for BOT dreams Dec 06 '17 at 07:01
  • With subclassing not having the same API is a recipe for mistakes but I agree that there could be still problems with this code. A solution could be not relying on binding at all or just take the original SpinBox code and rewrite it with real properties instead. – ceztko Dec 06 '17 at 09:16
  • As there is a difference in REAL and INT, it won't be the same API anyway. Why then not use a different, but clear API, which does not hide the fact, that it is internally a INT, so everyone will know the implications of that. – derM - not here for BOT dreams Dec 06 '17 at 09:46
  • It's a matter of taste, then. I would prefer have same name properties but, as you say, robustness has more priority. – ceztko Dec 06 '17 at 09:57
  • @derM, I removed the binding with external value property. Also limits from, to are respected. How it looks like to you now? – ceztko Dec 06 '17 at 11:27
  • A spurious value changed will be triggered if the spinbox is already at the maximum value and an attempt to break the limit is done. Example the maximum is 99, the current value is 99 and I attempt to set 100. I don't know how to solve this problem with just qml. Is it possible at all? – ceztko Dec 06 '17 at 11:32
  • This is causing serious issues. I will delete the answer until I fix the problem. C++ code is required. – ceztko Dec 06 '17 at 11:50
  • @derM Added the C++ part. Negative numbers are also working, when correct limits are set. For me it's good enough. I hope it's also robus – ceztko Dec 07 '17 at 00:51
  • I will test it this weekend. Looks promising! – derM - not here for BOT dreams Dec 07 '17 at 07:52
1

I see two problems in code above.

  1. You will have mistake if display a number like 1.299(period) DoubleSpinBox will show 1.2 not 1.3 For fix it you should write

    value = Math.round(preview.value * factor)

in DoubleSpinBox.qml function setValue(preview)

  1. In this line we have overwriting binding

    doublespinbox.value = Number.fromLocaleString(locale, text)

because we have "property alias value: valuePreview.value" and probably 'value' will bind somewere in code So you must not do this and entirely enough to write this:

function(text, locale) {
        return Number.fromLocaleString(locale, text) * factor
}
  1. I suppose we could remove this line too

    preview.value = value / factor

In function setValue(preview)