Search code examples
pythonpython-3.xpyside2

background color of cells in QTableview, PySide2


Is it possible to conditionally change the background color of items in a QTableView, using PySide2? I've read a lot on the model view framework . I cannot figure out if it is necessary to use a Delegate or not. Recently I was able to get a column of checkboxes without a Delegate. I believe that the virtual methods setItemData(index, roles) and itemData(index) could be what I need. However, there is no QMap in PySide2. My model must need somewhere to store the extra information to be used by QtCore.Qt.BackgroundRole (that enum, btw, says "the background brush used for items rendered with the default delegate") If I don't specify a delegate, is the "default delegate" used?. Should I be using QStandardItemModel instead? In the example code below, how would I get a particular column's background color to be red based on some thresholds (the min and max column are the thresholds?

from PySide2.QtWidgets import (QWidget, QApplication, QTableView,QVBoxLayout)
import sys
from PandasModel2 import  PandasModel2
import numpy as np
import pandas as pd
class Example(QWidget):
    def __init__(self):
        super().__init__()
        self.setGeometry(300, 300, 700, 300)
        self.setWindowTitle("QTableView")
        self.initData()
        self.initUI()

    def initData(self):

        data = pd.DataFrame(np.random.randint(1,10,size=(6,4)), columns=['Test#','MIN', 'MAX','MEASURED'])
        data['Test#'] = [1,2,3,4,5,6]                    
        #add the checkable column to the DataFrame
        data['Check'] = True
        self.model = PandasModel2(data)

    def initUI(self):
        self.tv = QTableView(self)
        self.tv.setModel(self.model)
        vbox = QVBoxLayout()
        vbox.addWidget(self.tv) 
        self.setLayout(vbox)  

app = QApplication([])
ex = Example()
ex.show()
sys.exit(app.exec_())

And I have a custom model using a pandas dataFrame:

import PySide2.QtCore as QtCore
class PandasModel2(QtCore.QAbstractTableModel):
    """
    Class to populate a table view with a pandas dataframe.
    This model is non-hierachical. 
    """
    def __init__(self, data, parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self._data = data

    def rowCount(self, parent=None):
        return self._data.shape[0]

    def columnCount(self, parent=None):
        return self._data.shape[1]

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role==QtCore.Qt.DisplayRole:
            if index.column() != 4: 
            #don't want what determines check state to be shown as a string
                if index.isValid():              
                    if index.column() in [1,2,3]:
                        return '{:.3f}'.format(self._data.iloc[index.row(), index.column()])    
                    if index.column() == 0:
                        return '{:.2f}'.format(self._data.iloc[index.row(), index.column()])
                    return str(self._data.iloc[index.row(), index.column()])
        if role==QtCore.Qt.CheckStateRole:  
            if index.column()==4:#had to add this check to get the check boxes only in column 10
                if self._data.iloc[index.row(), index.column()] == True:
                    return QtCore.Qt.Checked
                else:
                   return QtCore.Qt.Unchecked

    def getMinimum(self, row):
        return self._data.iloc[row, self.getColumnNumber('MIN')]
    def getMaximum(self, row):
        return self._data.iloc[row, self.getColumnNumber('MAX')]

    def getColumnNumber(self, string):
        '''
        Given a string that identifies a label/column, 
        return the location of that label/column.
        This enables the config file columns to be moved around. 
        '''
        return self._data.columns.get_loc(string)

    def headerData(self, col, orientation, role):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return self._data.columns[col]
        return None
    def flags(self, index):
        '''
        The returned enums indicate which columns are editable, selectable, 
        checkable, etc. 
        The index is a QModelIndex. 
        '''
        if index.column() == self.getColumnNumber('Check'):
            #print(index.column())
            return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
        else:
            return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
        return QtCore.Qt.ItemIsEnabled

    def setData(self, index, value, role=QtCore.Qt.DisplayRole):
        """Set the value to the index position depending on Qt::ItemDataRole and data type of the column
        Args:
            index (QtCore.QModelIndex): Index to define column and row.
            value (object): new value.
            role (Qt::ItemDataRole): Use this role to specify what you want to do.
        Raises:
            TypeError: If the value could not be converted to a known datatype.
        Returns:
            True if value is changed. Calls layoutChanged after update.
            False if value is not different from original value.
        """
        if not index.isValid(): 
            return False
        if role == QtCore.Qt.DisplayRole: #why not edit role?
            self._data.iat[index.row(),index.column()]= value
            self.layoutChanged.emit()
            return True
        elif role == (QtCore.Qt.CheckStateRole | QtCore.Qt.DisplayRole):
            #this block does get executed when toggling the check boxes, 
            #verified with debugger. Although the action is the same 
            #as the block above! 
            self._data.iat[index.row(),index.column()]= value
            self.layoutChanged.emit()
            return True
        else:
            return False

Solution

  • The delegate by default uses the BackgroundRole information if it is available so the solution is just to return a QColor, QBrush or similar.

    from PySide2 import QtCore, QtGui
    
    class PandasModel2(QtCore.QAbstractTableModel):
        # ...
        def data(self, index, role=QtCore.Qt.DisplayRole):
            if not index.isValid():
                return
            if not (0 <= index.row() < self.rowCount() and 0 <= index.column() <= self.columnCount()):
                return
            value = self._data.iloc[index.row(), index.column()]
            if role == QtCore.Qt.DisplayRole:
                if index.column() != 4: 
                    if index.column() in [1,2,3]:
                        return '{:.3f}'.format(value)    
                    if index.column() == 0:
                        return '{:.2f}'.format(value)
                    return str(value)
            elif role == QtCore.Qt.CheckStateRole:  
                if index.column() == 4:
                    return QtCore.Qt.Checked if value else QtCore.Qt.Unchecked
            elif index.column() == self.getColumnNumber('MEASURED'):
                if role == QtCore.Qt.BackgroundRole:
                    if self.getMinimum(index.row()) <= value <= self.getMaximum(index.row()):
                        return QtGui.QColor("red")