mcpField
P
pyqt-styling

pyqt-styling

github.com/codeatcode设计与 UI2757 次安装今天更新
代码仓库

关于 pyqt-styling

pyqt-styling 是面向 PyQt 开发者的 Qt Style Sheets(QSS)样式技能,说明 QSS 与 CSS 的差异,并介绍如何处理级联和跨平台渲染问题。

主要功能

  1. 01QSS 与 CSS 的差异说明
  2. 02跨平台样式问题排查
  3. 03主题令牌与自定义属性
  4. 04控件样式与语法示例
  5. 05复杂绘制的替代方案

适用场景

  1. 01定位选择器或属性不生效原因
  2. 02统一管理应用主题颜色
  3. 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 相同的语法,但支持的选择器、属性和层叠行为有所不同。这些差异是大多数跨平台问题的根源。

不支持或有所不同的选择器
选择器CSSQSS
::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;
}

深入了解

最佳实践

  1. 将主题令牌化 - 使用单一 QSS 源并替换占位符,而不是为每个控件单独设置样式表
  2. 在所有平台上测试 - 颜色、字体和原生渲染效果差异可能很大
  3. 使用自定义属性 - 使用 setProperty() 设置主题变量,而不是硬编码颜色
  4. 复杂渲染优先使用委托 - 不要用 QSS 实现渐变、动画或跨平台绘制
  5. 验证边框圆角裁剪效果 - 如果子控件必须遵循圆角,请使用遮罩或自定义 paintEvent
  6. 批量更新时阻止信号 - 批量更改前调用 widget.blockSignals(True),然后调用 blockSignals(False)

参考资料

评论

支持 Markdown