14_Qt 样式表(QSS)入门(语法、选择器、美化实战)

Qt 样式表(QSS)入门:语法、选择器、美化实战

引言

前十三篇解决的是"界面如何工作":窗口、布局、输入控件和条目控件各自保存什么状态、发出什么信号。本章解决另一个立刻可见的问题:界面如何形成一致的视觉语言

Qt Widgets 默认外观来自当前平台的原生风格。它保证了可用性,却不保证产品的品牌色、间距、圆角和不同页面之间的一致性。最直接的改法是在代码里调用 setStyleSheet(),但一旦每个窗口都拼接一大段字符串,主题切换、复用和排错都会迅速失控。

Qt Style Sheets,通常简称 QSS,是 Qt Widgets 的样式表语言。它借鉴 CSS 的"选择器 + 属性声明"形式,但不是浏览器 CSS 的完整实现。本文面向 Qt 6 Widgets,仍按"先会用 -> 再理解选择器 -> 最后追踪源码"的顺序展开。读完后应能:

  • 把 QSS 放进工程资源并在启动时加载;
  • 为同一程序维护浅色、深色两套主题,并在运行时切换;
  • 用类型、对象名、属性、状态和子控件选择器准确命中控件;
  • 理解 QSS 的覆盖关系,避免"明明写了却不生效";
  • setStyleSheet() 追到 QStyleSheetStyle、样式解析和重绘请求;
  • 知道 QSS 适合解决什么,以及何时应改用调色板、自定义绘制或 QML。

图 1:应用不直接依赖磁盘上的主题路径,而是通过资源路径读取 QSS。主题管理器选择一份完整主题,再一次性设置为应用级样式表。

一、先会用:给一个控件加样式

最小 QSS 只有"选择器、花括号、属性和值"三部分:

cpp 复制代码
auto *button = new QPushButton(tr("保存"), this);
button->setStyleSheet(R"(
    QPushButton {
        background-color: #1677ff;
        color: white;
        border: none;
        border-radius: 4px;
        padding: 8px 16px;
    }

    QPushButton:hover {
        background-color: #4096ff;
    }

    QPushButton:pressed {
        background-color: #0958d9;
    }
)");

运行后,按钮正常处理点击和键盘操作;QSS 只改变它的视觉呈现。这里的 QPushButton 是选择器,第一段规则定义普通状态,:hover:pressed 定义鼠标悬停与按下状态。

这段代码适合验证一个属性或临时调试,但不适合作为工程主题方案。样式字符串与 C++ 代码混在一起,编辑器没有独立的语法高亮,重复代码难以发现,也无法由一处切换整套颜色。

1.1 控件级、窗口级和应用级:先选作用范围

setStyleSheet() 同名 API 可以出现在两个层级:

cpp 复制代码
// 只影响 saveButton 自己。
saveButton->setStyleSheet("QPushButton { color: #1677ff; }");

// 影响 settingsPanel 以及它的子控件。
settingsPanel->setStyleSheet("QLabel { color: #334155; }");

// 影响当前 QApplication 管理的所有 Widgets。
qApp->setStyleSheet("QPushButton { border-radius: 4px; }");

应用级样式是产品主题的默认位置;窗口级样式适合一个独立、局部的视觉区域;单控件样式只适合真正独特的控件。后两者不是"优先级更高的主题文件",而是更近的作用域。若同一属性发生冲突,控件自身或更近祖先设置的样式表会遮住应用级规则,即使应用级选择器写得更具体。

因此有一条足够实用的约定:产品的通用外观只放应用级主题;局部差异通过对象名、动态属性或自定义控件表达;不要为了改一个颜色给普通控件再调用一次 setStyleSheet()

1.2 QSS 与调色板、原生 Style 的边界

三者都能影响 Widgets 外观,但职责不同:

工具 更适合做什么 不适合做什么
QPalette 基于角色的前景、背景、禁用色;尊重系统主题 精确控制圆角、间距、子控件位置
QSS 控件外观、状态、间距、边框、图标和局部品牌风格 浏览器 CSS 的全部布局与动画能力
QStyle / 自定义绘制 新控件、平台级一致性、复杂绘制与度量 用少量颜色修改解决的问题

QSS 不是 QML 的 Rectangle、锚点和状态机,也不会替代 QLayout。控件放在哪里、能有多大,仍然由第九章的布局系统决定;QQSS 中的 paddingmarginmin-width 等属性会参与 Qt Style 对控件内容区域、尺寸提示等方面的计算,但它们不是 CSS 那种完整的页面布局系统 ,控件之间的整体布局仍由 QLayout、布局管理器和控件几何属性负责。

二、工程中组织 QSS:从文件加载到可切换主题

