(PyQt) QTreeView - want to expand/collapse all children and grandchildren

14,639

Solution 1

Ok... siblings did not actually get me to where I wanted to go. I managed to get the code working as follows (and it seems like a decent implementation). Kudos still to Prof.Ebral who got me going on the right track with the idea of siblings (turns out I needed to use QModelIndex.child(row, column) and iterate recursively from there).

Note that there is the following assumption in the code: It assumes that your underlying data store objects have the ability to report how many children they have (get_child_count() in my code). If that is not the case, you will somehow have to get a child count differently... perhaps by just arbitrarily trying to get child indexes - using QModelIndex.child(row, col) - with an ever increasing row count till you get back an invalid index? - this is what Prof.Ebral suggested and I might still try that (It is just that I already have an easy way to get the child count by requesting it from my data store).

Also note that I actually expand/collpase each node at a different point in the recursion based on whether I am expanding or collapsing. This is because, through trial and error, I discovered that animated tree views would stutter and pop if I just did it at one place in the code. Now, by reversing the order in which I do it based on whether I am at the top level (i.e. the root of the branch I am affecting - not the root of the entire treeview) I get a nice smooth animation. This is documented below.

The following code is in a QTreeView subclass.

#---------------------------------------------------------------------------
def keyPressEvent(self, event):

    if (event.key() == QtCore.Qt.Key_Space and self.currentIndex().column() == 0):
        shift = event.modifiers() & QtCore.Qt.ShiftModifier
        if shift:
            self.expand_all(self.currentIndex())
        else:                
            expand = not(self.isExpanded(self.currentIndex()))
            self.setExpanded(self.currentIndex(), expand)


#---------------------------------------------------------------------------
def expand_all(self, index):
    """
    Expands/collapses all the children and grandchildren etc. of index.
    """
    expand = not(self.isExpanded(index))
    if not expand: #if collapsing, do that first (wonky animation otherwise)
        self.setExpanded(index, expand)    
    childCount = index.internalPointer().get_child_count()
    self.recursive_expand(index, childCount, expand)
    if expand: #if expanding, do that last (wonky animation otherwise)
        self.setExpanded(index, expand)


#---------------------------------------------------------------------------
def recursive_expand(self, index, childCount, expand):
    """
    Recursively expands/collpases all the children of index.
    """
    for childNo in range(0, childCount):
        childIndex = index.child(childNo, 0)
        if expand: #if expanding, do that first (wonky animation otherwise)
            self.setExpanded(childIndex, expand)
        subChildCount = childIndex.internalPointer().get_child_count()
        if subChildCount > 0:
            self.recursive_expand(childIndex, subChildCount, expand)
        if not expand: #if collapsing, do it last (wonky animation otherwise)
            self.setExpanded(childIndex, expand)

Solution 2

model.rowCount(index) is the method you want.

model = index.model()   # or some other way of getting it
for i in xrange(model.rowCount(index)):
  child = model.index(i,0, index)
  # do something with child

model.index(row,col, parent) is essentially the same as calling index.child(row,col); just with fewer indirections.

Solution 3

I make a evnetFilter Class for that. My particular use case is shift click the drop indicator then expand all or collapse all the child nodes like software maya outliner.


class MTreeExpandHook(QtCore.QObject):
    """
    MTreeExpandHook( QTreeView )
    """

    def __init__(self, tree):
        super(MTreeExpandHook, self).__init__()
        tree.viewport().installEventFilter(self)
        self.tree = tree

    def eventFilter(self, receiver, event):
        if (
            event.type() == QtCore.QEvent.Type.MouseButtonPress
            and event.modifiers() & QtCore.Qt.ShiftModifier
        ):
            pos = self.tree.mapFromGlobal(QtGui.QCursor.pos())
            index = self.tree.indexAt(pos)
            if not self.tree.isExpanded(index):
                self.tree.expandRecursively(index)
                return True
        return super(MTreeExpandHook, self).eventFilter(self.tree, event)

Usage Example below


import sys
from PySide2 import QtCore,QtGui,QtWidgets

