Search code examples
pythonqtpyqt5qt5paintevent

Qt Custom Paint Event Progressbar


I want to make custom progressbar on Qt.

Design of progressbar (It's PNG):

Design Pic1

Here is the result on Qt:

Pic2

Code of Pic2:

import sys, os, time
from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *

class EProgressbar(QProgressBar):
    valueChanged = QtCore.Signal(int)
    _val = 0
    def __init__(self):        
        super(EProgressbar, self).__init__(None)
        self.r = 15
        self.setFixedHeight(40)
        self._animation = QtCore.QPropertyAnimation(self, b"_vallll", duration=600)
        self.valueChanged.connect(self.update)

    def setValue(self, value:int) -> None:
        self._animation.setStartValue(self.value())
        self._animation.setEndValue(value)
        self._val = value
        self._animation.start()

    def value(self) -> int:
        return self._val

    def ESetValue(self, value):
        if self._val != value:
            self._val = value
            self.valueChanged.emit(value)
    _vallll = QtCore.Property(int, fget=value, fset=ESetValue, notify=valueChanged)

    def paintEvent(self, event: QPaintEvent) -> None:
        pt = QPainter();pt.begin(self);pt.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing)
        path = QPainterPath();path2 = QPainterPath(); path3 = QPainterPath()
        font = QFont('Helvetica', 11, weight=QFont.Bold); font.setStyleHint(QFont.Times, QFont.PreferAntialias)

        BRUSH_BASE_BACKGROUND, BRUSH_BASE_FOREGROUND, BRUSH_POLYGON, BRUSH_CORNER = QColor(247,247,250), QColor(255,152,91), QColor(255,191,153), QColor(203,203,205)


        pt.setPen(QPen(BRUSH_CORNER,1.5));pt.setBrush(BRUSH_BASE_BACKGROUND)
        rect = self.rect().adjusted(2,2,-2,-2)#QRect(1, 0, self.width()-2, self.height())
        path.addRoundedRect(rect, self.r, self.r)
        #pt.setBrush(BRUSH_BASE_FOREGROUND)
        #path.addRect(self.rect())

        path2.addRoundedRect(QRect(2,2, self._vallll/ 100 * self.width()-4, self.height()-4), self.r, self.r)
        #path2.addRoundedRect(QRect(20,2,10, self.height()), self.r, self.r)




        pt.drawPath(path)
        pt.setBrush(BRUSH_BASE_FOREGROUND)

        pt.drawPath(path2)

        pt.setPen(Qt.NoPen)
        pt.setBrush(BRUSH_POLYGON)

        start_x = 20
        y, dx = 3, 6
        polygon_width = 14
        polygon_space =18 #15#18
        progress_filled_width = self.value()/self.maximum()*self.width()

        pt.setClipPath(path2, Qt.ClipOperation.ReplaceClip) # bu olmazsa polygon taşıyor, clip yapılması lazım

        for i in range(100):
            x = start_x + (i*polygon_width) + (i*polygon_space)
            if x >= progress_filled_width or (x+ polygon_width >= progress_filled_width):
                break
            path2.addPolygon(QPolygon([
            QPoint(x, y),
            QPoint(x+polygon_width, y),
            QPoint(x+polygon_width/2, self.height()-y),
            QPoint(x-polygon_width/2, self.height()-y)]))

        pt.drawPath(path2)



        pt.setFont(font)
        pt.setPen(Qt.white)
        pt.drawText(QRect(2,2,self.width()-4,self.height()-4), Qt.AlignCenter, f"%{self.value()}")

        pt.end()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    wind = QMainWindow();wind.setStyleSheet("QMainWindow{background-color:blue}");wind.setWindowTitle("EProgressBar")
    wind.resize(221,150)
    wid = QWidget();lay = QHBoxLayout(wid);lay.setAlignment(Qt.AlignCenter)
    e = EProgressbar();e.setValue(80)
    timer = QTimer(wind)
    def lamb():
        import random
        e.setValue(random.randint(0,100))
    timer.timeout.connect(lamb)
    #timer.start(1000)
    #e.setGeometry(QRect(10,10,170,250))
    lay.addWidget(e)
    wind.setCentralWidget(wid)
    #e.setParent(wind)
    wind.show()
    sys.exit(app.exec())

