QgsTaskの使い方とよくある落とし穴

# はじめに
この記事は、「QgsTask使いたいけど、なんか上手く動かないなぁ...」と悩んでいる方向けに、とりあえず動くサンプルを提供しつつ、使い方やよくある落とし穴について説明する記事です。

<font color="gray">余談：</font>
<font color="gray">厳密に言うと「QgsTaskクラスそのもの」ではなく「QgsTaskを継承して、非同期処理させたい内容を追記したクラス」を指しているのですが、そんなこと誰も気にしないと思うので、本記事ではQgsTaskと呼びます。</font>

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

QGIS, QgsTask, 使い方, 動かない, 落ちる

# 実行環境
- QGIS 3.34

# QgsTaskの使い方
## まずは使ってみる
エンジニア的には「とりあえず動くコードがあれば、あとは勝手に上手いことやります」ってタイプの人も多いと思うので、サンプルコードを置いておきます。（Pythonコンソールで実行できます）

```python
import processing
from PyQt5.QtGui import QColor
from qgis.core import QgsApplication, QgsTask
from qgis.utils import iface


class TileTask(QgsTask):
    """表示している地図のMBTilesを生成するタスク"""

    def __init__(self, description):
        super().__init__(description, QgsTask.CanCancel)
        self.error = None

    def run(self):
        """メインの処理"""
        try:
            # MBTilesの生成
            processing.run(
                "native:tilesxyzmbtiles",
                {
                    "EXTENT": "136.48,139.91,34.55,35.68 [EPSG:6668]",
                    "ZOOM_MIN": 12,
                    "ZOOM_MAX": 13,
                    "DPI": 96,
                    "BACKGROUND_COLOR": QColor(0, 0, 0, 0),
                    "ANTIALIAS": True,
                    "TILE_FORMAT": 0,
                    "QUALITY": 75,
                    "METATILESIZE": 4,
                    "OUTPUT_FILE": "TEMPORARY_OUTPUT",
                },
            )
            return True
        except Exception as e:
            self.error = str(e)
            return False

    def finished(self, result):
        """処理完了後に実行されるメソッド"""
        if result:
            iface.messageBar().pushMessage(
                "完了", "MBTilesへの変換が完了しました", level=3
            )
        else:
            iface.messageBar().pushMessage(
                "エラー", f"タスクが失敗しました: {self.error}", level=2
            )


# タスクの実行
task = TileTask("Generate MBTiles")
QgsApplication.taskManager().addTask(task)
```
なお、タスクが完了すると下図のように、メッセージバーが表示されます。
![スクリーンショット 2024-11-09 16.32.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1147458/76b4cbad-da6c-2cd0-dd4a-b7c09d94ea21.png)

## QgsTaskの構成
QgsTask初見の場合、`run`や`finished`に困惑する方もいらっしゃると思うので、QgsTask自体の構成について説明します。

QgsTaskは以下のような構成になっています。

```python
class MyTask(QgsTask):
    def __init__(self, description):
        """タスクの初期化"""
        super().__init__(description, QgsTask.CanCancel)

    def run(self):
        """メインの処理"""
        try:
            # 非同期で実行する処理
            return True
        except Exception as e:
            # エラー処理
            print(f"エラー発生: {e}")
            return False

    def finished(self, result):
        """処理完了後に実行されるメソッド"""
        if result:
            # 処理が成功した場合の処理
            print("タスクが正常に完了しました")
        else:
            # 処理が失敗した場合の処理
            print("タスクが失敗しました")
```
上記コード内にコメントとして記入していますが、それぞれのメソッドの役割は以下の通りです。
- `__init__` ： タスクを初期化するコンストラクタ
- `run` : 非同期に走らせるメインの処理
- `finished` : タスク終了時の処理

:::note info
補足：コンストラクタとは
>コンストラクタ（英: constructor）は、オブジェクト指向のプログラミング言語で新たなオブジェクトを生成する際に呼び出されて内容の初期化などを行なう関数あるいはメソッドのことである。
> 
> 出典：[Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF)
:::

## QgsTaskの処理が実行される仕組み
前述のメソッドの役割を把握していれば、あとは処理させたい内容を各メソッドに記述すればいいだけなので、QgsTaskを動かすことはできると思います。（なので、「動かせればOK！」という方は、本節はスルーしてOKです）

