关于 pyqt-styling
pyqt-styling 是面向 PyQt 开发者的 Qt Style Sheets(QSS)样式技能,说明 QSS 与 CSS 的差异,并介绍如何处理级联和跨平台渲染问题。
主要功能
- 01QSS 与 CSS 的差异说明
- 02跨平台样式问题排查
- 03主题令牌与自定义属性
- 04控件样式与语法示例
- 05复杂绘制的替代方案
适用场景
- 01定位选择器或属性不生效原因
- 02统一管理应用主题颜色
- 03验证不同平台的控件外观
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codeatcode/ oss-ai-skills --skill pyqt-styling安装后在 Claude Code 中输入/pyqt-styling确认已加载。
附带文件references/dark-theme.md
详细介绍
PyQt 样式 - QSS(Qt 样式表)
QSS(Qt 样式表)是 Qt 的样式系统。它与 CSS 相似,但存在一些关键差异,会导致跨平台问题。
QSS 不是 CSS
QSS 使用与 CSS 相同的语法,但支持的选择器、属性和层叠行为有所不同。这些差异是大多数跨平台问题的根源。
不支持或有所不同的选择器
| 选择器 | CSS | QSS |
|---|---|---|
::pseudo-element | 支持(例如 ::before、::after) | 支持有限——仅支持 Qt 特有的伪元素,例如 ::indicator、::drop-down、::item |
后代选择器(A B) | 匹配嵌套元素 | 在大多数控件中不起作用——Qt 使用从父级到子级的层叠方式 |
子选择器(A > B) | 匹配直接子元素 | 部分支持,但控件层级比类似 DOM 的嵌套关系更重要 |
兄弟选择器(A + B、A ~ B) | 支持 | 不支持——控件之间不像 DOM 元素那样存在兄弟关系 |
属性选择器([attr]) | 完全支持 | 可通过 setProperty() 支持,但语法不同([attr="value"] 可用) |
类选择器(.my-class) | 支持 | 不支持——请改用 objectName 或自定义属性 |
Qt 忽略的属性
Qt 会静默忽略它未实现的 CSS 属性:
box-shadow- Qt 改用自定义属性或qproperty-前缀text-shadow- 未实现filter- 未实现(请改用QGraphicsEffect)transform- 未实现(请改用控件几何方法)position: absolute/fixed- Qt 使用布局或手动设置几何位置display: flex/grid- Qt 使用 QLayout 类,而非 CSS display::before、::after- 不支持(请改用子控件)
Border-Radius 裁剪问题
除非父控件设置了遮罩,否则父控件上的 border-radius 不会裁剪子控件:
# Bad - children overflow rounded corners
parent.setStyleSheet("border-radius: 10px;")
parent.addWidget(child) # child shows outside rounded area
# Good - use a mask
from PySide6.QtGui import QRegion
from PySide6.QtCore import QRect
parent.setMask(QRegion(QRect(0, 0, parent.width(), parent.height()), QRegion.Ellipse))
# Or use a custom paintEvent for rounded rectangle mask
层叠行为
控件从父级到子级的样式层叠方式与 DOM 不同:
- DOM:样式根据选择器优先级沿文档树层叠
- Qt:应用于父控件的样式会传播到子控件,前提是子控件没有显式设置样式表
- 主要区别:设置
app.setStyleSheet()会全局应用样式,但parent.setStyleSheet()只会影响未设置自己样式表的子控件
# Bad - child overrides parent
app.setStyleSheet("QPushButton { background: blue; }")
child_button.setStyleSheet("") # Empty string clears inherited style
# Good - use custom properties for theming
app.setStyleSheet("QPushButton { background: $primary-color; }")
child_button.setProperty("primary-color", "#0078d4")
child_button.style().polish(child_button)
平台差异
由于控件的原生渲染方式不同,相同的 QSS 在 Windows、macOS 和 Linux 上的呈现效果也不同。
默认控件尺寸
- 字体大小:macOS 的默认字体较大;Windows 的默认字体较小
- 内边距:原生外观各不相同——macOS 按钮的内边距较大,Windows 的较小
- 行高:不同平台间并不一致
解决方法:对关键控件显式设置 min-height 和 padding,不要依赖默认值。
禁用原生外观
Qt 控件可能会使用原生平台样式进行渲染,从而忽略 QSS:
# Force QSS rendering on Windows/macOS
widget.setAttribute(Qt.WidgetAttribute.WA_StyleSheet)
# For QComboBox dropdown - native rendering ignores QSS
combo.setStyleSheet("QComboBox::drop-down { border: none; }")
combo.view().setAttribute(Qt.WidgetAttribute.WA_StyleSheet)
高 DPI 缩放
QSS 使用设备无关像素,但不同平台的缩放行为各不相同:
# Set high-DPI scaling policy (Qt 6)
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt
app = QApplication(sys.argv)
app.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
在无法使用的平台上验证:使用 Qt 的远程桌面或 CI 服务(带 Xvfb 的 GitHub Actions),或在运行目标操作系统的 VM 中测试。
主题规范
将主题设计为令牌
主题应采用单一的 QSS 源文件,并通过占位符/调色板替换,而不是为每个控件分别设置样式表:
# Bad - per-widget stylesheets
class MainWindow(QMainWindow):
def __init__(self):
self.button1.setStyleSheet("background: #0078d4;")
self.button2.setStyleSheet("background: #0078d4;")
self.label.setStyleSheet("color: #333;")
# Good - tokenized theme
class Theme:
@staticmethod
def apply(app):
app.setStyleSheet("""
QPushButton { background-color: $primary; }
QLabel { color: $text-primary; }
""".replace("$primary", "#0078d4").replace("$text-primary", "#333"))
# Or use a palette
from PySide6.QtGui import QPalette, QColor
class Theme:
@staticmethod
def apply_dark(app):
palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor("#1e1e1e"))
palette.setColor(QPalette.ColorRole.WindowText, QColor("#ffffff"))
app.setPalette(palette)
在不同控件类型上测试主题更改
创建一个控件展示页面来验证主题更改:
class ThemeGallery(QWidget):
def __init__(self):
super().__init__()
# Create one of each widget type
self.buttons = [QPushButton(f"Button {i}") for i in range(3)]
self.inputs = [QLineEdit(f"Input {i}") for i in range(3)]
self.labels = [QLabel(f"Label {i}") for i in range(3)]
# Layout them all
# Switch themes with a button to verify consistency
何时使用自定义 paintEvent/Delegate
遇到以下情况时,请使用自定义绘制,而不是勉强使用 QSS:
- 需要 QSS 无法实现的渐变、阴影或复杂形状
- 需要逐像素控制(例如自定义进度条动画)
- 控件数量较多时,QSS 性能不佳
- 需要跨平台一致的渲染效果(QSS 会因平台而异)
class RoundedButton(QPushButton):
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Custom rounded rectangle with gradient
gradient = QLinearGradient(0, 0, 0, self.height())
gradient.setColorAt(0, "#0078d4")
gradient.setColorAt(1, "#106ebe")
painter.setBrush(gradient)
painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(self.rect().adjusted(1, 1, -1, -1), 8, 8)
基本语法(快速参考)
假设你已熟悉 CSS 选择器和优先级。重点介绍 Qt 特有的语法。
应用样式
全局应用
from PySide6.QtWidgets import QApplication
app = QApplication()
# Inline
app.setStyleSheet("""
QLabel { color: #333; }
QPushButton { padding: 5px 10px; }
""")
# From file
with open("style.qss", "r") as f:
app.setStyleSheet(f.read())
针对控件应用
button = QPushButton("Styled")
button.setStyleSheet("""
QPushButton {
background-color: blue;
color: white;
border-radius: 5px;
}
QPushButton:hover {
background-color: darkblue;
}
""")
自定义属性
# Set custom property
button = QPushButton("Primary")
button.setProperty("primary", True)
# Force style refresh
button.style().unpolish(button)
button.style().polish(button)
/* Use in QSS */
QPushButton[primary="true"] {
background-color: #0078d4;
color: white;
}
QPushButton[primary="true"]:hover {
background-color: #106ebe;
}
特定控件的样式
QPushButton
QPushButton {
background-color: #0078d4;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-weight: bold;
}
QPushButton:hover {
background-color: #106ebe;
}
QPushButton:pressed {
background-color: #005a9e;
}
QPushButton:disabled {
background-color: #cccccc;
color: #666666;
}
/* Flat button */
QPushButton[flat="true"] {
background-color: transparent;
color: #0078d4;
border: 1px solid #0078d4;
}
QLineEdit
QLineEdit {
background-color: white;
border: 1px solid #cccccc;
border-radius: 4px;
padding: 4px 8px;
selection-background-color: #0078d4;
}
QLineEdit:focus {
border: 2px solid #0078d4;
}
QLineEdit:disabled {
background-color: #f5f5f5;
color: #999999;
}
/* Password field */
QLineEdit[echoMode="2"] {
lineedit-password-character: 9679; /* Unicode bullet */
}
QComboBox
QComboBox {
background-color: white;
border: 1px solid #cccccc;
border-radius: 4px;
padding: 4px 8px;
}
QComboBox:hover {
border-color: #999999;
}
QComboBox::drop-down {
border: none;
width: 24px;
}
QComboBox::down-arrow {
image: url(down_arrow.png);
width: 12px;
height: 12px;
}
/* Dropdown list */
QComboBox QAbstractItemView {
background-color: white;
border: 1px solid #cccccc;
selection-background-color: #0078d4;
}
QTabWidget
QTabWidget::pane {
border: 1px solid #cccccc;
border-radius: 4px;
}
QTabBar::tab {
background-color: #f5f5f5;
border: 1px solid #cccccc;
padding: 8px 16px;
margin-right: 2px;
}
QTabBar::tab:selected {
background-color: white;
border-bottom-color: white;
}
QTabBar::tab:hover {
background-color: #e5e5e5;
}
QScrollBar
/* Vertical scrollbar */
QScrollBar:vertical {
background-color: #f5f5f5;
width: 12px;
margin: 0;
}
QScrollBar::handle:vertical {
background-color: #cccccc;
border-radius: 6px;
min-height: 30px;
}
QScrollBar::handle:vertical:hover {
background-color: #999999;
}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0;
}
深入了解
- 深色主题示例:请参阅 references/dark-theme.md,了解完整的 VS Code 风格深色主题样式表。
最佳实践
- 将主题令牌化 - 使用单一 QSS 源并替换占位符,而不是为每个控件单独设置样式表
- 在所有平台上测试 - 颜色、字体和原生渲染效果差异可能很大
- 使用自定义属性 - 使用
setProperty()设置主题变量,而不是硬编码颜色 - 复杂渲染优先使用委托 - 不要用 QSS 实现渐变、动画或跨平台绘制
- 验证边框圆角裁剪效果 - 如果子控件必须遵循圆角,请使用遮罩或自定义 paintEvent
- 批量更新时阻止信号 - 批量更改前调用
widget.blockSignals(True),然后调用blockSignals(False)
参考资料
评论
支持 Markdown