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
相关产品推荐
相关产品推荐