class MTreeExpandHook(QtCore.QObject):
    """
    MTreeExpandHook( QTreeView )
    """

    def __init__(self, tree):
        super(MTreeExpandHook, self).__init__()
        self.setParent(tree)
        # NOTE viewport for click event listen
        tree.viewport().installEventFilter(self)
        self.tree = tree

    def eventFilter(self, receiver, event):
        if (
            # NOTE mouse left click 
            event.type() == QtCore.QEvent.Type.MouseButtonPress
            # NOTE keyboard shift press
            and event.modifiers() & QtCore.Qt.ShiftModifier
        ):
            # NOTE get mouse local position
            pos = self.tree.mapFromGlobal(QtGui.QCursor.pos())
            index = self.tree.indexAt(pos)
            if not self.tree.isExpanded(index):
                # NOTE expand all child
                self.tree.expandRecursively(index)
                return True
        return super(MTreeExpandHook, self).eventFilter(self.tree, event)
    

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    
    model = QtGui.QStandardItemModel()
    
    # NOTE create nested data
    for i in range(3):
        parent = QtGui.QStandardItem('Family {}'.format(i))
        for j in range(3):
            child = QtGui.QStandardItem('Child {}'.format(i*3+j))
            for k in range(3):
                sub_child = QtGui.QStandardItem("Sub Child")
                child.appendRow([sub_child])
                for x in range(2):
                    sub_child_2 = QtGui.QStandardItem("Sub Child 2")
                    sub_child.appendRow([sub_child_2])
            parent.appendRow([child])
        model.appendRow(parent)

        
    treeView = QtWidgets.QTreeView()
    treeView.setHeaderHidden(True)
    MTreeExpandHook(treeView)
    treeView.setModel(model)
    treeView.show()
    
    sys.exit(app.exec_())

example gif

Share:
14,639
bvz
Author by

bvz

Updated on June 05, 2022

Comments

  • bvz
    bvz almost 2 years

    I want to be able to expand or collapse all children of a particular branch in a QTreeView. I am using PyQt4.

    I know that QTreeView's have an expand all children feature that is bound to *, but I need two things: It needs to be bound to a different key combination (shift-space) and I also need to be able to collapse all children as well.

    Here is what I have tried so far: I have a subclass of a QTreeView wherein I am checking for the shift-space key combo. I know that QModelIndex will let me pick a specific child with the "child" function, but that requires knowing the number of children. I am able to get a count of the children by looking at the internalPointer, but that only gives me info for the first level of the hierarchy. If I try to use recursion, I can get a bunch of child counts, but then I am lost as to how to get these converted back into a valid QModelIndex.

    Here is some code:

    def keyPressEvent(self, event):
        """
        Capture key press events to handle:
        - enable/disable
        """
        #shift - space means toggle expanded/collapsed for all children
        if (event.key() == QtCore.Qt.Key_Space and 
            event.modifiers() & QtCore.Qt.ShiftModifier):
            expanded = self.isExpanded(self.selectedIndexes()[0])
            for cellIndex in self.selectedIndexes():
                if cellIndex.column() == 0: #only need to call it once per row
                    #I can get the actual object represented here
                    item = cellIndex.internalPointer()
                    #and I can get the number of children from that
                    numChildren = item.get_child_count()
                    #but now what? How do I convert this number into valid
                    #QModelIndex objects? I know I could use: 
                    #   cellIndex.child(row, 0)
                    #to get the immediate children's QModelIndex's, but how
                    #would I deal with grandchildren, great grandchildren, etc...
                    self.setExpanded(cellIndex, not(expanded))
            return
    

    Here is the beginning of the recursion method I was investigating, but I get stuck when actually trying to set the expanded state because once inside the recursion, I lose "contact" with any valid QModelIndex...

    def toggle_expanded(self, item, expand):
        """
        Toggles the children of item (recursively)
        """
        for row in range(0,item.get_child_count()):
            newItem = item.get_child_at_row(row)
            self.toggle_expanded(newItem, expand)
        #well... I'm stuck here because I'd like to toggle the expanded
        #setting of the "current" item, but I don't know how to convert
        #my pointer to the object represented in the tree view back into
        #a valid QModelIndex
        #self.setExpanded(?????, expand)   #<- What I'd like to run
        print "Setting", item.get_name(), "to", str(expand) #<- simple debug statement that indicates that the concept is valid
    

    Thanks to all for taking the time to look at this!