Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. QML and Qt Quick
  4. SOLVED: QStandardListModel + Data Update from QML
QtWS25 Last Chance

SOLVED: QStandardListModel + Data Update from QML

Scheduled Pinned Locked Moved Unsolved QML and Qt Quick
qmlqabstractlistmoc++
5 Posts 2 Posters 2.8k Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • C Offline
    C Offline
    cirquit
    wrote on 11 May 2019, 22:30 last edited by cirquit
    #1

    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();
    }
    
    
    1 Reply Last reply
    0
    • SGaistS Offline
      SGaistS Offline
      SGaist
      Lifetime Qt Champion
      wrote on 12 May 2019, 20:55 last edited by
      #2

      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.

      Interested in AI ? www.idiap.ch
      Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

      1 Reply Last reply
      0
      • C Offline
        C Offline
        cirquit
        wrote on 13 May 2019, 07:22 last edited by cirquit
        #3

        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();
                        }
                    }
                }
            }
        }
        
        1 Reply Last reply
        0
        • SGaistS Offline
          SGaistS Offline
          SGaist
          Lifetime Qt Champion
          wrote on 13 May 2019, 21:20 last edited by
          #4

          @cirquit said in QStandardListModel + Data Update from QML:

          dataChanged

          And if you pass a vector with the role modified ?

          Interested in AI ? www.idiap.ch
          Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

          C 1 Reply Last reply 14 May 2019, 07:59
          0
          • SGaistS SGaist
            13 May 2019, 21:20

            @cirquit said in QStandardListModel + Data Update from QML:

            dataChanged

            And if you pass a vector with the role modified ?

            C Offline
            C Offline
            cirquit
            wrote on 14 May 2019, 07:59 last edited by cirquit
            #5

            @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").

            1 Reply Last reply
            0

            1/5

            11 May 2019, 22:30

            • Login

            • Login or register to search.
            1 out of 5
            • First post
              1/5
              Last post
            0
            • Categories
            • Recent
            • Tags
            • Popular
            • Users
            • Groups
            • Search
            • Get Qt Extensions
            • Unsolved