Sign up for the KDAB Newsletter
Stay on top of the latest news, publications, events and more.
Go to Sign-up
Javier Cordero
16 May 2024
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:
For maximum flexibility, the factory design approach should support all four methods of component placement.
// 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;
// ...
}
}
}
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
}
}
}
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.
About KDAB
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
Stay on top of the latest news, publications, events and more.
Go to Sign-up
Upgrade your applications from Qt 5 to Qt 6 with KDAB’s migration services. Get a free migration assessment and join a hands-on workshop to prepare your team for a successful transition!
Learn more