はじめてのBlenderアドオン開発 (Blender 2.7版)

Last Update: 2019.4.2

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

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

作成するアドオンの仕様

アドオンを作成する

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

import bpy
from bpy.props import StringProperty, FloatVectorProperty, EnumProperty
from mathutils import Vector


bl_info = {
    "name": "サンプル2-5: オブジェクトを複製するアドオン",
    "author": "Nutti",
    "version": (2, 0),
    "blender": (2, 75, 0),
    "location": "3Dビュー > オブジェクト",
    "description": "オブジェクトを複製するアドオン",
    "warning": "",
    "support": "TESTING",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Object"
}


# EnumPropertyで表示したい項目リストを作成する関数
def location_list_fn(scene, context):
    items = [
        ('3D_CURSOR', "3Dカーソル", "3Dカーソル上に配置します"),
        ('ORIGIN', "原点", "原点に配置します")]
    items.extend([
        ('OBJ_' + o.name, o.name, "オブジェクトに配置します")
        for o in bpy.data.objects
    ])

    return items


# 選択したオブジェクトを複製するアドオン
class ReplicateObject(bpy.types.Operator):

    bl_idname = "object.replicate_object"
    bl_label = "オブジェクトの複製"
    bl_description = "オブジェクトを複製します"
    bl_options = {'REGISTER', 'UNDO'}

    location = EnumProperty(
        name="配置位置",
        description="複製したオブジェクトの配置位置",
        items=location_list_fn
    )
    scale = FloatVectorProperty(
        name="拡大率",
        description="複製したオブジェクトの拡大率を設定します",
        default=(1.0, 1.0, 1.0),
        subtype='XYZ',
        unit='LENGTH'
    )
    rotation = FloatVectorProperty(
        name="回転角度",
        description="複製したオブジェクトの回転角度を設定します",
        default=(0.0, 0.0, 0.0),
        subtype='AXISANGLE',
        unit='ROTATION'
    )
    offset = FloatVectorProperty(
        name="オフセット",
        description="複製したオブジェクトの配置位置からのオフセットを設定します",
        default=(0.0, 0.0, 0.0),
        subtype='TRANSLATION',
        unit='LENGTH'
    )
    src_obj_name = StringProperty()

    def execute(self, context):
        # bpy.ops.object.duplicate()は選択中のオブジェクトをコピーするため、
        # メニューで選択されたオブジェクトを選択された状態にする
        # context.scene.objects:オブジェクト一覧
        # context.scene.objects.active:現在アクティブなオブジェクト
        for o in context.scene.objects:
            if self.src_obj_name == o.name:
                context.scene.objects.active = o
                o.select = True
                break
            else:
                o.select = False
        # オブジェクトの複製
        bpy.ops.object.duplicate()
        active_obj = context.active_object

        # 複製したオブジェクトを配置位置に移動
        if self.location == '3D_CURSOR':
            active_obj.location = context.scene.cursor_location.copy()
        elif self.location == 'ORIGIN':
            active_obj.location = Vector((0.0, 0.0, 0.0))
        elif self.location[0:4] == 'OBJ_':
            objs = bpy.data.objects
            active_obj.location = objs[self.location[4:]].location.copy()

        # 複製したオブジェクトの拡大率を設定
        active_obj.scale.x = active_obj.scale.x * self.scale[0]
        active_obj.scale.y = active_obj.scale.y * self.scale[1]
        active_obj.scale.z = active_obj.scale.z * self.scale[2]

        # 複製したオブジェクトの回転角度を設定
        rot_euler = active_obj.rotation_euler
        active_obj.rotation_euler.x = rot_euler.x + self.rotation[0]
        active_obj.rotation_euler.y = rot_euler.y + self.rotation[1]
        active_obj.rotation_euler.z = rot_euler.z + self.rotation[2]

        # 複製したオブジェクトの最終位置を設定
        active_obj.location = active_obj.location + Vector(self.offset)

        self.report({'INFO'}, "サンプル2-5: 「%s」を複製しました。" % (self.src_obj_name))
        print("サンプル2-5: オペレーション「%s」が実行されました。" % (self.bl_idname))

        return {'FINISHED'}


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

    bl_idname = "object.replicate_object_menu"
    bl_label = "オブジェクトの複製"
    bl_description = "オブジェクトを複製します"

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


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


def register():
    bpy.utils.register_module(__name__)
    bpy.types.VIEW3D_MT_object.append(menu_fn)
    print("サンプル2-5: アドオン「サンプル2-5」が有効化されました。")


def unregister():
    bpy.types.VIEW3D_MT_object.remove(menu_fn)
    bpy.utils.unregister_module(__name__)
    print("サンプル2-5: アドオン「サンプル2-5」が無効化されました。")