一个可维护的入门项目不需要复杂框架,但应把"主题数据"和"加载动作"分开。下面的结构足够覆盖多数 Widgets 项目:

text 复制代码
qss_theme_demo/
├── CMakeLists.txt
├── main.cpp
├── resources.qrc
└── themes/
    ├── light.qss
    └── dark.qss

themes 中的文件只描述外观,不写业务判断;main.cppThemeController 只负责读取资源、应用完整样式表和通知界面更新。主题文件放进 .qrc 后会被编译进可执行文件,部署时不会出现"工作目录不对,找不到 qss"问题。

2.1 CMake 与资源文件

CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.21)
project(qss_theme_demo LANGUAGES CXX)

find_package(Qt6 REQUIRED COMPONENTS Widgets)

qt_standard_project_setup()

qt_add_executable(qss_theme_demo
    main.cpp
    resources.qrc
)

target_link_libraries(qss_theme_demo PRIVATE Qt6::Widgets)

set_target_properties(qss_theme_demo PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
)

resources.qrc

xml 复制代码
<!DOCTYPE RCC>
<RCC version="1.0">
    <qresource prefix="/themes">
        <file>themes/light.qss</file>
        <file>themes/dark.qss</file>
    </qresource>
</RCC>

编译后,上面两个文件的路径分别是 :/themes/themes/light.qss:/themes/themes/dark.qss。资源前缀 /themes 和实际文件夹 themes/ 都会出现在资源路径中;这正是初学者最容易漏掉的一层。若希望路径为 :/themes/light.qss,可给 <file> 增加别名:<file alias="light.qss">themes/light.qss</file>

下面示例采用别名,最终 resources.qrc 写成:

xml 复制代码
<!DOCTYPE RCC>
<RCC version="1.0">
    <qresource prefix="/themes">
        <file alias="light.qss">themes/light.qss</file>
        <file alias="dark.qss">themes/dark.qss</file>
    </qresource>
</RCC>

2.2 先写一份完整浅色主题

themes/light.qss

css 复制代码
QWidget {
    color: #1f2937;
    font-size: 14px;
}

/* 仅在确定目标环境存在该字体时指定 */
QWidget {
    font-family: "Microsoft YaHei";
}

QMainWindow, QWidget#centralPanel {
    background-color: #f5f7fa;
}

QFrame#card {
    background-color: white;
    border: 1px solid #e2e8f0;
    border-radius: 6px;
}

QLabel#pageTitle {
    color: #172033;
    font-size: 22px;
    font-weight: 600;
}

QLabel#hintLabel {
    color: #64748b;
}

QLineEdit, QComboBox {
    min-height: 34px;
    padding: 0 10px;
    background-color: white;
    border: 1px solid #cbd5e1;
    border-radius: 4px;
}

QLineEdit:focus, QComboBox:focus {
    border: 2px solid #1677ff;
}

QPushButton {
    min-height: 34px;
    padding: 0 14px;
    color: #334155;
    background-color: #ffffff;
    border: 1px solid #cbd5e1;
    border-radius: 4px;
}

QPushButton:hover {
    background-color: #f1f5f9;
}

QPushButton:pressed {
    background-color: #e2e8f0;
}

QPushButton[role="primary"] {
    color: white;
    background-color: #1677ff;
    border-color: #1677ff;
}

QPushButton[role="primary"]:hover {
    background-color: #4096ff;
    border-color: #4096ff;
}

QPushButton:disabled {
    color: #94a3b8;
    background-color: #f1f5f9;
    border-color: #e2e8f0;
}

先观察这份文件的策略:QWidget 只放整个应用都能继承的文字基线;输入框、下拉框和按钮使用分组选择器共享基础规则;真正的主要按钮不靠 #saveButton 这类业务对象名,而是以 role="primary" 这类视觉角色区分。这样新增"提交""确认""下一步"按钮时不需要再复制一组样式。

font-family 要填目标平台确实安装或打包的字体。跨平台产品通常不强制指定单一中文字体,优先让系统选择本地默认字体;这里仅为了让 Windows 截图更稳定。

2.3 再写深色主题:同一结构,不同令牌

themes/dark.qss 可以保持相同选择器,只替换颜色和边框对比度:

css 复制代码
QWidget {
    color: #e5e7eb;
    font-size: 14px;
}

QMainWindow, QWidget#centralPanel {
    background-color: #171b24;
}

QFrame#card {
    background-color: #222936;
    border: 1px solid #3a4556;
    border-radius: 6px;
}

QLabel#pageTitle {
    color: #f8fafc;
    font-size: 22px;
    font-weight: 600;
}

