试验环境:
特别说明试验环境是有原因的。Qt 开发的程序在不同操作系统上有不同的显示效果——即风格,这比较好理解。但是,不同版本的 Qt 在同一操作系统上的显示效果也是有不同的。以树形控件为例,Qt 5.15.2 和 6.7.1 版本下 selectItems
行为的选中样式是不同的,要注意版本升级问题,这是我踩过的坑。
先看效果对比图:
左边的树是 QTreeWidget 默认的选中样式,右边的树是定制后的选中效果。主要有以下几个不同点:
- 选中行的前景色,也就是文本颜色
- 选中行的背景色
- 选中范围
- 行高
接下来说明具体实现。
1.1 修改 selectionBehavior
值为 SelectItems
修改后的选中范围由整行变成了仅有内容区域,虽然不是最终效果,但是它提供了自定义内容区域矩形的可能:
1 2 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 去配置!如果有
1 2 3
| QTreeView::item:selected { background-color: red; }
|
这样的声明,一定把 background-color
去掉。原因见下面代码片段:
1 2 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 类
1 2 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); } };
|
调用:
1 2
| 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、图标、文本、选中态、焦点态,逻辑无处不在;有些又互有影响,需要找准上层的统一修改入口。