试验环境:
特别说明试验环境是有原因的。Qt 开发的程序在不同操作系统上有不同的显示效果——即风格,这比较好理解。但是,不同版本的 Qt 在同一操作系统上的显示效果也是有不同的。以树形控件为例,Qt 5.15.2 和 6.7.1 版本下 selectItems 行为的选中样式是不同的,要注意版本升级问题,这是我踩过的坑。
先看效果对比图:

左边的树是 QTreeWidget 默认的选中样式,右边的树是定制后的选中效果。主要有以下几个不同点:
- 选中行的前景色,也就是文本颜色
- 选中行的背景色
- 选中范围
- 行高
接下来说明具体实现。
1.1 修改 selectionBehavior 值为 SelectItems

修改后的选中范围由整行变成了仅有内容区域,虽然不是最终效果,但是它提供了自定义内容区域矩形的可能:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | 
 case PE_PanelItemViewItem:
 if (const QStyleOptionViewItem *vopt = qstyleoption_cast<const QStyleOptionViewItem *>(opt)) {
 QPalette::ColorGroup cg = (widget ? widget->isEnabled() : (vopt->state & QStyle::State_Enabled))
 ? QPalette::Normal : QPalette::Disabled;
 if (cg == QPalette::Normal && !(vopt->state & QStyle::State_Active))
 cg = QPalette::Inactive;
 
 if (vopt->showDecorationSelected && (vopt->state & QStyle::State_Selected)) {
 p->fillRect(vopt->rect, vopt->palette.brush(cg, QPalette::Highlight));
 } else {
 if (vopt->backgroundBrush.style() != Qt::NoBrush) {
 QPointF oldBO = p->brushOrigin();
 p->setBrushOrigin(vopt->rect.topLeft());
 p->fillRect(vopt->rect, vopt->backgroundBrush);
 p->setBrushOrigin(oldBO);
 }
 
 if (vopt->state & QStyle::State_Selected) {
 QRect textRect = subElementRect(QStyle::SE_ItemViewItemText,  opt, widget);
 p->fillRect(textRect, vopt->palette.brush(cg, QPalette::Highlight));
 }
 }
 }
 break;
 
 | 
后边代码实现里可以看到,正是通过重写 subElementRect 虚函数调整相关矩形的。
1.2 修改调色板

Highlight 项是选中行背景色,HighlightText 是选中行文本颜色。
背景色一定不要用 qss 去配置!如果有
| 12
 3
 
 | QTreeView::item:selected {background-color: red;
 }
 
 | 
这样的声明,一定把 background-color 去掉。原因见下面代码片段:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | 
 if (pseudoElement != PseudoElement_None) {
 QRenderRule subRule = renderRule(w, opt, pseudoElement);
 if (subRule.hasDrawable()) {
 subRule.drawRule(p, rect);
 } else {
 baseStyle()->drawPrimitive(pe, opt, p, w);
 }
 } else {
 baseStyle()->drawPrimitive(pe, opt, p, w);
 }
 
 | 
qss 背景被认为是 drawable,它的渲染矩形即 rect 宽是整行宽度,而非仅有内容区域。
1.3 修改 icon 选中模式
同 palette 一样,一个 QIcon 对象是一组状态的集合。默认选中状态下,会有一个暗色的掩码覆在图标文件之上(效果如 1.1 配图),这通常不是期望的效果。可以按下图把 Selected 两个分量值修改。

QIcon 对应的编程接口,可以查阅文档或看一下 .ui 生成的 C++ 代码。
2. 自定义 style 类
2.1 自定义 style 类
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 
 | static const int MARGIN = 8;
 class BgStyle : public QCommonStyle
 {
 public:
 int pixelMetric(PixelMetric metric, const QStyleOption* option, const QWidget* widget) const override
 {
 if (metric == QStyle::PM_FocusFrameHMargin)
 {
 return MARGIN;
 }
 return QCommonStyle::pixelMetric(metric, option, widget);
 }
 
 QSize sizeFromContents(ContentsType ct, const QStyleOption* opt, const QSize& contentsSize, const QWidget* w) const override
 {
 auto size = QCommonStyle::sizeFromContents(ct, opt, contentsSize, w);
 if (ct == QStyle::CT_ItemViewItem)
 {
 size.rheight() += 2 * MARGIN;
 }
 return size;
 }
 
 QRect subElementRect(SubElement subElement, const QStyleOption* option, const QWidget* widget) const override
 {
 auto rect = QCommonStyle::subElementRect(subElement, option, widget);
 if (subElement == SE_ItemViewItemDecoration)
 {
 rect.adjust(-MARGIN, 0, -MARGIN, 0);
 }
 else if (subElement == SE_ItemViewItemText)
 {
 
 
 rect.adjust(-2 * MARGIN, -MARGIN, -2 * MARGIN, MARGIN);
 }
 return rect;
 }
 
 void drawPrimitive(PrimitiveElement pe, const QStyleOption* opt, QPainter* p, const QWidget* w) const override
 {
 if (pe == PE_FrameFocusRect)
 {
 return;
 }
 QCommonStyle::drawPrimitive(pe, opt, p, w);
 }
 };
 
 | 
调用:
| 12
 
 | ui.treeWidget->setStyle(new BgStyle());
 
 | 
3. 吐槽
Qt 的 Style 是一套非常非常复杂的系统,下边是 Style 类的关系图。

在有相关 qss 配置时,控件实际持有的是 QStyleSheetStyle 类型的对象;没有 qss 配置时,依运行操作系统不同,可能是 “windows” 或 “windowsvista” 风格。
你可能想从 QWindowsVistaStyle 或 QStyleSheetStyle 派生出一个子类,微调其中一些效果,但这是做不到的——QWindowsStyle 及其子类,对用户是不可见的,即开发者最多只能从 QCommonStyle 类继承。
然后你想到利用 QProxyStyle,为现有 Style 类设置一个代理,改写其个别行为,似乎比重写 QCommonStyle 类更方便。但实测发现,QProxyStyle::sizeFromContents 总是不能触发,导致不能调整行高。
把类关系理清地差不多了,还有具体的渲染过程——尤其是对树形控件这类 ItemView,渲染控制分散在了 View、ItemDelegate、qss、Style 里,可渲染的元素又被层层分解,控件、行、indicator、图标、文本、选中态、焦点态,逻辑无处不在;有些又互有影响,需要找准上层的统一修改入口。