はじめてのBlenderアドオン開発
Last Update: 2023.3.1
Blender 2.7
はじめてのBlenderアドオン開発
Blender 2.7
Last Update: 2023.3.1
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_name
は StringProperty
クラスの変数で定義します。
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階層以上のメニューを作成することもできます。
以下のサンプルでは、先ほど作成したサンプルのメニューとサブメニューの間に新たなメニューとして、[オブジェクトの複製(サブメニュー)] を追加しています。
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作りに活かしましょう。
bpy.types.Menu
クラスを継承して作成するdraw
メソッド内でオペレータクラスのクラス変数 bl_idname
を self.layout.operation
関数の引数に指定し、メニュークラスのクラス変数 bl_idname
を引数にして self.layout.menu
関数を呼び出すことで、サブメニューを作成できるdraw
メソッド内でサブメニュー用に作成したクラスのクラス変数 bl_idname
を self.layout.menu
関数の引数に指定することで、3階層以上のメニューを作成することができる