qmlqt-quickqtquickcontrols2

Change shape of a custom QtQuick Control template


I'm currently writing a custom style module for my QML application (using Qt 6.6.1). I don't want to reinvent the wheel and rewrite the whole backend of basic controls, so I'm using the QtQuick Control template method, as decribed here.

My problem is that I want to create a SpinBox with rounded corner, like on the following design :

SpinBox design

I achieve to round the corners of the text field by applying a radius to the background property's Rectangle. But I find hard to round the corner of the + and - buttons.

SpinBox generated by qml

My first idea was to have a kind of parent rounded Rectangle with clip: true, but I can't do this with the Quick Control Template method. The root Item must be T.SpinBox, and I cannot set another parent to sub-elements properties like background, up.indicator, down.indicator and contentItem

Here is my current code :

import QtQuick
import QtQuick.Controls.impl
import QtQuick.Templates as T


T.SpinBox {
    id: control

    implicitWidth: background.implicitWidth
                   + up.indicator.width
                   + leftPadding
                   + rightPadding

    implicitHeight: background.implicitHeight
                    + topPadding
                    + bottomPadding

    leftPadding: padding
                 + (control.mirrored ?
                        (up.indicator ?
                            up.indicator.width
                            : 0
                        )
                        : (down.indicator ?
                            down.indicator.width
                            : 0
                        )
                    )

    rightPadding: padding
                  + (control.mirrored ?
                         (down.indicator ?
                            down.indicator.width
                            : 0)
                        : (up.indicator ?
                            up.indicator.width
                            : 0
                        )
                    )

    wheelEnabled: true
    editable: true
    font: Theme.fonts.controlText

    validator: IntValidator {
        locale: control.locale.name
        bottom: Math.min(control.from, control.to)
        top: Math.max(control.from, control.to)
    }

    contentItem: TextInput {
        id: textField
        z: 2
        text: control.displayText
        clip: width < implicitWidth

        font: control.font
        color: Theme.colors.textDisabled
        selectionColor: control.palette.highlight
        selectedTextColor: control.palette.highlightedText
        horizontalAlignment: Qt.AlignHCenter
        verticalAlignment: Qt.AlignVCenter
        anchors.left: parent.left
        anchors.right: up.indicator.left
        anchors.top: parent.top
        anchors.bottom: parent.bottom

        readOnly: !control.editable
        validator: control.validator
        inputMethodHints: control.inputMethodHints
    }

    up.indicator: Rectangle {
        id: upIndicator
        implicitWidth: 12
        implicitHeight: 9
        height: parent.height / 2
        anchors.right: parent.mirrored ? undefined : parent.right
        anchors.left: parent.mirrored ? parent.left : undefined
        anchors.top: parent.top
        color: "red"
    }

    down.indicator: Rectangle {
        id: downIndicator
        implicitWidth: 12
        implicitHeight: 9
        height: parent.height / 2
        color: "blue"

        anchors.right: parent.mirrored ? undefined : parent.right
        anchors.left: parent.mirrored ? parent.left : undefined
        anchors.bottom: parent.bottom
    }

    background: Rectangle {
        id: backgroundRectangle
        implicitWidth: 30
        implicitHeight: 18
        color: "green"
        border.width: 0
        radius: 2
    }
}

I've also tried to set Items to SpinBox fields properties, from Items instancied elsewhere (in a clipping Rectangle for example, but it didn't cropped the items :

//imports

T.SpinBox {

    //... some size management

    contentItem: textField
    up.indicator: upIndicator
    down.indicator: downIndicator
    background: backgroundRectangle

    Rectangle {
        id: clippingRectangle
        anchors.fill: parent
        radius: 2
        clip: true

        TextInput {
            id: textField
            //...
        }

        Rectangle {
            id: upIndicator
            //...
        }

        Rectangle {
            id: downIndicator
            //...
        }

        Rectangle {
            id: backgroundRectangle
            //...
        }
    }
}

I've also tried to work with Layers and MultiEffect to put a mask on different items, without success again

import QtQuick.Effects

T.SpinBox {

    //... Size management + assign different fields as in first exemple

    Rectangle {
        id: maskRectangle
        color: "transparent"
        radius: 2
        anchors.fill: control
        border.width: 1
        border.color: Theme.colors.widgetBorder
        z: 3
    }

    layer.enabled: true
    MultiEffect {                  // Mask whole control -> doesn't works !
        source: control
        anchors.fill: control
        maskEnabled: true
        maskSource: maskRectangle
    }

    MultiEffect {                  // Mask up and down indicator items -> doesn't works !
        source: upIndicator
        anchors.fill: upIndicator
        maskEnabled: true
        maskSource: maskRectangle
    }
}

