Monday, February 02, 2015

PySide Tree Tutorial IIIB: QAbstractItemModel's API

Part of a series on treebuilding in PySide: see Table of Contents.

In the next two posts, we will go through the methods instantiated in our model, starting now with  rowCount(), columnCount(), data(), and headerData(). In the following post we will round it out with a discussion of index() and parent(), which are especially important in hierarchical models.

Let's start with rowCount().

The rowCount() method takes a parent index and returns the number of children the corresponding parent item has. Views call rowCount() to determine how many rows need to be displayed underneath a given parent item.

Recall that in simple, single-level data structures like tables, each item has the same (invalid) parent, so we can get away with returning a single number in response to rowCount() (Figure 2 in post IB). This strategy won't work with tree models, in which different parents typically have different numbers of children.

In our example, rowCount() is implemented as follows:

def rowCount(self, parent):
    if parent.column() > 0:
        return 0
    if not parent.isValid():
        parentItem = self.rootItem
        parentItem = parent.internalPointer()
    return parentItem.childCount()

The basic strategy in rowCount() is to extract the parent index's corresponding parentItem and then return the number of children this item has using the built-in item method TreeItem.childCount(). The parentItem is extracted from its index using internalPointer(). While this might seem a strange name (Python has no pointers), you can think of internalPointer() as a getItemFromIndex() method that refers to the TreeItem corresponding to an index (Figure 6).

Figure 6: internalPointer() pulls an item from an index.
Each index includes an internalPointer() method
that returns the TreeItem associated with that index.

While the core calculation is relatively simple, there are a couple of wrinkles. First, our convention is that only the first column in a row has children, so if parent.column() is greater than 0, then rowCount() returns 0. Second, as discussed above, if the parent index is the invalid QModelIndex(), then the parent item is the root item. Finally, if the parent is not the root, then we follow the algorithm described in the previous paragraph.

The columnCount() method takes in a parent index and returns the number of columns the corresponding parent item has. The view calls this method to ask the model how many columns to display under a parent item:

def columnCount(self, parent):
    if not parent.isValid():
        return self.rootItem.columnCount()
        return parent.internalPointer().columnCount()

The basic algorithm is simple: extract the parent item corresponding to the given parent index, and then call TreeItem's built-in columnCount() on this parent item. As before, if the parent index is invalid, we assume the index corresponds to the root item.

data(index, role)
Given an item's index and a desired role, data() tells the view what to display at that index's location:

def data(self, index, role):
    if not index.isValid():
        return None
    if role != QtCore.Qt.DisplayRole:
        return None
    item = index.internalPointer()

The model does not know when it will be used, or which data the view will ask for. It lies in wait, providing data each time the view requests it, using the universal interface.

When the role is set as DisplayRole, the view is asking what text to display at the location specified by the given index. Recall that each TreeItem contains all the data for an entire row of the tree (Figure 4, post IIB), but the view needs to know what text to display in just one column. Luckily, each index already has a built-in column() method, and each TreeItem has a built-in data() method that returns the data from column j of that item. In, we compose these two functions to pull the appropriate column of data from the item corresponding to the given index.

When querying the model with data(), the view sends the index of the item to be displayed, as well as a single role parameter (of type QtCore.Qt.itemDataRole). What exactly is this itemDataRole? Roles are sent by the view to indicate the type of data it is looking for, such as text, font styles, background color, and other information.

Each role is sent as a separate call to by the view. The model should always return values of the appropriate type for a given role. A partial list of the different roles and their expected return type is shown in Table 2. You can find an exhaustive enumeration in the PySide documentation.

Role Description Expected return type
DisplayRole Data to be displayed as text. Python str
ToolTipRole Text to temporarily display when you hover mouse over an item. Python str
FontRole Font with which to render items. QtGui.QFont
TextAlignmentRole Text alignment for item. QtCore.Qt.AlignmentFlag
BackgroundRole Set background color. QtGui.QBrush

Table 2: Some itemDataRoles, their descriptions, and return types.

Our simpletreemodel example only supports the DisplayRole. However, it is instructive to play around with other roles. For instance you could try adding:

if role == QtCore.Qt.ToolTipRole:
    return "Scalawag!"

This will display the helpful tooltip "Scalawag!" over each item in the view when you hover over it with your mouse. To change the background color in the first column of the tree, try this:

if role == QtCore.Qt.BackgroundRole:
    if index.column() == 0:
        return QtGui.QBrush(QtGui.QColor(QtCore.Qt.yellow))

The result is ugly, to be sure, but it's the principal that matters.

Some developers argue that this functionality, whereby the model controls how items appear, violates the desired division of labor between views and models. This is a valid concern, and some programmers leave all such appearance customization to delegates. But since we are ignoring delegates for now, it is useful to know how to sneak formatting in via the model.

headerData(section, orientation, role)
The headerData() function extracts the header data from the root item, and paints it in the column headers:

def headerData(self, section, orientation, role):
    if orientation==QtCore.Qt.Horizontal and role==QtCore.Qt.DisplayRole:
    return None

headerData() works similarly to Note also that section is a generic term for the row or column number, depending on whether the orientation is vertical or horizontal, respectively.

No comments: