Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

QGraphicsLayout not properly resizing to change of content



  • Hi,
    I am trying to create a QGraphicsWidget with a QGraphicsLinearLayout (for a QGraphicsItem) containing more layouts and widgets while adding and removing content as a result of user interaction. By left-clicking, I add content. By right-clicking, I remove content. The behavior strongly differs from what I would expect from QLayouts. There are two problems:

    • When adding content, the layout (self.inputs_layout see below) expands like expected. But it does not shrink when removing content. I would assume this has to be done using QSizePolicies but also no luck so far.
    • The stretches I add between two items to the layout somehow stay there also when the content around them is removed which creates an increasing blank area at the top when adding and removing content multiple times. And I cannot find a way to access and remove them. (is this a bug?)

    The following runnable code shows how I manage the layouts. Only the NodeInstance class should be of the essence for this issue:

    import sys
    
    from PySide2.QtWidgets import QMainWindow, QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem, \
        QGraphicsLinearLayout, QGraphicsLayoutItem, QGraphicsWidget
    from PySide2.QtGui import QPainter, QColor, QPen, QFont, QFontMetrics
    from PySide2.QtCore import QRectF, Qt, QPointF, QSizeF
    
    
    class NodeInstance(QGraphicsItem):
        def __init__(self):
            super(NodeInstance, self).__init__()
    
            # main widget
            self.body_widget = QGraphicsWidget(self)
            self.body_layout = QGraphicsLinearLayout(Qt.Horizontal)
            self.body_layout.setSpacing(30)
    
            #       inputs
            self.inputs_layout = self.get_ports_layout(num_ports=5)  # returns QGraphicsLinearLayout
            self.body_layout.addItem(self.inputs_layout)
            self.body_layout.setAlignment(self.inputs_layout, Qt.AlignLeft | Qt.AlignVCenter)
    
            self.body_layout.addStretch()  # add space in between
    
            #       outputs
            self.outputs_layout = self.get_ports_layout(num_ports=2)  # returns QGraphicsLinearLayout
            self.body_layout.addItem(self.outputs_layout)
            self.body_layout.setAlignment(self.outputs_layout, Qt.AlignRight | Qt.AlignVCenter)
    
            self.body_widget.setLayout(self.body_layout)
    
        def get_ports_layout(self, num_ports):
            """Creating vertical layout with PortInstances and stretches in between them."""
            layout = QGraphicsLinearLayout(Qt.Vertical)
            layout.setSpacing(3)
            for i in range(num_ports):
                inp = PortInstance()  # a horizontal QGraphicsLinearLayout containing two QGraphicsWidgets
                layout.addItem(inp)
                if i != num_ports-1:
                    layout.addStretch()
            return layout
    
        def boundingRect(self):
            return self.body_layout.geometry().toRect()
    
        def get_header_rect(self):
            header_height = self.get_header_height()
            header_width = self.get_width()
            height = self.get_height()
    
            return QRectF(-header_width/2, -height/2, header_width, header_height)
    
        def paint(self, painter, option, widget=None):
            painter.setBrush(QColor('yellow'))
            painter.setPen(QPen(QColor(0, 0, 0), 3))
            painter.drawRoundedRect(self.boundingRect(), 20, 20)
    
        def mousePressEvent(self, event):
            if event.button() == Qt.LeftButton:
                # add new input
                self.inputs_layout.addStretch()
                self.inputs_layout.addItem(PortInstance())
    
                self.body_layout.invalidate()
            elif event.button() == Qt.RightButton:
                inp: PortInstance = self.inputs_layout.itemAt(0)
    
                # remove input's contents from scene (for some reason, it doesn't happen automatically, even when deleting)
                self.scene().removeItem(inp.pin)
                self.scene().removeItem(inp.label)
    
                self.inputs_layout.removeAt(0)
    
                self.body_layout.invalidate()
    
    
    class PortInstance(QGraphicsLinearLayout):
        def __init__(self):
            super(PortInstance, self).__init__(Qt.Horizontal)
    
            self.pin = Pin()  # QGraphicsWidget
            self.label = Label()  # QGraphicsWidget
    
            self.addItem(self.pin)
            self.addItem(self.label)
    
            self.setAlignment(self.pin, Qt.AlignCenter)
            self.setAlignment(self.label, Qt.AlignCenter)
            self.setSpacing(10)
    
    
    class Pin(QGraphicsWidget):
        def __init__(self):
            super(Pin, self).__init__()
            self.setGraphicsItem(self)
    
            self.width = 30
            self.height = 30
    
        def boundingRect(self):
            return QRectF(QPointF(0, 0), self.geometry().size())
    
        def setGeometry(self, rect):
            self.prepareGeometryChange()
            QGraphicsLayoutItem.setGeometry(self, rect)
            self.setPos(rect.topLeft())
    
        def sizeHint(self, which, constraint=...):
            return QSizeF(self.width, self.height)
    
        def paint(self, painter, option, widget=None):
            painter.setBrush(QColor(0, 0, 255, 150))
            painter.setPen(Qt.NoPen)
            painter.drawEllipse(self.boundingRect())
    
    
    class Label(QGraphicsWidget):
        def __init__(self):
            super(Label, self).__init__()
            self.setGraphicsItem(self)
    
            self.text = 'label'
            self.font = QFont('Arial', 15)
            f_m = QFontMetrics(self.font)
            self.width = f_m.width(self.text)
            self.height = f_m.height()
    
        def boundingRect(self):
            return QRectF(QPointF(0, 0), self.geometry().size())
    
        def setGeometry(self, rect):
            self.prepareGeometryChange()
            QGraphicsLayoutItem.setGeometry(self, rect)
            self.setPos(rect.topLeft())
    
        def sizeHint(self, which, constraint=...):
            return QSizeF(self.width, self.height)
    
        def paint(self, painter, option, widget=None):
            painter.setFont(self.font)
            painter.drawText(self.boundingRect(), self.text)
    
    
    class MyView(QGraphicsView):
        def __init__(self):
            super(MyView, self).__init__()
            scene = QGraphicsScene(self)
            scene.setSceneRect(0, 0, self.width(), self.height())
            self.setScene(scene)
    
            ni = NodeInstance()
            self.scene().addItem(ni)
            ni.setPos(150, 80)
    
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super(MainWindow, self).__init__()
            self.setCentralWidget(MyView())
    
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
    
        mw = MainWindow()
        mw.show()
    
        sys.exit(app.exec_())
    

    I am heavily stuck and hope someone with a little more experience with these QGraphicsWidgets can help. I could not find any hints regarding these issues in the doc.
    Thanks for answers!



  • @Niagarer
    I hope I'm right about this --- I stand to be corrected.

    I have done Qt QGraphicsItem stuff, though without using any layouts. However, I'm hoping the layouts is not the issue.

    When you add QGraphicItems on a QGraphicsScene/View, the size of the scene/view grows to accommodate wherever the items are placed. However, when you delete items the scene/view does not automatically shrink down to the minimum to accommodate whatever is left on it, it just stays at the larger size.

    Personally I did not bother, but if I wanted it to shrink back down I would have used the result from https://doc.qt.io/qt-5/qgraphicsscene.html#itemsBoundingRect

    Calculates and returns the bounding rect of all items on the scene. This function works by iterating over all items, and because of this, it can be slow for large scenes.

    to resize the scene/view back down to whatever minimum is now required.

    Does that address your issue?



  • @JonB edit: sorry, I don't mean the layout, the view is placed in! I mean the layout of the QGraphicsWidget that's placed inside the scene.

    It is actually just a strange behavior of the geometry updates of the layouts/widgets used (for the NodeInstance.body_widget), the NodeInstance.body_widget does not shrink when removing content, although it does expand when adding stuff. And the second issue must be due to some very weird stretch behavior of QGraphicsLinearLayouts...



  • The only workaround for the second issue with the stretches so far is rebuilding the affected layout on change. That's possible but ridiculous.
    And it still doesn't solve the first issue, the main layout does not shrink, even if it could and the SizePolicy is set to Minimum like that:

    ...
            # main widget
            self.body_widget = QGraphicsWidget(self)
            self.body_layout = QGraphicsLinearLayout(Qt.Horizontal)
            self.body_layout.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
            self.body_layout.setSpacing(30)
    ...
            self.body_layout.invalidate()
            print('body min height:', self.body_layout.minimumHeight())
            print('body actual height:', self.body_layout.geometry().height())
            # prints for esample
            # body min height: 84.0
            # body actual height: 192.0
    

    how!?


Log in to reply