13
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

QSSでQGISプラグインをおしゃれなUIにする

Last updated at Posted at 2025-12-13

Frame 1.png

実際のプラグインのソースコードはこちら

QGISプラグインを作るときにUIをいい感じにカスタマイズしたいなと思っていたら、Qt Style Sheets(QSS)というのを知りました。QSSは、QtアプリケーションのUIの見た目をカスタマイズするための仕組みです。CSSに似た構文を使って、ウィジェットのスタイルを定義できます。

QSSを書くための準備

Qt DesignerでQSSを編集、プレビューする機能はあるので、これさえあればすぐにQSSを書くことは可能です。

image.png

しかし、個人的にはこれがかなり使いにくかったので、VS Codeなどの使い慣れたコードエディタで編集する方法を紹介します。
まず.uiファイルとは別に.qssという拡張子のファイルを作成します。

image.png

そして、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のスタイルング例

Frame 2.png

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のスタイリング例

Frame 3.png

/* 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の画面側で即反映されるようになります。
Kapture 2025-12-11 at 00.11.59.gif

Kapture 2025-12-11 at 00.16.47.gif

ちなみに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側によるpadingmarginの制御はできず、.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のホットリロードや画像の相対パス対応のやり方はこちらのプラグインのソースコードを参考にしました。

13
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?