PyQt custom AbstractItemModel

This is difficult to wrap my head around, even with the two pretty explanatory tutorials on qt-project.org:
http://qt-project.org/doc/qt-4.8/model-view-programming.html
With this being the most comprehensive:
http://qt-project.org/doc/qt-4.8/itemviews-simpletreemodel.html

But here’s a shorter version, in python, brought the other way around. I’ll start with a screen, then fill it with data.

It means you can check for errors during the process instead of copying everything from the example and hoping it works in the end plus you get rewarded in steps instead of having to do a lot at once without seeing results.

So here’s a little widget with a QTreeView, just creating the custom model here, custom views are mroe of a follow up thing:

from PyQt4 import QtCore, QtGui

class CustomModel(QtCore.QAbstractItemModel):
    def __init__(self, in_nodes):
        QtCore.QAbstractItemModel.__init__(self)

class CustomNode(object):
    def __init__(self, in_data):
        pass

def main():
    items = []
    for i in 'abc':
        items.append( CustomNode(i) )
    v = QtGui.QTreeView()
    v.setModel( CustomModel(items) )
    v.show()
    return v
v = main()

I define stub classes, initialize the model and add three stub items which are not even used in the initializer.
This shows the application, but also gives two errors to start with:

# NotImplementedError: QAbstractItemModel.columnCount() is abstract and must be overridden
# NotImplementedError: QAbstractItemModel.rowCount() is abstract and must be overridden

So the view asks the model the number of rows and columns to show. Interesting point about the treeview: it asks the rowCount on each item, so the model does not need to return the total number of items, only the number of root items, making it easy.

Then there’s the columncount, this is proving cumbersome as it will not change the number of columns per item and therefore the maximum number of columns needs to be returned. But let’s just start with one.

class CustomModel(QtCore.QAbstractItemModel):
    def __init__(self, in_nodes):
        QtCore.QAbstractItemModel.__init__(self)
        self._root = CustomNode(None)

    def rowCount(self, in_index):
        if in_index.isValid():
            return in_index.internalPointer().childCount()
        return self._root.childCount()

    def columnCount(self, in_index):
        return 1

class CustomNode(object):
    def __init__(self, in_data):
        self._children = []

    def childCount(self):
        return len(self._children)

Here’s for making it error free, also I added a root item. The root item determines the root level children, so any children of the root node are the items on display. This functionality can in fact be incorporated into the model but as it’s all behaviour we need on the items it’s easier to just add an item to the model.

The rowCount gets a QModelIndex as argument in case the viewer is fetching children of something that is not the root. In this case we can simply take the pointer to the node and get its childcount. Otherwise we assume the root’s childcount.

Next we can start filling in the CustomNode. This node needs to know quite a lot of things. First we can parse the input data to determine what text it should display:

class CustomNode(object):
    def __init__(self, in_data):
        self._data = in_data
        if type(in_data) == tuple:
            self._data = list(in_data)
        if type(in_data) in (str,unicode) or not hasattr(in_data, '__getitem__'):
            self._data = [in_data]

        self._columncount = len(self._data)
        self._children = []
        self._parent = None
        self._row = 0

Also the data needs a getter:

    def data(self, in_column):
        if in_column >= 0 and in_column < len(self._data):
            return self._data[in_column]

How many columns its data yields:

    def columnCount(self):
        return self._columncount

How many children (rows) it contains,

    def childCount(self):
        return len(self._children)

What a child on a given row is:

    def child(self, in_row):
        if in_row >= 0 and in_row < self.childCount():
            return self._children[in_row]

What it’s own parent is,

    def parent(self):
        return self._parent

What it’s own row in that parent is,

    def row(self):
        return self._row

Also it should be able to add child nodes (both for tree views and for the _root node).

    def addChild(self, in_child):
        in_child._parent = self
        in_child._row = len(self._children)
        self._children.append(in_child)
        self._columncount = max(in_child.columnCount(), self._columncount)

All this using getter functions so the data remains private and up to date as long as not touchded directly. The columncount is always the max columncount required to display all columns of any children simply because otherwise the QTreeView doesn’t understand to display additional columns for children that have more columns than their parent.

Now that the item is setup we actually have to setup the model to contain the items and to display them properly. We must provide an index method for QTreeView to use and also update the initializer to add nodes.

class CustomModel(QtCore.QAbstractItemModel):
    def __init__(self, in_nodes):
        QtCore.QAbstractItemModel.__init__(self)
        self._root = CustomNode(None)
        for node in in_nodes:
            self._root.addChild(node)

    def addChild(self, in_node, in_parent):
        if not in_parent or not in_parent.isValid():
            parent = self._root
        else:
            parent = in_parent.internalPointer()
        parent.addChild(in_node)

The index method needs to return a QModelIndex based upon the row, column and parent given. The parent is used in case we wish to query child nodes, as the rows do not travel through levels but are always direct children.

    def index(self, in_row, in_column, in_parent=None):
        if not in_parent or not in_parent.isValid():
            parent = self._root
        else:
            parent = in_parent.internalPointer()
    
        if not QtCore.QAbstractItemModel.hasIndex(self, in_row, in_column, in_parent):
            return QtCore.QModelIndex()
    
        child = parent.child(in_row)
        if child:
            return QtCore.QAbstractItemModel.createIndex(self, in_row, in_column, child)
        else:
            return QtCore.QModelIndex()

