场景 | 推荐方式 |
---|---|
简单样式更改 | setStyleSheet |
复杂自绘 | 重写 paintEvent |
保留原生样式+扩展 | QStyleOption + style()->drawControl |
全局样式统一 | 自定义 QStyle 或 QProxyStyle |
高自由度图形绘制 | QGraphicsItem / QML |
setStyleSheet
QPushButton* btn = new QPushButton("Click Me",&widget);
btn->setStyleSheet("QPushButton { background-color: green; color: white; border-radius: 10px; }");
上面例子是一个按钮使用了setStyleSheet方法绘制,内部流程可以通过Qt源码发现
void QWidget::setStyleSheet(const QString& styleSheet)
{
Q_D(QWidget);
if (data->in_destructor)
return;
d->createExtra();
QStyleSheetStyle *proxy = qt_styleSheet(d->extra->style);
d->extra->styleSheet = styleSheet;
//...
if (testAttribute(Qt::WA_SetStyle)) {
d->setStyle_helper(new QStyleSheetStyle(d->extra->style), true);
} else {
d->setStyle_helper(new QStyleSheetStyle(nullptr), true);
}
}
在没有进行设置过style的情况下,代码走到d->setStyle_helper(new QStyleSheetStyle(nullptr), true);
进入后发现
void QWidgetPrivate::setStyle_helper(QStyle *newStyle, bool propagate)
{
Q_Q(QWidget);
QStyle *oldStyle = q->style();
createExtra();
#ifndef QT_NO_STYLE_STYLESHEET
QPointer<QStyle> origStyle = extra->style;
#endif
extra->style = newStyle;
可以看到新创建的QStyleSheetStyle已经赋值给extra的style里了,这个extra是QWidgetPrivate类的一个成员变量std::unique_ptr extra;其中QWExtra是一个结构体,用来存储widget的部分信息。其中一个widget的style就是存放在该结构体中的style成员中。
QStyle *QWidget::style() const
{
Q_D(const QWidget);
if (d->extra && d->extra->style)
return d->extra->style;
return QApplication::style();
}
所以,在setStyleSheet之后,Qt会为你创建一个QStyleSheetStyle作为你这个控件(这里是btn)的style,然后在paintEvent中
void QStylePainter::drawControl(QStyle::ControlElement ce, const QStyleOption &opt)
{
wstyle->drawControl(ce, &opt, this, widget);
}
就会进入到QStyleSheetStyle::drawControl的方法中了。
那么setStyleSheet()函数入参QStr如何生效呢?上面setStyleSheet实现中可以看到d->extra->styleSheet = styleSheet;存下了设置的styleSheet,而在QStyleSheetStyle::styleRules会用QStyleSheetStyleSelector解析存下的styleSheet
QVector<QCss::StyleRule> QStyleSheetStyle::styleRules(const QObject *obj) const
{
//...
QStyleSheetStyleSelector styleSelector;
StyleSheet defaultSs;
QHash<const void *, StyleSheet>::const_iterator defaultCacheIt = styleSheetCaches->styleSheetCache.constFind(baseStyle());
if (defaultCacheIt == styleSheetCaches->styleSheetCache.constEnd()) {
defaultSs = getDefaultStyleSheet();
QStyle *bs = baseStyle();
styleSheetCaches->styleSheetCache.insert(bs, defaultSs);
QObject::connect(bs, SIGNAL(destroyed(QObject*)), styleSheetCaches, SLOT(styleDestroyed(QObject*)), Qt::UniqueConnection);
} else {
defaultSs = defaultCacheIt.value();
}
styleSelector.styleSheets += defaultSs;
if (!qApp->styleSheet().isEmpty()) {
StyleSheet appSs;
QHash<const void *, StyleSheet>::const_iterator appCacheIt = styleSheetCaches->styleSheetCache.constFind(qApp);
if (appCacheIt == styleSheetCaches->styleSheetCache.constEnd()) {
QString ss = qApp->styleSheet();
//...
styleSheetCaches->styleSheetCache.insert(qApp, appSs);
} else {
appSs = appCacheIt.value();
}
styleSelector.styleSheets += appSs;
}
QVector<QCss::StyleSheet> objectSs;
for (const QObject *o = obj; o; o = parentObject(o)) {
QString styleSheet = o->property("styleSheet").toString();
if (styleSheet.isEmpty())
continue;
StyleSheet ss;
QHash<const void *, StyleSheet>::const_iterator objCacheIt = styleSheetCaches->styleSheetCache.constFind(o);
if (objCacheIt == styleSheetCaches->styleSheetCache.constEnd()) {
parser.init(styleSheet);
//...
ss.origin = StyleSheetOrigin_Inline;
styleSheetCaches->styleSheetCache.insert(o, ss);
} else {
ss = objCacheIt.value();
}
objectSs.append(ss);
}
for (int i = 0; i < objectSs.count(); i++)
objectSs[i].depth = objectSs.count() - i + 2;
styleSelector.styleSheets += objectSs;
StyleSelector::NodePtr n;
n.ptr = const_cast<QObject *>(obj);
QVector<QCss::StyleRule> rules = styleSelector.styleRulesForNode(n);
styleSheetCaches->styleRulesCache.insert(obj, rules);
return rules;
}
由低到高优先级,其中越靠近当前对象的样式,其 depth 越大,depth 越大,优先级越高!
来源 | 示例说明 |
---|---|
默认 style fallback | Qt 内建默认规则 |
应用级 styleSheet | qApp->setStyleSheet(...) |
父对象级别样式 | 父控件设置的样式将对子控件生效 |
对象本地的 setStyleSheet |
button->setStyleSheet(...) |
这里提出一个问题,给大家思考一下,具体答案在上面有给出过,调用setStyleSheet后创建的QStyleSheetStyle 是所有控件共用一个还是多个控件分别一个?
答案多个控件分别一个,源码中可以看到在setStyleSheet里会d->setStyle_helper(new QStyleSheetStyle(…), true);创建一个新的QStyleSheetStyle 实例,并设置为该控件的 QStyle,这个实例不会不会影响其他控件。
但是这些QStyleSheetStyle 实例会共用同一套缓存,就在styleRules开头出现的static QStyleSheetStyleCaches *styleSheetCaches = nullptr;静态变量,他的成员变量会缓存解析过的StyleRule,而在QStyleSheetStyle 实例获取rules时会优先从缓存中查找,其中的key就像QPushButton:hover、#myButton、.myClass 等
StyleSelector::NodePtr n;
n.ptr = const_cast<QObject *>(obj);
QVector<QCss::StyleRule> rules = styleSelector.styleRulesForNode(n);
styleSheetCaches->styleRulesCache.insert(obj, rules);
setStyleSheet 这个绘制方法,个人建议是一般不要使用,经验来看有几点不好,第一个是你用了这个,那么他会覆盖可能软件框架设计中存在的默认style,导致UI风格不统一;第二个是最讨厌的,他会导致子窗口继承,然后又因为第一个,他的优先级还挺高,导致你的子窗口如果要修改样式与父窗口不一样,就只有继续setStyleSheet,然后这玩意的入参就像str硬编码(当然可以通过arg进行通用设置),所以没什么原因,一般不用这个方法。
重写paintEvent
如果说setStyleSheet优先级很高,那么这位更是重量级,他的优先级比setStyleSheet还要高,源码中可以看到绘制的流程是各个控件从自身的paintEvent里进入,例如上文举例的btn,
void QPushButton::paintEvent(QPaintEvent *)
{
QStylePainter p(this);
QStyleOptionButton option;
initStyleOption(&option);
p.drawControl(QStyle::CE_PushButton, option);
}
在一系列的事件传递下,最后走到了这个控件自身的paintEvent中,接着就是到上面那套流程了
void QStylePainter::drawControl(QStyle::ControlElement ce, const QStyleOption &opt)
{
wstyle->drawControl(ce, &opt, this, widget);
}
所以,可以发现,paintEvent是绘制事件的开始,那么如果用户继承实现了一个customBtn,通过重写paintEvent虚函数,可以在相关实现里为所欲为,如果你不接着调用QPushButton::paintEvent方法,那么后续的setStyleSheet生效流程也不会走到,所以paintEvent重写是优先级最高的绘制方法 。
QStyle与QProxyStyle
首先我们区分 QStyle与QProxyStyle,这里QStyle是一个纯虚类(部分函数),定义了所有空间的绘制和行为方式,windows端上Qt的默认style是QWindowsStyle,他就是继承于QStyle。
而QProxyStyle则是使用了proxy设计模式,它的类中存了一个QStyle引用
QProxyStyle::QProxyStyle(QStyle *style) :
QCommonStyle(*new QProxyStylePrivate())
{
Q_D(QProxyStyle);
if (style) {
d->baseStyle = style;
style->setProxy(this);
style->setParent(this); // Take ownership
}
}
而且,当你没有显式设置style时,他会已当前application的style为base
QStyle *QProxyStyle::baseStyle() const
{
Q_D (const QProxyStyle);
d->ensureBaseStyle();
return d->baseStyle;
}
所以QProxyStyle更适合你需要小改的场景,更多的style设置继续沿用basestyle即可,这里我以drawControl举例
void QProxyStyle::drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const
{
Q_D (const QProxyStyle);
d->ensureBaseStyle();
d->baseStyle->drawControl(element, option, painter, widget);
}
而当你仅需要更改一部分时,可以自定义继承QProxyStyle,实现单独需要定制的部分
class MyStyle : public QProxyStyle {
public:
using QProxyStyle::QProxyStyle;
void drawControl(ControlElement element, const QStyleOption *opt,
QPainter *p, const QWidget *widget) const override {
if (element == CE_PushButton) {
// 自定义按钮绘制
// ...
} else {
QProxyStyle::drawControl(element, opt, p, widget);
}
}
};
///////////////////////////////////////////////////////
QApplication::setStyle(new MyStyle(QStyleFactory::create("Fusion")));
而如果你需要设计一套新的内容,在对应style上不想延续Qt的默认风格,甚至于可能你有一些新的自定义控件,就像上方pushbutton上对应CE_PushButton(qstyle中定义的枚举,代表着不同的控件类型),那么你可以继承QCommomStyle;
像平台软件中,可以自定义一些按钮,例如SplitButton,它可以上下分割成两部分,点击上方触发click信号,点击下方展开menu,也可以整个为一个整体,点击展开menu,那么你可以在你自己的CustomCommonStyle中,定义新的枚举,去区分对应的绘制逻辑,在你的drawControl中就可以case对应的枚举值,而你自定义的WholeSplitButton就可以在对应的paintEvent类似QPushbutton那样实现
p.drawControl(CustomCommonStyle::CE_WHOLESPLITBUTTON, option);
这样做好整体的控件实现,可以统一软件的UI风格,并且可以避免重复实现自定义控件,同时如果希望控件颜色或者大小,间距等方便配置,那么可以在style中定义一个config对象,这个config对象可以在开始时读取你的配置文件,以一些匹配方式(例如类名或者objectName),能够让自定义style在绘制时可以通过config读取到配置文件中记录的长度尺寸或者颜色。例如:
void CustomCommonStyle::drawControl(QStyle::ControlElement element, const QStyleOption* opt, QPainter* p, const QWidget* w) const
{
Q_D(const CustomCommonStyle);
if (!d.widgetStyleSupport(w))
{
QProxyStyle::drawControl(element, opt, p, w);
return;
}
bool draw = false;
switch (static_cast<CommonStyle::ControlElementEx>(element))
{
case CommonStyle::CE_RibbonTabShapeLabel: draw = d.drawRibbonTabShapeLabel(opt, p, w); break;
}
}
int CustomCommonStyle::pixelMetric(QStyle::PixelMetric metric, const QStyleOption* opt, const QWidget* widget) const
{
Q_D(const CustomCommonStyle);
if (!d.widgetStyleSupport(widget))
return QProxyStyle::pixelMetric(metric, opt, widget);
switch (metric)
{
case PM_SplitterWidth:
val = d.m_config.pixelMetric(widget, QString(), QStringLiteral("SplitterWidth"), defaultValue, &ok);
break;
}
}
总结
优先级 | 方式 | 生效条件 | 说明 |
---|---|---|---|
🥇 1 | 重写 paintEvent() 并不调用 style()->drawControl |
无条件 | 你完全接管绘制,其他一切无效 |
🥈 2 | setStyleSheet() |
控件支持样式表时 | Qt 自动把 style() 替换为 QStyleSheetStyle ,覆盖你的 QStyle/QProxyStyle 行为 |
🥉 3 | QStyle / QProxyStyle |
控件没有使用样式表时 | 控件默认走 style() 绘制,你设置的才有效 |
🟨 4 | 调用 style()->drawControl + QStyleOption |
控件本身使用默认绘制时 | 依赖 style() 实际类型决定表现(受上面影响) |
🟩 5 | QGraphicsItem / QML` |
与 QWidget 独立 | 单独系统,不影响上述逻辑 |