一方で「`run`とか`finished`は誰も呼んでいないのに実行されるのが気持ち悪い」と感じる方もいらっしゃるかもしれませんね。（というか私がそうでした

ここでは`run`や`finished`が実行される仕組みをある程度納得できるレベルまで説明します。（本気で理解するにはC++の知識が必要になるので、そこまでは説明しません、というかできません）

まずは、[QgsTaskのドキュメント](https://qgis.org/pyqgis/master/core/QgsTask.html)を覗いてみましょう。Methodの一覧から`run`や`finished`はQgsTaskに元々用意されているメソッドであることがわかりますね。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1147458/eac6fdbf-17a3-7abc-0b90-7bd87e4160cc.png)

`finished`の欄にも記載されていますが、QgsTaskManagerはタスクの実行時に`run`、タスクの終了時に`finished`を実行するようにプログラムされています。つまりこちらで`run`や`finished`を指定する必要はありません（裏を返せば、`run2`とかを定義しても実行してくれません）

なので、任意の処理をQgsTaskManagerに実行させたい場合は、QgsTaskが持つ`run`や`finished`の内容をオーバーライド（上書き）してあげる必要があります。

コード+コメントで示すと以下のようになります。

```python
class MyTask(QgsTask):  # QgsTaskを継承
    def __init__(self, description):
        """タスクの初期化"""
        super().__init__(description, QgsTask.CanCancel)

    def run(self):  # run()をオーバーライド
        """メインの処理"""
        try:
            return True
        except Exception as e:
            print(f"エラー発生: {e}")
            return False

    def finished(self, result):  # finished()をオーバーライド
        """処理完了後に実行されるメソッド"""
        if result:
            print("タスクが正常に完了しました")
        else:
            print("タスクが失敗しました")
```

# よくある落とし穴
基本的な使い方を踏まえた上で、よくある（というか私がハマった）落とし穴について紹介します。ご利用の際にはお気をつけください。
## Q1. 「タスク完了」って表示されないんですけど...
### A1. 処理内容が軽すぎる可能性があります
QgsTaskの処理内容が軽すぎる場合、「タスク完了」のメッセージバーは表示されません。
ただし、今回のサンプルコードのように明示的に出力させているものはキチンと出力されるはずです。
```python
    def finished(self, result):
        if result:
            # 明示的にメッセージを出力させる
            iface.messageBar().pushMessage(
                "完了", "MBTilesへの変換が完了しました", level=3
            )
```
もし、明示的に出力しているメッセージバーすら表示されない場合は、別の要因を探す必要があります。

## Q2. QMessageBoxを使ったらQGISが落ちるんですけど...
### A2. QgsTask内でQtWidgetsにアクセスするとクラッシュします
これに関しては、[PyQGIS 開発者用 Cookbook](https://docs.qgis.org/3.34/ja/docs/pyqgis_developer_cookbook/tasks.html)にも以下のような記載があります。
![スクリーンショット 2024-11-13 18.05.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1147458/baf67785-4a3b-7513-d7a8-73a68a3c4b2d.png)

## Q3. 明らかに処理が実行されていないんですけど...
### A3. QgsTaskをローカル変数として宣言していないか確認してください
Pythonでは、関数の処理が完了した時点で関数内のローカル変数は破棄されます。つまり、以下のようにQgsTaskを関数内のローカル変数として宣言してしまうと、関数が完了した時点で（例えタスクが実行中だろうと）タスクのインスタンスは削除されます。
```python
class Hogehoge:
    def perform_task(self):
        my_task = MyTask(self)  # ローカル変数として宣言している
        QgsApplication.taskManager().addTask(my_task)
        return
```
そのため、バックグラウンド処理したいQgsTaskは適切な生存時間を持つ変数として宣言しておきましょう。QGISのプラグイン内で利用したいのであれば、UI（QDialogやQDockWidget）のインスタンス変数で宣言しておくのが良いかと思います。
```python
class HogeDialog(QDialog):
    def __init__(self):
        super().__init__()
        
    def hogehoge(self):
        self.my_task = MyTask(self)  # インスタンス変数として宣言している
        QgsApplication.taskManager().addTask(self.my_task)
        return
```

# おわりに
QgsTaskを触っている中で、結構落とし穴が多いなーと感じたので、いつかの自分のために記事として書いてみました。今回記載した内容以外にも、思わぬ落とし穴はあると思うので、「こんなことでハマった」という話があればコメント頂けると嬉しいです。