a fresh look at C++ models and QML
-
Hi all -
As those regular visitors to this forum are aware, I've been struggling with getting a C++ model to properly notify my QML views/delegates when changes occur. I've created a new application for simplicity's sake, and would like to walk through it here from the very beginning.
I've created a small struct and then created a QList containing a few elements of that struct. Both the size of the QList and the contents of any of the elements are subject to programmatic (unsolicited by the user) change. For now, though, I'm ignoring the contents of the struct, and simply want to draw a rectangle for each list element.
Here's a bit of code:
struct ListItem { qint32 nbr; QString description; }; typedef QList<ListItem> DataList; class ListModel : public QAbstractListModel { Q_OBJECT DataList *m_list; // created in c'tor // some other stuff
and in main.cpp:
ListModel *listModel = new ListModel(); listModel->insertRows(0, 3, QModelIndex()); //verified in debugger qmlRegisterType<ListModel>("ListModel", 1, 0, "ListModel"); engine.rootContext()->setContextProperty("listModel", listModel);
in Main.qml:
ListView { model: listModel.rowCount() delegate: Rectangle { height: 100 width: 100 color: 'lightblue' border.width: 1 } }
So, question #1: what is the preferred method for causing a display update when main.cpp adds those elements to the list in my model?
I realize some of this is ground already covered, but I see value in taking this from the beginning, so I thank you in advance for your patience (and your help).
-
Hi all -
As those regular visitors to this forum are aware, I've been struggling with getting a C++ model to properly notify my QML views/delegates when changes occur. I've created a new application for simplicity's sake, and would like to walk through it here from the very beginning.
I've created a small struct and then created a QList containing a few elements of that struct. Both the size of the QList and the contents of any of the elements are subject to programmatic (unsolicited by the user) change. For now, though, I'm ignoring the contents of the struct, and simply want to draw a rectangle for each list element.
Here's a bit of code:
struct ListItem { qint32 nbr; QString description; }; typedef QList<ListItem> DataList; class ListModel : public QAbstractListModel { Q_OBJECT DataList *m_list; // created in c'tor // some other stuff
and in main.cpp:
ListModel *listModel = new ListModel(); listModel->insertRows(0, 3, QModelIndex()); //verified in debugger qmlRegisterType<ListModel>("ListModel", 1, 0, "ListModel"); engine.rootContext()->setContextProperty("listModel", listModel);
in Main.qml:
ListView { model: listModel.rowCount() delegate: Rectangle { height: 100 width: 100 color: 'lightblue' border.width: 1 } }
So, question #1: what is the preferred method for causing a display update when main.cpp adds those elements to the list in my model?
I realize some of this is ground already covered, but I see value in taking this from the beginning, so I thank you in advance for your patience (and your help).
-
@mzimmers said in a fresh look at C++ models and QML:
model: listModel.rowCount()
model: listModel.rowCount() <========?
-
@JoeCFD I didn't post that code because I was trying to keep my post down to the bare essentials. I do have such a routine, though, and it does work. How would you prefer that I get the list size in my QML?
-
ListView { model: listModel delegate: Rectangle { height: 100 width: 100 color: 'lightblue' border.width: 1 } }
@JoeCFD thank you. (I'd tried that, but it didn't show anything, because I neglected to assign sizing to my ListView...works fine now).
OK, question #2: Let's say I'd like to display the contents of my list elements in the Rectangle(s). This isn't right:
Text { text: "value is " + model.nbr + "\ndescription is" + model.description}
What is the preferred method of doing this? Thanks...
-
ListView { model: listModel delegate: Rectangle { height: 100 width: 100 color: 'lightblue' border.width: 1 } }
-
@JoeCFD override this func in class ListModel : public QAbstractListModel
int rowCount( const QModelIndex & parent = QModelIndex() ) const { if ( nullptr != m_list ) { return m_list->size(); } return 0; }
you will see rows. That is how model works.
@JoeCFD I'd already done that:
int ListModel::rowCount(const QModelIndex &parent) const { int count; // For list models only the root node (an invalid parent) should return the list's size. For all // other (valid) parents, rowCount() should return 0 so that it does not become a tree model. if (parent.isValid()) { count = 0; } else { count = m_list->size(); } qDebug() << __PRETTY_FUNCTION__ << "returning" << count; return count; }
The problem was, I hadn't given my ListView a size, so I was creating little tiny rectangles. Now it works:
ListView { Layout.fillHeight: true Layout.fillWidth: true orientation: ListView.Horizontal model: listModel delegate: Rectangle { height: 100 width: 100 color: 'lightblue' border.width: 1 } }
Produces this:
So, question 1 is answered...thank you. Now, how about question 2?
-
@JoeCFD thank you. (I'd tried that, but it didn't show anything, because I neglected to assign sizing to my ListView...works fine now).
OK, question #2: Let's say I'd like to display the contents of my list elements in the Rectangle(s). This isn't right:
Text { text: "value is " + model.nbr + "\ndescription is" + model.description}
What is the preferred method of doing this? Thanks...
-
@mzimmers define a role for each item of ListItem. use role to access data.
Text { text: "value is " + model.nbr + "\ndescription is" + model.description}
will not work.
@JoeCFD I have this struct
struct InfoItem { QString textColor; QString backgroundColor; QString name; };
override
QHash< int, QByteArray > Your class::roleNames() const { QHash< int, QByteArray > roles; roles[ Qt::UserRole + 1 ] = "textColor"; roles[ Qt::UserRole + 2 ] = "backgroundColor"; roles[ Qt::UserRole + 3 ] = "name"; return roles; }
access item in your delegate
Text { text: name color: textColor }
find the listview example in qt 6 and I guess it has a struct as well.
-
@mzimmers define a role for each item of ListItem. use role to access data.
Text { text: "value is " + model.nbr + "\ndescription is" + model.description}
will not work.
@JoeCFD my class contains this:
enum ListEnums { NbrRole = Qt::UserRole, DescriptionRole }; QHash<int, QByteArray> ListModel::roleNames() const { QHash<int, QByteArray> names; names[NbrRole] = "value"; names[DescriptionRole] = "description"; return names; }
and my delegate now contains this:
Text { text: "value is " + nbr + "\ndescription is" + description}
I now get "ReferenceError: nbr is not defined."
-
@JoeCFD my class contains this:
enum ListEnums { NbrRole = Qt::UserRole, DescriptionRole }; QHash<int, QByteArray> ListModel::roleNames() const { QHash<int, QByteArray> names; names[NbrRole] = "value"; names[DescriptionRole] = "description"; return names; }
and my delegate now contains this:
Text { text: "value is " + nbr + "\ndescription is" + description}
I now get "ReferenceError: nbr is not defined."
-
@mzimmers
from your definitions:Text { text: "value is " + value + "\ndescription is" + description}
@JoeCFD oh, right - I'm still getting used to that feature of roles...thanks for the correction.
Now, I have a timer that goes off once a second, and signals this slot:
void ListModel::update() { for (int i = 0; i < m_list->size(); ++i) { int newNbr = m_list->at(i).nbr + 1; QModelIndex qmi = index(i, 0, QModelIndex()); setData(qmi, newNbr, NbrRole); } }
My setData function:
bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role) { bool rc = false; do { if (m_list == nullptr) { continue; } ListItem item = m_list->at(index.row()); switch (role) { case NbrRole: item.nbr = value.toInt(); break; case DescriptionRole: item.description = value.toString(); break; } if (data(index, role) != value) { // how best to change the item in the list here? emit dataChanged(index, index, {role}); rc = true; } } while (false); return rc; }
I have used the debugger to verify that everything is working as expected. All that's missing (I think) is how to actually make the change to the list.
Question 3: should I just use a QList::replace(), or is there a better way to do it?
EDIT:
I've replaced the above:
int newNbr = m_list->at(i).nbr + 1;
with:
int newNbr = data(qmi, NbrRole).toInt() + 1;
Might as well get with the program, right?
-
@JoeCFD oh, right - I'm still getting used to that feature of roles...thanks for the correction.
Now, I have a timer that goes off once a second, and signals this slot:
void ListModel::update() { for (int i = 0; i < m_list->size(); ++i) { int newNbr = m_list->at(i).nbr + 1; QModelIndex qmi = index(i, 0, QModelIndex()); setData(qmi, newNbr, NbrRole); } }
My setData function:
bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role) { bool rc = false; do { if (m_list == nullptr) { continue; } ListItem item = m_list->at(index.row()); switch (role) { case NbrRole: item.nbr = value.toInt(); break; case DescriptionRole: item.description = value.toString(); break; } if (data(index, role) != value) { // how best to change the item in the list here? emit dataChanged(index, index, {role}); rc = true; } } while (false); return rc; }
I have used the debugger to verify that everything is working as expected. All that's missing (I think) is how to actually make the change to the list.
Question 3: should I just use a QList::replace(), or is there a better way to do it?
EDIT:
I've replaced the above:
int newNbr = m_list->at(i).nbr + 1;
with:
int newNbr = data(qmi, NbrRole).toInt() + 1;
Might as well get with the program, right?
OK, this is getting interesting now (at least to me).
Back to my original app, the app receives messages containing information to be used for updating the model. I now see that I need to use data() and setData() for this, but...
Question 4: if there are multiple changes to a given list entry, do I have to handle each one individually (as in the example below), or is there some way to do this automatically? I notice there's a setItemData() function, but I'm not sure how to handle the fact that my roles pertain to different data types. Here's my snippet:
// create a temporary item from the new values from the back end. EquipmentItem item(uuid, equipName, fwVersion, modelId, serialNumber, powerControllable, state); // create a QModelIndex for the data()/setData() calls. QModelIndex qmi = index(listIndex, 0, QModelIndex()); listIndex = getIndex(uuid); EquipmentItem itemFromList = m_list->at(listIndex); if (itemFromList == item) { // do nothing; no change. } else { // do I have to do this for each item in the struct? if (data(qmi, UuidRole).toUuid() != item.m_uuid) { setData(qmi, item.m_uuid, UuidRole); } } }
Thanks...
-
OK, this is getting interesting now (at least to me).
Back to my original app, the app receives messages containing information to be used for updating the model. I now see that I need to use data() and setData() for this, but...
Question 4: if there are multiple changes to a given list entry, do I have to handle each one individually (as in the example below), or is there some way to do this automatically? I notice there's a setItemData() function, but I'm not sure how to handle the fact that my roles pertain to different data types. Here's my snippet:
// create a temporary item from the new values from the back end. EquipmentItem item(uuid, equipName, fwVersion, modelId, serialNumber, powerControllable, state); // create a QModelIndex for the data()/setData() calls. QModelIndex qmi = index(listIndex, 0, QModelIndex()); listIndex = getIndex(uuid); EquipmentItem itemFromList = m_list->at(listIndex); if (itemFromList == item) { // do nothing; no change. } else { // do I have to do this for each item in the struct? if (data(qmi, UuidRole).toUuid() != item.m_uuid) { setData(qmi, item.m_uuid, UuidRole); } } }
Thanks...
-
ListItem item = m_list->at(index.row()); <=== this is a copy. nothing has been changed in your model. switch (role) { case NbrRole: item.nbr = value.toInt(); break; case DescriptionRole: item.description = value.toString(); break; }
@JoeCFD I see your comment. So, I've added the following line in order to modify my model:
ListItem item = m_list->at(index.row()); switch (role) { case NbrRole: item.nbr = value.toInt(); break; case DescriptionRole: item.description = value.toString(); break; } if (data(index, role) != value) { int row = index.row(); m_list->replace(row, item); // <== changes the model, right? emit dataChanged(index, index, {role}); rc = true; }
This appears to work. My concern is, if my updating routine changes several elements in the struct, I end up calling setData() several times, which could be inefficient in a larger example. Hence my question #4: is there a way to "group" the changes before I replace the item in my list? It appears that setItemData() would accomplish this, but I'm not sure how to use it (the doc is fairly terse).
-
@JoeCFD I see your comment. So, I've added the following line in order to modify my model:
ListItem item = m_list->at(index.row()); switch (role) { case NbrRole: item.nbr = value.toInt(); break; case DescriptionRole: item.description = value.toString(); break; } if (data(index, role) != value) { int row = index.row(); m_list->replace(row, item); // <== changes the model, right? emit dataChanged(index, index, {role}); rc = true; }
This appears to work. My concern is, if my updating routine changes several elements in the struct, I end up calling setData() several times, which could be inefficient in a larger example. Hence my question #4: is there a way to "group" the changes before I replace the item in my list? It appears that setItemData() would accomplish this, but I'm not sure how to use it (the doc is fairly terse).
You generally don't call
setData
inside your own logic code.
Just change the underlying data and emitdataChanged
for the relevant index and roles.setData
is there when you want to let your views update your model. For example if you wanted to have a TextField in your delegate to modify the description of one of your element. -
You generally don't call
setData
inside your own logic code.
Just change the underlying data and emitdataChanged
for the relevant index and roles.setData
is there when you want to let your views update your model. For example if you wanted to have a TextField in your delegate to modify the description of one of your element.@GrecKo I see. So then, my updating routine would look like this:
QList<int> changedRoles; ListItem item; for (int i = 0; i < m_list->size(); ++i) { QModelIndex qmi = index(i, 0, QModelIndex()); // initialize my temporary item from the model. item.nbr = m_list->at(i).nbr; item.description = m_list->at(i).description; // update the temporary item. item.nbr++; changedRoles.append(NbrRole); QChar qc = QChar(item.nbr + 0x40); item.description.append(qc); changedRoles.append(DescriptionRole); // replace the list item. m_list->replace(i, item); emit dataChanged(qmi, qmi, changedRoles); }
And, to return to my question #4, I guess this solves the problem of multiple replaces and signals.
So, does this look about right? Any room for improvement?
Thanks...
-
@GrecKo I see. So then, my updating routine would look like this:
QList<int> changedRoles; ListItem item; for (int i = 0; i < m_list->size(); ++i) { QModelIndex qmi = index(i, 0, QModelIndex()); // initialize my temporary item from the model. item.nbr = m_list->at(i).nbr; item.description = m_list->at(i).description; // update the temporary item. item.nbr++; changedRoles.append(NbrRole); QChar qc = QChar(item.nbr + 0x40); item.description.append(qc); changedRoles.append(DescriptionRole); // replace the list item. m_list->replace(i, item); emit dataChanged(qmi, qmi, changedRoles); }
And, to return to my question #4, I guess this solves the problem of multiple replaces and signals.
So, does this look about right? Any room for improvement?
Thanks...
@mzimmers That's not how I would write it.
Your for loop is quite synthetic but here's how I would do the samefor (ListItem& item : m_list) { // use range for loop, also get a reference to your items in your list item.nbr++; // edit the item reference in place QChar qc = QChar(item.nbr + 0x40); // ? not sure what you wanted to do there but I copied it item.description.append(qc); } // if you modify contiguous items at the same time, emit dataChanged once for all the range. emit dataChanged(index(0), index(m_list.size() - 1), {NbrRole, DescriptionRole});
-
@mzimmers That's not how I would write it.
Your for loop is quite synthetic but here's how I would do the samefor (ListItem& item : m_list) { // use range for loop, also get a reference to your items in your list item.nbr++; // edit the item reference in place QChar qc = QChar(item.nbr + 0x40); // ? not sure what you wanted to do there but I copied it item.description.append(qc); } // if you modify contiguous items at the same time, emit dataChanged once for all the range. emit dataChanged(index(0), index(m_list.size() - 1), {NbrRole, DescriptionRole});
@GrecKo I like some things about your approach better than mine, but it leaves a small issue: without a conventional loop, how do I know what row I'm working on?
In my real app, the updates are conditional; it would look a little more like this:
QList<int> changedRoles; for (ListItem &item: *m_list) { QModelIndex qmi = index(i, 0, QModelIndex()); // how to get row? if (something) { item.nbr++; changedRoles.append(NbrRole); } if (something else) { QChar qc = QChar(item.nbr + 0x40; item.description.append(qc); changedRoles.append(DescriptionRole); } if (changedRoles.size() > 0) { emit dataChanged(qmi, qmi, changedRoles); } }
Is there some magic way to get the row number from within your loop, so I can create a QModelIndex for the signal?
Thanks...
EDIT:
Evidently, C++20 would give me the ability to do what I was asking for, but since I'm using C++17, I guess I can just do it manually:
void ListModel::update() { QList<int> changedRoles; int row; row = 0; for (ListItem &item: *m_list) { QModelIndex qmi = index(row, 0, QModelIndex()); // update the temporary item. item.nbr++; changedRoles.append(NbrRole); QChar qc = QChar(item.nbr + 0x40); item.description.append(qc); changedRoles.append(DescriptionRole); if (changedRoles.size() > 0) { emit dataChanged(qmi, qmi, changedRoles); } row++; } }
@GrecKo I saw your comment about minimizing signals if contiguous items are changed, but in my real app, that's rarely going to be the case, and I think trying to reduce the number of signals would complicate the logic, so I'll probably leave that part as it is. Apart from this, how does this look to you?
-
@GrecKo I like some things about your approach better than mine, but it leaves a small issue: without a conventional loop, how do I know what row I'm working on?
In my real app, the updates are conditional; it would look a little more like this:
QList<int> changedRoles; for (ListItem &item: *m_list) { QModelIndex qmi = index(i, 0, QModelIndex()); // how to get row? if (something) { item.nbr++; changedRoles.append(NbrRole); } if (something else) { QChar qc = QChar(item.nbr + 0x40; item.description.append(qc); changedRoles.append(DescriptionRole); } if (changedRoles.size() > 0) { emit dataChanged(qmi, qmi, changedRoles); } }
Is there some magic way to get the row number from within your loop, so I can create a QModelIndex for the signal?
Thanks...
EDIT:
Evidently, C++20 would give me the ability to do what I was asking for, but since I'm using C++17, I guess I can just do it manually:
void ListModel::update() { QList<int> changedRoles; int row; row = 0; for (ListItem &item: *m_list) { QModelIndex qmi = index(row, 0, QModelIndex()); // update the temporary item. item.nbr++; changedRoles.append(NbrRole); QChar qc = QChar(item.nbr + 0x40); item.description.append(qc); changedRoles.append(DescriptionRole); if (changedRoles.size() > 0) { emit dataChanged(qmi, qmi, changedRoles); } row++; } }
@GrecKo I saw your comment about minimizing signals if contiguous items are changed, but in my real app, that's rarely going to be the case, and I think trying to reduce the number of signals would complicate the logic, so I'll probably leave that part as it is. Apart from this, how does this look to you?
Time for question #5:
I think I have my model working correctly now. I use it in a couple of QML screens. One works fine (I can get the fields I need):
ListView { model: spaceModel delegate: SpaceCard { titleText: name // one of my role names
and the name shows up in the SpaceCard just fine.
When I try to use it like this, though:
TabBar { Repeater { model: spaceModel delegate: TabButton { contentItem: Text { text: name // one of my role names
The name field remains blank. Could this be because I'm using a repeater to populate my TabBar -- from the docs:
The Repeater type creates all of its delegate items when the repeater is first created.
This idea doesn't really hold water, because my tab bar is indeed updating (verified by prepending a string to my "name" field, but...I can't think of anything else. Any ideas why this might not be properly populating the name field?
Thanks...