Search code examples
pythonpython-3.xpyside2qpainterqpropertyanimation

How to use QPropertyAnimation with QPainter to draw an arc


I'm writing a desktop widget that performs the function of a system monitor. Using a QPainter I draw an arc which represents a graphical representation of a cpu usage level. Every second a paintevent redraw this arc with a span angle based on a cpu_percent() function value.

The result is a jerk transition between new and previous level representations. I'd like to use a QPropertyAnimation to create a smooth easing arc animation. Unfortunately I don't know the propeties I should use. I'd be glad if you tell me how to do it in a proper way.

Here's a widget class that I use:

from PySide2 import QtWidgets, QtCore, QtGui
from psutil import cpu_percent


class cpu_diagram(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(cpu_diagram, self).__init__()
        self.resize(600, 600)   # todo

        # color constants
        self.dark = "#3B3A44"
        self.light = "#4A4953"
        self.color = "#75ECB5"

        # text constants
        self.module_name = "CPU"
        self.postfix = "average"

        # timer with an interval of 1 sec
        self.timer = QtCore.QTimer()
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.update)
        self.timer.start()

    def paintEvent(self, event:QtGui.QPaintEvent):
        # get cpu usage
        self.percent = cpu_percent()

        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)

        # draw base
        basic_rect = self.rect().adjusted(20, 20, -20, -20)
        painter.setBrush(QtGui.QBrush(QtGui.QColor(self.dark)))
        painter.drawEllipse(basic_rect)

        # draw arc
        pen = QtGui.QPen(QtGui.QColor(self.light))
        pen.setWidth(40)
        painter.setPen(pen)
        arc_rect = basic_rect.adjusted(40, 40, -40, -40)
        painter.drawEllipse(arc_rect)

        # draw active arc
        pen.setColor(QtGui.QColor(self.color))
        start_angle = 90
        span_angle = self.percent_to_angle(self.percent)
        painter.setPen(pen)
        painter.drawArc(arc_rect, start_angle * 16, span_angle * 16)

        # draw text

        # draw module name
        painter.setPen(QtGui.QPen(QtGui.QColor(QtCore.Qt.white)))
        font = QtGui.QFont()
        font.setPixelSize(110)
        painter.setFont(font)
        arc_rect.moveTop(-25)
        painter.drawText(arc_rect, QtCore.Qt.AlignCenter, self.module_name)

        # draw postfix
        font = QtGui.QFont()
        font.setPixelSize(60)
        painter.setFont(font)
        arc_rect.moveTop(-125)
        painter.drawText(arc_rect, QtCore.Qt.AlignCenter | QtCore.Qt.AlignBottom, self.postfix)

        # draw percents
        arc_rect.moveBottom(460)
        painter.setPen(QtGui.QPen(self.color))
        painter.drawText(arc_rect, QtCore.Qt.AlignCenter | QtCore.Qt.AlignBottom, f"{str(self.percent)} %")

    def percent_to_angle(self, percent):
        return -percent / 100 * 360


Solution

  • You have to create a QProperty that represents the percentage and use it in the QPropertyAnimation.

    from PySide2 import QtWidgets, QtCore, QtGui
    from psutil import cpu_percent
    
    
    class CpuDiagram(QtWidgets.QWidget):
        percentChanged = QtCore.Signal(float)
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self.resize(600, 600)  # todo
    
            # color constants
            self.dark = "#3B3A44"
            self.light = "#4A4953"
            self.color = "#75ECB5"
    
            # text constants
            self.module_name = "CPU"
            self.postfix = "average"
    
            # timer with an interval of 1 sec
            self.timer = QtCore.QTimer()
            self.timer.setInterval(1000)
            self.timer.timeout.connect(self.onTimeout)
            self.timer.start()
    
            self._percent = 0
            self._animation = QtCore.QPropertyAnimation(self, b"percent", duration=400)
    
            self.percentChanged.connect(self.update)
    
        @QtCore.Slot()
        def onTimeout(self):
            start_value = self.percent
            end_value = cpu_percent()
    
            self._animation.setStartValue(start_value)
            self._animation.setEndValue(end_value)
            self._animation.start()
    
        def get_percent(self):
            return self._percent
    
        def set_percent(self, p):
            if self._percent != p:
                self._percent = p
                self.percentChanged.emit(p)
    
        percent = QtCore.Property(
            float, fget=get_percent, fset=set_percent, notify=percentChanged
        )
    
        def paintEvent(self, event: QtGui.QPaintEvent):
            painter = QtGui.QPainter(self)
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
    
            # draw base
            basic_rect = self.rect().adjusted(20, 20, -20, -20)
            painter.setBrush(QtGui.QBrush(QtGui.QColor(self.dark)))
            painter.drawEllipse(basic_rect)
    
            # draw arc
            pen = QtGui.QPen(QtGui.QColor(self.light))
            pen.setWidth(40)
            painter.setPen(pen)
            arc_rect = basic_rect.adjusted(40, 40, -40, -40)
            painter.drawEllipse(arc_rect)
    
            # draw active arc
            pen.setColor(QtGui.QColor(self.color))
            start_angle = 90
            span_angle = self.percent_to_angle(self.percent)
            painter.setPen(pen)
            painter.drawArc(arc_rect, start_angle * 16, span_angle * 16)
    
            # draw text
    
            # draw module name
            painter.setPen(QtGui.QPen(QtGui.QColor(QtCore.Qt.white)))
            font = QtGui.QFont()
            font.setPixelSize(110)
            painter.setFont(font)
            arc_rect.moveTop(-25)
            painter.drawText(arc_rect, QtCore.Qt.AlignCenter, self.module_name)
    
            # draw postfix
            font = QtGui.QFont()
            font.setPixelSize(60)
            painter.setFont(font)
            arc_rect.moveTop(-125)
            painter.drawText(
                arc_rect, QtCore.Qt.AlignCenter | QtCore.Qt.AlignBottom, self.postfix
            )
    
            # draw percents
            arc_rect.moveBottom(460)
            painter.setPen(QtGui.QPen(self.color))
            painter.drawText(
                arc_rect,
                QtCore.Qt.AlignCenter | QtCore.Qt.AlignBottom,
                f"{self.percent:.2f} %",
            )
    
        def percent_to_angle(self, percent):
            return -percent / 100 * 360
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        w = CpuDiagram()
        w.show()
        sys.exit(app.exec_())