Ben Pyton
Ben Pyton

Reputation: 348

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...

Upvotes: 2

Views: 267

Answers (3)

smr
smr

Reputation: 1004

I tested your code in different ways, either by defining a global context property as suggested by @JarMan and as done in the documentation, or by using the QVariant::fromValue function while defining a Q_INVOKABLE function. However, I didn't really understand why the global one results in a list<Item>, while the other solutions result in a JS Array.

By printing the model inside the delegate, the type for list<Item> is QQmlDMObjectData, and for others, it is QQmlDMListAccessorData.

Anyway, after @iam_peter's answer, An idea crossed my mind. It reminded me of QQmlListProperty and its ease of conversion from a QList<Object *> (using QQmlListProperty(QObject *, QList<T *> *)).

Note: QQmlListProperty example

So, I tried changing the property type to QQmlListProperty, and it worked. It actually doesn't need too many changes.

The changes are as follows:

class Backend : public QObject {
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<Item> model READ model)
public:
    QQmlListProperty<Item> model() {
        return {this, &m_model};
    }
}

And you can use it like:

Item {
    Backend {
        id: back
    }

    ListView {
        model: back.model
        anchors.fill: parent
        delegate: Label {
            required property string name
            text: name
        }
    }
}

Final note: As others have also suggested, it is best to use the QAbstractItemModel.

Upvotes: 2

iam_peter
iam_peter

Reputation: 3924

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.

Upvotes: 1

JarMan
JarMan

Reputation: 8277

Exposing the model directly from QML works fine, even though accessing it through a Q_PROPERTY seems to fail for some reason. I don't understand the difference myself, but here is how I got it to work:

First create an accessor to get the model:

class Backend : public QObject
{
    Q_OBJECT

public:
    ...

    QList<QObject *> model() { return m_model; }
};

Then expose that model to QML:

Backend backend;
engine.rootContext()->setContextProperty("cppBackendModel", QVariant::fromValue(backend.model()));

And finally access that model from QML.

    ListView {
        id: listView

        model: cppBackendModel

        delegate: Item {
            id: myItem
            required property string name
            ...
        }
    }

Upvotes: 0

Related Questions