Qt Style Sheets – QSS の理解を深めよう

QSS,Tips,StyleSheet,PySide,PySide2,Python,Qt,Maya

QSSとはQt Style Sheetsの略で基本的にQtスタイルシートの用語と構文規則は、HTMLのCSSとほぼ同じです
CSSに親しみのある方は意外とすんなりできるかもしれませんがCSSってなに?という方には少々わかりずらいものかもしれません
この記事ではQSSに関しての理解を深めるためにQSSの基本的な使い方などを説明していきます

スタイルルール

スタイルルールはセレクタと宣言(デクラレイション)で構成されています

宣言(デクラレーション)プロパティの設定
セレクタ影響させるウェジェットの指定

それぞれは上の表のように指定していき、実際に宣言する場合は下のようになります

QPushButton {
    color: red
}

上のスタイルルールでは、QPushButtonがセレクタで、{ color: red }が宣言(ディクラレーション)です
このスタイルルールは、QPushButtonとそのサブクラスが文字色(前景色)に赤色を指定しています

注意する点

Qtスタイルシートは大文字と小文字を区別しないため、color、Color、COLOR、cOloRはすべて同じプロパティを参照しますがクラス名、オブジェクト名やQtのプロパティ名は大文字と小文字を区別しますので設定する際は注意する必要があります

1つのデクラレーションに対して複数のセレクタを指定することがする場合はコンマ , で区切ることができます

QLineEdit, QComboBox,
QPushButton {
    color: red
}

上のスタイルルールは下のスタイルルールと全く同じものです

QLineEdit {
    color: red
}
QComboBox {
    color: red
}
QPushButton {
    color: red
}

パターンマッチング

Qt Style Sheetsは、CSS2で定義されているすべてのセレクタをサポートしているためしているため、パターンマッチ(Pattern matching)の規則を用いて,単純なものから,子供や子孫,兄弟など前後関係に基づいて、各ウィジェットに適用されるスタイル規則を設定することができます
詳しい説明は記事にしていきます

パターンセレクタ説明
*ユニバーサルセレクタすべてのウィジェットにマッチする
QDialogタイプセレクタQDialogとそのサブクラスのインスタンスにマッチします
.QDialogクラスセレクタQDialogのインスタンスにマッチしますが、そのサブクラスにはマッチしません *[class~="QDialog"]とおなじです
QPushButton#myidIDセレクタ(ID型) myid であるすべてのQPushButtonインスタンスにマッチする
QDialog QPushButtonディセンダントセレクタQDialogの子孫(子、孫など)であるQPushButtonのすべてのインスタンスにマッチする
QDialog > QPushButtonチャイルドセレクタQDialogの直接の子であるQPushButtonのすべてのインスタンスにマッチします
QDialog + QPushButtonアジェイセントセレクタ兄弟要素QDialogの直前にあるQPushButtonにマッチします 
QPushButton[room]アトリビュートセレクタroomがアトリビュートに設定されているQPushButtonウィジェットにマッチする(値は問いません)
QPushButton[room="True"]アトリビュートセレクタQPushButtonウィジェットのアトリビュートroomの値が "True"と一致するものにマッチします
QPushButton[room~="True"]アトリビュートセレクタアトリビュートroomの値が空白類文字で区切られた語のリストであり,そのうちひとつが True という語であるQPushButtonウィジェットにマッチする
QPushButton[lang|="en"]アトリビュートセレクタアトリビュートlang値がハイフンで区切られた語のリストであり,そのリストが en という語で始っているQPushButtonウィジェットにマッチする
QWidget.sliderクラスセレクタQWidget[class~="slider"] と同じ
QPushButton:activeQPushButton:hoverQPushButton:focusダイナミック疑似クラス特定のユーザーアクション時にQPushButtonにマッチします
QDialog:first-child:first-child疑似クラス親ウィジェット内の最初の子供のそれがQDialogウィジェットならマッチする
QDialog:lang(en)言語に関する疑似クラス内容がenという言語で書かれているQDialogウィジェットにマッチする

サブコントロール

QSSを使いこなすには、サブコントロールもしっかり把握しておく必要があります
これはCSSにはない、Qt独自のものになっています
QSpinboxやQSliderなどの複雑なWidget(ウィジェット)は複数のパーツで構成されています
複数のパーツはウィジェットであったり論理的なコンポーネントだったりします
QCheckBoxは、アイコンとテキストの2つのパーツで構成されおり、テキストのスタイルだけでなく、アイコンに関連するスタイルも定義することができます。
アイコンの部分は、QCheckBox::indicatorのサブコントロールにあたります

