Is it possible to disable the highlighting effect on a selected cell in a QTableWidget?

Question:

In this example, I have a grid with cells that can be colored and uncolored.

import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setGeometry(100, 100, 750, 750)

        self.rows=60
        self.columns=50
        self.tableWidget=QTableWidget(self.rows, self.columns)
        self.tableWidget.horizontalHeader().hide()
        self.tableWidget.verticalHeader().hide()
        
        self.activated={}
        self.item={}
        for r in range(self.rows):
            for c in range(self.columns):
                self.activated[r,c]=False
                self.item[r,c]=QTableWidgetItem()
                self.tableWidget.setItem(r,c,self.item[r,c])

        self.tableWidget.setFocusPolicy(Qt.NoFocus)
        #self.tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
        #self.tableWidget.setStyleSheet("""QTableView:item:selected{selection-background-color: rgb(100,100,150);}""")
        #self.tableWidget.setStyleSheet("""QTableView:item:selected{selection-background-color: transparent;}""")
        self.tableWidget.setEditTriggers(QTableWidget.NoEditTriggers)
        
        self.tableWidget.cellClicked.connect(self.dothing)
        self.tableWidget.cellDoubleClicked.connect(self.dothing)

        self.setCentralWidget(self.tableWidget)
    
    def dothing(self,r,c):
        if self.activated[r,c]: 
            self.activated[r,c]=False
            self.item[r,c].setBackground(QColor(255,255,255))

        else: 
            for column in range(60):
                self.activated[column]=False
                self.item[column,c].setBackground(QColor(255,255,255))
            self.activated[r,c]=True
            self.item[r,c].setBackground(QColor(100,100,150))

app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

However, as you might notice from running the code, the colored-in color is obscured by the highlight color, as a result of selection.
Without changing the selection mode (ie. self.tableWidget.setSelectionMode(QAbstractItemView.NoSelection)), or adjusting the stylesheet, is it possible for me to completely disable/bypass the highlight color paint event?

I must note that it is important that the selection mode and stylesheets aren’t adjusted, as I intend on using the selection range to create a length of colored cells in a single row at once. If selection is disabled, then this cannot happen at all, and if the stylesheet is edited, then it would show multiple rows of cells being highlighted rather than just one row.

Hence, these solutions from previous SO questions such as this don’t really work for me.

Asked By: rllysleepy

||

Answers:

The solution is quite simple: use an item delegate.

Just subclass a QStyledItemDelegate and reimplement its initStyleOption() in order to override the selected state of the item, so that it will be painted just using the default background color:

class DeselectedDelegate(QStyledItemDelegate):
    def initStyleOption(self, opt, index):
        super().initStyleOption(opt, index)
        opt.state &= ~QStyle.State_Selected


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        # ...
        self.tableWidget.setItemDelegate(
            DeselectedDelegate(self.tableWidget))

Note that setting the backgrounds of all the items in the column is not very effective, especially if it’s only for display reasons and always consistent as your code shows (not to mention that you used the name column that actually refers to a row).

A more appropriate solution could just use the self.activated dictionary and keep the same reference in the delegate. By further implementing the above, you could just change the backgroundBrush of the style option whenever the row/column reference is True in the dict, and ensure that all items are repainted[1]:

class DeselectedDelegate(QStyledItemDelegate):
    def __init__(self, parent, activated):
        super().__init__(parent)
        self.activated = activated

    def initStyleOption(self, opt, index):
        super().initStyleOption(opt, index)
        opt.state &= ~QStyle.State_Selected
        if self.activated[index.row(), index.column()]:
            opt.backgroundBrush = QColor(100,100,150)


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        # ...
        self.tableWidget.setItemDelegate(
            DeselectedDelegate(self.tableWidget, self.activated))
        # note that added argument

    def dothing(self, row, column):
        active = not self.activated[row, column]
        self.activated[row, column] = active
        if active:
            for r in range(self.tableWidget.rowCount()):
                if r != row:
                    self.activated[r, column] = False
        self.tableWidget.viewport().update()

[1] Note that I specifically called the viewport.update(), because item views, as all Qt scroll areas paint their contents in their viewport (calling self.tableWidget.update() would have no result); while repainting the whole viewport might not seem very optimal, it’s certainly better than setting the background of all items in the same column (including those that are not currently shown). It’s worth noticing that it is possible to update only a region of the viewport (in your case, that referring to the column interested by the value changes), but, unless extremely complex painting is required on each item (for instance, custom pixmaps), that solution is not worth it.

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.