python-3.xpyqt5

QTableWidget item color lost when item out of focus?


I set 4th row color to red of the QTableWidget, When I select an item and then click the QLineEdit, the Item color had lose? Can someone solve this?

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

import random

class Win(QWidget):
    def __init__(self):
        super().__init__()
        
        self.le = QLineEdit()
        self.table = QTableWidget()

        self.table.setHorizontalHeaderLabels(list("ABCD"))
        self.table.setColumnCount(4)
        self.table.setRowCount(10)
       
        for row in range(self.table.rowCount()):
            for col in range(self.table.columnCount()):
                it = QTableWidgetItem( chr(random.randint(11300, 21300) ) )
                self.table.setItem(row, col, it)
        
        row = 3
        for col in range(self.table.columnCount()):
            self.table.item(row, col).setBackground(QColor(Qt.red))
            
        
        lay = QVBoxLayout()
        lay.addWidget(self.le)
        lay.addWidget(self.table)
        
        self.setLayout(lay)
            

app = QApplication([])
win = Win()
win.show()
app.exec()

enter image description here


Solution

  • No, the color is not lost.

    What you're seeing is the inactive selection color: if you look more carefully, it has not the same color as the other items.

    There are various possibilities, then.

    Always show the selection color

    This is the easiest solution, but has a drawback: the table will look focused even if it's not, since the selected items will always have the same color.

            p = self.table.palette()
            p.setBrush(p.Inactive, p.Highlight, p.brush(p.Highlight))
            self.table.setPalette(p)
    

    Don't show the selection when inactive

    This is slightly better than the above, but still not perfect: the selection should always be shown, even with a different color when the view is not focused, but with the following there is no way to tell which item is selected until the view gets focused again.

    To do so, we can use an item delegate and just clear the State_Selected flag from the option if the view is not active:

    class MyDelegate(QStyledItemDelegate):
        def initStyleOption(self, opt, index):
            super().initStyleOption(opt, index)
            if (opt.state & QStyle.State_Selected
                and not opt.state & QStyle.State_Active
                and index.data(Qt.BackgroundRole) is not None
            ):
                opt.state &= ~QStyle.State_Selected
    
    # ...
    
    self.table.setItemDelegate(MyDelegate(self.table))
    

    Blend the background and the selection color

    In this case, we blend the background color with the palette selection color, overlaying the Highlight color by a factor of half its alpha channel. Note that this might not work in certain systems that don't directly use the palette for painting.

    class MyDelegate(QStyledItemDelegate):
        def initStyleOption(self, opt, index):
            super().initStyleOption(opt, index)
            if opt.state & QStyle.State_Selected:
                bgd = index.data(Qt.BackgroundRole)
                if bgd is None or not bgd.isOpaque():
                    return
                r1, g1, b1, a1 = bgd.color().getRgbF()
                if opt.widget and opt.widget.isEnabled():
                    if opt.state & QStyle.State_Active:
                        group = QPalette.Active
                    else:
                        group = QPalette.Inactive
                else:
                    group = QPalette.Disabled
                r2, g2, b2, a2 = opt.palette.color(group, QPalette.Highlight).getRgbF()
                a2 *= .5
                factor = (1 - a2) * a1
                a = factor + a2
                r = (factor * r1 + a2 * r2) / a
                g = (factor * g1 + a2 * g2) / a
                b = (factor * b1 + a2 * b2) / a
                opt.palette.setColor(
                    QPalette.Highlight, QColor.fromRgbF(r, g, b, a))
    

    Override the painting

    In case the solution above won't work, the only remaining alternative is to override the painting of the item. For this situation, the simplest option is to draw the base of the item in an "unselected" state (showing the default background), then draw it again with the selected state but with half opacity, and finally draw the item contents clearing any possible background.

    class MyDelegate(QStyledItemDelegate):
        def paint(self, qp, opt, index):
            if (opt.state & QStyle.State_Selected 
                and index.data(Qt.BackgroundRole) is not None
            ):
                opt = QStyleOptionViewItem(opt)
                self.initStyleOption(opt, index)
                widget = opt.widget
                style = widget.style() if widget else QApplication.style()
                opt.state &= ~style.State_Selected
                style.drawPrimitive(style.PE_PanelItemViewItem, opt, qp, widget)
                qp.save()
                qp.setOpacity(.5)
                opt.state |= style.State_Selected
                style.drawPrimitive(style.PE_PanelItemViewItem, opt, qp, widget)
                qp.restore()
                opt.palette.setColor(QPalette.Highlight, Qt.transparent)
                opt.backgroundBrush = QBrush(Qt.NoBrush)
                style.drawControl(style.CE_ItemViewItem, opt, qp, widget)
            else:
                super().paint(qp, opt, index)