Search code examples
qtqmlqtquick2qt6

QML ListView delegate that works for both QML and C++ models


I'm asking this question after getting some insights on my first question on displaying C++ backend model in QML.

I am new to Qt6 and QML, and I think I understood how to link models to delegates in QML, but maybe I missed something...

I would like to have a QML ListView delegate that works for both a QML list model and a C++ list model.
This requirement is needed to be able to have the designers to work in Qt Design Studio with a QML model mockup, to have a C++ mockup for unit tests, and to have the real C++ model loaded from the backend.

However, QML and C++ models seem to be accessed differently in the QML ListView delegate:

  • C++ model item properties are accessed through modelData.propertyName
  • QML model item properties are accessed through required propertyName (for an existing property in the delegate)

My question: is there an unified way to access properties for both C++ and QML models?

Here is a minimal working example to demonstrate my issue (see Main.qml for the difference between QML and C++ access):

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 to display in the ListView.
// The data will be loaded before loading the QML file.
class Backend : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QList<Item*> model MEMBER m_model NOTIFY onModelChanged)

public:
    explicit Backend(QObject *parent = nullptr)
        : QObject{parent}
    {
        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 (Item* item : m_model)
            delete item;
    }

signals:
    void onModelChanged();

private:
    QList<Item*> 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();
}

MyListItem.qml

import QtQuick

Item {
    id: myItem
    property string name: "Default Name"

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

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 // Works only with 'required name'
        //model: cppBackend.model // Works only with 'name: modelData.name'

        delegate: MyListItem {
            required name // Works only with QML list model
            //name: modelData.name // Works only with C++ list model
        }
    }
}

Here are the results I get when commenting in/out the model and name lines in the Main.qml file:

  • mockupList with required name: QML model items are properly displayed
  • mockupList with name: modelData.name: all items show the default name in MyListItem.qml
  • cppBackend.model with required name: No item was created and throws the warning Required property name was not initialized
  • cppBackend.model with name: modelData.name: C++ model items are properly displayed

Solution

  • The answer of @iam_peter in my linked question solved this question too.

    TL;DR:
    There was 3 elements missing in my example code:

    • Add QML_ELEMENT in Item class.
    • Add import TestBackend (the URI of the project defined in the CMake file) in Main.qml
    • Use a property in Main.qml to store the backend model outside the list view like that:
    Window {
        id: root
        // ...
        property list<Item> itemList: cppBackend.model
        ListView {
            // ...
            model: root.itemList
            // ...
        }
    }
    

    With these minimal changes to my example code, both the QML model and the C++ model can be accessed with the delegate's required name line in Main.qml.

    EDIT:

    The answer of @smr on my linked question solves also this question and requires even less code changes. The only change to the example code is to replace the Q_PROPERTY of the model by QQmlListProperty<Item> like that:

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