I have a QListView
which uses a model to display custom Alarm
objects as text applying different colors based on the Alarm.level
attribute of each object.
@dataclass
class Alarm:
name: str
level: int
started_at: datetime
class Alarm_Viewer_Model(QAbstractListModel):
def __init__(
self,
alarms: List[Alarm] = None,
alarm_colors: Dict[int, QColor] = None,
*args,
**kwargs
):
super().__init__(*args, **kwargs)
self._alarms = alarms or []
self._alarm_colors = alarm_colors or {}
def data(self, index: QModelIndex, role: int):
alarm = self._alarms[index.row()]
if role == Qt.DisplayRole:
dt = alarm.started_at.strftime('%Y/%m/%d %H:%M:%S')
return f"{dt} - {alarm.level.name} - {alarm.name}"
elif role == Qt.ForegroundRole:
alarm_color = self._alarm_colors.get(alarm.level, Qt.black)
return QBrush(alarm_color)
This works fine until I try load a stylesheet which applies a default text color to all widgets. In this case the stylesheet color overrides whatever color is set by the model (other questions, like this or this, confirm that this is normal Qt behaviour).
My question is: how can I still have control over the style of each list item while a generic style is applied via the stylesheet?
One way I tried to do this (which is also suggested in one of the links) is to have the model set a custom property on the item which could be accessed by the stylesheet with an appropriate selector (e.g.
QListView::item[level=1] { color: red; }
). However, I could not find any way to set the property in the list view item: is it even possibile?Another way that was suggested (but have not yet tried) is via QStyledItemDelegate. However from what I have seen, even if it works, it looks way overkill: is it the only way to solve this?
PS: I tagged this with PySide2 as I am using it, but am more interested in the general Qt behaviour: finding a way to implement it in PySide2 specifically is a secondary problem for now.
Qt Style Sheets are fun and useful, but they also are double-edged swords.
An important thing to always be aware of is that they mostly respect the concept behind CSS, from which they originate.
When a QSS is set, it can possibly, if not completely override the default behavior, similarly to what happens in web browsers.
In the Qt world, this works with a fundamental hierarchy:
Remember that a style will always have the final word on how any aspect of the widget is eventually drawn, as long as you call its functions. That's also why it is wrong to set generic properties on parent/container widgets.
Whenever a QSS affects a widget in some way, it accesses its render rules and eventually reverts back to the upper level of the style hierarchy, but it will always override
If a
QAbstractItemDelegate::item
rule is set, this means that it will completely override any underlying QStyle function.When the
paint()
function of QStyledItemDelegate is called, it will just callinitStyleOption()
with the given option and index, and then asks the style to paint it. How the item is finally drawn is completely up to the style. Normally, styles follow theQt.BackgroundRole
(and/or the giveoption.backgroundBrush
) andQt.ForegroundRole
, but that's just an assumption: a style may decide to use its own colors and completely ignore the model.This brings us back to the point: as said above, a QStyleSheetStyle always overrides the underlying QStyle as soon as some of its properties are set.
Setting the
color
orbackground[-color]
properties in a QSS means that those rules are built upon the default styling, which is also valid for subcontrols like::item
.Now, for generic widget properties, it's quite easy to access the palette that the QStyleSheetStyle updates, but, unfortunately, there's no direct access to subcontrols rules.
As explained in a comment to the related QTBUG-75191, they don't plan to change the current behavior. That may be annoying, but I completely understand it.
So, how can we work around this?
Well, it depends. As the comment above says:
I explained it in the beginning: QSS can be double-edged swords. If you want to use them, you have to accepts their shortcomings.
As long as you have complete and absolute control over the views, their models and the stylesheet syntax, you can go along with a solution similar to the one you already got (but you should probably get the
textRect
from the stylesubElementRect()
, usingSE_ItemViewItemText
, and finally paint withdrawItemText
).Unfortunately, this won't solve all problems.
For example, while the answer linked above may provide a possible solution, it won't work well for a QSS that also sets borders, margins and paddings.
A possible (but bad) work around can be to use a fake view that is used as style target, eventually altering its stylesheet for each item. That's not good, it obviously is a terrible choice if you have lots of items, but it may still be an alternative:
Note that you must create the delegate with the view as its parent (which is what should be always be done, anyway).
In reality, if you want custom painting for each item, QSS is probably not the right choice: it may be easy, but such advanced painting customization isn't compatible with QSS in general. Just use custom delegates, ensure that you properly use the QStyle functions of the option's
widget
to get proper geometries and request drawings, and possibly consider to do the whole painting on your own.