SOLVED: QStandardListModel + Data Update from QML
-
After a week of testing different possibilities on how to create a C++ model and call it from QML, I found this example and followed it.
My use case is to have 3 elements at all times with changing internal states which modify the enabled / visible properties. The problem is that on editing of an attribute, the other elements didn't notice a change.
This makes sense for me, as the
Q_PROPERTIES
of my FileItem don't have anNOTIFY
section (seefileitem.h
). This in turn needs aQ_OBJECT
macro and has to inherit fromQObject
, which makes theQVariant::fromValue(file_item)
(seefileitemlist.h:_add_file_item()
) throw a runtime exception.I'm kind of at a dead end here, because the implementation in pure QML worked with a simple
ListModel
andListView
by default. When porting to C++ this was the only tutorial to work without implementing the wholeQAbstractListModel
interface for a custom Item. This looks like a very manual task which does not seem like a best practice.Here's a video of the example functionality. The second list Button should have
enabled = true
after clicking the first Button. SeeExample.qml:Button:enabled/onClicked
implementation.fileitem.h
#ifndef FILEITEM_H #define FILEITEM_H #include <QObject> #include <QString> #include <QInternal> //! FileItem class which manages the name and meta data of a video file class FileItem { Q_GADGET Q_PROPERTY(QString name READ name WRITE setName ) // NOTIFY nameChanged) Q_PROPERTY(quint32 sizeMB READ sizeMB WRITE setSizeMB ) // NOTIFY sizeMBChanged) Q_PROPERTY(qint8 position READ position WRITE setPosition ) // NOTIFY positionChanged) Q_PROPERTY(QString container READ container WRITE setContainer ) // NOTIFY containerChanged) Q_PROPERTY(bool fileSelected READ fileSelected WRITE setFileSelected) // NOTIFY fileSelectionChanged) //! constructor public: //! default constructor creates a default initialization for each member //! file_selected is set to false FileItem() : _name("") , _size_mb(0) , _position(0) , _container("") , _file_selected(false) { } //! used for debugging FileItem(const QString & name) : _name(name) , _size_mb(0) , _position(0) , _container("Default container") , _file_selected(true) { } FileItem(const FileItem & other) = default; FileItem & operator=(const FileItem & other) = default; //! setter methods for Q_PROPERTY public: void setName(const QString & _other){ _name = _other; } // emit nameChanged(); } void setSizeMB(const quint32 & _other){ _size_mb = _other; } // emit sizeMBChanged(); } void setPosition(const quint8 & _other){ _position = _other; } // emit positionChanged(); } void setContainer(const QString & _other){ _container = _other; } // emit containerChanged(); } void setFileSelected(const bool & _other){ _file_selected = _other; } //emit fileSelectionChanged(); } //! getter methods for Q_PROPERTY public: QString name() const { return _name; } quint32 sizeMB() const { return _size_mb; } quint8 position() const { return _position; } QString container() const { return _container; } bool fileSelected() const { return _file_selected; } //! change signals for Q_PROPERTY //signals: // void nameChanged(); // void sizeMBChanged(); // void positionChanged(); // void containerChanged(); // void fileSelectionChanged(); //! member private: QString _name; // file path quint32 _size_mb; // size in megabyte of the selected file quint8 _position; // position in the file management list QString _container; // container type bool _file_selected; // is a file currently selected for this item }; #endif // FILEITEM_H
fileitemlist.h
#ifndef FILEITEMLIST_H #define FILEITEMLIST_H #include <QAbstractListModel> #include <QStandardItemModel> #include <QDebug> #include "fileitem.h" //! TODO class FileItemList : public QObject { Q_OBJECT Q_PROPERTY(QAbstractItemModel* model READ model CONSTANT) Q_DISABLE_COPY(FileItemList) //! constructor public: //! main constructor FileItemList(QObject* parent = nullptr) : QObject(parent) { _model = new QStandardItemModel(this); _model->insertColumn(0); _add_default_elements(); } // TODO destructor for _model (?) //! methods public: //! adds a default fileitem Q_SLOT void addDefaultFileItem() { const FileItem file_item; _add_file_item(file_item); } //! a custom get function to access another elements properties by row index Q_SLOT QVariant get(int row_index) { return _model->data( _model->index(row_index, 0)); } //! open the file and do stuff later Q_SLOT void openFile(int index, QString filename) { qDebug() << index << ", " << filename << '\n'; // open file and do stuff and verify if } //! remove the item at the index from the model Q_SLOT void removeItem(int index) { _model->removeRow(index); } //! getter methods for Q_PROPERTY public: //! model getter QAbstractItemModel * model() const { return _model;} //! methods private: //! add file item object void _add_file_item(const FileItem & file_item) { const int newRow = _model->rowCount(); _model->insertRow(newRow); _model->setData(_model->index(newRow,0) , QVariant::fromValue(file_item) , Qt::EditRole); } //! add three default items void _add_default_elements() { addDefaultFileItem(); addDefaultFileItem(); addDefaultFileItem(); } //! member private: //! the model which is used by a QML ListView QAbstractItemModel * _model; }; #endif // FILEITEMLIST_H
Example.qml
import QtQuick 2.0 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.12 ListView { id: listView anchors.fill: parent model: fileItemList.model delegate: Item { implicitHeight: text.height width: listView.width RowLayout { id: text Text { text: "Name: " + edit.name color: "#FFFFFF" } Text { text: "Container: " + edit.container color: "#FFFFFF" } Text { text: "Position: " + edit.position color: "#FFFFFF" } Text { text: "Index: " + index color: "#FFFFFF" } Button { text: "Click me!" enabled: { if (index === 0) { return true; } else if (index === 1) { return fileItemList.get(0).fileSelected; } else if (index === 2) { return fileItemList.get(0).fileSelected && fileItemList.get(1).fileSelected; } } onClicked: { edit.name = "Hello!" edit.fileSelected = true; } } } } }
main.cpp
#include <QApplication> #include <QQmlApplicationEngine> #include <QQuickStyle> #include <QQuickView> #include <QQmlContext> #include <QFontDatabase> #include "fileitemlist.h" int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication app(argc, argv); qmlRegisterType<FileItem>(); FileItemList file_item_list; QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("fileItemList", &file_item_list); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); }
-
Hi,
Since you are modifying the internal objects directly, the model can't know that this is happening and thus can't signal back that something has changed. You need to make your model emit dataChanged at some point.
-
Thank you for your answer! I did try to emit the
dataChanged
signal, but unfortunately the Buttons enabled property were still not "recomputed".The only way I got it working for now is by going the full
QAbstractListModel
way and trigger bothbeginResetModel(); endResetModel();
in an Q_INVOCABLE function after I set a state-changing property in an element. (seeExample.qml::Button::onClicked
implementation)The access to other elements of the model is still not done in a nice way, because I had to write a custom
isFileSelected(int index)
function with a forward to the internalFileItem::fileSelected()
method instead of accessing it like the QML model:model.get(index).fileSelected
This was my alternative to the problem provided by
QVariant::fromValue(FileItem)
when I wrote aQVariant get(int index)
method for the model as the convertion could not happen and I did not manage to register FileItem as a valid QML type (tried inheriting fromQObject
andQ_GADGET
).This is the resulting functionality (video).
fileitem.h
andmain.cpp
is the same as in the original post.
fileitemmodel.h
#ifndef FILEITEMMODEL_H #define FILEITEMMODEL_H #include <QAbstractTableModel> #include <QDebug> #include "fileitem.h" class FileItemModel : public QAbstractListModel { Q_OBJECT //! constructors public: //! FileItemModel(QObject * parent = nullptr) : QAbstractListModel(parent) { _setup_role_names(); appendDefaultFileItem(); appendDefaultFileItem(); appendDefaultFileItem(); } //! enum FileItemRoles { NameRole = Qt::UserRole , SizeMBRole = Qt::UserRole + 1 , PositionRole = Qt::UserRole + 2 , ContainerRole = Qt::UserRole + 3 , FileSelectedRole = Qt::UserRole + 4 }; //! methods public: //! number of elements in the _file_item_list which correlate to the rows int rowCount(const QModelIndex & parent = QModelIndex()) const override { Q_UNUSED(parent) return _file_item_list.size(); } //! a getter for the _role_names to enable the access via QML QHash<int, QByteArray> roleNames() const override { return _role_names; } //! TODO Q_INVOKABLE QVariant isFileSelected(int index) { return QVariant::fromValue(_file_item_list.at(index).fileSelected()); } //! TODO Q_INVOKABLE void remove(int index) { emit beginRemoveRows(QModelIndex(), index, index); _file_item_list.removeAt(index); emit endRemoveRows(); } //! QVariant data(const QModelIndex & index,int role) const override { int row = index.row(); // if the index is out of bounds, return QVariant if(row < 0 || row >= _file_item_list.size()) { return QVariant(); } // otherwise get the item const FileItem & file_item = _file_item_list.at(row); // check which member is accessed and return accordingly switch (role) { case NameRole: return file_item.name(); case SizeMBRole: return file_item.sizeMB(); case PositionRole: return file_item.position(); case ContainerRole: return file_item.container(); case FileSelectedRole: return file_item.fileSelected(); default: return QVariant(); } } //! bool setData(const QModelIndex & index, const QVariant & value, int role) override { FileItem & file_item = _file_item_list[index.row()]; if (role == NameRole) file_item.setName(value.toString()); else if (role == SizeMBRole) file_item.setSizeMB(value.toUInt()); else if (role == PositionRole) file_item.setPosition(static_cast<quint8>(value.toUInt())); else if (role == ContainerRole) file_item.setContainer(value.toString()); else if (role == FileSelectedRole) file_item.setFileSelected(value.toBool()); else return false; emit dataChanged(index, index); // <- this does not trigger a recompution of the view return true ; } //! tells the views that the model's state has changed -> this triggers a "recompution" of the delegate Q_INVOKABLE void resetModel() { beginResetModel(); endResetModel(); } //! adds a default fileitem Q_INVOKABLE void appendDefaultFileItem() { const FileItem file_item; _append_file_item(file_item); } //! methods private: //! Set names to the role name hash container (QHash<int, QByteArray>) //! model.name, model.sizeMB, model.position, model.container, model.fileSelected void _setup_role_names() { _role_names[NameRole] = "name"; _role_names[SizeMBRole] = "sizeMB"; _role_names[PositionRole] = "position"; _role_names[ContainerRole] = "container"; _role_names[FileSelectedRole] = "fileSelected"; } //! add file item object void _append_file_item(const FileItem file_item) { int new_row = rowCount(); emit beginInsertRows(QModelIndex(), new_row, new_row); _file_item_list.append(file_item); emit endInsertRows(); } //! member private: //! TODO QList<FileItem> _file_item_list; //! TODO QHash<int, QByteArray> _role_names; }; #endif // FILEITEMMODEL_H
Example.qml
import QtQuick 2.0 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.12 ListView { id: listView anchors.fill: parent model: fileItemModel delegate: Item { implicitHeight: text.height width: listView.width RowLayout { id: text Text { text: "Name: " + model.name color: "#FFFFFF" } Text { text: "Container: " + model.container color: "#FFFFFF" } Text { text: "Position: " + model.position color: "#FFFFFF" } Text { text: "Index: " + index color: "#FFFFFF" } Button { text: "Click me!" enabled: { if (index === 0) { return true; } else if (index === 1) { return fileItemModel.isFileSelected(0); } else if (index === 2) { return fileItemModel.isFileSelected(0) && fileItemModel.isFileSelected(1); } else return false; } onClicked: { model.name = "Hello!"; model.fileSelected = true; fileItemModel.resetModel(); } } Button { text: "Remove it!" onClicked: { fileItemModel.remove(index); fileItemModel.appendDefaultFileItem(); } } } } }
-
@cirquit said in QStandardListModel + Data Update from QML:
dataChanged
And if you pass a vector with the role modified ?
-
When I remove the
dataChanged(index,index)
no update gets recognized.
When I manually set the role vector todataChanged(index, index, role)
it behaves the same way as without therole
specification (updates the current element, not the other ones).As per https://forum.qt.io/topic/39357/solved-qabstractitemmodel-datachanged-question/6 , I noticed that the other elements have to get a "recompute" signal and tried the following:
bool setData(const QModelIndex & index, const QVariant & value, int role) override { // ... QModelIndex toIndex(createIndex(rowCount() - 1, index.column())); qDebug() << toIndex.row() << ',' << toIndex.column(); emit dataChanged(index, toIndex); }
Which should've helped and would've made sense as it helped in the other thread, but it didn't trigger a recomputation :( I found this blogpost which discusses the problem at hand, but solves it in QML only because he would use the
dataChanged
signal in C++.EDIT: Marking this question as solved as my solution with the
resetModel()
worked. I had to implement a similar functionality with the same model, which was also dependent on thedataChanged
signal, but it worked withoutresetModel
. The only difference was that this functionality was encapsulated in a single "Item", e.g (color choose dialog -> change multiple textfields in the same "row").