if __name__ == "__main__":
    register()

アドオンを使用する

アドオンを有効化する

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

サンプル2-5: アドオン「サンプル2-5」が有効化されました。

アドオンを有効化後、[3Dビュー] エリアのメニューである [オブジェクト] > [オブジェクトの複製] にサブメニューが追加されていることを確認します。 サブメニューには、[3Dビュー] エリアに存在するオブジェクト名が追加されています。

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

1 [3Dビュー] エリアのメニューである [オブジェクト] > [オブジェクトの複製] から複製するオブジェクト名を選んで実行すると、選択したオブジェクトが複製されます。
2 2-4節 と同様、複製されたオブジェクトの拡大率・回転角度・配置先を [ツール・シェルフ] の [オプション] から変更することができます。

アドオンを無効化する

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

サンプル2-5: アドオン「サンプル2-5」が無効化されました。

ソースコードの解説

サブメニューを作成するコードを追加したことを除き、ソースコードの大部分は 2-4節 からの流用です。 ここでは、新規で追加した部分について解説します。

サブメニューの追加

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

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

    bl_idname = "object.replicate_object_menu"
    bl_label = "オブジェクトの複製"
    bl_description = "オブジェクトを複製します"

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

オペレータクラスと同様、メニュークラスにはクラス変数 bl_idname , bl_label , bl_description を定義する必要がありますが、bl_options を指定する必要はありません。

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

引数 値の説明
self 呼ばれた draw メソッドが定義されているメニュークラス メニュークラスのインスタンス
context bpy.types.Context draw メソッドが呼ばれた時のコンテキスト

オペレータクラスをメニューに登録した時と同様、サブメニューへの項目追加は self.layout.operator 関数で行うことができます。 本節のサンプルでは、[3Dビュー] エリア上の全てのオブジェクト名をメニュー項目に追加するため、layout.operator 関数の第1引数にオペレータクラスの bl_idname を指定し、引数 text にオブジェクト名を指定しています。

オペレータクラスは、複製するオブジェクトをオブジェクト名で判定するため、オペレータクラスのクラス変数 src_obj_name にオブジェクト名を代入します。 src_obj_nameStringProperty クラスの変数で定義します。

    src_obj_name = StringProperty()

オペレータクラスの execute メソッドでは、クラス変数 src_obj_name に代入されたオブジェクト名を用いてオブジェクトを複製するように処理を変更しています。 本書については説明しませんが、ソースコードのコメントに処理内容を細かく記載しているため確認してください。

最後に、[3Dビュー] エリアのメニューである [オブジェクト] へ項目を追加します。

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

これまでオペレータクラスをメニューに追加する時は self.layout.operator 関数を利用していましたが、メニュークラスをメニューに追加する場合は self.layout.menu 関数を利用します。 self.layout.menu 関数にメニュークラスのクラス変数 bl_idname を引数として渡すことで、メニューをメニューの項目に追加することができます。

3階層以上のメニュー

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

以下のサンプルでは、先ほど作成したサンプルのメニューとサブメニューの間に新たなメニューとして、[オブジェクトの複製(サブメニュー)] を追加しています。

import bpy
from bpy.props import StringProperty, FloatVectorProperty, EnumProperty
from mathutils import Vector


bl_info = {
    "name": "サンプル2-5: オブジェクトを複製するアドオン",
    "author": "Nutti",
    "version": (2, 0),
    "blender": (2, 75, 0),
    "location": "3Dビュー > オブジェクト",
    "description": "オブジェクトを複製するアドオン",
    "warning": "",
    "support": "TESTING",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Object"
}


# EnumPropertyで表示したい項目リストを作成する関数
def location_list_fn(scene, context):
    items = [
        ('3D_CURSOR', "3Dカーソル", "3Dカーソル上に配置します"),
        ('ORIGIN', "原点", "原点に配置します")]
    items.extend([
        ('OBJ_' + o.name, o.name, "オブジェクトに配置します")
        for o in bpy.data.objects
    ])

    return items


