添加固定尺寸Item时QGraphicsScene的异常行为及解决方案问询
我想给QGraphicsScene添加缩放时保持屏幕固定尺寸的Item,创建方式如下:
class LocationGraphicsItem(QtWidgets.QGraphicsEllipseItem): def __init__(self, coordinate, parent = None): super().__init__(parent=parent) x = coordinate[0] y = -coordinate[1] self.setRect(-5, -5, 10, 10) self.setPos(x, y) self.setFlag(self.GraphicsItemFlag.ItemIgnoresTransformations)
添加Item的代码:
coordinate = (3412770.9, 5358376.3) item = LocationGraphicsItem(coordinate) my_scene.addItem(item) item.ensureVisible()
之后调用视图适配:
my_view.fitInView(my_scene.sceneRect(), Qt.KeepAspectRatio)
当所有Item在Qt事件循环启动前添加时,视图显示正常,sceneRect如下:
PySide6.QtCore.QRectF(3412765.400000, -5371895.600000, 82420.600000, 13524.800000)
但如果在事件循环启动、QGraphicsView已可见后添加Item,视图的感兴趣区域会挤在右上角,sceneRect变成:
PySide6.QtCore.QRectF(-5.500000, -5371895.600000, 3495191.500000, 5371901.100000)
需要解决以下问题:
- 如何避免该问题?请解释原因(已知
my_scene.itemsBoundingRect()可以重新计算,但速度慢)。 - 实现固定尺寸标记的替代方案/最佳实践有哪些?希望以像素为单位指定尺寸。
import random import sys print("Python: ", sys.version) import PySide6 print("PySide: ", PySide6.__version__) from PySide6 import QtCore, QtGui, QtWidgets class LocationGraphicsItem(QtWidgets.QGraphicsEllipseItem): def __init__(self, coordinate, parent = None): super().__init__(parent=parent) x = coordinate[0] y = -coordinate[1] self.setRect(-5, -5, 10, 10) self.setPos(x, y) self.setBrush(QtGui.QColor("blue")) self.setFlag(self.GraphicsItemFlag.ItemIgnoresTransformations) def add_something(): print(scene.sceneRect()) point =(3.4e6+random.random()*1e5, 5.3e6+random.random()*1e5) print("Random point: ", point) item = LocationGraphicsItem(point) scene.addItem(item) item.setVisible(True) item.ensureVisible() view.fitInView(scene.sceneRect(), QtCore.Qt.KeepAspectRatio) QtWidgets.QApplication.processEvents() print(scene.sceneRect()) if __name__ == "__main__": my_points = [(3412770.9, 5358376.3), (3495180.5, 5371890.1), (3495099.1, 5370624.6), (3485765.4, 5371030.1)] app = QtWidgets.QApplication() scene = QtWidgets.QGraphicsScene() for point in my_points: item = LocationGraphicsItem(point) scene.addItem(item) item.setVisible(True) item.ensureVisible() window = QtWidgets.QMainWindow(parent = None) window.setGeometry(50, 50, 1300, 750) basic_widget = QtWidgets.QWidget(parent=window) window.setCentralWidget(basic_widget) layout = QtWidgets.QHBoxLayout() basic_widget.setLayout(layout) button = QtWidgets.QPushButton("add something", parent = basic_widget) button.clicked.connect(add_something) layout.addWidget(button) view = QtWidgets.QGraphicsView(scene, parent = basic_widget) layout.addWidget(view) view.fitInView(scene.sceneRect(), QtCore.Qt.KeepAspectRatio) window.setVisible(True) app.exec()
Python: 3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)] PySide: 6.4.0.1 PySide6.QtCore.QRectF(3412765.400000, -5371895.600000, 82420.600000, 13524.800000) Random point: (3449076.7327629146, 5323846.465410875) PySide6.QtCore.QRectF(-5.500000, -5371895.600000, 3495191.500000, 5371901.100000)
原因解释
问题出在ItemIgnoresTransformations标记和sceneRect的自动计算逻辑:
- 设置
ItemIgnoresTransformations后,Item的视觉尺寸固定,但Qt会特殊处理它的逻辑边界——忽略Item的变换后,Qt错误地将Item的rect((-5,-5,10,10))视为场景中的逻辑范围,而非基于setPos的真实坐标。 - 事件循环启动前添加Item时,Qt会一次性计算所有Item的边界,此时能正确识别
ItemIgnoresTransformations类型Item的真实场景位置,生成正确的sceneRect。 - 事件循环启动后动态添加Item时,Qt更新
sceneRect的逻辑出现偏差:将新增Item的rect直接纳入场景范围,导致sceneRect左边界被拉到-5.5,场景范围被错误扩大。
快速解决方法
无需调用慢的itemsBoundingRect(),可以手动维护sceneRect:
方法1:添加Item后手动修正sceneRect
在add_something函数中,基于已有场景范围和新Item的真实位置更新sceneRect:
def add_something(): print(scene.sceneRect()) point =(3.4e6+random.random()*1e5, 5.3e6+random.random()*1e5) print("Random point: ", point) item = LocationGraphicsItem(point) scene.addItem(item) item.setVisible(True) # 手动更新sceneRect:仅考虑Item的真实场景位置 current_rect = scene.sceneRect() item_scene_pos = item.pos() new_rect = current_rect.united(QtCore.QRectF(item_scene_pos.x(), item_scene_pos.y(), 0, 0)) scene.setSceneRect(new_rect) view.fitInView(scene.sceneRect(), QtCore.Qt.KeepAspectRatio) print(scene.sceneRect())
方法2:初始化时固定sceneRect,后续手动扩展
如果已知Item的大致坐标范围,初始化时手动设置sceneRect,后续添加Item时仅扩展超出的部分:
# 初始化场景时 scene = QtWidgets.QGraphicsScene() # 基于初始点计算初始sceneRect min_x = min(p[0] for p in my_points) max_x = max(p[0] for p in my_points) min_y = -max(p[1] for p in my_points) max_y = -min(p[1] for p in my_points) scene.setSceneRect(min_x - 10, min_y - 10, max_x - min_x + 20, max_y - min_y + 20)
后续添加Item时,检查新位置是否超出当前范围,再调用scene.setSceneRect(scene.sceneRect().united(...))即可。
如果希望以像素为单位指定标记尺寸,有以下更灵活的方案:
方案1:重写Item的paint方法,手动适配缩放
不使用ItemIgnoresTransformations,在paint方法中根据视图缩放比例计算绘制尺寸:
class LocationGraphicsItem(QtWidgets.QGraphicsItem): def __init__(self, coordinate, parent=None): super().__init__(parent) self.x = coordinate[0] self.y = -coordinate[1] self.setPos(self.x, self.y) self.pixel_radius = 5 # 固定像素尺寸 def boundingRect(self): # 预留足够边界,避免裁剪 return QtCore.QRectF(-self.pixel_radius*2, -self.pixel_radius*2, self.pixel_radius*4, self.pixel_radius*4) def paint(self, painter, option, widget): if widget is None: return # 获取视图缩放比例 scale = widget.viewportTransform().m11() # 计算场景中的绘制尺寸 scene_radius = self.pixel_radius / scale painter.setBrush(QtGui.QColor("blue")) painter.drawEllipse(QtCore.QPointF(0,0), scene_radius, scene_radius)
这种方式让Item参与正常场景变换,Qt能正确计算sceneRect,无边界错误。
方案2:在QGraphicsView的视口上绘制标记
直接在视图的paintEvent中绘制固定像素尺寸的标记,无需添加到场景:
class CustomGraphicsView(QtWidgets.QGraphicsView): def __init__(self, scene, parent=None): super().__init__(scene, parent) self.markers = [] # 存储标记的场景坐标 def addMarker(self, scene_coordinate): self.markers.append(scene_coordinate) self.update() def paintEvent(self, event): super().paintEvent(event) painter = QtGui.QPainter(self.viewport()) painter.setRenderHint(QtGui.QPainter.Antialiasing) painter.setBrush(QtGui.QColor("blue")) # 场景坐标转视图像素坐标 for coord in self.markers: scene_point = QtCore.QPointF(coord[0], -coord[1]) view_point = self.mapFromScene(scene_point) painter.drawEllipse(view_point, 5, 5)
使用时直接调用view.addMarker(坐标)即可,标记完全脱离场景变换,适合标记数量较少的场景。
方案3:保留ItemIgnoresTransformations并手动维护sceneRect
即前面的解决方法,适合不想修改现有Item结构的场景,通过手动控制sceneRect避免Qt自动计算错误。
内容的提问来源于stack exchange,提问作者julian

