0

I have built a custom widget that is placed inside a custom scene. The custom scene rect resizes when the itemBoundingRect crosses the scene rect. In the beginning, the scene rectangle is set to (0, 0, 2000, 2000). I am able to resize my widgets properly inside this rectangle. The problem arises when I try to move the item against the top (i,e when the item moves in negative y-axis), The item altogether decreases the size on the y-axis when I try to increase it.

Here is the demonstration of the problem:

enter image description here

(Note: the scene resizes when the item is placed at any of the edges by a factor of 500. eg:- after first resize the scene rect would be (-500, -500, 2500, 2500) )

Here is the code:

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QGraphicsItem, QStyle, QGraphicsView


# ---------class size grip use to increase widget size----------#
class SizeGrip(QtWidgets.QSizeGrip):
    def __init__(self, parent):
        super().__init__(parent)
        parent.installEventFilter(self)
        self.setFixedSize(30, 30)
        self.polygon = QtGui.QPolygon([
            QtCore.QPoint(10, 20),
            QtCore.QPoint(20, 10),
            QtCore.QPoint(20, 20),
        ])

    def eventFilter(self, source, event):
        if event.type() == QtCore.QEvent.Resize:
            geo = self.rect()
            geo.moveBottomRight(source.rect().bottomRight())
            self.setGeometry(geo)
        return super().eventFilter(source, event)

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        qp.setPen(QtCore.Qt.white)
        qp.setBrush(QtCore.Qt.gray)
        qp.drawPolygon(self.polygon)


class Container(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.sizeGrip = SizeGrip(self)
        self.startPos = None
        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(6, 6, 6, 30)
        self.setStyleSheet('''
            Container {
                background: lightblue;
                border: 0px;
                border-radius: 4px;
            }
        ''')

    def resizeEvent(self, event):
        super(Container, self).resizeEvent(event)


# ------------------ Creating custom item to place in scene--------------------#
class GraphicsFrame(QtWidgets.QGraphicsWidget):
    def __init__(self):
        super().__init__()

        graphic_layout = QtWidgets.QGraphicsLinearLayout(Qt.Vertical, self)
        self.container = Container()

        proxyWidget = QtWidgets.QGraphicsProxyWidget()
        proxyWidget.setWidget(self.container)

        graphic_layout.addItem(proxyWidget)

        self.pen = QtGui.QPen()
        self.pen.setColor(Qt.red)

        self.container.setMinimumSize(150, 150)
        self.container.setMaximumSize(400, 800)

        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)

        self.container.resizeEvent = lambda _: self.resize()

        self.container.startPos = None

    def addWidget(self, widget):
        self.container.layout().addWidget(widget)

    def paint(self, qp, opt, widget):
        qp.save()

        self.pen.setWidth(3)

        p = QtGui.QPainterPath()
        p.addRoundedRect(self.boundingRect().adjusted(0, 0, -.5, -.5), 4, 4)

        if self.isSelected():
            self.pen.setColor(Qt.yellow)

        qp.setBrush(Qt.transparent)
        qp.setPen(self.pen)
        qp.drawPath(p)

        qp.setClipPath(p)

        opt.state &= ~QStyle.State_Selected
        super().paint(qp, opt, widget)
        qp.restore()

    def resize(self):
        width = self.container.size().width()
        height = self.container.size().height()
        rect = QtCore.QRectF(self.pos().x(), self.pos().y(), width + 22, height + 22)

        self.setGeometry(rect)


