Search code examples
qtpyqtqtreeview

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


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!


Solution

  • 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)