Привет друзья!
Предисловие:Весьма долго осваивал драг и дроп в представлениях. Забагованный пример widgets/itemviews/puzzle только внёс неясности. Баг его заключается в том, что если я перетащу детальку не на поле, а просто внутри представления вверх, то при дропе затирается существующая деталька из списка, перед которой была сброшена текущая.
Также в ходе изучения был выявлен баг Qt (https://bugreports.qt.io/browse/QTBUG-59324).
Свои потуги решил зафиксировать здесь в качестве примера. Буду признателен за конструктивную критику кода, подхода.
Сам пример.Привожу ссылку (https://github.com/dsbabkov/SimpleDragableTree) для скачивания проекта.
Для данного подхода важно, чтобы свойство представления dragDropOverwriteMode было установлено в true. Это позволит избежать лишнего для нас вызова removeRows, аргументы которого для меня тоже оказались какими-то магическими.
C++ (Qt)
// TreeNode.h
#pragma once
#include <QString>
#include <QVector>
#include <memory>
class TreeNode: public std::enable_shared_from_this<TreeNode>
{
public:
using ChildPtr = std::shared_ptr<TreeNode>;
using ConstChildPtr = std::shared_ptr<const TreeNode>;
using ParentPtr = std::weak_ptr<TreeNode>;
using ConstParentPtr = std::weak_ptr<const TreeNode>;
using LockedParentPtr = ChildPtr;
using ConstLockedParentPtr = ConstChildPtr;
public:
const QString name;
public:
explicit TreeNode(const QString &name);
ParentPtr parent() const;
int row() const;
int childrenCount() const;
void insertChild(const ChildPtr &child, int position = -1);
void removeChild(const ChildPtr &child);
ChildPtr child(int row) const;
private:
void setParent(const ParentPtr &parent);
void moveChild(const ChildPtr &child, int newPosition);
private:
ParentPtr parent_;
QVector<ChildPtr> children_;
};
C++ (Qt)
// TreeNode.cpp
#include "TreeNode.h"
#include <set>
TreeNode::TreeNode(const QString &name)
: name{name}
, parent_{}
{
}
TreeNode::ParentPtr TreeNode::parent() const
{
return parent_;
}
int TreeNode::row() const
{
if (parent_.expired()){
return -1;
}
return parent_.lock()->children_
.indexOf(std::const_pointer_cast<TreeNode>(shared_from_this()));
}
int TreeNode::childrenCount() const
{
return children_.count();
}
void TreeNode::insertChild(const TreeNode::ChildPtr &child, int position)
{
const ParentPtr lastParent = child->parent();
if (!lastParent.expired()){
const auto &lockedParent = lastParent.lock();
if (lockedParent.get() == this){
moveChild(child, position);
return;
}
lockedParent->removeChild(child);
}
child->setParent(shared_from_this());
if (position == -1){
children_ << child;
}
else{
children_.insert(position, child);
}
}
void TreeNode::removeChild(const TreeNode::ChildPtr &child)
{
const int pos = children_.indexOf(child);
if (pos != -1){
children_.takeAt(pos)->setParent({});
}
}
TreeNode::ChildPtr TreeNode::child(int row) const
{
return children_.at(row);
}
void TreeNode::setParent(const TreeNode::ParentPtr &parent)
{
parent_ = parent;
}
void TreeNode::moveChild(const TreeNode::ChildPtr &child, int newPosition)
{
int from = child->row();
if (from == newPosition){
return;
}
if (from < newPosition)
{
--newPosition;
}
children_.insert(newPosition, children_.takeAt(from));
}
C++ (Qt)
// TreeModel.h
#pragma once
#include <QAbstractItemModel>
#include <memory>
class TreeNode;
class TreeModel : public QAbstractItemModel
{
Q_OBJECT
public:
explicit TreeModel(QObject *parent = {});
virtual QModelIndex index(int row, int column,
const QModelIndex &parent = QModelIndex()) const override;
virtual QModelIndex parent(const QModelIndex &index) const override;
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override;
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
virtual Qt::DropActions supportedDropActions() const override;
virtual QStringList mimeTypes() const override;
virtual QMimeData *mimeData(const QModelIndexList &indexes) const override;
virtual bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override;
virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
private:
void fillTreeWithData();
static QByteArray saveIndexes(const QModelIndexList &indexes);
QModelIndexList restoreIndexes(QByteArray data);
static void sortIndexes(QModelIndexList &indexes);
private:
std::shared_ptr<TreeNode> rootNode_;
};
C++ (Qt)
// TreeModel.cpp
#include "TreeModel.h"
#include "TreeNode.h"
#include <QDataStream>
#include <QMimeData>
#include <QByteArray>
#include <QStack>
#include <algorithm>
using namespace std;
namespace {
static constexpr char mimeType[] = "MyNode";
struct MovableChild{
TreeNode::ChildPtr ptr;
QModelIndex parentIndex;
};
QList<MovableChild> convertIndexesToMovableChildren(const QModelIndexList &indexes){
QList<MovableChild> result;
result.reserve(indexes.count());
for (const QModelIndex &index: indexes){
if (!index.isValid()){
result.append({nullptr, {}});
continue;
}
TreeNode *child = static_cast<TreeNode *>(index.internalPointer());
result.append({child->shared_from_this(), index.parent()});
}
return result;
}
}
TreeModel::TreeModel(QObject *parent)
: QAbstractItemModel(parent)
, rootNode_{std::make_shared<TreeNode>("ROOT_NODE")}
{
fillTreeWithData();
}
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
TreeNode *parentNode = parent.isValid() ?
static_cast<TreeNode *>(parent.internalPointer()) :
rootNode_.get();
return createIndex(row, column, parentNode->child(row).get());
}
QModelIndex TreeModel::parent(const QModelIndex &index) const
{
if (!index.isValid()){
return {};
}
TreeNode *node = static_cast<TreeNode *>(index.internalPointer());
TreeNode::ParentPtr parentNode = node->parent();
if (parentNode.expired()){
return {};
}
std::shared_ptr<TreeNode> lockedParentNode = parentNode.lock();
return createIndex(lockedParentNode->row(), 0, lockedParentNode.get());
}
int TreeModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid()){
return rootNode_->childrenCount();
}
TreeNode *parentNode = static_cast<TreeNode *>(parent.internalPointer());
return parentNode->childrenCount();
}
int TreeModel::columnCount(const QModelIndex &/*parent*/) const
{
return 1;
}
QVariant TreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() ||
role != Qt::DisplayRole) {
return QVariant();
}
return static_cast<TreeNode *>(index.internalPointer())->name;
}
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid()){
return {};
}
Qt::ItemFlags result = Qt::ItemIsEnabled |
Qt::ItemIsSelectable;
if (!index.parent().isValid()){ // Group nodes cann accept drop
result |= Qt::ItemIsDropEnabled;
}
else{ //leaves can be draged
result |= Qt::ItemIsDragEnabled;
}
return result;
}
Qt::DropActions TreeModel::supportedDropActions() const
{
return Qt::MoveAction;
}
QStringList TreeModel::mimeTypes() const
{
return {mimeType};
}
QMimeData *TreeModel::mimeData(const QModelIndexList &indexes) const
{
QMimeData *result = new QMimeData();
result->setData(mimeType, saveIndexes(indexes));
return result;
}
bool TreeModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int /*column*/, const QModelIndex &parent) const
{
if (!parent.isValid() || // root is not drop enabled
action != Qt::MoveAction ||
!data->hasFormat(mimeType) ||
row < 0 ||
row > rowCount(parent)){
return false;
}
return true;
}
bool TreeModel::dropMimeData(const QMimeData *data, Qt::DropAction /*action*/, int row, int /*column*/, const QModelIndex &parent)
{
QModelIndexList indexes = restoreIndexes(data->data(mimeType));
if (indexes.isEmpty()){
return false;
}
sortIndexes(indexes);
const QList<MovableChild> &childrenToMove = convertIndexesToMovableChildren(indexes);
TreeNode *parentNode = static_cast<TreeNode *>(parent.internalPointer());
for (const MovableChild &movableChild: childrenToMove){
const int srcRow = movableChild.ptr->row();
const bool interParentMove = movableChild.parentIndex == parent;
const bool incrementRow = !(interParentMove && srcRow < row);
beginMoveRows(movableChild.parentIndex, srcRow, srcRow, parent, row);
parentNode->insertChild(movableChild.ptr, row);
endMoveRows();
row += incrementRow;
}
return true;
}
void TreeModel::fillTreeWithData()
{
const std::vector<TreeNode::ChildPtr> groups = {
make_shared<TreeNode>("Group 0"),
make_shared<TreeNode>("Group 1"),
make_shared<TreeNode>("Group 2")
};
for (TreeNode::ChildPtr group: groups){
rootNode_->insertChild(group);
for(int i = 0; i < 5; ++i){
group->insertChild(
make_shared<TreeNode>(QStringLiteral("Item %1 of %2").arg(i).arg(group->name))
);
}
}
}
QByteArray TreeModel::saveIndexes(const QModelIndexList &indexes)
{
QByteArray result;
QDataStream stream(&result, QIODevice::WriteOnly);
for (const QModelIndex &index: indexes){
QModelIndex localIndex = index;
QStack<int> indexParentStack;
while (localIndex.isValid()){
indexParentStack << localIndex.row();
localIndex = localIndex.parent();
}
stream << indexParentStack.size();
while (!indexParentStack.isEmpty()){
stream << indexParentStack.pop();
}
}
return result;
}
QModelIndexList TreeModel::restoreIndexes(QByteArray data)
{
QModelIndexList result;
QDataStream stream(&data, QIODevice::ReadOnly);
while(!stream.atEnd()){
int childDepth = 0;
stream >> childDepth;
QModelIndex currentIndex = {};
for (int i = 0; i < childDepth; ++i){
int row = 0;
stream >> row;
currentIndex = index(row, 0, currentIndex);
}
result << currentIndex;
}
return result;
}
void TreeModel::sortIndexes(QModelIndexList &indexes)
{
std::sort(indexes.begin(), indexes.end(), [](const QModelIndex &left, const QModelIndex &right){
if (left.parent() < right.parent()){
return true;
}
else if (right.parent() < left.parent()){
return false;
}
else{
return left < right;
}
});
}