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 an NOTIFY section (see fileitem.h). This in turn needs a Q_OBJECT macro and has to inherit from QObject, which makes the QVariant::fromValue(file_item) (see fileitemlist.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 and ListView by default. When porting to C++ this was the only tutorial to work without implementing the whole QAbstractListModel 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. See Example.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();
    }
    
    

  • Lifetime Qt Champion

    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 both beginResetModel(); endResetModel(); in an Q_INVOCABLE function after I set a state-changing property in an element. (see Example.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 internal FileItem::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 a QVariant 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 from QObject and Q_GADGET).

    This is the resulting functionality (video).

    fileitem.h and main.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();
                    }
                }
            }
        }
    }
    

  • Lifetime Qt Champion

    @cirquit said in QStandardListModel + Data Update from QML:

    dataChanged

    And if you pass a vector with the role modified ?



  • @SGaist

    When I remove the dataChanged(index,index) no update gets recognized.
    When I manually set the role vector to dataChanged(index, index, role) it behaves the same way as without the role 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 the dataChanged signal, but it worked without resetModel. 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").


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.