Creating QML Properties Dynamically at Runtime from C++

Creating QML Properties dynamically at runtime. 9 minutes read

I have been searching for an adequate solution for this problem for a few years and recently got a good hint for closing this open loop at the Qt World Summit 2017.

In fact, I have not found only one, but two new solutions for this problem.

I explain all three solutions, the quick fix, a better approach and my current state of the art, in this blog post.

UPDATE on December 8th, 2017: Grecko gave me a hint to take a look at QQmlPropertyMap. So now we have four solutions to solve the problem.

Introduction

You may be familiar with the Q_PROPERTY macro and defining properties via the property keyword inside QML. However, in some situations, it is useful to create the properties for QML item at runtime.

However, this is not as trivial as it seems. QML is just not designed to create properties dynamically at runtime.

There is no public API exposing something like addProperty in QML or C++.

In fact, Qt’s property system supports creating dynamic properties at runtime. However, these properties (created with setProperty) are ignored by QML.

Use Case

The QtQuickVcp remote UI framework Machinekit syncs the machine status via the Machinekitalk middleware.

One of the core concepts in Machinetalk is the full and incremental update approach. This means the Machinetalk server sends a complete status message once and consequent updates as incremental update messages.

In QtQuickVcp, we represent the application status as a tree-like structure. The Protobuf definitions of Machinetalk specify how this tree-like structure looks like – so it makes sense to auto-generate this structure from the Protobuf message descriptor.

One could argue here that a tree model would fit this application. However, the use case does not exactly match a scenario where I would use a tree model.

Let’s say we have Text element in QML where we want to show the position value of the A axis from Machinekit.

Text {
    text: status.motion.axis[0].position.toFixed(3)
}

In another part of the application we need the enabled field of the motion object:

CheckBox {
    checked: status.motion.enabled
}

As we can see, the status object contains very different items and the data is not used in a tree view.

In QML, a JSON object feels very natural to represent our status data.

The Quick Fix – QJsonObject

The easiest way to create nested QML items with varying properties at runtime is to use the JavaScript engine that comes with QtQuick.

For this purpose, we use the QJsonObject which the QmlEngine supports per default.

QJsonObject is the C++ representation of a JSON object with Qt. Therefore, it supports all the features a native JavaScript object does inside a QML application, including dynamic object properties.

Let’s take a look at this solution.

In our C++ object we create a Q_PROPERTY of the type QJsonObject.

Q_PROPERTY(QJsonObject data READ data WRITE setData NOTIFY dataChanged)

We create our JSON object like this.

QJsonObject jsonObject;

// property
{
    QJsonValue jsonValue = 0;
    jsonObject.insert("id", jsonValue);
    jsonObject.insert("name", "");
}

// nested object
{
    QJsonObject object;
    object.insert("number", 0);
    object.insert("country", "");
    jsonObject.insert("phone", object);
}

To update the object we can use the following code snippet.

// partial property update
{
    jsonObject.insert("id", 10);
}

// partial update of nested object
{
    QJsonObject object = jsonObject.value("phone").toObject();
    object.insert("number", 12345);
    jsonObject.insert("phone", object);
}

However, it is important to note at this point, that properties of JavaScript objects aren’t the same as the properties of QML components. To make it short, they don’t support the property binding which makes QML such a great programming language.

But the QmlEngine helps us here. If you create a property binding which references a property of a JavaScript object the QmlEngine automatically resolves the root object and property. This means when you update the root object your property binding is also re-evaluated.

This comes at a high cost since every single update to the object tree structure triggers a re-evaluation of all our property bindings. As you can imagine, this leads to low performance when we update the message structure frequently.

A better Solution – QmlEngine

Another way to create new QML properties came to my mind during the KDAB training day of the Qt World Summit 2017.

By creating a custom Loader component through subclassing the QQuickItem class, one can interpret QML code at runtime.

So my idea was to generate a QML code including the custom properties. This way I can add and change properties at runtime.

customloader.h:

#ifndef CUSTOMLOADER_H
#define CUSTOMLOADER_H

#include <QObject>
#include <QQuickItem>
#include <QQmlParserStatus>

