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

Last Update: 2023.3.1

Blender 2.8~3.0

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

Blender 2.8~3.0

Last Update: 2023.3.1

2-4. サブメニューを作成する

ここまで紹介したアドオンは、1階層のメニューに対して項目を追加するだけでしたが、サブメニュー(マウスオーバーすると展開されるメニュー)を作成して2階層以上のメニューを作ることもできます。 例えば、[3Dビューポート] スペースのメニュー [追加] > [メッシュ] は、親メニュー [追加] の下に子メニュー [メッシュ] がある2階層のメニューとなっています。 本節では、2-3節 のサンプルアドオンを改良し、並進移動するオブジェクトをメニューから選択できるようにします。 そして、このサンプルアドオンを説明しながら、多階層のメニューを作成する方法を解説します。

作成するアドオンの仕様

アドオンを作成する

1-5節 を参考にして次に示すソースコードを入力し、ファイル名を sample_2-4.py で保存してください。

import bpy
from bpy.props import StringProperty, FloatProperty, EnumProperty


bl_info = {
    "name": "サンプル 2-4: オブジェクトを並進移動するアドオン③",
    "author": "ぬっち(Nutti)",
    "version": (3, 0),
    "blender": (2, 80, 0),
    "location": "3Dビューポート > オブジェクト",
    "description": "オブジェクトを並進移動するサンプルアドオン",
    "warning": "",
    "support": "TESTING",
    "doc_url": "",
    "tracker_url": "",
    "category": "Object"
}


# オブジェクトを並進移動するオペレータ
class SAMPLE24_OT_TranslateObject(bpy.types.Operator):

    bl_idname = "object.sample24_translate_object"
    bl_label = "並進移動"
    bl_description = "オブジェクトを並進移動します"
    bl_options = {'REGISTER', 'UNDO'}

    axis: EnumProperty(
        name="移動軸",
        description="移動軸を設定します",
        default='X',
        items=[
            ('X', "X軸", "X軸に沿って並進移動します"),
            ('Y', "Y軸", "Y軸に沿って並進移動します"),
            ('Z', "Z軸", "Z軸に沿って並進移動します"),
        ]
    )
    amount: FloatProperty(
        name="移動量",
        description="移動量を設定します",
        default=1.0,
    )
    obj_name: StringProperty(options={'HIDDEN'})

    def execute(self, context):
        # 並進移動するオブジェクトを取得
        obj = bpy.data.objects[self.obj_name]
        if self.axis == 'X':
            obj.location[0] += self.amount
        elif self.axis == 'Y':
            obj.location[1] += self.amount
        elif self.axis == 'Z':
            obj.location[2] += self.amount
        self.report({'INFO'}, "サンプル 2-4: 『{}』を{}軸方向へ {} 並進移動しました。"
                              .format(obj.name, self.axis, self.amount))
        print("サンプル 2-4: オペレータ『{}』が実行されました。".format(self.bl_idname))

        return {'FINISHED'}


# メインメニュー
class SAMPLE24_MT_TranslateObject(bpy.types.Menu):

    bl_idname = "SAMPLE24_MT_TranslateObject"
    bl_label = "オブジェクトの並進移動"
    bl_description = "オブジェクトを並進移動します"

    def draw(self, context):
        layout = self.layout
        # サブメニューの登録
        # bpy.data.objects:オブジェクト一覧
        for o in bpy.data.objects:
            if o.type == 'MESH':
                ops = layout.operator(
                    SAMPLE24_OT_TranslateObject.bl_idname, text=o.name
                )
                ops.obj_name = o.name


def menu_fn(self, context):
    self.layout.separator()
    self.layout.menu(SAMPLE24_MT_TranslateObject.bl_idname)


classes = [
    SAMPLE24_OT_TranslateObject,
    SAMPLE24_MT_TranslateObject,
]


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


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


if __name__ == "__main__":
    register()

アドオンを使用する

アドオンを有効化する

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

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

アドオンを有効化したあと、[3Dビューポート] スペースのメニュー [オブジェクト] > [オブジェクトの並進移動] にサブメニューが追加されていることを確認します。 サブメニューには、[3Dビューポート] スペースに存在するメッシュ型のオブジェクト名の項目が追加されています。 なお、Blenderを日本語化している場合、自動翻訳により「Cube」は「立方体」と表示されるため、注意してください。

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

1 [3Dビューポート] スペースのメニュー [オブジェクト] > [オブジェクトの並進移動] から、並進移動するオブジェクトの名前を選んで実行すると、選択したオブジェクトが並進移動します。
2 2-3節 と同様、並進移動時の移動軸・移動量を、オペレータプロパティから変更できます。

アドオンを無効化する

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

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

ソースコードの解説

