You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyQt5可调整大小QGraphicsRectItem如何正确更新场景坐标位置

问题说明

实现可调整大小的QGraphicsRectItem时,边缘拖拽缩放的尺寸调整逻辑正常,但缩放后图元在场景中的位置无法正确更新:向上拖拽顶边缩放时,图元会出现持续向上偏移的问题,位置计算逻辑失效。
原有问题代码如下:

import typing
import sys
from PyQt5.QtGui import QPen, QBrush, QColor, QResizeEvent
from PyQt5.QtCore import QRectF, QSize
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsRectItem, QMainWindow, QVBoxLayout, QWidget

class ResizableRect(QGraphicsRectItem):
    def __init__(self, *args):
        super().__init__(*args)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setPen(QPen(QBrush(QColor('blue')), 5))
        self.selected_edge = None
        self.click_pos = self.click_rect = None

    def mousePressEvent(self, event):
        """ The mouse is pressed, start tracking movement. """
        self.click_pos = event.pos()
        self.newY = self.pos().y()
        rect = self.rect()
        if abs(rect.left() - self.click_pos.x()) < 5:
            self.selected_edge = 'left'
        elif abs(rect.right() - self.click_pos.x()) < 5:
            self.selected_edge = 'right'
        elif abs(rect.top() - self.click_pos.y()) < 5:
            self.selected_edge = 'top'
        elif abs(rect.bottom() - self.click_pos.y()) < 5:
            self.selected_edge = 'bottom'
        else:
            self.selected_edge = None
        self.click_pos = event.pos()
        self.click_rect = rect
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        """ Continue tracking movement while the mouse is pressed. """
        # Calculate how much the mouse has moved since the click.
        pos = event.pos()
        x_diff = pos.x() - self.click_pos.x()
        y_diff = pos.y() - self.click_pos.y()

        # Start with the rectangle as it was when clicked.
        rect = QRectF(self.click_rect)

        # Then adjust by the distance the mouse moved.
        if self.selected_edge is None:
            rect.translate(x_diff, y_diff)
        elif self.selected_edge == 'top':
            rect.adjust(0, y_diff, 0, 0)
            # Test when resize rectangle upward; not working properly for now
            if y_diff < 0:              
                    newCenter = (rect.bottom() - pos.y()) / 2
                    self.newY = self.pos().y() - newCenter

        elif self.selected_edge == 'left':
            rect.adjust(x_diff, 0, 0, 0)
        elif self.selected_edge == 'bottom':
            rect.adjust(0, 0, 0, y_diff)
        elif self.selected_edge == 'right':
            rect.adjust(0, 0, x_diff, 0)


        # Also check if the rectangle has been dragged inside out.
        if rect.width() < 5:
            if self.selected_edge == 'left':
                rect.setLeft(rect.right() - 5)
            else:
                rect.setRight(rect.left() + 5)
        if rect.height() < 5:
            if self.selected_edge == 'top':
                rect.setTop(rect.bottom() - 5)
            else:
                rect.setBottom(rect.top() + 5)

        # Finally, update the rect that is now guaranteed to stay in bounds.
        self.setY(self.newY)
        self.setRect(rect)

        
    def mouseReleaseEvent(self, event): # for printing only i.e., after resizing
        print(f"item.pos(): {self.pos()}")


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        central = QWidget(self)
        self.setCentralWidget(central)

        self.rect = ResizableRect(-100, -50, 200, 100)
        scene = QGraphicsScene(0, 0, 300, 300)
        scene.addItem(self.rect)
        self.view = QGraphicsView(central)
        self.view.setScene(scene)

        layout = QVBoxLayout(central)
        layout.addWidget(self.view)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()

    app.exec_()


main()
错误原因
  • 位置计算公式完全不成立:拖拽顶边时使用的中心点计算逻辑不符合图元坐标系规则,且每次鼠标移动都基于当前实时的self.pos().y()做增量计算,没有以鼠标按下瞬间的初始位置为基准,导致偏移量持续累加,出现图元漂移。
  • 逻辑覆盖不全:仅处理了向上拖拽顶边(y_diff<0)的场景,向下拖拽顶边缩小高度时完全没有更新位置。
  • 遗漏边缘位置同步:拖拽左边缩放和顶边逻辑一致,需要同步更新图元x坐标,原有代码未处理这部分。
修正逻辑

