PythonもQGISプラグイン開発も素人が8日間没頭してQGISプラグインを作ってみた


開発のきっかけ

業務でQGIS+ogr2ogrを使った手作業が面倒だなと思っていた。
PythonにもQGISのプラグイン開発にも興味があった。
コロナ休業+ゴールデンウィークで長期の休み発生!。
プラグイン作ってみるしかないっしょっ!
ということで、
PythonもQGISプラグイン開発も素人だけどQGISプラグインを作ってみた。
をまとめます。

まず自分のスペック

位置情報関係の開発業務経験有、C++でのWindowsアプリ開発経験有、ESRI社のArcObjectsでのアプリ開発経験有、GUIフレームワークは一通り経験有(MFC、wxWidget、Qt etc)。
しかし、pythonはググりながら読めばなんとなく理解できる程度。pyqtもpyqgisも触ったことは無い。
ということで、pythonもpyqtもpyqgis初心者レベル。

つくるもの

  • ソースDB(群)の表示設定されているQGISプロジェクトファイルを読み込んで
  • Clippingしたいエリアを指定して
  • 出力するDBファイル名(GeoPackageのみ)
  • 出力するQGISプロジェクトファイル名を指定して実行すると
  • 指定エリアでソースDB(群)をクリッピングして
  • 指定されたDBに出力して
  • 元のQGISプロジェクトファイルで設定されているスタイルが適用されている
  • 指定されたQGISプロジェクトを作成する

というQGISのプラグインを開発する。

開発環境準備

Windows10での開発環境構築方法です。

QGISのダウンロードとインストール

QGISビギナーズマニュアル(3系)を参照しLTR版(長期リリース版) QGISスタンドアロンインストーラバージョン版(64bit)をダウンロードし、インストールする。

念のためPATH環境変数に以下が設定されていない場合は設定しておく。
確認方法はこちらを参照。

%QGIS%\bin
%QGIS%\apps\qgis-ltr\bin

Visual Studio 2019(コミュニティ版)

Pythonのデバッグ環境としてVisual Studio 2019(コミュニティ版)を選択しました。
VSCodeという選択肢もありましたが、今回の開発は初物づくしのため次回以降に使ってみることにしました。
Visual StudioのインストールとPython環境の設定はこちらを参照して行います。

QGISプラグイン開発準備

Plugin Builderを動かしてみる

以下のサイトを参考にしてダミーのプラグインのGUIを作成。
https://gis-oer.github.io/gitbook/book/materials/python/10/10.html
https://blog.goo.ne.jp/yoossh/e/925867ada61a401daa7602d8bcc3270d

pyrcc5の実行

以下の手順で実行し、プラグインの動作確認を行います。

  • Osgeo4W Shellを開く
  • ダミーのプラグインを保存したディレクトリに移動
  • pyrcc5 -o resources.py resources.qrc を実行
  • 動作確認
    • QGISを起動して「プラグインの管理とインストール」メニューから作成したダミーのプラグインをインストールし動作確認しエラーが出なければOK

環境変数 QGIS_PLUGINPATH の設定

QGISがプラグインを検索するデフォルトは
ユーザーのホームディレクトリ/AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins
と奥深い場所になることと、開発の過程でソース管理する場合Gitを使いたいこともあるでしょうから、環境変数でQGIS_PLUGINPATH を設定することを強くお勧めします。

スタンドアロンスクリプトでクリッピング処理を試す

ここでは、処理の骨格となる以下の機能を勉強も兼ねてスタンドアロンスクリプトと実装してみます。

  • 指定エリアでソースDB(群)をクリッピングして
  • 指定されたDBに出力して
  • 元のQGISプロジェクトファイルで設定されているスタイルが適用されている
  • 指定されたQGISプロジェクトを作成する

クリッピング処理の検討

結果的に試してみたのは以下の4パターン。
Processingプラグイン(clipvectorbyextent)で解決するのは今となっては自明でしたが、取り組み当初は暗中模索で、取り敢えず以下の順番で試しました。

  1. ogr2ogrをpythonから呼び出すパターン
  2. QgsVectorFileWriter.writeAsVectorFormatV2()を利用するパターン
  3. Processingプラグイン(clipvectorbyextent)を利用するパターン
  4. Processingプラグイン(nativeclip)を利用するパターン

ogr2ogrをpythonから呼び出すパターン

スタンドアロンスクリプトはこちら

よくよく調べてみると、Processing(clipvectorbyextent)ではogr2ogrを実行していることを発見。
しかし、ogr2ogrの引数を作るためのpythonの文字列処理の勉強することになったので良かった。

QgsVectorFileWriter.writeAsVectorFormatV2()を利用するパターン

得られる結果がClippingではなくIntersectであったためボツ。
しかし、QgsVectorFileWriterの使い方などいろいろ勉強になりました。

スタンドアロンスクリプトはこちら

processing(clipvectorbyextent)

