Reputation: 348
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
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 forlist<Item>
isQQmlDMObjectData
, and for others, it isQQmlDMListAccessorData
.
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
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
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