概要
QGISのプラグインの実装例を紹介します。ここでは、プラグインを起動すると以下のようなウィンドウが開き、指定したレイヤのCRSを変更するプラグインを実装します。
このような機能はQGISの標準機能として既にあるので車輪の再発明ではありますが、レイヤやCRSを指定するUIの実装や、レイヤオブジェクトの操作など、プラグイン開発においてよく必要となる部分なので紹介したいと思います。
使用環境
OS | Windows 11 |
---|---|
QGIS | 3.28.7 LTR |
Python | 3.9.5 |
プラグイン開発に必要なファイルについて
プラグインに必要なファイルについては下記ドキュメントに記載されています。
上記ページにも記載がありますが、プラグインに必要なファイル一式が揃ったテンプレートの作成にはPlugin Builderが利用できます。Plugin Builderの使い方や生成されたファイルの中身については下記で解説されています。
実装するプラグインのファイル構成
今回実装するプラグインはPlugin Builderのテンプレートを参考にしつつ、アイコンファイルやリソースファイルは省略し、以下のようなファイル構成としました。
📂test_plugin ├─📄metadata.txt ├─📄__init__.py ├─📄plugin.py ├─📄my_dialog.py └─📄my_dialog_base.ui
各ファイルの内容については、以下で説明します。
metadata.txt
metadata.txt
にはプラグインの詳細画面などに表示するメタデータを ini
ファイル形式で記述します。
[general] name=Test Plugin qgisMinimumVersion=3.0 description=this is test plugin about=this is test plugin version=1.0 author=Hoge email=hoge@example.com repository=http://example.com
上記は必須項目だけ入力していますが、他にも様々な項目が記述できます。例えば、アイコンファイル icon.png
をフォルダに含め、以下のように記述することでアイコン付きのプラグインにすることができます。
icon=icon.png
その他、 metadata.txt
に記述可能な項目は下記のページで解説されています。
__init__.py
__init__.py
は作成したプラグインフォルダをモジュールとして認識させるのに必要なファイルです。ここではプラグインがQGISに読み込まれた際に呼び出される処理として、 classFactory()
関数を定義する必要があります。後述する plugin.py
の TestPlugin
クラスのインスタンスを返す処理を以下のように記述します。
def classFactory(iface): from .plugin import TestPlugin return TestPlugin(iface)
plugin.py
plugin.py
はプラグインのメイン処理となる部分をクラスとして実装します。具体的には、メニューやツールバーへのプラグインの追加処理、後述するダイアログの初期化処理、プラグイン呼び出し時のコールバック処理等を記述します。このクラスで実装する __init__()
, initGui()
, unload()
の3つの関数はQGISから呼び出しが行われる必須の関数です。
import configparser import pathlib from collections.abc import Callable from typing import Optional from qgis.core import Qgis from qgis.gui import QgisInterface from qgis.PyQt.QtCore import QObject from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction from .my_dialog import MyDialog class TestPlugin: def __init__(self, iface: QgisInterface): self.iface = iface # メタデータの読み込み self.module_dir = pathlib.Path(__file__).parent self.metadata = configparser.ConfigParser() self.metadata.read(self.module_dir.joinpath("metadata.txt"), encoding="utf-8") self.plugin_name = self.metadata["general"]["name"] # unload関数によりメニューやツールバーから削除するアクションの参照用 self.actions = [] # ダイアログの型アノテーション self.dialog: MyDialog def add_action( self, icon_path: str, text: str, callback: Callable[[], None], enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip: Optional[str] = None, whats_this: Optional[str] = None, parent: Optional[QObject] = None, ) -> QAction: icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if add_to_toolbar: self.iface.addToolBarIcon(action) if add_to_menu: self.iface.addPluginToMenu(self.plugin_name, action) self.actions.append(action) return action def initGui(self): # メニューやツールバーへの追加 self.add_action( icon_path=":images/themes/default/propertyicons/plugin.svg", text="Test plugin", callback=self.run, parent=self.iface.mainWindow(), ) # ダイアログの初期化 self.dialog = MyDialog(self.iface.mainWindow()) def unload(self): for action in self.actions: self.iface.removePluginMenu(self.plugin_name, action) self.iface.removeToolBarIcon(action) def run(self): # アクティブレイヤをデフォルトの処理対象としてダイアログを表示 self.dialog.show(self.iface.activeLayer()) # ダイアログがOKボタン押下以外で閉じられた時は何もしない result = self.dialog.exec() if result != 1: return # 対象レイヤがない時は何もしない if self.dialog.target_layer is None: return # 完了メッセージの表示 layer_name = self.dialog.target_layer.name() crs_id = self.dialog.target_layer.crs().authid() self.iface.messageBar().pushMessage( title="Info", text=f"レイヤ {layer_name} を {crs_id} に設定しました。", level=Qgis.Info, )
クラスに実装した各関数の内容を以下で説明します。
__init__()
関数
以降の処理に用いる各変数の初期化を行っています。メニュー表示用として self.plugin_name
に代入するプラグイン名については metadata.txt
のプラグイン名を利用するため、標準ライブラリの configparser
を用いて metadata.txt
の項目を取得しています。
self.module_dir = pathlib.Path(__file__).parent self.metadata = configparser.ConfigParser() self.metadata.read(self.module_dir.joinpath("metadata.txt"), encoding="utf-8") self.plugin_name = self.metadata["general"]["name"]
add_action()
関数
プラグイン呼び出しのための表示をメニューやツールバーに追加するユーティリティ関数で、次の initGui()
で呼び出します。
この関数はQGISから呼び出しが行われる関数ではないので関数名や引数は任意に変更できます。ここでは、Plugin Builderのテンプレートと同様に以下のような引数を設定しています。
引数 | 説明 |
---|---|
icon_path |
アイコンのパスを指定します。リソースファイルからビルドしたアイコンの場合は :/plugins/test_plugin/icon.png のようなパスを指定します。 |
text |
メニューに追加されるアイコンの説明テキストです。 |
callback |
アイコンをクリックした際に呼び出されるコールバック関数を指定します。 |
enabled_flag |
True を指定することでアクションが有効になります。 |
add_to_menu |
True を指定することでメニューへアイコンが追加されます。 |
add_to_toolbar |
True を指定することでツールバーへアイコンが追加されます。 |
status_tip |
アイコンにマウスホバーした際にステータスバーに表示するテキストです。 |
whats_this |
ウィジェットの説明文です。ただし、現在のQGISでは基本的に使用されていない機能なので、指定する必要はないです。 |
parent |
追加するアクションの親となるウィジェットを指定します。 |
initGui()
関数
プラグインの読み込みの際にQGISから呼び出される関数で、メニューやツールバーへのプラグインの追加とダイアログの初期化を行っています。
add_action()
ではアイコンとして icon_path=":images/themes/default/propertyicons/plugin.svg"
を指定しています。これは、QGISのプリセットとして利用できるsvgファイルで、利用可能なアイコンは下記のページに一覧にされています。
unload()
関数
プラグインを無効化した際にQGISから呼び出される関数で、メニューやツールバーのプラグインの削除を行っています。
run()
関数
プラグインで実装した機能を呼び出す際のコールバック関数で、initGui()
で add_action()
の引数として指定します。この関数が呼び出されると、次に実装するダイアログを表示します。ダイアログで行う処理が正常に終了したら、メッセージバーにメッセージを表示します。
my_dialog.py
my_dialog.py
はダイアログを定義するクラスを実装しています。後述の my_dialog_base.ui
をベースクラスとして読み込み、ダイアログのロジックをここで実装しています。
import pathlib from typing import Optional from qgis.core import QgsMapLayer from qgis.PyQt import uic from qgis.PyQt.QtWidgets import QDialog class MyDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.target_layer: Optional[QgsMapLayer] = None # uiファイルをベースクラスとしてダイアログを初期化 ui_file = pathlib.Path(__file__).parent.joinpath("my_dialog_base.ui") ui_class, _ = uic.loadUiType(ui_file, self) self.base_ui = ui_class() self.base_ui.setupUi(self) # コンボボックスのレイヤが変更された時のイベントを設定 self.base_ui.LayerComboBox.layerChanged.connect(self.layerChanged) def show(self, target_layer: Optional[QgsMapLayer]): # ダイアログが表示された時の処理で、コンボボックスに表示するレイヤを初期化する self.base_ui.LayerComboBox.setLayer(target_layer) self.layerChanged() super().show() def layerChanged(self): # コンボボックスのレイヤが変更された時の処理で、変数に格納するレイヤとコンボボックスに表示するCRSを変更する self.target_layer = self.base_ui.LayerComboBox.currentLayer() if not self.target_layer is None: self.base_ui.CrsComboBox.setCrs(self.target_layer.crs()) def accept(self): # ダイアログでOKが押下された時の処理で、対象レイヤのCRSを変更する if not self.target_layer is None: self.target_layer.setCrs(self.base_ui.CrsComboBox.crs()) super().accept() def reject(self): # ダイアログでキャンセルが押下された時の処理 super().reject()
my_dialog_base.ui
my_dialog.py
で使用するダイアログのベースクラスを定義しています。このuiファイルはQGISにバンドルされている Qt Designer で編集可能で、ドラッグアンドドロップ等の操作によりウィジェットの追加、配置が可能です。
my_dialog_base.ui
はXML形式で以下のように記述されます。
<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>MyDialogBase</class> <widget class="QDialog" name="MyDialogBase"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>600</width> <height>300</height> </rect> </property> <property name="windowTitle"> <string>Test Plugin</string> </property> <widget class="QLabel" name="LayerComboBoxLabel"> <property name="geometry"> <rect> <x>30</x> <y>30</y> <width>340</width> <height>30</height> </rect> </property> <property name="text"> <string>CRSを変更するレイヤー</string> </property> </widget> <widget class="QgsMapLayerComboBox" name="LayerComboBox"> <property name="geometry"> <rect> <x>30</x> <y>60</y> <width>540</width> <height>30</height> </rect> </property> </widget> <widget class="QLabel" name="CrsComboBoxLabel"> <property name="geometry"> <rect> <x>30</x> <y>120</y> <width>340</width> <height>30</height> </rect> </property> <property name="text"> <string>設定するCRS</string> </property> </widget> <widget class="QgsProjectionSelectionWidget" name="CrsComboBox"> <property name="geometry"> <rect> <x>30</x> <y>150</y> <width>540</width> <height>30</height> </rect> </property> </widget> <widget class="QDialogButtonBox" name="ButtonBox"> <property name="geometry"> <rect> <x>30</x> <y>240</y> <width>540</width> <height>30</height> </rect> </property> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> <property name="standardButtons"> <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> </property> </widget> </widget> <customwidgets> <customwidget> <class>QgsMapLayerComboBox</class> <extends>QComboBox</extends> <header>qgsmaplayercombobox.h</header> </customwidget> <customwidget> <class>QgsProjectionSelectionWidget</class> <extends>QWidget</extends> <header>qgsprojectionselectionwidget.h</header> </customwidget> </customwidgets> <resources /> <connections> <connection> <sender>ButtonBox</sender> <signal>accepted()</signal> <receiver>MyDialogBase</receiver> <slot>accept()</slot> <hints> <hint type="sourcelabel"> <x>20</x> <y>20</y> </hint> <hint type="destinationlabel"> <x>20</x> <y>20</y> </hint> </hints> </connection> <connection> <sender>ButtonBox</sender> <signal>rejected()</signal> <receiver>MyDialogBase</receiver> <slot>reject()</slot> <hints> <hint type="sourcelabel"> <x>20</x> <y>20</y> </hint> <hint type="destinationlabel"> <x>20</x> <y>20</y> </hint> </hints> </connection> </connections> </ui>
connections
タグでは、ButtonBox
のボタン押下時のシグナルに対して実行する MyDialogBase
の関数 accept()
, reject()
をスロットに指定しています。そのため、ButtonBox
のボタン押下時に my_dialog.py
の MyDialog
で定義した accept()
, reject()
が実行されるようになっています。
プラグインの動作確認
プラグインに必要なファイルとしては以上で、test_plugin
フォルダをプラグインディレクトリ %APPDATA%\QGIS\QGIS3\profiles\default\python\plugins
に保存し、QGIS上で有効化することで使用できます。
実際に動作を確認します。ここでは、サンプルデータとして政府統計の総合窓口(e-Stat)(https://www.e-stat.go.jp/)の令和2年国勢調査人口集中地区境界データを使用します。
レイヤを追加し、プラグインを起動すると、元々のCRSとして EPSG:6668
が表示されています。
CRSを EPSG:6677
に変更し、OKを押します。
CRSが変更され、完了したことを知らせるメッセージバーが表示されました。
あとがき
以上でプラグインの実装が完了しました。実装した機能としては車輪の再発明ですが、プラグインの基本的なファイル構成や、レイヤやCRSを指定するUIの実装、レイヤオブジェクトの操作等、プラグイン開発に必要な事項が一通り確認できました。