# QssParser **Repository Path**: birdman1992/qss-parser ## Basic Information - **Project Name**: QssParser - **Description**: 校验qt样式表合法性,以及使用sass编译scss文件展开成qss样式表。 包含多主题实现方案 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-22 - **Last Updated**: 2026-05-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # QssParser Qt 样式表(QSS)编译工具链 —— SCSS 预编译 + 语法校验 + 编码检测。 ## 解决的问题 Qt 项目通常把多个 `.qss` 文件拼接后 `qApp->setStyleSheet()`。一旦某个文件有语法错误(缺少分号、括号不匹配、编码乱码),Qt 的 QSS 引擎会**截断后续所有样式**,导致部分控件样式失效,排查困难。 本项目的方案: | 阶段 | 工具 | 作用 | |------|------|------| | 源码维护 | SCSS(`.scss`) | 变量、嵌套、模块化,编译期语法检查 | | 编译 | sass(dart-sass 独立二进制) | SCSS → 标准 QSS,错误时直接拒绝产出 | | 运行时兜底 | QssValidator(C++) | 编码检测、分号/括号校验,拦截漏网之鱼 | ## 项目结构 ``` ├── styles/ # SCSS 源码(人维护) │ ├── main.scss # 入口,@use 聚合所有模块 │ ├── _variables.scss # 变量(颜色、尺寸、字体) │ ├── _base.scss # 全局基础样式 │ ├── _button.scss # 按钮(含嵌套 & 伪状态) │ └── _input.scss # 输入框 ├── resources/ │ └── theme.qss # 编译产物(机器生成,已 gitignore) ├── src/ │ ├── qssvalidator.h │ └── qssvalidator.cpp # QSS 语法 / 编码校验器 ├── test/ │ └── main.cpp # 命令行工具 qsscheck ├── scripts/ │ └── qss_to_scss.py # .qss → .scss 迁移脚本 ├── tools/dart-sass/ # sass 独立二进制(离线可用,已 gitignore) └── CMakeLists.txt ``` ## SCSS 原理 SCSS 不是浏览器/Qt 能直接读懂的语言,它是个代码生成器。核心能力: ### 变量(编译期文本替换) ```scss // 源码 $primary-color: #0078d7; QPushButton { background-color: $primary-color; } ``` 编译后变量消失,变成普通 QSS: ```css QPushButton { background-color: #0078d7; } ``` ### 嵌套(展平为平铺选择器) 嵌套是 SCSS 最节省工作量的特性——源码里用缩进表达层级关系,编译器把每一层展平成完整的 CSS 选择器链。 **`&` 代表父选择器**,sass 编译时会把它替换为上层的完整选择器路径。 #### 伪状态嵌套 ```scss QPushButton { background-color: $primary-color; color: $text-white; &:hover { background-color: $primary-hover; } &:pressed { background-color: $primary-pressed; } &:disabled { background-color: $primary-disabled; } } ``` 编译后 `&` 被替换为 `QPushButton`,产生四个独立规则块: ```css QPushButton { background-color: #0078d7; color: #ffffff; } QPushButton:hover { background-color: #1a8fe8; } QPushButton:pressed { background-color: #005a9e; } QPushButton:disabled { background-color: #a0c4e8; } ``` #### 多级嵌套 + 子选择器 ```scss QMainWindow { background: white; > QCentralWidget { background: gray; QPushButton { color: blue; &:hover { color: red; } } } } ``` 展平过程——sass 从内向外拼接选择器链: ``` QMainWindow QMainWindow > QCentralWidget QMainWindow > QCentralWidget QPushButton QMainWindow > QCentralWidget QPushButton:hover ``` 编译后: ```css QMainWindow { background: white; } QMainWindow > QCentralWidget { background: gray; } QMainWindow > QCentralWidget QPushButton { color: blue; } QMainWindow > QCentralWidget QPushButton:hover { color: red; } ``` #### 多选择器列表嵌套 `sass` 会把逗号分隔的选择器和嵌套一一展开,组合出全部排列: ```scss QLineEdit, QTextEdit, QPlainTextEdit { border: 1px solid $border-color; &:focus { border-color: $border-focus; } &:disabled { background-color: $bg-light; } } ``` 编译后: ```css QLineEdit, QTextEdit, QPlainTextEdit { border: 1px solid #cccccc; } QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus { border-color: #0078d7; } QLineEdit:disabled, QTextEdit:disabled, QPlainTextEdit:disabled { background-color: #f5f5f5; } ``` #### Qt 子控件嵌套 ```scss QComboBox { border: 1px solid $border-color; &::drop-down { width: 30px; &::down-arrow { image: url(:/icons/arrow-down.png); } } } ``` 编译后: ```css QComboBox { border: 1px solid #cccccc; } QComboBox::drop-down { width: 30px; } QComboBox::drop-down::down-arrow { image: url(:/icons/arrow-down.png); } ``` ### @use 导入机制 SCSS 通过 `@use` 加载模块,编译器递归解析依赖树: ```scss // main.scss —— 入口 @use 'variables'; // 对应 _variables.scss @use 'base'; // 对应 _base.scss @use 'button'; // 对应 _button.scss @use 'input'; // 对应 _input.scss ``` - 文件名前缀 `_` 表示模块,编译时不单独产出文件 - `@use 'xxx'` 自动查找 `_xxx.scss` - sass 只接受入口文件,其余模块沿 `@use` 自动追踪 ### Qt 特有语法兼容性 sass 基于 W3C CSS 语法规范,Qt QSS 中有些扩展语法 sass 无法解析。以下列出已知的不兼容语法及解决方案。 #### 否定伪状态 `:!xxx` QSS 支持用 `!` 前缀对任意伪状态取反(如 `:!enabled`、`:!hover`、`:!focus`)。`!` 不是标准 CSS 选择器字符,sass 会拒绝解析。 但**很多常见否定伪状态有直接的正向等价物**,优先用等价写法而非 `#{}` 绕过。 ##### Qt 全部伪状态速查 | 伪状态 | 描述 | |--------|------| | `:active` | 控件位于活动窗口中 | | `:adjoins-item` | `::branch` 与一个 item 相邻(QTreeView) | | `:alternate` | `alternatingRowColors` 开启时的交替行 | | `:bottom` | 位于底部(如 QTabBar 底部标签) | | `:checked` | 已勾选(如按钮勾选状态) | | `:closable` | 可关闭(如可关闭的 QDockWidget) | | `:closed` | 已关闭/折叠(如 QTreeView 未展开项) | | `:default` | 默认项(如默认 QPushButton) | | `:disabled` | 已禁用 | | `:editable` | QComboBox 可编辑 | | `:edit-focus` | 有编辑焦点(仅 Qt Extended) | | `:enabled` | 已启用 | | `:exclusive` | 属于排他组(如排他 QActionGroup 的菜单项) | | `:first` | 列表中的第一项 | | `:flat` | 扁平样式(如 flat QPushButton) | | `:floatable` | 可浮动(如 QDockWidget) | | `:focus` | 有输入焦点 | | `:has-children` | 有子项(如 QTreeView 父节点) | | `:has-siblings` | 有兄弟项 | | `:horizontal` | 水平方向 | | `:hover` | 鼠标悬停 | | `:indeterminate` | 半选状态(如 QCheckBox 部分勾选) | | `:last` | 列表中的最后一项 | | `:left` | 位于左侧 | | `:maximized` | 已最大化 | | `:middle` | 位于中间位置 | | `:minimized` | 已最小化 | | `:movable` | 可移动(如 QDockWidget) | | `:no-frame` | 无边框(如 frameless QLineEdit) | | `:non-exclusive` | 属于非排他组 | | `:off` | 切换控件处于 off 态 | | `:on` | 切换控件处于 on 态 | | `:only-one` | 列表中的唯一项 | | `:open` | 已打开/展开 | | `:next-selected` | 下一个项被选中 | | `:pressed` | 鼠标按下中 | | `:previous-selected` | 上一个项被选中 | | `:read-only` | 只读或不可编辑 | | `:right` | 位于右侧 | | `:selected` | 已选中 | | `:top` | 位于顶部 | | `:unchecked` | 未勾选 | | `:vertical` | 垂直方向 | | `:window` | 顶层窗口 | ##### 有正向等价物的否定伪状态 以下常见否定写法有现成的正选伪状态替代,**无需 `#{}`,直接用正向写法即可**: | 否定写法 | 正向等价物 | 说明 | |---------|-----------|------| | `:!enabled` | **`:disabled`** | 禁用状态 | | `:!editable` | **`:read-only`** | QComboBox 不可编辑 | | `:!checked` | **`:unchecked`** | 未勾选 | | `:!on` | **`:off`** | 切换控件 off 态 | | `:!open` | **`:closed`** | 折叠/关闭 | | `:!exclusive` | **`:non-exclusive`** | 非排他组 | **推荐写法**——直接用正向伪状态,sass 无需任何绕过: ```scss // 推荐:等价、简洁、sass 原生兼容 QLineEdit:disabled { background-color: $bg-light; } QComboBox:read-only { color: gray; } QPushButton:unchecked { border: none; } // 不推荐(除非确实需要 :!xxx 的语义) #{'QLineEdit:!enabled'} { background-color: $bg-light; } ``` ##### 无正向等价物的否定伪状态 以下伪状态没有直接的正向对应物,需要用 `#{}` 插值绕过: | 否定写法 | 原因 | 方案 | |---------|------|------| | `:!hover` | 无 `:not-hover` 等价 | `#{'Widget:!hover'}` | | `:!focus` | 无 `:not-focus` 等价 | `#{'Widget:!focus'}` | | `:!pressed` | 无 `:not-pressed` 等价 | `#{'Widget:!pressed'}` | | `:!selected` | 无 `:not-selected` 等价 | `#{'Widget:!selected'}` | | `:!active` | 无 `:not-active` 等价 | `#{'Widget:!active'}` | | `:!flat` | 无 `:not-flat` 等价 | `#{'Widget:!flat'}` | | `:!maximized` | 无 `:not-maximized` 等价 | `#{'Widget:!maximized'}` | ```scss // 没有正向等价物,必须用 #{} 绕过 #{'QWidget:!hover'} { opacity: 0.8; } #{'QLineEdit:!focus'} { border-color: $border-color; } ``` #### 函数参数中的 `:`(渐变、`qproperty-*`) sass 遇到 `()` 内的 `:` 时会误判为属性分隔符。典型场景是 `qlineargradient`: ```scss // 直接写 SCSS 会报错:Error: expected ")". QWidget { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 white, stop: 1 gray); } ``` **解决方法一**——用 `unquote()` 整段原样输出: ```scss QWidget { background: unquote("qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 white, stop: 1 gray)"); } ``` **解决方法二**——用 `#{}` 插值把带 `:` 的值拼回去: ```scss QWidget { background: qlineargradient(#{'x1: 0'}, #{'y1: 0'}, #{'x2: 0'}, #{'y2: 1'}, stop: 0 white, stop: 1 gray); } ``` `unquote()` 适合整个属性值都是 Qt 特有语法的场景,`#{}` 适合局部绕过。编译后均原样输出。 `qproperty-*` 动态属性声明同理需要 `#{}` 包裹: ```scss #{'QWidget[qproperty-visible="true"]'} { opacity: 1; } ``` #### 总结 | 不兼容语法 | 原因 | 首选方案 | 备选方案 | |-----------|------|---------|---------| | `:!enabled` 等,有正向等价物 | `!` 不是标准 CSS 选择器字符 | **用 `:disabled` / `:read-only` 等正向伪状态代替** | `#{}` 插值 | | `:!hover` 等,无正向等价物 | `!` 不是标准 CSS 选择器字符 | `#{}` 插值 | — | | `qlineargradient(x1: 0, ...)` | `:` 在 `()` 内被 sass 误判为属性分隔符 | `unquote("...")` | `#{}` 局部拼接 | | `qproperty-*` 属性选择器 | Qt 特有语法 | `#{}` 包裹整段选择器 | — | > 这些绕过手段只影响 SCSS 源码的写法,编译产物是标准 QSS,Qt 引擎正常解析。如需在 sass 层面彻底回避,可以把包含这些语法的样式单独写在 `.qss` 文件中,编译时通过脚本拼接。 ### 多主题支持 SCSS 变量可以出现在 `url()` 内(通过 `#{}` 插值),配合不同的入口文件和变量文件即可实现多主题输出。 核心思路:**一个入口文件对应一套主题变量,公共样式不变,编译时变量替换产生不同产物**。 #### 文件结构 ``` styles/ ├── theme-light/ │ └── _variables.scss # 亮色主题(颜色 + 图标路径) ├── theme-dark/ │ └── _variables.scss # 暗色主题 ├── _base.scss # 公共样式,所有主题共用 ├── _button.scss ├── main-light.scss # 入口 A:亮色 └── main-dark.scss # 入口 B:暗色 ``` **`styles/theme-light/_variables.scss`**: ```scss $primary-color: #0078d7; $icon-path: ":/icons/light"; $close-icon: "close.svg"; $search-icon: "search.svg"; ``` **`styles/theme-dark/_variables.scss`**(只改变量值,结构不变): ```scss $primary-color: #2b5f8a; $icon-path: ":/icons/dark"; $close-icon: "close.svg"; $search-icon: "search.svg"; ``` **`styles/_button.scss`**(两套主题共用): ```scss QToolButton { qproperty-iconSize: 16px 16px; &#closeBtn { qproperty-icon: url(#{$icon-path}/#{$close-icon}); } &#searchBtn { qproperty-icon: url(#{$icon-path}/#{$search-icon}); } } ``` > `url()` 内必须用 `#{}` 才能展开变量值。直接写 `url($icon-path)` 会被 sass 当作字面量原样输出,变量不会被替换。 **`styles/main-light.scss`**: ```scss @use 'theme-light/variables'; @use 'base'; @use 'button'; ``` **`styles/main-dark.scss`**: ```scss @use 'theme-dark/variables'; @use 'base'; @use 'button'; ``` #### 编译 ```bash # 产出亮色主题 QSS sass styles/main-light.scss resources/theme-light.qss --no-source-map --style expanded # 产出暗色主题 QSS sass styles/main-dark.scss resources/theme-dark.qss --no-source-map --style expanded ``` 产物中 `url()` 的路径已经不同: ```css /* theme-light.qss */ QToolButton#closeBtn { qproperty-icon: url(:/icons/light/close.svg); } QToolButton#searchBtn { qproperty-icon: url(:/icons/light/search.svg); } /* theme-dark.qss */ QToolButton#closeBtn { qproperty-icon: url(:/icons/dark/close.svg); } QToolButton#searchBtn { qproperty-icon: url(:/icons/dark/search.svg); } ``` #### CMake 多主题编译 用循环为每个主题生成独立的构建规则: ```cmake set(THEMES light dark) foreach(theme ${THEMES}) set(entry "${SCSS_DIR}/main-${theme}.scss") set(output "${CMAKE_SOURCE_DIR}/resources/theme-${theme}.qss") add_custom_command( OUTPUT ${output} COMMAND ${SASS_EXECUTABLE} ${entry} ${output} --no-source-map --style expanded DEPENDS ${SCSS_SOURCES} COMMENT "Compiling ${theme} theme" ) list(APPEND ALL_THEME_QSS ${output}) endforeach() add_custom_target(compile_qss DEPENDS ${ALL_THEME_QSS}) ``` 运行时按需加载对应的 `.qss` 文件即可。 #### 扩展:更多维度的差异化 变量不仅可以控制图标路径和颜色,也可以控制尺寸、字体等任何属性值: ```scss // theme-compact/_variables.scss $font-size: 12px; $btn-height: 24px; $spacing: 4px; // theme-comfortable/_variables.scss $font-size: 14px; $btn-height: 32px; $spacing: 8px; ``` 公共样式引用这些变量后,同一套 SCSS 源码即可编译出不同尺寸主题的 QSS 产物。 ## 从现有 .qss 迁移 使用 `scripts/qss_to_scss.py` 一键迁移: ```bash python scripts/qss_to_scss.py <输入目录> <输出目录> # 示例 —— 把项目里所有 .qss 迁移为 .scss python scripts/qss_to_scss.py ./old_qss ./styles # 强制覆盖已有文件 python scripts/qss_to_scss.py ./old_qss ./styles -f ``` 脚本做了什么: 1. 递归扫描输入目录下所有 `*.qss` 2. 保持相对路径复制到输出目录,后缀改为 `.scss` 3. 在输出目录根生成 `main.scss`,用 `@use` 导入全部模块 迁移后只需创建 `_variables.scss`,把常用颜色/尺寸提取为变量替换到各个模块即可。 ## 命令行手动编译 ```bash # 路径:项目根目录 tools/dart-sass/dart-sass/sass.bat styles/main.scss resources/theme.qss --no-source-map --style expanded ``` 参数说明: | 参数 | 含义 | |------|------| | `styles/main.scss` | 入口文件(唯一输入) | | `resources/theme.qss` | 输出文件 | | `--no-source-map` | 不生成 `.map` 调试映射文件 | | `--style expanded` | 输出风格。可选:`expanded`(格式化)、`compressed`(压缩为一行) | ### 验证错误拦截能力 故意在 `_button.scss` 中删掉闭合的 `}`: ```scss QPushButton { background-color: $primary-color; color: $text-white; /* ← 缺少闭合大括号 */ ``` 重新执行编译命令,sass 会直接报错并**拒绝产出 `theme.qss`**: ``` Error: expected "}". ╷ 4 │ /* ← 缺少闭合大括号 */ │ ^ ╵ styles/_button.scss 4:23 @use styles/main.scss 1:1 root stylesheet ``` 错误信息包含文件名、行号、列号,定位精确。 > **注意**:SCSS 语法中分号是可选的,sass 会在编译时自动补全。因此缺少分号不会触发 sass 报错,但缺少分号的 `.qss` 被 Qt 引擎加载时仍会引发截断。**缺少分号由 QssValidator 在运行时检测**——sass 负责结构性语法(括号匹配、嵌套正确性),QssValidator 负责编码和分号等 Qt 引擎敏感的细节。 ## CMake 集成 在你的主项目 `CMakeLists.txt` 中: ```cmake # 告诉 QssParser 你的 SCSS 目录和输出路径 set(SCSS_DIR "${CMAKE_SOURCE_DIR}/styles") set(SCSS_ENTRY "${SCSS_DIR}/main.scss") set(SCSS_OUTPUT "${CMAKE_SOURCE_DIR}/resources/theme.qss") # 引入 QssParser(可选:获取 qsscheck 用于运行时校验) add_subdirectory(path/to/QssParser) add_dependencies(YourApp compile_qss) ``` 编译链路: 1. CMake 先运行 sass,把 `.scss` 编译为 `theme.qss` 2. 编译后自动运行 `qsscheck` 校验产物 3. 校验通过后编译你的 Qt 程序 4. 你在 `.qrc` 资源中引用 `resources/theme.qss` ### SASS_EXECUTABLE 的查找策略 CMake 按以下优先级找 sass: 1. 系统 PATH 中的 `sass`(npm 全局安装时自动在 PATH 中) 2. 项目自带 `tools/dart-sass/dart-sass/sass.bat`(离线环境) 两个都找不到时,跳过 SCSS 编译但不阻断 C++ 构建,同时输出提示。 ## C++ 侧集成 编译产物是标准 QSS 文件,直接加载即可: ```cpp void loadStylesheet() { QFile f(":/resources/theme.qss"); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { qWarning() << "Cannot open compiled QSS"; return; } qApp->setStyleSheet(QString::fromUtf8(f.readAll())); } ``` 如果需要运行时的编码/语法兜底校验(例如还加载了用户提供的第三方 `.qss`): ```cpp QList errors = QssValidator::validateDirectory("path/to/extra/qss"); if (!errors.isEmpty()) { qWarning().noquote() << QssValidator::formatReport(errors); } ``` ## 环境要求 - C++17、Qt 5 或 Qt 6 - CMake 3.14+ - sass(已内置于 `tools/dart-sass/`,运行 `npm install -g sass` 即可使用系统版本)