QML Singleton Exposes Empty QVariantList for Custom QVector Data
-
I'm working on a modular Qt project using CMake where shared logic lives in a separate module. One of these modules defines a QML singleton (here called MyManager) that internally stores a QVector of a custom type (for example, a Light structure) and exposes this data via a Q_PROPERTY of type QVariantList. In the getter and setter, the QVector is converted to/from a QVariantList.
The custom type is defined as follows (simplified for brevity):
struct Light { uint id; quint16 intensity; QColor color; quint8 gobo; quint8 strobe; quint16 pan; quint16 tilt; bool operator==(const Light &other) const { return id == other.id && intensity == other.intensity && color == other.color && gobo == other.gobo && strobe == other.strobe && pan == other.pan && tilt == other.tilt; } };
The singleton MyManager is declared like this:
MyManager.h#ifndef MYMANAGER_H #define MYMANAGER_H #include <QObject> #include <QVariantList> #include <QQmlEngine> #include "Light.h" class MyManager : public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON Q_PROPERTY(QVariantList lightsList READ lightsList WRITE setLightsList NOTIFY lightsListChanged) public: explicit MyManager(QObject *parent = nullptr); QVariantList lightsList() const; void setLightsList(const QVariantList &list); Q_INVOKABLE void fillTestData(); signals: void lightsListChanged(); private: QVector<Light> m_lightsList; }; #endif // MYMANAGER_H
And in MyManager.cpp, I use helper templates to convert between QVector and QVariantList:
#include "MyManager.h" #include <QDebug> template<typename T> QVariantList toVariantList(const QVector<T>& vector) { QVariantList list; for (const auto &item : vector) { list.append(QVariant::fromValue(item)); } return list; } template<typename T> QVector<T> fromVariantList(const QVariantList &list) { QVector<T> vector; for (const auto &item : list) { vector.append(item.value<T>()); } return vector; } MyManager::MyManager(QObject *parent) : QObject(parent) {} QVariantList MyManager::lightsList() const { return toVariantList(m_lightsList); } void MyManager::setLightsList(const QVariantList &list) { auto converted = fromVariantList<Light>(list); if (m_lightsList == converted) return; m_lightsList = converted; emit lightsListChanged(); } void MyManager::fillTestData() { m_lightsList.clear(); for (int i = 0; i < 5; ++i) { m_lightsList.append({ static_cast<uint>(i), static_cast<quint16>(i * 100), QColor(255, 0, 0), 1, 2, 300, 400 }); } qDebug() << "Filled lights:" << m_lightsList.count(); emit lightsListChanged(); }
In my main application, I register the singleton after calling fillTestData():
#include <QGuiApplication> #include <QQmlApplicationEngine> #include "MyManager.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; MyManager manager; manager.fillTestData(); // Debug output confirms that data is filled // Register as a QML singleton under the module "MyModule" qmlRegisterSingletonInstance("MyModule", 1, 0, "MyManager", &manager); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec(); }
Finally, in QML I access the data like this:
import QtQuick 2.15 import MyModule 1.0 Item { Component.onCompleted: { console.log("Lights List:", MyManager.lightsList) } }
Even though the debug output confirms that the QVector is populated in fillTestData(), the lightsList property in QML is always empty. I’m not sure if this is due to the conversion method, the custom type registration, or some subtle issue with the singleton registration.
Has anyone encountered similar issues exposing a QVector of a custom type via QVariantList in a QML singleton? Any suggestions on what might be wrong or how to correctly expose the data to QML would be greatly appreciated.
Thanks in advance!
-
It looks like your issue might be due to the custom type Light not being registered properly for use in QVariant and QML. To fix this:
Register the Light type: You need to tell Qt how to handle your Light type by registering it with the meta-object system. Add this line in your
main.cpp (before using Light with QVariant or QML):qRegisterMetaType<Light>("Light");
Ensure Light is QVariant-compatible: Make sure Light is properly handled by QVariant. By registering it as mentioned above, Qt will know how to store and retrieve Light in QVariant.
Check QML code: In QML, make sure you're accessing the lightsList property correctly. Here's an example of how you might log each light in the list:Component.onCompleted: { console.log("Lights List:", MyManager.lightsList) MyManager.lightsList.forEach(light => { console.log("Light ID:", light.id) }); }
Debug the data: Double-check that the data is actually getting added to m_lightsList in C++ and that the lightsListChanged() signal is emitted when the data changes.
By making sure Light is registered and QVariant-compatible, your lightsList should show up correctly in QML. -
Hey Shrishashank,
Thanks for the quick reply.
I registered the Light class in my main.cpp with the qRegisterMetaType but still got an empty list.After a bit more debugging I figured out that the object created in my main.cpp and on which I call fillTestData(), was not the same object as the one QML is trying to access (different memory addresses). After calling the function inside of qml on Component.onComplete like this:
Component.onCompleted: { PatchManager.fillTestData(); console.log("PatchManager:", PatchManager); console.log("Lights List:", PatchManager.lightsList) PatchManager.lightsList.forEach(light => { console.log("Light ID:", light.id) }); }
I now do get elements inside of the lightsList but when trying to acces its properties like .id I get Undefined.
Output:
qml: PatchManager: PatchManager(0x16922a9e060) qml: Lights List: [QVariant(Light, ),QVariant(Light, ),QVariant(Light, ),QVariant(Light, ),QVariant(Light, )] qml: Light ID: undefined qml: Light ID: undefined qml: Light ID: undefined qml: Light ID: undefined qml: Light ID: undefined
Do you have any idea why the object created in my main.cpp and in my qml file are not the same object?
And why i cannot access the properties of the Light struct, even though Lights is using just regular Qt/C++ containers?Thanks in advance for helping me out!
-
@Pivit I tried your example but for now I have only this in my
onCompleted
:console.log("Lights List:", MyManager.lightsList) console.log("Lights List length:", MyManager.lightsList.length)
and if I debug from the QML into the C++, I see that the correct manager object is called and that the
lisghtsList
is non-empty:Filled lights: 5 qml: Lights List: [QVariant(Light, ),QVariant(Light, ),QVariant(Light, ),QVariant(Light, ),QVariant(Light, )] qml: Lights List length: 5
I think your issue is that you have not properly exposed
Light
to QML. For a simple struct-type class the easiest thing is to make it aQ_GADGET
to expose the properties to QML:#include <QVariant> struct Light { private: Q_GADGET Q_PROPERTY(uint id MEMBER id) Q_PROPERTY(uint intensity MEMBER intensity) public: uint id; quint16 intensity; // ... same as your code }; Q_DECLARE_METATYPE(Light)
I have only exposed the
id
andintensity
members for now, but it is enough to try it out. I added an extra print in theonCompleted
:console.log("Lights List first id:", MyManager.lightsList[0].id)
The output is now:
Filled lights: 5 qml: Lights List: [Light(0, 0),Light(1, 100),Light(2, 200),Light(3, 300),Light(4, 400)] qml: Lights List length: 5 qml: Lights List first id: 0
-
After some further fiddling around, i've managed to print the correct Light Id's to the console in the onComplete function. Now when trying to actually access this data in a ListView, nothing is showing. It cannot even access how many items are in this list since just showing a white-bordered rectangle for each entry is not showing anything.
ListView { id: listView Layout.fillWidth: true Layout.fillHeight: true clip: true spacing: 5 model: MyManager.lightsList delegate: Rectangle { width: listView.width height: 60 color: "#1c1c1c" radius: 5 border.color: "#ffffff" border.width: 2 anchors.margins: 5 Text { text: modelData.id anchors.left: parent.left anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter color: "white" font.pixelSize: 16 } }
I've figured out that defining the Light struct as a Q_GADGET is not enough for showing it in a ListView since there are no signal/slots available in Q_GADGET.
So i've changed the whole Light struct into a QObject:
class Light : public QObject { Q_OBJECT Q_PROPERTY(uint id READ id WRITE setId NOTIFY idChanged) Q_PROPERTY(quint16 intensity READ intensity WRITE setIntensity NOTIFY intensityChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY(quint8 gobo READ gobo WRITE setGobo NOTIFY goboChanged) Q_PROPERTY(quint8 strobe READ strobe WRITE setStrobe NOTIFY strobeChanged) Q_PROPERTY(quint16 pan READ pan WRITE setPan NOTIFY panChanged) Q_PROPERTY(quint16 tilt READ tilt WRITE setTilt NOTIFY tiltChanged) public: explicit Light(QObject* parent = nullptr) : QObject(parent), m_id(0), m_intensity(0), m_color(Qt::black), m_gobo(0), m_strobe(0), m_pan(0), m_tilt(0) {} uint id() const { return m_id; } void setId(uint id) { if (m_id != id) { m_id = id; emit idChanged(); } } quint16 intensity() const { return m_intensity; } void setIntensity(quint16 intensity) { if (m_intensity != intensity) { m_intensity = intensity; emit intensityChanged(); } } QColor color() const { return m_color; } void setColor(const QColor &color) { if (m_color != color) { m_color = color; emit colorChanged(); } } quint8 gobo() const { return m_gobo; } void setGobo(quint8 gobo) { if (m_gobo != gobo) { m_gobo = gobo; emit goboChanged(); } } quint8 strobe() const { return m_strobe; } void setStrobe(quint8 strobe) { if (m_strobe != strobe) { m_strobe = strobe; emit strobeChanged(); } } quint16 pan() const { return m_pan; } void setPan(quint16 pan) { if (m_pan != pan) { m_pan = pan; emit panChanged(); } } quint16 tilt() const { return m_tilt; } void setTilt(quint16 tilt) { if (m_tilt != tilt) { m_tilt = tilt; emit tiltChanged(); } } // Equality operator comparing each property bool operator==(const Light &other) const { return m_id == other.m_id && m_intensity == other.m_intensity && m_color == other.m_color && m_gobo == other.m_gobo && m_strobe == other.m_strobe && m_pan == other.m_pan && m_tilt == other.m_tilt; } signals: void idChanged(); void intensityChanged(); void colorChanged(); void goboChanged(); void strobeChanged(); void panChanged(); void tiltChanged(); private: uint m_id; quint16 m_intensity; QColor m_color; quint8 m_gobo; quint8 m_strobe; quint16 m_pan; quint16 m_tilt; };
After this change it still prints out the correct stuff in the output log but would still not show anything in the ListView...
qml: MyManager: MyManager(0x2cc521f0fa0) qml: Lights List: [Light(0x2cc5543c410),Light(0x2cc5543c6b0),Light(0x2cc5543c830),Light(0x2cc5543c590),Light(0x2cc5543cbf0)] qml: Light ID: 0 qml: Light ID: 1 qml: Light ID: 2 qml: Light ID: 3 qml: Light ID: 4
Any thoughts?