pyqgisでQGISの標準機能を呼び出す方法

:::note info
これは [MIERUNE Advent Calendar 2024](https://qiita.com/advent-calendar/2024/mierune) の8日目の記事です。
昨日は @northprint さんによる [DuckDB-Wasmで簡単な地理空間情報分析アプリを作る](https://qiita.com/northprint/items/0bb2113814a8878fdfef) でした。
:::
# はじめに
QGISの機能のうち、プロセシングツールに含まれているものは、比較的簡単にpyqgisのコードを確認することができますよね。一方で、標準機能（ウィンドウ上部のメニューから起動するやつ）については、Python APIが用意されていないものもあります。（私が見つけられなかっただけ、という可能性もありますが）

本記事では、そんなQGISの標準機能をpyqgisで~~無理やり~~呼び出す方法について解説します。

# 予想される検索ワード
以下、この記事を求めているであろう方が入力しそうな検索ワードです。

QGIS, pyqgis, 標準機能

# 実行環境
- QGIS 3.34

# 実行方法
## 一回やってみる
兎にも角にも、一回動かしてみて、標準機能が本当に呼び出せるのかやってみましょう。

QGISのジオリファレンサ機能（下図）を呼び出すためのサンプルコードをご用意しました。
![スクリーンショット 2024-12-04 13.23.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1147458/b7fca4e4-9f95-57c3-6a4d-54bc62b76c43.png)
```python
actions = iface.mainWindow().menuBar().actions()

for act in actions:
    if act.menu().objectName() == "mLayerMenu":
        for a in act.menu().actions():
            if a.objectName() == "mActionShowGeoreferencer":
                a.trigger()
                break
        break
```
サンプルコードをPythonコンソールで実行すると、ジオリファレンサが立ち上がることを確認できますね。
![スクリーンショット 2024-12-04 13.39.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1147458/9c159206-01b9-817f-c86e-69a88bb5e7e6.png)
<font color="gray">（ここが上手くいかない場合、QGISのverが3.34になっているか確認してみてください）</font>

## 任意の機能を呼び出す方法
さて、~~ジオリファレンサ機能だけ呼び出せても仕方ないので~~ここからは任意の機能を呼び出す方法について解説していきます。

ご存知の方も多いと思いますが、QGISのGUIはQtフレームワークを用いて構築されています。そのため、上段のメニュー（メニューバー）についても、Qtフレームワークの仕組みを利用して操作（項目の追加、削除、実行など）することが可能です。

可能であれば、各機能に対して具体的なコードをサクッと示したいところですが、メニューバーには多くの機能が登録されているため、ここでそれぞれの呼び出し方を示すのは現実的ではありません。

そのため、ここからはハンズオン形式で手を動かしながら、任意の機能を呼び出す方法について解説していきたいと思います。

### 1. pyqgisでメニューの内容を取得してみる
まずは、以下のコードを実行してみましょう。
```python
# メニューバーの全アクションを取得
actions = iface.mainWindow().menuBar().actions()

for act in actions:
    # 各メニューのタイトルを表示
    print(act.menu().title())
```
```:実行結果
プロジェクト(&J)
編集(&E)
ビュー(&V)
レイヤ(&L)
設定(&S)
プラグイン(&P)
ベクタ(&O)
ラスタ(&R)
Web(&W)
メッシュ(&M)
プロセシング(&C)
ヘルプ(&H)
```
実行結果を見てみると、メニューバーの項目が取得できていることがわかりますね。
![スクリーンショット 2024-12-04 14.24.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1147458/a97de243-51d0-9f99-9bc7-c1a928fb8fa5.png)

### 2. メソッドの正体を理解する
先ほどのコードで、当然のように使っている`act.menu().title()`って何？ と疑問に思った方もいらっしゃるかもしれません。その謎に迫るために、以下を実行してみましょう。
```python
# メニューバーの全アクションを取得
actions = iface.mainWindow().menuBar().actions()

for act in actions:
    # actのデータ型を表示
    print(type(act))
    
    # 各メニューのタイトルを表示
    print(act.menu().title())

    break
```
```:実行結果
<class 'PyQt5.QtWidgets.QAction'>
プロジェクト(&J)
```
actの正体は、'PyQt5.QtWidgets.QAction'だということがわかりました。ということは、`menu()`はQActionのメソッドであると推測できますね。

ということで、[QActionのドキュメント](https://doc.qt.io/qtforpython-5/PySide2/QtWidgets/QAction.html#)を覗いてみましょう。
![スクリーンショット 2024-12-04 18.03.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1147458/88a2bc30-d064-b07e-8145-7c6ded6b4ec0.png)

確かにQActionには、`menu()`というFunctionが用意されていることが分かりますね。（上図の下から2行目に載っています）

つまり、サンプルコード上で実行していたメソッドは、**Qt側で用意しているメソッドだった**ということが分かります。

### 3. 他の機能を呼び出してみる
上記で確認した通り、QGISのメニューバーはQtを利用して構築されているため、Qt側で用意している操作はpyqgisから実行することができます。

ということで、任意の機能を（Qtを利用して無理やり）起動させてみましょう。

今度は例として、オプション（設定→オプション）を起動させます。
![スクリーンショット 2024-12-04 18.26.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1147458/163e0630-05a0-543c-b4ce-35e42ee69ada.png)

まずは「設定」に含まれるアクション一覧を取得してみます。
```python
# メニューバーの全アクションを取得
actions = iface.mainWindow().menuBar().actions()

for act in actions:
    if act.menu().title() == "設定(&S)":  # 設定タブかどうか判定
        for a in act.menu().actions():  # 設定タブの全アクションでループ処理
            print(a.text())  # 各アクションのテキストを表示
```
```:実行結果
ユーザープロファイル(&U)
スタイルマネージャ...
カスタム投影法...

キーボードショートカット...
インタフェースのカスタマイズ...

オプション(&O)...
```
ちゃんと設定タブの各アクションを取得できていますね。あとは、アクションの名前からオプションを判別して、無理やり叩けば、起動できそうです。

ということで叩いてみます。
```python
# メニューバーの全アクションを取得
actions = iface.mainWindow().menuBar().actions()

for act in actions:    
    if act.menu().title() == "設定(&S)":
        for a in act.menu().actions():
            
            if a.text() == "オプション(&O)...":  # オプションかどうか判定
                a.trigger()  # アクションを走らせる
                break
```
無事に立ち上がりましたね！
![スクリーンショット 2024-12-04 18.55.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1147458/402c3735-ee27-b32b-57c7-94fe8a8b12b4.png)

こんな感じで、各メニューの表記名を基にif文で判定しつつ、該当するメニューをtrigger()で叩いてやれば、~~無理やり~~処理を走らせることができます。

## ちょっと応用的な話
（自身の環境で叩ければOKという方は、本節を読む必要はありません）

さて、お気づきになった方もいるかもしれませんね。

```python:最初に示したサンプルコード
actions = iface.mainWindow().menuBar().actions()

for act in actions:
    if act.menu().objectName() == "mLayerMenu":
        for a in act.menu().actions():
            if a.objectName() == "mActionShowGeoreferencer":
                a.trigger()
                break
        break
```
```python:ハンズオンで試したコード
# メニューバーの全アクションを取得
actions = iface.mainWindow().menuBar().actions()

for act in actions:    
    if act.menu().title() == "設定(&S)":
        for a in act.menu().actions():
            
            if a.text() == "オプション(&O)...":  # オプションかどうか判定
                a.trigger()  # アクションを走らせる
                break
```

「おい、サンプルコードは、`title()`とか`text()`を使ってないじゃん！ なんでサンプルコードと違うメソッド使ってんのさ！」

はい、その通りです。ハンズオンでは、実際にQGISのメニューが取得できていることを体感してもらうために、ワザと`title()`や`text()`で判定させています。`title()`や`text()`は、GUI上で表示されているテキストが取得できるので、実際にどのメニューを処理したいのか、分かりやすいですよね。

ですが、QGISを使うのは日本人だけだったでしょうか？違いますよね。

`title()`や`text()`の内容はQGISの言語設定によって、揺れがあります。そのため、言語設定によらずに確実に起動させるためには、別の方法で判定させる必要があります。

サンプルコードで示した`objectName()`であれば、オブジェクト自体の名前（言語設定によらず不変）を取得することができるので、そういった多言語での利用想定（プラグインとして公開する等）に適した手法であると言えます。

# おわりに
「プラグイン上の処理で標準機能を呼び出したい」というシチュエーションに陥ることは極めて稀だと思いますが、必要になった際は、今回のやり方を参考にして頂ければ幸いです。

おまけで、Qtの機能でできることは何でもできることを示すために、以下のコードを用意してみましたので、是非実行してみてくださいね！（素敵なメニューが追加されますよ！）
```python
from qgis.PyQt.QtWidgets import QAction, QMenu
from qgis.utils import iface
import webbrowser

# "QGIS LAB"メニューを作成
qgis_lab_menu = QMenu("QGIS LAB", iface.mainWindow().menuBar())
iface.mainWindow().menuBar().addMenu(qgis_lab_menu)

# "QGIS LABにアクセス"アクションを作成
qgis_lab_action = QAction("QGIS LABにアクセス", iface.mainWindow())
help_action = QAction("ヘルプ", iface.mainWindow())

# アクションの動作を定義
def open_qgis_lab():
    webbrowser.open("https://qgis.mierune.co.jp/")
def contact():
    webbrowser.open("https://www.mierune.co.jp/contactus")

qgis_lab_action.triggered.connect(open_qgis_lab)
help_action.triggered.connect(contact)

# アクションをQGIS LABメニューに追加
qgis_lab_menu.addAction(qgis_lab_action)
qgis_lab_menu.addAction(help_action)

print("QGIS LAB menu added successfully!")
```
:::note info
明日は@groovyjovyさんによる記事です！お楽しみに！
:::