はじめてのBlenderアドオン開発

Last Update: 2023.3.1

Blender 2.8~3.0

はじめてのBlenderアドオン開発

Blender 2.8~3.0

Last Update: 2023.3.1

3-3. タイマのイベントを扱う

3-1節3-2節 では、マウスやキーボードといった、ユーザからの入力イベントを扱う方法を説明しました。 イベントを発生させるほかの方法として、一定時間経過したときにイベントを発生する、タイマを設定する方法もあります。 本節では、タイマから発生するイベントを扱う方法を説明します。

作成するアドオンの仕様

タイマのイベントを扱う方法を理解するため、定期的に発生するイベントを利用した、次の機能を備えるアドオンを作成します。

アドオンを作成する

1-5節 を参考にして次のソースコードを入力し、ファイル名を sample_3-3.py として保存してください。

import datetime

import bpy


bl_info = {
    "name": "サンプル 3-3: 日時を表示するアドオン①",
    "author": "ぬっち(Nutti)",
    "version": (3, 0),
    "blender": (2, 80, 0),
    "location": "3Dビューポート > Sidebar > サンプル 3-3",
    "description": "現在の日時をテキストオブジェクトとして表示するアドオン",
    "warning": "",
    "support": "TESTING",
    "doc_url": "",
    "tracker_url": "",
    "category": "Object"
}


# 日時をテキストオブジェクトとして表示するオペレータ
class SAMPLE33_OT_ShowDatetime(bpy.types.Operator):

    bl_idname = "object.sample33_show_datetime"
    bl_label = "日時を表示"
    bl_description = "日時をテキストオブジェクトとして表示します"

    # タイマのハンドラ
    __timer = None
    # テキストオブジェクトの名前
    __text_object_name = None

    @classmethod
    def is_running(cls):
        # モーダルモード中はTrue
        return True if cls.__timer else False

    def __handle_add(self, context):
        if not self.is_running():
            # タイマを登録
            SAMPLE33_OT_ShowDatetime.__timer = \
                context.window_manager.event_timer_add(
                    0.5, window=context.window
                )
            # モーダルモードへの移行
            context.window_manager.modal_handler_add(self)

    def __handle_remove(self, context):
        if self.is_running():
            # タイマの登録を解除
            context.window_manager.event_timer_remove(
                SAMPLE33_OT_ShowDatetime.__timer)
            SAMPLE33_OT_ShowDatetime.__timer = None

    def modal(self, context, event):
        op_cls = SAMPLE33_OT_ShowDatetime

        # エリアを再描画
        if context.area:
            context.area.tag_redraw()

        # パネル [日時を表示] のボタン [終了] を押したときに、モーダルモードを終了
        if not self.is_running():
            # テキストオブジェクトを削除
            if op_cls.__text_object_name in bpy.data.objects:
                bpy.data.objects.remove(bpy.data.objects[op_cls.__text_object_name])
            op_cls.__text_object_name = None
            return {'FINISHED'}

        if event.type == 'TIMER':
            bpy.data.objects[op_cls.__text_object_name].data.body = datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S")

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        op_cls = SAMPLE33_OT_ShowDatetime

        if context.area.type == 'VIEW_3D':
            # [開始] ボタンが押された時の処理
            if not op_cls.is_running():
                # テキストオブジェクト作成
                bpy.ops.object.text_add(location=(0.0, 0.0, 0.0), radius=2.0)
                op_cls.__text_object_name = context.active_object.name
                bpy.data.objects[op_cls.__text_object_name].data.body = ""
                # モーダルモードを開始
                self.__handle_add(context)
                print("サンプル 3-3: 日時の表示処理を開始しました。")
                return {'RUNNING_MODAL'}
            # [終了] ボタンが押された時の処理
            else:
                # モーダルモードを終了
                self.__handle_remove(context)
                print("サンプル 3-3: 日時の表示処理を終了しました。")
                return {'FINISHED'}
        else:
            return {'CANCELLED'}


# UI
class SAMPLE33_PT_ShowDatetime(bpy.types.Panel):

    bl_label = "日時を表示"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "サンプル 3-3"
    bl_context = "objectmode"

    def draw(self, context):
        op_cls = SAMPLE33_OT_ShowDatetime

        layout = self.layout
        # [開始] / [停止] ボタンを追加
        if not op_cls.is_running():
            layout.operator(op_cls.bl_idname, text="開始", icon="PLAY")
        else:
            layout.operator(op_cls.bl_idname, text="終了", icon="PAUSE")


classes = [
    SAMPLE33_OT_ShowDatetime,
    SAMPLE33_PT_ShowDatetime,
]


def register():
    for c in classes:
        bpy.utils.register_class(c)
    print("サンプル 3-3: アドオン『サンプル 3-3』が有効化されました。")


def unregister():
    for c in classes:
        bpy.utils.unregister_class(c)
    print("サンプル 3-3: アドオン『サンプル 3-3』が無効化されました。")


if __name__ == "__main__":
    register()

アドオンを使用する

アドオンを有効化する

1-5節 を参考にして作成したアドオンを有効化すると、コンソールウィンドウに次の文字列が出力されます。

サンプル 3-3: アドオン『サンプル 3-3』が有効化されました。

Sidebarを表示し、タブ [サンプル 3-3] にパネル [日時を表示] が追加されていることを確認します。

