実際のプラグインのソースコードはこちら
QGISプラグインを作るときにUIをいい感じにカスタマイズしたいなと思っていたら、Qt Style Sheets(QSS)というのを知りました。QSSは、QtアプリケーションのUIの見た目をカスタマイズするための仕組みです。CSSに似た構文を使って、ウィジェットのスタイルを定義できます。
QSSを書くための準備
Qt DesignerでQSSを編集、プレビューする機能はあるので、これさえあればすぐにQSSを書くことは可能です。
しかし、個人的にはこれがかなり使いにくかったので、VS Codeなどの使い慣れたコードエディタで編集する方法を紹介します。
まず.uiファイルとは別に.qssという拡張子のファイルを作成します。
そして、Python側で.uiファイルと一緒に.qssファイルもプラグインの初期化時にsetStyleSheetメソッドで読みこむようにします。
import os
from PyQt5.QtWidgets import QDockWidget
class DockPanel(QDockWidget):
def __init__(self, iface, parent=None):
super().__init__()
self.iface = iface
self.load_style()
def load_style(self):
# .qssを読み込む
style_path = os.path.join(os.path.dirname(__file__), "style.qss")
if os.path.exists(style_path):
with open(style_path, encoding="utf-8") as f:
self.setStyleSheet(f.read())
こうすることでCSSと同じようにスタイルシートを分離でき、お好みのコードエディッタ上で容易に編集ができるようになります。
また、VS Codeの settings.json でファイルの関連付け(File Associations)を行うことで、CSSのシンタックスハイライトや自動フォーマットが効くようになり、可読性が向上します。
(※あくまで「CSS扱い」にするため、コード補完機能には注意が必要です。)
"files.associations": {
"*.qss": "css",
"*.ui": "xml"
}
ちなみに.uiファイルから.qssファイルを直接読み込むことはできず、.uiファイル側だけでスタイルを適用するには、styleSheetプロパティにQSSのコードを直接記述するしかないみたいです。
<property name="styleSheet">
<string notr="true">
QWidget {
background-color: rgb(255, 255, 255);
color: rgb(0, 0, 0);
}
QPushButton {
background-color: rgb(33, 150, 243);
border-radius: 4px;
}
</string>
</property>
QSSの基本構文
QSSはセレクタと宣言で構成されます。
セレクタ {
プロパティ: 値;
プロパティ: 値;
}
QPushButtonのスタイルング例
QPushButton {
background-color: #589632; /* 背景色を緑にする */
color: #f4f7f9; /* 文字色を白にする */
border: none; /* 枠線をなくす */
border-radius: 15px; /* 角を丸くする */
}
セレクタの種類
ユニバーサルセレクタ
すべてのウィジェットに適用されます。
/* すべてのウィジェットに適用 */
* {
font-family: "Noto Sans JP";
font-size: 14px;
}
タイプセレクタ
すべてのウィジェット型に適用されます。
/* すべてのQPushButtonに適用 */
QPushButton {
background-color: blue;
}
/* すべてのQLineEditに適用 */
QLineEdit {
border: 1px solid gray;
}
IDセレクタ
特定の名前(objectName)のウィジェットに適用されます。
/* objectName="saveButton"のボタンのみ */
#saveButton {
background-color: green;
}
/* または */
QPushButton#saveButton {
background-color: green;
}
プロパティセレクタ
カスタムプロパティで条件指定します。
/* variant="primary"のボタン */
QPushButton[variant="primary"] {
background-color: #2196F3;
}
/* variant="secondary"のボタン */
QPushButton[variant="secondary"] {
background-color: #757575;
}
.uiファイル:
<widget class="QPushButton" name="saveButton">
<property name="variant" stdset="0">
<string>primary</string>
</property>
</widget>
疑似状態
ウィジェットの状態に応じて適用されます。
/* ホバー時 */
QPushButton:hover {
background-color: lightblue;
}
/* 押下時 */
QPushButton:pressed {
background-color: darkblue;
}
/* 無効時 */
QPushButton:disabled {
background-color: gray;
color: darkgray;
}
/* チェックされている時 */
QCheckBox:checked {
color: green;
}
/* フォーカス時 */
QLineEdit:focus {
border: 2px solid blue;
}
疑似状態の組み合わせ
/* ホバー中かつ有効 */
QPushButton:hover:enabled {
background-color: lightblue;
}
/* ホバー中かつ押下 */
QPushButton:hover:pressed {
background-color: navy;
}
/* 無効でない(有効) */
QPushButton:!disabled {
background-color: blue;
}
子孫セレクタ
特定のウィジェット内にあるウィジェットに適用されます。
/* QGroupBox内のすべてのQPushButton */
QGroupBox QPushButton {
background-color: yellow;
}
/* QDialog内のQLineEdit */
QDialog QLineEdit {
border: 2px solid blue;
}
子セレクタ
直接の子ウィジェットのみに適用されます。
/* QGroupBoxの直接の子のQPushButtonのみ */
QGroupBox > QPushButton {
background-color: yellow;
}
複数セレクタ
複数のセレクタに同じスタイルが適用されます。
/* カンマで区切る */
QPushButton,
QLineEdit,
QComboBox {
border: 1px solid gray;
border-radius: 4px;
}
/* 複数の条件 */
QPushButton[variant="primary"],
QPushButton[variant="secondary"] {
padding: 10px 20px;
}
サブコントロール
ウィジェットの一部分を指定します。
/* QComboBoxのドロップダウン矢印 */
QComboBox::drop-down {
border: none;
width: 20px;
}
/* QScrollBarのハンドル */
QScrollBar::handle:vertical {
background-color: gray;
min-height: 20px;
}
/* QSpinBoxの上ボタン */
QSpinBox::up-button {
background-color: lightblue;
}
QSliderのスタイリング例
/* QSlider本体 */
QSlider {
min-height: 40px;
}
/* グルーブ(トラック全体) */
QSlider::groove:horizontal {
border: none;
height: 8px;
background: qlineargradient(
x1: 0,
y1: 0,
x2: 1,
y2: 0,
stop: 0 #589632 stop: 1 #93b024
);
border-radius: 4px;
}
/* ハンドル(つまみ) */
QSlider::handle:horizontal {
background-color: #cdd1d4;
width: 20px;
height: 20px;
margin: -6px 0;
border-radius: 10px;
}
/* サブページ(ハンドル左側) */
QSlider::sub-page:horizontal {
background: transparent;
}
/* アッドページ(ハンドル右側) */
QSlider::add-page:horizontal {
background: #adadad;
border-radius: 4px;
}
優先度(カスケード)
先ほど紹介したセレクタは以下の順番で優先順位が強くなります。
* {
font-size: 14px;
}
QPushButton {
background-color: gray;
}
QWidget QPushButton {
background-color: lightgray;
}
QPushButton:hover {
background-color: blue;
}
QPushButton[variant="primary"] {
background-color: blue;
}
QPushButton[variant="primary"]:hover {
background-color: lightblue;
}
#saveButton {
background-color: green;
}
QPushButton#saveButton {
background-color: green;
}
QPushButton#saveButton:hover {
background-color: lightgreen;
}
QPushButton#saveButton[variant="primary"]:hover {
background-color: lightgreen;
}
優先順位が同じセレクタの場合は、後に書かれた方が優先されます。特に優先順位が同じ:hoverと:pressedの2つの疑似状態セレクタを使用する場合:pressedを後に書かないとうまく効きません。
/* 間違い:pressedが効かない */
QPushButton:pressed {
background-color: darkblue;
}
QPushButton:hover {
background-color: lightblue; /* 後なので優先 */
}
/* ボタンを押すと、hoverも同時に適用されるため、lightblueになる */
/* 正しい:pressedを後に書く */
QPushButton:hover {
background-color: lightblue;
}
QPushButton:pressed {
background-color: darkblue; /* 後なので優先 */
}
背景画像の読み込みに相対パスを対応させる
QSSのbackground-imageで背景画像やアイコン画像をUIに貼り付けることできますが、画像を読み込ませるには通常リソースファイルを別で作成する必要があります。
/* 通常の画像読み込みの記法 */
QPushButton {
background-image: url(:/icons/home.svg); /* 画像パス(Qtリソース) */
}
/* できれば以下のように相対パスで特定のディレクトリの画像を直接参照したい */
QPushButton {
background-image: url(./icons/home.svg);
}
この場合はPython側でQSS上に記述した相対パスの画像を絶対パスに変換して読み込むようにすることで、相対パスによる画像参照が可能になります。
def load_style(self):
"""スタイルシートを読み込む"""
if os.path.exists(self.style_path):
with open(self.style_path, encoding="utf-8") as f:
stylesheet = f.read()
# 画像パスを絶対パスに変換
# url(:/...) 形式(Qtリソース)は除外し、通常のファイルパスのみ変換
path = os.path.dirname(self.style_path).replace("\\", "/")
# url() の中身が : で始まらない場合のみ絶対パスに変換
def replace_url(match):
url_content = match.group(1)
# : で始まる場合(Qtリソース)はそのまま返す
if url_content.startswith(':'):
return match.group(0)
# それ以外は絶対パスに変換
return f'url("{path}/{url_content}")'
stylesheet = re.sub(
r'url\((.*?)\)',
replace_url,
stylesheet
)
self.setStyleSheet(stylesheet)
print(f"Loaded stylesheet: {self.style_path}")
else:
print(f"Stylesheet not found: {self.style_path}")
スタイルシートを自動反映させたい
QSSを書きながらプラグインのUIの見た目を整える際、Webフロントエンドを普段開発してる身としてはHTML/CSSのように編集内容をリアルタイムで画面上に反映されてほしいところです。Plugin Reloaderで毎回手動更新するのはすこし手間になります。
QtフレームワークにQFileSystemWatcherというファイルやディレクトリの変更を監視するためのクラスがあったので、これを利用してホットリロードを実装できます。
(※この機能は開発時のみ有効にし、リリース版では無効化することを推奨します)
QDockWidgetの例です
import os
import re
from PyQt5.QtCore import QFileSystemWatcher, Qt
from PyQt5.QtWidgets import QApplication, QDockWidget
from qgis.PyQt import uic
class DockPanel(QDockWidget):
def __init__(self, iface, parent=None):
super().__init__()
self.iface = iface
# パスを保存
self.ui_path = os.path.join(os.path.dirname(__file__), "dock.ui")
self.style_path = os.path.join(os.path.dirname(__file__), "dock.qss")
# UIを読み込む
self.load_ui()
# .qss を読み込む
self.load_style()
# ファイル監視を設定
self.setup_file_watcher()
def load_ui(self):
# UIファイルを読み込む
if os.path.exists(self.ui_path):
# 既存のウィジェットをクリア(再読み込み時)
if hasattr(self, '_ui_loaded'):
# 子ウィジェットを全て削除
for child in self.findChildren(QDockWidget):
child.deleteLater()
# UIを読み込み
uic.loadUi(self.ui_path, self)
self._ui_loaded = True
print(f"Loaded UI: {self.ui_path}")
else:
print(f"UI file not found: {self.ui_path}")
def load_style(self):
# スタイルシートを読み込む
if os.path.exists(self.style_path):
with open(self.style_path, encoding="utf-8") as f:
stylesheet = f.read()
self.setStyleSheet(stylesheet)
else:
print(f"Stylesheet not found: {self.style_path}")
def setup_file_watcher(self):
# ファイル監視を設定
self.file_watcher = QFileSystemWatcher()
# .uiファイルを監視
if os.path.exists(self.ui_path):
self.file_watcher.addPath(self.ui_path)
# .qssファイルを監視
if os.path.exists(self.style_path):
self.file_watcher.addPath(self.style_path)
def on_file_changed(self, path):
# ファイルが変更されたときに呼ばれる
# ファイル監視をクリア
self.file_watcher.removePaths(self.file_watcher.files())
# どのファイルが変更されたか判定
if path == self.ui_path:
self.load_ui()
# UIリロード後はスタイルも再適用
self.load_style()
elif path == self.style_path:
self.load_style()
# ファイル監視を再追加
if os.path.exists(self.ui_path):
self.file_watcher.addPath(self.ui_path)
if os.path.exists(self.style_path):
self.file_watcher.addPath(self.style_path)
# UIを強制更新
app = QApplication.instance()
app.processEvents()
これにより、.uiまたは.qssを更新するとQGISの画面側で即反映されるようになります。