# -------------------- Custom view to hold the items -----------------#
class View(QtWidgets.QGraphicsView):

    context_menu_signal = QtCore.pyqtSignal()

    def __init__(self, bg_color=Qt.white):
        super().__init__()
        self.scene = Scene()

        self.setRenderHints(QtGui.QPainter.Antialiasing)

        self.setDragMode(self.RubberBandDrag)
        self._isPanning = False
        self._mousePressed = False
        self.setCacheMode(self.CacheBackground)
        self.setMouseTracking(True)
        self.setScene(self.scene)

        self.scene.selectionChanged.connect(self.selection_changed)
        self._current_selection = []

        texture = QtGui.QImage(30, 30, QtGui.QImage.Format_ARGB32)
        qp = QtGui.QPainter(texture)
        qp.setBrush(bg_color)
        qp.setPen(QtGui.QPen(QtGui.QColor(189, 190, 191), 2))
        qp.drawRect(texture.rect())
        qp.end()

        self.scene.setBackgroundBrush(QtGui.QBrush(texture))
        self.setViewportUpdateMode(self.FullViewportUpdate)  # This will avoid rendering artifacts

        testFrame = GraphicsFrame()
        newFrame = GraphicsFrame()

        testFrame.addWidget(QtWidgets.QLineEdit())
        newFrame.addWidget(QtWidgets.QLabel('Bruh'))

        self.scene.addItem(testFrame)
        self.scene.addItem(newFrame)


    def wheelEvent(self, event):
        # Save the scene pos
        oldPos = self.mapToScene(event.pos())
        if event.modifiers() == Qt.ControlModifier:

            delta = event.angleDelta().y()
            if delta > 0:
                self.on_zoom_in()

            elif delta < 0:
                self.on_zoom_out()

        # Get the new position
        newPos = self.mapToScene(event.pos())

        # Move scene to old position
        delta = newPos - oldPos
        self.translate(delta.x(), delta.y())

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._mousePressed = True

            if self._isPanning:
                self.viewport().setCursor(Qt.ClosedHandCursor)
                self._dragPos = event.pos()

                event.accept()

            else:

                super().mousePressEvent(event)

        elif event.button() == Qt.MidButton:

            self._mousePressed = True
            self._isPanning = True
            self.viewport().setCursor(Qt.ClosedHandCursor)
            self._dragPos = event.pos()
            event.accept()
            super().mousePressEvent(event)

    def mouseMoveEvent(self, event):

        if self._mousePressed and self._isPanning:
            newPos = event.pos()
            diff = newPos - self._dragPos
            self._dragPos = newPos
            self.horizontalScrollBar().setValue(
                self.horizontalScrollBar().value() - diff.x()
            )
            self.verticalScrollBar().setValue(
                self.verticalScrollBar().value() - diff.y()
            )

            event.accept()
        else:
            super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):

        if event.button() == Qt.LeftButton:
            if self._isPanning:
                self.viewport().setCursor(Qt.OpenHandCursor)
            else:
                self._isPanning = False
                self.viewport().unsetCursor()
            self._mousePressed = False

            zoomed = False if self.transform().m11() == 1.0 else True
            self.scene.adjust(zoomed) # adjust the item scene rectangle

        elif event.button() == Qt.MiddleButton:
            self._isPanning = False
            self.viewport().unsetCursor()
            self._mousePressed = False

        super().mouseReleaseEvent(event)

    def select_items(self, items, on):
        pen = QtGui.QPen(
            QtGui.QColor(245, 228, 0) if on else Qt.white,
            0.5,
            Qt.SolidLine,
            Qt.RoundCap,
            Qt.RoundJoin,
        )

        for item in items:
            item.pen = pen

    def selection_changed(self):
        try:
            self.select_items(self._current_selection, False)
            self._current_selection = self.scene.selectedItems()
            self.select_items(self._current_selection, True)
        except RuntimeError:
            pass

    def on_zoom_in(self):
        if self.transform().m11() < 2.25:
            self.scale(1.5, 1.5)

    def on_zoom_out(self):
        if self.transform().m11() > 0.7:
            self.scale(1.0 / 1.5, 1.0 / 1.5)

    def resizeEvent(self, event):
        super().resizeEvent(event)


# ------------Custom scene which resizes the scene rect on when the item boundary hits the scene rect---------#
class Scene(QtWidgets.QGraphicsScene):

    def __init__(self):
        super(Scene, self).__init__()
        self.setSceneRect(0, 0, 2000, 2000)
        self.sceneRect().adjust(-20, -20, 20, 20)
        self.old_rect = self.itemsBoundingRect()

    def adjust(self, zoomed):
        w = self.sceneRect().width()
        h = self.sceneRect().height()
        x = self.sceneRect().x()
        y = self.sceneRect().y()
        adjust_factor = 500
        adjust_factor2 = 200

        smaller = self.is_smaller()
        self.old_rect = self.itemsBoundingRect()

        if not self.sceneRect().contains(self.old_rect):
            self.setSceneRect(-adjust_factor + x, -adjust_factor + y, adjust_factor + w, adjust_factor + h)
            print(f'sceneRect: {self.sceneRect()}')

        if not zoomed and smaller:
            print('yes')
            self.setSceneRect(adjust_factor2 + x, adjust_factor2 + y, abs(adjust_factor2 - w),
                              abs(adjust_factor2 - h))

    def is_smaller(self):

        x = self.old_rect.x()
        y = self.old_rect.y()

        h = self.old_rect.height()
        w = self.old_rect.width()

        if ((x <= self.itemsBoundingRect().x()) and (y <= self.itemsBoundingRect().y())
                and (h > self.itemsBoundingRect().height()) and (w > self.itemsBoundingRect().width())):
            return True

        return False

