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 中的 padding、margin、min-width 等属性会参与 Qt Style 对控件内容区域、尺寸提示等方面的计算,但它们不是 CSS 那种完整的页面布局系统 ,控件之间的整体布局仍由 QLayout、布局管理器和控件几何属性负责。
二、工程中组织 QSS:从文件加载到可切换主题
一个可维护的入门项目不需要复杂框架,但应把"主题数据"和"加载动作"分开。下面的结构足够覆盖多数 Widgets 项目:
text
qss_theme_demo/
├── CMakeLists.txt
├── main.cpp
├── resources.qrc
└── themes/
├── light.qss
└── dark.qss
themes 中的文件只描述外观,不写业务判断;main.cpp 的 ThemeController 只负责读取资源、应用完整样式表和通知界面更新。主题文件放进 .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();
}
这里有三个常见细节:
- 在创建窗口前先应用默认主题,可避免窗口第一次出现时先闪过未设置的原生样式。
QFile打不开时不要继续把空字符串传给setStyleSheet(),否则会清除当前主题;先返回false,保留原有视觉状态。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 专有属性。下表覆盖入门项目的高频项:
| 类别 | 常用属性 | 说明 |
|---|---|---|
| 颜色与文字 | color、background、background-color、font-size、font-weight |
文字、背景与字体 |
| 边框与圆角 | border、border-color、border-width、border-radius |
一旦自定义按钮边框,通常要同时补全状态样式 |
| 间距与尺寸 | margin、padding、min-width、min-height |
不替代布局,但能改变控件内容区与最小尺寸 |
| 图片 | image、background-image、border-image |
可引用 :/ 资源路径 |
| Qt 专有 | qproperty-iconSize、subcontrol-position、subcontrol-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 的组成部分
QComboBox、QSpinBox、QScrollBar 并非一个矩形。它们内部有下拉按钮、箭头、加减按钮、滑块等可被 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-origin、subcontrol-position 协调。第一次定制复合控件时,建议先只改颜色和宽高,在目标平台实际观察;过早重写所有子控件,往往会丢掉原生 Style 的一部分可用细节。
五、覆盖规则:为什么"写了 QSS 还是没变化"
QSS 的结果不是简单的"最后一行覆盖前一行"。需要按以下顺序判断:
- 规则来自哪个作用域:应用、父控件还是目标控件自身?更近的控件样式表优先。
- 在同一有效样式表中,哪条选择器更具体?ID 和属性条件通常比单独类型更具体。
- 若具体程度相同,后出现的规则覆盖先出现的同名属性。
- 目标属性是否被该控件和当前平台 Style 支持?某些控件一旦深度定制,还需要同时覆盖其子控件。
例如下面三条规则处于同一份应用主题时:
css
QPushButton { color: #334155; }
QPushButton[role="primary"] { color: white; }
QPushButton#saveButton { color: #fef3c7; }
objectName 为 saveButton 且 role 是 primary 的按钮最终是淡黄色,因为 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 样式表会继承,font 和 color 仍要克制
应用或父控件的样式表会向子控件提供可匹配规则。font、color 等视觉基线也常会随控件树延续。这样很方便,但一条宽泛的 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++ 中的 objectName 和 role 是稳定语义锚点 ,QSS 中的颜色、边框和间距是可替换表现。后续改成深色主题、提高高对比度,或把主按钮换成品牌色,都不需要触碰任务控制逻辑。
七、源码视角:调用 setStyleSheet() 后发生了什么
到这里已经能写出可维护主题,但还需要校正一个直觉:setStyleSheet() 不是"把颜色立即画到每个像素上"。它保存一段文本、建立或更新 QSS 代理 Style、让控件失效并在后续绘制时按规则计算结果。

图 3:样式设置与真正绘制是两件事。设置阶段解析并更新规则,随后由 StyleChange、布局/重绘请求和事件循环驱动控件在需要时重新绘制。私有类名称用于理解源码,不是业务代码应直接依赖的 API。
以下路径以 Qt 6 的 Widgets 源码为参照。私有函数名和局部细节可能随小版本变化,但公开 API 到样式代理再到绘制这一层次保持稳定。
本节涉及的 QStyleSheetStyle、QCss::Parser、qcssparser_p.h 等名称主要用于阅读 Qt 源码。它们属于 Qt 内部实现或 private API,不应该作为业务代码的依赖。实际开发只需要使用 setStyleSheet()、QStyle、QPalette 等公开 API。
7.1 第一步:应用级与控件级入口不同
当调用:
cpp
qApp->setStyleSheet(styleSheetText);
入口在 src/widgets/kernel/qapplication.cpp 的 QApplication::setStyleSheet()。它会确保应用使用一个 QStyleSheetStyle 代理 Style,并将新文本设置为全局样式表。QStyleSheetStyle 包装原有的基础 QStyle;没有被 QSS 接管的绘制仍可委托给基础 Style。
当调用:
cpp
widget->setStyleSheet(styleSheetText);
入口在 src/widgets/kernel/qwidget.cpp 的 QWidget::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 property 或 Could not parse stylesheet 的信息。它不会因为一条样式错了就让程序崩溃,但"没崩溃"不代表规则正确。因此开发时要保留应用输出窗口,先修正第一个 QSS 警告,再判断是否存在优先级问题。
7.3 第三步:控件重绘时,代理 Style 回答"怎么画"
Widgets 的绘制通常不是业务代码直接画按钮边框,而是控件构造 QStyleOption,调用当前 QStyle 的 drawPrimitive()、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();
再检查选择器是否假定了错误的控件类型或父子层级。对 QComboBox、QSpinBox、QScrollBar 这类复合控件,还要确认自己改的是主体还是 ::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 选择器 |
role、severity 等动态属性用于视觉角色 |
可在多页面复用主操作、危险操作、错误状态 |
| 浅色与深色主题的选择器保持同构 | 切换主题不会遗漏某一类控件 |
| 业务逻辑不读取控件的颜色和 QSS 文本 | UI 表现不应成为业务规则来源 |
最后补充一个边界:可访问性和系统一致性优先于"所有像素都自定义"。文本与背景要有足够对比度,焦点状态不能只靠很细的颜色变化,禁用状态也不能让文字完全不可读。产品若要求严格跟随系统高对比度或辅助功能设置,应先评估系统 Palette 和原生 Style 的行为,再决定 QSS 覆盖到什么程度。
总结
QSS 的上手入口很小:一条 QPushButton { ... } 规则就能改变控件外观;真正可维护的入口则是"资源文件 + 应用级主题 + 稳定语义锚点"。先让 QSS 文件描述所有通用视觉,再用 objectName 描述唯一结构、用动态属性描述可复用角色、用伪状态描述交互反馈。
从源码看,setStyleSheet() 保存的不是一张立即画出的位图。Qt 将 QSS 解析为规则,通过 QStyleSheetStyle 包装基础 Style,并在控件查询度量、处理样式变化和后续绘制时应用这些规则。理解"设置、匹配、重绘"三步,就能解释绝大多数样式不生效、切换不刷新和复合控件错位的问题。
下一章将进入国际化与本地化:文字不再直接写死在界面上,而会通过 tr()、.ts 和 .qm 文件随语言环境切换。QSS 的主题系统与国际化共同构成 Widgets 界面层的两项基础配置能力。
下一篇预告:《15_国际化和本地化(tr()、ts 文件、QM 文件、多语言切换)》