Search code examples
pythonpyqtpysideqtableview

Change background of model index for QAbstractTableModel in PySide6


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.


Solution

  • 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