拖拽边缘缩放的核心规则:拖拽某一条边时,对面的边在场景中的位置保持固定不动。

  • 拖拽顶边时,底边位置固定:鼠标按下时先记录底边在场景中的初始y坐标,缩放后新的图元y坐标 = 底边固定y坐标 - 新矩形自身坐标系下的bottom值。
  • 拖拽左边时,右边位置固定:鼠标按下时先记录右边在场景中的初始x坐标,缩放后新的图元x坐标 = 右边固定x坐标 - 新矩形自身坐标系下的right值。
  • 拖拽底边/右边时,顶边/左边位置天然固定,不需要修改图元坐标,直接调整rect尺寸即可。
  • 所有计算都基于鼠标按下时存储的初始状态,不在鼠标移动过程中用实时位置做增量累加,从根源避免漂移。
修正后完整代码
import sys
from PyQt5.QtGui import QPen, QBrush, QColor
from PyQt5.QtCore import QRectF
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsRectItem, QMainWindow, QVBoxLayout, QWidget

class ResizableRect(QGraphicsRectItem):
    def __init__(self, *args):
        super().__init__(*args)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setPen(QPen(QBrush(QColor('blue')), 5))
        self.selected_edge = None
        # 存储鼠标按下时的初始状态
        self.click_pos = None
        self.click_rect = None
        self.initial_pos = None
        # 存储固定边的场景坐标
        self.fixed_x = None
        self.fixed_y = None

    def mousePressEvent(self, event):
        self.click_pos = event.pos()
        rect = self.rect()
        self.initial_pos = self.pos()
        # 判断点击的边缘,记录对侧固定边坐标
        if abs(rect.left() - self.click_pos.x()) < 5:
            self.selected_edge = 'left'
            self.fixed_x = self.initial_pos.x() + rect.right()
        elif abs(rect.right() - self.click_pos.x()) < 5:
            self.selected_edge = 'right'
            self.fixed_x = None
        elif abs(rect.top() - self.click_pos.y()) < 5:
            self.selected_edge = 'top'
            self.fixed_y = self.initial_pos.y() + rect.bottom()
        elif abs(rect.bottom() - self.click_pos.y()) < 5:
            self.selected_edge = 'bottom'
            self.fixed_y = None
        else:
            self.selected_edge = None
            self.fixed_x = None
            self.fixed_y = None
        self.click_rect = rect
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        pos = event.pos()
        x_diff = pos.x() - self.click_pos.x()
        y_diff = pos.y() - self.click_pos.y()

        rect = QRectF(self.click_rect)
        new_x = self.initial_pos.x()
        new_y = self.initial_pos.y()

        if self.selected_edge is None:
            # 整体拖拽移动
            new_x = self.initial_pos.x() + x_diff
            new_y = self.initial_pos.y() + y_diff
        elif self.selected_edge == 'top':
            rect.adjust(0, y_diff, 0, 0)
            # 保持底边位置固定
            new_y = self.fixed_y - rect.bottom()
        elif self.selected_edge == 'left':
            rect.adjust(x_diff, 0, 0, 0)
            # 保持右边位置固定
            new_x = self.fixed_x - rect.right()
        elif self.selected_edge == 'bottom':
            rect.adjust(0, 0, 0, y_diff)
        elif self.selected_edge == 'right':
            rect.adjust(0, 0, x_diff, 0)

        # 限制最小尺寸
        if rect.width() < 5:
            if self.selected_edge == 'left':
                rect.setLeft(rect.right() - 5)
                new_x = self.fixed_x - rect.right()
            else:
                rect.setRight(rect.left() + 5)
        if rect.height() < 5:
            if self.selected_edge == 'top':
                rect.setTop(rect.bottom() - 5)
                new_y = self.fixed_y - rect.bottom()
            else:
                rect.setBottom(rect.top() + 5)

        # 同步更新位置和矩形尺寸
        self.setPos(new_x, new_y)
        self.setRect(rect)

    def mouseReleaseEvent(self, event):
        print(f"item.pos(): {self.pos()}")
        super().mouseReleaseEvent(event)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        central = QWidget(self)
        self.setCentralWidget(central)

        self.rect = ResizableRect(-100, -50, 200, 100)
        scene = QGraphicsScene(0, 0, 300, 300)
        scene.addItem(self.rect)
        self.view = QGraphicsView(central)
        self.view.setScene(scene)

        layout = QVBoxLayout(central)
        layout.addWidget(self.view)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()


if __name__ == "__main__":
    main()

修正后拖拽任意边缩放时,对侧固定边不会发生位移,图元位置和尺寸完全跟随鼠标操作,无漂移问题。


内容的提问来源于stack exchange,提问作者MinhTNguyen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 14:21:21