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

PySide2:基于选中项边界框中心水平镜像QGraphicsItems的问题

QGraphicsItems围绕集体中心水平镜像问题修复

问题描述

尝试实现选中QGraphicsItems围绕集体边界框中心水平镜像的功能,但每次操作元素都会飞出屏幕。此前在3D应用中实现过类似功能,但PySide2的实现遗漏关键环节,目标是点击镜像按钮后,选中元素能围绕集体中心来回水平翻转。

原问题代码

import sys
from PySide2 import QtWidgets, QtCore, QtGui
from PySide2.QtGui import QPixmap, QPainter, QColor
import os
import random


class ImageWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.resize(1500,1080)

        # controls
        self.scene = QtWidgets.QGraphicsScene(self)
        self.scene.setBackgroundBrush(QColor(40,40,40))
        
        self.graphicsView = QtWidgets.QGraphicsView(self)
        self.graphicsView.setSceneRect(-4000, -4000, 8000, 8000)
        self.graphicsView.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
        self.graphicsView.setScene(self.scene)

        # align actions
        self.mirrorItemsHorizontalAct = QtWidgets.QAction('Mirror Horizontal', self)
        self.mirrorItemsHorizontalAct.triggered.connect(self.mirror_items_horizontal)

        transformToolbar = QtWidgets.QToolBar('Transform', self)
        transformToolbar.addAction(self.mirrorItemsHorizontalAct)
        self.addToolBar(transformToolbar)

        self.setCentralWidget(self.graphicsView)

        # Load images from subfolder
        self.create_shapes()


    def create_shapes(self):
        gradient = QtGui.QLinearGradient(0, 0, 100, 0)
        gradient.setColorAt(0, QtGui.QColor(0, 0, 255))
        gradient.setColorAt(1, QtGui.QColor(255, 0, 0))

        rectA = QtWidgets.QGraphicsRectItem(0,0,150,100)
        rectA.setBrush(QtGui.QBrush(gradient))
        rectA.setPen(QtGui.QPen(QtCore.Qt.NoPen))
        rectA.setPen(QtGui.QPen(QtCore.Qt.green, 10, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
        rectA.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemIsMovable)
        rectA.setSelected(True)
        rectA.setPos(-120,50)
        self.scene.addItem(rectA)

        rectB = QtWidgets.QGraphicsRectItem(0,0,70,35)
        rectB.setBrush(QtGui.QBrush(gradient))
        rectB.setPen(QtGui.QPen(QtCore.Qt.NoPen))
        rectB.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemIsMovable)
        rectB.setSelected(True)
        rectB.setPos(150,-150)
        self.scene.addItem(rectB)

        rectC = QtWidgets.QGraphicsRectItem(0,0,120,75)
        rectC.setBrush(QtGui.QBrush(gradient))
        rectC.setPen(QtGui.QPen(QtCore.Qt.NoPen))
        rectC.setFlags(QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemIsMovable)
        rectC.setSelected(True)
        rectC.setPos(400,70)
        self.scene.addItem(rectC)


    # Align
    def mirror_items_horizontal(self):
        items = self.scene.selectedItems()
        if not items:
            items = self.scene.items()   
        
        # Calculate the collective bounding box
        collectivePath = QtGui.QPainterPath()
        for item in items:
            collectivePath.addRect(item.sceneBoundingRect())
        collectiveRect = collectivePath.boundingRect()

        # Calculate the horizontal center of the collective bounding box
        centerX = collectiveRect.center().x()

        # print('centerX:', centerX)
        # print('collectiveRect:', collectiveRect)

        # Debug
        cItem = QtWidgets.QGraphicsRectItem(collectiveRect)
        cItem.setBrush(QtGui.QBrush(QtGui.QColor(255,0,0,128)))
        cItem.setZValue(-1000)
        self.scene.addItem(cItem)

        cItem = QtWidgets.QGraphicsEllipseItem(0, 0, 10, 10)
        cItem.setPos(collectiveRect.center() - QtCore.QPointF(cItem.boundingRect().width()*0.5, cItem.boundingRect().height()*0.5) )
        cItem.setBrush(QtGui.QBrush(QtGui.QColor(255,255,0,128)))
        cItem.setZValue(-1000)
        self.scene.addItem(cItem)

        for item in items:
            local_offset = item.boundingRect().center().x() * 2.0
            global_offset = collectiveRect.width() + item.boundingRect().x() * 2.0
            print('scenePos', item.scenePos())
            print('boundingRect', item.boundingRect())
            print('boundingRect center', item.boundingRect().center())
            print('local_offset', local_offset)
            print('global_offset', global_offset)

            scaleTm = QtGui.QTransform()
            scaleTm.translate(local_offset, 0)
            # scaleTm.translate(global_offset, 0)
            # scaleTm.translate(645, 0)
            scaleTm.scale(-1, 1)

            tm = item.transform() * scaleTm
            item.setTransform(tm)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    mainWindow = ImageWindow()
    mainWindow.show()
    sys.exit(app.exec_())

