Using QTreeView with QAbstractItemModel
For someone coming from the Cocoa world, understanding how
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
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?
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
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
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.