pyside/pyqt how to animate an arc simply? - python

I'm looking for a solution, to animate this arc from 0 - 360°. I'm relative new to Pyside/Pyqt and I don't find such a simple solution (only beginner "unfriedly"). I tried it with while loops aswell, but it doesn't works. At the moment I don't understand this animation system, but I want to work on it.
import sys
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import Qt
from PySide6.QtGui import QBrush, QPen, QPainter
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("AnimateArc")
self.setGeometry(100, 100, 600, 600)
def paintEvent(self, event):
self.anim = QtCore.QPropertyAnimation(self, b"width", duration=1000) #<- is there a documentation for b"width", b"geometry"?
self.anim.setStartValue(0)
start = 0
painter = QPainter(self)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.drawArc(100, 100, 400, 400, 90 * 16, start * 16) # I want to make the change dynamicly
self.anim.setEndValue(360)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()

QPropertyAnimation is used to animate Qt properties of any QObject. If you refer to self (the current instance of QMainWindow), then you can animate all properties of a QMainWindow and all inherited properties (QMainWindow inherits from QWidget, so you can animate all the QWidget properties as well).
In your case, you're trying to animate the width property of the window, and that's certainly not what you want to do.
Since what you want to change is a value that is not a property of the window, you cannot use QPropertyAnimation (unless you create a Qt property using the #Property decorator), and you should use a QVariantAnimation instead.
Then, a paintEvent is called by Qt every time the widget is going to be drawn (which can happen very often), so you cannot create the animation there, otherwise you could end up with a recursion: since the animation would require a repaint, you would create a new animation everytime the previous requires an update.
Finally, consider that painting on a QMainWindow is normally discouraged, as a Qt main window is a special kind of QWidget intended for advanced features (menus, status bar, etc) and uses a central widget to show the actual contents.
The correct approach is to create and set a central widget, and implement the painting on that widget instead.
Here is a revised and working version of your code:
class ArcWidget(QWidget):
def __init__(self):
super().__init__()
self.anim = QtCore.QVariantAnimation(self, duration=1000)
self.anim.setStartValue(0)
self.anim.setEndValue(360)
self.anim.valueChanged.connect(self.update)
self.anim.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.drawArc(
100, 100, 400, 400, 90 * 16, self.anim.currentValue() * 16)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("AnimateArc")
self.setGeometry(100, 100, 600, 600)
self.arcWidget = ArcWidget()
self.setCentralWidget(self.arcWidget)
The valueChanged connection ensures that everytime the value changes the widget schedules an update (thus calling a paintEvent as soon as the event queue allows it), then you can use the current value of the animation to draw the actual arc.

Thanks #musicamante for the solution regarding animating arc using QAnimationProperty
Modified #musicmante code to create a loading effect guess it will help developers and might save their time who are trying to make loading effect using Qt
Source
#!/usr/bin/env python3.10
import sys
import string
import random
from PySide6.QtWidgets import (QMainWindow, QPushButton, QVBoxLayout,
QApplication, QWidget)
from PySide6.QtCore import (Qt, QVariantAnimation)
from PySide6.QtGui import (QPen, QPainter, QColor)
class Arc:
colors = list(string.ascii_lowercase[0:6]+string.digits)
shades_of_blue = ["#7CB9E8","#00308F","#72A0C1", "#F0F8FF",
"#007FFF", "#6CB4EE", "#002D62", "#5072A7",
"#002244", "#B2FFFF", "#6F00FF", "#7DF9FF","#007791",
"#ADD8E6", "#E0FFFF", "#005f69", "#76ABDF",
"#6A5ACD", "#008080", "#1da1f2", "#1a1f71", "#0C2340"]
shades_of_green = ['#32CD32', '#CAE00D', '#9EFD38', '#568203', '#93C572',
'#8DB600', '#708238', '#556B2F', '#014421', '#98FB98', '#7CFC00',
'#4F7942', '#009E60', '#00FF7F', '#00FA9A', '#177245', '#2E8B57',
'#3CB371', '#A7F432', '#123524', '#5E8C31', '#90EE90', '#03C03C',
'#66FF00', '#006600', '#D9E650']
def __init__(self):
self.diameter = random.randint(100, 600)
#cols = list(Arc.colors)
#random.shuffle(cols)
#_col = "#"+''.join(cols[:6])
#print(f"{_col=}")
#self.color = QColor(_col)
#self.color = QColor(Arc.shades_of_blue[random.randint(0, len(Arc.shades_of_blue)-1)])
self.color = QColor(Arc.shades_of_green[random.randint(0, len(Arc.shades_of_green)-1)])
#print(f"{self.color=}")
self.span = random.randint(40, 150)
self.direction = 1 if random.randint(10, 15)%2 == 0 else -1
self.startAngle = random.randint(40, 200)
self.step = random.randint(100, 300)
class ArcWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.arcs = [Arc() for i in range(random.randint(10, 20))]
self.startAnime()
def initUI(self):
#self.setAutoFillBackground(True)
self.setAttribute(Qt.WA_StyledBackground, True)
self.setStyleSheet("background-color:black;")
def startAnime(self):
self.anim = QVariantAnimation(self, duration = 2000)
self.anim.setStartValue(0)
self.anim.setEndValue(360)
self.anim.valueChanged.connect(self.update)
self.anim.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
#painter.setPen(QPen(QColor("#b6faec"), 5, Qt.SolidLine))
#painter.drawArc(
# 100, 100, 400, 400, 90*16,
# self.anim.currentValue() * 16)
#width = 400
#height = 400
#painter.drawArc(self.width()/2 -width/2, self.height()/2 - height/2, 400, 400, self.anim.currentValue()*16, 45*16)
for arc in self.arcs:
painter.setPen(QPen(arc.color, 6, Qt.SolidLine))
painter.drawArc(self.width()/2 - arc.diameter/2,
self.height()/2 - arc.diameter/2, arc.diameter,
arc.diameter, self.anim.currentValue()*16*arc.direction+arc.startAngle*100, arc.span*16)
#print(f"currentValue : {self.anim.currentValue()}")
#arc.startAngle = random.randint(50, 200)
if self.anim.currentValue() == 360:
#print("360")
self.startAnime()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Animate Arc")
self.setGeometry(100, 100, 600, 600)
self.arcWidget = ArcWidget()
self.setCentralWidget(self.arcWidget)
if __name__ == "__main__":
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
app.exec()
output:
$ ./arc_widget.py

Related

Why my program is closing when I am using Slider in pyqt5?

Why my program is closing when I am using Slider in pyqt5? It starts normal but after using Slider it close. I have to change size of circle in first window by using Slider from second window. This is my full code, beacuse I don't know what could be wrong.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QPainter, QBrush, QPen
from PyQt5.QtCore import Qt
import sys
class ThirdWindow(QWidget):
def __init__(self):
super().__init__()
hbox = QHBoxLayout()
sld = QSlider(Qt.Horizontal, self)
sld.setRange(0, 100)
sld.setFocusPolicy(Qt.NoFocus)
sld.setPageStep(5)
sld.valueChanged.connect(self.updateLabel)
self.label = QLabel('0', self)
self.label.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
self.label.setMinimumWidth(80)
hbox.addWidget(sld)
hbox.addSpacing(15)
hbox.addWidget(self.label)
self.setLayout(hbox)
self.setGeometry(600, 60, 500, 500)
self.setWindowTitle('QSlider')
self.show()
def updateLabel(self, value):
self.label.setText(str(value))
self.parentWidget().radius = value
self.parentWidget().repaint()
class Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setWindowTitle("Dialogi")
self.w = ThirdWindow()
actionFile = self.menuBar().addMenu("Dialog")
action = actionFile.addAction("Zmień tło")
action1 = actionFile.addAction("Zmień grubość koła")
action1.triggered.connect(self.w.show)
self.setGeometry(100, 60, 300, 300)
self.setStyleSheet("background-color: Green")
self.radius = 100 # add a variable radius to keep track of the circle radius
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.gray, 8, Qt.SolidLine))
# change function to include radius
painter.drawEllipse(100, 100, self.radius, self.radius)
app = QApplication(sys.argv)
screen = Window()
screen.show()
sys.exit(app.exec_())
parentWidget allows access to a widget declared as parent for Qt.
Creating an object using self.w = ... doesn't make that object a child of the parent (self), you are only creating a reference (w) to it.
If you run your program in a terminal/prompt you'll clearly see the following traceback:
Exception "unhandled AttributeError"
'NoneType' object has no attribute 'radius'
The NoneType refers to the result of parentWidget(), and since no parent has been actually set, it returns None.
While you could use parentWidget if you correctly set the parent for that widget (by adding the parent to the widget constructor, or using setParent()), considering the structure of your program it wouldn't be a good choice, and it's usually better to avoid changes to a "parent" object directly from the "child": the signal/slot mechanism of Qt is exactly intended for the modularity of Object Oriented Programming, as a child should not really "care" about its parent.
The solution is to connect to the slider from the "parent", and update it from there.
class ThirdWindow(QWidget):
def __init__(self):
# ...
# make the slider an *instance member*, so that we can easily access it
# from outside
self.slider = QSlider(Qt.Horizontal, self)
# ...
class Window(QMainWindow):
def __init__(self):
# ...
self.w.slider.valueChanged.connect(self.updateLabel)
def updateLabel(self, value):
self.radius = value
self.update()
You obviously need to remove the valueChanged connection in the ThirdWindow class, as you don't need it anymore.
Also note that you rarely need repaint(), and update() should be preferred instead.

