QGIS プラグインの動的ロードと実行

この記事は2018年3月くらいに一時公開していたものの再投稿です。

# 目標
「[QGIS プラグインから他のプラグインを呼び出してみる](https://qiita.com/ozo360/items/796db4ce379e28bf78ee)」を書いていたらプラグインの動的ロードと組み合わせたら色々できるのではないかと閃いたので試してみたいと思います。
ついでに動的ロードしたプラグイン同士をシグナルで繋げて動かしてみよう。

# 下準備
Githubにテスト用のダミープラグインを用意しました。ダウンロードは[こちら](https://github.com/ozo360/DynamicLoadTestPlugin)です。
QGIS3のプラグインフォルダに`aaa_plugin`、`bbb_plugin`というフォルダを作成してその中にダウンロードしたソースファイルをコピーしておきます。

# Pythonコンソールから動的ロード
QGISの[プラグイン]メニューから[Pythonコンソール]を起動します。
まずは現在読み込まれているプラグインリストを確認してみましょう。

```
>>> qgis.utils.plugins.keys()
dict_keys(['pluginbuilder3', 'plugin_reloader', 'db_manager', 'processing'])
```

`aaa_plugin`も`bbb_plugin`も読み込まれていませんね。
では`aaa_plugin`をロードしてみます。

```
>>> plugin_name = 'aaa_plugin'
>>> qgis.utils.loadPlugin(plugin_name)
True
>>> qgis.utils.startPlugin(plugin_name)
True
>>> qgis.utils.plugins.keys()
dict_keys(['pluginbuilder3', 'plugin_reloader', 'db_manager', 'processing', 'aaa_plugin'])
```

`aaa_plugin`がロードされて`plugins`に追加されていることが確認できました。
では`aaa_plugin`のメソッドを叩いてみましょう。

```
>>> plugin = qgis.utils.plugins[plugin_name]
>>> plugin.handle_trigger('dynamic_load')
```

引数に与えた文字列がメッセージダイアログで表示されました。
問題なくプラグインのロードができたようです。

# プラグイン内で動的ロード＆シグナル
今度はプラグイン(aaa_plugin)内で他のプラグイン(bbb_plugin)をロードして、ロード先からロード元のシグナルにコネクトして、ロード元のシグナルを受けてロード先がメッセージを表示するってのをやってみます。
言葉で書くと訳が判らないのでなんちゃってシーケンス図を貼っておきます。
![Dynamic Sequence.png](https://qiita-image-store.s3.amazonaws.com/0/240181/7003d50b-d452-466f-15a8-f19caa4980be.png)

プラグインを動的ロードする`loadAndExec`とカスタムシグナルを使ってシグナルを受け渡す、`emitMessage`と`connectTrigger`を追加します。
Gitからソースをダウンロードした方は、適宜コメント箇所を外してください。

カスタムシグナルの使い方は以下の記事を参考にさせていただきました。
[PyQt5とpython3によるGUIプログラミング［１］](https://qiita.com/kenasman/items/70a3ef914b0e7e55a123)

ソースはこんな感じになります。

```python:aaa.py
import os
from PyQt5.QtCore import QSettings, QTranslator, qVersion, QCoreApplication, pyqtSignal, QObject
from PyQt5.QtGui import QIcon
from qgis.utils import plugins, loadPlugin, startPlugin
from PyQt5.QtWidgets import QAction, QMessageBox

# Initialize Qt resources from file resources.py
from .resources import *

class CustomSignal(QObject):
    mySignal = pyqtSignal(str)

class AAA:
    myObject = CustomSignal()
    """QGIS Plugin Implementation."""
    def __init__(self, iface):
        self.iface = iface
        self.plugin_instance = None

    def initGui(self):
        pass

    def unload(self):
        pass

    def run(self):
        pass

    def loadAndExec(self, plugin_name, function_name, function_param):
        plugin_list = plugins.keys()
        if not plugin_name in plugin_list:
            # ロード実行
            try:
              loadPlugin(plugin_name)
              startPlugin(plugin_name)
              self.plugin_instance = plugins[plugin_name]
              # メソッド実行
              eval("self.plugin_instance." + function_name + "('" + function_param + "')")
            except:
                QMessageBox.critical(self.iface.mainWindow(), 'error', "%s error" % plugin_name)

    def connectTrigger(self, plugin_name):
        self.parent_plugin_instance = plugins[plugin_name]
        self.parent_plugin_instance.myObject.mySignal.connect(self.handle_trigger)
        
    def handle_trigger(self, message):
        QMessageBox.information(self.iface.mainWindow(), 'info', "%s" % message)

    def emitMessage(self, message):
        self.myObject.mySignal.emit(message)
```

では一旦プラグインの状態をリセットしたいので**QGISを再起動**してからPythonコンソールを開き直します。
`aaa_plugin`をロードしてインスタンスを取得するところまでは同じですが、今度は`loadAndExec`を実行して`bbb_plugin`をロードして`connectTrigger`メソッド実行させます。

```
>>> plugin_name = 'aaa_plugin'
>>> qgis.utils.loadPlugin(plugin_name)
True
>>> qgis.utils.startPlugin(plugin_name)
True
>>> plugin = qgis.utils.plugins[plugin_name]
>>> plugin.loadAndExec('bbb_plugin', 'connectTrigger', plugin_name)
```

これでbbb_pluginからaaa_pluginにコネクトできました。
aaa_pluginのシグナルを起こしてみましょう。

```
>>> plugin.emitMessage('test')
```

引数に与えた文字列がメッセージダイアログで表示されました。
**目的達成です。**

# 最後に
良い感じに連携が取れることが確認できました。
QGISを使った小中規模の業務システムならこの仕組みを使うことで柔軟な設計ができそうです。

以上です。

本記事のライセンス
<a rel="license" href="http://creativecommons.org/licenses/by/4.0/">![クリエイティブ・コモンズ・ライセンス](https://i.creativecommons.org/l/by/4.0/88x31.png)</a>
この記事は <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">クリエイティブ・コモンズ 表示 4.0 国際 ライセンス</a>の下に提供されています。