Send your request Join Sii

In this article, I will address the problem of preparing a tree model for any data containing mixed types and show you how to solve it.

Problem

The generic problem is to prepare a tree model for any data containing mixed types. To make the example more appealing let’s assume we want to read JSON data from the backend and display it on the GUI. Moreover, it would be nice to have the possibility to collapse all lists and objects. JSON could be constructed in any way so the app should be flexible. Properties could have different names which are unknown.

Preconditions

  • Qt 6.2,
  • QML frontend,
  • JSON string in the backend.

Ideas

If there would be no preconditions an option would be to use QTreeView class, but there are projects that do not use widgets. For GUI we would go with QML, and for the model, there are still some options. If we are open for an MIT-licensed code there is something like JSONListModel (QML) or QJsonModel (cpp). But what if we are using only pure qt libs? It’s not a simple list model so initial thoughts could lead to the cpp model, like using an abstract interface of QAbstractItemModel. It will go well with TreeView but after Qt 6.3 or before 6.0, since there is a gap in implementation.

But this model sounds like a solid cannon that will grow together with a couple of classes down on the backend side. Moreover, TreeView is not available in our case (Qt 6.2). Maybe the model is not that complicated and could be handled on the QML side. Let’s try this approach.

Solution

import QtQuick

ListModel {
    id: root

    function createAndLoadComponent(item) {
        var newObj = Qt.createComponent("CustomDataModel.qml").createObject(root)
        newObj.parseObject(item)
        return newObj
    }

    function extractValue(value) {
        if (Array.isArray(value))
            return parseArray(value)
        else if (value.constructor === ({}).constructor)
            return createAndLoadComponent(value)
        else
            return value
    }

    function parseArray(value) {
        var arr = []
        for (var i = 0; i < value.length; i++)
            arr.push({[""]: extractValue(value[i])})
        return arr
    }

    function parseObject(json) {
        for (var key in json)
            root.append({[key]: extractValue(json[key])})
    }
}

First step – CustomDataModel.qml creating

Let’s create CustomDataModel.qml which will represent our model. It will be based on ListModel. JSON file will be loaded into the front end as a QString. From there it will be parsed to the JavaScript value or object described by the string and then passed to the parseObject function inside CustomDataModel.

It could be an array; it could be a simple key value or it could be also an object with many properties. We need to identify what is it for each key. Inside a for loop there is a call to extractValue where check: Array.isArray that will do just fine. The action name is self-explanatory. If it’s not the case, the second task is to distinguish if the value is an object. It is done with the usage of the constructor property. If we have an object, we would need to read all properties of it and add those to the model.

Here a recursive approach is taken by creating the object of a parent class and calling parseObject again. If all nested parameters are loaded, we can go further. Append in ListModel needs jsobject in the parameter, and in our example, we need to pass a key in there. To achieve that we are using syntax: append({[key]: value}). If the item is just a value the simple append to the ListModel would be enough.

The parseArray function

Now let’s get back to the array case and investigate the parseArray function. Elements here are added in a loop. The length parameter is used to determine how many elements are there and before appending to the ListModel, an array is created where all items are pushed. Each element of an array could be an object, or value, or… an array so here we go again with the recursive approach. It is done with the help of the function extractValue . And that’s it. The whole model class has less than 35 lines of code. I would say it is pretty concise.

CustomDataView.qml creating

Now to the view, let’s create CustomDataView.qml

import QtQuick
import QtQuick.Controls

