Qt UI绘制多种方式介绍

发布于:2025-07-30 ⋅ 阅读:(17) ⋅ 点赞:(0)
场景 推荐方式
简单样式更改 setStyleSheet
复杂自绘 重写 paintEvent
保留原生样式+扩展 QStyleOption + style()->drawControl
全局样式统一 自定义 QStyleQProxyStyle
高自由度图形绘制 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 独立 单独系统,不影响上述逻辑

网站公告

今日签到

点亮在社区的每一天
去签到