乐于分享
好东西不私藏

Qt Style Plugin样式插件示例:解决 polish(QPalette&) 设置按钮颜色无效问题

Qt Style Plugin样式插件示例:解决 polish(QPalette&) 设置按钮颜色无效问题

1. 背景

本文基于 Qt 5.15 官方示例文档:

https://doc.qt.io/archives/qt-5.15/qtwidgets-tools-styleplugin-example.html

示例工程路径:

C:\Qt\Examples\Qt-5.15.2\widgets\tools\styleplugin

该示例演示如何通过 Qt 插件机制扩展 Qt 本身,为应用程序提供一个新的 GUI 外观,也就是一个新的 QStyle

官方文档中提到,这个示例通过实现一个 style plugin,使 Qt 可以通过 QStyleFactory 创建名为 SimpleStyle 的样式。该样式继承自 QProxyStyle,意图是在当前平台默认样式的基础上做少量调整,例如改变按钮背景颜色。

原始示例中的核心代码大致如下:

voidSimpleStyle::polish(QPalette &palette)
{
    palette.setBrush(QPalette::Button, Qt::red);
}

理论上,这段代码把调色板中的 QPalette::Button 角色设置为红色,按钮背景应该随之变成红色。但是在实际运行 stylewindow 示例时,StyleWindow 中创建的 styledButton 背景颜色可能并不会发生变化。

本文分析该问题的原因,并说明为什么将实现从 polish(QPalette&) 改为重写 drawControl() 后可以直接生效。


2. 项目结构概览

该示例大致分为两个部分:

styleplugin/
├── plugin/
│   ├── simplestyle.h
│   ├── simplestyle.cpp
│   ├── simplestyleplugin.h
│   ├── simplestyleplugin.cpp
│   └── simplestyle.json

└── stylewindow/
    ├── main.cpp
    ├── stylewindow.h
    └── stylewindow.cpp

2.1 plugin 目录

plugin 目录负责生成样式插件动态库。

其中:

  • SimpleStyle:真正的样式类,继承自 QProxyStyle
  • SimpleStylePlugin:插件入口类,继承自 QStylePlugin
  • simplestyle.json:插件元数据文件。

2.2 stylewindow 目录

stylewindow 目录是测试程序。

它创建 QApplication,通过 QStyleFactory::create("simplestyle") 创建插件提供的样式,然后把该样式设置给整个应用程序。

核心逻辑如下:

intmain(int argv, char *args[])
{
QApplication app(argv, args);
    QApplication::setStyle(QStyleFactory::create("simplestyle"));

    StyleWindow window;
    window.resize(20050);
    window.show();

return app.exec();
}

StyleWindow 内部只创建了一个普通的 QPushButton

StyleWindow::StyleWindow()
{
    QPushButton *styledButton = new QPushButton(tr("Big Blue Button"));

    QGridLayout *layout = new QGridLayout;
    layout->addWidget(styledButton);

    QGroupBox *styleBox = new QGroupBox(tr("A simple style button"));
    styleBox->setLayout(layout);

    QGridLayout *outerLayout = new QGridLayout;
    outerLayout->addWidget(styleBox, 00);
    setLayout(outerLayout);

    setWindowTitle(tr("Style Plugin Example"));
}

因此,按钮是否变色,主要取决于当前 QStyle 如何绘制 QPushButton


3. Qt Style Plugin 的加载流程

根据官方文档,style plugin 的关键流程如下:

  1. 插件类继承 QStylePlugin
  2. 插件类通过 Q_PLUGIN_METADATA 声明插件元数据。
  3. 插件实现 keys(),告诉 Qt 它支持哪些 style 名称。
  4. 插件实现 create(),根据 style 名称创建对应的 QStyle 对象。
  5. 应用程序通过 QStyleFactory::create() 请求创建该 style。
  6. QApplication::setStyle() 将该 style 应用到整个程序。

SimpleStylePlugin::keys() 返回:

QStringList SimpleStylePlugin::keys()const
{
return {"SimpleStyle"};
}

SimpleStylePlugin::create() 根据 key 创建 SimpleStyle

QStyle *SimpleStylePlugin::create(const QString &key)
{
if (key.toLower() == "simplestyle")
returnnew SimpleStyle;
returnnullptr;
}

在 main.cpp 中:

QApplication::setStyle(QStyleFactory::create("simplestyle"));

