Search code examples
pythonpyside

How to create a labelled QProgressBar in PySide?


This is exactly what I am trying to re-create

I've tried a 4x4 grid layout with QLabels underneath a QProgressBar but it looks awful and I am wondering if there are any possible ways I could approach creating this?


Solution

  • That widget must be created using a custom painting as shown below:

    import os
    
    from PySide2 import QtCore, QtGui, QtWidgets
    
    CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
    
    
    class CustomProgressBar(QtWidgets.QWidget):
        stepsChanged = QtCore.Signal(list)
        valueChanged = QtCore.Signal(int)
    
        def __init__(self, parent=None):
            super().__init__(parent)
    
            self._labels = []
            self._value = 0
    
            self._animation = QtCore.QVariantAnimation(
                startValue=0.0, endValue=1.0, duration=500
            )
            self._animation.valueChanged.connect(self.update)
    
        def get_labels(self):
            return self._labels
    
        def set_labels(self, labels):
            self._labels = labels[:]
            self.stepsChanged.emit(self._labels)
    
        labels = QtCore.Property(
            list, fget=get_labels, fset=set_labels, notify=stepsChanged
        )
    
        def get_value(self):
            return self._value
    
        def set_value(self, value):
            if 0 <= value < len(self.labels) + 1:
                self._value = value
                self.valueChanged.emit(value)
                self.update()
                if self.value < len(self.labels):
                    self._animation.start()
    
        value = QtCore.Property(int, fget=get_value, fset=set_value, notify=valueChanged)
    
        def sizeHint(self):
            return QtCore.QSize(320, 120)
    
        def paintEvent(self, event):
    
            grey = QtGui.QColor("#777")
            grey2 = QtGui.QColor("#dfe3e4")
            blue = QtGui.QColor("#2183dd")
            green = QtGui.QColor("#009900")
            white = QtGui.QColor("#fff")
    
            painter = QtGui.QPainter(self)
    
            painter.setRenderHints(QtGui.QPainter.Antialiasing)
    
            height = 5
            offset = 10
    
            painter.fillRect(self.rect(), white)
    
            busy_rect = QtCore.QRect(0, 0, self.width(), height)
            busy_rect.adjust(offset, 0, -offset, 0)
            busy_rect.moveCenter(self.rect().center())
    
            painter.fillRect(busy_rect, grey2)
    
            number_of_steps = len(self.labels)
    
            if number_of_steps == 0:
                return
    
            step_width = busy_rect.width() / number_of_steps
            x = offset + step_width / 2
            y = busy_rect.center().y()
            radius = 10
    
            font_text = painter.font()
    
            font_icon = QtGui.QFont("Font Awesome 5 Free")
            font_icon.setPixelSize(radius)
    
            r = QtCore.QRect(0, 0, 1.5 * radius, 1.5 * radius)
    
            fm = QtGui.QFontMetrics(font_text)
    
            for i, text in enumerate(self.labels, 1):
                r.moveCenter(QtCore.QPoint(x, y))
    
                if i <= self.value:
                    w = (
                        step_width
                        if i < self.value
                        else self._animation.currentValue() * step_width
                    )
                    r_busy = QtCore.QRect(0, 0, w, height)
                    r_busy.moveCenter(busy_rect.center())
    
                    if i < number_of_steps:
                        r_busy.moveLeft(x)
                        painter.fillRect(r_busy, blue)
    
                    pen = QtGui.QPen(green)
                    pen.setWidth(3)
                    painter.setPen(pen)
                    painter.setBrush(green)
                    painter.drawEllipse(r)
                    painter.setFont(font_icon)
                    painter.setPen(white)
                    painter.drawText(r, QtCore.Qt.AlignCenter, chr(0xF00C))
                    painter.setPen(green)
    
                else:
                    is_active = (self.value + 1) == i
                    pen = QtGui.QPen(grey if is_active else grey2)
                    pen.setWidth(3)
                    painter.setPen(pen)
                    painter.setBrush(white)
                    painter.drawEllipse(r)
                    painter.setPen(blue if is_active else QtGui.QColor("black"))
    
                rect = fm.boundingRect(text)
                rect.moveCenter(QtCore.QPoint(x, y + 2 * radius))
                painter.setFont(font_text)
                painter.drawText(rect, QtCore.Qt.AlignCenter, text)
    
                x += step_width
    
    
    def main():
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
    
        _id = QtGui.QFontDatabase.addApplicationFont(
            os.path.join(CURRENT_DIR, "fa-solid-900.ttf")
        )
        print(QtGui.QFontDatabase.applicationFontFamilies(_id))
    
        progressbar = CustomProgressBar()
        progressbar.labels = ["Step One", "Step Two", "Step Three", "Complete"]
    
        button = QtWidgets.QPushButton("Next Step")
    
        def on_clicked():
            progressbar.value = (progressbar.value + 1) % (len(progressbar.labels) + 1)
    
        button.clicked.connect(on_clicked)
    
        w = QtWidgets.QWidget()
        lay = QtWidgets.QVBoxLayout(w)
        lay.addWidget(progressbar)
        lay.addWidget(button, alignment=QtCore.Qt.AlignRight)
    
        w.show()
    
        sys.exit(app.exec_())
    
    
    if __name__ == "__main__":
        main()
    

    enter image description here

    Note: To paint the check icon I have used the awesome font so it must be downloaded from here and placed next to the script.

    PySide:

    import os
    
    from PySide import QtCore, QtGui
    
    CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
    
    
    class CustomVariantAnimation(QtCore.QVariantAnimation):
        def updateCurrentValue(self, value):
            pass
    
    
    class CustomProgressBar(QtGui.QWidget):
        stepsChanged = QtCore.Signal(list)
        valueChanged = QtCore.Signal(int)
    
        def __init__(self, parent=None):
            super().__init__(parent)
    
            self._labels = []
            self._value = 0
    
            self._percentage_width = 0
            self._animation = CustomVariantAnimation(startValue=0.0, endValue=1.0)
            self._animation.setDuration(500)
            self._animation.valueChanged.connect(self.update)
    
        def get_labels(self):
            return self._labels
    
        def set_labels(self, labels):
            self._labels = labels[:]
            self.stepsChanged.emit(self._labels)
    
        labels = QtCore.Property(
            list, fget=get_labels, fset=set_labels, notify=stepsChanged
        )
    
        def get_value(self):
            return self._value
    
        def set_value(self, value):
            if 0 <= value < len(self.labels) + 1:
                self._value = value
                self.valueChanged.emit(value)
                self.update()
                if self.value < len(self.labels):
                    self._animation.start()
    
        value = QtCore.Property(int, fget=get_value, fset=set_value, notify=valueChanged)
    
        def sizeHint(self):
            return QtCore.QSize(320, 120)
    
        def paintEvent(self, event):
    
            grey = QtGui.QColor("#777")
            grey2 = QtGui.QColor("#dfe3e4")
            blue = QtGui.QColor("#2183dd")
            green = QtGui.QColor("#009900")
            white = QtGui.QColor("#fff")
    
            painter = QtGui.QPainter(self)
    
            painter.setRenderHints(QtGui.QPainter.Antialiasing)
    
            height = 5
            offset = 10
    
            painter.fillRect(self.rect(), white)
    
            busy_rect = QtCore.QRect(0, 0, self.width(), height)
            busy_rect.adjust(offset, 0, -offset, 0)
            busy_rect.moveCenter(self.rect().center())
    
            painter.fillRect(busy_rect, grey2)
    
            number_of_steps = len(self.labels)
    
            if number_of_steps == 0:
                return
    
            step_width = busy_rect.width() / number_of_steps
            x = offset + step_width / 2
            y = busy_rect.center().y()
            radius = 10
    
            font_text = painter.font()
    
            font_icon = QtGui.QFont("Font Awesome 5 Free")
            font_icon.setPixelSize(radius)
    
            r = QtCore.QRect(0, 0, 1.5 * radius, 1.5 * radius)
    
            fm = QtGui.QFontMetrics(font_text)
    
            for i, text in enumerate(self.labels, 1):
                r.moveCenter(QtCore.QPoint(x, y))
    
                if i <= self.value:
                    w = (
                        step_width
                        if i < self.value
                        else self._animation.currentValue() * step_width
                    )
                    r_busy = QtCore.QRect(0, 0, w, height)
                    r_busy.moveCenter(busy_rect.center())
    
                    if i < number_of_steps:
                        r_busy.moveLeft(x)
                        painter.fillRect(r_busy, blue)
    
                    pen = QtGui.QPen(green)
                    pen.setWidth(3)
                    painter.setPen(pen)
                    painter.setBrush(green)
                    painter.drawEllipse(r)
                    painter.setFont(font_icon)
                    painter.setPen(white)
                    painter.drawText(r, QtCore.Qt.AlignCenter, chr(0xF00C))
                    painter.setPen(green)
    
                else:
                    is_active = (self.value + 1) == i
                    pen = QtGui.QPen(grey if is_active else grey2)
                    pen.setWidth(3)
                    painter.setPen(pen)
                    painter.setBrush(white)
                    painter.drawEllipse(r)
                    painter.setPen(blue if is_active else QtGui.QColor("black"))
    
                rect = fm.boundingRect(text)
                rect.moveCenter(QtCore.QPoint(x, y + 2 * radius))
                painter.setFont(font_text)
                painter.drawText(rect, QtCore.Qt.AlignCenter, text)
    
                x += step_width
    
    
    def main():
        import sys
    
        app = QtGui.QApplication(sys.argv)
    
        _id = QtGui.QFontDatabase.addApplicationFont(
            os.path.join(CURRENT_DIR, "fa-solid-900.ttf")
        )
        print(QtGui.QFontDatabase.applicationFontFamilies(_id))
    
        progressbar = CustomProgressBar()
        progressbar.labels = ["Step One", "Step Two", "Step Three", "Complete"]
    
        button = QtGui.QPushButton("Next Step")
    
        def on_clicked():
            progressbar.value = (progressbar.value + 1) % (len(progressbar.labels) + 1)
    
        button.clicked.connect(on_clicked)
    
        w = QtGui.QWidget()
        lay = QtGui.QVBoxLayout(w)
        lay.addWidget(progressbar)
        lay.addWidget(button, alignment=QtCore.Qt.AlignRight)
    
        w.show()
    
        sys.exit(app.exec_())
    
    
    if __name__ == "__main__":
        main()