qtlistviewdrag-and-dropmodel-view

Qt: How to implement simple internal drag&drop for reordering items in QListView using a custom model


I have a QList of custom structs and i'm using custom model class (subclass of QAbstractListModel) to display those structs in 1-dimensional QListView. I have overriden the methodsrowCount, flags and data to construct a display string from the struct elements.

Now i would like to enable internal drag&drop to be able to reorder the items in the list by dragging them and dropping them between some other items, but this task seems unbeliavably complicated. What exactly do i need to override and what parameters do i need to set? I tried a lot of things, i tried

view->setDragEnabled( true );
view->setAcceptDrops( true );
view->setDragDropMode( QAbstractItemView::InternalMove );
view->setDefaultDropAction( Qt::MoveAction );

I tried

Qt::DropActions supportedDropActions() const override {
    return Qt::MoveAction;
}
Qt::ItemFlags flags( const QModelIndex & index ) const override{
    return QAbstractItemModel::flags( index ) | Qt::ItemIsDragEnabled;
}

I tried implementing insertRows and removeRows, but it still doesn't work.

I haven't found a single example of a code doing exactly that. The official documentation goes very deeply into how view/model pattern works and how to make drag&drops from external apps or from other widgets, but i don't want any of that. I only want simple internal drag&drop for manual reordering of the items in that one list view.

Can someone please help me? Or i'll get nuts from this.

EDIT: adding insertRows/removeRows implementation on request:

bool insertRows( int row, int count, const QModelIndex & parent ) override
{
    QAbstractListModel::beginInsertRows( parent, row, row + count - 1 );

    for (int i = 0; i < count; i++)
        AObjectListModel<Object>::objectList.insert( row, Object() );

    QAbstractListModel::endInsertRows();
    return true;
}

bool removeRows( int row, int count, const QModelIndex & parent ) override
{
    if (row < 0 || row + count > AObjectListModel<Object>::objectList.size())
        return false;

    QAbstractListModel::beginRemoveRows( parent, row, row + count - 1 );

    for (int i = 0; i < count; i++)
        AObjectListModel<Object>::objectList.removeAt( row );

    QAbstractListModel::endRemoveRows();
    return true;
}

objectList is QList where Object is template parameter.


Solution

  • When you want to reorganize items in a custom model, you have to implement all needed actions: - how to insert and remove a row - how to get and set data - how to serialize items (build the mimedata) - how to unserialize items

    An example with a custom model with a QStringList as data source:

    The minimal implementation of the model should be:

    class CustomModel: public QAbstractListModel
    {
    public:
        CustomModel()
        {
            internalData = QString("abcdefghij").split("");
        }
        int rowCount(const QModelIndex &parent) const
        {
            return internalData.length();
        }
        QVariant data(const QModelIndex &index, int role) const
        {
            if (!index.isValid() || index.parent().isValid())
                return QVariant();
            if (role != Qt::DisplayRole)
                return QVariant();
            return internalData.at(index.row());
        }
    private:
        QStringList internalData;   
    };
    

    We have to add the way to insert/remove rows and set the data:

        bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole)
        {
            if (role != Qt::DisplayRole)
                return false;
            internalData[index.row()] = value.toString();
            return true;
        }
        bool insertRows(int row, int count, const QModelIndex &parent)
        {
            if (parent.isValid())
                return false;
            for (int i = 0; i != count; ++i)
                internalData.insert(row + i, "");
            return true;
        }
        bool removeRows(int row, int count, const QModelIndex &parent)
        {
            if (parent.isValid())
                return false;
            beginRemoveRows(parent, row, row + count - 1);
            for (int i = 0; i != count; ++i)
                internalData.removeAt(row);
            endRemoveRows();
            return true;
        }
    

    For the drag and drop part:

    First, we need to define a mime type to define the way we will deserialize the data:

        QStringList mimeTypes() const
        {
            QStringList types;
            types << CustomModel::MimeType;
            return types;
        }
    

    Where CustomModel::MimeType is a constant string like "application/my.custom.model"

    The method canDropMimeData will be used to check if the dropped data are legit or not. So, we can discard external data:

        bool canDropMimeData(const QMimeData *data,
            Qt::DropAction action, int /*row*/, int /*column*/, const QModelIndex& /*parent*/)
        {
            if ( action != Qt::MoveAction || !data->hasFormat(CustomModel::MimeType))
                return false;
            return true;
        }
    

    Then, we can create our mime data based on the internal data:

        QMimeData* mimeData(const QModelIndexList &indexes) const
        {
            QMimeData* mimeData = new QMimeData;
            QByteArray encodedData;
    
            QDataStream stream(&encodedData, QIODevice::WriteOnly);
    
            for (const QModelIndex &index : indexes) {
                if (index.isValid()) {
                    QString text = data(index, Qt::DisplayRole).toString();
                    stream << text;
                }
            }
            mimeData->setData(CustomModel::MimeType, encodedData);
            return mimeData;
        }
    

    Now, we have to handle the dropped data. We have to deserialize the mime data, insert a new row to set the data at the right place (for a Qt::MoveAction, the old row will be automaticaly removed. That why we had to implement removeRows):

    bool dropMimeData(const QMimeData *data,
            Qt::DropAction action, int row, int column, const QModelIndex &parent)
        {
            if (!canDropMimeData(data, action, row, column, parent))
                return false;
    
            if (action == Qt::IgnoreAction)
                return true;
            else if (action  != Qt::MoveAction)
                return false;
    
            QByteArray encodedData = data->data("application/my.custom.model");
            QDataStream stream(&encodedData, QIODevice::ReadOnly);
            QStringList newItems;
            int rows = 0;
    
            while (!stream.atEnd()) {
                QString text;
                stream >> text;
                newItems << text;
                ++rows;
            }
    
            insertRows(row, rows, QModelIndex());
            for (const QString &text : qAsConst(newItems))
            {
                QModelIndex idx = index(row, 0, QModelIndex());
                setData(idx, text);
                row++;
            }
    
            return true;
        }
    

    If you want more info on the drag and drop system in Qt, take a look at the documentation.