QCheckBoxは::indicator { image: url(:/indicator.png) }

Qtのヘルプドキュメントにはサブコントロールの説明がありますが、サブコントロールが何であるかを理解していない人いるかもしれません

サブコントロールは常に他の要素-参照要素-を基準にして配置されます
この参照要素は、ウィジェットであったり、他のサブコントロールであったりします
例えば、デフォルトではQComboBox::drop-downはQComboBoxのPadding rectangleの右上に配置されおり、QComboBox::drop-down は、QComboBox::drop-down サブコントロールの Contents rectangleの中央に配置されます
原点のrectangleは、subcontrol-origin を使用して変更できます
例えば、drop-downをデフォルトのPadding rectangleではなく、QComboBoxのmargin rectangleに配置したい場合は、下のコードで指定できます

QComboBox {
    margin-right: 20px;
}
QComboBox::drop-down {
    subcontrol-origin: margin;
}

注意する点

widthとheightは、サブコントロールのサイズを制御するために使用できますがイメージを設定すると、暗黙的にサブコントロールのサイズが設定されることに注意する必要があります

踏み込んだ解説をしている記事


Qt Style Sheets – QSS の理解を深めよう Subcontrol編 ①
Qt Style Sheets – QSS の理解を深めよう Subcontrol編 ②


疑似ステート

セレクタには、ウィジェットの状態に応じてルールの適用を制限することを示す疑似ステートを含めることができます
擬似ステート状態はセレクタの最後に表示され、間にはコロン(:)が入ります
例えば下のコードでは、マウスがQPushButtonの上に乗ったときに適用されます

QPushButton:hover { color: white }

擬似ステート状態は、感嘆符(!)を使って否定することができます
例えば下のコードでは、QPushButtonの上にマウスが乗っていないときにルールが適用されます

QPushButton!:hover { color: red }

擬似ステート状態は連鎖させることができ、その場合は論理的なANDを明示します
例えば下のコードでは、チェックされたQCheckBoxにマウスが重なった場合、次のようなルールが適用されます

QCheckBox:hover:checked { color: white }
疑似ステートのリスト
疑似ステート状態説明
:activeウィジェットがアクティブなウィンドウにあるときに設定されます
:adjoins-itemQTreeViewの::branchがアイテムに隣接している場合に設定されます
:alternateQAbstractItemView.AlternativeRowColors()がTrueに設定されている場合、QAbstractItemViewの行を描画するすべての代替行に設定されます
:bottomアイテムは下部に配置する
:checkedアイテムがチェックされた
:closableアイテムを閉じることが可能
:closedアイテムは閉じた状態
:defaultアイテムがデフォルト
:disabledアイテムが無効
:editableQComboBoxが編集可能
:edit-focusアイテムには編集フォーカスがある
:enabledアイテムが有効
:exclusiveアイテムは、排他的なアイテムグループの一部です
:firstアイテムは(リストの)最初のもの
:flatアイテムはフラットです
:floatableアイテムが浮かせることができます
:focusアイテムに入力フォーカスがあります
:has-childrenアイテムには子供がいます
:has-siblingsアイテムには兄弟がいます
:horizontalアイテムの向きは水平
:hoverマウスをアイテムの上に置いています
:indeterminateアイテムの状態が不確定
:lastアイテムは(リスト内の)最後
:leftアイテムは左側に配置されています
:maximizedアイテムが最大化されます
:middleアイテムは中央(リスト内)にあります
:minimizedアイテムは最小化されます
:movableアイテムは移動できます
:no-frame枠がありません
:non-exclusiveアイテムは非独占的なアイテムグループの一部です
:off切り替えることができるアイテムの場合、これは「オフ」状態のアイテムに適用されます
:on切り替えることができるアイテムの場合、これは「オン」状態のウィジェットに適用されます
:only-oneアイテムは(リスト内の)唯一のものです
:openアイテムは開いた状態です
:next-selected(リスト内の)次の項目が選択されます
:pressedアイテムはマウスを使用して押されています
:previous-selected(リスト内の)前の項目が選択されています
:read-onlyアイテムは読み取り専用または編集不可としてマークされています
:rightアイテムは右側に配置されています
:selectedアイテムが選択されています
:topアイテムは上部に配置されます
:uncheckedアイテムはチェックされていません
:verticalアイテムは垂直方向です
:windowウィジェットはウィンドウです(つまり、トップレベルのウィジェット)

