# [Qt 5] Moving Rows For Scene Tree (QAbstractItemModel)

This topic is 1133 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

## Recommended Posts

I know this is really not the place to ask this, but I couldn't get an answer to this question on the official Qt forums. I know others have gone this route when building GUIs for their own engines, so I have a better chance here. If this is an issue with this, I have no problem removing this post. That said, here's the contents of my post from the Qt forums:

I've setup my own derivative class of QAbstractItemModel for my engine's node hierarchy. It seems to perform inserts and deletes properly. On top of that, I can get my elements to my via drag and drop. I've overridden moveRows in my QAbstractItemModel subclass, and I call moveRows right before dropMimeData() returns. It appears that dropMimeData() needs to return false upon success after moveRows() is called for the QTreeView to update correctly. If it returns true, then my items will disappear in the view. I'll print out my scene's node hierarchy to the console, and everything looks correct. It just seems that the model doesn't update. Here's the implementation of my SceneModel class (subclassed from QAbstractItemModel):

const QString SceneModelMIMEType = "application/node.bin";

#include "SceneModel.h"
#include "Scene.h"
#include "Node.h"

#include <stdio.h>
#include <stdlib.h>

#include <QKeyEvent>
#include <QMimeData>
#include <QByteArray>
#include <QDebug>

Qt::ItemFlags SceneModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index);
if(index.isValid())
return defaultFlags | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsEditable;
else
return defaultFlags | Qt::ItemIsDropEnabled;

return QAbstractItemModel::flags(index);
}

Qt::DropActions SceneModel::supportedDropActions() const
{
return QAbstractItemModel::supportedDropActions() | Qt::MoveAction;
}

int SceneModel::rowCount(const QModelIndex &parent) const
{
// make sure the scene's root node is valid and the parent's column is zero
if(!Scene::GetInstance()->GetRoot() || parent.column() > 0)
return 0;

// return the parent's children, if valid
if(parent.isValid())
return static_cast<Node*>(parent.internalPointer())->GetNumChildren();

// otherwise, return the children
return Scene::GetInstance()->GetRoot()->GetNumChildren();
}

int SceneModel::columnCount(const QModelIndex &parent) const
{
return 1;
}

QModelIndex SceneModel::index(int row, int column, const QModelIndex &parent) const
{
// make sure the root, row and column data is valid
if(!Scene::GetInstance()->GetRoot() || !hasIndex(row, column, parent))
return QModelIndex();

// get the parent Node from the index
Node *parentItem = Scene::GetInstance()->GetRoot();
if(parent.isValid())
parentItem = static_cast<Node*>(parent.internalPointer());

Node *childItem = parentItem->GetChild(row);
if(childItem)
return createIndex(row, column, childItem);

return QModelIndex();
}

QModelIndex SceneModel::parent(const QModelIndex &index) const
{
// make sure the index is valid
if(!index.isValid())
return QModelIndex();

// get the child item to get the parent item
Node *childItem = static_cast<Node*>(index.internalPointer());
if(!childItem) return QModelIndex();
Node *parentItem = childItem->GetParent();

// return nothing if the parent item is the root
if(!parentItem || parentItem == Scene::GetInstance()->GetRoot())
return QModelIndex();

return createIndex(parentItem->GetChildIndex(), 0, parentItem);
}

QVariant SceneModel::data(const QModelIndex &index, int role) const
{
if(!index.isValid())
return QVariant();

// handle displaying the data
if(role == Qt::DisplayRole || role == Qt::EditRole)
{
// get the child and parent Node pointers
Node *childItem = static_cast<Node*>(index.internalPointer());
return QString(childItem->GetName().c_str());
}

return QVariant();
}

bool SceneModel::submit()
{
// check if the selected node is valid
Node *selectedNode = Scene::GetInstance()->GetSelectedNode();
if(selectedNode)
{
// get the selected node's index, and update its display data
QModelIndex selectedIndex = createIndex(selectedNode->GetChildIndex(), 0, selectedNode);
setData(selectedIndex, QString(selectedNode->GetName().c_str()), Qt::DisplayRole);
}
return true;
}

