pyside6/pyqt6/pyside/pyqt 构建QGraphicsItem拖拽设置形状的矩形框

pyside6/pyqt6实现矩形框尺寸调整

pyside6/pyqt6/pyside/pyqt 鼠标调整矩形框尺寸大小

目录

1.需求

2.实际效果

3.实际代码


1.需求

        [1] 采用pyside6(pyqt同样)的QGraphicsView、QGraphicsScene、QGraphicsItem实现ROI矩形框的绘制,类似labelimg等深度学习CV标注工具的矩形框标注功能;

        [2] 由于pyside6中QGraphicsRectItem只支持设置几个属性即可简单实现矩形框选中、平移、删除,但是不直接支持拖拽设置尺寸,因此继承这个类,重写部分函数,以实现句柄拖拽矩形框尺寸形状。

2.实际效果

3.实际代码

        代码摘自https://stackoverflow.com/questions/34429632/resize-a-qgraphicsitem-with-the-mouse

import sys

from PySide6.QtCore import Qt, QRectF, QPointF
from PySide6.QtGui import QBrush, QPainterPath, QPainter, QColor, QPen, QPixmap
from PySide6.QtWidgets import QGraphicsRectItem, QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem


class GraphicsRectItem(QGraphicsRectItem):

    handleTopLeft = 1
    handleTopMiddle = 2
    handleTopRight = 3
    handleMiddleLeft = 4
    handleMiddleRight = 5
    handleBottomLeft = 6
    handleBottomMiddle = 7
    handleBottomRight = 8

    handleSize = +8.0
    handleSpace = -4.0

    handleCursors = {
        handleTopLeft: Qt.SizeFDiagCursor,
        handleTopMiddle: Qt.SizeVerCursor,
        handleTopRight: Qt.SizeBDiagCursor,
        handleMiddleLeft: Qt.SizeHorCursor,
        handleMiddleRight: Qt.SizeHorCursor,
        handleBottomLeft: Qt.SizeBDiagCursor,
        handleBottomMiddle: Qt.SizeVerCursor,
        handleBottomRight: Qt.SizeFDiagCursor,
    }

    def __init__(self, *args):
        """
        Initialize the shape.
        """
        super().__init__(*args)
        self.handles = {}
        self.handleSelected = None
        self.mousePressPos = None
        self.mousePressRect = None
        self.setAcceptHoverEvents(True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
        self.setFlag(QGraphicsItem.ItemIsFocusable, True)
        self.updateHandlesPos()

    def handleAt(self, point):
        """
        Returns the resize handle below the given point.
        """
        for k, v, in self.handles.items():
            if v.contains(point):
                return k
        return None

    def hoverMoveEvent(self, moveEvent):
        """
        Executed when the mouse moves over the shape (NOT PRESSED).
        """
        if self.isSelected():
            handle = self.handleAt(moveEvent.pos())
            cursor = Qt.ArrowCursor if handle is None else self.handleCursors[handle]
            self.setCursor(cursor)
        super().hoverMoveEvent(moveEvent)

    def hoverLeaveEvent(self, moveEvent):
        """
        Executed when the mouse leaves the shape (NOT PRESSED).
        """
        self.setCursor(Qt.ArrowCursor)
        super().hoverLeaveEvent(moveEvent)

    def mousePressEvent(self, mouseEvent):
        """
        Executed when the mouse is pressed on the item.
        """
        self.handleSelected = self.handleAt(mouseEvent.pos())
        if self.handleSelected:
            self.mousePressPos = mouseEvent.pos()
            self.mousePressRect = self.boundingRect()
        super().mousePressEvent(mouseEvent)

    def mouseMoveEvent(self, mouseEvent):
        """
        Executed when the mouse is being moved over the item while being pressed.
        """
        if self.handleSelected is not None:
            self.interactiveResize(mouseEvent.pos())
        else:
            super().mouseMoveEvent(mouseEvent)

    def mouseReleaseEvent(self, mouseEvent):
        """
        Executed when the mouse is released from the item.
        """
        super().mouseReleaseEvent(mouseEvent)
        self.handleSelected = None
        self.mousePressPos = None
        self.mousePressRect = None
        self.update()

    def boundingRect(self):
        """
        Returns the bounding rect of the shape (including the resize handles).
        """
        o = self.handleSize + self.handleSpace
        return self.rect().adjusted(-o, -o, o, o)

    def updateHandlesPos(self):
        """
        Update current resize handles according to the shape size and position.
        """
        s = self.handleSize
        b = self.boundingRect()
        self.handles[self.handleTopLeft] = QRectF(b.left(), b.top(), s, s)
        self.handles[self.handleTopMiddle] = QRectF(b.center().x() - s / 2, b.top(), s, s)
        self.handles[self.handleTopRight] = QRectF(b.right() - s, b.top(), s, s)
        self.handles[self.handleMiddleLeft] = QRectF(b.left(), b.center().y() - s / 2, s, s)
        self.handles[self.handleMiddleRight] = QRectF(b.right() - s, b.center().y() - s / 2, s, s)
        self.handles[self.handleBottomLeft] = QRectF(b.left(), b.bottom() - s, s, s)
        self.handles[self.handleBottomMiddle] = QRectF(b.center().x() - s / 2, b.bottom() - s, s, s)
        self.handles[self.handleBottomRight] = QRectF(b.right() - s, b.bottom() - s, s, s)

    def interactiveResize(self, mousePos):
        """
        Perform shape interactive resize.
        """
        offset = self.handleSize + self.handleSpace
        boundingRect = self.boundingRect()
        rect = self.rect()
        diff = QPointF(0, 0)

        self.prepareGeometryChange()

        if self.handleSelected == self.handleTopLeft:

            fromX = self.mousePressRect.left()
            fromY = self.mousePressRect.top()
            toX = fromX + mousePos.x() - self.mousePressPos.x()
            toY = fromY + mousePos.y() - self.mousePressPos.y()
            diff.setX(toX - fromX)
            diff.setY(toY - fromY)
            boundingRect.setLeft(toX)
            boundingRect.setTop(toY)
            rect.setLeft(boundingRect.left() + offset)
            rect.setTop(boundingRect.top() + offset)
            self.setRect(rect)

        elif self.handleSelected == self.handleTopMiddle:

            fromY = self.mousePressRect.top()
            toY = fromY + mousePos.y() - self.mousePressPos.y()
            diff.setY(toY - fromY)
            boundingRect.setTop(toY)
            rect.setTop(boundingRect.top() + offset)
            self.setRect(rect)

        elif self.handleSelected == self.handleTopRight:

            fromX = self.mousePressRect.right()
            fromY = self.mousePressRect.top()
            toX = fromX + mousePos.x() - self.mousePressPos.x()
            toY = fromY + mousePos.y() - self.mousePressPos.y()
            diff.setX(toX - fromX)
            diff.setY(toY - fromY)
            boundingRect.setRight(toX)
            boundingRect.setTop(toY)
            rect.setRight(boundingRect.right() - offset)
            rect.setTop(boundingRect.top() + offset)
            self.setRect(rect)

        elif self.handleSelected == self.handleMiddleLeft:

            fromX = self.mousePressRect.left()
            toX = fromX + mousePos.x() - self.mousePressPos.x()
            diff.setX(toX - fromX)
            boundingRect.setLeft(toX)
            rect.setLeft(boundingRect.left() + offset)
            self.setRect(rect)

        elif self.handleSelected == self.handleMiddleRight:
            print("MR")
            fromX = self.mousePressRect.right()
            toX = fromX + mousePos.x() - self.mousePressPos.x()
            diff.setX(toX - fromX)
            boundingRect.setRight(toX)
            rect.setRight(boundingRect.right() - offset)
            self.setRect(rect)

        elif self.handleSelected == self.handleBottomLeft:

            fromX = self.mousePressRect.left()
            fromY = self.mousePressRect.bottom()
            toX = fromX + mousePos.x() - self.mousePressPos.x()
            toY = fromY + mousePos.y() - self.mousePressPos.y()
            diff.setX(toX - fromX)
            diff.setY(toY - fromY)
            boundingRect.setLeft(toX)
            boundingRect.setBottom(toY)
            rect.setLeft(boundingRect.left() + offset)
            rect.setBottom(boundingRect.bottom() - offset)
            self.setRect(rect)

        elif self.handleSelected == self.handleBottomMiddle:

            fromY = self.mousePressRect.bottom()
            toY = fromY + mousePos.y() - self.mousePressPos.y()
            diff.setY(toY - fromY)
            boundingRect.setBottom(toY)
            rect.setBottom(boundingRect.bottom() - offset)
            self.setRect(rect)

        elif self.handleSelected == self.handleBottomRight:

            fromX = self.mousePressRect.right()
            fromY = self.mousePressRect.bottom()
            toX = fromX + mousePos.x() - self.mousePressPos.x()
            toY = fromY + mousePos.y() - self.mousePressPos.y()
            diff.setX(toX - fromX)
            diff.setY(toY - fromY)
            boundingRect.setRight(toX)
            boundingRect.setBottom(toY)
            rect.setRight(boundingRect.right() - offset)
            rect.setBottom(boundingRect.bottom() - offset)
            self.setRect(rect)

        self.updateHandlesPos()

    def shape(self):
        """
        Returns the shape of this item as a QPainterPath in local coordinates.
        """
        path = QPainterPath()
        path.addRect(self.rect())
        if self.isSelected():
            for shape in self.handles.values():
                path.addEllipse(shape)
        return path

    def paint(self, painter, option, widget=None):
        """
        Paint the node in the graphic view.
        """
        painter.setBrush(QBrush(QColor(255, 0, 0, 100)))
        painter.setPen(QPen(QColor(0, 0, 0), 1.0, Qt.SolidLine))
        painter.drawRect(self.rect())

        painter.setRenderHint(QPainter.Antialiasing)
        painter.setBrush(QBrush(QColor(255, 0, 0, 255)))
        painter.setPen(QPen(QColor(0, 0, 0, 255), 1.0, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
        for handle, rect in self.handles.items():
            if self.handleSelected is None or handle == self.handleSelected:
                painter.drawEllipse(rect)


def main():

    app = QApplication(sys.argv)

    grview = QGraphicsView()
    scene = QGraphicsScene()

    item = GraphicsRectItem(0, 0, 300, 150)
    scene.addItem(item)

    grview.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
    grview.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

`QGraphicsRectItem.rect()` 是 PySide6 中用于**获取当前矩形几何属性**的方法,它返回一个 `QRectF` 对象,表示该图元内部所绘制的矩形区域(即通过 `setRect()` 设置的矩形)。 --- ### ✅ 方法说明:`rect()` #### 语法: ```python rect: QRectF = item.rect() ``` - **返回值类型**:`PySide6.QtCore.QRectF` - **含义**:返回 `QGraphicsRectItem` 的**本地坐标系下的矩形范围**,也就是你之前用 `setRect(x, y, width, height)` 设置的那个矩形。 - **不影响位置**:这个矩形是相对于图元自身的坐标原点(0,0),实际在场景中的位置还受 `pos()`(即 `setPos()`)影响。 --- ### 示例代码 ```python import sys from PySide6.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QMainWindow from PySide6.QtCore import Qt from PySide6.QtWidgets import QGraphicsRectItem class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("QGraphicsRectItem.rect() Example") self.resize(800, 600) # 创建场景 scene = QGraphicsScene() scene.setSceneRect(0, 0, 700, 500) # 创建矩形图元 rect_item = QGraphicsRectItem() # 设置内部矩形:左上角 (10, 20),宽 150,高 100 rect_item.setRect(10, 20, 150, 100) # 设置样式 rect_item.setPen(Qt.blue) rect_item.setBrush(Qt.yellow) # 移动整个图元到场景中的某个位置 rect_item.setPos(100, 100) # 图元整体移动到 (100,100) # 添加到场景 scene.addItem(rect_item) # 打印 rect() 获取的内容 local_rect = rect_item.rect() print(f"Local rect (from rect()): {local_rect}") # QRectF(10.0, 20.0, 150.0, 100.0) # 实际在场景中占据的区域(需要结合 pos) scene_rect = rect_item.mapToScene(local_rect).boundingRect() print(f"Actual scene bounding rect: {scene_rect}") # 创建视图 view = QGraphicsView(scene, self) self.setCentralWidget(view) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) ``` --- ### 🔍 输出示例解释: ``` Local rect (from rect()): PyQt6.QtCore.QRectF(10.0, 20.0, 150.0, 100.0) Actual scene bounding rect: PyQt6.QtCore.QRectF(110.0, 120.0, 150.0, 100.0) ``` - `rect()` 返回的是图元内部定义的矩形(本地坐标)。 - 实际显示在场景中的位置是 `(100+10, 100+20) = (110, 120)`,因为 `setPos(100,100)` 偏移了。 --- ### 🧩 常见用途 | 用途 | 说明 | |------|------| | **碰撞检测** | 使用 `rect()` 配合 `shape()` 或 `collidesWithItem()` 判断是否与其他图元重叠。 | | **自定义绘制** | 在重写 `paint()` 方法时,常根据 `rect()` 来决定如何绘制内容。 | | **动画或交互调整大小** | 动态读取当前大小并进行修改后调用 `setRect()` 更新。 | --- ### ⚠️ 注意事项 1. **`rect()` 不等于场景中的绝对位置** 它只是图元“内部”的矩形。要得到在场景中的真实范围,需使用: ```python scene_bounding_rect = rect_item.mapToScene(rect_item.rect()).boundingRect() ``` 2. **初始默认值** 如果从未调用 `setRect()`,则 `rect()` 默认返回 `QRectF(0, 0, 0, 0)`,即不可见。 3. **与 `boundingRect()` 的区别** - `rect()`:用户设置的逻辑矩形。 - `boundingRect()`:用于渲染和碰撞检测的外接矩形(通常基于 `rect()`,但可能包含边框、变换等扩展)。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值