QLabel#hintLabel {
    color: #a8b3c5;
}

QLineEdit, QComboBox {
    min-height: 34px;
    padding: 0 10px;
    color: #f1f5f9;
    background-color: #171b24;
    border: 1px solid #526174;
    border-radius: 4px;
}

QLineEdit:focus, QComboBox:focus {
    border: 2px solid #55a8ff;
}

QPushButton {
    min-height: 34px;
    padding: 0 14px;
    color: #e5e7eb;
    background-color: #2c3646;
    border: 1px solid #526174;
    border-radius: 4px;
}

QPushButton:hover {
    background-color: #3a4556;
}

QPushButton:pressed {
    background-color: #4b5a70;
}

QPushButton[role="primary"] {
    color: white;
    background-color: #1677ff;
    border-color: #1677ff;
}

QPushButton[role="primary"]:hover {
    background-color: #4096ff;
    border-color: #4096ff;
}

QPushButton:disabled {
    color: #8491a3;
    background-color: #2a3342;
    border-color: #3a4556;
}

入门阶段不必急于实现变量预处理器。QSS 本身没有浏览器 CSS 的自定义属性机制;两份主题的选择器应保持同构,颜色差异集中在对应位置。主题数量增长到三套以上时,再用 CMake、脚本或自己的"设计令牌"源文件生成 QSS,避免人工维护多份近似选择器。

2.4 完整可运行示例:读取与切换主题

把下面代码保存为 main.cpp,再创建上面的资源文件和两份 QSS,即可运行。示例的关键不在按钮,而在 ThemeController::apply():它读取资源,确认文件可打开后一次性调用 QApplication::setStyleSheet()

cpp 复制代码
#include <QApplication>
#include <QComboBox>
#include <QFile>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>

class ThemeController
{
public:
    static bool apply(const QString &resourcePath)
    {
        QFile file(resourcePath);
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            qWarning() << "Cannot open theme:" << resourcePath
                       << file.errorString();
            return false;
        }

        const QString styleSheet = QString::fromUtf8(file.readAll());
        qApp->setStyleSheet(styleSheet);
        return true;
    }
};

class SettingsWindow : public QWidget
{
public:
    SettingsWindow()
    {
        setWindowTitle(tr("QSS 主题切换"));
        resize(520, 330);
        setObjectName("centralPanel");

        auto *title = new QLabel(tr("账户设置"), this);
        title->setObjectName("pageTitle");

        auto *hint = new QLabel(
            tr("切换主题时,窗口和已创建的子控件会重新匹配样式规则。"), this);
        hint->setObjectName("hintLabel");
        hint->setWordWrap(true);

        auto *nameEdit = new QLineEdit(this);
        nameEdit->setPlaceholderText(tr("请输入显示名称"));

        auto *themeBox = new QComboBox(this);
        themeBox->addItem(tr("浅色主题"), QStringLiteral(":/themes/light.qss"));
        themeBox->addItem(tr("深色主题"), QStringLiteral(":/themes/dark.qss"));

        auto *saveButton = new QPushButton(tr("保存设置"), this);
        saveButton->setProperty("role", "primary");

        auto *cancelButton = new QPushButton(tr("取消"), this);

        auto *buttonLayout = new QHBoxLayout;
        buttonLayout->addStretch();
        buttonLayout->addWidget(cancelButton);
        buttonLayout->addWidget(saveButton);

        auto *card = new QFrame(this);
        card->setObjectName("card");
        auto *cardLayout = new QVBoxLayout(card);
        cardLayout->setContentsMargins(24, 24, 24, 24);
        cardLayout->setSpacing(12);
        cardLayout->addWidget(title);
        cardLayout->addWidget(hint);
        cardLayout->addWidget(new QLabel(tr("显示名称"), card));
        cardLayout->addWidget(nameEdit);
        cardLayout->addWidget(new QLabel(tr("界面主题"), card));
        cardLayout->addWidget(themeBox);
        cardLayout->addSpacing(8);
        cardLayout->addLayout(buttonLayout);

        auto *rootLayout = new QVBoxLayout(this);
        rootLayout->setContentsMargins(28, 28, 28, 28);
        rootLayout->addWidget(card);

        connect(themeBox, &QComboBox::currentIndexChanged, this,
                [themeBox](int) {
                    ThemeController::apply(themeBox->currentData().toString());
                });
    }
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    ThemeController::apply(QStringLiteral(":/themes/light.qss"));

    SettingsWindow window;
    window.show();
    return app.exec();
}