因此,只要插件路径正确、插件成功加载,应用程序实际使用的就是 SimpleStyle


4. 原始方案:通过 polish(QPalette&) 修改调色板

原始思路是重写:

voidSimpleStyle::polish(QPalette &palette)
{
    palette.setBrush(QPalette::Button, Qt::blue);
}

这段代码修改的是应用程序或控件使用的 QPalette

QPalette::Button 是 Qt 调色板中的一个颜色角色,通常表示按钮背景色。对于某些 Qt 样式来说,绘制按钮时会读取这个角色,然后用它作为按钮背景颜色。

也就是说,polish(QPalette&) 的作用可以理解为:

我告诉 Qt:按钮背景色这个调色板角色现在是蓝色。

但是,这并不等价于:

所有样式在绘制按钮时都必须把按钮背景画成蓝色。

这是问题的关键。


5. 为什么 palette.setBrush(QPalette::Button, Qt::blue) 可能无效

5.1 QPalette 只是颜色建议,不是强制绘制命令

QPalette 是一组颜色角色,它告诉 style:不同 UI 元素理论上应该使用什么颜色。

但最终怎么画控件,是由当前 QStyle 的绘制逻辑决定的。

对于 QPushButton 来说,按钮背景不是 QPushButton 自己直接填充的,而是交给当前 style 绘制。style 可以选择读取 QPalette::Button,也可以选择不读取,或者只在部分状态下读取。

因此,设置了:

palette.setBrush(QPalette::Button, Qt::blue);

只能说明调色板中的按钮颜色变成了蓝色,并不能保证当前平台样式一定按照该颜色绘制按钮。

5.2 QProxyStyle 默认代理到底层平台样式

SimpleStyle 继承自 QProxyStyle

QProxyStyle 的作用不是从零开始实现一整套样式,而是在已有样式基础上做局部修改。未重写的绘制逻辑会继续转发给 base style。

也就是说,如果 SimpleStyle 只重写了 polish(QPalette&),那么按钮的实际绘制仍然主要由底层平台样式完成。

在 Windows 上,底层样式可能是 Windows Vista/Windows native 风格;在其他平台上,也可能是 macOS、Fusion 或其他样式。

这些平台原生样式为了保持系统一致性,可能不会简单地使用 QPalette::Button 来填充按钮背景。

5.3 官方文档本身也提示了这个限制

Qt 官方文档在该示例中有一个重要提示:

On some platforms, the native style will prevent the button from having a red background. In this case, try to run the example in another style (e.g., fusion).

意思是:

在某些平台上,原生样式会阻止按钮显示为指定的背景色。在这种情况下,可以尝试使用其他样式运行该示例,例如 fusion

这正好解释了为什么示例中通过 polish(QPalette&) 修改按钮颜色,在某些环境下看不到效果。

也就是说,这不是插件没有加载,也不一定是 polish() 没有被调用,而是当前原生样式绘制按钮时没有按你设置的 QPalette::Button 去画背景。

5.4 QPushButton 的绘制是复合过程

QPushButton 的绘制并不是简单的一次填色。

通常会涉及多个 control element,例如:

  • CE_PushButton
  • CE_PushButtonBevel
  • CE_PushButtonLabel

其中:

  • CE_PushButtonBevel 主要负责按钮边框、背景、凸起/凹陷效果。
  • CE_PushButtonLabel 主要负责按钮文本、图标等内容。

如果只是修改 palette,而实际绘制 CE_PushButtonBevel 的 style 不使用这个 palette 角色,那么按钮背景就不会变成预期颜色。


6. 修改方案:重写 drawControl() 强制绘制按钮背景

为了解决 polish(QPalette&) 不稳定的问题,可以直接重写 QStyle::drawControl()

修改后的 simplestyle.h 中声明:

classSimpleStyle :public QProxyStyle
{
    Q_OBJECT

public:
    SimpleStyle() = default;

voiddrawControl(ControlElement element,
const QStyleOption *option,
                     QPainter *painter,
const QWidget *widget = nullptr)
constoverride
;
};

修改后的 simplestyle.cpp 中实现:

#include"simplestyle.h"

#include<QPainter>

