Skip to content

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:

  1. Using the point coordinate system, where we pass x and y coordinates.
  2. Using Item Positioners
  3. Using Qt Quick Layouts, which are like positioners but can also resize their items using attached properties
  4. 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

About KDAB

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.

Categories: KDAB Blogs / KDAB on Qt / QML / Qt

Tags:
Leave a Reply

Your email address will not be published. Required fields are marked *