What causes a nested QRubberband to move unexpectedly?

44 Views Asked by At

I am just curious if I can make a nested QRubberband. (I or someone might find a use to it). I managed to edit the code from this answer to make a nested QRubberband. It is all fine and working until I move the QRubberband inside its parent QRubberband. I was very confused as it moves wildly when I'm dragging it.

Here is a short gif of the event

This is the sample code:

import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class ResizableRubberBand(QRubberBand):
    moving = False
    def __init__(self, parent=None):
        super(ResizableRubberBand, self).__init__(QRubberBand.Rectangle, parent)
        self.setAttribute(Qt.WA_TransparentForMouseEvents, False)

        self.draggable = True
        self.dragging = False
        self.is_dragging = False
        self.dragging_threshold = 5
        self.mousePressPos = None
        self.borderRadius = 5

        self.setWindowFlags(Qt.SubWindow)
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(
            QSizeGrip(self), 0,
            Qt.AlignLeft | Qt.AlignTop)
        layout.addWidget(
            QSizeGrip(self), 0,
            Qt.AlignRight | Qt.AlignBottom)
        self.show()

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

    def paintEvent(self, event):
        super().paintEvent(event)
        qp = QPainter(self)
        qp.setRenderHint(QPainter.Antialiasing)
        qp.translate(.5, .5)
        qp.drawRoundedRect(self.rect().adjusted(0, 0, -1, -1), 
            self.borderRadius, self.borderRadius)

    def mousePressEvent(self, event):
        if self.draggable and event.button() == Qt.RightButton:
            self.mousePressPos = event.pos()

        if event.button() == Qt.LeftButton:
            self.first_mouse_location = (event.x(), event.y())
            self.band = ResizableRubberBand(self)
            self.band.setGeometry(event.x(), event.y(), 0, 0)
        
        super(ResizableRubberBand, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self.draggable and event.buttons() & Qt.RightButton:
            diff = event.pos() - self.mousePressPos
            if not self.dragging:
                if diff.manhattanLength() > self.dragging_threshold:
                    self.dragging = True
            if self.dragging:
                geo = self.geometry()
                parentRect = self.parent().rect()
                geo.translate(diff)
                if not parentRect.contains(geo):
                    if geo.right() > parentRect.right():
                        geo.moveRight(parentRect.right())
                    elif geo.x() < parentRect.x():
                        geo.moveLeft(parentRect.x())
                    if geo.bottom() > parentRect.bottom():
                        geo.moveBottom(parentRect.bottom())
                    elif geo.y() < parentRect.y():
                        geo.moveTop(parentRect.y())
                self.move(geo.topLeft())

        if event.buttons() & Qt.LeftButton:
            first_mouse_location_x = self.first_mouse_location[0]
            first_mouse_location_y = self.first_mouse_location[1]
            new_x, new_y = event.x(), event.y()
            difference_x = new_x - first_mouse_location_x
            difference_y = new_y - first_mouse_location_y
            self.band.resize(difference_x, difference_y)
        super(ResizableRubberBand, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if self.mousePressPos is not None:
            if event.button() == Qt.RightButton and self.dragging:
                event.ignore()
                self.dragging = False
            self.mousePressPos = None
        super(ResizableRubberBand, self).mouseReleaseEvent(event)

class mQLabel(QLabel):
    def __init__(self, parent=None):
        QLabel.__init__(self, parent)
        self.setContentsMargins(0,0,0,0)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.first_mouse_location = (event.x(), event.y())
            self.band = ResizableRubberBand(self)
            self.band.setGeometry(event.x(), event.y(), 0, 0)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            first_mouse_location_x = self.first_mouse_location[0]
            first_mouse_location_y = self.first_mouse_location[1]
            new_x, new_y = event.x(), event.y()
            difference_x = new_x - first_mouse_location_x
            difference_y = new_y - first_mouse_location_y
            self.band.resize(difference_x, difference_y)

class App(QWidget):

    def __init__(self):
        super().__init__()

        ## Set main window attributes
        self.setFixedSize(1000,600)

        # Add Label
        self.label = mQLabel()
        self.label.setStyleSheet("border: 1px solid black;")
        self.label_layout = QHBoxLayout(self)
        self.label_layout.addWidget(self.label)

        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())

I'm trying to figure it out for 2 hours but I can't really seem to figure out what causes the unnecessary movement. My best guess is it is coming from the mouseMoveEvent but I'm not quite sure if it is from the parent QRubberband or from the QRubberband inside. I hope someone can figure out what is happening here.

1

There are 1 best solutions below

1
On BEST ANSWER

The problem is the call to the base implementation of mouse events, which by default gets propagated to the parent for widgets that do not directly implement them, including QRubberBand, which normally doesn't intercept mouse events at all (which we restored by disabling the relative window attribute).

Since the parent itself is a rubber band, it will be moved itself too, making the movement recursive for the child, since it receives a mouse move exactly due to the fact that its been moved: remember that if a widget is moved and the mouse doesn't directly follow the same movement, it will potentially receive a mouse move event relative to its new position.

You can either return before calling it when you handle it, or not call it at all, depending on your needs.

The important thing is that it's consistent (especially for press and move), otherwise a widget could receive a mouse move without receiving the mouse press, which will crash as the variables have not been set yet.

Be aware that if you're in the process of making a more advanced editor for clipping/selections, drawing, etc, you should really consider using the Graphics View Framework: while much more complex and with a more steep learning curve, you'll soon find out that continuing development on basic QWidgets becomes gradually much more convoluted and difficult, to a point where it is really hard to fix things, especially if you're going to deal with image scaling or even basic scroll and zoom.
QWidget and QLabel implementations are not intended for image management, not even simple editing, and custom placed/painted/nested widgets are often difficult to deal with. Consider that doing a similar selection tool would have been much more easy in a graphics scene: for instance, the moving implementation would be almost completely unnecessary, as it's enough to set a simple flag to make an item movable.