ListView {
    id: root
    property var customModel
    function createObjectElement(prop, rootId) {
        return Qt.createQmlObject(`
              import QtQuick
              import QtQuick.Controls
              import QtQuick.Layouts
              GridLayout {
                  property alias childsId: childsId
                  columns: 2
                  Button { text: childsId.visible ? "-" : "+"
                           onClicked: childsId.visible = !childsId.visible }
                  Text { text: "${prop}:" }
                  Item {}
                  ColumnLayout { id: childsId }}`, rootId)
    }

    function prepareNestedElements(prop, rootId, value, ifArray) {
        var child = createObjectElement(prop, rootId)
        for (var i = 0; i < value.count; i++)
            createView(value.get(i), child.childsId, ifArray ? null : i)
    }

    function createView(item, rootId, index) {
        let count = 0;
        for (var prop in item)
            if ((index !== undefined && index !== null && index !== count++) || !item[prop]) {
            } else if (item[prop] instanceof ListModel && !(item[prop] instanceof CustomDataModel)) {
                prepareNestedElements(prop, rootId, item[prop], true)
            } else if (item[prop].constructor === ({}).constructor) {
                prepareNestedElements(prop, rootId, item[prop], false)
            } else {
                var middle = prop === "" ? "" : ": "
                Qt.createQmlObject(`
                    import QtQuick
                    Row {
                        Item { width: 20; height: 10 }
                        Text { text: "${prop}${middle}${item[prop]}" }}`, rootId
                )
            }
    }
    model: customModel
    delegate: Column {
        id: colId
        Component.onCompleted: root.createView(customModel.get(index), colId)
    }
}

It is based on the ListView component and has a simple Column delegate which will be dynamically created in Component.onCompleted, where simply the createView function is called.

This function goes through all properties of an item and checks what it is. It will decode once created model items are. So, starting from the bottom we have created plain Text: key: value with some indentation done by the Item. Going up there is again check if the constructor property points to the object. In this case, the prepareNestedElements function is called. It’s because an object can have multiple properties so it needs to be nested.

CreateObjectElement function

Moving inside this function, another one is called: createObjectElement. It will create a QML object by calling Qt.createQmlObject and putting it inside rootId which is the param here. The object will be created based on GridLayout, and inside there will be a button to collapse nested items. The alias to the childsId is so the access is provided to outside. In that way, we add new elements inside, directly to childsId.

Going back to prepareNestedElements, after creating the object we need to fill it in. So recursive call to create a view will take care of all that’s inside: arrays, objects, and simple elements. Mentioned child.childsId is passed as a param, so it will be created inside that ColumnLayout which is collapsible by the button. One important thing worth mentioning here is the third param in createView.

If it’s not an array but an object, we are passing the index here. It will extract the correct parameter from the model because each item has a different key and on the other hand, all items in CustomListModel has all parameter names present. In simple words, if we add item[“a”] and item[“b”] to a ListModel, each item will have both params, one with the value and the other empty. So, from each one, we would like to extract only valid values.

An array cases

The one case that is left is an array. To identify this, we can check if the value is ListModel and not a CustomListModel. Reason for that we can find in a documentation of a ListModel in a get function: “properties of the returned object that are themselves objects will also be models”. If we check the type, it is QQmlListModel. And since we do not explicitly make it CustomListModel we have a solution how to distinguish that we in fact have an array.

Summary

A similar problem occurred in real life project I had. The thing worth mentioning is that from Qt6.3 we have a TreeView that could ease some things out. All of this is like 100 lines of code in QML. Sounds good to me. Presented code example could be extended using dynamicRoles property from ListModel to also support property names with different types, however according to documentation it could hit performance so it’s 4-6 times slower.

If this would not be JSON but some other tree like data structure it would be clearer that this model has to be custom, and with that in mind, I hope this article could show you the possible approach in such a situation. The same goes for the QML view, which is more popular nowadays and embedded-friendly. The whole example would be attached somewhere, so go ahead and check it out.

Application files

Sources

***

If you are interested in the topic of QML, read also the author’s other article.

5/5 ( votes: 2)
Rating:
5/5 ( votes: 2)
Author
Avatar
Tomasz Broniszewski

The Graduate of the Lublin University of Technology. Software engineer with 9 years of experience and almost 8 years with C++ & Qt/QML in commercial projects. If there is some spare time between sleep, work and parenting, he chooses powerlifting training.

Leave a comment

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

You might also like

More articles

Don't miss out

Subscribe to our blog and receive information about the latest posts.

Get an offer

If you have any questions or would like to learn more about our offer, feel free to contact us.

Send your request Send your request

Natalia Competency Center Director

Get an offer

Join Sii

Find the job that's right for you. Check out open positions and apply.

Apply Apply

Paweł Process Owner

Join Sii

SUBMIT

Ta treść jest dostępna tylko w jednej wersji językowej.
Nastąpi przekierowanie do strony głównej.

Czy chcesz opuścić tę stronę?