修复方案

问题根源在于变换矩阵的应用顺序错误、坐标系混淆,且未处理镜像状态的切换。以下是修复后的核心代码:

修改后的mirror_items_horizontal方法

def mirror_items_horizontal(self):
    items = self.scene.selectedItems()
    if not items:
        items = self.scene.items()   
    
    # 计算选中元素的集体边界框(用united更高效)
    collectiveRect = QtCore.QRectF()
    for item in items:
        collectiveRect = collectiveRect.united(item.sceneBoundingRect())
    if collectiveRect.isNull():
        return
    
    center = collectiveRect.center()

    # 调试用:显示集体边界框和中心(可选,可删除)
    cItem = QtWidgets.QGraphicsRectItem(collectiveRect)
    cItem.setBrush(QtGui.QBrush(QtGui.QColor(255,0,0,128)))
    cItem.setZValue(-1000)
    self.scene.addItem(cItem)

    cItem = QtWidgets.QGraphicsEllipseItem(0, 0, 10, 10)
    cItem.setPos(center - QtCore.QPointF(5,5))
    cItem.setBrush(QtGui.QBrush(QtGui.QColor(255,255,0,128)))
    cItem.setZValue(-1000)
    self.scene.addItem(cItem)

    for item in items:
        current_transform = item.transform()
        # 检查当前是否已水平镜像(m11为x方向缩放因子,-1表示已镜像)
        is_mirrored = current_transform.m11() < 0

        # 获取元素在场景中的中心坐标
        item_scene_center = item.sceneBoundingRect().center()
        # 计算元素中心到集体中心的偏移量
        offset = item_scene_center - center

        # 构建变换矩阵:QTransform的变换是从右向左执行的
        transform = QtGui.QTransform()
        # 1. 平移元素,使其中心与集体中心重合
        transform.translate(offset.x(), offset.y())
        # 2. 执行水平镜像(或恢复原状态)
        transform.scale(-1 if not is_mirrored else 1, 1)
        # 3. 平移回原相对位置
        transform.translate(-offset.x(), -offset.y())

        # 切换镜像状态:已镜像则恢复,未镜像则应用变换
        if is_mirrored:
            item.setTransform(QtGui.QTransform())
        else:
            item.setTransform(current_transform * transform)

关键修改说明

  • 集体边界框计算优化:使用QRectF.united()替代QPainterPath,更高效且结果准确
  • 变换顺序修正:QTransform的变换逻辑是逆序执行,因此正确顺序为:平移到集体中心 → 水平镜像 → 平移回原位置
  • 镜像状态切换:通过变换矩阵的m11值判断当前镜像状态,实现点击按钮来回切换的效果
  • 坐标系统一:所有计算基于场景坐标系,避免局部坐标系的混淆导致元素偏移

完整修复代码

将上述修改后的mirror_items_horizontal方法替换原代码中的对应方法即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 20:54:58