Laying Out Components with Qt Quick and JSON Factory Design Techniques - Part 2
I was tasked to come up with a simple architecture for remote real time instantiation of arbitrary QML components. I’ve split my findings into 3 blog entries, each one covering a slightly different topic. Part 1 focuses on the software design pattern used to dynamically instantiate components. Part 2 shows how to layout these dynamic components by incorporating QML’ s positioning and layout APIs. The last entry, consisting of Parts 3 and 4, addresses the anchors API and important safety aspects.
This is Part 2: Laying Out Components with Qt Quick and JSON
Now that we know how to instantiate trees of components, the next thing we should be able to do is lay them out on screen. If you’ve watched our Introduction to QML series, you’ll know Qt provides 4 ways to place objects in QML:
- Using the point coordinate system, where we pass x and y coordinates.
- Using Item Positioners
- Using Qt Quick Layouts, which are like positioners but can also resize their items using attached properties
- Using anchors, which allows us to link the item’s center, baseline, and edges, to anchors from other items.
For maximum flexibility, the factory design approach should support all four methods of component placement.
- 1. The coordinate system we get almost for free. To use it, we can assign the values for x and y from onItemChanged in the instantiator Loader, as seen in Part I:
// Root component of the factory and nodes
Component {
id: loaderComp
required property var modelData
Loader {
id: instantiator
// ...
onItemChanged: {
// ...
if (typeof(modelData.x) === "number")
loaderComp.x = modelData.x;
if (typeof(modelData.y) === "number")
loaderComp.y = modelData.y;
// ...
}
}
}
- 2. & 3…Item Positioners and Qt Quick Layouts work in very similar ways. So, let’s have a look at how to approach Qt Quick Layouts, which is the most sophisticated of the two. Let’s remember how Qt Quick Layouts are commonly used in the first place: First, we import QtQuick.Layouts. Then, instantiate any of these Layout components: https://doc.qt.io/qt-6/qtquick-layouts-qmlmodule.html, and set dimensions to it, often by means of attached properties (https://doc.qt.io/qt-6/qml-qtquick-layouts-layout.html#attached-properties). For the outermost Layout in the QML stack, we might use one of the previous APIs to achieve this. Here’s a simple example for how that looks:
import QtQuick.Layouts
Item {
ColumnLayout {
Button {
text: "1st button"
Layout.fillWidth: true
}
Button {
text: "2nd button"
Layout.fillWidth: true
}
}
}
Now, for the Layouts API to work in our factory, the recursion described in Part I must be in place.
In addition to that, we need to take into account a property of the Loader object component: Loader inherits from Item. The items loaded by the Loader component are actually children of Loader and, as a result, must be placed relative to the loader, not its parent. This means we shouldn’t be setting Layout attached properties onto the instantiated components, but instead should set them on the Loader that is parent to our item, IDed as instantiator.
Here’s an example of what the model could define. As you can see, I’ve replaced the dot used for attached properties with an underscore.
property var factoryModel: [
{
"component": "ColumnLayout",
"children": [
{
"component": "Button",
"text": "1st button",
"Layout_fillWidth": true
},
{
"component": "Button",
"text": "2nd button",
"Layout_fillWidth": true
}
]
}
]
Here’s what we will do, based on that model:
// Root component of the factory and nodes
Component {
id: loaderComp
Loader {
id: instantiator
required property var modelData
sourceComponent: switch (modelData.component) {
case "Button":
return buttonComp;
case "Column":
return columnComp;
case "ColumnLayout":
return columnLayoutComp;
}
onItemChanged: {
// Pass children
if (typeof(modelData.children) === "object")
item.model = modelData.children;
// Layouts
if (typeof(modelData.Layout_fillWidth) === "bool") {
// Apply fillWidth to the container instead of the item
instantiator.Layout.fillWidth = modelData.Layout_fillWidth;
// Anchor the item to the container so that it produces the desired behavior
item.anchors.left = loaderComp.left;
item.anchors.right = loaderComp.right;
}
// Button properties
switch (modelData.component) {
case "Button":
// If the model contains certain value, we may assign it:
if (typeof(modelData.text) === "string")
item.text = modelData.text;
break;
}
// ...
}
}
}
As you can see, the attached property is set on the instantiator which acts as a container, and the component item is then anchored to that container. I do not simply anchor all children to fill the parent Loader because different components have different default sizes, and the Loader is agnostic of its children’s sizes.
Here’s the implementation for the Button, Column, and ColumnLayout components. Feel free to modify the JSON from factoryModel to use Column instead of ColumnLayouts, or any componentizations that you implement yourself.
Component {
id: buttonComp
Button {
property alias children: itemRepeater.model
children: Repeater {
id: itemRepeater
delegate: loaderComp
}
}
}
Component {
id: columnComp
Column {
property alias model: itemRepeater.model
children: Repeater {
id: itemRepeater
delegate: loaderComp
}
}
}
Component {
id: columnLayoutComp
ColumnLayout {
property alias model: itemRepeater.model
children: Repeater {
id: itemRepeater
delegate: loaderComp
}
}
}
- 4. Anchors will be covered in the next entry. Some complications and security implications arise due to the fact that anchors can point to IDs, which is why I think they deserve their own separate article.
To summarize, we can dynamically attach attributes to our dynamically instantiated components to configure QML layouts. It’s important to keep in mind that the Loader will hold our dynamic component as its children, so we must assign our dimensions to the Loader and have the child mimic its behavior, possibly by anchoring to it, but this could also be done the other way around.
In the next entry I’ll be covering how to implement anchors and the security implications for which dynamically instantiating components from JSON might not be a good idea after all. Our previous entry is Recursive Instantiation with Qt Quick and JSON.
Reference
- The Loader element is the key part of this entry. To learn more about it, I recommend you watch “Introduction to Qt / QML (Part 21) – The Loader Element” from our Introduction to QML training series: https://www.youtube.com/watch?v=nteJeojg07k
If you like this article and want to read similar material, consider subscribing via our RSS feed.
Subscribe to KDAB TV for similar informative short video content.
KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.