How do I prevent out-of-range intermediate text values in a QML SpinBox with a DoubleValidator?

Ben Green Source

I'm trying to make an editable floating-point SpinBox QML element. It all works, except it's possible to type numbers such that the text in the SpinBox displays an invalid value (e.g. 105 when the max is 100). I tried to catch the key presses with Keys.onPressed, but that doesn't seem to be possible. I also tried to use a signal like onTextChanged, but that doesn't seem to exist for a SpinBox. Finally, I have tried to subclass QValidator and use that as the validator for the spinbox, but I get the "Cannot assign object to property" error. I assume this is because the custom validator I made is not a Validator QML type.

spinbox-test.py

import sys

from PyQt5 import QtGui
from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterType
from PyQt5.QtWidgets import QApplication

class MyDoubleValidator(QtGui.QValidator):
    def __init__(self, parent=None):
        QtGui.QValidator.__init__(self, parent)
        print("Validator created")

    def validate(self, inputStr, pos):
        print("validating")

        if len(inputStr) > 2:
            return (QtGui.QValidator.Invalid, pos)
        elif len(inputStr) == 0:
            return (Qt.QValidator.Intermediate, pos)
        else:
            return (Qt.Qvalidator.Acceptable, pos)


app = QApplication(sys.argv)

qmlRegisterType(MyDoubleValidator, 'MyValidators', 1, 0, 'MyDoubleValidator')

engine = QQmlApplicationEngine()
engine.load("spinbox-test.qml")

spinbox-test.qml

import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import MyValidators 1.0

ApplicationWindow {
    visible: true
    title: qsTr("Spinbox Test")
    width: 400
    height: 350
    color: "whitesmoke"

    Item {
        id: doubleSpinbox

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

        anchors.centerIn: parent

        SpinBox {
            id: spinbox

            property real factor: Math.pow(10, doubleSpinbox.decimals)

            stepSize: doubleSpinbox.realStepSize * spinbox.factor
            value: doubleSpinbox.realValue * spinbox.factor
            to: doubleSpinbox.realTo * spinbox.factor
            from: doubleSpinbox.realFrom * spinbox.factor

            editable: true

            onValueChanged: label.text = spinbox.value / spinbox.factor

            validator: MyDoubleValidator { }

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

    Label {
        id: label
        text: doubleSpinbox.realValue
    }
}
pythonqtpyqtqmlpyqt5

Answers

answered 1 week ago Ben Green #1

I found an answer to my problem. I located the source code for the SpinBox QML element and copied the code for the TextInput contentItem. I then wrote a quick function in onTextEdited that checked the value against to and from in the SpinBox. It's not the most reusable solution, but it's all I needed. Also, make sure to import QtQuick.Control.impl to get the Default values.

import QtQuick.Controls.impl 2.2

SpinBox {
    id: spinbox

    ...

    contentItem: TextInput {
        id: spinboxTextInput

        property string oldText: spinboxTextInput.text

        z: 2
        text: spinbox.textFromValue(spinbox.value, spinbox.locale)
        opacity: spinbox.enabled ? 1 : 0.3

        font: spinbox.font
        color: Default.textColor
        selectionColor: Default.focusColor
        selectedTextColor: Default.textLightColor
        horizontalAlignment: Qt.AlignHCenter
        verticalAlignment: Qt.AlignVCenter

        readOnly: !spinbox.editable
        validator: spinbox.validator
        inputMethodHints: spinbox.inputMethodHints

        //Check the value of the new text, and revert back if out of range
        onTextEdited: {
            var val = spinbox.valueFromText(spinboxTextInput.text, spinbox.locale)
            if (val < spinbox.from || val > spinbox.to) {
                spinboxTextInput.text = spinboxTextInput.oldText
            }
            else {
                spinboxTextInput.oldText = spinboxTextInput.text
            }
        }

        Rectangle {
            x: -6 - (spinbox.down.indicator ? 1 : 0)
            y: -6
            width: spinbox.width - (spinbox.up.indicator ? spinbox.up.indicator.width - 1 : 0) - (spinbox.down.indicator ? spinbox.down.indicator.width - 1 : 0)
            height: spinbox.height
            visible: spinbox.activeFocus
            color: "transparent"
            border.color: Default.focusColor
            border.width: 2
        }
    }
}

comments powered by Disqus