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 –
- First, it’ll check if the filter accepts the row itself (which is the default behaviour),
- After that it traverses all the way upto root to check if any of them match, and finally
- 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