ちなみにQDialogだと、以下のようにウィジェット、レイアウトを一旦削除してから再読み込みする処理を書かないとうまくいきませんでした。
def load_ui(self):
# 現在のウィンドウ情報を保存
geometry = self.geometry()
was_visible = self.isVisible()
# 全ての子ウィジェットを削除(file_watcher以外)
for child in self.children():
if child != self.file_watcher and hasattr(child, "deleteLater"):
child.deleteLater()
# レイアウトをクリア
if self.layout():
old_layout = self.layout()
while old_layout.count():
item = old_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
# レイアウト自体も削除
old_layout.deleteLater()
# UIを再読み込み
self.load_ui()
# スタイルを再適用
self.load_style()
# ウィンドウ状態を復元
self.setGeometry(geometry)
if was_visible:
self.show()
# 強制的に再描画
QApplication.processEvents()
self.update()
self.repaint()
QSSをさわってみて
CSSにくらべてプロパティの自由度が低いので書くのがちょっとつらいです。スタイリングの表現も少ないため、どうしても表現しきれないものは画像で表現して、その画像をUIに貼り付けるという昔のCSSみたいなゴリ押し技になるとおもいます。
またスタイリングをする上で、layoutタグはQSS側によるpadingやmarginの制御はできず、.ui側でlayoutタグの中にpropertyタグを記述して余白を調整するしかないみたいです。
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>20</number>
</property>
<property name="topMargin">
<number>20</number>
</property>
<property name="rightMargin">
<number>20</number>
</property>
<property name="bottomMargin">
<number>20</number>
</property>
<property name="spacing">
<number>10</number>
</property>
便利サイト
Qt Style Sheets Reference
Qt Style Sheets の公式リファレンスです。
https://doc.qt.io/qt-6/stylesheet-reference.html
Qss Stock
Qt Style Sheetsのテンプレート集を提供しているサイトです。スタイリングの参考になると思います。
https://qss-stock.devsecstudio.com/index.php
PyQGIS icon cheatsheet
QGISに既存で用意されているアイコン画像のパスのチートシートです。ここからアイコン画像を使用することもできます。
https://pyqgis-icons-cheatsheet.geotribu.fr/#themesdefaultstyleicons
また、QGIS上で確認できるプラグインもあります。
今回参考にしたもの
.qssを読み込んでQGIS全体のスタイルを変えるQGISプラグインがあります。この記事で紹介した.qssのホットリロードや画像の相対パス対応のやり方はこちらのプラグインのソースコードを参考にしました。