Draw centered circle and implement an object (bitmp) that moves along the circle

I created a new UI window and drew a circle. I would like to have a small figure (think small stick figure) move along the perimeter of the circle at a certain rate. The rate of movement is probably easy enough to change. How do I have an object move along the perimeter of the circle?
from PyQt5 import QtGui
from PyQt5.QtWidgets import QApplication, QMainWindow
import sys
from PyQt5.QtGui import QPainter, QPen, QBrush
from PyQt5.QtCore import Qt
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.title = "Drawing Ellipse"
self.top = 200
self.left = 200
self.width = 750
self.height = 750
self.InitWindow()
def InitWindow(self):
self.setWindowIcon(QtGui.QIcon("icon.png"))
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.drawEllipse(200,200, 400,400)
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
First of all, there are some issues with your code:
the circle is not correctly centered: if a centered circle positioned at (200, 200) has a 400 diameter, the window should have a width and height equal to 800; but this is not a good choice, as the window could be resized (by the user or the OS), and if you want to ensure that it is actually centered, you need to adapt the rect used for the painting to the current size;
no painting should ever happen in a QMainWindow unless absolutely required: a Qt main window uses a central widget to show its main contents, and if you want to do any "main" custom painting, it should happen in that central widget; there are very few cases for which a paintEvent should be implemented in a QMainWindow, but they are very only for specific and very advanced scenarios;
a QMainWindow has an internal layout that takes into account possible features such as a menu bar, a status bar, and possibly some amount of toolbars and dock widgets, so you cannot rely on a fixed geometry for the painting, as you risk that the painting would be partially hidden by other objects;
existing class properties should never, ever be overwritten; I'm specifically referring to self.width and self.height, which are existing dynamic properties of any QWidget subclass, and should never be overwritten; I know that there are some tutorials that use that pattern (I've already warned some bloggers about this), but you should not follow those suggestions;
Considering the above, the solution is to create a QWidget subclass, and implement the paintEvent of that class. Then, using a QVariantAnimation allows to create a smooth animation that can be used to correctly position the moving image on the perimeter.
In the following example I'm using a QPainterPath with a simple ellipse, which allows me to get the position using the pointAtPercent() function.
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtGui import QIcon, QPainter, QPen, QPixmap, QPainterPath
from PyQt5.QtCore import Qt, QRectF, QPoint, QPointF, QVariantAnimation
class CirclingWidget(QWidget):
offset = QPoint()
def __init__(self, diameter=1, relative=True, sprite=None,
anglesPerSecond=360, clockwise=True, offset=None, alignment=None):
super().__init__()
self.diameter = diameter
self.relative = relative
self.clockwise = clockwise
self.setSprite(sprite, offset or alignment)
self.animation = QVariantAnimation(startValue=0., endValue=1.,
duration=360000 / anglesPerSecond, loopCount=-1)
self.animation.valueChanged.connect(self.update)
def setSprite(self, sprite, arg):
if isinstance(sprite, str):
sprite = QPixmap(sprite)
if isinstance(sprite, QPixmap) and not sprite.isNull():
self.sprite = sprite
if isinstance(arg, Qt.AlignmentFlag):
self.setAlignment(arg)
else:
self.setOffset(arg)
else:
self.sprite = None
self.update()
def setAlignment(self, alignment=None):
if self.sprite:
x = y = 0
if alignment is not None:
if alignment & Qt.AlignHCenter:
x = -self.sprite.width() / 2
elif alignment & Qt.AlignRight:
x = -self.sprite.width()
if alignment & Qt.AlignVCenter:
y = -self.sprite.height() / 2
elif alignment & Qt.AlignBottom:
y = -self.sprite.height()
self.offset = QPointF(x, y)
self.update()
def setOffset(self, offset=None):
if self.sprite:
x = y = 0
if isinstance(offset, int):
x = y = offset
elif isinstance(offset, float):
x = self.sprite.width() * offset
y = self.sprite.height() * offset
elif isinstance(offset, (QPoint, QPointF)):
x = offset.x()
y = offset.y()
elif isinstance(offset, (tuple, list)):
x, y = offset
self.offset = QPointF(x, y)
self.update()
def setAnglesPerSecond(self, ratio):
self.animation.setDuration(360000 / ratio)
def setClockwise(self, clockwise=True):
if self.clockwise != clockwise:
self.clockwise = clockwise
self.animation.setCurrentTime(
self.animation.duration() - self.animation.currentTime())
self.update()
def start(self, angle=0):
self.animation.start()
# the angle is considered clockwise, starting from 12h
start = (angle - 90) / 360 % 1
self.animation.setCurrentTime(start * self.animation.duration())
self.update()
def stop(self):
self.animation.stop()
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHints(painter.Antialiasing)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
size = self.diameter
if self.relative:
size *= min(self.width(), self.height())
rect = QRectF(0, 0, size, size)
rect.moveCenter(QRectF(self.rect()).center())
path = QPainterPath()
path.addEllipse(rect)
painter.drawPath(path)
if self.sprite:
pos = self.animation.currentValue()
if not self.clockwise:
pos = 1 - pos
pos = path.pointAtPercent(pos)
painter.drawPixmap(pos + self.offset, self.sprite)
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.title = "Drawing Ellipse"
self.top = 200
self.left = 200
self._width = 800
self._height = 800
self.InitWindow()
def InitWindow(self):
self.setWindowIcon(QIcon("icon.png"))
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self._width, self._height)
self.circleWidget = CirclingWidget(.5, sprite='image.png',
anglesPerSecond=60, alignment=Qt.AlignCenter)
self.setCentralWidget(self.circleWidget)
self.circleWidget.start(90)
self.show()
if __name__ == '__main__':
import sys
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
Finally, avoid mixed import styles. The following lines might lead to confusion:
from PyQt5 import QtGui
from PyQt5.QtGui import QPainter, QPen, QBrush
You either import QtGui, or you import the individual classes of that submodule.

How to draw with QPainter on top of already placed QLabel or QPixmap?

While experimenting with Python and PyQt5 I got stuck on a problem. I have in my GUI few labels (QLabel) and images (QPixmap) and I want to draw something on them, depending on what the main program does. I can't figure out how though. For example, I change text on labels calling setLabels() from class BinColUI and I would like to draw something on them (i.e. QPainter.drawLine()) just after that. What I tried is not working, there's nothing drawn. My unsuccesful attempt is commented out in setLabels().
How do I do it?
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
class BinColUI(QMainWindow):
def __init__(self):
super().__init__()
self.initUi()
self.createLabelTop()
self.createLabelBot()
def initUi(self):
self.setWindowTitle('Bin Collection')
self.setFixedSize(500, 500)
# self.setStyleSheet('background-color: white')
self.generalLayout = QVBoxLayout()
self._centralWidget = QWidget(self)
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
def paintEvent(self, event):
self.qp = QPainter()
self.qp.begin(self)
self.drawLine(event, self.qp)
self.qp.end()
def drawLine(self, event, qp):
pen = QPen(Qt.red, 3, Qt.SolidLine)
qp.setPen(pen)
qp.drawLine(5, 5, 495, 5)
qp.drawLine(495, 5, 495, 495)
qp.drawLine(495, 495, 5, 495)
qp.drawLine(5, 495, 5, 5)
def createLabelTop(self):
self.label_top = QLabel('PLEASE WAIT')
self.label_top.setAlignment(Qt.AlignCenter)
self.label_top.setFixedSize(450, 60)
self.label_top.setStyleSheet("font: 14pt Bahnschrift; color: black; background-color: yellow")
self.generalLayout.addWidget(self.label_top, alignment=Qt.AlignCenter)
def createLabelBot(self):
self.label_bot = QLabel('PLEASE WAIT')
self.label_bot.setAlignment(Qt.AlignCenter)
self.label_bot.setFixedSize(450, 60)
self.label_bot.setStyleSheet("font: 14pt Bahnschrift; color: black; background-color: yellow")
self.generalLayout.addWidget(self.label_bot, alignment=Qt.AlignCenter)
def setLabels(self, texttop, textbot):
# qp = QPainter(self.label_top)
self.label_top.setText(texttop)
self.label_bot.setText(textbot)
# pen = QPen(Qt.red, 3)
# qp.setPen(pen)
# qp.drawLine(10, 10, 50, 50)
# self.label_top.repaint()
class BinColCtrl:
def __init__(self, view: BinColUI):
self._view = view
self.calculateResult()
def calculateResult(self):
line_top = 'NEW LABEL TOP'
line_bottom = 'NEW LABEL BOTTOM'
self._view.setLabels(line_top, line_bottom)
def main():
"""Main function."""
# Create an instance of `QApplication`
bincol = QApplication(sys.argv)
window = BinColUI()
window.show()
BinColCtrl(view=window)
sys.exit(bincol.exec_())
if __name__ == '__main__':
main()
In general, the painting of a QWidget (QLabel, QPushButton, etc.) should only be done in the paintEvent method as the OP seems to know. And that painting depends on the information that the widget has, for example QLabel uses a text and draws the text, OR uses a QPixmap and draws based on that pixmap. So in this case you must create a QPixmap where the line is painted, and pass that QPixmap to the QLabel to paint it.
def setLabels(self, texttop, textbot):
pixmap = QPixmap(self.label_top.size())
pixmap.fill(Qt.transparent)
qp = QPainter(pixmap)
pen = QPen(Qt.red, 3)
qp.setPen(pen)
qp.drawLine(10, 10, 50, 50)
qp.end()
self.label_top.setPixmap(pixmap)
self.label_bot.setText(textbot)
Update:
I can't have text and drawn line on the label?
As I already pointed out in the initial part of my answer: Either you paint a text or you paint a QPixmap, you can't do both in a QLabel.
Can I draw line then text on it using QPainter.drawText()?
Yes, you can use all the methods to paint the text in the QPixmap: be creative :-). For example:
def setLabels(self, texttop, textbot):
pixmap = QPixmap(self.label_top.size())
pixmap.fill(Qt.transparent)
qp = QPainter(pixmap)
pen = QPen(Qt.red, 3)
qp.setPen(pen)
qp.drawLine(10, 10, 50, 50)
qp.drawText(pixmap.rect(), Qt.AlignCenter, texttop)
qp.end()
self.label_top.setPixmap(pixmap)
self.label_bot.setText(textbot)

QPainter in pyqt5 is not painting anything when calling repaint() in a loop. How do I fix this?

I am totally new to PyQt. I want to do animation using PyQt5 .This is a simple test I am doing , so I am just trying to move a rectangle from top to the bottom of the window. Here's a gist of what I am doing to achieve this.
1. I have put whatever I wanted to paint inside paintEvent() method. I have painted the rectangle using variables not constant values
2. I have also created a update() function to update all the variables
3. I have created a loop function which calls self.update() and self.repaint() every 100 milliseconds
import sys
import random
from PyQt5.QtWidgets import ( QApplication, QWidget, QToolTip, QMainWindow)
from PyQt5.QtGui import QPainter, QBrush, QPen, QColor, QFont
from PyQt5.QtCore import Qt, QDateTime
class rain_animation(QMainWindow):
def __init__(self):
super().__init__()
self.painter = QPainter()
""" Variables for the Window """
self.x = 50
self.y = 50
self.width = 500
self.height = 500
"""Variables for the rain"""
self.rain_x = self.width/2
self.rain_y = 0
self.rain_width = 5
self.rain_height = 30
self.rain_vel_x = 0
self.rain_vel_y = 5
self.start()
self.loop()
def paintEvent(self, a0):
self.painter.begin(self)
# Draw a White Background
self.painter.setPen(QPen(Qt.white, 5, Qt.SolidLine))
self.painter.setBrush(QBrush(Qt.white, Qt.SolidPattern))
self.painter.drawRect(0, 0, self.width, self.height)
#Draw the rain
self.painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine))
self.painter.setBrush(QBrush(Qt.blue, Qt.SolidPattern))
self.painter.drawRect(self.rain_x, self.rain_y, self.rain_width, self.rain_height)
self.painter.end(self)
def update(self, diff):
self.rain_x += self.rain_vel_x
self.rain_y += self.rain_vel_y
def start(self):
self.setWindowTitle("Rain Animation")
self.setGeometry(self.x, self.y, self.width, self.height)
self.show()
def loop(self):
start = QDateTime.currentDateTime()
while True :
diff = start.msecsTo(QDateTime.currentDateTime())
if diff >= 100 :
print("time : {0} ms rain_x : {1} rain_y : {2}".format(diff, self.rain_x, self.rain_y))
start = QDateTime.currentDateTime()
self.update(diff)
self.repaint()
if __name__ == "__main__":
app = QApplication(sys.argv)
animation = rain_animation()
sys.exit(app.exec_())
What I should see is a rectangle moving from the top of window to the bottom of the screen but all I see is a window with a black background.
The loop() function seems working properly since the data I am printing shows that the variables are being updated every 100 milliseconds.
Though the problem seems to be something in the loop() function since after removing the self.loop() I can see a static picture of the blue box with a white background at the top of the window.
Problem:
Having a continuous loop does not allow the GUI to perform tasks such as painting, interaction with the OS, etc. Each GUI provides a way to make animations in a way that does not block the window.
Qt provides various classes that allow you to implement the animation as:
QTimer,
QTimeLine,
QVariantAnimation,
QPropertyAnimation.
On the other hand it is recommended that:
Do not create a QPainter outside of paintEvent if it is going to be responsible for the GUI painting.
Use the update() method (not your method but the one that provides Qt) instead of repaint, in case repaint will force the window to paint sometimes unnecessarily, instead update() will do it when necessary, remember that the painted is done with the refresh rate of the screen (60 Hz). For example, if you call repaint 5 times in 20 ms then paintEvent() will be called 3 times but the painting on the screen is every 16.6ms so you only need 1 paint, in the case of update() if you consider it.
Considering the above, it is best to use a QPropertyAnimation:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class RainAnimation(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Rain Animation")
self.setGeometry(50, 50, 500, 500)
self.m_rect_rain = QtCore.QRect()
animation = QtCore.QPropertyAnimation(
self,
b"rect_rain",
parent=self,
startValue=QtCore.QRect(self.width() / 2, 0, 5, 30),
endValue=QtCore.QRect(self.width() / 2, self.height() - 30, 5, 30),
duration=5 * 1000,
)
animation.start()
def paintEvent(self, a0):
painter = QtGui.QPainter(self)
# Draw a White Background
painter.setPen(QtGui.QPen(QtCore.Qt.white, 5, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect())
#Draw the rain
painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect_rain)
#QtCore.pyqtProperty(QtCore.QRect)
def rect_rain(self):
return self.m_rect_rain
#rect_rain.setter
def rect_rain(self, r):
self.m_rect_rain = r
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = RainAnimation()
w.show()
sys.exit(app.exec_())
Another option is use QVarianAnimation:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class RainAnimation(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Rain Animation")
self.setGeometry(50, 50, 500, 500)
self.m_rect_rain = QtCore.QRect()
animation = QtCore.QVariantAnimation(
parent=self,
startValue=QtCore.QRect(self.width() / 2, 0, 5, 30),
endValue=QtCore.QRect(self.width() / 2, self.height() - 30, 5, 30),
duration=5 * 1000,
valueChanged=self.set_rect_rain,
)
animation.start()
def paintEvent(self, a0):
painter = QtGui.QPainter(self)
# Draw a White Background
painter.setPen(QtGui.QPen(QtCore.Qt.white, 5, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect())
# Draw the rain
painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue, QtCore.Qt.SolidPattern))
painter.drawRect(self.m_rect_rain)
#QtCore.pyqtSlot(QtCore.QVariant)
def set_rect_rain(self, r):
self.m_rect_rain = r
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = RainAnimation()
w.show()
sys.exit(app.exec_())
The following example is using your logic but with a QTimer:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class RainAnimation(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Rain Animation")
self.setGeometry(50, 50, 500, 500)
self.m_rect_rain = QtCore.QRect(self.width() / 2, 0, 5, 30)
timer = QtCore.QTimer(self, timeout=self.update_rain, interval=100)
timer.start()
def paintEvent(self, a0):
painter = QtGui.QPainter(self)
# Draw a White Background
painter.setPen(QtGui.QPen(QtCore.Qt.white, 5, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect())
# Draw the rain
painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue, QtCore.Qt.SolidPattern))
painter.drawRect(self.m_rect_rain)
#QtCore.pyqtSlot()
def update_rain(self):
self.m_rect_rain.moveTop(self.m_rect_rain.top() + 5)
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = RainAnimation()
w.show()
sys.exit(app.exec_())

expand the size of QPaintDevice after it is initialzed

A little Context
I'm trying to make a QSplashScreen with a custom animation. I've tried many different approaches, with each failures. Mostly, my main technique was to create a new Class which inherits from QSplashScreen and then plan with the paintEvent(). It worked... ish. The animation is not the problem, It is really the QPaintDevice which seems corrupted.
Because I was calling the super(classname, self).__init__(args) only way after in my init and passing it arguments that I modified in the init, I always had corrupted pixels; The image was in weird tones and had lines of colorful pixels in the background. Sometimes it is patterns sometimes it is completely random.
I have tried changing every line of code and the only thing that removed those lines was calling the super() at the beginning of the __init__. Unfortunately, I was making a frame that I was passing to the init. Now that this isn't possible, I would like to modify the size of the QPaintDevice on which my QSplashScreen initializes, because my animation is displayed beyond that frame. I won't post all the code since the custom animation is quite heavy.
Minimal Working Exemple
from PyQt5.QtWidgets import QApplication, QSplashScreen, QMainWindow
from PyQt5.QtCore import Qt, QSize, pyqtSignal, QPoint
from PyQt5.QtGui import QPixmap, QPainter, QIcon, QBrush
import time, sys
class FakeAnimatedSplash(QSplashScreen):
def __init__(self, image):
self.image = image
self.newFrame = QPixmap(self.image.size()+QSize(0, 20))
super(FakeAnimatedSplash, self).__init__(self.newFrame, Qt.WindowStaysOnTopHint)
def showEvent(self, event):
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setPen(Qt.NoPen)
painter.drawPixmap(self.image.rect(), self.image)
painter.drawEllipse(QPoint(0, 110), 8, 8)
class App(QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.main = QMainWindow()
self.setAttribute(Qt.AA_EnableHighDpiScaling)
self.newSplash()
self.main.show()
def newSplash(self):
pixmap = QPixmap("yourImage.png")
smallerPixmap = pixmap.scaled(100, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation)
splash = FakeAnimatedSplash(smallerPixmap)
splash.setEnabled(False)
splash.show()
start = time.time()
while time.time() < start + 10:
self.processEvents()
def main():
app = App(sys.argv)
app.setWindowIcon(QIcon("ABB_icon.png"))
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Potential Solution
Changing the super() to the beginning makes it work but reduces the QPaintDevice window which hides my animation. I would like to expand it, but there are no methods which can do it after the initialization.
def __init__(self, image):
super(LoadingDotsSplash, self).__init__(image, QtCore.Qt.WindowStaysOnTopHint)
self.image = image
# here a function or method that changes the size of the QPaintDevice
The problem is that newFrame is an uninitialized QPixmap and for efficiency reasons the pixels are not modified so they have random values, and that is because the size of the FakeAnimatedSplash is larger than the QPixmap that is painted. The solution is to set the value of the newFrame pixels to transparent:
class FakeAnimatedSplash(QSplashScreen):
def __init__(self, image):
self.image = image
pix = QPixmap(self.image.size() + QSize(0, 20))
pix.fill(Qt.transparent)
super(FakeAnimatedSplash, self).__init__(pix, Qt.WindowStaysOnTopHint)
def paintEvent(self, event):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setPen(Qt.NoPen)
painter.drawPixmap(self.image.rect(), self.image)
painter.drawEllipse(QPoint(0, 110), 8, 8)

Categories