I'm out of ideas to achive this design. I would prefer to don't rewrite a full custom spinBox from scratch. I like the way that the custom style works, and can adapt controls depending on application stye. Does anyone have any idea or clues to help me ?


Solution

  • I achieved to do this according to @JarmMan's suggestion.

    I preffered use several Rectangles because it's easier to maintain and understand than Shapes.

    Here is the resulting code :

    import QtQuick
    import QtQuick.Controls.impl
    import QtQuick.Shapes
    import QtQuick.Templates as T
    
    import MyTheme
    
    T.SpinBox {
        id: control
    
        implicitWidth: background.implicitWidth + up.indicator.width + leftPadding + rightPadding
        implicitHeight: background.implicitHeight + topPadding + bottomPadding
    
        wheelEnabled: true
        editable: true
        font: Theme.fonts.controlText
        clip: true
    
        validator: IntValidator {
            locale: control.locale.name
            bottom: Math.min(control.from, control.to)
            top: Math.max(control.from, control.to)
        }
    
        contentItem: TextInput {
            id: textField
    
            font: control.font
            color: Theme.colors.textDisabled
            text: control.displayText
            selectionColor: control.palette.highlight
            selectedTextColor: control.palette.highlightedText
            horizontalAlignment: Qt.AlignHCenter
            verticalAlignment: Qt.AlignVCenter
            anchors.left: parent.left
            anchors.right: upIndicator.left
            anchors.top: parent.top
            anchors.bottom: parent.bottom
    
            readOnly: !control.editable
            validator: control.validator
            inputMethodHints: control.inputMethodHints
        }
    
        up.indicator: Rectangle {
            id: upIndicator
            implicitWidth: 12
            implicitHeight: 9
            height: control.height / 2
            anchors.right: control.right
            anchors.top: control.top
            color: "transparent"
            radius: 2
    
            Rectangle {
                height: parent.radius
                anchors.bottom: parent.bottom
                anchors.left: parent.left
                anchors.right: parent.right
                color: parent.color
                border.width: 0
            }
    
            Rectangle {
                width: parent.radius
                anchors.left: parent.left
                anchors.top: parent.top
                anchors.bottom: parent.bottom
                color: parent.color
                border.width: 0
            }
    
            Image {
                source: "qrc:/qt/qml/MyTheme/Images/SpinBox-Up.svg"
                sourceSize.height: 3
                sourceSize.width: 6
                fillMode: Image.PreserveAspectFit
                anchors.centerIn: upIndicator
            }
        }
    
        down.indicator: Rectangle {
            id: downIndicator
            implicitWidth: 12
            implicitHeight: 9
            height: control.height / 2
            color: "transparent"
            radius: 2
    
            anchors.right: control.right
            anchors.bottom: control.bottom
    
            Rectangle {
                height: parent.radius
                anchors.top: parent.top
                anchors.left: parent.left
                anchors.right: parent.right
                color: parent.color
                border.width: 0
            }
    
            Rectangle {
                width: parent.radius
                anchors.left: parent.left
                anchors.top: parent.top
                anchors.bottom: parent.bottom
                color: parent.color
                border.width: 0
            }
    
            Image {
                source: "qrc:/qt/qml/MyTheme/Images/SpinBox-Down.svg"
                sourceSize.height: 3
                sourceSize.width: 6
                fillMode: Image.PreserveAspectFit
                anchors.centerIn: downIndicator
            }
        }
    
        background: Rectangle {
            id: backgroundRectangle
            implicitWidth: 30
            implicitHeight: 18
            color: "transparent"
            border {
                width: 1
                color: Theme.colors.widgetBorder
            }
            radius: 2
    
            Shape {
                anchors.fill: parent
    
                ShapePath {
                    startX: upIndicator.x
                    startY: 0
                    PathLine {
                        relativeX: 0
                        relativeY: backgroundRectangle.height
                    }
    
                    strokeColor: Theme.colors.widgetBorder
                    strokeWidth: 1
                    fillColor: "transparent"
    
                }
    
                ShapePath {
                    startX: upIndicator.x
                    startY: control.height / 2
                    PathLine {
                        relativeX : upIndicator.width
                        relativeY: 0
                    }
    
                    strokeColor: Theme.colors.widgetBorder
                    strokeWidth: 1
                    fillColor: "transparent"
    
                }
            }
        }
    }