Inserting/Deleting Items in a drag&drop QML listView with cpp model



  • Greetings,

    I tried to add to a ListView in QML of N Items a way to add and delete a new Item at a given index.

    I did the following example, but the problem is that when I move some Items, when I try to insert a new one, the position might be incorrect and I have no clue why. When I check my DataList in my cpp model, positions are correct, however, new or deleted items won't be inserted/deleted at the right position.

    It seems that the error occurs when I insert a new Item, then I move it , and then I try to delete this Item or insert an Item next to this New Item.

    Here is a simple example (you can run it if you need). I called my Items Data : Blocks

    #include "mainwindow.h"
    #include <QApplication>
    #include <QtQml>
    #include <QQuickView>
    #include <QQuickWidget>
    #include <QQmlApplicationEngine>
    int main(int argc, char *argv[])
    {
    
        QApplication a(argc, argv);
        MainWindow w;
        w.show();
    
    
        return a.exec();
    }
    

    main.cpp

    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include <QMainWindow>
    #include "model.h"
    
    namespace Ui {
    class MainWindow;
    }
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        explicit MainWindow(QWidget *parent = 0);
        void addItem(int index);
        ~MainWindow();
    
    private slots:
    
    
    private:
        QList<QObject*> dataList;
        Ui::MainWindow *ui;
        BlockModel model;
        int cpt = 0;
    };
    
    #endif // MAINWINDOW_H
    

    mainwindow.h

    #include <QtQml>
    #include <QQuickView>
    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    #include "QQuickWidget"
    #include <QStringList>
    
    
    MainWindow::MainWindow(QWidget *parent) :
        QMainWindow(parent),
        ui(new Ui::MainWindow)
    {
    
         int nbItems = 5;
    
         for(; cpt < nbItems; cpt ++) {
             Block a = Block(QString("Item ")+QString::number(cpt));
             model.addBlock(a);
         }
    
        ui->setupUi(this);
    
        QQuickWidget *view = new QQuickWidget;
        QQmlContext *ctxt = view->rootContext();
    
        ctxt->setContextProperty("myModel", &model);
        view->setSource(QUrl::fromLocalFile("main.qml"));
        view->setGeometry(0, 200, 600, 400);
        view->setResizeMode(QQuickWidget::SizeRootObjectToView);
        ui->dockWidget_3->setWidget(view);
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    

    mainwindow.cpp

    #include <QAbstractListModel>
    #include <QStringList>
    #include <qqmlcontext.h>
    #include <QDebug>
    #include <QStringList>
    
    //![0]
    class Block
    {
    public:
        Block(){
        }
    
        Block(const QString &name);
    
        QString nameBlock() const;
    
        void setName(QString n) {
            m_name = n;
        }
    
    private:
        QString m_name;
    };
    
    class BlockModel : public QAbstractListModel
    {
        Q_OBJECT
    public:
    
        Block* getBlock(QString name);
    
        Q_INVOKABLE void moveBlock(int from,int to);
        Q_INVOKABLE void insertBlock(int index);
        Q_INVOKABLE void deleteBlock(int index);
    
        enum BlockRoles {
            nameRole = Qt::UserRole + 1,
        };
    
        BlockModel(QObject *parent = 0);
    
        void setContext(QQmlContext *ctx) {
            m_ctx = ctx;
        }
    
        void setName(const QString &name);
    
        void addBlock(const Block &Block);
    
        int rowCount(const QModelIndex & parent = QModelIndex()) const;
    
        QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
        QHash<int, QByteArray> roleNames() const;
    
    private:
        QList<Block> m_blocks;
        QQmlContext*  m_ctx;
        int cpt = 0;
    };
    

    mode.h

    #include "model.h"
    #include "qDebug"
    Block::Block(const QString &name)
        : m_name(name)
    {
    }
    
    
    
    QString Block::nameBlock() const
    {
        return m_name;
    }
    
    
    BlockModel::BlockModel(QObject *parent)
        : QAbstractListModel(parent)
    {
    }
    
    void BlockModel::addBlock(const Block &Block)
    {
        beginInsertRows(QModelIndex(), rowCount(), rowCount());
        m_blocks << Block;
        endInsertRows();
    }
    
    
    
    int BlockModel::rowCount(const QModelIndex & parent) const {
        Q_UNUSED(parent);
        return m_blocks.count();
    }
    
    void BlockModel::moveBlock(int from, int to) {
         m_blocks.move(from,to);
    }
    
    void BlockModel::insertBlock(int index) {
        Block b =(Block(QString("New Item ")+QString::number(cpt)));
        beginInsertRows(QModelIndex(),index+1,index+1);
        m_blocks.insert(index+1,b);
        endInsertRows();
        cpt++;
    }
    
    void BlockModel::deleteBlock(int index) {
        beginRemoveRows(QModelIndex(),index,index);
        m_blocks.removeAt(index);
        endRemoveRows();
    }
    
    QVariant BlockModel::data(const QModelIndex & index, int role) const {
        if (index.row() < 0 || index.row() >= m_blocks.count())
            return QVariant();
    
        const Block &Block = m_blocks[index.row()];
        if (role == nameRole)
            return Block.nameBlock();
    
        return QVariant();
    }
    
    //![0]
    QHash<int, QByteArray> BlockModel::roleNames() const {
        QHash<int, QByteArray> roles;
    
        roles[nameRole] = "nameBlock";
    
        return roles;
    }
    

    model.cpp

    import QtQuick 2.7
    import QtQuick.Controls 1.4
    import QtQuick.Window 2.2
    import QtQuick.Dialogs 1.2
    import QtQuick.Layouts 1.2
    import QtQml.Models 2.2
    import QtQuick.Controls.Styles 1.4
    
    Rectangle {
        id : rootRectangle
        visible: true
        ScrollView {
            anchors.fill:parent
            ListView{
                id: root
                width: parent.width; height: parent.height
                property int visualIndex: -1
    
                displaced: Transition {
                    NumberAnimation { properties: "y"; easing.type: Easing.OutQuad }
                }
    
                model: DelegateModel {
    
                    id: visualModel
                    model: myModel
                    delegate: Component {
                        MouseArea {
    
                            id: delegateRoot
    
                            property int visualIndex: DelegateModel.itemsIndex
                            cursorShape: Qt.PointingHandCursor
                            width: root.width; height: 100
    
                            drag.target:  icon
                            drag.axis: Drag.YAxis
    
                            Behavior on height {
                                PropertyAnimation { duration: 100 }
                            }
    
                            Rectangle {
                                anchors.top:  delegateRoot.top
                                anchors.left: delegateRoot.left
                                id: icon
                                objectName: nameBlock
                                width: root.width-5; height: 100
                                color:  "skyblue"
    
                                radius: 3
                                Text {
                                    objectName: "rect"
                                    id: title
                                    anchors.fill: parent
                                    anchors.margins: 10
                                    horizontalAlignment: Text.AlignLeft
                                    verticalAlignment: Text.AlignVCenter
                                    text: nameBlock
                                }
    
                                Drag.active: delegateRoot.drag.active
                                Drag.source: delegateRoot
                                Drag.hotSpot.x: 36
                                Drag.hotSpot.y: 36
    
                                    Button {
                                        id : buttonAdd
                                        text: "Add Block"
    
                                        anchors{
                                            right: parent.right
                                            top: parent.top
                                            bottom: parent.bottom
                                            margins: 30
                                        }
    
    
                                        onClicked: {
                                            myModel.insertBlock(visualIndex)
                                        }
                                    }
    
                                    Button {
                                        id : buttonDelete
                                        text: "Delete Block"
                                        anchors{
                                            right: buttonAdd.left
                                            top: parent.top
                                            bottom: parent.bottom
                                            margins: 30
                                        }
                                        onClicked: {
                                            myModel.deleteBlock(visualIndex)
                                        }
                                    }
    
    
                                states: [
                                    State {
                                        when: icon.Drag.active
                                        ParentChange {
                                            target: icon
                                            parent: root
                                        }
                                        AnchorChanges {
                                            target: icon;
                                            anchors.horizontalCenter: undefined;
                                            anchors.verticalCenter: undefined
                                        }
                                    }
                                ]
    
                                transitions: Transition {
                                    // Make the state changes smooth
                                    ParallelAnimation {
                                        ColorAnimation { property: "color"; duration: 500 }
                                        NumberAnimation { duration: 300; properties: "detailsOpacity,x,contentY,height,width,font.pixelSize,font.bold,visible" }
                                    }
                                }
                            }
    
                            DropArea {
                                anchors { fill: parent; margins: 15 }
                                onEntered: {
                                    visualModel.items.move(drag.source.visualIndex, delegateRoot.visualIndex)
                                    myModel.moveBlock(drag.source.visualIndex,delegateRoot.visualIndex)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    

    main.qml

    Do you have any idea of what I am doing wrong ?
    Thanks a lot and have a good day !



    • Remove all the +1 in BlockModel::insertBlock
    • add calls to beginMoveRows and endMoveRows in BlockModel::moveBlock


  • @VRonin
    About index+1, I need to insert the new Block after the currentBlock. But even if I write index, it doesn't work.

    About beginMoveRows, I already tried that but if I add beginMoveRows, it will cause the application to crash.

    void BlockModel::moveBlock(int from, int to) {
             beginMoveRows(QModelIndex(),from,from,QModelIndex(),to);
             m_blocks.move(from,to);
             endMoveRows();
    }
    

    I think it is because I move my blocks from the QML because I need to use some move animations.
    Here it is :

    DropArea {
         anchors { fill: parent; margins: 15 }
         onEntered: {
                visualModel.items.move(drag.source.visualIndex, delegateRoot.visualIndex)
                myModel.moveBlock(drag.source.visualIndex,delegateRoot.visualIndex)
         }
     }
    


  • The problem is that you move items twice (in ListView's visual model - visualModel.items.move- and data model - myModel.moveBlock) and the state eventually becomes inconsistent.

    I suggest the following solution:

    In the main.qml remove the visualModel.items.move:

    DragArea  {
       ...
       onEntered: {
          myModel.moveBlock(drag.source.visualIndex, delegateRoot.visualIndex)
       }
    }
    

    In the model.cpp:

    void BlockModel::moveBlock(int from, int to) {
        if (from != to)
        {
             // read beginMoveRows documentation why (from < to) ? to + 1 : to
             beginMoveRows(QModelIndex(), from, from, QModelIndex(), (from < to) ? to + 1 : to);
             m_blocks.move(from, to);
             endMoveRows();
        }
    }
    


  • Hello,

    I just read what you said in the documentation, for me it is strange way to implement the "move feature", I don't understand why they made this choice.

    But now it works fine thanks to you, have a good day !



  • Fheanor, just to make things clear. Logically, there is nothing wrong with your original code at all! It's only the visual representation that gets messed up (BlockModel::moveBlock() doesn't inform visual engine that items have been swapped and engine uses old/wrong - cached - names). The underlying data model is perfectly consistent all the time. For example, when you move and delete some recently added item, it only seems like the wrong item has been deleted.

    You can check this by yourself - the following code will force repaint on every drop and now you can see the actual model data:

    //--- model.h ---
    class BlockModel : public QAbstractListModel
    {
    public:
       ...
       Q_INVOKABLE void forceRepaint();
       ... 
    
    
    //--- model.cpp ---
    void BlockModel::forceRepaint() {
        layoutAboutToBeChanged();
        layoutChanged();
    }
    
    
    //--- main.qml ---
    ...
        delegate: Component {
            MouseArea {
                ...
                onReleased: {
                    // force repaint
                    myModel.forceRepaint()
               }
    


  • Hello Ray.
    Sorry for the late answer, but you are right. It was the visual representation that was wrong. I should have been more careful when reading the doc :-)
    Now I fixed my problem thanks to you, have a good day !


Log in to reply