ソースコードの大部分は 2-3節 からの流用です。 ここでは、新規で追加した部分について解説します。

サブメニューの追加

サブメニューを追加するためには、bpy.types.Menu クラスを継承した メニュークラス を作成する必要があります。

# メインメニュー
class SAMPLE24_MT_TranslateObject(bpy.types.Menu):

    bl_idname = "SAMPLE24_MT_TranslateObject"
    bl_label = "オブジェクトの並進移動"
    bl_description = "オブジェクトを並進移動します"

    def draw(self, context):
        layout = self.layout
        # サブメニューの登録
        # bpy.data.objects:オブジェクト一覧
        for o in bpy.data.objects:
            if o.type == 'MESH':
                ops = layout.operator(
                    SAMPLE24_OT_TranslateObject.bl_idname, text=o.name
                )
                ops.obj_name = o.name

オペレータクラスと同様、メニュークラスにはクラス変数 bl_idname , bl_label , bl_description を定義する必要がありますが、bl_options を定義する必要はありません。 ただし bl_idname は、メニュークラスのクラス名と同一の文字列を指定する必要があります。 メニュークラスのクラス名は、フォーマットXXX_MT_YYY にしたがう必要があります。 XXX は英大文字から始まる英字/数字/アンダースコア(_)から構成される文字列、YYY は英字/数字/アンダースコア(_)から構成される文字列です。

また、メニュークラスでは、メニューの描画に必要な draw メソッドを実装する必要があります。 メニューが表示されるたびに draw メソッドが呼ばれ、次の引数が渡されてきます。

引数 値の説明
self メニュークラス メニュークラスのインスタンス
context bpy.types.Context draw メソッドが呼ばれたときのコンテキスト

サブメニューへの項目追加は self.layout.operator メソッドで行います。 本節のサンプルアドオンでは、[3Dビューポート] スペース上の全てのオブジェクト名をメニュー項目に追加するため、self.layout.operator メソッドの第1引数にオペレータクラスのクラス変数 bl_idname を指定し、引数 text にオブジェクト名を指定しています。

オペレータクラス SAMPLE24_OT_TranslateObject は、並進移動するオブジェクトをオブジェクト名で判定するため、オペレータクラスのクラス変数 obj_name にオブジェクト名を代入します。 obj_nameStringProperty クラスとして定義します。 なお、引数 options に値 {'HIDDEN'} を渡していますが、これはオペレータプロパティとしてユーザに変更を許したくないプロパティに対して設定します。 これにより obj_name がオペレータプロパティとして表示されなくなります。

obj_name: StringProperty(options={'HIDDEN'})

オペレータクラスの execute メソッドでは、クラス変数 obj_name に代入されたオブジェクト名を用いて、並進移動の対象となるオブジェクトを取得して並進移動させています。 なお、詳細を省略した execute メソッドの具体的な処理については、ソースコードのコメントに処理内容を細かく記載しているため、確認してみてください。

最後に、[3Dビューポート] スペースのメニュー [オブジェクト] にメニューを追加します。

def menu_fn(self, context):
    self.layout.separator()
    self.layout.menu(SAMPLE24_MT_TranslateObject.bl_idname)

オペレータをメニューに追加するときは、self.layout.operator メソッドを利用していました。 しかし、サブメニューをメニューに追加する場合は、self.layout.menu メソッドを利用する必要があります。 self.layout.menu メソッドにメニュークラス SAMPLE24_MT_TranslateObject クラスのクラス変数 bl_idname を引数として渡すことで、サブメニューをメニューに追加できます。

3階層以上のメニュー

サブメニューにさらにサブメニュー(サブサブメニュー)を追加するなど、3階層以上のメニューを作成することもできます。

次のコードは、先ほど作成したサンプルアドオンのメニューとサブメニューの間に、新たなメニューとして [オブジェクトの並進移動(サブメニュー)] を追加する処理です。

import bpy
from bpy.props import StringProperty, FloatProperty, EnumProperty


bl_info = {
    "name": "サンプル 2-4: オブジェクトを並進移動するアドオン③",
    "author": "ぬっち(Nutti)",
    "version": (3, 0),
    "blender": (2, 80, 0),
    "location": "3Dビューポート > オブジェクト",
    "description": "オブジェクトを並進移動するサンプルアドオン",
    "warning": "",
    "support": "TESTING",
    "doc_url": "",
    "tracker_url": "",
    "category": "Object"
}