这里有三个常见细节:

  1. 在创建窗口前先应用默认主题,可避免窗口第一次出现时先闪过未设置的原生样式。
  2. QFile 打不开时不要继续把空字符串传给 setStyleSheet(),否则会清除当前主题;先返回 false,保留原有视觉状态。
  3. setProperty("role", "primary") 设置的是动态属性,它不需要在 C++ 类中声明 Q_PROPERTY,但它是数据,选择器才能据此把按钮识别为主要操作。

如果主题来源是用户可编辑的配置目录,不要把资源路径写死。可用 QStandardPaths::writableLocation() 找到应用数据目录,再使用 QFile 读取。无论来源是资源还是磁盘,读取、验证、应用这一层 API 都相同;差别只在部署和更新策略。

三、QSS 语法:先读懂一条规则

一条规则的通用形状如下:

css 复制代码
选择器 {
    属性名: 属性值;
    属性名: 属性值;
}

例如:

css 复制代码
QLineEdit#searchEdit:focus {
    color: #0f172a;
    background-color: #ffffff;
    border: 2px solid #1677ff;
    padding: 0 10px;
}

它的含义不是"所有输入框聚焦时都变蓝",而是"类名为 QLineEdit、对象名恰好为 searchEdit、当前又处于焦点状态的那个输入框,使用这些声明"。选择器过滤目标,声明块描述命中目标的样式。

3.1 属性:优先掌握 Widgets 常用子集

QSS 接受一部分 CSS 风格属性,也提供 Qt 专有属性。下表覆盖入门项目的高频项:

类别 常用属性 说明
颜色与文字 colorbackgroundbackground-colorfont-sizefont-weight 文字、背景与字体
边框与圆角 borderborder-colorborder-widthborder-radius 一旦自定义按钮边框,通常要同时补全状态样式
间距与尺寸 marginpaddingmin-widthmin-height 不替代布局,但能改变控件内容区与最小尺寸
图片 imagebackground-imageborder-image 可引用 :/ 资源路径
Qt 专有 qproperty-iconSizesubcontrol-positionsubcontrol-origin 控制 Qt 属性或复合控件子区域

颜色可使用 #RRGGBB#AARRGGBB 或命名色。对可复用主题,优先使用六位或八位十六进制颜色,审阅时更容易比较。padding: 8px 12px 的顺序和 CSS 一样,是上下、左右;四个值依次是上、右、下、左。

qproperty-属性名 会尝试给 Qt 属性赋值,例如:

css 复制代码
QToolButton#zoomInButton {
    qproperty-iconSize: 20px 20px;
}

它适用于一个已经存在且可从 QSS 设置的属性,不是随意调用成员函数的通道。动态状态变化不应依赖 qproperty- 反复"重设属性";这种需求通常应在 C++ 中调用公开 API,或交给状态选择器。

3.2 注释、分组与图片资源

QSS 支持 C 风格注释和逗号分组:

css 复制代码
/* 输入类控件共享一套基础边框。 */
QLineEdit, QSpinBox, QComboBox {
    border: 1px solid #cbd5e1;
    border-radius: 4px;
}

QPushButton#deleteButton {
    image: url(:/icons/delete.svg);
}

不要用绝对磁盘路径引用图片。url(:/icons/delete.svg) 和 QSS 文件一起使用资源系统,构建、打包与切换工作目录后仍然有效。图标若只需要出现在按钮上,button->setIcon(QIcon(":/icons/delete.svg")) 通常语义更清楚;image 更适合 QSS 正在控制的子控件或装饰图。

四、选择器:从"控件类型"到"准确命中"

选择器是 QSS 最值得花时间的部分。初学时最常见的失败不是属性拼错,而是规则命中了比预期更多或更少的控件。

图 2:选择器越具体,匹配范围越小。在同一个有效样式表内,满足条件更多的规则通常会胜出;但更近作用域的样式表先决定可用规则集合。

4.1 类型选择器:同类控件共享规则

css 复制代码
QPushButton {
    min-height: 34px;
    border-radius: 4px;
}

它会命中 QPushButton 及其子类。类型选择器适合建立全局基线,例如所有按钮的高度、所有输入框的基础边框。不要在其中写"删除按钮为红色"之类的业务差异,否则每个普通按钮都被误伤。

4.2 类选择器:识别 C++ 继承体系

Qt 的类选择器写法是 .类名

css 复制代码
.QPushButton {
    font-weight: 600;
}

它与 QPushButton 的差别在于:类型选择器会匹配该类型及其子类;.QPushButton 以 Qt 自动提供的类属性匹配精确类名,通常不匹配派生类。日常 Widgets 主题中,直接写 QPushButton 更直观;只有需要刻意排除派生控件时,才使用类选择器。不要把 CSS 网页里的 class="primary" 思维直接搬过来,Qt 控件没有 HTML class 属性。

