QTreeView and custom filter models

I’ve been learning PySide which is an amazingly easy library to get into GUI development. For an application I was developing, I needed a search filter for some hierarchical data represented by QTreeView.  QSortFilterProxyModel is the default choice to add a filter to a QTreeView, but it, by default, applies the filter to all nodes from any root to leaf. If any node has to show up in the search result, all the nodes from a root to that node have to match the search filter, which is mostly useless for hierarchical data. From the documentation: “For hierarchical models, the filter is applied recursively to all children. If a parent item doesn’t match the filter, none of its children will be shown.”

However, it is easy to setup our own class to inherit from QSortFilterProxyModel as shown in this forum post, and then write our own filterAcceptsRow method which will do three things –

  1. First, it’ll check if the filter accepts the row itself (which is the default behaviour),
  2. After that it traverses all the way upto root to check if any of them match, and finally
  3. Check if any of the children match

The filterAcceptsRow method will call a method for the three steps and return True if any of those return True

class LeafFilterProxyModel(QtGui.QSortFilterProxyModel):
    ''' Class to override the following behaviour:
            If a parent item doesn't match the filter,
            none of its children will be shown.

        This Model matches items which are descendants
        or ascendants of matching items.
    '''

    def filterAcceptsRow(self, row_num, source_parent):
        ''' Overriding the parent function '''

        # Check if the current row matches
        if self.filter_accepts_row_itself(row_num, source_parent):
            return True

        # Traverse up all the way to root and check if any of them match
        if self.filter_accepts_any_parent(source_parent):
            return True

        # Finally, check if any of the children match
        return self.has_accepted_children(row_num, source_parent)

Writing the code for first function – test if a row matches a filter – is easy, because the parent class already implements it:

    def filter_accepts_row_itself(self, row_num, parent):
        return super(LeafFilterProxyModel, self).filterAcceptsRow(row_num, parent)

Second function is to check if any of the ancestors (root to current node) match the filter. Traverse all the way to the root, and use method 1 for testing if any item matches the filter:

    def filter_accepts_any_parent(self, parent):
        ''' Traverse to the root node and check if any of the
            ancestors match the filter
        '''
        while parent.isValid():
            if self.filter_accepts_row_itself(parent.row(), parent.parent()):
                return True
            parent = parent.parent()
        return False

Lastly, check if any of the children match the filter. The method here is very inefficient (recursive depth first search), any large data will need some sort of caching.

    def has_accepted_children(self, row_num, parent):
        ''' Starting from the current node as root, traverse all
            the descendants and test if any of the children match
        '''
        model = self.sourceModel()
        source_index = model.index(row_num, 0, parent)

        children_count =  model.rowCount(source_index)
        for i in xrange(children_count):
            if self.filterAcceptsRow(i, source_index):
                return True
        return False

Putting all the pieces together:

from PySide import QtGui

class LeafFilterProxyModel(QtGui.QSortFilterProxyModel):
    ''' Class to override the following behaviour:
            If a parent item doesn't match the filter,
            none of its children will be shown.

        This Model matches items which are descendants
        or ascendants of matching items.
    '''

    def filterAcceptsRow(self, row_num, source_parent):
        ''' Overriding the parent function '''

        # Check if the current row matches
        if self.filter_accepts_row_itself(row_num, source_parent):
            return True

        # Traverse up all the way to root and check if any of them match
        if self.filter_accepts_any_parent(source_parent):
            return True

        # Finally, check if any of the children match
        return self.has_accepted_children(row_num, source_parent)

    def filter_accepts_row_itself(self, row_num, parent):
        return super(LeafFilterProxyModel, self).filterAcceptsRow(row_num, parent)

    def filter_accepts_any_parent(self, parent):
        ''' Traverse to the root node and check if any of the
            ancestors match the filter
        '''
        while parent.isValid():
            if self.filter_accepts_row_itself(parent.row(), parent.parent()):
                return True
            parent = parent.parent()
        return False

    def has_accepted_children(self, row_num, parent):
        ''' Starting from the current node as root, traverse all
            the descendants and test if any of the children match
        '''
        model = self.sourceModel()
        source_index = model.index(row_num, 0, parent)

        children_count =  model.rowCount(source_index)
        for i in xrange(children_count):
            if self.filterAcceptsRow(i, source_index):
                return True
        return False