5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FOSS4GAdvent Calendar 2024

Day 4

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

Last updated at Posted at 2024-12-04

はじめに

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

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

予想される検索ワード

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

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

実行環境

  • QGIS 3.34

QgsTaskの使い方

まずは使ってみる

エンジニア的には「とりあえず動くコードがあれば、あとは勝手に上手いことやります」ってタイプの人も多いと思うので、サンプルコードを置いておきます。(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

QgsTaskの構成

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

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

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 : タスク終了時の処理

補足:コンストラクタとは

コンストラクタ(英: constructor)は、オブジェクト指向のプログラミング言語で新たなオブジェクトを生成する際に呼び出されて内容の初期化などを行なう関数あるいはメソッドのことである。

出典:Wikipedia

QgsTaskの処理が実行される仕組み

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

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

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

まずは、QgsTaskのドキュメントを覗いてみましょう。Methodの一覧からrunfinishedはQgsTaskに元々用意されているメソッドであることがわかりますね。
図1.png

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

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

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

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の処理内容が軽すぎる場合、「タスク完了」のメッセージバーは表示されません。
ただし、今回のサンプルコードのように明示的に出力させているものはキチンと出力されるはずです。

    def finished(self, result):
        if result:
            # 明示的にメッセージを出力させる
            iface.messageBar().pushMessage(
                "完了", "MBTilesへの変換が完了しました", level=3
            )

もし、明示的に出力しているメッセージバーすら表示されない場合は、別の要因を探す必要があります。

Q2. QMessageBoxを使ったらQGISが落ちるんですけど...

A2. QgsTask内でQtWidgetsにアクセスするとクラッシュします

これに関しては、PyQGIS 開発者用 Cookbookにも以下のような記載があります。
スクリーンショット 2024-11-13 18.05.59.png

Q3. 明らかに処理が実行されていないんですけど...

A3. QgsTaskをローカル変数として宣言していないか確認してください

Pythonでは、関数の処理が完了した時点で関数内のローカル変数は破棄されます。つまり、以下のようにQgsTaskを関数内のローカル変数として宣言してしまうと、関数が完了した時点で(例えタスクが実行中だろうと)タスクのインスタンスは削除されます。

class Hogehoge:
    def perform_task(self):
        my_task = MyTask(self)  # ローカル変数として宣言している
        QgsApplication.taskManager().addTask(my_task)
        return

そのため、バックグラウンド処理したいQgsTaskは適切な生存時間を持つ変数として宣言しておきましょう。QGISのプラグイン内で利用したいのであれば、UI(QDialogやQDockWidget)のインスタンス変数で宣言しておくのが良いかと思います。

class HogeDialog(QDialog):
    def __init__(self):
        super().__init__()
        
    def hogehoge(self):
        self.my_task = MyTask(self)  # インスタンス変数として宣言している
        QgsApplication.taskManager().addTask(self.my_task)
        return

おわりに

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

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?