Full drag and drop support in QTreeView
-
Hello all.
I'm trying to implement a fully functional drag and drop in a TreeView/TreeModel tandem. I consider myself as a Qt beginner and I didn't write the pieces of code you'll see alone.
First, here are the needs.
- an inner drag-and-drop (not from another widget or an external application) ;
- if the item A is dropped on the item B, A is moved as a child of B ;
- the TreeView shows items A, B and C: if I drag A item between B and C, the order becomes B, A and C ;
- the TreeModel is built from a std::vector<std::string>.
If the line removeNode(oldNode); of the function TreeModel::dropMimeData is commented, the drop as the child works, excepted that it's not erased at the original position (of course). When uncommented, it crashes on drag-and-drop when I release the mouse button..
The debugger says TreeNode *oldNode pointer is 0x0.
If I change the mimeData function code into:
QMimeData *TreeModel::mimeData(const QModelIndexList &indexes) const { QMimeData *mimeData = new QMimeData; QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); QList<TreeNode *> nodes; foreach (const QModelIndex &index, indexes) { TreeNode *node = nodeForIndex(index); if (!nodes.contains(node)) nodes << node; } stream << QCoreApplication::applicationPid(); stream << nodes.count(); foreach (TreeNode *node, nodes) { stream << reinterpret_cast<qlonglong>(node); } mimeData->setData(s_treeNodeMimeType, data); return mimeData; }
there's no crash, but nothing happens on drag-and-drop
In fact, the question is: is there an easy way to get the pointer to the old TreeNode in order to remove it. Easy should be understood as "few lines".
Here's the complete source code of the subclassed QTreeModel:
/* treemodel.cpp Provides a simple tree model to show how to create and use hierarchical models. */ #include "TreeNode.h" #include "TreeModel.h" #include <QStringList> #include <QMimeData> #include <QIODevice> #include <QDataStream> TreeModel::TreeModel(const std::vector<std::string> &modalities, QObject *parent) : QAbstractItemModel(parent) { QStringList data; int i; for (i = 0 ; i < modalities.size() ; ++i) data.push_back(QString::fromStdString(modalities.at(i))); QList<QVariant> rootData; rootData << "Occurrences"; m_rootNode = new TreeNode(rootData, 0); setupModelData(data, m_rootNode); } QVariant TreeModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (role != Qt::DisplayRole) return QVariant(); TreeNode *node = nodeForIndex(index); return node->data(index.column()); } QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) return m_rootNode->data(section); return QVariant(); } Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const { Qt::ItemFlags flags = QAbstractItemModel::flags(index); if (index.isValid()) //teste si on est bien sur un item... return flags | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; else return flags | Qt::ItemIsDropEnabled; } Qt::DropActions TreeModel::supportedDropActions() const //supported actions when dropping { return Qt::CopyAction | Qt::MoveAction; } static const char s_treeNodeMimeType[] = "application/x-treenode"; QStringList TreeModel::mimeTypes() const { return QAbstractItemModel::mimeTypes() << s_treeNodeMimeType; } // Filling datas for moved objects QMimeData *TreeModel::mimeData(const QModelIndexList &indexes) const { QStringList list; foreach( const QModelIndex &index, indexes ) list << data(index, Qt::DisplayRole).toString(); QMimeData* mimeData = QAbstractItemModel::mimeData(indexes); mimeData->setText(list.join(", ")); return mimeData; } //called function when object dropped bool TreeModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { if (data->hasText()) { // on drop sur un item => le parent c'est l'item if (parent.isValid()) { const int lastRow = rowCount(parent); beginInsertRows(parent, lastRow, lastRow); // method to tell to the view that we will add a line (internal Qt stuff) //ajout de l'enfant TreeNode *parentNode = nodeForIndex(parent); QList<QVariant> columnData; columnData << data->text(); parentNode->appendChild(new TreeNode(columnData, parentNode)); QByteArray ddata = data->data(s_treeNodeMimeType); QDataStream stream(&ddata, QIODevice::ReadOnly); qint64 senderPid; stream >> senderPid; int count; stream >> count; qlonglong nodePtr; stream >> nodePtr; TreeNode *oldNode = reinterpret_cast<TreeNode *>(nodePtr); // Remove from old position removeNode(oldNode); endInsertRows();//calling the signals for updating the view (internal Qt cooking) return true; } } return QAbstractItemModel::dropMimeData(data, action, row, column, parent); } TreeModel::~TreeModel() { delete m_rootNode; } int TreeModel::columnCount(const QModelIndex &parent) const { return nodeForIndex(parent)->columnCount(); } TreeNode * TreeModel::nodeForIndex(const QModelIndex &index) const { if (!index.isValid()) return m_rootNode; else return static_cast<TreeNode*>(index.internalPointer()); } void TreeModel::removeNode(TreeNode *node) { const int row = node->row(); QModelIndex idx = createIndex(row, 0, node); beginRemoveRows(idx.parent(), row, row); node->parentNode()->removeChild(row); endRemoveRows(); } QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) return QModelIndex(); TreeNode *parentNode = nodeForIndex(parent); TreeNode *childNode = parentNode->child(row); if (childNode) return createIndex(row, column, childNode); else return QModelIndex(); } QModelIndex TreeModel::parent(const QModelIndex &index) const { TreeNode *childNode = nodeForIndex(index); if (childNode == m_rootNode) return QModelIndex(); TreeNode *parentNode = childNode->parentNode(); if (parentNode == m_rootNode) return QModelIndex(); return createIndex(parentNode->row(), 0, parentNode); } int TreeModel::rowCount(const QModelIndex &parent) const { if (parent.column() > 0) return 0; TreeNode *parentNode = nodeForIndex(parent); return parentNode->childCount(); } void TreeModel::setupModelData(const QStringList &lines, TreeNode *parent) { QList<TreeNode*> parents; QList<int> indentations; parents << parent; indentations << 0; int number = 0; while (number < lines.count()) { int position = 0; while (position < lines[number].length()) { if (lines[number].mid(position, 1) != " ") break; position++; } QString lineData = lines[number].mid(position).trimmed(); if (!lineData.isEmpty()) { // Read the column data from the rest of the line. QStringList columnStrings = lineData.split("\t", QString::SkipEmptyParts); QList<QVariant> columnData; for (int column = 0; column < columnStrings.count(); ++column) columnData << columnStrings[column]; if (position > indentations.last()) { // The last child of the current parent is now the new parent // unless the current parent has no children. if (parents.last()->childCount() > 0) { parents << parents.last()->child(parents.last()->childCount()-1); indentations << position; } } else { while (position < indentations.last() && parents.count() > 0) { parents.pop_back(); indentations.pop_back(); } } // Append a new node to the current parent's list of children. parents.last()->appendChild(new TreeNode(columnData, parents.last())); } ++number; } }
The header file :
#ifndef TREEMODEL_H #define TREEMODEL_H #include <QAbstractItemModel> #include <QModelIndex> #include <QVariant> class TreeNode; class TreeModel : public QAbstractItemModel { Q_OBJECT public: explicit TreeModel(const std::vector<std::__cxx11::string> &modalities, QObject *parent = 0); ~TreeModel(); QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; QStringList mimeTypes() const Q_DECL_OVERRIDE; QMimeData *mimeData(const QModelIndexList &indexes) const Q_DECL_OVERRIDE; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex & parent); void removeNode(TreeNode *node); private: TreeNode * nodeForIndex(const QModelIndex &index) const; void setupModelData(const QStringList &lines, TreeNode *parent); TreeNode *m_rootNode; }; #endif // TREEMODEL_H
-
In fact, here's the solution I used...
It's a very long, hard level source code and 3 subclass to define such a banal behavior... But now it works.TreeModel header:
#ifndef TREEMODEL_H #define TREEMODEL_H #include <QAbstractItemModel> #include <QModelIndex> #include <QVariant> class TreeNode; class TreeModel : public QAbstractItemModel { Q_OBJECT public: explicit TreeModel(const QStringList &modalities, QObject *parent = 0); ~TreeModel(); QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; Qt::DropActions supportedDragActions() const Q_DECL_OVERRIDE; QStringList mimeTypes() const Q_DECL_OVERRIDE; QMimeData *mimeData(const QModelIndexList &indexes) const Q_DECL_OVERRIDE; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex & parent); private: TreeNode * nodeForIndex(const QModelIndex &index) const; void removeNode(TreeNode *node); void setupModelData(const QStringList &lines, TreeNode *parent); TreeNode *m_rootNode; }; #endif // TREEMODEL_H
TreeModel source:
#include "TreeNode.h" #include "TreeModel.h" #include <QCoreApplication> #include <QStringList> #include <QMimeData> #include <QIODevice> #include <QDataStream> TreeModel::TreeModel(const QStringList &modalities, QObject *parent) : QAbstractItemModel(parent) { QStringList data = modalities; // for (int i = 1 ; i < modalities.size() ; ++i) // data.push_back(QString::fromStdString(modalities.at(i))); QList<QVariant> rootData; rootData << "Levels"; m_rootNode = new TreeNode(rootData, 0); setupModelData(data, m_rootNode); } TreeModel::~TreeModel() { delete m_rootNode; } int TreeModel::columnCount(const QModelIndex &parent) const { return nodeForIndex(parent)->columnCount(); } static const char s_treeNodeMimeType[] = "application/x-treenode"; //returns the mime type QStringList TreeModel::mimeTypes() const { return QStringList() << s_treeNodeMimeType; } //receives a list of model indexes list QMimeData *TreeModel::mimeData(const QModelIndexList &indexes) const { QMimeData *mimeData = new QMimeData; QByteArray data; //a kind of RAW format for datas //QDataStream is independant on the OS or proc architecture //serialization of C++'s basic data types, like char, short, int, char *, etc. //Serialization of more complex data is accomplished //by breaking up the data into primitive units. QDataStream stream(&data, QIODevice::WriteOnly); QList<TreeNode *> nodes; // foreach (const QModelIndex &index, indexes) { TreeNode *node = nodeForIndex(index); if (!nodes.contains(node)) nodes << node; } stream << QCoreApplication::applicationPid(); stream << nodes.count(); foreach (TreeNode *node, nodes) { stream << reinterpret_cast<qlonglong>(node); } mimeData->setData(s_treeNodeMimeType, data); return mimeData; } bool TreeModel::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_ASSERT(action == Qt::MoveAction); Q_UNUSED(column); //test if the data type is the good one if (!mimeData->hasFormat(s_treeNodeMimeType)) { return false; } QByteArray data = mimeData->data(s_treeNodeMimeType); QDataStream stream(&data, QIODevice::ReadOnly); qint64 senderPid; stream >> senderPid; if (senderPid != QCoreApplication::applicationPid()) { // Let's not cast pointers that come from another process... return false; } TreeNode *parentNode = nodeForIndex(parent); Q_ASSERT(parentNode); int count; stream >> count; if (row == -1) { // valid index means: drop onto item. I chose that this should insert // a child item, because this is the only way to create the first child of an item... // This explains why Qt calls it parent: unless you just support replacing, this // is really the future parent of the dropped items. if (parent.isValid()) row = 0; else // invalid index means: append at bottom, after last toplevel row = rowCount(parent); } for (int i = 0; i < count; ++i) { // Decode data from the QMimeData qlonglong nodePtr; stream >> nodePtr; TreeNode *node = reinterpret_cast<TreeNode *>(nodePtr); // Adjust destination row for the case of moving an item // within the same parent, to a position further down. // Its own removal will reduce the final row number by one. if (node->row() < row && parentNode == node->parentNode()) --row; // Remove from old position removeNode(node); // Insert at new position //qDebug() << "Inserting into" << parent << row; beginInsertRows(parent, row, row); parentNode->insertChild(row, node); endInsertRows(); ++row; } return true; } Qt::DropActions TreeModel::supportedDropActions() const { return Qt::MoveAction; } Qt::DropActions TreeModel::supportedDragActions() const { return Qt::MoveAction; } QVariant TreeModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (role != Qt::DisplayRole) return QVariant(); TreeNode *node = nodeForIndex(index); return node->data(index.column()); } Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::ItemIsDropEnabled; return QAbstractItemModel::flags(index) | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; } QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) return m_rootNode->data(section); return QVariant(); } //returns a pointer to the "index" TreeNode * TreeModel::nodeForIndex(const QModelIndex &index) const { if (!index.isValid()) return m_rootNode; else return static_cast<TreeNode*>(index.internalPointer()); } void TreeModel::removeNode(TreeNode *node) { const int row = node->row(); QModelIndex idx = createIndex(row, 0, node); beginRemoveRows(idx.parent(), row, row); node->parentNode()->removeChild(row); endRemoveRows(); } QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) return QModelIndex(); TreeNode *parentNode = nodeForIndex(parent); TreeNode *childNode = parentNode->child(row); if (childNode) return createIndex(row, column, childNode); else return QModelIndex(); } QModelIndex TreeModel::parent(const QModelIndex &index) const { TreeNode *childNode = nodeForIndex(index); if (childNode == m_rootNode) return QModelIndex(); TreeNode *parentNode = childNode->parentNode(); if (parentNode == m_rootNode) return QModelIndex(); return createIndex(parentNode->row(), 0, parentNode); } int TreeModel::rowCount(const QModelIndex &parent) const { if (parent.column() > 0) return 0; TreeNode *parentNode = nodeForIndex(parent); return parentNode->childCount(); } void TreeModel::setupModelData(const QStringList &lines, TreeNode *parent) { QList<TreeNode*> parents; QList<int> indentations; parents << parent; indentations << 0; int number = 0; while (number < lines.count()) { int position = 0; while (position < lines[number].length()) { if (lines[number].mid(position, 1) != " ") break; position++; } QString lineData = lines[number].mid(position).trimmed(); if (!lineData.isEmpty()) { // Read the column data from the rest of the line. QStringList columnStrings = lineData.split("\t", QString::SkipEmptyParts); QList<QVariant> columnData; for (int column = 0; column < columnStrings.count(); ++column) columnData << columnStrings[column]; if (position > indentations.last()) { // The last child of the current parent is now the new parent // unless the current parent has no children. if (parents.last()->childCount() > 0) { parents << parents.last()->child(parents.last()->childCount()-1); indentations << position; } } else { while (position < indentations.last() && parents.count() > 0) { parents.pop_back(); indentations.pop_back(); } } // Append a new node to the current parent's list of children. parents.last()->appendChild(new TreeNode(columnData, parents.last())); } ++number; } }
And the TreeView (nothing special).
Header:#ifndef TREEVIEW_H #define TREEVIEW_H #include <QObject> #include <QTreeView> #include "TreeModel.h" class TreeView : public QTreeView { public: TreeView(TreeModel *model); TreeView(); void setModel(TreeModel *model); }; #endif // TREEVIEW_H
Source:
#include "TreeView.h" #include "TreeModel.h" #include <QHeaderView> TreeView::TreeView(TreeModel *model) { setModel(model); TreeView(); } TreeView::TreeView() { connect(this, &QTreeView::pressed, this, &QTreeView::expandAll); setDragEnabled(true); setAcceptDrops(true); resizeColumnToContents(0); resize(400, 500); setSelectionMode(QAbstractItemView::SingleSelection); expandAll(); connect(this, &QTreeView::pressed, this, &QTreeView::expandAll); header()->setVisible(false); setIndentation(5); setEditTriggers(QAbstractItemView::EditTriggers()); } void TreeView::setModel(TreeModel *model) { //Kick what's already selected (http://doc.qt.io/qt-4.8/qabstractitemview.html#setModel) QItemSelectionModel *m = selectionModel(); delete m; }
And the TreeNode:
#ifndef TREENODE_H #define TREENODE_H #include <QList> #include <QVariant> class TreeNode { public: explicit TreeNode(const QList<QVariant> &data, TreeNode *parentNode); ~TreeNode(); void appendChild(TreeNode *child); void removeChild(int row); TreeNode *child(int row) const; int childCount() const; int columnCount() const; QVariant data(int column) const; int row() const; TreeNode *parentNode() const; void insertChild(int pos, TreeNode *child); private: QList<TreeNode*> m_childNodes; QList<QVariant> m_nodeData; TreeNode *m_parentNode; }; #endif // TREENODE_H
source:
#include "TreeNode.h" #include <QStringList> TreeNode::TreeNode(const QList<QVariant> &data, TreeNode *parent) : m_nodeData(data), m_parentNode(parent) { } TreeNode::~TreeNode() { qDeleteAll(m_childNodes); } void TreeNode::appendChild(TreeNode *node) { m_childNodes.append(node); } void TreeNode::removeChild(int row) { m_childNodes.removeAt(row); } TreeNode *TreeNode::child(int row) const { return m_childNodes.value(row); } int TreeNode::childCount() const { return m_childNodes.count(); } int TreeNode::columnCount() const { return m_nodeData.count(); } QVariant TreeNode::data(int column) const { return m_nodeData.value(column); } TreeNode *TreeNode::parentNode() const { return m_parentNode; } int TreeNode::row() const { if (m_parentNode) return m_parentNode->m_childNodes.indexOf(const_cast<TreeNode*>(this)); return 0; } void TreeNode::insertChild(int pos, TreeNode *child) { m_childNodes.insert(pos, child); child->m_parentNode = this; }
-
@Patou355 On the class TreeView method setModel, I think you might want to call the base class setModel :
void TreeView::setModel(TreeModel* model) { QItemSelectionModel* m = selectionModel(); delete m; QTreeView::setModel(model); // <-- add this line }