具体的な説明は記事にしていきます

コンフリクト

コンフリクトは、コードの重複によるリソースの無駄使いや二重処理を表しています
複数のスタイルルールが同じプロパティを異なる値で指定している場合、コンフリクトが発生します
ルールを決定するために、QtスタイルシートはCSS2の仕様に従っています
QSSは基本上書きになります

QPushButton#myButton { color: gray }
QPushButton { color: red }

上のように同じセレクタを複数書いた場合、「QPushButton」の文字色は赤{color:red;}になります
後に書いている指定で、前の指定が上書きされたイメージです
つまりmyButtonというQPushButtonインスタンスにマッチし、colorのプロパティに競合しているということです
この競合を解決するには、セレクター個別性(Specificity) の高いものを考慮する必要があるため下のように記述する必要があります

QPushButton { color: red }
QPushButton#myButton { color: gray }

個別性(Specificity)の優先度

QPushButton:hover { color: white }
QPushButton:enabled { color: red }

上の場合、どちらのセレクタも同じ個別性を持っているので、ボタンが有効になっている間にマウスがボタンの上に乗った場合、2番目のルールが優先されます
その場合にテキストを白にしたいのであれば、次のようにルールを並び替えます

QPushButton:enabled { color: red }
QPushButton:hover { color: white }

もしくは最初のルールをより具体的すると設定できます

QPushButton:hover:enabled { color: white }
QPushButton:enabled { color: red }

継承

Qtスタイルシートは、ウィジェットと、ウィジェットの階層内でその下にあるすべてのものに影響します
ウィジェットに明示的に(コードから、またはフォームエディターを使用して)設定した場合、ウィジェットの親は、アプリケーション全体には影響を受けません
たとえば、下のコードを設定した場合

app.setStyleSheet(QWidget { background-color: red; })

特定のウィジェットの場合、特定のウィジェットとそのすべての子の背景色は赤になります
アプリケーション全体のqssファイルから同じスタイルシートを設定すると、すべてのウィジェットの背景色が赤になってしまうため、ウィジェット間の継承には細心の注意を払う必要があります
適切なセレクタタイプを使用することが重要で親であるQWidgetの色を継承するのではなく、システムカラーを持っています
QWidgetとその子供に色を設定したい場合は、次のように書きます

app.setStyleSheet(QWidget, QWidget * { color: red; })

QWidget.setFont()やQWidget.setPalette()を使ってフォントやパレットを設定すると、子ウィジェットにも影響があります

もし、フォントやパレットが子ウィジェットにプロパゲートさせたい場合は、Qt.AA_UseStyleSheetPropagationInWidgetStylesを設定することでプロパゲートできます

QCoreApplication.setAttribute(Qt.AA_UseStyleSheetPropagationInWidgetStyles, True)

カスケード

カスケードは、様々なレベルで定義されたスタイルは、上流で定義されたものが下流へ引き継がれて適用されます
例えば、すべてのWidgetの文字サイズを20pxして、 QWidget要素の背景色を青くして、QPushButton要素の文字色を白くした場合には、 文字サイズは20px、背景色は青く、文字色は白いスタイルとなります

*{font-size: 20px;}
 /*この時点では、20pxの文字*/
QPushButton {background-color:red;}
/*この時点では、20pxの文字、赤い背景*/
QPushButton:hover {color:white;}
/*この時点では、20pxの文字、赤い背景、白い文字*/

競合が発生した場合、継承されたスタイルシートよりもウィジェット自身のスタイルシートが常に優先されます

具体的な説明は記事にしていきます

Setting QObject Properties

qproperty-構文を使って、Q_PROPERTYを設定することができます

propertyLabel {
    qproperty-pixmap: url(:/icon/property-pixmap.png);
}
propertyGroupBox {
    qproperty-titleColor: #ff0000;
}
QPushButton {
    qproperty-iconSize: 20px 20px;
}

参考
The Style Sheet Syntax
Qt Style Sheets Reference
selectors defined in CSS2