【QSS】QSSの理解を深めよう【PySide2】

python,qt,PySide,PySide2

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#myid IDセレクタ (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:active QPushButton:hover QPushButton:focus ダイナミック疑似クラス 特定のユーザーアクション時にQPushButtonにマッチします
QDialog:first-child :first-child疑似クラス 親ウィジェット内の最初の子供のそれがQDialogウィジェットならマッチする
QDialog:lang(en) 言語に関する疑似クラス 内容がenという言語で書かれているQDialogウィジェットにマッチする

サブコントロール

QSSを使いこなすには、サブコントロールもしっかり把握しておく必要があります
これはCSSにはない、Qt独自のものになっています

複雑なウィジェットは、複数のパーツで構成されており、ウィジェットであったり、論理的なコンポーネントであったりします
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は、サブコントロールのサイズを制御するために使用できますがイメージを設定すると、暗黙的にサブコントロールのサイズが設定されることに注意する必要があります

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

疑似ステート

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

QPushButton:hover { color: white }

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

QPushButton!:hover { color: red }

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

QCheckBox:hover:checked { color: white }

疑似ステートのリスト
疑似ステート状態 説明
:active ウィジェットがアクティブなウィンドウにあるときに設定されます
:adjoins-item QTreeViewの::branchがアイテムに隣接している場合に設定されます
:alternate QAbstractItemView.AlternativeRowColors()がTrueに設定されている場合、QAbstractItemViewの行を描画するすべての代替行に設定されます
:bottom アイテムは下部に配置する
:checked アイテムがチェックされた
:closable アイテムを閉じることが可能
:closed アイテムは閉じた状態
:default アイテムがデフォルト
:disabled アイテムが無効
:editable QComboBoxが編集可能
: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