QTableView + QAbstractTableModel: перемещение строк с помощью перетаскивания

У меня есть простая модель на основе QAbstractTableModel и QTableView для нее.

Моя цель также проста: разрешить перемещать / переупорядочивать строки с помощью drag'n'drop. Примечания:

  • Любые изменения внутри QTableView должны быть отражены в моей модели;
  • D'n должен был быть внутренним - перемещение должно выполняться только внутри моего представления, никакого внешнего экспорта MIME;
  • Я хочу перетащить всю строку. Отдельные элементы нельзя перетаскивать;
  • Перетаскивание горизонтального заголовка не является подходящим решением для меня, потому что я хочу, чтобы заголовки были скрыты, и потому, что я хочу, чтобы пользователь мог захватить строку в любом месте, чтобы перетащить ее;

Я действительно близок к своей цели. Но все равно это работает не так, как я ожидал. Теперь я могу перетаскивать строки, но кажется, что любая ячейка может принять перетаскивание, хотя я указал Qt::ItemIsDropEnabled только для родительской глобальной таблицы и не указываю этот флаг для фактических элементов таблицы, потому что я не хочу чтобы бросить к ним, я хочу как-нибудь «между рядами», просто чтобы выполнить перемещение ряда. Поскольку элементы таблицы по какой-то причине могут принимать отбрасывания, у меня возникает любопытное поведение: при переходе к первой ячейке любой строки я добиваюсь именно того, чего хочу: моя строка перемещается правильно. Но если я перейду к не первой ячейке любой строки, все пойдет совсем не так. Но лучше показать картинку того, что здесь происходит:

введите описание изображения здесь

Мой код (минимальный образец, в котором именно моя проблема):

main.cpp

void setupView(QTableView &t)
{
    t.verticalHeader()->hide();
    t.horizontalHeader()->hide();
    t.horizontalHeader()->setStretchLastSection(true);

    t.setSelectionBehavior(QAbstractItemView::SelectRows);
    t.setSelectionMode(QAbstractItemView::SingleSelection);

    t.setDragEnabled(true);
    t.setDropIndicatorShown(true);
    t.setAcceptDrops(true);
    t.viewport()->setAcceptDrops(true);
    t.setDefaultDropAction(Qt::MoveAction);
    t.setDragDropMode(QTableView::InternalMove);
    t.setDragDropOverwriteMode(false);
}

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

    QTableView *table = new QTableView(&w);
    setupView(*table);
    table->setModel(new TableModel);

    w.setCentralWidget(table);
    w.show();

    return a.exec();
}

tablemodel.cpp

#include "tablemodel.h"

TableModel::TableModel()
{
    // m_data is a QList<QStringList>
    m_data = {
        {"Name", "Kelly"},
        {"Age", "19"},
        {"Gender", "Female"},
    };
}

int TableModel::rowCount(const QModelIndex &parent) const {
    return m_data.size();
}

int TableModel::columnCount(const QModelIndex &parent) const {
    return 2;
}

QVariant TableModel::data(const QModelIndex &i, int r) const
{
    return (r == Qt::DisplayRole) ? m_data[i.row()][i.column()] : QVariant();
}

QVariant TableModel::headerData(int section, Qt::Orientation orientation, int r) const
{
    return QVariant();
}

Qt::ItemFlags TableModel::flags(const QModelIndex &index) const
{
    Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable
                    | Qt::ItemIsEditable | Qt::ItemIsDragEnabled;

    if(!index.isValid()) {
        f |= Qt::ItemIsDropEnabled;
    }

    return f;
}

Qt::DropActions TableModel::supportedDropActions() const
{
    return Qt::MoveAction | Qt::CopyAction;
}

bool TableModel::setData(const QModelIndex &i, const QVariant &v, int r)
{
    if(r == Qt::EditRole || r == Qt::DisplayRole) {
        m_data[i.row()][i.column()] = v.toString();
        return true;
    }
    return false;
}

bool TableModel::setItemData(const QModelIndex &i, const QMap<int, QVariant> &roles)
{
    if(!roles.contains(Qt::EditRole) && !roles.contains(Qt::DisplayRole)) {
        return false;
    }

    m_data[i.row()][i.column()] = roles[Qt::DisplayRole].toString();
    return true;
}

bool TableModel::insertRows(int row, int count, const QModelIndex &parent)
{
    beginInsertRows(QModelIndex(), row, row + count - 1);
    for(int i = 0; i<count; ++i) {
        m_data.insert(row, QStringList({"", ""}));
    }
    endInsertRows();
    return true;
}

bool TableModel::removeRows(int row, int count, const QModelIndex &parent)
{
    beginRemoveRows(QModelIndex(), row, row + count - 1);
    for(int i = 0; i<count; ++i) {
        m_data.removeAt(row);
    }
    endRemoveRows();
    return true;
}

bool TableModel::moveRows(const QModelIndex &srcParent, int srcRow, int count,
                          const QModelIndex &dstParent, int dstChild)
{
    beginMoveRows(QModelIndex(), srcRow, srcRow + count - 1, QModelIndex(), dstChild);
    for(int i = 0; i<count; ++i) {
        m_data.insert(dstChild + i, m_data[srcRow]);
        int removeIndex = dstChild > srcRow ? srcRow : srcRow+1;
        m_data.removeAt(removeIndex);
    }
    endMoveRows();
    return true;
}

Подскажите, пожалуйста, что сейчас не так с настройкой модели или вида.

UPD

Для тех, кто заинтересован в решении:

bool TableModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    Q_UNUSED(parent);
    Q_UNUSED(column);

    if(row == -1) {
        row = rowCount();
    }

    return QAbstractTableModel::dropMimeData(data, action, row, 0, parent);
}

person Nikolai Shalakin    schedule 13.07.2017    source источник
comment
@Dmitry Да, это именно то решение! Это решило мою проблему. Скопируйте свой комментарий к ответу, и я его приму.   -  person Nikolai Shalakin    schedule 13.07.2017
comment
@NikolaiShalakin, у меня такая же проблема, и я хотел спросить, можно ли опубликовать все решение с реализацией dropMimeData, пожалуйста   -  person Emanuele    schedule 28.04.2019
comment
@Emanuele, конечно, пожалуйста, проверьте, я добавил это в конец вопроса   -  person Nikolai Shalakin    schedule 29.04.2019
comment
Ваше решение отличное, но оно не относится к вопросу. Отредактируйте вопрос, удалите оттуда решение и опубликуйте его как свой собственный ответ на вопрос.   -  person Kuba hasn't forgotten Monica    schedule 24.06.2020


Ответы (2)


Вы должны добавить dropMimeData метод в свою модель и правильно его реализовать. Если перетаскивание в первый столбец вам подходит, вы, вероятно, можете просто вызвать QAbstractItemModel::dropMimeData изнутри dropMimeData вашей модели с параметром column, равным 0, независимо от того, в каком столбце действительно было выполнено перетаскивание.

person Dmitry    schedule 13.07.2017

Nitpick: Эти две строчки не нужны:

t.setAcceptDrops(true);
t.viewport()->setAcceptDrops(true);
person Kuba hasn't forgotten Monica    schedule 24.06.2020