Processingプラグインの下図の機能をプログラム的に実行してみます。

スタンドアロンスクリプトはこちら

今回は敢えて、クリッピングする領域のポリゴンをQgsVectorlayerのメモリ上に作成し、その後GeoPackageに保存して、QGISプロジェクトに追加しています。
processing.run()をデバッガーでステップインしていくことで、前述のコマンドプロンプト表示を回避している箇所(%QGIS%\apps\qgis-ltr\python\plugins\processing\algs\gdal\GdalUtils.pyの100行目あたりのsubprocess.Popen())の発見や、processingプラグインの作り方も何となく理解できました。

ハマりポイント(EXTENT指定)

params = { 
    'INPUT' : layer,
    'EXTENT' : cliplayer, # QgsRectangle指定はエラーとなる 敢えてQgsVectorLayer指定
    'OPTIONS': option,
    'OUTPUT' : outputlayer
    }
res = processing.run('gdal:clipvectorbyextent', params)

下記を実行し得られるヘルプではEXTENTにQgsRectangleを指定可能ですが、実際はエラーとなりました。

import os
import processing 
from processing.core.Processing import Processing,GdalAlgorithmProvider

Processing.initialize()
print(processing.algorithmHelp("gdal:clipvectorbyextent"))

processing(nativeclip)

Processingプラグインの下図の機能をプログラム的に実行してみます。

スタンドアロンスクリプトはこちら

ループ2回目以降は出力DBが上書きされてしまい、テーブルが追加されません。
QGISのC++ソースを見てみましたが、結果的にOUTPUTパラメータで出力DBにテーブルを追加する方法がわからず、QGIS自体をコンパイルしてデバッグしてみようかとも思いましたがそれだけで新たな記事を書けそうなボリュームになりそうなので深堀はしませんでした。おそらく仕様なのではないかなと推察。

これまでのまとめ

これまでの取り組みによって、クリッピング処理はclipvectorbyextentを利用することで決まり。
やりたいことの骨格はできあがりました。(取り消し線は確認済)
残りは、GUIを通してユーザに入力してもらう部分となります。

  • ソースDB(群)の表示設定されているQGISプロジェクトファイルを読み込んで
  • Clippingしたいエリアを指定して
  • 出力するDBファイル名(GeoPackageのみ)
  • 出力するQGISプロジェクトファイル名を指定して実行すると
  • 指定エリアでソースDB(群)をクリッピングして
  • 指定されたDBに出力して
  • 元のQGISプロジェクトファイルで設定されているスタイルが適用されている
  • 指定されたQGISプロジェクトを作成する

次はプラグインとして実装ですが、スクラッチから作るのはしんどいので、様々なプラグインを触っだりソースを見たりしてphoto2shapeが、画面イメージや処理フローが考えていたものに近かったためこれを参考にして開発を進めます。

QGISプラグイン実装開始

既に各種設定はできているので、取り組みの順番としては

  • Plugin Builderでプラグインを新規作成する
  • Qt Designerで画面を作成
  • photo2shapeを参考に、スタンドアロンスクリプトとして書いたコードをプラグイン用に修正

といったところになります。

Qt Designerで画面を作成

Plugin Builderでプラグインの作成してpyrcc5を実行したら、Qt Designerで画面を作成します。(Qt Designerの使い方こちら
Qt Designerは使ったことがあるので画面作成は時間をかけずにこんな感じの仕上がりにしました。

領域(現在:マップビュー)

現在のキャンバスの四隅座標が表示されています。
モードレスダイアログのため、GIS本体で拡大縮小やスクロールすると自動的に四隅座標が更新されるようにシグナル・スロットを設定します。
また、「レイヤから計算」「キャンバスに描画」ボタンは今回利用しないため常にDisableとなるように設定しました。

最小最大縮尺設定をクリアする

読み込んだプロジェクトをリネームして設定済のスタイルそのままでデータソースのみ書換を行いますが、その際縮尺設定をクリアするかどうかを選択できるようにします。
クリッピングするということはある程度の狭い領域ということを想定し、その場合縮尺設定されていると表示されなくなって焦ったりして使い勝手が悪いだろうとの想定です。

その他

処理の進捗を示すプログレスバーとログを表示するテキストエリアを用意しました。
スレッドを使ってクリッピング処理中にブロックされない工夫が必要です。

そして完成へ

photo2shapeという参考になるコードがあったので、ダイアログボックスへのアクセス・スレッドの扱いなどさほど苦労せず取り敢えず動くものが完成。

完成したプラグインのソースはこちら

まとめ

タイトルどおりの素人が8日間(実質50時間ぐらい)で作ったものなので、pythonの言語仕様やしきたりを知らずに書いている部分も多分にあり、また、PyQGISの使い方も見よう見真似で作っているのでもっとエレガントな書き方があるのだと思います。
今後も引き続き勉強しながら更新していきたいと考えています。