class CustomLoader
        : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(QObject* item READ item NOTIFY itemChanged)

public:
    CustomLoader(QQuickItem *parent = nullptr);

    void componentComplete();

    QObject* item() const;

    Q_INVOKABLE void load();

private:
    QObject* m_item;

    QObject *createCustomComponent();

signals:
    void itemChanged();
};


#endif // CUSTOMLOADER_H

customloader.cpp

#include "customloader.h"
#include "dynamicobject.h"
#include <QQmlComponent>
#include <QQuickItem>
#include <QQmlListProperty>
#include <QtQml>

CustomLoader::CustomLoader(QQuickItem *parent)
    : QQuickItem(parent)
    , m_item(new QObject(this))
{
}

void CustomLoader::componentComplete()
{
    m_item->deleteLater();
    m_item = createCustomComponent();
    emit itemChanged();
    emit otherChanged();
}

QObject *CustomLoader::item() const
{
    return m_item;
}

QObject *CustomLoader::createCustomComponent()
{
    QQmlEngine *engine = qmlEngine(this);

    if (engine == nullptr) {
        qWarning() << "could not detect qml engine";
        return nullptr;
    }

    QQmlComponent component(engine);
    component.setData("import QtQuick 2.0\n"
                      "QtObject {\n"
                        "property int id: 0\n"
                        "property string name: \"\"\n"
                        "property QtObject phone: QtObject {\n"
                            "property int number: 0\n"
                            "property string country: \"\"\n"
                        "}\n"
                      "}", QUrl());
    auto childItem = component.create();
    if (childItem == nullptr) {
        qCritical() << component.errorString();
        return nullptr;
    }
    childItem->setParent(this);
    return childItem;
}

To update the newly create QML item we can use:

// partial property update
{
    m_item->setProperty("id", 10);
}

// partial update of nested object
{
    auto phone = qvariant_cast<QObject *>(m_item->property("phone"));
    phone->setProperty("number", 12345);
}

The good thing about this approach is that the QML property bindings work as expected. You also can see that we don’t need to re-assign the phone object to the phone property.

The downside of this approach is that we rely on string-magic to create our QML item. This is a somewhat cumbersome since we need to care about closing parentheses and correct naming.

Note that a special bonus of the QJsobObject approach was that we could create properties with numbers as names, e.g. 0 or 1. This was very useful to fake indexes for array-like objects.

However, performance should be a lot better for big objects, since we don’t need to update the complete object tree if we just need to update a single property.

Dealing with the Root Cause of the Problem – QMetaObject

Since I was not a hundred percent satisfied with the QQmlEngine approach I researched deeper to find the root cause of the problem.

At the QtCon 2016, I attended a talk about the Qt SCXML state machine. I noticed that the QML implementation of the state machine generated properties at runtime when loading SCXML files.

That was exactly what I needed in my application. A Qt developer pointed out to me that the Qt SCXML state machine relies heavily on Qt MetaObject magic.

So I decided to take a deeper look at QMetaObject and how the QML engine works internally.

During my research, I came across a talk by Volker Krause from the Qt Developer Days 2014 explaining how to do what moc does from C++ at runtime.

This talk opened my mind, I finally understood how the Qt MetaObject system works and how to create dynamic meta-objects.

Every QObject derived class must implement the functions metaObject, qt_metacall and qt_metacast defined in the Q_OBJECT macro.

These methods are usually generated by the moc tool from the class header file and stored in the moc_<filename>.cpp source files.

If you ever wondered why you need to add the HEADERS directive qmake you now know why. When you remove the class header file from HEADERS you get compiler errors that the above-mentioned functions are missing.

So what do we need to do in order to create our properties at runtime? Yes – build our own QMetaObjects.

For this purpose, I wrote a new DynamicObject class with a addProperty function. To fully understand the class, I recommend watching Volker Krauses talk on YouTube.

dynmaicobject.h

#ifndef DYNAMICOBJECT_H
#define DYNAMICOBJECT_H

#include <QObject>
#include <QVariant>

class DynamicObject : public QObject
{
    Q_OBJECT
public:
    explicit DynamicObject(QObject *parent = nullptr);

    ~DynamicObject();

