'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 1:[1]

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()

Solution 2:[2]

For c++: https://i.stack.imgur.com/9iDlg.png

#ifndef NTUSTEPPROGRESSBAR_H
#define NTUSTEPPROGRESSBAR_H

#include <QObject>
#include <QWidget>
#include <QVariantAnimation>

class NTUStepProgressBar: public QWidget
{
Q_OBJECT
public:
NTUStepProgressBar(QWidget *widget =nullptr);

void setValue(int value);
void setLabel(QList<QString> label);

protected:
  void paintEvent(QPaintEvent* paintEvent);

private:
  QList<QString> mLabel;
  int mValue;
  QVariantAnimation mAnimation;
  };

#endif // NTUSTEPPROGRESSBAR_H

  #include "NTUStepProgressBar.h"
  #include <QPainter>
  NTUStepProgressBar::NTUStepProgressBar(QWidget* widget) : QWidget(widget)
  {
    this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
    setWindowFlags(Qt::FramelessWindowHint);
  
    this->mLabel << "Step One"
                 << "Step Two"
                 << "Step Three"
                 << "Complete";
    this->mValue = 0;
  
    mAnimation.setStartValue(0.0);
    mAnimation.setEndValue(1.0);
    mAnimation.setDuration(500);
  
    connect(&mAnimation, &QVariantAnimation::valueChanged, [this](const QVariant &value){
      this->update();
  });
  
  }
  
  void NTUStepProgressBar::setValue(int value)
  {
    if( 0 <= value < mLabel.size()+ 1)
    {
      mValue = value;
     // self.valueChanged.emit(value)
      this->update();
      if (mValue < mLabel.size())
          mAnimation.start();
  
    }
  }
  
  void NTUStepProgressBar::setLabel(QList<QString> label)
  {
    mLabel = label;
  }
  
  void NTUStepProgressBar::paintEvent(QPaintEvent* paintEvent)
  {
    QPainter painter(this);
  
    QColor grey = QColor("#777");
    QColor grey2 = QColor("#dfe3e4");
    QColor blue = QColor("#2183dd");
    QColor green = QColor("#009900");
    QColor white = QColor("#fff");
  
    painter.setRenderHints(QPainter::Antialiasing);
  
    int height = 5;
    int offset = 10;
  
    painter.fillRect(this->rect(), white);
  
    QRect busy_rect = QRect(0, 0, this->width(), height);
    busy_rect.adjust(offset, 0, -offset, 0);
    busy_rect.moveCenter(this->rect().center());
  
    painter.fillRect(busy_rect, grey2);
  
    int number_of_steps = this->mLabel.size();
  
    if (number_of_steps == 0)
      return;
  
    int step_width = busy_rect.width() / number_of_steps;
    int x = offset + step_width / 2;
    int y = busy_rect.center().y();
    int radius = 10;
  
    QFont font_text = painter.font();
  
    QFont font_icon = QFont("Font Awesome 5 Free");
    font_icon.setPixelSize(radius);
  
    QRect r = QRect(0, 0, 1.5 * radius, 1.5 * radius);
  
    QFontMetrics fm = QFontMetrics(font_text);
  
    for (int i = 0; i < mLabel.size(); i++)
    {
      r.moveCenter(QPoint(x, y));
      int w;
  
      if (i <= mValue)
      {
  
        if (i < mValue)
          w = step_width;
        else
          w = mAnimation.currentValue().toInt() * step_width;
  
        QRect r_busy = 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);
        }
        QPen pen = QPen(green);
        pen.setWidth(3);
        painter.setPen(pen);
        painter.setBrush(green);
        painter.drawEllipse(r);
        painter.setFont(font_icon);
        painter.setPen(white);
        painter.drawText(r, Qt::AlignCenter, tr(""));
        painter.setPen(green);
      }
      else
      {
        bool is_active = (this->mValue + 1) == i;
  
        QPen pen = is_active ? QPen(grey) : QPen(grey2);
        pen.setWidth(3);
        painter.setPen(pen);
        painter.setBrush(white);
        painter.drawEllipse(r);
        painter.setPen(is_active ? blue : QColor("black"));
      }
      QRect rect = fm.boundingRect(mLabel[i]);
      rect.moveCenter(QPoint(x, y + 2 * radius));
      painter.setFont(font_text);
      painter.drawText(rect, Qt::AlignCenter, mLabel[i]);
  
      x += step_width;
    }
  }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 user1742740