Qt:如何使用自定义模型实现简单的内部拖放以重新排序 QListView 中的项目

You*_*008 4 qt listview drag-and-drop model-view

我有一个QList自定义结构,我正在使用自定义模型类( 的子类QAbstractListModel)在一维 QListView 中显示这些结构。我已经覆盖了方法rowCountflagsdata从结构元素构造了一个显示字符串。

现在我想启用内部拖放功能,以便能够通过将它们拖放到其他项目之间来重新排序列表中的项目,但这项任务似乎非常复杂。我究竟需要覆盖什么以及我需要设置哪些参数?我尝试了很多东西,我试过了

view->setDragEnabled( true );
view->setAcceptDrops( true );
view->setDragDropMode( QAbstractItemView::InternalMove );
view->setDefaultDropAction( Qt::MoveAction );
Run Code Online (Sandbox Code Playgroud)

我试过

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

我尝试实施insertRowsand removeRows,但它仍然不起作用。

我还没有找到完全这样做的代码示例。官方文档非常深入地介绍了视图/模型模式的工作原理以及如何从外部应用程序或其他小部件进行拖放,但我不想要任何这些。我只需要简单的内部拖放操作来手动重新排序该列表视图中的项目。

有人可以帮帮我吗?否则我会因此而发疯。

编辑:根据要求添加 insertRows/removeRows 实现:

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;
}
Run Code Online (Sandbox Code Playgroud)

objectList 是 QList,其中 Object 是模板参数。

Rom*_*rev 7

当您想在自定义模型中重新组织项目时,您必须实现所有需要的操作: - 如何插入和删除行 - 如何获取和设置数据 - 如何序列化项目(构建 mimedata) - 如何反序列化项目

带有QStringList作为数据源的自定义模型的示例:

模型的最小实现应该是:

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;   
};
Run Code Online (Sandbox Code Playgroud)

我们必须添加插入/删除行和设置数据的方式:

    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;
    }
Run Code Online (Sandbox Code Playgroud)

对于拖放部分:

首先,我们需要定义一个 mime 类型来定义反序列化数据的方式:

    QStringList mimeTypes() const
    {
        QStringList types;
        types << CustomModel::MimeType;
        return types;
    }
Run Code Online (Sandbox Code Playgroud)

CustomModel::MimeType一个常量字符串在哪里"application/my.custom.model"

该方法canDropMimeData将用于检查删除的数据是否合法。所以,我们可以丢弃外部数据:

    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;
    }
Run Code Online (Sandbox Code Playgroud)

然后,我们可以根据内部数据创建我们的 mime 数据:

    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;
    }
Run Code Online (Sandbox Code Playgroud)

现在,我们必须处理丢弃的数据。我们必须反序列化 mime 数据,插入一个新行以将数据设置在正确的位置(对于 a Qt::MoveAction,旧行将被自动删除。这就是我们必须实现的原因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;
    }
Run Code Online (Sandbox Code Playgroud)

如果您想了解有关 Qt 中拖放系统的更多信息,请查看文档


You*_*008 6

除了 Romha 的出色回答之外,我还想补充一些关于它如何工作以及它有哪些令人困惑的细节。

官方文件说QAbstractItemModel有默认的实现mimeTypesmimeDatadropMimeData应该对内部工作移动和复制操作,只要你正确地贯彻执行datasetDatainsertRowsremoveRows

从某些角度来看,他们是对的。它确实可以在不覆盖mimeDataand 的情况下工作dropMimeData,但仅当您的底层数据结构仅包含单个字符串时,即作为 DisplayRole返回data和接收的字符串setData。例如,当您有一个包含多个元素的复合对象列表(例如我有)时,其中只有一个用于 DisplayRole

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();
}
Run Code Online (Sandbox Code Playgroud)

那么默认实现实际上会这样做

QVariant data = data( oldIndex, Qt::DisplayRole );
insertRows( newIndex, 1 )
setData( newIndex, data, Qt::DisplayRole )
removeRows( oldIndex, 1 )
Run Code Online (Sandbox Code Playgroud)

因此,只有正确移动名称并保持结构的其余部分不变。现在说得通,但是系统太复杂了,我以前没有意识到。

因此自定义mimeData并且dropMimeData需要移动结构的全部内容


bac*_*one 5

