2

I have made a custom widget similar to QPushbutton or label. I would like to let the user resize the widget when the mouse is over the edge of the widget. How can I do this?

(Note: I am not looking for Splitter window)

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
JacksonPro
  • 3,135
  • 2
  • 6
  • 29
  • Note that widgets are normally used within a "container" (a window or a parent widget) which use a [layout manager](https://doc.qt.io/qt-5/layout.html), as manually setting geometries on children (programmatically or by user interaction) is usually highly discouraged. Nonetheless, there are situations for which resizing *might* be possible on child widgets, but that depends on the situation, and I'd suggest you to try to clarify what you want, possibly with an example scenario of what you're trying to achieve. – musicamante Nov 11 '20 at 14:48
  • I only want to resize the widget. For example to resize an image we simply pull/drag the edges or corner of the image to resize it in many software. I want a similar thing where the user is given the freedom to resize the widget. – JacksonPro Nov 11 '20 at 15:45

1 Answers1

4

An image editing software, you have a dedicated "space" for the image, and the user is free to do anything she/he wants within the boundaries of that space. When a widget is placed within a layout-managed container (as it normally should) that can represent multiple issues. Not only you've to implement the whole mouse interaction to resize the widget, but you also need to notify the possible parent widget(s) about the resizing.

That said, what you're trying to achieve can be done, with some caveats.

The following is a very basic implementation of a standard QWidget that is able to resize itself, while notifying its parent widget(s) about the size hint modifications. Note that this is not complete, and its behavior doesn't correctly respond to mouse movements whenever they happen on the top or left edges of the widget. Moreover, while it (could) correctly resize the parent widget(s) while increasing its size, the resize doesn't happen when shrinking. This can theoretically be achieved by setting a minimumSize() and manually calling adjustSize() but, in order to correctly provide all the possible features required by a similar concept, you'll need to do the whole implementation by yourself.

from PyQt5 import QtCore, QtGui, QtWidgets

Left, Right = 1, 2
Top, Bottom = 4, 8
TopLeft = Top|Left
TopRight = Top|Right
BottomRight = Bottom|Right
BottomLeft = Bottom|Left

class ResizableLabel(QtWidgets.QWidget):
    resizeMargin = 4
    # note that the Left, Top, Right, Bottom constants cannot be used as class
    # attributes if you want to use list comprehension for better performance,
    # and that's due to the variable scope behavior on Python 3
    sections = [x|y for x in (Left, Right) for y in (Top, Bottom)]
    cursors = {
        Left: QtCore.Qt.SizeHorCursor, 
        Top|Left: QtCore.Qt.SizeFDiagCursor, 
        Top: QtCore.Qt.SizeVerCursor, 
        Top|Right: QtCore.Qt.SizeBDiagCursor, 
        Right: QtCore.Qt.SizeHorCursor, 
        Bottom|Right: QtCore.Qt.SizeFDiagCursor, 
        Bottom: QtCore.Qt.SizeVerCursor, 
        Bottom|Left: QtCore.Qt.SizeBDiagCursor, 
    }
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.startPos = self.section = None
        self.rects = {section:QtCore.QRect() for section in self.sections}

        # mandatory for cursor updates
        self.setMouseTracking(True)

        # just for demonstration purposes
        background = QtGui.QPixmap(3, 3)
        background.fill(QtCore.Qt.transparent)
        qp = QtGui.QPainter(background)
        pen = QtGui.QPen(QtCore.Qt.darkGray, .5)
        qp.setPen(pen)
        qp.drawLine(0, 2, 2, 0)
        qp.end()
        self.background = QtGui.QBrush(background)

    def updateCursor(self, pos):
        for section, rect in self.rects.items():
            if pos in rect:
                self.setCursor(self.cursors[section])
                self.section = section
                return section
        self.unsetCursor()

    def adjustSize(self):
        del self._sizeHint
        super().adjustSize()

    def minimumSizeHint(self):
        try:
            return self._sizeHint
        except:
            return super().minimumSizeHint()

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            if self.updateCursor(event.pos()):
                self.startPos = event.pos()
                return
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self.startPos is not None:
            delta = event.pos() - self.startPos
            if self.section & Left:
                delta.setX(-delta.x())
            elif not self.section & (Left|Right):
                delta.setX(0)
            if self.section & Top:
                delta.setY(-delta.y())
            elif not self.section & (Top|Bottom):
                delta.setY(0)
            newSize = QtCore.QSize(self.width() + delta.x(), self.height() + delta.y())
            self._sizeHint = newSize
            self.startPos = event.pos()
            self.updateGeometry()
        elif not event.buttons():
            self.updateCursor(event.pos())
        super().mouseMoveEvent(event)
        self.update()

    def mouseReleaseEvent(self, event):
        super().mouseReleaseEvent(event)
        self.updateCursor(event.pos())
        self.startPos = self.section = None
        self.setMinimumSize(0, 0)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        outRect = self.rect()
        inRect = self.rect().adjusted(self.resizeMargin, self.resizeMargin, -self.resizeMargin, -self.resizeMargin)
        self.rects[Left] = QtCore.QRect(outRect.left(), inRect.top(), self.resizeMargin, inRect.height())
        self.rects[TopLeft] = QtCore.QRect(outRect.topLeft(), inRect.topLeft())
        self.rects[Top] = QtCore.QRect(inRect.left(), outRect.top(), inRect.width(), self.resizeMargin)
        self.rects[TopRight] = QtCore.QRect(inRect.right(), outRect.top(), self.resizeMargin, self.resizeMargin)
        self.rects[Right] = QtCore.QRect(inRect.right(), self.resizeMargin, self.resizeMargin, inRect.height())
        self.rects[BottomRight] = QtCore.QRect(inRect.bottomRight(), outRect.bottomRight())
        self.rects[Bottom] = QtCore.QRect(inRect.left(), inRect.bottom(), inRect.width(), self.resizeMargin)
        self.rects[BottomLeft] = QtCore.QRect(outRect.bottomLeft(), inRect.bottomLeft()).normalized()

    # ---- optional, mostly for demonstration purposes ----

    def paintEvent(self, event):
        super().paintEvent(event)
        qp = QtGui.QPainter(self)
        if self.underMouse() and self.section:
            qp.save()
            qp.setPen(QtCore.Qt.lightGray)
            qp.setBrush(self.background)
            qp.drawRect(self.rect().adjusted(0, 0, -1, -1))
            qp.restore()
        qp.drawText(self.rect(), QtCore.Qt.AlignCenter, '{}x{}'.format(self.width(), self.height()))

    def enterEvent(self, event):
        self.update()

    def leaveEvent(self, event):
        self.update()


class Test(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QGridLayout(self)

        for row in range(3):
            for column in range(3):
                if (row, column) == (1, 1):
                    continue
                layout.addWidget(QtWidgets.QPushButton(), row, column)

        label = ResizableLabel()
        layout.addWidget(label, 1, 1)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = Test()
    w.show()
    sys.exit(app.exec_())
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thx for the code. If u don't mind can u tell me whether this **TopLeft = Top|Left** is bitwise or operator and why it is useful – JacksonPro Nov 12 '20 at 01:25
  • Yes, it's bitwise, and it's just for convenience, obviously I could've written `TopLeft = 5`, but doing it like that makes it clear that I'm using bit based values. – musicamante Nov 12 '20 at 10:42
  • **Left, Right = 1, 2 Top, Bottom = 4, 8 TopLeft = Top|Left TopRight = Top|Right BottomRight = Bottom|Right BottomLeft = Bottom|Left **. Sorry for bothering you. Can I also know how u chose these values? was it randomly chosen? – JacksonPro Nov 12 '20 at 13:50
  • @JacksonPro they are powers of 2 that represent simple bit states: 1 = `0b1`, 2 = `0b10`, 4 = `0b100`, 8 = `0b1000`. This allows to use bitwise operations to set and know which corners are actually used: `TopLeft = Top|Left = 5 = "0b101"`, so since 5 (being `0b101`) is equal to `0b100` **AND** `0b001`, it indicates that we're on the top left corner. – musicamante Nov 12 '20 at 14:00