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