    /** Adds a new property to the object, must be called before ready(). */
    void addProperty(const QByteArray &name, const QByteArray &type, const QVariant &value);

    /** Marks the object as ready and creates the metaObject. */
    void ready();

signals:

public slots:

private:
    struct DynamicProperty {
        QByteArray name;
        QByteArray type;
        int typeId;
        QVariant variant;
    };

    QMetaObject *m_metaObject;
    std::vector<DynamicProperty> m_properties;
};

#endif // DYNAMICOBJECT_H

and the source file
dynamicobject.cpp

#include "dynamicobject.h"
#include <private/qmetaobjectbuilder_p.h>
#include <cstring>
#include <QtDebug>
#include <QLoggingCategory>

Q_LOGGING_CATEGORY(loggingCategory, "DynamicObject");

DynamicObject::DynamicObject(QObject *parent)
    : QObject(parent)
    , m_metaObject(nullptr)
{
}

DynamicObject::~DynamicObject()
{
    free(m_metaObject);
}

void DynamicObject::addProperty(const QByteArray &name, const QByteArray &type, const QVariant &value)
{
    if (m_metaObject != nullptr) {
        qCWarning(loggingCategory) << "Can't add property after calling ready()";
        return;
    }

    m_properties.emplace_back(DynamicProperty{name, type, QMetaType::type(type), value});
}

void DynamicObject::ready()
{
    if (m_metaObject != nullptr) {
        qCWarning(loggingCategory) << "ready() should be called only once.";
        return;
    }

    QMetaObjectBuilder builder;
    builder.setClassName("DynamicObject");  // TODO: find out if it is legit to have single class name
    builder.setSuperClass(&QObject::staticMetaObject);

    for (const auto &dynamicProperty: m_properties) {
        auto property = builder.addProperty(dynamicProperty.name, dynamicProperty.type);
        property.setWritable(true);
        auto signal = builder.addSignal(dynamicProperty.name + "Changed()");
        property.setNotifySignal(signal);
    }

    m_metaObject = builder.toMetaObject();
}

const QMetaObject* DynamicObject::metaObject() const {
    return m_metaObject;
}

int DynamicObject::qt_metacall(QMetaObject::Call call, int id, void **argv)
{
    const int realId = id - m_metaObject->propertyOffset();
    if (realId < 0) {
        return QObject::qt_metacall(call, id, argv);
    }

    if (call == QMetaObject::ReadProperty) {
        const auto &property = m_properties.at(static_cast<size_t>(realId));
        QMetaType::construct(property.typeId, argv[0], property.variant.data());
    }
    else if (call == QMetaObject::WriteProperty) {
        auto &property = m_properties.at(static_cast<size_t>(realId));
        property.variant = QVariant(property.typeId, argv[0]);
        *reinterpret_cast<int*>(argv[2]) = 1;  // setProperty return value
        QMetaObject::activate(this, m_metaObject, realId, nullptr);
    }
    else {
        // not handled
    }

    return -1;
}

void* DynamicObject::qt_metacast(const char *name)
{
    if (strcmp(name, m_metaObject->className()) == 0) {
        return this;
    }
    else {
        return QObject::qt_metacast(name);
    }
}

You can find the complete source in the QtQuickVcp source tree.

So inside the class, we have our own representation of a property called DynamicProperty. The addProperty function adds a new property to our properties list.

When done setting up our dynamic object we call ready to create the QMetaObject. For this purpose, we use the QMetaObjectBuilder included in the private Qt Core API.

qt_metacall() contains the logic required by Qt’s meta-object system to match property ids with the underlying QVariant values.

qt_metacast() just does most basic stuff, no fancy casting logic required here.

Let’s take a look at how to use the DynamicObject in our code.

To create the object:

auto object = new DynamicObject(parent);

// property
{
    object->addProperty("id", "int", QVariant::fromValue(0));
    object->addProperty("name", "QString", QVariant::fromValue(QString("")));
}

// nested object
{
    QObject *subObject = new DynamicObject(object);
    subObject->addProperty("number", "int", QVariant::fromValue(0));
    subObject->addProperty("country", "QString", QVariant::fromValue(QString("")));
    subObject->ready();
    object->addProperty("phone", "QObject*", QVariant::fromValue(subObject));
}

