/ logic

Using the QtObject element

The absolute basic element in QML is QtObject:

import QtQml 2.2

QtObject {
    property var name: "John Doe"
}

A QtObject is a QML representation of the QObject element. This can be seen in Qt source code here where the QML base types are registered:

...
void QQmlEnginePrivate::registerBaseTypes(const char *uri, int versionMajor, int versionMinor)
{
    ...
    qmlRegisterType<QObject>(uri,versionMajor,versionMinor,"QtObject");
    ...

QtObject is a non-visual element and is very useful for grouping together or creating blocks of business logic in the otherwise UI-heavy QML land.

Private properties in QML

Controlling access to QML-based objects is not possible, i.e. it is not possible to use the membership keywords such as private, public, or protected in QML.

To workaround this slight inconvenience, developers can use the QtObject element to privatize methods or properties.

Let's say we want to build a Slider component that can be used by anyone. We want to ensure our API is clean and indestructible.

Start with the Slider UI.

  • First, build the track or the line that the slider circle will move on
  • Second, build the scrubber or the circle that the user will interact with
  • Third, add a MouseArea in the scrubber so it is interactive. We set the minimumX and maximumX so the center of the circle is at the ends of the line when its value is minimized or maximized, respectively.

Slider.qml

import QtQuick 2.7

Item {
    id: root
    width: 200
    height: 40

    Rectangle {
        id: _rectangleTrack
        anchors.centerIn: parent
        width: parent.width - _rectangleScrubber.width
        height: 4
        color: "#222222"
    }

    Rectangle {
    	id: _rectangleScrubber
    	anchors.verticalCenter: _rectangleTrack.verticalCenter
    	width: parent.height / 2
    	height: width
    	radius: width / 2

    	color: "#EEEEEE"

    	border.color: "#DDDDDD"
    	border.width: 2

        MouseArea {
            anchors.fill: parent
            drag.target: parent
            drag.minimumX: 0
            drag.maximumX: _rectangleTrack.width
    	}
    }
}

Now run your Slider.qml through qmlscene and you should see this:
Screen-Shot-2017-12-30-at-1.48.07-PM

Now that we have a UI for this Slider, let's expose an API.

Several great examples of APIs are already written, and in this case, Qt has already built a Slider. In the interest of learning from the ground up, let's take three properties from their API:

  • value - The current value held by the slider. This is calculated by mapping the percentage of the scrubber on the track to the difference between the maximumValue and minimumValue properties
  • minimumValue - The smallest possible value, i.e. the value when the scrubber is at 0%
  • maximumValue - The largest possible value, i.e. the value when the scrubber is at 100%
    property real value: 0.0
    property real minimumValue: 0.0
    property real maximumValue: 10.0

We will use floating point values for maximum flexibility.

Now, let's connect this API to our UI elements. It's important to consider the user of each property in order to build a stable, secure API.

The value property will be used by:

  • External: Developers who use the Slider element can set a real number to the value property
  • Internal: When a user of the Slider UI element drag the scrubber, logic that lives internal to the Slider will update the value property to accurately reflect the new value.

The minimumValue and maximumValue properties will be used by:

  • External: Developers who use the Slider element can set these two values to create a range of possible values for the Slider
  • Internal: This value is never changed by internal logic of the Slider element

Now we're ready to connect our properties to our UI elements. Let's begin with the value property. When the Slider element is first created, we want to ensure the default real number of the value property affects where the position of the scrubber is on the track.

Set the value property to 5.0; this indicates the scrubber is halfway through the track.

percentageAlongTheTrack = value / (maximumValue - minimumValue)

Plugging in our numbers we get:

50% = 5.0 / (10.0 - 0.0)

We can use this simple equation to set the x value of the scrubber:

x: _rectangleTrack.width * (root.value / (root.maximumValue - root.minimumValue))

Here's the entire file for clarity:

Slider.qml

import QtQuick 2.7

Item {
    id: root

    property real value: 5.0
    property real minimumValue: 0.0
    property real maximumValue: 10.0

    width: 200
    height: 40

    Rectangle {
        id: _rectangleTrack
        anchors.centerIn: parent
        width: parent.width - _rectangleScrubber.width
        height: 4
        color: "#222222"
    }

    Rectangle {
    	id: _rectangleScrubber
    	anchors.verticalCenter: _rectangleTrack.verticalCenter

    	x: _rectangleTrack.width * (root.value / (root.maximumValue - root.minimumValue))

    	width: parent.height / 2
    	height: width
    	radius: width / 2

    	color: "#EEEEEE"

    	border.color: "#DDDDDD"
    	border.width: 2

        MouseArea {
            anchors.fill: parent
            drag.target: parent
            drag.minimumX: 0
            drag.maximumX: _rectangleTrack.width
    	}
    }
}

That is already a decent amount of logic within a UI element. Let's move it to the root element:

import QtQuick 2.7

Item {
    id: root

    property real value: 4.0
    property real minimumValue: 0.0
    property real maximumValue: 10.0

    property int scrubberXPosition: _rectangleTrack.width * (value / (maximumValue - minimumValue))

    width: 200
    height: 40

    Rectangle {
        id: _rectangleTrack
        anchors.centerIn: parent
        width: parent.width - _rectangleScrubber.width
        height: 4
        color: "#222222"
    }

    Rectangle {
    	id: _rectangleScrubber
    	anchors.verticalCenter: _rectangleTrack.verticalCenter

    	x: root.scrubberXPosition

    	width: parent.height / 2
    	height: width
    	radius: width / 2

    	color: "#EEEEEE"

    	border.color: "#DDDDDD"
    	border.width: 2

        MouseArea {
            anchors.fill: parent
            drag.target: parent
            drag.minimumX: 0
            drag.maximumX: _rectangleTrack.width
    	}
    }
}

We can make it readonly so the developers who use the Slider component cannot overwrite our logic:

readonly property int scrubberXPosition: _rectangleTrack.width * (value / (maximumValue - minimumValue))

Our API has now been extended by one property; unwillingly:

  • value
  • minimumValue
  • maximumValue
  • scrubberXPosition

It is really necessary for us to expose a readonly property for the scrubber's x position? In this case, no. This is where a private member in C++ is quite useful; however, since we are in QML land, we do not have such capability.

Enter QtObject:

We can use a QtObject as a lightweight container to wrap our logic since it is unwanted as an exposed API:

import QtQuick 2.7

Item {
    id: root

    property real value: 4.0
    property real minimumValue: 0.0
    property real maximumValue: 10.0

    width: 200
    height: 40

    QtObject {
        id: internal

        readonly property int scrubberXPosition: _rectangleTrack.width * (root.value / (root.maximumValue - root.minimumValue))
    }

    Rectangle {
        id: _rectangleTrack
        anchors.centerIn: parent
        width: parent.width - _rectangleScrubber.width
        height: 4
        color: "#222222"
    }

    Rectangle {
    	id: _rectangleScrubber
    	anchors.verticalCenter: _rectangleTrack.verticalCenter

    	x: internal.scrubberXPosition

    	width: parent.height / 2
    	height: width
    	radius: width / 2

    	color: "#EEEEEE"

    	border.color: "#DDDDDD"
    	border.width: 2

        MouseArea {
            anchors.fill: parent
            drag.target: parent
            drag.minimumX: 0
            drag.maximumX: _rectangleTrack.width
    	}
    }
}

We have now reduced our API back to the original three properties we wanted:

  • value
  • minimumValue
  • maximumValue

Let's continue building our Slider element.

Whenever a user drags the scrubber, we want the value property to update. We can add this logic directly to the scrubber Rectangle using an onXChanged handler:

...
Rectangle {
    id: _rectangleScrubber
    anchors.verticalCenter: _rectangleTrack.verticalCenter

    x: internal.scrubberXPosition

    onXChanged: {
        root.value = (x / _rectangleTrack.width) * (root.maximumValue - root.minimumValue)
    }

    width: parent.height / 2
    height: width
    radius: width / 2
        
        ...

Now, we run into a similar problem as we did above. This logic should not be in a UI element; but rather a logic block. We now have the QtObject; let's move our logic into there:

QtObject {
    id: internal

    readonly property int scrubberXPosition: _rectangleTrack.width * (root.value / (root.maximumValue - root.minimumValue))

    Connections {
        target: _rectangleScrubber
        onXChanged: {
            root.value = (x / _rectangleTrack.width) * (root.maximumValue - root.minimumValue)
        }
    }
}

Now, use qmlscene to run your program:

file:///qmlguide/examples/QtObject/Slider.qml:18 Cannot assign to non-existent default property

Uh oh! What does this mean?

The issue here is that since QtObject is exactly a QObject registered using qmlRegisterType; as we saw above, it is not prepared for the likeness of QML. QQuickItem derived elements have a data property is set as the default property.

What is a default property? Whenever you use the QML syntax to create an object hierarchy, the child elements have to go somewhere.
The child elements have to be stored somewhere in the parent.

Car {
    Tire { }
    Tire { }
    Tire { }
    Tire { }
}

In this example, the Car element has four child Tire elements. Given that Car is a QQuickItem derived class, the Tire elements are automatically placed into the default property which is data.

Setting the default property in C++

In C++, this is accomplished using Q_CLASSINFO("DefaultProperty", "data") and building up a QQmlListProperty for the data member. You can see how Qt does this for QQuickItem here.

Setting the default property in QML

In QML, this is accomplished in a much easier way. We can simply use the default directive prior to the property name.

Create a new QML file called BaseObject.qml:

BaseObject.qml

import QtQml 2.2

QtObject {
    default property var data
}

Now, any new element that is created within this object instance is automatically placed within the data property.

Update your Slider element to look like this:

Slider.qml

import QtQuick 2.7

Item {
    id: root

    property real value: 5.0
    property real minimumValue: 0.0
    property real maximumValue: 10.0

    width: 200
    height: 40

    BaseObject {
        id: internal

        readonly property int scrubberXPosition: _rectangleTrack.width * (root.value / (root.maximumValue - root.minimumValue))

        Connections {
            target: _rectangleScrubber
            onXChanged: {
                root.value = (x / _rectangleTrack.width) * (root.maximumValue - root.minimumValue)
            }
        }
    }

    Rectangle {
        id: _rectangleTrack
        anchors.centerIn: parent
        width: parent.width - _rectangleScrubber.width
        height: 4
        color: "#222222"
    }

    Rectangle {
    	id: _rectangleScrubber
    	anchors.verticalCenter: _rectangleTrack.verticalCenter

    	x: internal.scrubberXPosition

    	width: parent.height / 2
    	height: width
    	radius: width / 2

    	color: "#EEEEEE"

    	border.color: "#DDDDDD"
    	border.width: 2
    	
        MouseArea {
            anchors.fill: parent
            drag.target: parent
            drag.minimumX: 0
            drag.maximumX: _rectangleTrack.width
    	}
    }
}

Run your code again, and voila! Everything's working great.

There are more touch ups you can do to the Slider element to avoid binding loops. Use the pressed boolean of the MouseArea to avoid recalculating the scrubberXPosition of the scrubber when it is being dragged. When the scrubber is released, the binding of root.value to internal.scrubberXPosition needs to be re-established.

The code in this tutorial is available at the qml.guide GitHub repo.

Niraj Desai

Niraj Desai

UI enthusiast, Qt / QML loyalist, passion for automotive UX – previously at Mercedes-Benz R&D, currently at NIO US.

Read More