Change color in QProgressBar delegate

How to dynamically change QProgressBar colors in a QTableView delegate
Heads up! You've already completed this tutorial.

How can I change the color of a QProgressBar delegate in a QTableView when a process starts or stops? I'm using a delegate to show an indeterminate progress bar (with setRange(0, 0)), and I want it to be green when running and red when stopped, but replacing the delegate doesn't seem to redraw the color.

The issue here comes from how delegates and persistent editors interact. When you call openPersistentEditor, Qt creates the editor widget and keeps it alive. Replacing the delegate on a row doesn't automatically close and recreate those existing editors — so even though you're assigning a new ProgressBarDelegate with a different color, the old progress bar widget is still sitting there unchanged.

The solution is to store the color information in the model data and have a single delegate read that color when it creates or updates the editor. This way, when the data changes, the delegate can update the editor's stylesheet accordingly.

Let's walk through how to set this up properly.

Store state in the model, not the delegate

Rather than creating a new delegate every time a button is clicked, store the running/stopped state in the model itself. You can use a custom Qt.ItemDataRole (or just Qt.UserRole) to hold the color or state for each row. The delegate then reads this value when painting or configuring the editor.

This keeps your architecture clean: the model holds the data, and the delegate is responsible only for presentation.

Use setEditorData to update the progress bar

The setEditorData method on your delegate is called whenever the model data changes for an index that has a persistent editor. This is where you update the progress bar's stylesheet based on the current state.

Here's how to write a ProgressBarDelegate that reads a color from the model:

python
class ProgressBarDelegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        if index.column() == 2:
            if (
                isinstance(self.parent(), QtWidgets.QAbstractItemView)
                and self.parent().model() is index.model()
            ):
                self.parent().openPersistentEditor(index)
        QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)

    def createEditor(self, parent, option, index):
        editor = QtWidgets.QProgressBar(parent)
        editor.setRange(0, 0)
        editor.setTextVisible(False)
        return editor

    def setEditorData(self, editor, index):
        color = index.data(QtCore.Qt.ItemDataRole.UserRole)
        if color:
            editor.setStyleSheet(
                f"QProgressBar::chunk {{background-color: {color}; "
                f"width: 20px; margin: 0.5px;}}"
            )

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

The delegate no longer stores any color itself. Instead, setEditorData looks up the color from Qt.UserRole on the model index and applies it to the stylesheet. Every time you call setData on the model with Qt.UserRole, Qt will automatically call setEditorData on the persistent editor for that index.

Update the model when the button is clicked

In your button click handler, instead of creating a new delegate, just update the model data:

python
def on_button_clicked(self, index):
    model = self.table_view.model()
    progress_index = model.index(index.row(), 2)

    current_state = progress_index.data(QtCore.Qt.ItemDataRole.DisplayRole)

    if current_state is None or current_state == 0:
        model.setData(progress_index, "green", QtCore.Qt.ItemDataRole.UserRole)
        model.setData(progress_index, 1, QtCore.Qt.ItemDataRole.DisplayRole)
    else:
        model.setData(progress_index, "red", QtCore.Qt.ItemDataRole.UserRole)
        model.setData(progress_index, 0, QtCore.Qt.ItemDataRole.DisplayRole)

The DisplayRole tracks the state (1 for running, 0 for stopped), and UserRole holds the color string. When setData is called, the model emits dataChanged, which triggers setEditorData on the persistent editor — and the progress bar updates its color.

Complete working example

Here's the full example putting everything together. The QTableView shows a list of paths, each with a Start/Stop button and an indeterminate progress bar. Clicking the button toggles the progress bar between green and red using a single delegate that reads color from the model.

python
import sys

from PySide6 import QtCore, QtGui, QtWidgets


class ProgressBarDelegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        if index.column() == 2:
            if (
                isinstance(self.parent(), QtWidgets.QAbstractItemView)
                and self.parent().model() is index.model()
            ):
                self.parent().openPersistentEditor(index)
        QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)

    def createEditor(self, parent, option, index):
        editor = QtWidgets.QProgressBar(parent)
        editor.setRange(0, 0)
        editor.setTextVisible(False)
        return editor

    def setEditorData(self, editor, index):
        color = index.data(QtCore.Qt.ItemDataRole.UserRole)
        if color:
            editor.setStyleSheet(
                f"QProgressBar::chunk {{background-color: {color}; "
                f"width: 20px; margin: 0.5px;}}"
            )

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)


class PushButtonDelegate(QtWidgets.QStyledItemDelegate):
    clicked = QtCore.Signal(QtCore.QModelIndex)

    def paint(self, painter, option, index):
        if (
            isinstance(self.parent(), QtWidgets.QAbstractItemView)
            and self.parent().model() is index.model()
        ):
            self.parent().openPersistentEditor(index)
        QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)

    def createEditor(self, parent, option, index):
        button = QtWidgets.QPushButton(parent)
        button.clicked.connect(lambda *args, ix=index: self.clicked.emit(ix))
        return button

    def setEditorData(self, editor, index):
        editor.setText("Start/Stop")

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFixedSize(600, 300)

        self.table_view = QtWidgets.QTableView()
        layout = QtWidgets.QHBoxLayout()
        layout.addWidget(self.table_view)
        cw = QtWidgets.QWidget()
        cw.setLayout(layout)
        self.setCentralWidget(cw)

        self.init_model()

        self.button_delegate.clicked.connect(self.on_button_clicked)

    def init_model(self):
        headers = ["Path", "Control", "Progress"]

        self.button_delegate = PushButtonDelegate(self.table_view)
        self.progress_delegate = ProgressBarDelegate(self.table_view)

        self.model = QtGui.QStandardItemModel()
        self.model.setHorizontalHeaderLabels(headers)

        for row in range(5):
            self.model.setItem(
                row, 0, QtGui.QStandardItem(f"some_path{row}")
            )
            self.model.setItem(row, 1, QtGui.QStandardItem())
            self.model.setItem(row, 2, QtGui.QStandardItem())

        self.table_view.setModel(self.model)
        self.table_view.setItemDelegateForColumn(1, self.button_delegate)
        self.table_view.setItemDelegateForColumn(2, self.progress_delegate)
        self.table_view.resizeColumnsToContents()
        self.table_view.horizontalHeader().setSectionResizeMode(
            QtWidgets.QHeaderView.ResizeMode.Stretch
        )

    def on_button_clicked(self, index):
        model = self.table_view.model()
        progress_index = model.index(index.row(), 2)

        current_state = progress_index.data(QtCore.Qt.ItemDataRole.DisplayRole)

        if current_state is None or current_state == 0:
            model.setData(
                progress_index, "green", QtCore.Qt.ItemDataRole.UserRole
            )
            model.setData(
                progress_index, 1, QtCore.Qt.ItemDataRole.DisplayRole
            )
        else:
            model.setData(
                progress_index, "red", QtCore.Qt.ItemDataRole.UserRole
            )
            model.setData(
                progress_index, 0, QtCore.Qt.ItemDataRole.DisplayRole
            )


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

When you run this, each row has a "Start/Stop" button. Clicking it the first time turns the progress bar green (running). Clicking again turns it red (stopped). Each subsequent click toggles between the two states.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick

(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!

More info Get the book

Martin Fitzpatrick

Change color in QProgressBar delegate was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.