Контекстно-зависимое перетаскивание в 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.

Можно ли добиться такого поведения без реализации my on QListView и чего можно добиться, адаптировав пользовательский QAbstractItemModel.

Возможным обходным путем может быть даже отказ от интерфейса перетаскивания и использование клавиш со стрелками вверх и вниз для перемещения элементов. Или даже более общее действие с операцией вырезания и вставки. Но я действительно предпочитаю придерживаться интерфейса перетаскивания.


person Aleph0    schedule 03.09.2019    source источник


Ответы (1)


Вы можете переопределить QStandardItemModel и переопределить метод canDropMimeData(). Есть и другие способы, хотя они, вероятно, будут более сложными, если вы уже довольны QStandardItemModel. Реализация собственной модели может повысить производительность, особенно если ваша структура данных довольно проста (например, список из одного столбца). Это также позволит вам настроить поведение перетаскивания в целом.

Обратите внимание, что это полностью игнорирует тип действия (по умолчанию QStandardItemModel разрешает только перемещение и копирование). Перемещение элемента на другой элемент полностью удалит целевой элемент, что может быть не совсем тем, что вам нужно, но является отдельной проблемой (см. комментарии в коде ниже).

Вы также можете реализовать ту же логику в методе dropMimeData() (перед вызовом метода базового класса), но я не уверен, что вижу какое-либо преимущество. А с помощью 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 кодирует данные (и декодирует его в следующем методе вниз).

ДОБАВЛЕНО: хорошо, это меня немного беспокоило, так что вот более эффективная версия... :-) Это экономит много времени на декодирование, встраивая приоритет перетаскиваемого элемента прямо в данные 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;
        }
};
person Maxim Paperno    schedule 04.09.2019
comment
Большое спасибо за обстоятельный ответ. Я попробую и проверю, удовлетворит ли это мои потребности. - person Aleph0; 04.09.2019
comment
@ Алеф0 Добро пожаловать! И я добавил более эффективную версию только потому, что все время думал об этом. :) Я уверен, что есть и другие способы сделать это. - person Maxim Paperno; 05.09.2019