Written by Bo Thorsen
2016/05/22
In QML, it’s so easy to use properties of components directly, and it’s even the way most examples do it. But here’s my quesion: If we consider this the wrong approach in C++, isn’t it wrong in QML as well? The answer is usually yes.
Here, I’ll show you how I think the best practice of building QML components is. It seems so simple, maybe even trivial. But I have met a lot of code that doesn’t follow it, and seen the problems that follow. QML is still so new, that best practice isn’t common knowledge. And in many cases, best practice haven’t even been discovered yet.
Let me give a quick example that shows the problem: You have created a Button component with all the bells and whistles such a component needs. You chose to build it using a Rectangle, so the border is handled by an existing component. Reuse is always a good and popular choice.
Here is some of the code of how the Button could be done:
Button.qml:
Rectangle {
id: button
border.width: 1
signal clicked()
property alias text: buttonText.text
Text {
id: buttonText
anchors.centerIn: parent
}
...
}
You now declare a Button, set the size and the text, and that’s it. However, you might also want to have a different border, so this is what we do in the UI QML file:
Ui.qml:
Rectangle {
width: 360
height: 360
Button {
anchors.centerIn: parent
text: "Hello World"
width: 100
height: 30
border.width: 2
border.color: "blue"
}
...
Now, here’s the problem: If you decide to expand the Button with other choices of how it can look, you will have a problem. For example, you might want to offer a BorderImage for the way the Button looks. But with the current use of a Rectangle, you can’t do this without modifying the code that uses this Button.
This is exactly the problem encapsulation solves: It allows you to make local changes to your component without modifying all the code that uses it.
The simple solution of the Button is to make it an Item that has a Rectangle. In OO terms, this changes from inheritance to aggregation.
My solution for creating QML components is to nearly always base them on Item, and expose whatever properties I want. The interface to my components is the sum of properties, functions and signals. How I choose to implement the component itself is an implementation detail, and should not be available to other components.
For a simple example, suppose we have a system where a bunch of Text items should always follow a shared style. If the style changes, all text must follow. In fact, the only thing the users of the text item can set, is the text itself.
This could clearly be done with Text directly:
StyledText.qml:
Text {
// Event handlers to set the Style parts here:
...
}
This a valid choice, we simply need to tell ourselves that we shouldn’t set the color, size etc. directly, but allow the central style code to set it. And this is of course doomed. Some day, we hire a new coder, or forget ourselves. Whatever the reason, we will break the rules.
The proper solution is to encapsulate the component and hide the implementation behind the Item:
StyledText.qml:
Item {
property alias text: textItem.text
Text {
anchors.fill: parent
// Event handlers to set the Style parts here:
...
}
}
It seems so simple, but I have seen a bunch of QML code that doesn’t do this. Every time you start modifying the code of components, you wonder what you break. And this only becomes much more valuable with real components that have much more complexity than the simple example code I have here.
If you follow this pattern instead, you will create robust and encapsulated components with no properties accessible by accident.