# -----------main---------#
import sys
app = QtWidgets.QApplication(sys.argv)
w = View()
w.show()
sys.exit(app.exec_())

I know the code is lengthy but any help is appreciated.

JacksonPro
  • 3,135
  • 2
  • 6
  • 29
  • I cannot reproduce the exact issue, but I have another one related to it, and that is due to the fact that you added the Container widget to the QGraphicsWidget that causes a partial recursion problem whenever you try to resize it using the resize grip (I already warned you about doing so): when you use `setGeometry()` on the QGraphicsWidget after the resizeEvent, you'll also resize the container (since the Container is in its layout), which is clearly wrong. Since you're clearly *not* using the layout of the graphics widget, there's no need for it, just add the QGraphicsProxy to the scene. – musicamante Dec 25 '20 at 12:26
  • If you want to paint a border around it, use a basic QGraphicsItem (implementing the boundingRect and paint event) and add the QGraphicsProxyWidget with that item as parent. – musicamante Dec 25 '20 at 12:26
  • @musicamante I did use QgraphicsProxy and added to the scene when I was starting out. The problem however arose when trying to select multiple items and moving them in the scene. It looked like you could only select one item at a time. So I decided to place the container inside the QGaphicsWidget. To replicate the problem just move one of the widgets against the top and try to increase the size using the triangle at the bottom of the widget. As you keep moving the widget to the top, each time you try to increase the widget height it keeps decreasing. – JacksonPro Dec 25 '20 at 13:42
  • @musicamante This problem however doesn't rise when the widget is moved to the left of the screen. Any further suggestion is appreciated. – JacksonPro Dec 25 '20 at 13:42
  • As said, I cannot reproduce the *exact* issue, as the problem arises even without moving the widget outside the scene rectangle, I don't know the reason for the difference, it might be due to the platform or the Qt version, but that's not the point, as the issue is the wrong handling of resizing. – musicamante Dec 25 '20 at 15:33

1 Answers1

0

The problem is a (partial) recursion caused by a wrong implementation of the object structure.
When the widget is resized using the size grip, it receives a resizeEvent that in your code is overridden by the GraphicsFrame.resize(): if you use setGeometry(), it will cause the graphics layout to resize its children, which in turn will call again a resize on the proxy widget. It's just a partial recursion because layout adjustments often require more than an event loop "cycle" (and with proxy widgets it might be even more).

The solution obviously is to avoid this recursion, which could be done by simply adding the proxy widget to the scene. Since the multiple item selection is required, this cannot be possible, as QGraphicsProxyWidgets are also panels (see the ItemIsPanel flag): only one panel graphics item can be active at once, so the only actual solution is to create a QGraphicsItem as a parent for the proxy. From that point, the geometry of the parent is based on the proxy, and the painting can be easily implemented.

class GraphicsFrame(QtWidgets.QGraphicsItem):
    def __init__(self):
        super().__init__()
        self.container = Container()
        self.container.setMinimumSize(150, 150)
        self.container.setMaximumSize(400, 800)
        self.proxy = QtWidgets.QGraphicsProxyWidget(self)
        self.proxy.setWidget(self.container)
        self.setFlags(self.ItemIsSelectable | self.ItemIsMovable)

    def addWidget(self, widget):
        self.container.layout().addWidget(widget)

    def boundingRect(self):
        # use the proxy for the bounding rect, adding the preferred margin
        return self.proxy.boundingRect().adjusted(-11, -11, 11, 11)

    def paint(self, qp, opt, widget):
        qp.save()
        qp.setPen(QtGui.QPen(Qt.yellow if self.isSelected() else Qt.red, 3))
        qp.drawRoundedRect(self.boundingRect().adjusted(0, 0, -.5, -.5), 4, 4)
        qp.restore()
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thx for the answer, But this doesn't help. When using your code I am having a problem with rubberband selection and this time it has similar behavior when moved to the left side too. I am on windows with qt version: 5.15.1 – JacksonPro Dec 25 '20 at 16:36
  • *what* problem with rubberband selection? I see the issue with the blocked resizing when moving *over* the left edge only in certain conditions. I believe that it might be related to the way QSizeGrip allows resizing, which takes into account the available area for the viewport, which might have some issues due to the contentsRect returned by the graphics view (the negative position of the sceneRect might be related). At this point, I'd suggest to avoid QSizeGrip at all and implement the resizing on your own. – musicamante Dec 25 '20 at 16:51
  • As per your advice, I have implemented the custom resizing. But I have encountered a problem here is the link to that question: https://stackoverflow.com/questions/65493612/readjust-the-custom-qgraphicswidgets-size-on-inserting-widget – JacksonPro Dec 29 '20 at 14:21