这是一个有证据的例子,但是是用 Python 编写的:

\n
import sys\nfrom PySide6 import QtCore, QtGui, QtWidgets\nfrom PySide6.QtCore import (Qt, QStringListModel, QModelIndex,\n                          QMimeData, QByteArray, QDataStream, QIODevice)\nfrom PySide6.QtWidgets import (QApplication, QMainWindow, QListView, QAbstractItemView, QPushButton, QVBoxLayout, QWidget)\n\n\nclass DragDropListModel(QStringListModel):\n    def __init__(self, parent=None):\n        super(DragDropListModel, self).__init__(parent)\n        # self.myMimeTypes = 'application/vnd.text.list' # \xe5\x8f\xaf\xe8\xa1\x8c\n\n        # self.myMimeTypes = "text/plain" # \xe5\x8f\xaf\xe8\xa1\x8c\n        self.myMimeTypes = 'application/json'  # \xe5\x8f\xaf\xe8\xa1\x8c\n\n    def supportedDropActions(self):\n        # return Qt.CopyAction | Qt.MoveAction  # \xe6\x8b\x96\xe5\x8a\xa8\xe6\x97\xb6\xe5\xa4\x8d\xe5\x88\xb6\xe5\xb9\xb6\xe7\xa7\xbb\xe5\x8a\xa8\xe7\x9b\xb8\xe5\x85\xb3\xe9\xa1\xb9\xe7\x9b\xae\n        return Qt.MoveAction  # \xe6\x8b\x96\xe5\x8a\xa8\xe6\x97\xb6\xe7\xa7\xbb\xe5\x8a\xa8\xe7\x9b\xb8\xe5\x85\xb3\xe9\xa1\xb9\xe7\x9b\xae\n\n    def flags(self, index):\n        defaultFlags = QStringListModel.flags(self, index)\n\n        if index.isValid():\n            return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultFlags\n        else:\n            return Qt.ItemIsDropEnabled | defaultFlags\n\n    def mimeTypes(self):\n        return [self.myMimeTypes]\n\n    # \xe7\x9b\xb4\xe6\x8e\xa5\xe5\xb0\x86indexes\xe9\x87\x8c\xe9\x9d\xa2\xe5\xaf\xb9\xe5\xba\x94\xe7\x9a\x84\xe6\x95\xb0\xe6\x8d\xae\xe5\x8f\x96\xe5\x87\xba\xe6\x9d\xa5\xef\xbc\x8c\xe7\x84\xb6\xe5\x90\x8e\xe6\x89\x93\xe5\x8c\x85\xe8\xbf\x9b\xe4\xba\x86QMimeData()\xe5\xaf\xb9\xe8\xb1\xa1\xef\xbc\x8c\xe5\xb9\xb6\xe8\xbf\x94\xe5\x9b\x9e\n    def mimeData(self, indexes):\n        mmData = QMimeData()\n        encodedData = QByteArray()\n        stream = QDataStream(encodedData, QIODevice.WriteOnly)\n\n        for index in indexes:\n            if index.isValid():\n                text = self.data(index, Qt.DisplayRole)\n                stream << text  # \xe6\xb5\x8b\xe8\xaf\x95\xef\xbc\x8c\xe4\xb9\x9f\xe8\xa1\x8c\n                # stream.writeQString(str(text))  # \xe5\x8e\x9f\xe5\xa7\x8b, \xe5\x8f\xaf\xe8\xa1\x8c\n\n        mmData.setData(self.myMimeTypes, encodedData)\n        return mmData\n\n    def canDropMimeData(self, data, action, row, column, parent):\n        if data.hasFormat(self.myMimeTypes) is False:\n            return False\n        if column > 0:\n            return False\n        return True\n\n    def dropMimeData(self, data, action, row, column, parent):\n        if self.canDropMimeData(data, action, row, column, parent) is False:\n            return False\n\n        if action == Qt.IgnoreAction:\n            return True\n\n        beginRow = -1\n        if row != -1:  # \xe8\xa1\xa8\xe7\xa4\xba\n            print("case 1: ROW IS NOT -1, meaning inserting in between, above or below an existing node")\n            beginRow = row\n        elif parent.isValid():\n            print("case 2: PARENT IS VALID, inserting ONTO something since row was not -1, "\n                  "beginRow becomes 0 because we want to "\n                  "insert it at the beginning of this parents children")\n            beginRow = parent.row()\n        else:\n            print("case 3: PARENT IS INVALID, inserting to root, "\n                  "can change to 0 if you want it to appear at the top")\n            beginRow = self.rowCount(QModelIndex())\n        print(f"row={row}, beginRow={beginRow}")\n\n        encodedData = data.data(self.myMimeTypes)\n        stream = QDataStream(encodedData, QIODevice.ReadOnly)\n        newItems = []\n        rows = 0\n\n        while stream.atEnd() is False:\n            text = stream.readQString()\n            newItems.append(str(text))\n            rows += 1\n\n        self.insertRows(beginRow, rows, QModelIndex())  # \xe5\x85\x88\xe6\x8f\x92\xe5\x85\xa5\xe5\xa4\x9a\xe8\xa1\x8c\n        for text in newItems:  # \xe7\x84\xb6\xe5\x90\x8e\xe7\xbb\x99\xe6\xaf\x8f\xe4\xb8\x80\xe8\xa1\x8c\xe8\xae\xbe\xe7\xbd\xae\xe6\x95\xb0\xe5\x80\xbc\n            idx = self.index(beginRow, 0, QModelIndex())\n            self.setData(idx, text)\n            beginRow += 1\n\n        return True\n\n\nclass DemoDragDrop(QWidget):\n    def __init__(self, parent=None):\n        super(DemoDragDrop, self).__init__(parent)\n\n        # \xe8\xae\xbe\xe7\xbd\xae\xe7\xaa\x97\xe5\x8f\xa3\xe6\xa0\x87\xe9\xa2\x98\n        self.setWindowTitle('drag&drop in PySide6')\n        # \xe8\xae\xbe\xe7\xbd\xae\xe7\xaa\x97\xe5\x8f\xa3\xe5\xa4\xa7\xe5\xb0\x8f\n        self.resize(480, 320)\n\n        self.initUi()\n\n    def initUi(self):\n        self.vLayout = QVBoxLayout(self)\n        self.listView = QListView(self)\n        self.listView.setSelectionMode(QAbstractItemView.ExtendedSelection)\n        self.listView.setDragEnabled(True)\n        self.listView.setAcceptDrops(True)\n        self.listView.setDropIndicatorShown(True)\n        self.ddm = DragDropListModel()  # \xe8\xaf\xa5\xe8\xa1\x8c\xe5\x92\x8c\xe4\xb8\x8b\xe9\x9d\xa24\xe8\xa1\x8c\xe7\x9a\x84\xe6\x95\x88\xe6\x9e\x9c\xe7\xb1\xbb\xe4\xbc\xbc\n        # self.listView.setDragDropMode(QAbstractItemView.InternalMove)\n        # self.listView.setDefaultDropAction(Qt.MoveAction)\n        # self.listView.setDragDropOverwriteMode(False)\n        # self.ddm = QStringListModel()\n\n        self.ddm.setStringList(['Item 1', 'Item 2', 'Item 3', 'Item 4'])\n        self.listView.setModel(self.ddm)\n\n        self.printButton = QPushButton("Print")\n\n        self.vLayout.addWidget(self.listView)\n        self.vLayout.addWidget(self.printButton)\n\n        self.printButton.clicked.connect(self.printModel)\n\n    def printModel(self):  # \xe9\xaa\x8c\xe8\xaf\x81\xe7\xa7\xbb\xe5\x8a\xa8view\xe4\xb8\xad\xe9\xa1\xb9\xe7\x9b\xae\xe5\x90\x8e\xef\xbc\x8c\xe8\x83\x8c\xe5\x90\x8emodel\xe4\xb8\xad\xe6\x95\xb0\xe6\x8d\xae\xe4\xb9\x9f\xe5\x8f\x91\xe7\x94\x9f\xe4\xba\x86\xe7\xa7\xbb\xe5\x8a\xa8\n        print(self.ddm.data(self.listView.currentIndex()))\n\n\nif __name__ == '__main__':\n\n    app = QApplication(sys.argv)\n    app.setStyle('fusion')\n    window = DemoDragDrop()\n    window.show()\n    sys.exit(app.exec_())\n
Run Code Online (Sandbox Code Playgroud)\n