bool SceneModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
// check if the index is valid, and either in EditRole or DisplayRole mode
if(index.isValid() && (role == Qt::EditRole || role == Qt::DisplayRole))
{
//
Node *node = static_cast<Node*>(index.internalPointer());
if(!node) return false;

// set the node's new name, and signal that data has changed
node->SetName(std::string(value.toString().toUtf8()));
emit dataChanged(index, index);
return true;
}
return setData(index, value, role);
}

bool SceneModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
// make sure this is aciton shouldn't be ignored
if(!data || action == Qt::IgnoreAction)
return false;

// get rowCount if row is invalid (this might be an old Qt bug)
if(row == -1)
row = rowCount() - 1;

// get the parent node
Node *parentNode = static_cast<Node*>(parent.internalPointer());
if(!parentNode) return false;

// get the encoded data of our Node pointer
QByteArray encodedData = data->data(SceneModelMIMEType);
Node *node = (Node*)encodedData.toULongLong();
if(!node) return false;

Node *sourceParentNode = node->GetParent();
QModelIndex sourceParent = createIndex(sourceParentNode->GetChildIndex(), 0, sourceParentNode);

//qDebug() << "dropMimeData() - movedNode:" << node->GetName().c_str() << "parentNode:" << sourceParentNode->GetName().c_str();
/*qDebug()  << "sourceParent:" << sourceParentNode->GetName().c_str()
<< "destinationParent:" << parentNode->GetName().c_str()
<< "movingNode:" << node->GetName().c_str()
<< "sourceRow:" << node->GetChildIndex()
<< "destinationRow:" << row;*/

// move the row, but don't return true as that'll trigger the drop event
moveRow(sourceParent, node->GetChildIndex(), parent, row);
return false; // returning true causes weird behavior
}

QStringList SceneModel::mimeTypes() const
{
QStringList types;
types << SceneModelMIMEType;
return types;
}

QMimeData *SceneModel::mimeData(const QModelIndexList &indexes) const
{
// make sure there's exactly 1 index and it's valid
if(indexes.size() != 1 || !indexes[0].isValid())
return nullptr;

// get the Node pointer from the index's internal pointer, convert it to quintptr, then to a UTF string

// allocate mimeData, provide it with encodedData, and return it
QMimeData *mimeData = new QMimeData();
mimeData->setData(SceneModelMIMEType, encodedData);
return mimeData;
}

bool SceneModel::insertRows(int row, int count, const QModelIndex &parent)
{
Node *parentNode = static_cast<Node*>(parent.internalPointer());
if(!parentNode)
parentNode = Scene::GetInstance()->GetRoot();

// make sure a parent node was found before continuing
if(parentNode)
{
beginInsertRows(parent, row, row+count-1);

// insert nodes
for(int i=0;i<count;++i)
endInsertRows();
return true;
}
return QAbstractItemModel::insertRows(row, count, parent);
}

bool SceneModel::removeRows(int row, int count, const QModelIndex &parent)
{
Node *parentItem = static_cast<Node*>(parent.internalPointer());
if(!parentItem)
parentItem = Scene::GetInstance()->GetRoot();

// make sure a parent node was found before continuing
if(parentItem)
{
beginRemoveRows(parent, row, row+count-1);
parentItem->RemoveChildren(row, count);
endRemoveRows();
return true;
}
return QAbstractItemModel::removeRows(row, count, parent);
}

bool SceneModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild)
{
Node *sourceParentNode = static_cast<Node*>(sourceParent.internalPointer());
Node *destinationParentNode = static_cast<Node*>(destinationParent.internalPointer());
Node *childNode = sourceParentNode->GetChild(sourceRow);

// if source and destination parents are the same, move elements locally
if(sourceParentNode == destinationParentNode)
{
// only process if a local move is possible
if(sourceParentNode->IsMovePossible(sourceRow, count, destinationChild))
{
beginMoveRows(sourceParent, sourceRow, sourceRow+count-1, destinationParent, destinationChild);
sourceParentNode->MoveChildren(sourceRow, count, destinationChild);
endMoveRows();
}
} else {
// otherwise, move the node under the parent
beginMoveRows(sourceParent, sourceRow, sourceRow+count-1, destinationParent, destinationChild);
childNode->SetParent(destinationParentNode);
endMoveRows();
}
Scene::GetInstance()->GetRoot()->PrintChildren();
return true;
}

