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

时间:2019-06-29 16:23:28

标签: qt listview drag-and-drop model-view

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

现在,我想使内部拖放能够通过将它们拖放到其他一些项目之间来对列表中的项目进行重新排序,但是此任务似乎难以置信地复杂。我到底需要覆盖什么,需要设置哪些参数?我尝试了很多事情,我尝试过

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

我尝试过

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

我尝试实现insertRowsremoveRows,但仍然无法正常工作。

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

有人可以帮我吗?否则我会发疯的。

编辑:根据要求添加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;
}

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

3 个答案:

答案 0 :(得分:3)

要在自定义模型中重新组织项目时,必须实施所有必需的操作: -如何插入和删除行 -如何获取和设置数据 -如何序列化项目(构建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;   
};

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

    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;
    }

对于拖放部分:

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

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

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;
    }

然后,我们可以基于内部数据创建哑剧数据:

    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;
    }

现在,我们必须处理删除的数据。我们必须反序列化mime数据,插入新行以将数据设置在正确的位置(对于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;
    }

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

答案 1 :(得分:3)

这是一个有根据的例子,但在 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_())

答案 2 :(得分:2)

除了Romha的出色回答外,我想补充一些其他细节,以了解Romha的工作原理和令人困惑的地方。

官方documentation says QAbstractItemModel具有mimeTypesmimeDatadropMimeData的默认实现,只要您进行内部移动和复制操作即可正确实现datasetDatainsertRowsremoveRows

从某些角度来看,他们是对的。它确实可以工作而不覆盖mimeDatadropMimeData,但是只有当您的基础数据结构仅包含单个字符串时,这些字符串才从data返回并在setData中作为DisplayRole接收。当您具有包含多个元素的复合对象(如我所拥有的)列表时,例如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();
}

然后默认的实现实际上会执行此操作

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

,因此只能正确移动名称,并保留其余结构。现在这很有意义,但是系统是如此复​​杂,以至于我以前从未意识到。

因此,需要自定义mimeDatadropMimeData才能移动结构的全部内容