4.3 ID 选择器:用 objectName 定位控件

cpp 复制代码
searchEdit->setObjectName("searchEdit");
css 复制代码
QLineEdit#searchEdit {
    padding-left: 32px;
}

#searchEdit 匹配的是 QObject::objectName()。Qt Designer 的"objectName"字段就是这个值。它适合页面标题、一个特殊容器、一个明确唯一的搜索框。不要把所有按钮都取业务对象名再逐个写 #xxx 规则,那会让主题退化成另一种散落的硬编码。

4.4 属性选择器:给视觉角色,不给业务身份

cpp 复制代码
saveButton->setProperty("role", "primary");
deleteButton->setProperty("role", "danger");
css 复制代码
QPushButton[role="primary"] {
    color: white;
    background-color: #1677ff;
}

QPushButton[role="danger"] {
    color: white;
    background-color: #dc2626;
}

属性选择器匹配 Qt 属性,包含动态属性。它非常适合"主操作、危险操作、静默操作"这类会在多个页面反复出现的视觉角色。动态属性改变后,已经显示的控件不一定立即重新计算样式;下一节会给出可靠的刷新方法。

4.5 后代与直接子控件:限定一个区域

css 复制代码
QFrame#settingsCard QLabel {
    color: #475569;
}

QFrame#settingsCard > QLabel#pageTitle {
    color: #172033;
}

空格表示后代选择器,QFrame#settingsCard QLabel 会匹配卡片下任意层级的标签;> 表示直接子控件,只匹配父对象正好是该卡片的标签。两者适合在一个局部容器里改变文字或按钮外观。范围一旦变大,优先使用对象名和动态属性,不要写多层 QWidget QWidget QWidget 来碰运气。

4.6 伪状态:让控件在交互中有反馈