アドオンの機能を使用する

有効化したアドオンの機能を使い、動作を確認します。

1 [3Dビューポート] スペースのSidebarの [サンプル 3-3] > [日時を表示] に配置されている [開始] ボタンを押します。
2 テキストオブジェクトが作成され、日時がテキストとして表示されるようになります。
3 [終了] ボタンを押すと、テキストオブジェクトが削除されます。

アドオンを無効化する

1-5節 を参考にして有効化したアドオンを無効化すると、コンソールウィンドウに次の文字列が出力されます。

サンプル 3-3: アドオン『サンプル 3-3』が無効化されました。

ソースコードの解説

本節では、タイマイベントを扱う処理と日時を表示する処理に限定し、サンプルアドオンのソースコードを解説します。 これまでに説明してきた内容については、説明を省いています。 本節のサンプルアドオンのソースコードに関して、ポイントとなる点を次に示します。

本節では、日時を表示するモードをモーダルモードと記載している部分があります。 以降、モーダルモードと書かれていたら、日時を表示するモードとして読みかえても問題ありません。

タイマの登録

タイマイベントを発生させるためには、タイマを登録する必要があります。 タイマの登録処理は、次に示す __handle_add メソッドで行います。

def __handle_add(self, context):
    if not self.is_running():
        # タイマを登録
        SAMPLE33_OT_ShowDatetime.__timer = \
            context.window_manager.event_timer_add(
                0.5, window=context.window
            )
        # モーダルモードへの移行
        context.window_manager.modal_handler_add(self)

タイマは、context.window_manager.event_timer_add メソッドを呼び出すことで登録できます。 context.window_manager.event_timer_add メソッドは次に示す引数を受け取り、戻り値としてタイマのハンドラを返します。

引数 値の意味
第1引数 float タイマイベントを発生させる間隔を秒単位で指定
第2引数 bpy.types.Window タイマの登録先ウィンドウ

本節のサンプルアドオンでは、第1引数に 0.5 を指定することで、タイマによるイベントを0.5秒ごとに発生させます。 [開始] ボタンが存在するウィンドウでタイマイベントを発生させたいため、第2引数には context.window を指定します。

戻り値として返されたハンドラは、タイマを登録解除するときに使用するため、クラス変数 __timer に保存します。

__handle_add メソッドは、最後にモーダルモードへ移行しますが、必ずしも __handle_add メソッド内で行う必要はありません。 __handle_add メソッド自体が、invoke メソッドから呼び出されていることになるため、3-1節3-2節 と同様に、invoke メソッドの処理内で context.window_manager.modal_handler_add メソッドを呼び出して、モーダルモードへ移行しても問題ありません。

タイマの登録を解除

タイマを登録すると、登録解除するまでタイマイベントが発生します。 このため、タイマが不要になったら登録を解除する必要があります。

タイマの登録解除処理は、次に示す __handle_remove メソッドで行っています。

def __handle_remove(self, context):
    if self.is_running():
        # タイマの登録を解除
        context.window_manager.event_timer_remove(
            SAMPLE33_OT_ShowDatetime.__timer)
        SAMPLE33_OT_ShowDatetime.__timer = None

context.window_manager.event_timer_remove メソッドを呼び出すことで、タイマを登録解除できますが、引数には、 context.window_manager.event_timer_add メソッドの戻り値として返されたタイマのハンドラを渡す必要があります。 本節のサンプルアドオンでは、タイマのハンドラを保存したクラス変数 __timer を引数に渡し、タイマを登録解除します。 なお、登録解除済のタイマのハンドラにアクセスすることによる不正な動作を避けるために、クラス変数 __timerNone を代入します。

modalメソッド

タイマイベントが発生すると、modal メソッドが呼ばれます。

3-1節3-2節 と同様に、modal メソッドの最初で [3Dビューポート] スペースを持つエリアの画面更新と、modal メソッドの終了判定処理を行います。

3-1節3-2節 で説明したように、modal メソッドはキーボードやマウスのイベントが発生したときにも呼ばれます。 このため、タイマイベントが発生したとき(event.type'TIMER' のとき)のみ、日時を更新するようにします。

if event.type == 'TIMER':
    bpy.data.objects[op_cls.__text_object_name].data.body = datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S")

invokeメソッド

[開始] ボタンが押されたとき、日時を表示するためのテキストオブジェクトを作成する必要があります。 テキストオブジェクトは、bpy.ops.object.text_add 関数を使って作成できます。 作成したテキストオブジェクトは、モーダルモード終了時に削除できるように、テキストオブジェクトの名前をクラス変数 __text_object_name に保存しておきます。 また、テキストオブジェクトのテキストには、空文字列を設定します。

# テキストオブジェクト作成
bpy.ops.object.text_add(location=(0.0, 0.0, 0.0), radius=2.0)
op_cls.__text_object_name = context.active_object.name
bpy.data.objects[op_cls.__text_object_name].data.body = ""

まとめ

本節では、タイマのイベントを扱う方法を説明しました。 タイマを使うと、指定した間隔でイベントを発生させることができるため、定期的に処理を実行するような機能を実現できます。

3-1節 から本節まで、3節にわたってイベントを扱う処理を説明しましたが、イベントを扱う場合は、modal メソッドや invoke メソッドを実装する必要があることを、理解できたのではないでしょうか。

ポイント