7

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.

Youda008
  • 1,788
  • 1
  • 17
  • 35

3 Answers3

11

In addition to the Romha's great answer, i would like to supplement few more details about how it works and what's confusing on it.

The official documentation says the QAbstractItemModel has default implementations of mimeTypes, mimeData and dropMimeData which should work for internal move and copy operations as long as you correctly implement data, setData, insertRows and removeRows.

And from certain point of view, they were right. It does work without overriding mimeData and dropMimeData, but only when your underlying data structure contains only single strings, those that are returned from data and received in setData as DisplayRole. When you have a list of compound objects (like i have) with multiple elements, only one of which is used for the DisplayRole, for example

struct Elem {
    QString name;
    int i;
    bool b;
}

QVariant data( const QModelIndex & index, int role ) const override
{
    return objectList[ index.row() ].name;
}
bool setData( const QModelIndex & index, const QVariant & value, int role ) override
{
    objectList[ index.row() ].name = value.toString();
}

then the default implementations will actually do this

QVariant data = data( oldIndex, Qt::DisplayRole );
insertRows( newIndex, 1 )
setData( newIndex, data, Qt::DisplayRole )
removeRows( oldIndex, 1 )

and therefore only correctly move the names and leave the rest of the struct as is. Which makes sense now, but the system is so complicated that i didn't realize it before.

Therefore custom mimeData and dropMimeData are required to move the whole content of the structs

Youda008
  • 1,788
  • 1
  • 17
  • 35
10

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.

Dimitry Ernot
  • 6,256
  • 2
  • 25
  • 37
  • 1
    Awesome, thank's for this detailed answer. You're my saviour. The documentation says the QAbstractListModel has default implementations of `mimeData` and `dropMimeData` which work well in most cases as long as you correctly implement `data`, `setData`, `insertRows` and `removeRows`. That's what confused me. After some more fiddling i found out how they meant it and why it doesn't work that way. I will write a complementary answer that will explain it. – Youda008 Jun 30 '19 at 18:07
  • Feel free to edit my answer if it's wrong or if it can be improved. – Dimitry Ernot Jun 30 '19 at 18:21
  • It's not wrong. I only have one question: Why do you check if parent index is valid and abort in those cases? – Youda008 Jun 30 '19 at 18:45
  • The `parent` is valid for structure based on a tree and the model is a list. So, the model cannot handle an item with a parent. – Dimitry Ernot Jun 30 '19 at 19:04
5

Here is a evidenced example for you ,but in Python:

import sys
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import (Qt, QStringListModel, QModelIndex,
                          QMimeData, QByteArray, QDataStream, QIODevice)
from PySide6.QtWidgets import (QApplication, QMainWindow, QListView, QAbstractItemView, QPushButton, QVBoxLayout, QWidget)


class DragDropListModel(QStringListModel):
    def __init__(self, parent=None):
        super(DragDropListModel, self).__init__(parent)
        # self.myMimeTypes = 'application/vnd.text.list' # 可行

        # self.myMimeTypes = "text/plain" # 可行
        self.myMimeTypes = 'application/json'  # 可行

    def supportedDropActions(self):
        # return Qt.CopyAction | Qt.MoveAction  # 拖动时复制并移动相关项目
        return Qt.MoveAction  # 拖动时移动相关项目

    def flags(self, index):
        defaultFlags = QStringListModel.flags(self, index)

        if index.isValid():
            return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultFlags
        else:
            return Qt.ItemIsDropEnabled | defaultFlags

    def mimeTypes(self):
        return [self.myMimeTypes]

    # 直接将indexes里面对应的数据取出来,然后打包进了QMimeData()对象,并返回
    def mimeData(self, indexes):
        mmData = QMimeData()
        encodedData = QByteArray()
        stream = QDataStream(encodedData, QIODevice.WriteOnly)

        for index in indexes:
            if index.isValid():
                text = self.data(index, Qt.DisplayRole)
                stream << text  # 测试,也行
                # stream.writeQString(str(text))  # 原始, 可行

        mmData.setData(self.myMimeTypes, encodedData)
        return mmData

    def canDropMimeData(self, data, action, row, column, parent):
        if data.hasFormat(self.myMimeTypes) is False:
            return False
        if column > 0:
            return False
        return True

    def dropMimeData(self, data, action, row, column, parent):
        if self.canDropMimeData(data, action, row, column, parent) is False:
            return False

        if action == Qt.IgnoreAction:
            return True

        beginRow = -1
        if row != -1:  # 表示
            print("case 1: ROW IS NOT -1, meaning inserting in between, above or below an existing node")
            beginRow = row
        elif parent.isValid():
            print("case 2: PARENT IS VALID, inserting ONTO something since row was not -1, "
                  "beginRow becomes 0 because we want to "
                  "insert it at the beginning of this parents children")
            beginRow = parent.row()
        else:
            print("case 3: PARENT IS INVALID, inserting to root, "
                  "can change to 0 if you want it to appear at the top")
            beginRow = self.rowCount(QModelIndex())
        print(f"row={row}, beginRow={beginRow}")

        encodedData = data.data(self.myMimeTypes)
        stream = QDataStream(encodedData, QIODevice.ReadOnly)
        newItems = []
        rows = 0

        while stream.atEnd() is False:
            text = stream.readQString()
            newItems.append(str(text))
            rows += 1

        self.insertRows(beginRow, rows, QModelIndex())  # 先插入多行
        for text in newItems:  # 然后给每一行设置数值
            idx = self.index(beginRow, 0, QModelIndex())
            self.setData(idx, text)
            beginRow += 1

        return True


class DemoDragDrop(QWidget):
    def __init__(self, parent=None):
        super(DemoDragDrop, self).__init__(parent)

        # 设置窗口标题
        self.setWindowTitle('drag&drop in PySide6')
        # 设置窗口大小
        self.resize(480, 320)

        self.initUi()

    def initUi(self):
        self.vLayout = QVBoxLayout(self)
        self.listView = QListView(self)
        self.listView.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.listView.setDragEnabled(True)
        self.listView.setAcceptDrops(True)
        self.listView.setDropIndicatorShown(True)
        self.ddm = DragDropListModel()  # 该行和下面4行的效果类似
        # self.listView.setDragDropMode(QAbstractItemView.InternalMove)
        # self.listView.setDefaultDropAction(Qt.MoveAction)
        # self.listView.setDragDropOverwriteMode(False)
        # self.ddm = QStringListModel()

        self.ddm.setStringList(['Item 1', 'Item 2', 'Item 3', 'Item 4'])
        self.listView.setModel(self.ddm)

        self.printButton = QPushButton("Print")

        self.vLayout.addWidget(self.listView)
        self.vLayout.addWidget(self.printButton)

        self.printButton.clicked.connect(self.printModel)

    def printModel(self):  # 验证移动view中项目后,背后model中数据也发生了移动
        print(self.ddm.data(self.listView.currentIndex()))


if __name__ == '__main__':

    app = QApplication(sys.argv)
    app.setStyle('fusion')
    window = DemoDragDrop()
    window.show()
    sys.exit(app.exec_())
bactone
  • 107
  • 1
  • 8