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_())
Related
I tried to customize QGraphicsItem to make a pawn item that will be movable but will be centered on a case when dropped (modeling a checkers or chess board)
i encountered a problem at the very start as my custom graphic item does not move properly, when dragged it leaves a trail and the object disappear
import sys
from PyQt5.QtWidgets import (QApplication, QWidget, QGridLayout,
QGraphicsView, QGraphicsScene, QGraphicsItem)
from PyQt5.QtGui import QPen, QBrush, QTransform
from PyQt5.QtCore import Qt, QRectF, QPointF
class Pawn(QGraphicsItem):
def __init__(self, parent = None):
super().__init__(parent)
self.setFlag(QGraphicsItem.ItemIsMovable)
def paint(self, painter, options, widget):
painter.drawEllipse(0, 0, 30, 30)
def boundingRect(self):
return QRectF(self.x() - 10, self.y() - 10, 50, 50)
class MainWin(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
scene = QGraphicsScene()
view = QGraphicsView(scene, self)
view.setGeometry(0, 0, 290, 290)
ellipse = scene.addEllipse(200, 200, 20, 20, QPen(Qt.yellow), QBrush(Qt.yellow))
custom = Pawn()
scene.addItem(custom)
ellipse.setFlag(QGraphicsItem.ItemIsMovable)
self.setWindowTitle('doodling')
self.setGeometry(200, 200, 300, 300)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWin()
sys.exit(app.exec_())
The boundingRect() is a rectangle that is used to indicate the area where it should be painted, and this area is with respect to the item's coordinate system, and not the scene's coordinate system, so you should not use x() or y() since they are measured with respect to the coordinates of the scene.
def boundingRect(self):
return QRectF(-10, -10, 50, 50)
See Graphics View Framework for more information
stack overflow community. Let me explain my question refering to the code snipped below
from PyQt5 import QtCore, QtGui, QtWidgets
class PortItem(QtWidgets.QGraphicsPathItem):
def __init__(self, parent=None):
super().__init__(parent)
pen=QtGui.QPen(QtGui.QColor("black"), 2)
self.setPen(pen)
self.end_ports = []
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
class Symbol_Z(PortItem):
__partCounter=0
def __init__(self):
super().__init__()
self.__partName= "FixedTerms"
self.__partCounter+=1
self.drawSymbol()
def drawSymbol(self):
path=QtGui.QPainterPath()
path.moveTo(0, 40)
path.lineTo(20, 40)
path.addRect(QtCore.QRectF(20, 30, 40, 20))
path.moveTo(60, 40)
path.lineTo(80, 40)
path.addText(20, 25, QtGui.QFont('Times', 20), self.__partName)
self.setPath(path)
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, scene=None, parent=None):
super().__init__(scene, parent)
self.setRenderHints(QtGui.QPainter.Antialiasing)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
scene=QtWidgets.QGraphicsScene()
graphicsview=GraphicsView(scene)
item=Symbol_Z()
item.setPos(QtCore.QPointF(0, 250))
scene.addItem(item)
self.setCentralWidget(graphicsview)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
My problem is that the lines:
pen=QtGui.QPen(QtGui.QColor("black"), 2)
self.setPen(pen)
impact the line:
path.addText(20, 25, QtGui.QFont('Times', 20), self.__partName)
Do you know how to add text independet? When I tried to add it I had the problem that the drawing and the text are not connected and only the symbol could be mouved with the mouse and not both (drawing and text) together.
One possible solution is to create another QGraphicsPathItem that is the child of the item so the relative coordinates will not change and the parent's QPen will not affect him.
def drawSymbol(self):
path = QtGui.QPainterPath()
path.moveTo(0, 40)
path.lineTo(20, 40)
path.addRect(QtCore.QRectF(20, 30, 40, 20))
path.moveTo(60, 40)
path.lineTo(80, 40)
self.setPath(path)
text_item = QtWidgets.QGraphicsPathItem(self)
text_item.setBrush(QtGui.QColor("black"))
child_path = QtGui.QPainterPath()
child_path.addText(20, 25, QtGui.QFont("Times", 20), self.__partName)
text_item.setPath(child_path)
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 want to display an image in a QGraphicsView, actually in QGraphicsScene, this is the easy part, bu, when I move the cursor over the image, I want to see the X and Y coordinates lines (the yellow lines), like in this image, can anyone explain me how to do this?
To implement what you want there are 2 tasks:
Obtain the position of the cursor, for this case the flag mouseTracking is enabled so that mouseMoveEvent() is called where the position is obtained.
Paint on the top layer, for this we use the drawForeground() function.
from PyQt5 import QtCore, QtGui, QtWidgets
class GraphicsScene(QtWidgets.QGraphicsScene):
def drawForeground(self, painter, rect):
super(GraphicsScene, self).drawForeground(painter, rect)
if not hasattr(self, "cursor_position"):
return
painter.save()
pen = QtGui.QPen(QtGui.QColor("yellow"))
pen.setWidth(4)
painter.setPen(pen)
linex = QtCore.QLineF(
rect.left(),
self.cursor_position.y(),
rect.right(),
self.cursor_position.y(),
)
liney = QtCore.QLineF(
self.cursor_position.x(),
rect.top(),
self.cursor_position.x(),
rect.bottom(),
)
for line in (linex, liney):
painter.drawLine(line)
painter.restore()
def mouseMoveEvent(self, event):
self.cursor_position = event.scenePos()
self.update()
super(GraphicsScene, self).mouseMoveEvent(event)
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.setMouseTracking(True)
scene = GraphicsScene(QtCore.QRectF(-200, -200, 400, 400), self)
self.setScene(scene)
if __name__ == "__main__":
import sys
import random
app = QtWidgets.QApplication(sys.argv)
w = GraphicsView()
for _ in range(4):
r = QtCore.QRectF(
*random.sample(range(-200, 200), 2),
*random.sample(range(50, 150), 2)
)
it = w.scene().addRect(r)
it.setBrush(QtGui.QColor(*random.sample(range(255), 3)))
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
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_())