Now we get to the guts of the API, and what really separates our model from a table model. If we were just building a table model, subclassing QAbstractTableModel, our model would be done. Because we are subclassing QAbstractItemModel, we must provide two additional methods: index() and parent(). The view needs these methods to navigate among items in the tree.
We can view index() and parent() as inverse methods; parent() takes in a child index and returns the index of its parent, while index() takes in a parent index and returns the index of one of its children (Figure 7A). We implement both methods as outlined in Figure 7B, which you might want to study before looking at the details of the code: it is sometimes easy to lose the forest for the trees with these functions.
|Figure 7: Implementing parent() and index()
A. The basic logic of parent() and index(). TreeModel.parent() takes
in the index of a child and returns the index of its parent, while TreeModel.index()
takes in a parent index and returns the index of one of its children. B. Details about
how parent() and index() are implemented. The flow of index() is
counterclockwise (red arrows), and parent() is clockwise (green arrows). In each
case, the given index's associated TreeItem is retrieved using internalPointer().
Then, its parent (child) item is accessed using the child() (parent()) method
that was built into TreeItem. Finally, that item's index is created using
createIndex(), which takes this item as one of its parameters.
Figure 7B adapted from:
We'll start by looking at the implementation of index().
index(row, column, parent)
This method takes in the index of a parent item, and returns the index of one of its children. We implement it with:
While the basic strategy here is outlined in Figure 7B, it also has to handle some special cases. First, we check the validity of the input coordinates with hasIndex(): it determines if row and column are nonnegative and fit within the range of values allowed by parent. For instance, if the parent item has three children, hasIndex() will return False if the row argument exceeds two.
If hasIndex() returns False, then the coordinates submitted by the view are not valid, and we return the invalid index. Otherwise, we retrieve the parentItem that corresponds to parent, and then extract that item's desired child using TreeItem.child(row).
Once we have extracted the appropriate child item from its parent, we wrap it up into an index using createIndex(row, column, childItem). This is the built-in method that all models use to create new indexes. It requires that we specify the item's row number and column number, as well the TreeItem to which the resulting index will refer--this is the item that its internalPointer() will return.
Recall that each element in TreeItem.itemData corresponds to a different column in a row of our model (Figure 4, post IIB). Hence, in general there is a many-to-one relationship (in this case a 2:1 relationship) between model indexes and TreeItems. Given N rows in our tree,then 2N calls to index() would be required to specify all the indexes.
As discussed above, this method takes in an item's index, and returns the index of its parent:
The basic strategy is illustrated in Figure 7B, but there are a few wrinkles we should consider. First, if the index is invalid (i.e., it is the root index), then it has no parent and we return an invalid index. Also, as discussed in part IIIA, we return the invalid index as the parent of any top-level items in the model, i.e., the items whose parents are rootItem.
For all lower-level items, we create the parent index with createIndex(). This is a little more subtle than in the index() method: we must specify the row and column numbers of the parentItem relative to its parent (i.e, the grandparent of childItem). To find the row that parentItem occupies among its siblings, we use TreeItem.row(). For the column value, we follow the convention that only items in the first column of our model have children (that is, we set the column parameter for createIndex() to zero).
We are pretty much done going over the code. While we will briefly discuss setupModelData() in the next post, we are done going over the API provided by the model for the view. Once the tree structure and model are built, then the model is ready to be connected to a view. This is easily done with QTreeView.setModel(TreeModel). When we call show() on the view, the GUI should appear on our screen as in Figure 3 (post IIA).