I didn't provide the interface since it doesn't have any member fields. It just has a constructor and destructor, but the constructor that initializes the base class with the passed-in QObject *parent parameter. I couldn't figure out how to embed the code above into code tags so that it's much easier to read. Sorry if something didn't make sense in my description above. I'm tired today.

##### Share on other sites
I'm having some problems trying to read the post. What are you trying to ask exactly?

##### Share on other sites
I don't have much experience in the area, when I have to deal with QAbstractItemModel it's usually for QML and everything I do there works (which is not much and less than you want there). Have you tried doing something that absolutely forces the QTreeView to reevaluate the model? Like setting an empty model and then setting the real model back? That's not a solution but it would be nice to know for sure if the model is basically sound.

Additionally, investigate if you can modify the tree without the drop events (maybe move an item to the root if a key is pressed). I'm unsure if it's healthy to modify items while receiving drop events for those items. Maybe you need to queue them up and process them after returning (for example via QMetaMethod::invoke... with Qt::QueuedConnection)?
I would look for examples though, moving stuff around in a tree view should not be a singular use case. Either there are very nice examples around or you should find people with similar issues.

##### Share on other sites

I'm having some problems trying to read the post. What are you trying to ask exactly?

I'm having issues with dropMimeData() in my code whenever have it return true on a successful drop. Nodes seem to disappear a frame later. I found out why, and I'll elaborate below.

I don't have much experience in the area, when I have to deal with QAbstractItemModel it's usually for QML and everything I do there works (which is not much and less than you want there). Have you tried doing something that absolutely forces the QTreeView to reevaluate the model? Like setting an empty model and then setting the real model back? That's not a solution but it would be nice to know for sure if the model is basically sound.

I wanted to do this myself, but I'm unable to. The reason being because I need to reset my model once SceneModel's dropMimeData() method is called. I would emit a signal to a slot in my MainWindow class where I could reset my model. The problem is, it seems like the only way one could reset a model in Qt5 is by unsetting the model from the QTreeView, releasing the model from memory, reallocating it, and re-setting the newly allocated model to the QTreeView. This will cause a crash because this is happening from emitting a signal within the scope of a method from an object that's going to be destroyed during the emitting call because the method hasn't returned yet.

Solution Found!

I found that when SceneModel::dropMimeData() returns true, SceneModel's base class, QAbstractItemModel, would internally call removeRows() on the node I just moved. My reimplementation of removeRows(), which is required, tells the parent node to delete the new child node I just moved under it. This would make sense why my pointers to nodes became dangling pointers. I looked into why this was, and it was due to my dropAction being set to InternalMove for my QTreeView. I thought InternalMove was what I needed, but it looks like DragDrop, does the job. The dangling pointer issue is resolved since my SceneModel no-longer removes nodes unexpectedly. My QTreeView's drag and drop performance also seems to work more fluidly. I used to have to select my node in a sweet spot to get it to work. I can also drag nodes into my the top-level hierarchy.

My hierarchy system is usable, but still kind of clunky. Here is a list of outstanding issues:

1. My QTreeView only has single selection enabled, but when I child two nodes under another node, the second node under the new parent is always selected until I select it.
2. Whenever I child a node, selection defaults to the second child to the parent node instead of what was selected
3. Whenever moving a node to another node, it always gets added to the bottom of the list when I place it between two nodes. I have to set its parent, then I can reorder the siblings. I think this is due to my Node class' functionality.
4. Whenever dragging Nodes to the top-level view, I'll get a warning in the console saying that there's an invalid index, although I still get the desired results.

I've found some answers to these issues:

1. It appears that whatever sourceRow is highlights that row indefinitely for some reason. It happened to be the second row under the new parent because I've been dragging the second child to the scene's root (index of 1) to the new parent. Then, I provide sourceRow to beginMoveRows(). I think this is to preserve selection after a move occurs, but it should be based on the destination child's index. After some Googling, I found that another person was having an issue with how their QTreeView would visually display his model's selection. It was due to how he overrode QAbstractItemModel::parent(). My thought was if modifying parent() fixed his selection behavior, then that's probably where my issue lies. In my parent() implementation, I wasn't allowing parent() to create a valid QModelIndex if the Node's parent was the scene's root. This is advice I got from a previous tutorial, but I'm not sure why that logic's there. I'm not sure what parent() even does. I removed the extra check against my scene's root, and this issue seems to be resolved.
2. See the previous point. This is fixed.
3. This was my SetParent() logic, as mentioned previously. I've added additional logic to my SetParent() method that resolves this issue.
4. Still working on this, but I think it has to do with how I'm passing parameters to beginMoveRows().
Edited by Vincent_M

##### Share on other sites

Found this post while doing something similar for a scenegraph editor, hopefully its not too late to add more to it.

I looked into why this was, and it was due to my dropAction being set to InternalMove for my QTreeView. I thought InternalMove was what I needed, but it looks like DragDrop, does the job. The dangling pointer issue is resolved since my SceneModel no-longer removes nodes unexpectedly. My QTreeView's drag and drop performance also seems to work more fluidly. I used to have to select my node in a sweet spot to get it to work. I can also drag nodes into my the top-level hierarchy.

Actually that does not seem to be entirely accurate, I think removeRows should remove (delete scene nodes), such that you can use it for that specific purpose, say in a context menu or when pressing the delete key while a node is selected. What I found is that you should enable the DragDropOverrideMode on the view for it not to call remove on the original indices, DragDrop vs InternalMove seems not to make a difference to me.

Either way, the design seems to be missing something, models deal almost exclusively with QModelIndex objects, with row,column and internalPointer being the most important aspects of them, but then when drag and drop comes into the picture, you have to deal with mime types, with no alternate option to just have a "drop" method for indices or an index list. Furthermore, the information encoded in the default mime data is row, column and a map with what the data method returns for each role, no internalPointer which is most likely what you really need in this situation.

Here is a partial solution that overrides QTreeView::dropEvent in order to access the selected items, but I rather not have to create a new class that inherits from QTreeView just for that, so I am looking into options, so far, I guess the least intrusive option would be overriding QAbstractItemModel::itemData to include a UserRole with the item's internalPointer so I dont have to override QAbstractItemModel::mimeData which would be a different mess.

Either way you definitely need to override QAbstractItemModel::dropMimeData, the default implementation calls insertRows, and tries to fill the inserted rows with setData/setItemData, which is probably not what you or I want since my insertRows implementation calls new Node and we don't want a new node, we want the already existing one moved. So a call to moveRows would be much more appropriate, but you need to rebuild the source indexes from the mime data.

##### Share on other sites

Actually that does not seem to be entirely accurate, I think removeRows should remove (delete scene nodes), such that you can use it for that specific purpose, say in a context menu or when pressing the delete key while a node is selected. What I found is that you should enable the DragDropOverrideMode on the view for it not to call remove on the original indices, DragDrop vs InternalMove seems not to make a difference to me.

I override removeRows() to specifically remove scene nodes from my own hierarchy's structure between beginRemoveRows() and endRemoveRows(). It's been a few weeks since I worked on this, but I think removeRows() was being called internally by the base class, QAbstractItemModel, whenever I moved something in my hierarchy. Changing drag and drop operations from InternalMove to DragDrop seemed to stop calling removeRows() internally.

I agree with trying to avoid having to subclass QTreeView for simple operations. You'd think it'd provide more intuitive signals for when selections change and occur, but hey, there are reimplementation methods for that (ugh)! Again, I'm still pretty new to Qt, and it looks like it doesn't directly handle selections, but rather the QSelectionModel field it has does this.

Either way, my hierarchy system appears to work like a charm now. There are only minor changes here, but here's the working source code I've got:

#include "SceneModel.h"
#include "Scene.h"
#include "Node.h"

#include <stdio.h>
#include <stdlib.h>

#include <QKeyEvent>
#include <QMimeData>
#include <QByteArray>
#include <QColor>
#include <QDebug>

Qt::ItemFlags SceneModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index);
if(index.isValid())
return defaultFlags | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsEditable;
else
return defaultFlags | Qt::ItemIsDropEnabled;

return QAbstractItemModel::flags(index);
}

Qt::DropActions SceneModel::supportedDropActions() const
{
return QAbstractItemModel::supportedDropActions() | Qt::MoveAction;
}

int SceneModel::rowCount(const QModelIndex &parent) const
{
// make sure the scene's root node is valid and the parent's column is zero
if(!scene || !scene->GetRoot() || parent.column() > 0)
return 0;

// return the parent's children, if valid
if(parent.isValid())
return static_cast<Node*>(parent.internalPointer())->GetNumChildren();

// otherwise, return the children
return scene->GetRoot()->GetNumChildren();
}

int SceneModel::columnCount(const QModelIndex&) const
{
return 1;
}

QModelIndex SceneModel::index(int row, int column, const QModelIndex &parent) const
{
// make sure the root, row and column data is valid
if(!scene || !scene->GetRoot() || !hasIndex(row, column, parent))
return QModelIndex();

// get the parent Node from the index
Node *parentItem = scene->GetRoot();
if(parent.isValid())
parentItem = static_cast<Node*>(parent.internalPointer());

Node *childItem = parentItem->GetChild(row);
if(childItem)
return createIndex(row, column, childItem);

return QModelIndex();
}

QModelIndex SceneModel::parent(const QModelIndex &index) const
{
// make sure the index is valid
if(!scene || !index.isValid())
return QModelIndex();

// get the child item to get the parent item
Node *childItem = static_cast<Node*>(index.internalPointer());
if(!childItem) return QModelIndex();
Node *parentItem = childItem->GetParent();

// return nothing if the parent item is the root
if(!parentItem || parentItem == scene->GetRoot())
return QModelIndex();

return createIndex(parentItem->GetChildIndex(), 0, parentItem);
}

QVariant SceneModel::data(const QModelIndex &index, int role) const
{
if(!index.isValid())
return QVariant();

if(role == Qt::ForegroundRole)
{
Node *node = static_cast<Node*>(index.internalPointer());
if(node && !node->IsActive())
return QColor(Qt::gray);
return QVariant();
}

// handle displaying the data
if(role == Qt::DisplayRole || role == Qt::EditRole)
{
// get the child and parent Node pointers
Node *node = static_cast<Node*>(index.internalPointer());
return QString(node->GetName().c_str());
}

return QVariant();
}

bool SceneModel::submit()
{
// make sure the scene is available
if(scene)
{
// check if the selected node is valid
if(selectedNode)
{
// get the selected node's index, and update its display data
QModelIndex selectedIndex = createIndex(selectedNode->GetChildIndex(), 0, selectedNode);
setData(selectedIndex, QString(selectedNode->GetName().c_str()), Qt::DisplayRole);
}
}
return QAbstractItemModel::submit();
}

bool SceneModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
// check if the index is valid, and either in EditRole or DisplayRole mode
if(index.isValid() && (role == Qt::EditRole || role == Qt::DisplayRole))
{
Node *node = static_cast<Node*>(index.internalPointer());
if(!node) return false;

// set the node's new name, and signal that data has changed
node->SetName(std::string(value.toString().toUtf8()));
emit dataChanged(index, index);

// emit signal that specifically signifies that the name has changed
if(role == Qt::EditRole)
emit SelectedNodeNameUpdated(node->GetName());
return true;
}
return QAbstractItemModel::setData(index, value, role);
}