# オブジェクトを並進移動するオペレータ
class SAMPLE24_OT_TranslateObject(bpy.types.Operator):

    bl_idname = "object.sample24_translate_object"
    bl_label = "並進移動"
    bl_description = "オブジェクトを並進移動します"
    bl_options = {'REGISTER', 'UNDO'}

    axis: EnumProperty(
        name="移動軸",
        description="移動軸を設定します",
        default='X',
        items=[
            ('X', "X軸", "X軸に沿って並進移動します"),
            ('Y', "Y軸", "Y軸に沿って並進移動します"),
            ('Z', "Z軸", "Z軸に沿って並進移動します"),
        ]
    )
    amount: FloatProperty(
        name="移動量",
        description="移動量を設定します",
        default=1.0,
    )
    obj_name: StringProperty(options={'HIDDEN'})

    def execute(self, context):
        # 並進移動するオブジェクトを取得
        obj = bpy.data.objects[self.obj_name]
        if self.axis == 'X':
            obj.location[0] += self.amount
        elif self.axis == 'Y':
            obj.location[1] += self.amount
        elif self.axis == 'Z':
            obj.location[2] += self.amount
        self.report({'INFO'}, "サンプル 2-4: 『{}』を{}軸方向へ {} 並進移動しました。"
                              .format(obj.name, self.axis, self.amount))
        print("サンプル 2-4: オペレータ『{}』が実行されました。".format(self.bl_idname))

        return {'FINISHED'}


# サブメニュー
class SAMPLE24_MT_TranslateObjectSub(bpy.types.Menu):

    bl_idname = "SAMPLE24_MT_TranslateObjectSub"
    bl_label = "オブジェクトの並進移動(サブメニュー)"
    bl_description = "オブジェクトを並進移動します(サブメニュー)"

    def draw(self, context):
        layout = self.layout
        # サブサブメニューの登録
        # bpy.data.objects:オブジェクト一覧
        for o in bpy.data.objects:
            if o.type == 'MESH':
                ops = layout.operator(
                    SAMPLE24_OT_TranslateObject.bl_idname, text=o.name
                )
                ops.obj_name = o.name


# メインメニュー
class SAMPLE24_MT_TranslateObject(bpy.types.Menu):

    bl_idname = "SAMPLE24_MT_TranslateObject"
    bl_label = "オブジェクトの並進移動"
    bl_description = "オブジェクトを並進移動します"

    def draw(self, context):
        layout = self.layout
        # サブメニューの登録
        layout.menu(SAMPLE24_MT_TranslateObjectSub.bl_idname)


def menu_fn(self, context):
    self.layout.separator()
    self.layout.menu(SAMPLE24_MT_TranslateObject.bl_idname)


classes = [
    SAMPLE24_OT_TranslateObject,
    SAMPLE24_MT_TranslateObject,
    SAMPLE24_MT_TranslateObjectSub,
]


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


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


if __name__ == "__main__":
    register()

アドオンを作成して有効化すると、図のように3階層のメニューが作成されていることが確認できます。

コードを見るとわかると思いますが、3階層のメニューは2階層のメニューを作成したときの応用です。

# サブメニュー
class SAMPLE24_MT_TranslateObjectSub(bpy.types.Menu):

    bl_idname = "SAMPLE24_MT_TranslateObjectSub"
    bl_label = "オブジェクトの並進移動(サブメニュー)"
    bl_description = "オブジェクトを並進移動します(サブメニュー)"

    def draw(self, context):
        layout = self.layout
        # サブサブメニューの登録
        # bpy.data.objects:オブジェクト一覧
        for o in bpy.data.objects:
            if o.type == 'MESH':
                ops = layout.operator(
                    SAMPLE24_OT_TranslateObject.bl_idname, text=o.name
                )
                ops.obj_name = o.name
# メインメニュー
class SAMPLE24_MT_TranslateObject(bpy.types.Menu):

    bl_idname = "SAMPLE24_MT_TranslateObject"
    bl_label = "オブジェクトの並進移動"
    bl_description = "オブジェクトを並進移動します"

    def draw(self, context):
        layout = self.layout
        # サブメニューの登録
        layout.menu(SAMPLE24_MT_TranslateObjectSub.bl_idname)

メニュークラス SAMPLE24_MT_TranslateObject では、サブサブメニュー用に作成したメニュークラス SAMPLE24_MT_TranslateObjectSub のクラス変数 bl_idname を、self.layout.menu メソッドに渡しています。 そして、SAMPLE24_MT_TranslateObjectSub クラスの中でオペレータを登録することで、3階層のメニューを作成しています。

同様の手順を踏むことで、4階層、5階層、・・・とメニューの階層を増やすことができます。

まとめ

2-3節 で紹介したサンプルアドオンを改造し、並進移動するオブジェクトをサブメニューから選択できるようにしました。

サブメニューを用いることで、本節のサンプルアドオンのように処理対象を選択できるようにしたり、機能ごとにメニュー項目を整理できるようになります。 ぜひここでサブメニューの作り方を習得し、わかりやすいUI作りに活かしましょう。

ポイント