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

添加固定尺寸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)

需要解决以下问题:

  1. 如何避免该问题?请解释原因(已知my_scene.itemsBoundingRect()可以重新计算,但速度慢)。
  2. 实现固定尺寸标记的替代方案/最佳实践有哪些?希望以像素为单位指定尺寸。

完整测试代码
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 18:44:57