We must validate the parent given, or resort to the _root node, then from this parent we need to fetch the child in the given row and column, but if it doesn’t exist we simply return an empty index so the view knows it’s trying something impossible.

Then we need to forward some of the methods of the nodes, such as getting the parent and converting it to an index, fetching display data and we need to update the columnCount. We already took care of rowCount previously.

    def parent(self, in_index):
        if in_index.isValid():
            p = in_index.internalPointer().parent()
            if p:
                return QtCore.QAbstractItemModel.createIndex(self, p.row(),0,p)
        return QtCore.QModelIndex()

    def columnCount(self, in_index):
        if in_index.isValid():
            return in_index.internalPointer().columnCount()
        return self._root.columnCount()

    def data(self, in_index, role):
        if not in_index.isValid():
            return None
        node = in_index.internalPointer()
        if role == QtCore.Qt.DisplayRole:
            return node.data(in_index.column())
        return None

Updating the main function to prove it’s functionality as a tree and table:

def main():
    items = []
    for i in 'abc':
        items.append( CustomNode(i) )
        items[-1].addChild( CustomNode(['d','e','f']) )
        items[-1].addChild( CustomNode(['g','h','i']) )
    v = QtGui.QTreeView()
    v.setModel( CustomModel(items) )
    v.show()
    return v
v = main()

Here’s the full code again:

from PyQt4 import QtCore, QtGui


class CustomNode(object):
    def __init__(self, in_data):
        self._data = in_data
        if type(in_data) == tuple:
            self._data = list(in_data)
        if type(in_data) in (str,unicode) or not hasattr(in_data, '__getitem__'):
            self._data = [in_data]

        self._columncount = len(self._data)
        self._children = []
        self._parent = None
        self._row = 0

    def data(self, in_column):
        if in_column >= 0 and in_column < len(self._data):
            return self._data[in_column]

    def columnCount(self):
        return self._columncount

    def childCount(self):
        return len(self._children)

    def child(self, in_row):
        if in_row >= 0 and in_row < self.childCount():
            return self._children[in_row]

    def parent(self):
        return self._parent

    def row(self):
        return self._row

    def addChild(self, in_child):
        in_child._parent = self
        in_child._row = len(self._children)
        self._children.append(in_child)
        self._columncount = max(in_child.columnCount(), self._columncount)


class CustomModel(QtCore.QAbstractItemModel):
    def __init__(self, in_nodes):
        QtCore.QAbstractItemModel.__init__(self)
        self._root = CustomNode(None)
        for node in in_nodes:
            self._root.addChild(node)

    def rowCount(self, in_index):
        if in_index.isValid():
            return in_index.internalPointer().childCount()
        return self._root.childCount()

    def addChild(self, in_node, in_parent):
        if not in_parent or not in_parent.isValid():
            parent = self._root
        else:
            parent = in_parent.internalPointer()
        parent.addChild(in_node)

    def index(self, in_row, in_column, in_parent=None):
        if not in_parent or not in_parent.isValid():
            parent = self._root
        else:
            parent = in_parent.internalPointer()
    
        if not QtCore.QAbstractItemModel.hasIndex(self, in_row, in_column, in_parent):
            return QtCore.QModelIndex()
    
        child = parent.child(in_row)
        if child:
            return QtCore.QAbstractItemModel.createIndex(self, in_row, in_column, child)
        else:
            return QtCore.QModelIndex()

    def parent(self, in_index):
        if in_index.isValid():
            p = in_index.internalPointer().parent()
            if p:
                return QtCore.QAbstractItemModel.createIndex(self, p.row(),0,p)
        return QtCore.QModelIndex()

    def columnCount(self, in_index):
        if in_index.isValid():
            return in_index.internalPointer().columnCount()
        return self._root.childCount()

    def data(self, in_index, role):
        if not in_index.isValid():
            return None
        node = in_index.internalPointer()
        if role == QtCore.Qt.DisplayRole:
            return node.data(in_index.column())
        return None


def main():
    items = []
    for i in 'abc':
        items.append( CustomNode(i) )
        items[-1].addChild( CustomNode(['d','e','f']) )
        items[-1].addChild( CustomNode(['g','h','i']) )
    v = QtGui.QTreeView()
    v.setModel( CustomModel(items) )
    v.show()
    return v
v = main()

5 thoughts on “PyQt custom AbstractItemModel

  1. Thank you! Very comprehensive explanation of quite obscure subject.

    BTW, CustomModel.columnCount shouldn’t return self._root.columnCount() by default — instead of self._root.childCount()?

  2. In your function rowCount() for the CustomModel I believe you have inadvertently indented return self._root.childCount() so you have two returns for the if statement rather than one for the if and one for if the if fails 😉

Leave a Reply

Your email address will not be published. Required fields are marked *