This one looks good but when I set progressbar value to 0, result like this:

Progressbar value is 0 (Pic3)

Notes:

  1. I need to use pt.setClipPath(path2, Qt.ClipOperation.ReplaceClip) else If you look closely, the polygon in the upper right has crossed the progressbar.

Polygon in the upper right has crossed the progressbar (Pic4)

So I think all drawing things must be? in the same QPainterPath? When I try all the drawing in the same path (like OnePathCode) result like this:

Pic5


Solution

  • The problem is in the width of the rectangle, which becomes too narrow due to the reduced width and the rounded border.

    A better approach would be to clip the path to the external border and merge that clip with a rounded rectangle extended to the left (so that its width will always be enough.

    Note that I choose to radically change most aspects in your code, also to improve readability. For slightly better performance, I've set the font for the widget (which is better than recreating it every time) and ignored the bar painting whenever the value was 0.

    Finally, since you're painting the value color in white, you must also paint it with another color whenever the value is less than 50%, otherwise the user won't be able to see it until it reaches that point.

    class EProgressbar(QProgressBar):
        valueChanged = QtCore.pyqtSignal(int)
        _val = 0
        def __init__(self):        
            super(EProgressbar, self).__init__(None)
            self.r = 15
            self.setFixedHeight(40)
            self._animation = QtCore.QPropertyAnimation(self, b"_vallll", duration=600)
            self.valueChanged.connect(self.update)
            font = QFont('Helvetica', 11, weight=QFont.Bold)
            font.setStyleHint(QFont.Times, QFont.PreferAntialias)
            self.setFont(font)
    
        # ...
    
        def paintEvent(self, event: QPaintEvent) -> None:
            pt = QPainter(self)
            pt.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing)
    
            border = QPainterPath()
            border.addRoundedRect(
                QRectF(self.rect().adjusted(2, 2, -3, -3)), 
                self.r, self.r)
    
            pt.setPen(QColor(203,203,205))
            pt.setBrush(QColor(247,247,250))
            pt.drawPath(border)
    
            pt.setClipPath(border)
    
            foreground = QColor(255,191,153)
            pt.setPen(foreground.darker(110))
            pt.drawText(self.rect(), Qt.AlignCenter, '{}%'.format(self.value()))
    
            if self._vallll <= self.minimum():
                return
    
            polygon_width = 14
            brush_polygon = QPolygonF([
                QPoint(0, 3),
                QPoint(polygon_width, 3),
                QPoint(polygon_width / 2, self.height() - 3),
                QPoint(-polygon_width / 2, self.height() - 3)
            ])
    
            bar_width = (self.width() - 4) * self._vallll * .01
            brush_size = brush_polygon.boundingRect().width() + 4
            bar_count = int(bar_width / brush_size) + 1
    
            value_clip = QPainterPath()
            rect = QRectF(-20, 2, 20 + bar_width, self.height() - 3)
            value_clip.addRoundedRect(rect, self.r, self.r)
            pt.setClipPath(value_clip, Qt.IntersectClip)
    
            brush_path = QPainterPath()
            for i in range(bar_count):
                brush_path.addPolygon(brush_polygon.translated(brush_size * i, 0))
    
            pt.setPen(Qt.NoPen)
            pt.setBrush(QColor(255,152,91))
            pt.drawPath(border)
            pt.setBrush(foreground)
            pt.drawPath(brush_path)
    
            pt.setPen(Qt.white)
            pt.setFont(self.font())
            pt.drawText(self.rect(), Qt.AlignCenter, '{}%'.format(self.value()))
    

    On an unrelated note, be aware that your code has lots of readability issues; for instance, you should not use semicolons to separate functions: doing it doesn't provide any benefit, and they only make the code unnecessarily annoying to read; spaces between function arguments are also very important; read more about these extremely important aspects in the official Style Guide for Python Code.