Does pyqt support a stacked progress bar with two values?

Question:

Similar to this question, but for pyqt. I have an application that has two threads, one of which processes some data (time consuming), and the second thread that presents the results and asks for verification on the results. I want to show the number of objects processed in a progress bar. However, I also want to show the number of objects verified by user. Number processed will always be equal or greater than the number of objects verified (since you can’t verify what hasn’t been verified). In essence, it’s kind of like the loading bar of a youtube video or something, showing a grey part that is “loaded” and red part that is “watched.” Is this something that can be supported in pyqt? The documentation for QProgressBar does not seem to hint that there’s any support. Using PyQt5 and Python 3.6.

It should look similar to this: multi progress bar

Here’s a minimal viable code that has TWO separate progress bars, one for the number of objects processed and the other for the number verified, but I want them overlapped…

import sys

from PyQt5.QtWidgets import (QApplication, QDialog,
                             QProgressBar, QPushButton)

class Actions(QDialog):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle('Progress Bar')
        self.objectsToProcess = 100
        self.objectsProcessed = 0
        self.objectsVerified = 0

        self.processProgress = QProgressBar(self)
        self.processProgress.setGeometry(5, 5, 300, 25)
        self.processProgress.setMaximum(self.objectsToProcess)

        self.verifyProgress = QProgressBar(self)
        self.verifyProgress.setGeometry(5, 35, 300, 25)
        self.verifyProgress.setMaximum(self.objectsToProcess)

        self.processButton = QPushButton('Process', self)
        self.processButton.move(5, 75)

        self.verifyButton = QPushButton('Verify', self)
        self.verifyButton.move(90, 75)

        self.show()

        self.processButton.clicked.connect(self.process)
        self.verifyButton.clicked.connect(self.verify)

    def process(self):
        if self.objectsProcessed + 1 < self.objectsToProcess:
            self.objectsProcessed += 1
            self.processProgress.setValue(self.objectsProcessed)

    def verify(self):
        if self.objectsVerified < self.objectsProcessed:
            self.objectsVerified += 1
            self.verifyProgress.setValue(self.objectsVerified)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Actions()
    sys.exit(app.exec_())

Result from above code:

enter image description here

Asked By: Endyd

||

Answers:

A possible solution is to create a new attribute in QProgressBar that shows the alternative advance, and to do the painting we can use a QProxyStyle:

from PyQt5 import QtCore, QtGui, QtWidgets

class ProxyStyle(QtWidgets.QProxyStyle):
    def drawControl(self, element, option, painter, widget):
        if element == QtWidgets.QStyle.CE_ProgressBar:
            super(ProxyStyle, self).drawControl(element, option, painter, widget)
            if hasattr(option, 'alternative'):
                alternative = option.alternative

                last_value = option.progress
                last_pal = option.palette
                last_rect = option.rect

                option.progress = alternative
                pal = QtGui.QPalette()
                # alternative color
                pal.setColor(QtGui.QPalette.Highlight, QtCore.Qt.red)
                option.palette = pal
                option.rect = self.subElementRect(QtWidgets.QStyle.SE_ProgressBarContents, option, widget)
                self.proxy().drawControl(QtWidgets.QStyle.CE_ProgressBarContents, option, painter, widget)

                option.progress = last_value 
                option.palette = last_pal
                option.rect = last_rect
            return
        super(ProxyStyle, self).drawControl(element, option, painter, widget)

class ProgressBar(QtWidgets.QProgressBar):
    def paintEvent(self, event):
        painter =  QtWidgets.QStylePainter(self)
        opt = QtWidgets.QStyleOptionProgressBar()
        if hasattr(self, 'alternative'):
            opt.alternative = self.alternative()
        self.initStyleOption(opt)
        painter.drawControl(QtWidgets.QStyle.CE_ProgressBar, opt)

    @QtCore.pyqtSlot(int)
    def setAlternative(self, value):
        self._alternative = value
        self.update()

    def alternative(self):
        if not hasattr(self, '_alternative'):
            self._alternative = 0
        return self._alternative

