Populating a TabBar from a model
-
@Bob64 said in Populating a TabBar from a model:
isn't going to work as seamlessly on the QML side as the approach I mentioned
I thought I was using the approach you mentioned. Where did I deviate from your suggestion?
Thanks...
@mzimmers sorry, there is more than one thing going on here. My original suggestion was to use the
Repeaterwhich you have indeed followed. But then you asked about the model exposure in QML.The usual way to expose a
QAbstractListModelto QML is to override thedatafunction. This returns aQVariantso whatever you return needs to be storable in aQVariant.If you have a model that has a number of properties at each index, let's say "name", "age", "occupation", probably the most natural way to expose this is to override
roleNames()anddata():class PersonListModel : public QAbstractListModel { Q_OBJECT public: PersonListModel (QObject* parent); enum MyRoles { NameRole = Qt::UserRole + 1, AgeRole, OccupationRole }; ... private: QHash<int, QString> m_roles;PersonListModel ::PersonListModel (QObject* parent) { // init roles m_roles[NameRole] = "name"; m_roles[AgeRole] = "age"; m_roles[OccupationRole] = "occupation"; ... } // Override QAbstractItemModel function... QHash<int, QByteArray> PersonListModel ::roleNames() const { return m_roles; } // Override QAbstractItemModel function... QVariant PersonListModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); // For illustration only, the below assumes your actual data is stored // in something like a list of structs with `name`, `age` fields etc. // Obviously you can implement this in any way you like as long as you // can take an index and return the requested value if (index.row() < 0 || index.row() >= m_myModelData.size()) return QVariant(); switch (role) { case NameRole: return m_myModelData[index.row()].name; case AgeRole: ... }Now, the point about
roleNames()in the above is that those strings that you associated with the enum values are what get exposed as property names on the QML side.The QML does a lot of work for you given the above. In a delegate that references the model, you can directly refer to
name,ageetc., and under the hood, Qt will be callingdatawith the currentindexand the requested role ID and will be extracting the value from the returnedQVariantinto a QML-compatible type.Relating this to your case, there are two approaches you could take. One is to "flatten" the model in a sense so that you don't need
Zoneas a type in this particular part of the QML code. So, what you might do is to define aZoneNameRolemapped to"zoneName". You would implement yourdata()method something like this:case ZoneNameRole: return getZone(index.row()).name; ...Then in your QML, you could assign your button text simply as:
Repeater { model: zoneModel delegate: TabButton { text: zoneName } }The other option is to expose
Zoneexplicitly from the model as a role in its own right. This is possible becauseZoneis aQ_GADGETso is itself a type that interoperates with QML. The only thing you need to do if you aren't doing so already is to have a line like this in the header where you defineZone:Q_DECLARE_METATYPE(Zone)This allows a
Zoneto be stored in aQVariant.With this approach your code might be more like this:
class ZoneModel : public QAbstractListModel { Q_OBJECT public: ZoneModel (QObject* parent); enum MyRoles { ZoneRole = Qt::UserRole + 1, }; ... private: QHash<int, QString> m_roles;ZoneModel ::ZoneModel (QObject* parent) { // init roles m_roles[ZoneRole] = "zone"; ... } // Override QAbstractItemModel function... QHash<int, QByteArray> ZoneModel ::roleNames() const { return m_roles; } // Override QAbstractItemModel function... QVariant ZoneModel::data(const QModelIndex &index, int role) const { // ... check preconditions etc as before ... switch (role) { case ZoneRole: return getZone(index.row()); ... }And in the QML:
Repeater { model: zoneModel delegate: TabButton { text: zone.name } }The official documentation is here:
https://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html#qabstractitemmodel-subclass
-
Hi all -
I'd like to create a TabBar with buttons derived from a C++ model. Each button would get its text from a property in the model.
Doable? Perhaps with addItem()?
Thanks...
-
@mzimmers you could use a
Repeaterin yourTabBarwith a delegate that creates a button. It could be driven by something based onQAbstractItemModelorQAbstractListModel.You could mock it up using a QML
ListModelto start with.@Bob64 yep...that did the trick:
TabBar { width: parent.width Repeater { model: zoneModel delegate: TabButton { text: zoneModel.getZone(index).name } }This works just fine. I wonder, though...is there a way for me to eliminate the inner reference to zoneModel? I tried just model.getZone() but it says it's not a function. There's probably another way to form that line but I don't know what it is.
Thanks...
-
@Bob64 yep...that did the trick:
TabBar { width: parent.width Repeater { model: zoneModel delegate: TabButton { text: zoneModel.getZone(index).name } }This works just fine. I wonder, though...is there a way for me to eliminate the inner reference to zoneModel? I tried just model.getZone() but it says it's not a function. There's probably another way to form that line but I don't know what it is.
Thanks...
-
@Bob64 yep...that did the trick:
TabBar { width: parent.width Repeater { model: zoneModel delegate: TabButton { text: zoneModel.getZone(index).name } }This works just fine. I wonder, though...is there a way for me to eliminate the inner reference to zoneModel? I tried just model.getZone() but it says it's not a function. There's probably another way to form that line but I don't know what it is.
Thanks...
-
@piervalli I tried that:
Repeater { model: zoneModel delegate: TabButton { text: getZone(index).name } }Still yet runtime error not defined.
@Bob64 my ZoneModel class contains a list of Zone objects. In order to get the names of the individual zones, I use getZone() to return the zone, then get its name from one of its properties.
class ZoneModel: public QAbstractListModel { Q_OBJECT ZoneList *m_list; public: Q_INVOKABLE Zone getZone(int index); ... class Zone { Q_GADGET QString m_name; public: Q_PROPERTY(QString name MEMBER m_name) QString name() const { return m_name; } ... -
@piervalli I tried that:
Repeater { model: zoneModel delegate: TabButton { text: getZone(index).name } }Still yet runtime error not defined.
@Bob64 my ZoneModel class contains a list of Zone objects. In order to get the names of the individual zones, I use getZone() to return the zone, then get its name from one of its properties.
class ZoneModel: public QAbstractListModel { Q_OBJECT ZoneList *m_list; public: Q_INVOKABLE Zone getZone(int index); ... class Zone { Q_GADGET QString m_name; public: Q_PROPERTY(QString name MEMBER m_name) QString name() const { return m_name; } ...@mzimmers ok, I see. The approach you have taken is a way to expose a backend C++ model to QML but it probably isn't going to work as seamlessly on the QML side as the approach I mentioned, which essentially involves overriding
roleNamesanddata.Another approach is a hybrid, where you define a
"zone"role and havedatareturn aZonecorresponding to the requested index. Note thatdatareturns aQVariantbut it should be possible to arrange for yourZonetype to be storable in aQVariant(it might already just work with it being aQ_GADGET- I forget the details at the moment and can't look them up). On the QML side, you should then be able to do something liketext: zone.name. -
@mzimmers ok, I see. The approach you have taken is a way to expose a backend C++ model to QML but it probably isn't going to work as seamlessly on the QML side as the approach I mentioned, which essentially involves overriding
roleNamesanddata.Another approach is a hybrid, where you define a
"zone"role and havedatareturn aZonecorresponding to the requested index. Note thatdatareturns aQVariantbut it should be possible to arrange for yourZonetype to be storable in aQVariant(it might already just work with it being aQ_GADGET- I forget the details at the moment and can't look them up). On the QML side, you should then be able to do something liketext: zone.name. -
@Bob64 said in Populating a TabBar from a model:
isn't going to work as seamlessly on the QML side as the approach I mentioned
I thought I was using the approach you mentioned. Where did I deviate from your suggestion?
Thanks...
@mzimmers sorry, there is more than one thing going on here. My original suggestion was to use the
Repeaterwhich you have indeed followed. But then you asked about the model exposure in QML.The usual way to expose a
QAbstractListModelto QML is to override thedatafunction. This returns aQVariantso whatever you return needs to be storable in aQVariant.If you have a model that has a number of properties at each index, let's say "name", "age", "occupation", probably the most natural way to expose this is to override
roleNames()anddata():class PersonListModel : public QAbstractListModel { Q_OBJECT public: PersonListModel (QObject* parent); enum MyRoles { NameRole = Qt::UserRole + 1, AgeRole, OccupationRole }; ... private: QHash<int, QString> m_roles;PersonListModel ::PersonListModel (QObject* parent) { // init roles m_roles[NameRole] = "name"; m_roles[AgeRole] = "age"; m_roles[OccupationRole] = "occupation"; ... } // Override QAbstractItemModel function... QHash<int, QByteArray> PersonListModel ::roleNames() const { return m_roles; } // Override QAbstractItemModel function... QVariant PersonListModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); // For illustration only, the below assumes your actual data is stored // in something like a list of structs with `name`, `age` fields etc. // Obviously you can implement this in any way you like as long as you // can take an index and return the requested value if (index.row() < 0 || index.row() >= m_myModelData.size()) return QVariant(); switch (role) { case NameRole: return m_myModelData[index.row()].name; case AgeRole: ... }Now, the point about
roleNames()in the above is that those strings that you associated with the enum values are what get exposed as property names on the QML side.The QML does a lot of work for you given the above. In a delegate that references the model, you can directly refer to
name,ageetc., and under the hood, Qt will be callingdatawith the currentindexand the requested role ID and will be extracting the value from the returnedQVariantinto a QML-compatible type.Relating this to your case, there are two approaches you could take. One is to "flatten" the model in a sense so that you don't need
Zoneas a type in this particular part of the QML code. So, what you might do is to define aZoneNameRolemapped to"zoneName". You would implement yourdata()method something like this:case ZoneNameRole: return getZone(index.row()).name; ...Then in your QML, you could assign your button text simply as:
Repeater { model: zoneModel delegate: TabButton { text: zoneName } }The other option is to expose
Zoneexplicitly from the model as a role in its own right. This is possible becauseZoneis aQ_GADGETso is itself a type that interoperates with QML. The only thing you need to do if you aren't doing so already is to have a line like this in the header where you defineZone:Q_DECLARE_METATYPE(Zone)This allows a
Zoneto be stored in aQVariant.With this approach your code might be more like this:
class ZoneModel : public QAbstractListModel { Q_OBJECT public: ZoneModel (QObject* parent); enum MyRoles { ZoneRole = Qt::UserRole + 1, }; ... private: QHash<int, QString> m_roles;ZoneModel ::ZoneModel (QObject* parent) { // init roles m_roles[ZoneRole] = "zone"; ... } // Override QAbstractItemModel function... QHash<int, QByteArray> ZoneModel ::roleNames() const { return m_roles; } // Override QAbstractItemModel function... QVariant ZoneModel::data(const QModelIndex &index, int role) const { // ... check preconditions etc as before ... switch (role) { case ZoneRole: return getZone(index.row()); ... }And in the QML:
Repeater { model: zoneModel delegate: TabButton { text: zone.name } }The official documentation is here:
https://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html#qabstractitemmodel-subclass
-
@mzimmers sorry, there is more than one thing going on here. My original suggestion was to use the
Repeaterwhich you have indeed followed. But then you asked about the model exposure in QML.The usual way to expose a
QAbstractListModelto QML is to override thedatafunction. This returns aQVariantso whatever you return needs to be storable in aQVariant.If you have a model that has a number of properties at each index, let's say "name", "age", "occupation", probably the most natural way to expose this is to override
roleNames()anddata():class PersonListModel : public QAbstractListModel { Q_OBJECT public: PersonListModel (QObject* parent); enum MyRoles { NameRole = Qt::UserRole + 1, AgeRole, OccupationRole }; ... private: QHash<int, QString> m_roles;PersonListModel ::PersonListModel (QObject* parent) { // init roles m_roles[NameRole] = "name"; m_roles[AgeRole] = "age"; m_roles[OccupationRole] = "occupation"; ... } // Override QAbstractItemModel function... QHash<int, QByteArray> PersonListModel ::roleNames() const { return m_roles; } // Override QAbstractItemModel function... QVariant PersonListModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); // For illustration only, the below assumes your actual data is stored // in something like a list of structs with `name`, `age` fields etc. // Obviously you can implement this in any way you like as long as you // can take an index and return the requested value if (index.row() < 0 || index.row() >= m_myModelData.size()) return QVariant(); switch (role) { case NameRole: return m_myModelData[index.row()].name; case AgeRole: ... }Now, the point about
roleNames()in the above is that those strings that you associated with the enum values are what get exposed as property names on the QML side.The QML does a lot of work for you given the above. In a delegate that references the model, you can directly refer to
name,ageetc., and under the hood, Qt will be callingdatawith the currentindexand the requested role ID and will be extracting the value from the returnedQVariantinto a QML-compatible type.Relating this to your case, there are two approaches you could take. One is to "flatten" the model in a sense so that you don't need
Zoneas a type in this particular part of the QML code. So, what you might do is to define aZoneNameRolemapped to"zoneName". You would implement yourdata()method something like this:case ZoneNameRole: return getZone(index.row()).name; ...Then in your QML, you could assign your button text simply as:
Repeater { model: zoneModel delegate: TabButton { text: zoneName } }The other option is to expose
Zoneexplicitly from the model as a role in its own right. This is possible becauseZoneis aQ_GADGETso is itself a type that interoperates with QML. The only thing you need to do if you aren't doing so already is to have a line like this in the header where you defineZone:Q_DECLARE_METATYPE(Zone)This allows a
Zoneto be stored in aQVariant.With this approach your code might be more like this:
class ZoneModel : public QAbstractListModel { Q_OBJECT public: ZoneModel (QObject* parent); enum MyRoles { ZoneRole = Qt::UserRole + 1, }; ... private: QHash<int, QString> m_roles;ZoneModel ::ZoneModel (QObject* parent) { // init roles m_roles[ZoneRole] = "zone"; ... } // Override QAbstractItemModel function... QHash<int, QByteArray> ZoneModel ::roleNames() const { return m_roles; } // Override QAbstractItemModel function... QVariant ZoneModel::data(const QModelIndex &index, int role) const { // ... check preconditions etc as before ... switch (role) { case ZoneRole: return getZone(index.row()); ... }And in the QML:
Repeater { model: zoneModel delegate: TabButton { text: zone.name } }The official documentation is here:
https://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html#qabstractitemmodel-subclass
@Bob64 first, thank you very much for the detailed answer. This very nicely corroborates and augments the Qt tutorial on this subject.
As it turns out, I had already reimplemented data(), setData() and roleNames() in my model. Everything was there; I just wasn't using it (properly) in my QML. This is my roleNames:
QHash<int, QByteArray> ZoneModel::roleNames() const { QHash<int, QByteArray> zones; zones[NameRole] = "name"; ... return zones; }and my data() (error checking removed):
QVariant ZoneModel::data(const QModelIndex &index, int role) const { QVariant qv = QVariant(); Zone item = m_list->zones().at(index.row()); switch (role) { case NameRole: qv = item.name(); break; default: qv = QVariant(); } return qv; }(The additional level of indirection is that my actual list is defined in its own class (not the model class); again, this is from the tutorial.
So, all I had to do in my QML is:
Repeater { model: zoneModel delegate: TabButton { text: name } }Very easy, once one understands all the pieces of the model and how they work together.
Thank you again for the great explanation.
-
M mzimmers has marked this topic as solved on
