Search code examples
pythonpyqt5pyside2

QTreeWidget: How to change sizeHint dynamically?


I have a QTreeWidget where the TopLevelIteps are replaced by a custom widget. Said widget have its maximum and minimum Heights animated with a QStateMachine.

QTreeWidget (GIF):
QTreeWidget(gif)

Custom Widget Animation (GIF):
Custom Widget Animation(gif)

The problem is that the rows will not adjust its height to fit the custom widget when it expands:

Row height fixed size (GIF):
Row height fixed size(gif)

Causing overlap between the widget items instead of pushing each other away like this:

The results that I'm after (GIF):
The results that I'm after(gif)

I tried using setSizeHint() on the top level item to but it creates a big empty space between items/widgets:

Using setSizeHint()(GIF):
Using setSizeHint()(gif)

I'm thinking that maybe I have to implement sizeHint() but I'm not really sure what to put there. Or is there a better approach to this problem?

I would really appreciate some hints.

Example code:

# Custom Widget
class ExpandableFrame(QFrame):

    def __init__(self):
        QFrame.__init__(self)

        # Default Properties
        self.setFixedSize(200,50)
        self.setStyleSheet('background-color: #2e4076; border: 2px solid black; border-radius: 10px;')
     
        # Setup expand button
        self.expandToggle = QToolButton()
        self.expandToggle.setText('[]')
        self.expandToggle.setMaximumSize(10,20)
        self.expandToggle.setCursor(Qt.PointingHandCursor)

        # Setup layout
        layout = QHBoxLayout()
        layout.setAlignment(Qt.AlignRight)
        layout.addWidget(self.expandToggle)
        self.setLayout(layout)

        self.expandArea()

    # Animates minimumHeight and maximumHeight
    def expandArea(self):

        heigth = self.height()
        newHeigth = 100

        machine = QStateMachine(self)

        state1 = QState()
        state1.assignProperty(self, b'minimumHeight', heigth)
        state1.assignProperty(self, b'maximumHeight', heigth)

        state2 = QState()
        state2.assignProperty(self, b'minimumHeight', newHeigth)
        state2.assignProperty(self, b'maximumHeight', newHeigth)

        # Create animations
        expandAnim = QPropertyAnimation(self, 'minimumHeight')
        closeAnim = QPropertyAnimation(self, 'maximumHeight')
        expandAnim.setDuration(125)
        closeAnim.setDuration(125)
        expandAnim.setEasingCurve(QEasingCurve.Linear)  

        # Create event transitions
        eventTransition1 = QEventTransition(self.expandToggle, QEvent.MouseButtonPress)
        eventTransition1.setTargetState(state2)
        eventTransition1.addAnimation(expandAnim)
        state1.addTransition(eventTransition1)

        eventTransition2 = QEventTransition(self.expandToggle, QEvent.MouseButtonPress)
        eventTransition2.setTargetState(state1)
        eventTransition2.addAnimation(closeAnim)
        state2.addTransition(eventTransition2)
        
        # Add the states to the machine
        machine.addState(state1)
        machine.addState(state2)
        machine.setInitialState(state1)
        machine.start()
    
# Tree Widget
class CustomTree(QTreeWidget):

    customWidget = []

    def __init__(self, parent=None):
        QTreeWidget.__init__(self, parent)

        # Create top level items
        itemCount = 3
        for element in range(itemCount):
            topLevelItem = QTreeWidgetItem()
            self.addTopLevelItem(topLevelItem)

            # Replace the top level items with the expandable custom widget:
            # Get the Model Index to each item
            modelIndex = self.indexFromItem(topLevelItem, 0)
            
            # Create the ExpandableFrame widgets for all the top level items
            self.customWidget.append(ExpandableFrame())

            # Set the widget to each top level item based on its index
            self.setIndexWidget(modelIndex, self.customWidget[element])
            
            # Create more items and add them as children of the top level items
            for x in range(itemCount):
                child = QTreeWidgetItem()
                child.setText(0,'Child')
                topLevelItem.addChild(child)

This is how the example looks:

Minimal Code Example running:
Minimal Code Example running


Solution

  • One solution could be to emit the sizeHintChanged signal of the item delegate of your view every time the size of any of the widgets is changed. This tells the view that the position of the items should be updated. To achieve this you could override the resizeEvent of ExpandableFrame and emit a custom signal, e.g.

    class ExpandableFrame(QFrame):
        resized = pyqtSignal(QVariant)
    
        def resizeEvent(self, event):
            self.resized.emit(self.height())
            super().resizeEvent(event)
    

    In CustomTree, you can then connect ExpandableFrame.resized to the sizeHintChanged signal of the delegate of the custom tree by overriding setIndexWidget, e.g.

    class CustomTree(QTreeWidget):
        ...
    
        def setIndexWidget(self, index, widget):
            super().setIndexWidget(index, widget)
            if isinstance(widget, ExpandableFrame):
                widget.resized.connect(lambda: self.itemDelegate().sizeHintChanged.emit(index))
    

    Screen capture:

    screen capture

    The disadvantage of this approach is that you would need to update the connections if the the widgets are moved or replaced.