Search code examples
qtqmlqtquick2qt6

Qt6: Warnings when QML Listview accesses C++ model


First of all, I'm new to Qt6 and QML, so maybe I am missing something obvious.

I'm trying to link a C++ model to a ListView in QML from a QObject property.
From this doc I should be able to use a List<QObject*> as a static model in a QML view.

However, in that example, the QList<QObject*> is passed directly to a QQuickView.
I would like to access the object list from a property of a QObject I can already access in QML.
But when I try to do that, nothing is shown in the list view, and I don't know what I am doing wrong...
Also, QML reports me a warning (see below my example code).

Here a working minimal example of what I am trying to achieve:

backend.h

#ifndef BACKEND_H
#define BACKEND_H

#include <QObject>

// Contains the data I want to display for each element
class Item : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name MEMBER m_name NOTIFY onNameChanged)

public:
    Item(QString name, QObject *parent = nullptr)
        : QObject{parent}, m_name(name)
    {}

signals:
    void onNameChanged();

private:
    QString m_name {"NULL"};
};

// This class contains the model I want to display.
// The data will be loaded before loading the QML file.
// It can be switched between a mockup and a real backend depending on the context.
class Backend : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString header MEMBER m_header NOTIFY onHeaderChanged)
    Q_PROPERTY(QList<QObject*> model MEMBER m_model NOTIFY onModelChanged)

public:
    explicit Backend(QObject *parent = nullptr)
        : QObject{parent}
    {
        m_header = "Cpp Backend";
        m_model.append(new Item {"Cpp"});
        m_model.append(new Item {"backend"});
        m_model.append(new Item {"is"});
        m_model.append(new Item {"great!"});
    }

    virtual ~Backend() override
    {
        for (QObject* item : m_model)
            delete item;
    }

signals:
    void onHeaderChanged();
    void onModelChanged();

private:
    QString m_header;
    QList<QObject*> m_model;
};

#endif // BACKEND_H

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQMLContext>

#include "backend.h"

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

    QQmlApplicationEngine engine;

    // Exposing the backend to QML with the name "cppBackend"
    Backend backend;
    engine.rootContext()->setContextProperty("cppBackend", &backend);

    const QUrl url(u"qrc:/TestBackend/Main.qml"_qs);
    QObject::connect(
        &engine,
        &QQmlApplicationEngine::objectCreationFailed,
        &app,
        []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

Main.qml

import QtQuick

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")

    ListModel {
        id: mockupList
        ListElement { name: "Hello" }
        ListElement { name: "World!" }
        ListElement { name: "How" }
        ListElement { name: "are" }
        ListElement { name: "you?" }
    }

    ListView {
        id: listView
        anchors.fill: parent
        anchors.margins: 20
        spacing: 10
        orientation: ListView.Vertical

        //model: mockupList // this works as expected
        model: cppBackend.model // this doesn't show anything in the listview

        delegate: Item {
            id: myItem
            required property string name

            width: label.width
            height: label.height
            Text {
                id: label
                text: myItem.name
                font.pointSize: 24
            }
        }

        header: Text {
            width: parent.width
            horizontalAlignment: Text.AlignHCenter
            font.pointSize: 48
            font.bold: true

            text: cppBackend.header // This works as expected
        }
    }
}

When I use te mockupList instead of the C++ backend, the items are displayed as expected.
However, when using the cppBackend I'm getting this warning:

qrc:/TestBackend/Main.qml:30:13: Required property name was not initialized qrc:/TestBackend/Main.qml: 30

It seems that the property cppBackend.model is accessed, but the items inside do not provide access to their properties as it seems it should to be done in the Qt doc...


Solution

  • I've made you an example that uses QML_SINGLETON to register the backend in QML and defines DataObject (Item) as a QML_ELEMENT so it will be made visible in QML and the properties are being exposed.

    I changed from setContextProperty to a singleton due to the reasons explained in this article.

    The important thing to make DataObject known in Main.qml is to import the URI of the module the DataObject sources were added to (import Demo77967427).

    The only weird part is that without "casting" the Backend.model to a list<DataObject> it doesn't work.

    property list<DataObject> myModel: Backend.model
    

    And then continue to use myModel as the model that gets bound to the ListView.

    main.cpp

    ...
    qmlRegisterSingletonType<Backend>("FooBar", 1, 0, "Backend", [](QQmlEngine *, QJSEngine *) {
        return new Backend();
    });
    ...
    

    dataobject.h

    class DataObject : public QObject
    {
        Q_OBJECT
        Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
        QML_ELEMENT
    
    public:
        DataObject(QObject *parent = nullptr);
        DataObject(QString name, QObject *parent = nullptr);
    
    signals:
        void nameChanged();
    
    private:
        QString m_name;
    };
    

    backend.h

    class Backend : public QObject
    {
        Q_OBJECT
        Q_PROPERTY(QString header MEMBER m_header NOTIFY headerChanged)
        Q_PROPERTY(QList<QObject *> model MEMBER m_model NOTIFY modelChanged)
        QML_ELEMENT
        QML_SINGLETON
    
    public:
        explicit Backend(QObject *parent = nullptr);
        virtual ~Backend() override;
    
    signals:
        void headerChanged();
        void modelChanged();
    
    private:
        QString m_header;
        QList<QObject *> m_model;
    };
    

    Main.qml

    import QtQuick
    import FooBar
    import Demo77967427
    
    Window {
        id: root
        width: 640
        height: 480
        visible: true
        title: qsTr("Hello World")
    
        property list<DataObject> myModel: Backend.model
    
        ListView {
            id: listView
            anchors.fill: parent
            anchors.margins: 20
            spacing: 10
            orientation: ListView.Vertical
    
            model: root.myModel
    
            delegate: Item {
                id: myItem
                required property string name
    
                width: label.width
                height: label.height
                Text {
                    id: label
                    text: myItem.name
                    font.pointSize: 24
                }
            }
    
            header: Text {
                width: parent.width
                horizontalAlignment: Text.AlignHCenter
                font.pointSize: 48
                font.bold: true
    
                text: Backend.header
            }
        }
    }
    

    Have a look at the complete application here.

    That said, the best way to expose models from C++ to QML is to derive from QAbstractListModel.


    You shouldn't prefix your notifier with on, have a look here.

    Note: It is recommended that the NOTIFY signal be named <property>Changed where <property> is the name of the property. The associated property change signal handler generated by the QML engine will always take the form on<Property>Changed, regardless of the name of the related C++ signal, so it is recommended that the signal name follows this convention to avoid any confusion.