I am trying to draw a rect around the items that are selected in the scene (either via RubberBandDrag or ctrl+click for each item).
In order to do this I've subclassed QGraphicsScene and reimplemented the selectionChanged method to add a QGraphicsRectItem around the selected area, but for some reason, this method is not being called when items are selected or unselected in the scene. I've made sure that the items are in fact selectable.
Here is a minimal example of what I'm trying to do:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys
class DiagramScene(QGraphicsScene):
def __init__(self, parent=None):
super().__init__(parent)
self.selRect = None
def selectionChanged(self):
area = self.selectionArea().boundingRect()
pen = QPen()
pen.setColor(Qt.black)
pen.setStyle(Qt.DashLine)
self.selRect = self.addRect(area, pen)
if __name__ == "__main__":
app = QApplication(sys.argv)
view = QGraphicsView()
view.setDragMode(QGraphicsView.RubberBandDrag)
scene = DiagramScene()
scene.setSceneRect(0, 0, 500, 500)
rect1 = scene.addRect(20, 20, 100, 50)
rect2 = scene.addRect(80, 80, 100, 50)
rect3 = scene.addRect(140, 140, 100, 50)
rect1.setFlag(QGraphicsItem.ItemIsSelectable, True)
rect2.setFlag(QGraphicsItem.ItemIsSelectable, True)
rect3.setFlag(QGraphicsItem.ItemIsSelectable, True)
view.setScene(scene)
view.show()
sys.exit(app.exec_())
selectionChanged is a signal, not a method that you have to implement. What you need to do is to connect this signal to slot and your the implementation in the slot, so whenever the signal is emitted, your code gets executed:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys
class DiagramScene(QGraphicsScene):
def __init__(self, parent=None):
super().__init__(parent)
self.selRect = None
self.selectionChanged.connect(self.onSelectionChanged)
#pyqtSlot()
def onSelectionChanged(self):
area = self.selectionArea().boundingRect()
pen = QPen()
pen.setColor(Qt.black)
pen.setStyle(Qt.DashLine)
self.selRect = self.addRect(area, pen)
if __name__ == "__main__":
app = QApplication(sys.argv)
view = QGraphicsView()
view.setDragMode(QGraphicsView.RubberBandDrag)
scene = DiagramScene()
scene.setSceneRect(0, 0, 500, 500)
rect1 = scene.addRect(20, 20, 100, 50)
rect2 = scene.addRect(80, 80, 100, 50)
rect3 = scene.addRect(140, 140, 100, 50)
rect1.setFlag(QGraphicsItem.ItemIsSelectable, True)
rect2.setFlag(QGraphicsItem.ItemIsSelectable, True)
rect3.setFlag(QGraphicsItem.ItemIsSelectable, True)
view.setScene(scene)
view.show()
sys.exit(app.exec_())
Related
I know there are already lots of threads opened with this topic, I was trying to follow their recommendations, but I still struggle to achieve this.
Here is my initial code for window:
from PyQt5.QtWidgets import *
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Python ")
self.setGeometry(100, 100, 600, 400)
self.UiComponents()
self.show()
def UiComponents(self):
emphysemaLabel = QLabel("EMPHYSEMA", self)
emphysemaLabel.move(10, 10)
ggoLabel = QLabel("GGO", self)
ggoLabel.move(10, 300)
condensLabel = QLabel("Condens", self)
condensLabel.move(10, 590)
emphysema_graph_lin = QLabel(self)
emphysema_graph_lin.resize(302, 232)
emphysema_graph_lin.move(10, 50)
emphysema_graph_lin.setStyleSheet("background-color:yellow;")
ggo_graph_lin = QLabel(self)
ggo_graph_lin.resize(302, 232)
ggo_graph_lin.move(10, 340)
ggo_graph_lin.setStyleSheet("background-color:yellow;")
condens_graph_lin = QLabel(self)
condens_graph_lin.resize(302, 232)
condens_graph_lin.move(10, 630)
condens_graph_lin.setStyleSheet("background-color:yellow;")
if __name__ == "__main__":
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
An example if what I found useful and working is code found here https://www.pythonguis.com/tutorials/qscrollarea/
I tried to apply it to my code, like this:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Python ")
self.setGeometry(100, 100, 600, 400)
self.UiComponents()
self.show()
def UiComponents(self):
self.scroll = QScrollArea() # Scroll Area which contains the widgets, set as the centralWidget
self.widget = QWidget() # Widget that contains the collection of Vertical Box
self.vbox = QVBoxLayout() # The Vertical Box that contains the Horizontal Boxes of labels and buttons
emphysemaLabel = QLabel("EMPHYSEMA", self)
emphysemaLabel.move(10, 10)
self.vbox.addWidget(emphysemaLabel)
ggoLabel = QLabel("GGO", self)
ggoLabel.move(10, 300)
self.vbox.addWidget(ggoLabel)
condensLabel = QLabel("Condens", self)
condensLabel.move(10, 590)
self.vbox.addWidget(condensLabel)
emphysema_graph_lin = QLabel(self)
emphysema_graph_lin.resize(302, 232)
emphysema_graph_lin.move(10, 50)
emphysema_graph_lin.setStyleSheet("background-color:yellow;")
self.vbox.addWidget(emphysema_graph_lin)
ggo_graph_lin = QLabel(self)
ggo_graph_lin.resize(302, 232)
ggo_graph_lin.move(10, 340)
ggo_graph_lin.setStyleSheet("background-color:yellow;")
self.vbox.addWidget(ggo_graph_lin)
condens_graph_lin = QLabel(self)
condens_graph_lin.resize(302, 232)
condens_graph_lin.move(10, 630)
condens_graph_lin.setStyleSheet("background-color:yellow;")
self.vbox.addWidget(condens_graph_lin)
self.widget.setLayout(self.vbox)
# Scroll Area Properties
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll.setWidgetResizable(True)
self.scroll.setWidget(self.widget)
self.setCentralWidget(self.scroll)
if __name__ == "__main__":
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
But it's not working, how should I do it?
Thank you for any advice.
You can add a vertical spacer at the end of the layout using the addStretch() method of the QVBoxLayout object.
And adjust the maximum size to view the scroll working, setMaximumSize().
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Python ")
self.setGeometry(100, 100, 600, 400)
# set maximum size of the QMainWindow
self.setMaximumSize(600, 100)
self.UiComponents()
self.show()
def UiComponents(self):
self.scroll = QScrollArea() # Scroll Area which contains the widgets, set as the centralWidget
self.widget = QWidget() # Widget that contains the collection of Vertical Box
self.vbox = QVBoxLayout() # The Vertical Box that contains the Horizontal Boxes of labels and buttons
emphysemaLabel = QLabel("EMPHYSEMA", self)
emphysemaLabel.move(10, 10)
self.vbox.addWidget(emphysemaLabel)
ggoLabel = QLabel("GGO", self)
ggoLabel.move(10, 300)
self.vbox.addWidget(ggoLabel)
condensLabel = QLabel("Condens", self)
condensLabel.move(10, 590)
self.vbox.addWidget(condensLabel)
emphysema_graph_lin = QLabel(self)
emphysema_graph_lin.resize(302, 232)
emphysema_graph_lin.move(10, 50)
emphysema_graph_lin.setStyleSheet("background-color:yellow;")
self.vbox.addWidget(emphysema_graph_lin)
ggo_graph_lin = QLabel(self)
ggo_graph_lin.resize(302, 232)
ggo_graph_lin.move(10, 340)
ggo_graph_lin.setStyleSheet("background-color:yellow;")
self.vbox.addWidget(ggo_graph_lin)
condens_graph_lin = QLabel(self)
condens_graph_lin.resize(302, 232)
condens_graph_lin.move(10, 630)
condens_graph_lin.setStyleSheet("background-color:yellow;")
self.vbox.addWidget(condens_graph_lin)
# add a vertical spacer with addStretch() method
self.vbox.addStretch()
self.widget.setLayout(self.vbox)
# Scroll Area Properties
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll.setWidgetResizable(True)
self.scroll.setWidget(self.widget)
self.setCentralWidget(self.scroll)
if __name__ == "__main__":
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
I have a QFrame on my QWidget and after clicking QPushButton my
QFrame animates from (QRect(0, 0, 100, 100)) to (QRect(600, 500, 100,
100))
Question is: I want to know QFrame every x and y
position when it is animating
from PyQt5.QtWidgets import QWidget, QApplication, QFrame, QPushButton
from PyQt5.QtCore import QPropertyAnimation, QRect
from PyQt5.QtGui import QFont
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("Animation Window")
self.setGeometry(100, 100, 400, 400)
self.widgets()
self.show()
def widgets(self):
font = QFont("Times New Roman")
font.setPixelSize(20)
self.start = QPushButton("Start", self)
self.start.setFont(font)
self.start.setGeometry(100, 100, 100, 50)
self.start.clicked.connect(self.doAnimation)
self.frame = QFrame(self)
self.frame.setStyleSheet("background-color:darkGreen;")
self.frame.setFrameStyle(QFrame.Panel | QFrame.Raised)
self.frame.setGeometry(250, 100, 100, 100)
def doAnimation(self):
self.anim = QPropertyAnimation(self.frame, b"geometry")
self.anim.setDuration(10000)
self.anim.setStartValue(QRect(0, 0, 100, 100))
self.anim.setEndValue(QRect(600, 500, 100, 100))
self.anim.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
When an animation is running, it will trigger the signal valueChanged each time the animated value has changed:
def doAnimation(self):
self.anim = QPropertyAnimation(self.frame, b"geometry")
self.anim.valueChanged.connect(lambda: print(self.frame.pos()))
self.anim.setDuration(10000)
self.anim.setStartValue(QRect(0, 0, 100, 100))
self.anim.setEndValue(QRect(600, 500, 100, 100))
self.anim.start()
I have drawn a rectangle on QWidget after clicking push button rectangle moves from "LEFT TOP CORNER" to "RIGHT TOP CORNER"
How to move rectangle:
from "LEFT TOP CORNER" to "RIGHT TOP CORNER"
and "RIGHT TOP CORNER" to "RIGHT BOTTOM CORNER"
and "RIGHT BOTTOM CORNER" to "LEFT BOTTOM CORNER"
and "LEFT BOTTOM CORNER" to "LEFT TOP CORNER"
from PyQt5.QtWidgets import QWidget, QApplication, QFrame, QPushButton
from PyQt5.QtCore import QPropertyAnimation, QRect
from PyQt5.QtGui import QFont
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("Animation Window")
self.setGeometry(100, 100, 400, 400)
self.widgets()
self.show()
def widgets(self):
font = QFont("Times New Roman")
font.setPixelSize(20)
self.start = QPushButton("Start", self)
self.start.setFont(font)
self.start.setGeometry(100, 100, 100, 50)
self.start.clicked.connect(self.doAnimation)
self.frame = QFrame(self)
self.frame.setStyleSheet("background-color:darkGreen;")
self.frame.setFrameStyle(QFrame.Panel | QFrame.Raised)
self.frame.setGeometry(250, 100, 100, 100)
def doAnimation(self):
self.anim = QPropertyAnimation(self.frame, b"geometry")
self.anim.setDuration(10000)
self.anim.setStartValue(QRect(0, 0, 100, 100))
self.anim.setEndValue(QRect(1366, 0, 100, 100))
self.anim.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Although using the finished signal is interesting, a more compact and scalable method is to use QSequentialAnimationGroup where you can concatenate animations that will run sequentially.
from PyQt5 import QtCore, QtGui, QtWidgets
class Example(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("Animation Window")
self.setGeometry(100, 100, 400, 400)
self.widgets()
self.show()
def widgets(self):
font = QtGui.QFont("Times New Roman")
font.setPixelSize(20)
self.start = QtWidgets.QPushButton("Start", self)
self.start.setFont(font)
self.start.setGeometry(100, 100, 100, 50)
# self.start.clicked.connect(self.doAnimation)
self.frame = QtWidgets.QFrame(self)
self.frame.setStyleSheet("background-color:darkGreen;")
self.frame.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Raised)
self.frame.setGeometry(250, 100, 100, 100)
r = self.frame.rect()
rects = []
r.moveTopLeft(self.rect().topLeft())
rects.append(QtCore.QRect(r))
r.moveTopRight(self.rect().topRight())
rects.append(QtCore.QRect(r))
r.moveBottomRight(self.rect().bottomRight())
rects.append(QtCore.QRect(r))
r.moveBottomLeft(self.rect().bottomLeft())
rects.append(QtCore.QRect(r))
r.moveTopLeft(self.rect().topLeft())
rects.append(QtCore.QRect(r))
sequential_animation = QtCore.QSequentialAnimationGroup(self, loopCount=-1)
for rect_start, rect_end in zip(rects[:-1], rects[1:]):
animation = QtCore.QPropertyAnimation(
targetObject=self.frame,
propertyName=b"geometry",
startValue=rect_start,
endValue=rect_end,
duration=1000,
)
sequential_animation.addAnimation(animation)
self.start.clicked.connect(sequential_animation.start)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Example()
sys.exit(app.exec())
You can use signal to run next animation when first is finished
self.anim.finished.connect(self.doAnimation_2)
This code moves rectangle all time - last animation starts first animation.
from PyQt5.QtWidgets import QWidget, QApplication, QFrame, QPushButton
from PyQt5.QtCore import QPropertyAnimation, QRect
from PyQt5.QtGui import QFont
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("Animation Window")
self.setGeometry(100, 100, 400, 400)
self.widgets()
self.show()
def widgets(self):
font = QFont("Times New Roman")
font.setPixelSize(20)
self.start = QPushButton("Start", self)
self.start.setFont(font)
self.start.setGeometry(100, 100, 100, 50)
self.start.clicked.connect(self.doAnimation_1)
self.frame = QFrame(self)
self.frame.setStyleSheet("background-color:darkGreen;")
self.frame.setFrameStyle(QFrame.Panel | QFrame.Raised)
self.frame.setGeometry(250, 100, 100, 100)
def doAnimation_1(self):
self.anim = QPropertyAnimation(self.frame, b"geometry")
self.anim.setDuration(1000)
self.anim.setStartValue(QRect(0, 0, 100, 100))
self.anim.setEndValue(QRect(300, 0, 100, 100))
self.anim.finished.connect(self.doAnimation_2)
self.anim.start()
def doAnimation_2(self):
self.anim = QPropertyAnimation(self.frame, b"geometry")
self.anim.setDuration(1000)
self.anim.setStartValue(QRect(300, 0, 100, 100))
self.anim.setEndValue(QRect(300, 300, 100, 100))
self.anim.finished.connect(self.doAnimation_3)
self.anim.start()
def doAnimation_3(self):
self.anim = QPropertyAnimation(self.frame, b"geometry")
self.anim.setDuration(1000)
self.anim.setStartValue(QRect(300, 300, 100, 100))
self.anim.setEndValue(QRect(0, 300, 100, 100))
self.anim.finished.connect(self.doAnimation_4)
self.anim.start()
def doAnimation_4(self):
self.anim = QPropertyAnimation(self.frame, b"geometry")
self.anim.setDuration(1000)
self.anim.setStartValue(QRect(0, 300, 100, 100))
self.anim.setEndValue(QRect(0, 0, 100, 100))
self.anim.finished.connect(self.doAnimation_1)
self.anim.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
If you will have to run different animations in different moments then probably you could use QTimer
I have two questions:
what does QTransform() mean in itemAt()? The sentence below is what it says in Qt doc, but I can't understand:
deviceTransform is the transformation that applies to the view, and needs to be provided if the scene contains items that ignore transformations.
why focusItemChanged signal is not working?
Here is my code:
import sys
from PyQt5.QtGui import QTransform
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsScene, QGraphicsView
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 = self.scene.addRect(100, 30, 100, 30)
self.ellipse = self.scene.addEllipse(100, 80, 50, 40)
self.rect.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
self.ellipse.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
self.setScene(self.scene)
# Question 1
print(self.scene.itemAt(110, 40, QTransform()))
# Question 2
self.scene.focusItemChanged.connect(self.my_slot)
def my_slot(self, new_item, old_item):
print(new_item)
print(old_item)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
Any help would be appreciated.
1. what does QTransform() mean in itemAt()?
As it indicates the docs it is only necessary to pass the deviceTransform if there is an item that ignores the transformations, then how is it done so that an item does not support transformation? you must enable the flag Qt::ItemIgnoresTransformations.
With your code you can not see the difference so I have implemented the following example where there are 2 items one with the flag ItemIgnoresTransformations activated and the other not. Then when you press any of the items it is expected that the item will be printed in the console but you will see that the item that has the ItemIgnoresTransformations flag returns None if you pass QTransform (), if you press the radiobutton to pass the viewportTransform() you will see that Now both items are printed on the console. So you must pass the deviceTransform if there is any item with the flag ItemIgnoresTransformations enabled.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Demo(QtWidgets.QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self._scene = QtWidgets.QGraphicsScene()
self._scene.setSceneRect(0, 0, 300, 300)
self.setScene(self._scene)
self.rect1 = self._scene.addRect(
100, 30, 100, 30, brush=QtGui.QBrush(QtGui.QColor("red"))
)
self.rect1.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations)
self.rect2 = self._scene.addRect(
200, 30, 100, 30, brush=QtGui.QBrush(QtGui.QColor("green"))
)
self.rotate(50)
self._use_deviceTransform = False
def mousePressEvent(self, event):
sp = self.mapToScene(event.pos())
item = self._scene.itemAt(
sp,
self.viewportTransform()
if self._use_deviceTransform
else QtGui.QTransform(),
)
print(item)
def set_use_deviceTransform(self, t):
self._use_deviceTransform = t
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
radiobutton = QtWidgets.QRadioButton("use deviceTransform")
demo = Demo()
radiobutton.toggled.connect(demo.set_use_deviceTransform)
w = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(w)
lay.addWidget(radiobutton)
lay.addWidget(demo)
w.show()
w.resize(640, 480)
sys.exit(app.exec_())
2. why focusItemChanged signal is not working?
The signal is triggered if the focus of an item changes, but by default the items have no focus so the signal is not emitted, the solution is to activate the flag QGraphicsItem::ItemIsFocusable:
import sys
from PyQt5.QtGui import QTransform
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsScene, QGraphicsView
from PyQt5.QtCore import pyqtSlot, Qt
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 = self.scene.addRect(100, 30, 100, 30)
self.ellipse = self.scene.addEllipse(100, 80, 50, 40)
self.rect.setFlags(
QGraphicsItem.ItemIsMovable
| QGraphicsItem.ItemIsSelectable
| QGraphicsItem.ItemIsFocusable
)
self.ellipse.setFlags(
QGraphicsItem.ItemIsMovable
| QGraphicsItem.ItemIsSelectable
| QGraphicsItem.ItemIsFocusable
)
self.setScene(self.scene)
self.scene.focusItemChanged.connect(self.my_slot)
#pyqtSlot("QGraphicsItem*", "QGraphicsItem*", Qt.FocusReason)
def my_slot(self, new_item, old_item, reason):
print(old_item, new_item)
I want to use QPropertyAnimation on QGraphicsItem, hoping the rect item can move from point(100, 30) to point(100, 90). But why the rect is moving itself on the right side of the window? The x coordinate 100 should make the rect move at the middle according to the Scene's size.
Here is my code:
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_())
It seems that they do not know the different coordinate systems of the Graphics View Framework.
In this system there are at least the following coordinate systems:
The coordinate system of the window(viewport()) where the (0, 0) will always be the top-left of the window.
The coordinate system of the scene, this is with respect to some pre-established point.
The coordinate coordinate system of each item, this coordinate system is used by the paint() method to do the painting, and the boundingRect() and shape() methods to obtain the edges of the item.
You also have to have another concept, the position of an item is with respect to the parent if he has it, if he does not have it, it is with respect to the scene.
Analogy
To explain the different coordinate systems I use the analogy of recording a scene using a camera.
The QGraphicsView would be the screen of the camera.
The QGraphicsScene is the scene that is recorded, so the point (0, 0) is some point that is convenient.
The QGraphicsItem are the elements of the scene, their position can be relative to other items or to the scene, for example we can consider the position of the actor's shoes with respect to the actor, or the item can be the same actor.
Based on the above I will explain what happens and we will give several solutions.
The rect item has no parent and by default the posicon of an item is (0, 0) so at that moment the coordinate system of the item and the scene coincide so the boundingRect will visually define the position and as you have placed QRectF(100, 30, 100, 30) this will be drawn in that position that coincidentally will be the same in the scene. But when you apply the animation the first thing that will be done is to set the position of the item to (100, 30) so that since the coordinate systems of the scene and the item do not match, one is displaced from the other, so the boundingRect no longer matches the QRectF(100, 30, 100, 30) of the scene, but will move in the same factor (only because there is a displacement, there is no scaling or rotation) and the rectangle will be QRectF(200, 60, 100, 30) and with respect to the ellipse that was always in the QRect(100, 180, 100, 50) so rectangle is on the right since 200>100 and it is up since 60<180.
So if you want the rectangle to be on top of the ellipse there are at least 2 solutions:
Modify the boundingRect so that it is in position 0,0 so that with the displacement caused by the animation it makes them match:
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_())
Modify the animation so that it does not generate the displacement:
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_())
Try it:
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_())