PDA

View Full Version : Implementing a QAbstractItemModel that reacts to external changes



Glax
4th January 2020, 17:35
I'm writing a QAbstractItemModel for a tree view that represents a hierarchy of QObjects.
I'm storing the pointer to the QObject as the internal pointer in the model indexes.

Sometimes the objects are added/removed from the hierarchy (always outside the view / item model) and I want the model to notify the view about the change.

The problem I'm having is that functions like beginRemoveRows etc say that they need to be called before the removal has happened and functions like endRemoveRows should happen after the data has been removed.

Since I'm reacting to changes happening outside the model this isn't possible as I can be notified of the change having already happened in the storage.
I tried just calling the begin/end functions but doing that causes the model to call parent() passing indexes with objects which have already been deleted.

Is there any way around this issue?

d_stranz
4th January 2020, 18:46
QObject instances send a signal (QObject::destroyed()) just before they are completely destroyed. If you are storing pointers to QObjects in your model, you could connect a slot to these signals and set the stored pointers to null. Obviously when you access a stored pointer, you'll have to also check it for validity. Depending on how large your tree is, traversing the tree to find the QModelIndex containing a specific QObject pointer could be expensive, so you might want to create a map between QObject and QModelIndex (using QPersistentModelIndex).

If this is not workable, then instead of using begin/end..Rows() calls, use beginModelReset / endModelReset. This tells any views that the entire model is invalid, so it will not try to access anything and will simply re-create the entire view from scratch. This also works if you have proxy models in-between your core model and views.

Glax
4th January 2020, 18:58
I'm already using the destroyed signal (calling beginRemoveRows / endRemoveRows from there) the invalid objects are accessed on beginInsertRows when something is added after something else got deleted.
I tried using reset model but that collapses all tree items in the view which is annoying.

At the moment I'm using a dirty work-around that keeps all pointers in a set and add/removes them recursively when something changes, then checking for validity to avoid segmentation faults.
But I still haven't figured why Qt is sending me invalid indexes from rows that have already been deleted.

d_stranz
5th January 2020, 01:50
But I still haven't figured why Qt is sending me invalid indexes from rows that have already been deleted.

Maybe you are sending the wrong indexes or parent in your calls to beginRemoveRows() ?

Glax
5th January 2020, 12:37
I don't think so. The view does react correctly, removing the rows that have been deleted.

This is the relevant code, element_deleted is connected to QObject::destroyed


QModelIndex ProjectModel::index_from_qobject(QObject* obj) const
{
if ( !obj->parent() )
return createIndex(0, 0, obj);
return createIndex(obj->parent()->children().indexOf(obj), 0, obj);
}

void ProjectModel::element_deleted(QObject* obj)
{
QObject* parent = obj->parent();
auto parent_index = index_from_qobject(parent);
int row = parent->children().indexOf(obj);
beginRemoveRows(parent_index, row, row);
endRemoveRows();
}

d_stranz
6th January 2020, 01:48
You are accessing the properties of the QObject even though it has been destroyed and those properties are almost certainly no longer valid. For example, in line 10, you access the object's parent(). You don't check to see if the pointer returned is non-null before passing it to index_from_qobject(), which then checks -that- QObject's parent(), and further, accesses that parent's children() and the indexOf() for those children. Nowhere do you check any return value for validity. Any of those parent() calls could be returning a null pointer, and the children() call could be returning an empty list.

I think that by the time you receive the QObject::destroyed() signal, you have to assume that the QObject pointer is no better than a void *, since you really don't know where in the destruction cycle that signal get emitted. Maybe it is bottom-up in the QObject parent-child hierarchy, but you can't assume that the parent of the QObject hasn't already cleaned up its children() list before deleting the child. So maybe indexOf() is returning -1.

I think the basic problem is that your tree model is coupled too closely with the QObject hierarchy, and you are relying on the QObject hierarchy remaining intact throughout the deletion process so you can maintain your tree hierarchy. You need to decouple them by creating a separate tree data structure that mirrors the QObject hierarchy but has its own methods for determining parent-child relationships, independently of the QObject hierarchy.

Glax
6th January 2020, 07:49
I'm just going by the documentation

https://doc.qt.io/qt-5/qobject.html#destroyed

destroyed is emitted while the object still exists, debugging shows it still has the parent pointer set up correctly.

Children are destroyed before their parents in my setup but in any case I'm having this issue with leaf objects