我在QGraphicsItem上添加动画时遇到的一个问题

A question I meet when adding animation on QGraphicsItem

我想在 QGraphicsItem 上使用 QPropertyAnimation,希望矩形项目可以从点 (100, 30) 移动到点 (100, 90)。但为什么 rect 在 window 的右侧移动自己? x 坐标 100 应该使 rect 根据场景的大小在中间移动。

这是我的代码:

import sys
from PyQt5.QtCore import QPropertyAnimation, QPointF, QRectF
from PyQt5.QtWidgets import QApplication, QGraphicsEllipseItem, QGraphicsScene, QGraphicsView, \
                            QGraphicsObject


class CustomRect(QGraphicsObject):
    def __init__(self):
        super(CustomRect, self).__init__()

    def boundingRect(self):
        return QRectF(100, 30, 100, 30)

    def paint(self, painter, styles, widget=None):
        painter.drawRect(self.boundingRect())


class Demo(QGraphicsView):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(300, 300)

        self.scene = QGraphicsScene()
        self.scene.setSceneRect(0, 0, 300, 300)

        self.rect = CustomRect()
        self.ellipse = QGraphicsEllipseItem()
        self.ellipse.setRect(100, 180, 100, 50)

        self.scene.addItem(self.rect)
        self.scene.addItem(self.ellipse)

        self.setScene(self.scene)

        self.animation = QPropertyAnimation(self.rect, b'pos')
        self.animation.setDuration(1000)
        self.animation.setStartValue(QPointF(100, 30))
        self.animation.setEndValue(QPointF(100, 90))
        self.animation.setLoopCount(-1)
        self.animation.start()


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

试一试:

import sys
from PyQt5.QtCore import QPropertyAnimation, QPointF, QRectF
from PyQt5.QtWidgets import QApplication, QGraphicsEllipseItem, QGraphicsScene, QGraphicsView, \
                            QGraphicsObject


class CustomRect(QGraphicsObject):
    def __init__(self):
        super(CustomRect, self).__init__()

    def boundingRect(self):
#        return QRectF(100, 30, 100, 30)
        return QRectF(0, 0, 100, 30)                          # +++

    def paint(self, painter, styles, widget=None):
        painter.drawRect(self.boundingRect())


class Demo(QGraphicsView):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(300, 300)

        self.scene = QGraphicsScene()
        self.scene.setSceneRect(0, 0, 300, 300)

        self.rect = CustomRect()
        self.ellipse = QGraphicsEllipseItem()
        self.ellipse.setRect(100, 180, 100, 50)

        self.scene.addItem(self.rect)
        self.scene.addItem(self.ellipse)

        self.setScene(self.scene)

        self.animation = QPropertyAnimation(self.rect, b'pos')
        self.animation.setDuration(3000)
        self.animation.setStartValue(QPointF(100, 30))
        self.animation.setEndValue(QPointF(100, 90))
        self.animation.setLoopCount(-1)
        self.animation.start()


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

好像不知道Graphics View Framework的不同坐标系。

在这个系统中至少有以下坐标系:

  • window(viewport()) 的坐标系,其中 (0, 0) 始终位于 window 的左上角。

  • 场景的坐标系,这是相对于一些预先建立的点。

  • 每个item的坐标坐标系,paint()方法使用这个坐标系进行绘画,boundingRect()和shape()方法获取边缘item.

你还要有另外一个概念,item的位置是相对于parent有的,没有的就是相对于场景的。

类比

为了解释不同的坐标系,我使用了用相机记录场景的类比。

  • QGraphicsView 将是相机的屏幕。
  • QGraphicsScene是记录的场景,所以点(0, 0)是一些方便的点。
  • QGraphicsItem 是场景的元素,它们的位置可以是相对于其他项目或场景的,例如我们可以考虑演员的鞋子相对于演员的位置,或者项目可以是同一个演员。

基于以上我将解释发生了什么,我们将给出几种解决方案。

矩形项没有父项,默认情况下,项的位置图标是 (0, 0) 因此此时项的坐标系与场景重合,因此 boundingRect 将在视觉上定义位置并作为您已经放置了 QRectF(100, 30, 100, 30) 这将被绘制在场景中恰好相同的位置。但是当您应用动画时,首先要做的是将项目的位置设置为 (100, 30),这样由于场景和项目的坐标系不匹配,所以一个从另一个移开,所以 boundingRect 不再与场景的 QRectF(100, 30, 100, 30) 匹配,而是以相同的因子移动(只是因为有位移,没有缩放或旋转)并且矩形将是 QRectF( 200, 60, 100, 30) 并且相对于始终位于 QRect(100, 180, 100, 50) 中的椭圆,因此矩形位于右侧,因为 200>100 并且由于 60<180 而向上。


因此,如果您希望矩形位于椭圆的顶部,至少有 2 种解决方案:

  1. 修改 boundingRect 使其位于位置 0,0 以便与动画引起的位移匹配:
import sys
from PyQt5 import QtCore, QtWidgets


class CustomRect(QtWidgets.QGraphicsObject):
    def boundingRect(self):
        return QtCore.QRectF(0, 0, 100, 30)  # <---

    def paint(self, painter, styles, widget=None):
        painter.drawRect(self.boundingRect())


class Demo(QtWidgets.QGraphicsView):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(300, 300)

        self.scene = QtWidgets.QGraphicsScene()
        self.scene.setSceneRect(0, 0, 300, 300)

        self.rect = CustomRect()
        self.ellipse = QtWidgets.QGraphicsEllipseItem()
        self.ellipse.setRect(100, 180, 100, 50)
        self.scene.addItem(self.rect)
        self.scene.addItem(self.ellipse)

        self.setScene(self.scene)

        self.animation = QtCore.QPropertyAnimation(
            self.rect,
            b"pos",
            duration=1000,
            startValue=QtCore.QPointF(100, 30),
            endValue=QtCore.QPointF(100, 90),
            loopCount=-1,
        )
        self.animation.start()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())
  1. 修改动画使其不产生位移:
import sys
from PyQt5 import QtCore, QtWidgets


class CustomRect(QtWidgets.QGraphicsObject):
    def boundingRect(self):
        return QtCore.QRectF(100, 30, 100, 30)

    def paint(self, painter, styles, widget=None):
        painter.drawRect(self.boundingRect())


class Demo(QtWidgets.QGraphicsView):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(300, 300)

        self.scene = QtWidgets.QGraphicsScene()
        self.scene.setSceneRect(0, 0, 300, 300)

        self.rect = CustomRect()
        self.ellipse = QtWidgets.QGraphicsEllipseItem()
        self.ellipse.setRect(100, 180, 100, 50)
        self.scene.addItem(self.rect)
        self.scene.addItem(self.ellipse)

        self.setScene(self.scene)

        self.animation = QtCore.QPropertyAnimation(
            self.rect,
            b"pos",
            duration=1000,
            startValue=QtCore.QPointF(0, 0), # <---
            endValue=QtCore.QPointF(0, 60), # <---
            loopCount=-1,
        )
        self.animation.start()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())