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

如何在QGraphicsPixmapItem上实现带控制点的缩放旋转功能?

问题描述

我在网上多次查找QGraphicsView是否具备图像旋转/缩放功能,但未找到合适方案。我希望实现类似绘图软件的效果:每个图像/图形带有边界控制点,用户可通过这些控制点对图像/图形进行缩放或旋转,效果如下:
图像控制点操作效果

目前我编写的代码如下:

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


class ImagePoint(QGraphicsRectItem):

    def __init__(self, x, y, w=15, h=15, parent=None):
        super(ImagePoint, self).__init__(x - w / 2, y - w / 2, w, h, parent)
        self.setAcceptHoverEvents(True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
        self.setAcceptHoverEvents(True)
        self.setBrush(QColor(Qt.blue))

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemPositionChange:
            self.parentItem().setScale(value)
        super(ImagePoint, self).itemChange(change, value)


class Back(QGraphicsPixmapItem):

    def __init__(self, file_name, scene):
        super(Back, self).__init__()
        self.setAcceptHoverEvents(True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
        self.setAcceptHoverEvents(True)

        pixmap = QPixmap(file_name)
        self.scene = scene
        self.setPixmap(pixmap)
        self.init_boundre_points()

    def init_boundre_points(self):
        ImagePoint(self.boundingRect().topLeft().x(), self.boundingRect().topLeft().y(), parent=self)
        ImagePoint(self.boundingRect().topRight().x(), self.boundingRect().topRight().y(), parent=self)
        ImagePoint(self.boundingRect().bottomLeft().x(), self.boundingRect().bottomLeft().y(), parent=self)
        ImagePoint(self.boundingRect().bottomRight().x(), self.boundingRect().bottomRight().y(), parent=self)

        x = self.boundingRect().topLeft().x() + self.boundingRect().width() / 2
        y = self.boundingRect().topLeft().y()
        ImagePoint(x, y, parent=self)

        x = self.boundingRect().bottomLeft().x() + self.boundingRect().width() / 2
        y = self.boundingRect().bottomLeft().y()
        ImagePoint(x, y, parent=self)

        x = self.boundingRect().topLeft().x()
        y = self.boundingRect().topLeft().y() + self.boundingRect().height() / 2
        ImagePoint(x, y, parent=self)

        x = self.boundingRect().topRight().x()
        y = self.boundingRect().topRight().y() + self.boundingRect().height() / 2
        ImagePoint(x, y, parent=self)


class MyGraphicsView(QGraphicsView):
    def __init__(self):
        super(MyGraphicsView, self).__init__()
        self.setScene(MyGraphicsScene(self))


class MyGraphicsScene(QGraphicsScene):
    def __init__(self, parent):
        super(MyGraphicsScene, self).__init__()
        self.setBackgroundBrush(QBrush(QColor(50, 50, 50)))
        back = Back("Path_image", self)
        self.addItem(back)


class MyMainWindow(QMainWindow):
    def __init__(self):
        super(MyMainWindow, self).__init__()
        self.setWindowTitle("Test")
        self.resize(800, 600)
        self.gv = MyGraphicsView()
        self.setCentralWidget(self.gv)


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

请问如何有效实现该功能?我是否遗漏了QGraphicsView框架中的相关特性?


解决方案

一、你遗漏的QGraphicsView框架特性

QGraphicsItem本身支持setScale()、setRotation()、setTransform()等变换接口,但直接用这些接口结合控制点需要处理坐标映射和拖动时的变换逻辑计算,你当前的代码没有正确处理这两点,比如把坐标值直接传给setScale()的错误逻辑。

二、核心修改与实现步骤

  1. 设置变换锚点,避免偏移
    在Back类的__init__方法中添加,将图像的变换中心设为自身中心,缩放旋转时不会偏移:

    self.setTransformOriginPoint(self.boundingRect().center())
    
  2. 重构控制点类,添加类型标记与正确的拖动逻辑
    给每个控制点标记类型(角点、边中点、旋转点),通过鼠标拖动的位移计算缩放比例或旋转角度:

    class ImagePoint(QGraphicsRectItem):
        def __init__(self, pos, control_type, w=15, h=15, parent=None):
            # 以自身中心为原点初始化,避免拖动时偏移
            super().__init__(-w/2, -h/2, w, h, parent)
            self.setPos(pos)
            self.control_type = control_type  # 标记控制点类型
            self.setAcceptHoverEvents(True)
            self.setFlag(QGraphicsItem.ItemIsSelectable, True)
            self.setFlag(QGraphicsItem.ItemIsMovable, True)
            # 给旋转点设置不同样式,区分功能
            if control_type == "rotate":
                self.setBrush(QColor(Qt.red))
                self.setRect(-w/2, -h/2, w, h)
            else:
                self.setBrush(QColor(Qt.blue))
            self.last_scene_pos = None
    
        def mousePressEvent(self, event):
            self.last_scene_pos = event.scenePos()
            super().mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            if self.last_scene_pos is None or not self.parentItem():
                super().mouseMoveEvent(event)
                return
    
            parent_item = self.parentItem()
            current_pos = event.scenePos()
            delta = current_pos - self.last_scene_pos
            self.last_scene_pos = current_pos
    
            # 获取父项的场景中心坐标
            parent_center = parent_item.mapToScene(parent_item.boundingRect().center())
    
            # 根据控制点类型处理变换
            if self.control_type in ["top-left", "bottom-right"]:
                # 对角缩放:计算距离比例作为缩放因子
                old_dist = QLineF(parent_center, self.last_scene_pos - delta).length()
                new_dist = QLineF(parent_center, self.last_scene_pos).length()
                if old_dist < 1:
                    old_dist = 1
                scale_factor = new_dist / old_dist
                parent_item.setScale(parent_item.scale() * scale_factor)
            elif self.control_type == "rotate":
                # 旋转:计算角度差
                old_angle = QLineF(parent_center, self.last_scene_pos - delta).angle()
                new_angle = QLineF(parent_center, self.last_scene_pos).angle()
                parent_item.setRotation(parent_item.rotation() + (new_angle - old_angle))
            elif self.control_type in ["top-middle", "bottom-middle"]:
                # 垂直等比例缩放
                scale_factor = 1 + delta.y() / parent_item.boundingRect().height()
                parent_item.setScale(parent_item.scale() * scale_factor)
            elif self.control_type in ["left-middle", "right-middle"]:
                # 水平等比例缩放
                scale_factor = 1 + delta.x() / parent_item.boundingRect().width()
                parent_item.setScale(parent_item.scale() * scale_factor)
    
            super().mouseMoveEvent(event)
    
  3. 修改控制点初始化逻辑,添加类型标记
    在Back类中添加辅助方法,统一创建带类型标记的控制点:

    def init_boundre_points(self):
        rect = self.boundingRect()
        # 四个角控制点
        self._add_control_point(rect.topLeft(), "top-left")
        self._add_control_point(rect.topRight(), "top-right")
        self._add_control_point(rect.bottomLeft(), "bottom-left")
        self._add_control_point(rect.bottomRight(), "bottom-right")
        # 四条边中点
        self._add_control_point(rect.topLeft() + QPointF(rect.width()/2, 0), "top-middle")
        self._add_control_point(rect.bottomLeft() + QPointF(rect.width()/2, 0), "bottom-middle")
        self._add_control_point(rect.topLeft() + QPointF(0, rect.height()/2), "left-middle")
        self._add_control_point(rect.topRight() + QPointF(0, rect.height()/2), "right-middle")
        # 右上角外侧添加旋转控制点
        rotate_pos = rect.topRight() + QPointF(0, -30)
        self._add_control_point(rotate_pos, "rotate")
    
    def _add_control_point(self, pos, control_type):
        ImagePoint(pos, control_type, parent=self)
    
  4. 处理坐标映射细节
    所有变换计算都通过mapToScene()将局部坐标转换为场景坐标,确保计算在同一空间内进行,避免坐标混乱。

三、额外优化建议

  • 限制缩放比例范围(比如0.1到10倍),防止图像过度缩放。
  • 添加Shift键绑定,按住Shift时强制等比例缩放。
  • 优化控制点的hover样式,比如鼠标悬停时改变光标形状。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 07:40:22