Search code examples
pythonpyqtpyqt5qprogressbar

Does pyqt support a stacked progress bar with two values?


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


Solution

  • 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