Search code examples
python-3.xqtpyqtpyside2qtstylesheets

How to set color of the whole row in QTreeView when item is selected?


I'm working on a PySide2 GUI project on Centos7.6.

I attempted to change the color of a row when it is selected:

 QTreeView::item:selected {
        background-color: rgba(48, 140, 198, 128);
    }

Here is what I got:

selected row

Item selector did not affect the whole row.

After some search on internet, including GTP, I added branch selector as follow:

QTreeView::branch:selected {
        background-color: rgba(48, 140, 198, 128);
    }

Unfortunately, it still did not work as I expected:

branch selector

Branch selector did not make the front area transparent, instead it made expand/collapse mark disappear.

Then I tried to overwrite paint method in Delegate:

if option.state & QStyle.State_Selected:
            painter.fillRect(option.rect, QColor(48, 140, 198, 128))

But still:

overwrite paint method

What I want is to change the whole row when it is selected, but somehow I can not make it work on branch area. Now I am really confused, can someone help me?


Solution

  • Your attempts didn't work for the following reasons:

    • the QTreeView::item selector only specifies the item, but tree views use an indentation level that horizontally shifts the item rectangle: most styles just draw the selection background only within the item geometry;
    • in almost any complex widget (see the note at the bottom of the sub-controls QSS documentation), when customizing one property or a subcontrol (including one property of a subcontrol), all other properties must be set: QTreeView::branch certainly falls into the category, meaning that setting the branch background will completely ignore any default behavior, including drawing the arrow;
    • similarly to the QTreeView::item attempt, overriding the paint of the delegate will not help, because the delegate can only draw within the contents of the item, which is horizontally translated;

    Some styles actually draw the whole row in the highlighted color, but that is not a requirement, and completely depends on the style implementation, meaning that we cannot rely on partial stylesheets or common QStyle functions using proxy styles (which would also stop working with style sheets).

    The (almost) only reliable solution is to completely draw the background of the whole row before the item, and that must be done from the tree view, which is achieved by overriding QTreeView.drawRow():

    • if the index is selected:
      • draw the custom background;
      • force the Highlight role of the option.palette to a transparent brush, so that the delegate won't draw anything even if the item is selected;
    • else:
      • restore the default palette color, so that it may be properly used for other meanings;
    • finally, call the default implementation using super().drawRow(...), which will eventually draw the background of the item using the above brush if necessary;

    The last passage is quite important, because the QStyleOptionViewItem used in some functions of item views is normally created just once and then reused in the various for loops that iterate through items; this is done for optimization purposes, so that only the aspects that should be changed are actually modified in the option members, but has also the drawback that any change done to the option that is not known to the view/delegate will also not be restored back.

    The palette is normally unmodified while iterating through items, so if you change it in the meantime, any following item may inherit it even if it shouldn't: for instance, some QStyles (probably including WindowsXP/WindowsVista) use the Highlight role color to decide how to draw the background of hovered items and show a shade of that specific palette color role, and that color may also be important to decide an appropriate contrasting color for the text if not explicitly set.

    In the following example I'm using QTreeWidget to provide a simple MRE, but the drawRow() implementation would be the same in a QTreeView as well.

    class TreeTest(QTreeWidget):
        # default "static" class members, so we don't need to continuously and 
        # unnecessarily create brush objects every time they are required
        SelectBrush = QBrush(QColor(48, 140, 198, 128))
        NoBrush = QBrush(Qt.transparent, style=Qt.NoBrush)
    
        def __init__(self):
            super().__init__()
            top = QTreeWidgetItem(self, ['top'])
            QTreeWidgetItem(top, ['whatever'])
            QTreeWidgetItem(top, ['wherever'])
            QTreeWidgetItem(top, ['whenever'])
            self.expandAll()
    
        def drawRow(self, qp, opt, index):
            if self.selectionModel().isSelected(index):
                qp.fillRect(opt.rect, self.SelectBrush)
                opt.palette.setBrush(QPalette.Highlight, self.NoBrush)
            else:
                # IMPORTANT! The default highlight palette must be restored!
                opt.palette.setBrush(
                    QPalette.Highlight, self.palette().highlight())
            super().drawRow(qp, opt, index)
    
    
    app = QApplication([])
    test = TreeTest()
    test.show()
    app.exec()
    

    Note that the NoBrush explicitly sets the color to Qt.transparent instead of being a simpler QBrush(): while QBrush() with no arguments has a Qt.BrushStyle.NoBrush style (meaning that it should not be painted), some styles would still use it's color() as a reference, and by default that color is a valid QColor: full opaque black. Setting it as transparent ensures that the any attempt from the style to use that color for any purpose will result in no painting at all (at least in theory, assuming that the painting doesn't ignore the alpha channel).

    A possible variation, if you want to customize the color of the selection with stylesheets, would be to set the selection-background-color property of the tree view.

    Note, though, that using style sheets always complicate things for custom widgets that rely on style functions. Any attempt to use QTreeView::item:<whatever> will probably invalidate all this.

    class Tree(QTreeWidget):
        NoBrush = QBrush(Qt.transparent, style=Qt.NoBrush)
    
        def __init__(self):
            ...
            self.setStyleSheet('''
                QTreeView { 
                    selection-background-color: rgba(48, 140, 198, 128); 
                }
            ''')
    
        def drawRow(self, qp, opt, index):
            brush = self.palette().highlight()
            if self.selectionModel().isSelected(index):
                qp.fillRect(opt.rect, brush)
                brush = self.NoBrush
            opt.palette.setBrush(QPalette.Highlight, brush)
            super().drawRow(qp, opt, index)
    

    A possible workaround for the issue noted above would be to use a custom delegate that automatically unsets the State_Selected flag of the option, but be aware that this could potentially break other things unexpectedly.

    class Delegate(QStyledItemDelegate):
        def initStyleOption(self, opt, index):
            super().initStyleOption(opt, index)
            opt.state &= ~QStyle.State_Selected
    
    
    class Tree(QTreeWidget):
        ...
        def __init__(self):
            ...
            self.setItemDelegate(Delegate(self))
        ...
    

    Another important aspect to remember is that the user may use an alternate color scheme (for instance, for dark mode), which could potentially make the item text almost invisible under certain conditions.

    As a precaution, you should probably also set a proper contrasting foreground brush for the Text role, remembering to reset it to default whenever the index is not selected:

        def drawRow(self, qp, opt, index):
            textColor = self.palette().text()
            ...
            if self.selectionModel().isSelected(index):
                textColor = <QBrush color for text with appropriate contrast>
                ...
            opt.palette.setBrush(QPalette.Text, textColor)
            ...
    

    As said above, though, setting a QTreeView::item { color: ...; } will completely disregard all that, since the style always has precedence on such matters. The only alternative to customize the default text color for items is to set the text property for the generic QTreeView selector, similarly to what done with the above selection-background-color.

    Finally, as said, this is not 100% reliable. Most importantly, some styles may completely disregard/break any attempt done with the above, and custom delegate subclasses that override paint() on their own should be aware of all this.