exposing entire C++ object to QML
-
Hi all -
My app uses a C++ (list) model which is exposed to QML via the means mentioned in the docs. Everything seems to work fine.
I use the model on a summary page to display item information. When the user clicks on a list item, he'll go to a detail page.
A minimal bit of my model:
struct Space { QUuid m_uuid; QString m_name; } class SpaceModel: public QAbstractListModel { Q_OBJECT QList<Space> *m_list; ... }
and some of my QML:
// space summary screen delegate: SpaceCard { uuid: spaceUuid // from model drawerEnabled: false onTileClicked: (uuid) => { drawer.open() drawerStack.push("SpaceDetail.qml", { "uuid": uuid, "name": displayName // from model } // SpaceDetail.qml ColumnLayout { id: spaceDetail required property string uuid required property string name // etc...
My question: what if the struct used in the model list were to contain, say, 100 elements? Obviously I don't want to pass each one individually from the summary to the detail. How best to handle this situation?
Thanks...
-
@GrecKo said in exposing entire C++ object to QML:
@mzimmers What role are you using for returning your complete object? What roles do you emit dataChanged on?
Sorry if this wasn't clear but that was meant to push you to find the correct solution.
You don't emit
dataChanged
for theCompleteObjectRole
, so if you use this role and change the data, the view and delegates won't be aware of any change.Don't do what Marko suggested with
property space mySpace: spaceCards.currentItem().mySpace property string titleText: mySpace.name()
or emitting
dataChanged
in thedata
function.You also don't need the Q_INVOKABLE macro on your getters, they are are already exposed with Q_PROPERTY. You don't even need the getters if you are using MEMBER in the Q_PROPERTY (which makes sense for a Q_GADGET).
-
@mzimmers said in exposing entire C++ object to QML:
Obviously I don't want to pass each one individually from the summary to the detail. How best to handle this situation?
Make the struct a
Q_GADGET
(or a Q_OBJECT but it's a bit more "heavy"), register it with QML and you should be able to pass it to yourSpaceDetail.qml
as a whole object instead of manually passing all members. -
@mzimmers said in exposing entire C++ object to QML:
Obviously I don't want to pass each one individually from the summary to the detail. How best to handle this situation?
I don't understand the question. What @sierdzio mentioned will allow you to pass and copy your struct around and keep it in a
property var
, recent Qt should (if it's been pushed) support also named value types. If you want to create the struct from QML though, you'd either need a factory function in C++ that you call, or (what I'd preferred in the past) is to expose a constructor from the struct that takesQJSValue
. -
I'm missing something:
struct Space { Q_GADGET public: QUuid m_uuid; QString m_name; } qmlRegisterType<Space>("Space", 1, 0, "Space");
Now how do I use this in my QML? I tried this:
ColumnLayout { id: spaceDetail required property Space mySpace
But I got a runtime error that Space is not a type (along with a warning that value type names should begin with a lowercase letter).
-
@mzimmers one annoying thing about Q_GADGET is that you cannot instantiate it in QML. But you can create it in C++ and pass around you QML - that's why I suggested it. You can keep you Q_GADGET objects in C++ model and then use them to pass the whole entry data in a single property. Like so:
drawerStack.push("SpaceDetail.qml", { "mySpace": space // fromModel }
In SpaceDetail.qml you can have a property but you cannot let it auto-construct because QML can't do it :( So, maybe this will work:
property Space mySpace: null // or property Space mySpace: undefined
-
@mzimmers said in exposing entire C++ object to QML:
struct Space { Q_GADGET Q_PROPERTY(QUuid uuid READ uuid WRITE setUuid) Q_PROPERTY(QString name READ name WRITE setName) QML_VALUE_TYPE(space) // Method definitions here ... QUuid m_uuid; QString m_name; }
(potentially you may want to have
Space(const QJSValue&)
to be able to construct from QML json.)
this this should work:ColumnLayout { id: spaceDetail required property space mySpace // ...
More details here
-
@kshegunov @sierdzio I'm still missing something here -- I have access to the Space object from the model used by the QML code that invokes SpaceDetail.qml. I want to use that object (not create a new one) to pass to SpaceDetail. How do I do this?
I'm also getting a startup error:
qt.qml.typeregistration: Invalid QML element name "Space"; value type names should begin with a lowercase letter
which I assume comes from this line:
qmlRegisterType<Space>("Space", 1, 0, "Space");
and which I don't understand, because all of my other calls to qmlRegisterType() work without error, eg:
qmlRegisterType<SpaceModel>("SpaceModel", 1, 0, "SpaceModel");
I suspect this is because SpaceModel is a subclass of QAbstractListModel, where Space is just a struct, but I don't fully understand the implications.
-
@mzimmers said in exposing entire C++ object to QML:
Hi all -
My app uses a C++ (list) model which is exposed to QML via the means mentioned in the docs. Everything seems to work fine.
I use the model on a summary page to display item information. When the user clicks on a list item, he'll go to a detail page.
A minimal bit of my model:
struct Space {
QUuid m_uuid;
QString m_name;
}
class SpaceModel: public QAbstractListModel
{
Q_OBJECT
QList<Space> *m_list;
...
}and some of my QML:
// space summary screen
delegate: SpaceCard {
uuid: spaceUuid // from model
drawerEnabled: false
onTileClicked: (uuid) => {
drawer.open()
drawerStack.push("SpaceDetail.qml",
{
"uuid": uuid,
"name": displayName // from model
}// SpaceDetail.qml
ColumnLayout {
id: spaceDetail
required property string uuid
required property string name
// etc...My question: what if the struct used in the model list were to contain, say, 100 elements? Obviously I don't want to pass each one individually from the summary to the detail. How best to handle this situation?
Thanks...Hi there,
If the struct used in the model list contains 100 elements, it would be inefficient to pass each one individually from the summary to the detail. A better way to handle this situation would be to use a data model.
A data model is a way of representing data in a way that is easy to access and manipulate. In QML, data models are implemented using the QtObject class.
To use a data model, you would first need to create a class that inherits from QtObject. This class would then contain the properties that you want to pass to the detail page. For example, you could create a class called SpaceModel that contains the following properties:
uuid: string
name: string
Once you have created the data model, you can then pass it to the detail page using the Object.fromJson() function. For example, you could do the following in the QML:// space summary screen delegate: SpaceCard { uuid: spaceUuid // from model drawerEnabled: false onTileClicked: (uuid) => { drawer.open() drawerStack.push("SpaceDetail.qml", JSON.stringify({ "uuid": uuid, "name": displayName // from model }) } } // SpaceDetail.qml ColumnLayout { id: spaceDetail required property string uuid required property string name // etc... SpaceModel { uuid: uuid name: name } }
This would create a data model object in the detail page that contains the uuid and name properties of the selected item from the summary page.
-
@mzimmers said in exposing entire C++ object to QML:
@kshegunov @sierdzio I'm still missing something here -- I have access to the Space object from the model used by the QML code that invokes SpaceDetail.qml. I want to use that object (not create a new one) to pass to SpaceDetail. How do I do this?
You can return
Space
object in yourdata()
method of your model. Something like (pseudocode):class SpaceModel: public QAbstractListModel { // ... QVartiant data(const QModelIndex &index, const int role) const { if (index is valid and role is correct etc.) { m_list.at(index.row()); } } }
I'm also getting a startup error:
qt.qml.typeregistration: Invalid QML element name "Space"; value type names should begin with a lowercase letter
which I assume comes from this line:
qmlRegisterType<Space>("Space", 1, 0, "Space");
This is some recent change in Qt 6. Uppercase gadgets work fine with Qt 5 & 6 but Qt 6 warns against them. If you don't need Qt 5 support, just make it lower-case and it will be fine.
Ah, one more thing: since Q_GADGETs cannot be constructed in QML, you should use
qmlRegisterUncreatableType
instead. -
@sierdzio said in exposing entire C++ object to QML:
You can return Space object in your data() method of your model. Something like (pseudocode):
Great idea - I'm going to do this. It never occurred to me to use data() for the entire object as well as the individual roles.
But: again, how do I code the access to the entire object in my QML? What is the construct for accessing a complete object from within a delegate?
Thanks...
-
@GrecKo more the latter. I'm trying to pass an instance from my model list from a delegate to another QML module, all in one line of code (if possible).
Right now, I only know how to access individual properties; I'm sure there's a way to refer to the entire object that the delegate is currently set on, but I don't know what that is.
EDIT: this is what I want to be able to do:
EDIT 2: simplified example (no need for the drawer)// space summary screen ListView { model: spaceModel delegate: SpaceCard { // MAGIC to pass the model element } // ... // SpaceCard.qml Item { id: spaceCard // MAGIC to reference the model element text: MAGIC.text uuid: MAGIC.uuid // etc. (imagine that the model element had 100 fields
-
@mzimmers said in exposing entire C++ object to QML:
@sierdzio said in exposing entire C++ object to QML:
You can return Space object in your data() method of your model. Something like (pseudocode):
Great idea - I'm going to do this. It never occurred to me to use data() for the entire object as well as the individual roles.
But: again, how do I code the access to the entire object in my QML? What is the construct for accessing a complete object from within a delegate?
Thanks...
If you use
roleNames()
and custom roles then it is entirely up to you.
If you just return a list of objects to act as a model, then you can access current element throughmodelData
, for examplemodelData.uuid
. -
@sierdzio OK, I think I now understand what you're saying. I've done the following:
- modified my Space struct:
struct Space { Q_GADGET QML_VALUE_TYPE(space) // so "space" can be used in QML. public: QString m_name; Q_INVOKABLE QString name() { return m_name; }
-
added a CompleteObjectRole to my SpaceModel roles
-
added to SpaceModel::data() for this role:
QVariant SpaceModel::data(const QModelIndex &index, int role) const { QVariant qv = QVariant(); do { Space item = m_list->at(index.row()); switch (role) { case CompleteObjectRole: qv = QVariant::fromValue(item)); break;
- use this in my QML:
// space summary screen ListView { id: spaceCards model: spaceProxyModel delegate: SpaceCard { mySpace: completeObject // SpaceCard.qml Item { id: spaceCard property space mySpace property string titleText: mySpace.name()
It works, and is what I was asking for. Is this what you expected/intended? Thanks...
EDIT:
I discovered a flaw in this approach: the property mySpace in SpaceCard.qml is evidently a static copy, and so its data isn't updated when the model changes. Is there a way to fix this?
Thanks...
-
@mzimmers yup, that's exactly what I had in mind!
EDIT:
I discovered a flaw in this approach: the property mySpace in SpaceCard.qml is evidently a static copy, and so its data isn't updated when the model changes. Is there a way to fix this?
First, try adding a MEMBER
Q_PROPERTY
to your gadget, it might work.struct Space { Q_GADGET QML_VALUE_TYPE(space) // so "space" can be used in QML. Q_PROPERTY(QString name MEMBER m_name) public: QString m_name; Q_INVOKABLE QString name() { return m_name; } // ... property string titleText: mySpace.name
If not, then you'll have to change Space to Q_OBJECT which does support signals & slots so it will also dynamically update for sure.
-
@sierdzio I'd already tried using Q_PROPERTY; sadly, no success. I guess I'm going to have to go through the effort of basing Space off of QObject...ugh.
I'll report back when I'm done.
EDIT:
@sierdzio is there really no other way to accomplish this? I ask because
- this is going to be a non-trivial effort, and
- the example in the docs doesn't do it this way.
Any alternative is welcome.
-
@sierdzio said in exposing entire C++ object to QML:
But have you used the property (
name
without()
) instead of invokable method?property string titleText: mySpace.name
Yes:
Item { // needed for the DropShadow. id: spaceCard property space spaceObject property string spaceName: spaceObject.name
Also, do you emit
dataChanged()
in your model when a change occurs?Yes. When I receive a change, my slot loops through the list of spaces, and updates the appropriate list items:
for (int spaceIndex = 0; spaceIndex < m_list->size(); spaceIndex++) { space = m_list->at(spaceIndex); if (space.containsEquipment(equipmentUuid)) { // update the temperature-related fields. QModelIndex qmi = index(spaceIndex, 0, QModelIndex()); space.m_temperature = temperature; space.m_temperatureTimestamp = temperatureTimestamp; space.m_temperatureUpdated = true; changedRoles << TemperatureRole << TemperatureTimestampRole << TimestampUpdatedRole; m_list->replace(spaceIndex, space); emit dataChanged(qmi, qmi, changedRoles); } }
I've verified this behavior with telltales.
Rather strange, isn't it...
-
@GrecKo I've created a separate role for the complete object:
enum SpaceRoleNames { UuidRole = Qt::UserRole, ... CompleteObjectRole, NbrRoles };
and my data() function does this:
QVariant SpaceModel::data(const QModelIndex &index, int role) const { QVariant qv = QVariant(); do { if (!index.isValid() || m_list == nullptr) continue; Space item = m_list->at(index.row()); switch (role) { case UuidRole: qv = item.m_uuid; break; ... case CompleteObjectRole: qv = QVariant::fromValue(item); break;
When any of the temperature related stuff changes, I perform the code in my last post.