Change background of model index for QAbstractTableModel in PySide6

Question:

I would like to change the background color of specific index on my table, but only after a specific task is completed.

I know that I can use the Background role to change the color in my Table model, but I want to change the background color on external factors and not based on changes to the table itself. For example, the code below shows a basic example of a QTableView with 6 rows displayed in a QWidget. Inside the main app I am able to change the text of specific indexes using setData as seen below.

model.setData(model.index(2, 0), "Task Complete")

Here is the full code:

import sys
from PySide6.QtWidgets import (
    QApplication, QWidget, QTableView, QVBoxLayout
)
from PySide6.QtCore import Qt, QAbstractTableModel
from PySide6.QtGui import QBrush


class TableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def data(self, index, role=Qt.DisplayRole):
        # display data
        if role == Qt.DisplayRole:
            try:
                return self._data[index.row()][index.column()]
            except IndexError:
                return ''

    def setData(self, index, value, role=Qt.EditRole):
        if role in (Qt.DisplayRole, Qt.EditRole):
            # if value is blank
            if not value:
                return False
            self._data[index.row()][index.column()] = value
            self.dataChanged.emit(index, index)
        return True

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])
    
    def flags(self, index):
        return super().flags(index) | Qt.ItemIsEditable


class MainApp(QWidget):

    def __init__(self):
        super().__init__()
        self.window_width, self.window_height = 200, 250
        self.setMinimumSize(self.window_width, self.window_height)

        self.layout = {}
        self.layout['main'] = QVBoxLayout()
        self.setLayout(self.layout['main'])

        self.table = QTableView()

        self.layout['main'].addWidget(self.table)

        model = TableModel(data)
        self.table.setModel(model)

        # THIS IS WHERE THE QUESTION IS
        model.setData(model.index(2, 0), "Task Complete") # Change background color instead of text
        model.setData(model.index(5, 0), "Task Complete") # Change background color instead of text

if __name__ == '__main__':

    data = [
            ["Task 1"],
            ["Task 2"],
            ["Task 3"],
            ["Task 4"],
            ["Task 5"],
            ["Task 6"],
        ]

    app = QApplication(sys.argv)

    myApp = MainApp()
    myApp.show()

    try:
        sys.exit(app.exec())
    except SystemExit:
        print('Closing Window...')

I have tried to change the setData function to use the Qt.BackgroundRole instead of Qt.EditRole, but that does not work for changing the color. The result is that the code runs, but nothing happens.

I want to be able to fill the background with whatever color I choose based on the specific index I pick. However, I want this code to reside inside the MainApp class and not in the TableModel Class.

Suggestions Tried

Added code to data()

if role == Qt.BackgroundRole:
            return QBrush(Qt.green)

Changed setData()

def setData(self, index, value, role=Qt.BackgroundRole):
        if role in (Qt.DisplayRole, Qt.BackgroundRole):
            # if value is blank
            if not value:
                return False
            self._data[index.row()][index.column()] = value
            self.dataChanged.emit(index, index)
        return True

Changed setData in MainApp too

model.setData(model.index(5, 0), QBrush(Qt.green))

This resulted in highlighting the entire table in green instead of specific index.

Asked By: roboticEagle

||

Answers:

If you want to set different colors for each index, you must store the color information in another data structure and return the corresponding value for the index.

Both data() and setData() must access different values depending on the role (see the documentation about item roles), meaning that you must not use self._data indiscriminately for anything role. If you set the color for a row/column in the same data structure you use for the text, then the text is lost.

A simple solution is to create a list of lists that has the same size of the source data, using None as default value.

class TableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data
        rows = len(data)
        cols = len(data[0])
        self._backgrounds = [[None] * cols for _ in range(rows)]

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return
        elif role in (Qt.DisplayRole, Qt.EditRole):
            return self._data[index.row()][index.column()]
        elif role == Qt.BackgroundRole:
            return self._backgrounds[index.row()][index.column()]

    def setData(self, index, value, role=Qt.EditRole):
        if (
            not index.isValid() 
            or index.row() >= len(self._data)
            or index.column() >= len(self._data[0])
        ):
            return False
        if role == Qt.EditRole:
            self._data[index.row()][index.column()] = value
        elif role == Qt.BackgroundRole:
            self._backgrounds[index.row()][index.column()] = value
        else:
            return False
        self.dataChanged.emit(index, index, [role])
        return True

Note: you should always ensure that data has at least one row, otherwise columnCount() will raise an exception.

Then, to update the color, you must also use the proper role:

    model.setData(model.index(5, 0), QBrush(Qt.green), Qt.BackgroundRole)

Note that if you don’t need to keep the data structure intact (containing only the displayed values), a common solution is to use dictionaries.

You could use common dictionary that has the role as key and the data structure as value:

class TableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        rows = len(data)
        cols = len(data[0])
        self._data = {
            Qt.DisplayRole: data,
            Qt.BackgroundRole: [[None] * cols for _ in range(rows)]
        }

    # implement the other functions accordingly

Otherwise, use a single structure that uses unique dictionaries for each item:

class TableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = []
        for rowData in data:
            self._data.append([
                {Qt.DisplayRole: item} for item in rowData
            ])

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return
        data = self._data[index.row()][index.column()]
        if role == Qt.EditRole:
            role = Qt.DisplayRole
        return data.get(role)

    def setData(self, index, value, role=Qt.EditRole):
        if (
            not index.isValid() 
            or role not in (Qt.EditRole, Qt.BackgroundRole)
        ):
            return False
        self._data[index.row()][index.column()][role] = value
        self.dataChanged.emit(index, index, [role])
        return True
Answered By: musicamante
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.