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 theminimumX
andmaximumX
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:
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.