bool SceneModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int, const QModelIndex &parent)
{
// make sure this is aciton shouldn't be ignored
if(!scene || !data || action == Qt::IgnoreAction)
return false;

// get the encoded data of our Node pointer
QByteArray encodedData = data->data(SceneModelMIMEType);
Node *node = (Node*)encodedData.toULongLong();
if(!node) return false;

// get the parent node
QModelIndex destinationParentIndex;
Node *parentNode = static_cast<Node*>(parent.internalPointer());
if(parentNode)
{
destinationParentIndex = parent;
} else {
parentNode = scene->GetRoot();
destinationParentIndex = createIndex(0, 1, scene->GetRoot());
}

// get rowCount if row is invalid (this might be an old Qt bug)
if(row == -1)
row = parentNode->GetNumChildren();

// move the row
Node *sourceParentNode = node->GetParent();
QModelIndex sourceParent = createIndex(sourceParentNode->GetChildIndex(), 0, sourceParentNode);
moveRow(sourceParent, node->GetChildIndex(), destinationParentIndex, row);
return true;
}

QStringList SceneModel::mimeTypes() const
{
QStringList types;
types << SceneModelMIMEType;
return types;
}

QMimeData *SceneModel::mimeData(const QModelIndexList &indexes) const
{
// make sure there's exactly 1 index and it's valid
if(indexes.size() != 1 || !indexes[0].isValid())
return nullptr;

// get the Node pointer from the index's internal pointer, convert it to quintptr, then to a UTF string

// allocate mimeData, provide it with encodedData, and return it
QMimeData *mimeData = new QMimeData();
mimeData->setData(SceneModelMIMEType, encodedData);
return mimeData;
}

bool SceneModel::insertRows(int row, int count, const QModelIndex &parent)
{
if(!scene)
return false;

Node *parentNode = static_cast<Node*>(parent.internalPointer());
if(!parentNode)
parentNode = scene->GetRoot();

// make sure a parent node was found before continuing
if(parentNode)
{
beginInsertRows(parent, row, row+count-1);

// insert nodes
for(int i=0;i<count;++i)
endInsertRows();
return true;
}
return QAbstractItemModel::insertRows(row, count, parent);
}

bool SceneModel::removeRows(int row, int count, const QModelIndex &parent)
{
if(!scene)
return false;

Node *parentItem = static_cast<Node*>(parent.internalPointer());
if(!parentItem)
parentItem = scene->GetRoot();

// make sure a parent node was found before continuing
if(parentItem)
{
beginRemoveRows(parent, row, row+count-1);
parentItem->RemoveChildren(row, count);
endRemoveRows();
return true;
}
return QAbstractItemModel::removeRows(row, count, parent);
}

bool SceneModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild)
{
Node *sourceParentNode = static_cast<Node*>(sourceParent.internalPointer());
Node *destinationParentNode = static_cast<Node*>(destinationParent.internalPointer());
Node *childNode = sourceParentNode->GetChild(sourceRow);

// make sure the child isn't one of the parent nodes
if(childNode == sourceParentNode || childNode == destinationParentNode)
return false;

// if source and destination parents are the same, move elements locally
if(sourceParentNode == destinationParentNode)
{
// only process if a local move is possible
if(sourceParentNode->IsMovePossible(sourceRow, count, destinationChild))
{
beginMoveRows(sourceParent, sourceRow, sourceRow+count-1, destinationParent, destinationChild);
sourceParentNode->MoveChildren(sourceRow, count, destinationChild);
endMoveRows();
return true;
}
} else {
// otherwise, move the node under the parent
beginMoveRows(sourceParent, sourceRow, sourceRow+count-1, destinationParent, destinationChild);
childNode->SetParent(destinationParentNode, destinationChild);
endMoveRows();
return true;
}
return false;
}



Are you trying to achieve a typical hierarchy-based scene graph with a tree view as well? Also, were you able to get it to work without the mandatory MIME data nonsense? I can see where MIME data is necessary when dragging files from say, Explorer or Finder into your view or items between separate views, but not from within the same view.

1. 1
2. 2
3. 3
Rutin
18
4. 4
JoeJ
14
5. 5

• 14
• 10
• 23
• 9
• 34
• ### Forum Statistics

• Total Topics
632634
• Total Posts
3007547
• ### Who's Online (See full list)

There are no registered users currently online

×