c++qtqmlqpropertyqgadget

How to expose a pointer to a Q_GADGET to QML through a Q_PROPERTY


I have a Q_GADGET MyGadget defined in a file mygadget.h

#include <QObject>

class MyGadget {
    Q_GADGET
    Q_PROPERTY(int value READ value CONSTANT)

public:
    MyGadget() = default;
    MyGadget(int i) 
        : _value{i}
    {
    }

    int value() const
    {
        return _value;
    }

private:
    int _value{0};
};
Q_DECLARE_METATYPE(MyGadget)
Q_DECLARE_METATYPE(MyGadget*)

and a Context class that holds an instance of MyGadget and exposes a pointer to it to QML via a Q_PROPERTY:

#include <QObject>
#include "mygadget.h"

class Context : public QObject
{
    Q_OBJECT
    Q_PROPERTY(MyGadget* gadget READ gadget CONSTANT)
public:
    explicit Context()
        : QObject{nullptr}
     {
     }

    MyGadget* gadget() {
        return &_gadget;
    }
private:
    MyGadget _gadget{4};
};

An instance of Context is created in main and exposed to QML as a context property:

#include <QGuiApplication>
#include <QQuickView>
#include <QString>
#include <QQmlContext>

#include "context.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView view;
    Context c;

    // register MyGadget
    qmlRegisterUncreatableType<MyGadget>("Test", 1, 0, "MyGadget", "");
    qRegisterMetaType<MyGadget*>(); // <- removing this doesn't change anything

    // make Context instance a context propery
    view.rootContext()->setContextProperty("context", &c);

    // show QML
    view.setSource(QUrl{QStringLiteral("qrc:/main.qml")});
    view.show();

    return app.exec();
}

The QML file used with this is

import QtQuick 2.5
import Test 1.0

Rectangle {
    height: 600
    width: 800
    visible: true

    Text {
        text: qsTr("Hello World " + context.gadget.value)
        anchors.centerIn: parent
    }
}

Everything compiles fine, but when running it no text isn't shown and QML emits a warning

qrc:/main.qml:9: TypeError: Cannot read property 'value' of null.

If I remove the call to qmlRegisterUncreatableType<MyGadget> in main and the corresponding import Test 1.0 in the QML file, the text "Hello World undefined" is shown instead.

The only way I got it to print "Hello World 4" as expected is to have Context::gadget return a copy of the stored MyGadget object instead of a pointer to it, or to make MyGadget a Q_OBECT instead. But both of these are not viable options in my real application, since I need reference semantics here, but in other places I'd also like to have value semantics for the class corresponding to MyGadget in this example.

How can I get QML to read the correct property value?


Solution

  • Gadgets are fundamentally limited by design, you cannot work on a pointer basis with them from QML, you have to work with instances, which also implies passing and returning by value. Which also means any manipulations you make are applied to the copy, not the actual object you intended.

    There is a way to work around that, by using PIMPL, and essentially have the gadget object be a pointer, that is, it only has a single member variable that is a pointer to the actual object data implementation, which is a separate object.

    This makes the copying of the gadget object very cheap, since it is just a pointer, and all copies refer to the same object data so your changes will not be lost.

    Update:

    At any rate, if your gadget property is simply an int or some other primitive as stated in your comment, then have it as an int property instead. You can have properties with notifications, QML will whine that they don't have notifications if you use them in bindings, but you can simply pass a single dummy signal that is never emitted and you are set.

    And even if you go for PIMPL, it is not necessary for to allocate dynamically on a per-primitive basis or at all. The pointer doesn't care what it points to, unless there is danger for the memory to become invalid, and the pointer - dangling.