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:
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 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:
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?
Your attempts didn't work for the following reasons:
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;QTreeView::branch
certainly falls into the category, meaning that setting the branch background will completely ignore any default behavior, including drawing the arrow;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()
:
Highlight
role of the option.palette
to a transparent brush, so that the delegate won't draw anything even if the item is selected;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.