# 選択したオブジェクトを複製するアドオン
class ReplicateObject(bpy.types.Operator):

    bl_idname = "object.replicate_object"
    bl_label = "オブジェクトの複製"
    bl_description = "オブジェクトを複製します"
    bl_options = {'REGISTER', 'UNDO'}

    location = EnumProperty(
        name="配置位置",
        description="複製したオブジェクトの配置位置",
        items=location_list_fn
    )
    scale = FloatVectorProperty(
        name="拡大率",
        description="複製したオブジェクトの拡大率を設定します",
        default=(1.0, 1.0, 1.0),
        subtype='XYZ',
        unit='LENGTH'
    )
    rotation = FloatVectorProperty(
        name="回転角度",
        description="複製したオブジェクトの回転角度を設定します",
        default=(0.0, 0.0, 0.0),
        subtype='AXISANGLE',
        unit='ROTATION'
    )
    offset = FloatVectorProperty(
        name="オフセット",
        description="複製したオブジェクトの配置位置からのオフセットを設定します",
        default=(0.0, 0.0, 0.0),
        subtype='TRANSLATION',
        unit='LENGTH'
    )
    src_obj_name = StringProperty()

    def execute(self, context):
        # bpy.ops.object.duplicate()は選択中のオブジェクトをコピーするため、
        # メニューで選択されたオブジェクトを選択された状態にする
        # context.scene.objects:オブジェクト一覧
        # context.scene.objects.active:現在アクティブなオブジェクト
        for o in context.scene.objects:
            if self.src_obj_name == o.name:
                context.scene.objects.active = o
                o.select = True
                break
            else:
                o.select = False
        # オブジェクトの複製
        bpy.ops.object.duplicate()
        active_obj = context.active_object

        # 複製したオブジェクトを配置位置に移動
        if self.location == '3D_CURSOR':
            active_obj.location = context.scene.cursor_location.copy()
        elif self.location == 'ORIGIN':
            active_obj.location = Vector((0.0, 0.0, 0.0))
        elif self.location[0:4] == 'OBJ_':
            objs = bpy.data.objects
            active_obj.location = objs[self.location[4:]].location.copy()

        # 複製したオブジェクトの拡大率を設定
        active_obj.scale.x = active_obj.scale.x * self.scale[0]
        active_obj.scale.y = active_obj.scale.y * self.scale[1]
        active_obj.scale.z = active_obj.scale.z * self.scale[2]

        # 複製したオブジェクトの回転角度を設定
        rot_euler = active_obj.rotation_euler
        active_obj.rotation_euler.x = rot_euler.x + self.rotation[0]
        active_obj.rotation_euler.y = rot_euler.y + self.rotation[1]
        active_obj.rotation_euler.z = rot_euler.z + self.rotation[2]

        # 複製したオブジェクトの最終位置を設定
        active_obj.location = active_obj.location + Vector(self.offset)

        self.report({'INFO'}, "サンプル2-5: 「%s」を複製しました。" % (self.src_obj_name))
        print("サンプル2-5: オペレーション「%s」が実行されました。" % (self.bl_idname))

        return {'FINISHED'}


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

    bl_idname = "object.replicate_object_sub_menu"
    bl_label = "オブジェクトの複製(サブメニュー)"
    bl_description = "オブジェクトを複製します(サブメニュー)"

    def draw(self, context):
        layout = self.layout
        # サブサブメニューの登録
        for o in bpy.data.objects:
            layout.operator(
                ReplicateObject.bl_idname, text=o.name
            ).src_obj_name = o.name


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

    bl_idname = "object.replicate_object_menu"
    bl_label = "オブジェクトの複製"
    bl_description = "オブジェクトを複製します"

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


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


def register():
    bpy.utils.register_module(__name__)
    bpy.types.VIEW3D_MT_object.append(menu_fn)
    print("サンプル2-5: アドオン「サンプル2-5」が有効化されました。")


def unregister():
    bpy.types.VIEW3D_MT_object.remove(menu_fn)
    bpy.utils.unregister_module(__name__)
    print("サンプル2-5: アドオン「サンプル2-5」が無効化されました。")


if __name__ == "__main__":
    register()

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

サンプルのソースコードを見るとわかると思いますが、3階層のメニューは2階層のメニューを作成した時の応用であることがわかります。

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

    bl_idname = "object.replicate_object_sub_menu"
    bl_label = "オブジェクトの複製(サブメニュー)"
    bl_description = "オブジェクトを複製します(サブメニュー)"

    def draw(self, context):
        layout = self.layout
        # サブサブメニューの登録
        for o in bpy.data.objects:
            layout.operator(
                ReplicateObject.bl_idname, text=o.name
            ).src_obj_name = o.name
# メインメニュー
class ReplicateObjectMenu(bpy.types.Menu):

    bl_idname = "object.replicate_object_menu"
    bl_label = "オブジェクトの複製"
    bl_description = "オブジェクトを複製します"

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

サブメニュー登録時に self.layout.operator 関数の代わりに self.layout.menu 関数を用い、サブメニュー用に作成したメニュークラスのクラス変数 bl_idname を指定します。 そしてサブメニュー用に作成したクラスの中で、オペレータクラスを登録することで、3階層のメニューを作成することができます。

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

まとめ

2-4節 で紹介したサンプルを改造し、複製するオブジェクトをメニューから選択できるようにしました。 また、サブメニューから複製するオブジェクトを選べるようにしました。

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

ポイント