class Actions(QtWidgets.QDialog):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle('Progress Bar')

        self.objectsToProcess = 100
        self.objectsProcessed = 0
        self.objectsVerified = 0

        self.progress_bar = ProgressBar(maximum=self.objectsToProcess)
        self.process_btn = QtWidgets.QPushButton('Process')
        self.verify_btn = QtWidgets.QPushButton('Verify')

        self.process_btn.clicked.connect(self.process)
        self.verify_btn.clicked.connect(self.verify)

        lay = QtWidgets.QGridLayout(self)
        lay.addWidget(self.progress_bar, 0, 0, 1, 2)
        lay.addWidget(self.process_btn, 1, 0)
        lay.addWidget(self.verify_btn, 1, 1)

    def process(self):
        if self.objectsProcessed + 1 < self.objectsToProcess:
            self.objectsProcessed += 1
            self.progress_bar.setValue(self.objectsProcessed)

    def verify(self):
        if self.objectsVerified < self.objectsProcessed:
            self.objectsVerified += 1
            self.progress_bar.setAlternative(self.objectsVerified)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    app.setStyle(ProxyStyle(app.style()))
    w = Actions()
    w.show()
    sys.exit(app.exec_())

enter image description here

Answered By: eyllanesc

Thanks for @eyllanesc for providing a robust solution. I chose to go with a lighter (admittedly hackish) solution of overlapping two progress bars and making the top bar slightly transparent using QGraphicsOpacityEffect.

    # Opaque prog bar
    self.verifyProgress = QProgressBar(self)
    self.verifyProgress.setGeometry(5, 5, 300, 25)
    self.verifyProgress.setMaximum(self.objectsToProcess)
    self.verifyProgress.setFormat('%p% /                 ')
    self.verifyProgress.setAlignment(Qt.AlignCenter)

    # Must set the transparent prog bar second to overlay on top of opaque prog bar
    self.processProgress = QProgressBar(self)
    self.processProgress.setGeometry(5, 5, 300, 25)
    self.processProgress.setMaximum(self.objectsToProcess)
    self.processProgress.setFormat('   %p%')
    self.processProgress.setAlignment(Qt.AlignCenter)
    op = QGraphicsOpacityEffect(self.processProgress)
    op.setOpacity(0.5)
    self.processProgress.setGraphicsEffect(op)

Result:

enter image description here

Answered By: Endyd

I needed to do something similar recently and choose to use a gradient color for the progressbar chunk since I needed to use stylesheets too.

    def set_pb_value(self, pb, value_1, value_2):
        if value_2 > value_1:
            pb.setValue(value_2)
            pb.setFormat("{} / {}".format(value_1, value_2))
            pb.setStyleSheet('QProgressBar::chunk {' +
                         'background-color: qlineargradient(spread:pad, x1:' + str(value_1/pb.maximum()) + ', y1:0, x2:' +
                         str(value_1/value_2) + ', y2:0, stop:' + str(value_1/value_2) + ' rgba(0, 255, 0, 255), stop:1 '
                                            'rgba(255, 0, 0, 255)); width: -1px; margin: -1px;}')
        else:
            pb.setValue(value_1)
            pb.setFormat("%v")

My values were whole numbers so value_1/pb.maximum() was necessary for me, but change it per your needs.
I also had some issues with the other stylesheets and the progressbar margins which is why they are set to -1 right now, you may not need to include that.

Answered By: TJ_Thornsberry

I’m interested in @eyllanesc’s solution into a Ui_MainWindow object, but I’m running into an issue where the control element QtWidgets.QStyle.ControlElement.CE_ProgressBar is never seen by the ProxyStyle class, which never allows the secondary progress bar to be overlaid. Is there a quick way to resolve this? Maybe something like associating a CE_ProgressBar with the custom ProgressBar class? Is there a good way to do this?

Answered By: cellTransformer
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.