pyqt-styling について
pyqt-stylingは、PyQt開発者向けにQt Style Sheets(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 Style Sheets)
QSS(Qt Style Sheets)は Qt のスタイリングシステムです。CSS に似ていますが、クロスプラットフォームの不具合につながる重要な違いがあります。
QSS は CSS ではありません
QSS は CSS の構文を共有していますが、サポートされるセレクター、プロパティ、カスケードの動作が異なります。こうした違いが、クロスプラットフォームの不具合の大半の原因です。
サポートされない、または異なるセレクター
| セレクター | CSS | QSS |
|---|---|---|
::pseudo-element | サポートされています(例:::before、::after) | サポートは限定的です。::indicator、::drop-down、::item など、Qt 固有の疑似要素のみです |
子孫セレクター(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 では CSS の display ではなく、QLayout クラスを使用します::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)を利用するか、対象 OS の仮想マシンでテストしてください。
テーマ設定の原則
テーマをトークン化する
テーマは、ウィジェットごとのスタイルシートではなく、プレースホルダーまたはパレットを置き換える単一の 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/デリゲートを使用する場合
次のような場合は、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;
}
詳細解説
- ダークテーマの例:VS Code をイメージした完全なダークテーマのスタイルシートについては、references/dark-theme.md を参照してください。
ベストプラクティス
- テーマをトークン化する - ウィジェットごとのスタイルシートではなく、プレースホルダー置換を使う単一の QSS ソースを使用します
- すべてのプラットフォームでテストする - 色、フォント、ネイティブの描画は大きく異なります
- カスタムプロパティを使用する - 色をハードコードするのではなく、テーマ変数に
setProperty()を使用します - 複雑な描画にはデリゲートを優先する - グラデーション、アニメーション、プラットフォームに依存しない描画を QSS で無理に実現しようとしないでください
- border-radius のクリッピングを確認する - 子要素にも角丸を適用する必要がある場合は、マスクまたはカスタム paintEvent を使用します
- 一括更新中はシグナルをブロックする - 一括変更の前に
widget.blockSignals(True)を呼び出し、その後blockSignals(False)を呼び出します