voidSimpleStyle::drawControl(ControlElement element,
const QStyleOption *option,
                              QPainter *painter,
const QWidget *widget)
const
{
if (element == CE_PushButtonBevel) {
        painter->save();
        painter->setBrush(Qt::blue);
        painter->setPen(Qt::black);
        painter->drawRect(option->rect.adjusted(00-1-1));
        painter->restore();
return;
    }
    QProxyStyle::drawControl(element, option, painter, widget);
}

这段代码的含义是:

  1. 当 Qt 要绘制按钮的 bevel 部分时,进入自定义逻辑。
  2. 使用 QPainter 直接画一个蓝色矩形。
  3. 画完后直接 return,不再交给底层样式绘制这个部分。
  4. 对于其他 control element,仍然调用 QProxyStyle::drawControl(),保持原有样式行为。

7. 为什么 drawControl() 能生效

polish(QPalette&) 修改的是“数据”:

palette.setBrush(QPalette::Button, Qt::blue);

它只是告诉 style:按钮颜色角色是蓝色。

但 drawControl() 修改的是“绘制行为”:

painter->setBrush(Qt::blue);
painter->drawRect(option->rect.adjusted(00-1-1));

它直接告诉 painter:在按钮背景区域画一个蓝色矩形。

二者区别如下:

方案
修改对象
是否强制影响绘制
是否受平台原生样式影响
polish(QPalette&)
调色板颜色角色
drawControl()
控件绘制过程
较少

因此,在平台原生样式忽略 QPalette::Button 的情况下,drawControl() 更直接、更可靠。


8. CE_PushButtonBevel 与按钮文字的关系

当前修改只处理了:

if (element == CE_PushButtonBevel) {
    ...
}

这意味着只接管按钮背景/边框部分的绘制。

按钮文字并不是在这里绘制的,通常会由 CE_PushButtonLabel 负责。

因为代码对其他 element 仍然调用:

QProxyStyle::drawControl(element, option, painter, widget);

所以按钮文字、图标等其他部分仍然由原样式绘制。

这种做法的好处是修改范围小,只改变背景,不需要完整重写按钮的所有绘制细节。


9. 当前实现的局限

虽然 drawControl() 可以让按钮背景稳定变红,但当前实现仍然比较简单:

painter->drawRect(option->rect.adjusted(00-1-1));

它只是画了一个普通蓝色矩形和黑色边框,没有考虑很多按钮状态,例如:

  • 鼠标悬停状态;
  • 鼠标按下状态;
  • disabled 状态;
  • default button 状态;
  • focus rect;
  • 高 DPI 下的边框细节;
  • 不同平台的圆角、阴影、渐变等视觉效果。

因此它适合用于学习 QStyle 绘制机制,但如果要做生产级样式,还需要进一步处理 QStyleOptionButton 中的状态信息。

例如,可以通过 option->state 判断按钮状态:

if (option->state & State_Sunken) {
// 按下状态
}

if (option->state & State_MouseOver) {
// 鼠标悬停状态
}

if (!(option->state & State_Enabled)) {
// 禁用状态
}

如果需要访问按钮特有信息,也可以将 QStyleOption 转换为 QStyleOptionButton

const QStyleOptionButton *buttonOption = qstyleoption_cast<const QStyleOptionButton *>(option);

10. 总结

本示例的问题可以总结为:

palette.setBrush(QPalette::Button, Qt::blue) 修改的是调色板角色,但按钮最终如何绘制由当前 QStyle 决定。某些平台原生样式不会按照 QPalette::Button 来绘制按钮背景,因此看不到蓝色按钮。

官方文档已经明确提示:

某些平台上的原生样式会阻止按钮显示为指定背景色,可以尝试使用 fusion 等其他样式。

因此,polish(QPalette&) 无效并不代表插件机制失败,也不代表 QPalette 设置错误,而是 style 绘制策略导致的结果。

改用 drawControl() 后,代码直接拦截 CE_PushButtonBevel 的绘制流程,用 QPainter 画出蓝色背景,因此可以绕过平台原生样式对 palette 的忽略,从而稳定看到按钮背景变化。

在学习 Qt 样式系统时,可以这样理解:

  • polish(QPalette&):适合做全局颜色角色调整,但效果依赖具体 style 是否使用这些颜色。
  • drawControl():适合精确接管某类控件或控件局部元素的绘制,效果更直接,但需要自行维护绘制细节。

对于这个示例来说,如果目标只是验证 style plugin 能够改变按钮背景,重写 drawControl(CE_PushButtonBevel, ...) 是更直观、更可靠的方式。