How to pass container with custom objects from C++ to QML?

Viewed 1808

I know something about C++ but I'm pretty new to QML.
I want to pass multiple custom C++ objects in a container to QML but I am having trouble doing so.
The code provided is narrowed down to the basics.

I can communicate from and to QML in a single object which I register with setContextProperty, this works fine. But if I try to do so with a QHash, then I get an error:

‘QVariant::QVariant(void*)’ is private within this context‘

Maybe you can help me out or give me a direction? Thanks a bunch.

Update:
Thanks derM, here is my try at it:

I have added: Q_DECLARE_METATYPE(MyData); at the end of the header file. I have changed the container to a QVariantMap.
If I try: QVariant qvtest1(test1); I get the error:

no matching function for call to ‘QVariant::QVariant(MyData&)’

However this works:

QVariant qvtest1, qvtest2;  
qvtest1.setValue(test1);  
qvtest2.setValue(test2);  

But again I get an error with: setContextProperty("mymap", &mymap); error:

calling a private constructor of class 'QVariant'

Code is adjusted accordingly.

Update 2
Thanks eyllanesc, your approach is working!
However I now get confronted with related issues in QML. It seems that I can not acces all the QMap functions from QML.

For example:

var test_data = mymap["three"]  // works fine  
var test_data2 = mymap.find("two").value()  // results in: Property 'find' of object [object Object] is not a function

Same problem:

var tmp1 = mydata_qml_object // object was created before  
mymap["four"] = tmp1 // works fine  
mymap.insert("four", tmp1) // Property 'insert' of object [object Object] is not a function  

I am using Qt 5.11.1
Is this a bug or am I missing something?

C++ code
mydata.hpp:

#ifndef MYDATA_HPP  
#define MYDATA_HPP

#include <QObject>
#include <QString>

class MyData : public QObject
{
    Q_OBJECT

    Q_PROPERTY(QString name READ get_name WRITE set_name NOTIFY nameChanged)

  public:
    explicit MyData(QObject *parent = nullptr);
    MyData(QString name);
    MyData(const MyData &other);
    MyData(MyData &&other) = delete;
    MyData &operator=(const MyData &other);
    MyData operator=(MyData &&other) = delete;
    ~MyData() override = default;

  signals:
    void nameChanged();

  public slots:
    void set_name(const QString &name);
    QString get_name();

  private:
    QString _name;
};

Q_DECLARE_METATYPE(MyData);

#endif // MYDATA_HPP

mydata.cpp:

#include "mydata.hpp"

MyData::MyData(QObject *parent)
    : QObject(parent)
{
}

MyData::MyData(QString name)
    : _name(name)
{
}

MyData::MyData(const MyData &other)
{
    _name = other._name;
}

MyData &MyData::operator=(const MyData &other)
{
    if (this != &other)
    {
        _name = other._name;
        return *this;
    }
}

void MyData::set_name(const QString &name)
{
    _name = name;
}
QString MyData::get_name()
{
    return _name;
}

main.cpp:

#include <mydata.hpp>

#include <QGuiApplication>
#include <QMap>
#include <QQmlApplicationEngine>
#include <QQmlComponent>
#include <QQmlContext>
#include <QQuickView>

#include <iostream>

int main(int argc, char *argv[])
{
    MyData test1("Hi");
    MyData test2("Hello");
    QMap<QString, QVariant> mymap; // QVariantMap

    QVariant qvtest1(test1); // error: no matching function for call to ‘QVariant::QVariant(MyData&)’

    //working:
    QVariant qvtest1, qvtest2;
    qvtest1.setValue(test1);
    qvtest2.setValue(test2);


    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);
    auto *engine = new QQmlEngine;
    QQuickView view;
    QQmlContext *ctxt = view.rootContext();

    // this is working:
    qmlRegisterType<MyData>("MyData", 1, 0, "MyData");
    engine->rootContext()->setContextProperty("test1", &test1);


    // this produces an error: calling a private constructor of class 'QVariant'
    engine->rootContext()->setContextProperty("mymap", &mymap);

    QQmlComponent component(engine, QUrl("qrc:/main.qml"));
    QQmlEngine::setObjectOwnership(engine, QQmlEngine::CppOwnership);
    QObject *object = component.create();

    return app.exec();
}
1 Answers

According to the docs, the classes that inherit from QObject can not have a copy constructor or assignment operator:

No Copy Constructor or Assignment Operator

QObject has neither a copy constructor nor an assignment operator. This is by design. Actually, they are declared, but in a private section with the macro Q_DISABLE_COPY(). In fact, all Qt classes derived from QObject (direct or indirect) use this macro to declare their copy constructor and assignment operator to be private. The reasoning is found in the discussion on Identity vs Value on the Qt Object Model page.

The main consequence is that you should use pointers to QObject (or to your QObject subclass) where you might otherwise be tempted to use your QObject subclass as a value. For example, without a copy constructor, you can't use a subclass of QObject as the value to be stored in one of the container classes. You must store pointers.

On the other hand, if you want a class that inherits from QObject to support the QMetaType to use it as QVariant, you must pass it to the pointer because, as noted, the QObjects do not have a copy constructor, but the pointers are copyable.

//mydata.h
#ifndef MYDATA_H
#define MYDATA_H

#include <QObject>
class MyData : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ get_name WRITE set_name NOTIFY nameChanged)
public:
    explicit MyData(QObject *parent = nullptr);
    MyData(const QString & name);
    ~MyData() override = default;
signals:
    void nameChanged();
public slots:
    void set_name(const QString &name);
    QString get_name();
private:
    QString _name;
};
Q_DECLARE_METATYPE(MyData*)
#endif // MYDATA_H

//mydata.cpp

#include "mydata.h"

MyData::MyData(QObject *parent) : QObject(parent)
{}
MyData::MyData(const QString & name)
    : _name(name){}
void MyData::set_name(const QString &name)
{
    if(_name == name) return;
    _name = name;
    emit nameChanged();
}
QString MyData::get_name()
{return _name;}

You point out that setValue works, have you bought that it works ?, besides this method is used for the types that do not support QVariant so they will probably accept any type of data, what must be done is to pass the pointer:

MyData test1("Hi");
MyData test2("Hello");
QMap<QString, QVariant> mymap;
QVariant qvtest1 = QVariant::fromValue(&test1); 
QVariant qvtest_1, qvtest_2;
qvtest_1.setValue(&test1);
qvtest_2.setValue(&test2);

On the other hand is setContextProperty, this accepts a QVariant or a pointer to QObject, and in your first case you pass QObject, the correct thing is to pass the pointer, in the second case you pass a QVariant so there are no problems as it can be copied.

engine->rootContext()->setContextProperty("test1", &test1);
engine->rootContext()->setContextProperty("mymap", mymap);

In conclusion you must pass an object copyable to setContextProperty.


qmlRegisterType only records that a type can be transmitted through the signals, but this does not guarantee that it works, this is a necessary condition but not enough since it is not copyable, so you must use MyData*.

Related