Using QTreeView with QAbstractItemModel



For someone coming from the Cocoa world, understanding how QTreeView and QAbstractItemModel work can be quite hard. While the concept of an item model for a tree view also exists in Cocoa, the way QAbstractItemModel works is quite different from NSOutlineDatasource's.

The biggest difference between the two models is in the way they refer to cells. In Cocoa, we use an arbitrary identifier supplied by the model through outlineView:child:ofItem:. Then, this identifier is used in combination with a NSTableColumn instance for calls like outlineView:objectValueForTableColumn:byItem: (which is supposed to return what must be displayed for a particular cell). On the Qt side, we use a QModelIndex instance that is supplied by the index(row, column, parent) method. The strange thing with QModelIndex is that it not only holds an identifier, but it also holds a row and a column. Therefore, if index(1, 2, parentId) is called on the model, what must be returned is a QModelIndex(1, 2, childId) instance.

To a Qt newbie, this requirement for the model to return redundant data (row and column) might seem stupid. They might even think that it's downright insane when they discover that subclassing QAbstractItemModel also requires you to implement a parent(index) method. After all, the view knew what was the parent of that index when it asked for it in index(row, column, parent)! The poor Qt newbie might spend quite some time wondering what the heck was TrollTech thinking when they designed this API.

It turns out that there's a reason for this strange and complex API: TrollTech over-engineered this part of the toolkit. They wanted to design their item model so that it would be possible for a single model instance to have table views and tree views connected to it at the same time. While I'm sure that there are cases where this feature is very useful, there's a lot of QTreeView users out there who have no need for it and are suffering because of this over-engineering.

What to do?

While QAbstractItemModel is very hard to work with, it's possible to create tools to make it easier. The best way to do so, I think, is to create a subclass of QAbstractItemModel that abstracts away the worst aspects of dealing with it. Why Qt doesn't include such a subclass in the toolkit like they did with QAbstractTableModel and QAbstractListModel is beyond me, but well...

Alright, let's get to it. Typically, what we have is an internal structure of elements organized in a tree structure. We want the tree view to display our elements using the same internal organization, and for each element, we want to display some data in each columns of the tree view.

In a sane tree model system, you could, most of the time, directly use the element instance as an "internal reference" (a reference to an instance that the tree view keeps and supplies back to the model in its calls. QModelIndex.internalPointer() in Qt), but you can't do that in Qt because when the view calls parent(), your internal references need to know their own parent's row. If it just so happens that your internal elements have that, you're lucky, but you most likely don't have that. You could change your internal structure to accommodate Qt's need, but it is, IMHO, bad practice to have the gui layer dictate how the business layer organizes itself. You could also, like they do in the itemviews/simpletreemodel example, use indexOf() in each parent() call, but that would be very inefficient because parent() is called very often.

So, what do we do? We create a wrapper around each element. Let's call it TreeNode. It would look like this:

class TreeNode(object):
    def __init__(self, parent, row):
        self.parent = parent
        self.row = row
        self.subnodes = self._getChildren()

    def _getChildren(self):
        raise NotImplementedError()

It might seem very basic, but remember that all we want to deal with is the index() and parent() calls which are a pain to work with. For the TreeNode to be usable in any circumstances, it must be fairly minimal. The model part (which we can call TreeModel) would look like this:

class TreeModel(QAbstractItemModel):
    def __init__(self):
        QAbstractItemModel.__init__(self)
        self.rootNodes = self._getRootNodes()

    def _getRootNodes(self):
        raise NotImplementedError()

    def index(self, row, column, parent):
        if not parent.isValid():
            return self.createIndex(row, column, self.rootNodes[row])
        parentNode = parent.internalPointer()
        return self.createIndex(row, column, parentNode.subnodes[row])

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()
        node = index.internalPointer()
        if node.parent is None:
            return QModelIndex()
        else:
            return self.createIndex(node.parent.row, 0, node.parent)

    def reset(self):
        self.rootNodes = self._getRootNodes()
        QAbstractItemModel.reset(self)

    def rowCount(self, parent):
        if not parent.isValid():
            return len(self.rootNodes)
        node = parent.internalPointer()
        return len(node.subnodes)

Now that your base TreeModel subclass is done, it becomes fairly easy to implement tree models without having to touch the ugliest parts of model implementation, for example:

class NamedElement(object): # your internal structure
    def __init__(self, name, subelements):
        self.name = name
        self.subelements = subelements

class NamedNode(TreeNode):
    def __init__(self, ref, parent, row):
        self.ref = ref
        TreeNode.__init__(self, parent, row)

    def _getChildren(self):
        return [NamedNode(elem, self, index)
            for index, elem in enumerate(self.ref.subelements)]

class NamesModel(TreeModel):
    def __init__(self, rootElements):
        self.rootElements = rootElements
        TreeModel.__init__(self)

    def _getRootNodes(self):
        return [NamedNode(elem, None, index)
            for index, elem in enumerate(self.rootElements)]

    def columnCount(self, parent):
        return 1

    def data(self, index, role):
        if not index.isValid():
            return None
        node = index.internalPointer()
        if role == Qt.DisplayRole and index.column() == 0:
            return node.ref.name
        return None

    def headerData(self, section, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole \
            and section == 0:
            return 'Name'
        return None

This is, of course, very basic, and if you actually use a TreeModel-like class, you likely will add stuff like lazy subnodes creation and data buffering (as I did). Feel free to use my own implementation if you want.