css 复制代码
QPushButton:hover { background-color: #f1f5f9; }
QPushButton:pressed { background-color: #e2e8f0; }
QPushButton:disabled { color: #94a3b8; }
QCheckBox:checked { color: #1677ff; }
QLineEdit:focus { border-color: #1677ff; }
QPushButton:!enabled { background-color: #f1f5f9; }

常用状态包括 :hover:pressed:focus:disabled:checked:unchecked:selected:open。否定形式用 !,如 :!checked:!enabled。状态规则不替代 C++ 逻辑:按钮能否点击仍由 setEnabled() 决定,QSS 只是根据控件当前状态选择外观。

一个容易忽略的细节是:一旦给某个状态单独写了背景、边框等属性,就应检查普通、悬停、按下、禁用和焦点状态是否完整。否则某些平台风格会在未覆盖的状态中回退,视觉看起来像"跳了一下"。

4.7 子控件:美化复合 Widgets 的组成部分

QComboBoxQSpinBoxQScrollBar 并非一个矩形。它们内部有下拉按钮、箭头、加减按钮、滑块等可被 QSS 命名的子控件:

css 复制代码
QComboBox {
    padding-right: 28px;
}

QComboBox::drop-down {
    width: 26px;
    border: none;
    border-left: 1px solid #cbd5e1;
}

QComboBox::down-arrow {
    image: url(:/icons/chevron-down.svg);
    width: 12px;
    height: 12px;
}

QScrollBar:vertical {
    width: 10px;
    background: transparent;
}

QScrollBar::handle:vertical {
    min-height: 28px;
    background: #94a3b8;
    border-radius: 5px;
}

双冒号 :: 表示子控件,不是伪状态。子控件的位置与尺寸常由 subcontrol-originsubcontrol-position 协调。第一次定制复合控件时,建议先只改颜色和宽高,在目标平台实际观察;过早重写所有子控件,往往会丢掉原生 Style 的一部分可用细节。

五、覆盖规则:为什么"写了 QSS 还是没变化"

QSS 的结果不是简单的"最后一行覆盖前一行"。需要按以下顺序判断:

  1. 规则来自哪个作用域:应用、父控件还是目标控件自身?更近的控件样式表优先。
  2. 在同一有效样式表中,哪条选择器更具体?ID 和属性条件通常比单独类型更具体。
  3. 若具体程度相同,后出现的规则覆盖先出现的同名属性。
  4. 目标属性是否被该控件和当前平台 Style 支持?某些控件一旦深度定制,还需要同时覆盖其子控件。

例如下面三条规则处于同一份应用主题时:

css 复制代码
QPushButton { color: #334155; }
QPushButton[role="primary"] { color: white; }
QPushButton#saveButton { color: #fef3c7; }

objectNamesaveButtonroleprimary 的按钮最终是淡黄色,因为 ID 选择器更具体。若随后对这个按钮调用:

cpp 复制代码
saveButton->setStyleSheet("QPushButton { color: #16a34a; }");

它会使用自身样式表中的绿色。此时继续在应用主题里调整 #saveButton 不会看到效果,根因不是选择器不够具体,而是规则已经来自更近的作用域。

5.1 动态属性改了,为什么不立即变色?

设置动态属性本身只改变对象数据:

cpp 复制代码
saveButton->setProperty("role", "danger");

若该控件已经显示,可在最小作用域内重新 polish:

cpp 复制代码
#include <QStyle>

void refreshStyle(QWidget *widget)
{
    widget->style()->unpolish(widget);
    widget->style()->polish(widget);
    widget->update();
}

saveButton->setProperty("role", "danger");
refreshStyle(saveButton);

unpolish()polish() 让当前 QStyle 重新应用样式相关初始化,update() 请求下一次绘制。不要为了刷新一个按钮而重新设置整份应用主题,更不要在每次鼠标移动时调用这段代码;动态角色适合低频状态,例如校验失败、危险模式或页面模式切换。

5.2 样式表会继承,fontcolor 仍要克制

应用或父控件的样式表会向子控件提供可匹配规则。fontcolor 等视觉基线也常会随控件树延续。这样很方便,但一条宽泛的 QWidget { color: ... } 也会影响表格、菜单、提示框和未来新增的自定义控件。

建议先在应用主题中设置尽量少的全局基线,然后按控件类别补充规则;对特殊区域先给容器对象名,再在该容器内选择后代。主题写完后至少检查:禁用按钮、输入焦点、选中行、弹出菜单和对话框,而不是只看首页。

六、美化实战:把"任务面板"做成可维护界面

第十二章的任务控制台包含标题、数值输入、滑块、复选框、进度条和按钮。它非常适合用来验证 QSS:控件状态丰富,却不需要新的业务逻辑。

先在 C++ 中只提供结构与语义,不把颜色写进去:

cpp 复制代码
auto *panel = new QFrame(this);
panel->setObjectName("taskPanel");

auto *title = new QLabel(tr("批量任务设置"), panel);
title->setObjectName("panelTitle");

auto *runButton = new QPushButton(tr("开始任务"), panel);
runButton->setProperty("role", "primary");

auto *progressBar = new QProgressBar(panel);
progressBar->setObjectName("taskProgress");
progressBar->setRange(0, 100);
progressBar->setValue(40);

然后在主题中表达视觉层次:

css 复制代码
QFrame#taskPanel {
    background: #ffffff;
    border: 1px solid #e2e8f0;
    border-radius: 6px;
}

QLabel#panelTitle {
    color: #172033;
    font-size: 20px;
    font-weight: 600;
}

QProgressBar#taskProgress {
    min-height: 10px;
    color: transparent;
    background: #e2e8f0;
    border: none;
    border-radius: 5px;
    text-align: center;
}

QProgressBar#taskProgress::chunk {
    background: #1677ff;
    border-radius: 5px;
}

QProgressBar::chunk 是进度已完成的那一段。设置圆角时,轨道和 chunk 都要设置,才能避免进度较小时出现方角。color: transparent 用来隐藏本例进度条内置文字;若用户必须知道精确百分比,应保留 setFormat() 文本或在旁边放清晰的 QLabel,不要为了"干净"而丢掉任务反馈。

这个分工很重要:C++ 中的 objectNamerole稳定语义锚点 ,QSS 中的颜色、边框和间距是可替换表现。后续改成深色主题、提高高对比度,或把主按钮换成品牌色,都不需要触碰任务控制逻辑。

七、源码视角:调用 setStyleSheet() 后发生了什么

到这里已经能写出可维护主题,但还需要校正一个直觉:setStyleSheet() 不是"把颜色立即画到每个像素上"。它保存一段文本、建立或更新 QSS 代理 Style、让控件失效并在后续绘制时按规则计算结果。

图 3:样式设置与真正绘制是两件事。设置阶段解析并更新规则,随后由 StyleChange、布局/重绘请求和事件循环驱动控件在需要时重新绘制。私有类名称用于理解源码,不是业务代码应直接依赖的 API。

以下路径以 Qt 6 的 Widgets 源码为参照。私有函数名和局部细节可能随小版本变化,但公开 API 到样式代理再到绘制这一层次保持稳定。

本节涉及的 QStyleSheetStyleQCss::Parserqcssparser_p.h 等名称主要用于阅读 Qt 源码。它们属于 Qt 内部实现或 private API,不应该作为业务代码的依赖。实际开发只需要使用 setStyleSheet()QStyleQPalette 等公开 API。

7.1 第一步:应用级与控件级入口不同

当调用:

cpp 复制代码
qApp->setStyleSheet(styleSheetText);

入口在 src/widgets/kernel/qapplication.cppQApplication::setStyleSheet()。它会确保应用使用一个 QStyleSheetStyle 代理 Style,并将新文本设置为全局样式表。QStyleSheetStyle 包装原有的基础 QStyle;没有被 QSS 接管的绘制仍可委托给基础 Style。

当调用:

cpp 复制代码
widget->setStyleSheet(styleSheetText);

入口在 src/widgets/kernel/qwidget.cppQWidget::setStyleSheet()。Qt 为该控件记录局部样式表,必要时同样建立样式表代理,让这份局部规则作用于该控件及其子树。

这也解释了前面"更近作用域优先"的现象:应用级规则不是简单复制到每个控件字符串里,Qt 在解析目标控件时会收集它可见的样式表来源,并处理局部覆盖。

7.2 第二步:解析文本并建立规则缓存

样式表文本不是每个 paintEvent() 都从头按字符扫描。Qt 的 QSS 解析器位于 src/gui/text/qcssparser.cpp,相关解析数据结构在 qcssparser_p.h;Widgets 侧的样式表实现主要在 src/widgets/styles/qstylesheetstyle.cpp

简化后,设置阶段会经历:

text 复制代码
QString QSS 文本
    -> Qt 内部 QCss 解析器:识别选择器、声明、URL 等语法结构
    -> QStyleSheetStyle 保存应用/对象规则
    -> 目标控件发生 StyleChange 等失效通知
    -> 需要时重新计算规则并更新尺寸/绘制请求

注意:QCss 并不是 Qt 6 对外提供的公共模块或 API。它是 Qt 源码内部实现 QSS 的解析相关代码,相关头文件通常属于 Qt 的 private API,应用程序不应该直接依赖。

解析失败时,Qt 通常通过日志报告类似 Unknown propertyCould not parse stylesheet 的信息。它不会因为一条样式错了就让程序崩溃,但"没崩溃"不代表规则正确。因此开发时要保留应用输出窗口,先修正第一个 QSS 警告,再判断是否存在优先级问题。

7.3 第三步:控件重绘时,代理 Style 回答"怎么画"

Widgets 的绘制通常不是业务代码直接画按钮边框,而是控件构造 QStyleOption,调用当前 QStyledrawPrimitive()drawControl()drawComplexControl()。例如按钮、复选框、滑块分别把不同的状态与几何信息交给 Style。

当 QSS 生效时,当前 Style 是 QStyleSheetStyle。它根据目标控件、对象名、属性、伪状态和子控件名称取得匹配的 render rule:

  • QSS 对该部分有声明时,代理 Style 按规则绘制背景、边框、文字或子控件;
  • QSS 未覆盖的部分,代理 Style 可交回底层平台 Style 绘制;
  • 规则涉及尺寸、边距或子控件位置时,pixelMetric()sizeFromContents()subControlRect() 等查询结果也会受到影响。

所以"给 QComboBox 改了 ::drop-down,箭头位置也变了"并不奇怪:QSS 不只参与最后一笔绘制,也会影响 Style 提供的度量与子区域。

7.4 第四步:为什么调用后不一定立刻看到像素变化

Qt GUI 是事件驱动的。样式改变会使相关控件需要重新 polish、重新计算或重绘,但屏幕更新通常由事件循环合并安排。调用:

cpp 复制代码
qApp->setStyleSheet(newTheme);

之后不必对每个顶层窗口手动 repaint()。Qt 会让受影响 Widgets 接收样式变化并在下一轮可绘制时更新。强行频繁 repaint() 会同步绘制,可能造成卡顿;只有诊断非常特殊的绘制问题时才应讨论它。

这条链路也说明主题切换为什么不能放在高频槽函数中。每切换一次完整应用 QSS,都可能影响大量控件的规则匹配、度量与重绘。它适合启动、用户显式选择、系统外观变化等低频事件,不适合每次滑块 valueChanged()

八、排错与性能:按顺序定位,不靠反复试颜色

8.1 QSS 完全没有生效

先检查资源是否打开成功:

cpp 复制代码
QFile file(":/themes/light.qss");
qDebug() << file.exists();

若是 false,核对 .qrc 中的 prefix、alias 和文件路径;若为 true,继续查看应用输出中的 QSS 解析警告。资源修改后必须重新构建,因为 .qrc 是编译输入,不是运行时自动监控的普通文本文件。

8.2 规则只对一部分控件有效

先打印目标对象的类名与对象名:

cpp 复制代码
qDebug() << widget->metaObject()->className()
         << widget->objectName();

再检查选择器是否假定了错误的控件类型或父子层级。对 QComboBoxQSpinBoxQScrollBar 这类复合控件,还要确认自己改的是主体还是 ::drop-down::handle 等子控件。

8.3 调整应用主题却没有变化

搜索代码中的 setStyleSheet(。最常见的遮挡来源是某个 Designer 生成的 setupUi() 或某个局部控件调用了 setStyleSheet()。先删除或迁移局部硬编码,再判断应用级选择器优先级;不要在全局 QSS 中无限增加 #id 试图压过局部样式。

8.4 图标不显示或切换主题后仍是旧图标

检查 url(:/...) 是否与资源路径一致。深浅主题若使用不同颜色的图标,可让两份主题分别引用各自资源;若图标来自 QIcon,则在主题切换逻辑中显式替换图标。QSS 不会把深色 PNG 自动变成浅色图标。

8.5 大型程序切换主题卡顿

先确认是否每个控件都有独立长样式表,或是否在高频信号中反复调用应用级 setStyleSheet()。将规则收敛为少量应用级文件,避免深层、宽泛、重复的选择器,并只在真正需要时切换主题。若某个复杂视图本身就需要逐项自定义外观,评估委托绘制或自定义 QStyle,不要让 QSS 承担成千上万个条目的动态业务状态。

QSS 性能问题通常不是"使用 QSS 就慢",而是规则复杂度、控件数量、样式频繁重新设置以及复杂复合控件共同造成的。

特别要避免频繁调用的情况:

cpp 复制代码
for (auto *widget : widgets) {
    widget->setStyleSheet(...);
}

九、入门阶段的主题约定

在项目早期统一下面几条规则,后续页面会明显更容易维护:

约定 原因
默认主题只通过 qApp->setStyleSheet() 设置一次 避免窗口各自维护颜色与边距
资源使用 :/themes/:/icons/ 等稳定前缀 与工作目录和安装位置解耦
对象名用于唯一结构节点 避免把每个业务按钮都写成 ID 选择器
roleseverity 等动态属性用于视觉角色 可在多页面复用主操作、危险操作、错误状态
浅色与深色主题的选择器保持同构 切换主题不会遗漏某一类控件
业务逻辑不读取控件的颜色和 QSS 文本 UI 表现不应成为业务规则来源

最后补充一个边界:可访问性和系统一致性优先于"所有像素都自定义"。文本与背景要有足够对比度,焦点状态不能只靠很细的颜色变化,禁用状态也不能让文字完全不可读。产品若要求严格跟随系统高对比度或辅助功能设置,应先评估系统 Palette 和原生 Style 的行为,再决定 QSS 覆盖到什么程度。

总结

QSS 的上手入口很小:一条 QPushButton { ... } 规则就能改变控件外观;真正可维护的入口则是"资源文件 + 应用级主题 + 稳定语义锚点"。先让 QSS 文件描述所有通用视觉,再用 objectName 描述唯一结构、用动态属性描述可复用角色、用伪状态描述交互反馈。

从源码看,setStyleSheet() 保存的不是一张立即画出的位图。Qt 将 QSS 解析为规则,通过 QStyleSheetStyle 包装基础 Style,并在控件查询度量、处理样式变化和后续绘制时应用这些规则。理解"设置、匹配、重绘"三步,就能解释绝大多数样式不生效、切换不刷新和复合控件错位的问题。

下一章将进入国际化与本地化:文字不再直接写死在界面上,而会通过 tr().ts.qm 文件随语言环境切换。QSS 的主题系统与国际化共同构成 Widgets 界面层的两项基础配置能力。


下一篇预告:《15_国际化和本地化(tr()、ts 文件、QM 文件、多语言切换)》

相关推荐
Quz9 小时前
QML 拖拽篇:接收拖拽与数据交换
qt
欧特克_Glodon11 小时前
OpenCV计算机视觉开发入门与实践<二十七>:图像分割概述
c++·人工智能·opencv·计算机视觉
蒸蒸yyyyzwd11 小时前
cpp 选手秋招学习笔记 day21
c++·面试·八股
「QT(C++)开发工程师」1 天前
C++ auto 用法详解
开发语言·c++
OPEN-F1 天前
C++STL教程:容器适配器与实用工具
开发语言·c++
OPEN-F1 天前
C++模板教程:变参模板、折叠表达式与SFINAE
java·开发语言·c++
有点。1 天前
C++二叉搜索树进阶
开发语言·c++
HugoStudio_SWAN1 天前
【擦除重绘】C++ 控制台动画:弹跳 Logo DVD 屏保效果
开发语言·c++·学习·程序人生
kyle~1 天前
C++_STL---迭代器失效
开发语言·c++