object.ready(); // finalize the QMetaObject

As we can see, this enables us to create new properties very conveniently at runtime. The only non-static thing left here is the type names required by the QMetaObject system.

Updating the object properties works similar to the QQmlEngine approach.

// partial property update
{
    m_item->setProperty("id", 10);
}

// partial update of nested object
{
    auto phone = qvariant_cast<QObject *>(m_item->property("phone"));
    phone->setProperty("number", 12345);
}

As mentioned earlier, one advantage of the QMetaObject approach for creating dynamic properties at runtime is that we also can create properties with non-QML conform names.

For example:

object->addProperty("0", "int", QVariant::fromValue(0));

This allows us to access the property from QML by using:

property int foo: object[0]

which is very convenient for index-based access to array-like structures, such as for example a position vector.

The big upside of this approach is that property bindings work perfectly fine. Moreover, we can make use of the complete variety of types available in the Qt meta-object system, plus we don’t need to rely on cumbersome string-building magic.

The downsides of this approach for creating dynamic properties include that we rely on including private Qt headers, which gives us a big fat warning when compiling the project that this may break binary API compatibility to other Qt versions. Moreover, we need to add an additional DynamicObject class to our project.

If you need more advanced usage examples such as how to use create dynamic lists, please take a look at the QtQuickVcp source code on Github.

An even better Solution: QQmlPropertyMap

Grecko pointed out another solution: using QQmlPropertyMap. And indeed, this class provides a similar interface to the DynamicObject class presented in the previous chapter.

So for most of your applications, I suggest choosing this approach over the custom QMetaObject method.

First of all, you don’t need to use any private Qt APIs, which ensures binary compatibility with future Qt 5 versions.

Moreover, you don’t need to write your own DynamicObject class, so this method is far simpler.

Let’s take a look at how to use the QQmlPropertyMap.

To create the object:

auto object = new QQmlPropertyMap(parent);

// property
{
    object->insert("id", QVariant::fromValue(0));
    object->insert("name", QVariant::fromValue(QString("")));
}

// nested object
{
    auto subObject = new QQmlPropertyMap(object);
    subObject->insert("number", "int", QVariant::fromValue(0));
    subObject->insert("country", QVariant::fromValue(QString("")));
    subObject->ready();
    object->insert("phone", QVariant::fromValue(subObject));
}

Updating the object properties works exactly like the DynamicObject approach.

// partial property update
{
    m_item->setProperty("id", 10);
}

// partial update of nested object
{
    auto phone = qvariant_cast<QObject *>(m_item->property("phone"));
    phone->setProperty("number", 12345);
}

When you take at the source code of the QQmlPropertyMap class you will see that it makes of the private QMetaObjectBuilder class.

Additionally, I could verify that numbered properties (e.g. “0”) work here as well. In the QtQuickVcp I just needed to replace six lines to make everything work with QQmlPropertyMap.

Conclusion

In this article, you learned about the three ways I discovered to create QML properties at runtime from C++:

  • Using QJsonObject
  • Using QQmlEngine
  • Using QMetaObject
  • Using QQmlPropertyMap

All four approaches have their pros and cons and I recommend you to review them carefully for your application.

If you find any potential problems or if you have additional ideas on how to create QML properties dynamically please add your thoughts in the comments section.

I hope you have enjoyed reading this blog post and if so please subscribe and share it with your friends.

Your
Machine Koder

Spread the love
  • 3
    Shares

3 thoughts on “Creating QML Properties Dynamically at Runtime from C++”

    1. Thank you for pointing this out. I was not aware of `QmlPropertyMap`.

      I quickly verified if `QmlPropertyMap` would be usable for this job and yes it is! Basically, it can be used in a similar fashion as the `DynamicObject`, I will update the blog post shortly.

    2. `QMetaObjectBuilder` is the only easy way to build a `QMetaObject` at runtime. One could also subclass `QMetaObject` and override the `property` and `propertyCount` methods. Moreover, it looks like the `QMetaProperty` also has no public constructor, so this means you would need to subclass this class too.

Leave a Reply

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