上下文依赖于 QListView 中的拖放

Context depending drag and drop in QListView

在我的一个项目中,我必须管理一个项目列表,这些项目可以通过拖放按顺序重新排列。

现在,所有项目都有优先级,用户无法更改。列表中元素的顺序有限制,即优先级低的元素必须在前,但优先级相同的元素可以互换。

例如,下面的列表是合理的:

(A,1),(B,1),(C,1),(D,2),(E,3)

而以下内容已损坏:

(A,1),(B,1),(E,3),(D,2)

以下代码显示了我的问题的起点:

#include <QApplication>
#include <QFrame>
#include <QHBoxLayout>
#include <QListView>
#include <QStandardItemModel>

QStandardItem* create(const QString& text, int priority) {
    auto ret = new QStandardItem(text);
    ret->setData(priority);
    return ret;
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    auto frame = new QFrame;
    frame->setLayout(new QVBoxLayout);
    auto view = new QListView;
    frame->layout()->addWidget(view);
    auto model = new QStandardItemModel;
    view->setModel(model);
    model->appendRow(create("1. A", 1));
    model->appendRow(create("1. B", 1));
    model->appendRow(create("2. X", 2));
    model->appendRow(create("2. Y", 2));
    model->appendRow(create("2. Z", 2));

    view->setDragEnabled(true);
    view->viewport()->setAcceptDrops(true);
    view->setDropIndicatorShown(true);
    view->setDragDropMode(QAbstractItemView::DragDropMode::InternalMove);
    view->setDefaultDropAction(Qt::DropAction::MoveAction);
    view->setDragDropOverwriteMode(false);

    frame->show();
    return a.exec();
}

现在,DefaultDropAction 必须根据要移动的项目以及要放置的项目更改上下文。

如果两个元素的优先级相等,那么我有一个MoveAction。如果两个元素的优先级不同,我有一个IgnoreAction

如果不在 QListView 上执行我的操作,是否可以实现此行为? 可以通过调整自定义 QAbstractItemModel 来实现。

一个可能的解决方法甚至可能是放弃拖放界面并使用向上和向下箭头键来移动项目。或更一般的具有剪切和粘贴操作的动作。但是,我真的更喜欢坚持拖放界面。

您可以重新实现 QStandardItemModel 并覆盖 canDropMimeData() 方法。还有其他方法,但如果您已经对 QStandardItemModel 感到满意,它们可能会涉及更多。实现您自己的模型可能具有性能优势,尤其是当您的数据结构相当简单(如单列列表)时。这也可以让您更普遍地自定义 drag/drop 行为。

请注意,这会完全忽略操作类型(QStandardItemModel 默认情况下仅允许移动和复制)。将一个项目移动到另一个项目上将完全删除目标项目——这可能不是您想要的,但这是一个单独的问题(请参阅下面代码中的注释)。

您也可以在 dropMimeData() 方法中实现相同的逻辑(在调用基础 class 方法之前),但我不确定我是否看到任何优势。通过使用 canDropMimeData(),用户还可以获得关于什么有效和无效的视觉反馈。


#include <QStandardItemModel>

class ItemModel : public QStandardItemModel
{
    public:
        using QStandardItemModel::QStandardItemModel;

        bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override
        {
            if (!QStandardItemModel::canDropMimeData(data, action, row, column, parent))
                return false;

            const int role = Qt::UserRole + 1;  // what QStandardItem uses for setData() by default
            int originPriority;
            int destPriority;

            // Find destination item priority.
            if (parent.isValid()) {
                // dropping onto an item
                // Note: if you don't want MoveAction to overwrite items you could:
                //   if (action == Qt::MoveAction) return false;
                destPriority = parent.data(role).toInt();
            }
            else if (row > -1) {
                // dropping between items
                destPriority = this->data(index(row, 0), role).toInt();
            }
            else {
                // dropping somewhere else onto the view, treat it as drop after last item in model
                destPriority = this->data(index(rowCount() - 1, 0), role).toInt();
            }

            // Need to find priority of item(s) being dragged (encoded in mime data). Could be several.
            // This part decodes the mime data in a way compatible with how QAbstractItemModel encoded it.
            // (QStandardItemModel includes it in the mime data alongside its own version)
            QByteArray ba = data->data(QAbstractItemModel::mimeTypes().first());
            QDataStream ds(&ba, QIODevice::ReadOnly);
            while (!ds.atEnd()) {
                int r, c;
                QMap<int, QVariant> v;
                ds >> r >> c >> v;
                // If there were multiple columns of data we could also do a
                //   check on the column number, for example.
                originPriority = v.value(role).toInt();
                if (originPriority != destPriority)
                    break;  //return false;  Could exit here but keep going to print our debug info.
            }

            qDebug() << "Drop parent:" << parent << "row:" << row << 
                        "destPriority:" << destPriority << "originPriority:" << originPriority;

            if (originPriority != destPriority)
                return false;

            return true;
        }
};

作为参考,这里是 QAbstractItemModel encodes data 的方式(并在下一个方法中对其进行解码)。

已添加: 好的,这让我有点烦,所以这是一个更有效的版本...:-) 通过在拖动开始时将拖动项目的优先级直接嵌入到 mime 数据中来节省大量解码时间。

#include <QStandardItemModel>

#define PRIORITY_MIME_TYPE   QStringLiteral("application/x-priority-data")

class ItemModel : public QStandardItemModel
{
    public:
        using QStandardItemModel::QStandardItemModel;

        QMimeData *mimeData(const QModelIndexList &indexes) const override
        {
            QMimeData *mdata = QStandardItemModel::mimeData(indexes);
            if (!mdata)
                return nullptr;

            // Add our own priority data for more efficient evaluation in canDropMimeData()
            const int role = Qt::UserRole + 1;  // data role for priority value
            int priority = -1;
            bool ok;

            for (const QModelIndex &idx : indexes) {
                // Priority of selected item
                const int thisPriority = idx.data(role).toInt(&ok);
                // When dragging multiple items, check that the priorities of all selected items are the same.
                if (!ok || (priority > -1 && thisPriority != priority))
                    return nullptr;  // Cannot drag items with different priorities;

                priority = thisPriority;
            }
            if (priority < 0)
                return nullptr;  // couldn't find a priority, cancel the drag.

            // Encode the priority data
            QByteArray ba;
            ba.setNum(priority);
            mdata->setData(PRIORITY_MIME_TYPE, ba);

            return mdata;
        }

        bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override
        {
            if (!QStandardItemModel::canDropMimeData(data, action, row, column, parent))
                return false;
            if (!data->hasFormat(PRIORITY_MIME_TYPE))
                return false;

            const int role = Qt::UserRole + 1;  // what QStandardItem uses for setData() by default
            int destPriority = -1;
            bool ok = false;

            // Find destination item priority.
            if (parent.isValid()) {
                // dropping onto an item
                destPriority = parent.data(role).toInt(&ok);
            }
            else if (row > -1) {
                // dropping between items
                destPriority = this->data(index(row, 0), role).toInt(&ok);
            }
            else {
                // dropping somewhere else onto the view, treat it as drop after last item in model
                destPriority = this->data(index(rowCount() - 1, 0), role).toInt(&ok);
            }
            if (!ok || destPriority < 0)
                return false;

            // Get priority of item(s) being dragged which we encoded in mimeData() method.
            const int originPriority = data->data(PRIORITY_MIME_TYPE).toInt(&ok);

            qDebug() << "Drop parent:" << parent << "row:" << row
                     << "destPriority:" << destPriority << "originPriority:" << originPriority;

            if (!ok || originPriority != destPriority)
                return false;

            return true;
        }
};