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

Last Update: 2023.3.1

Blender 2.8~3.0

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

Blender 2.8~3.0

Last Update: 2023.3.1

2-7. BlenderのUIを制御する

BlenderのUIを自由に変えたいと思ったことはありませんか?

BlenderのUIを構成する処理の多くはPythonで書かれているため、Pythonを理解できていればある程度自由にBlenderのUIを変更できます。 本節では、PythonからBlenderのUIを制御する方法を説明します。 Blenderを自分の好みのUIへ改造したい、という場合にも本節は参考になると思います。

BlenderのUI

BlenderのUIを変更するためには、多く労力が必要だと思っている人が多いのではないでしょうか。 しかし、BlenderのUIの多くがPythonで書かれていることから、本書をここまで読んできた人であれば、UIを制御するための基本的な知識が身についているはずです。 BlenderのUIが、Pythonで書かれていることを確認するために、[3Dビューポート] スペースのメニューを変更してみましょう。

[3Dビューポート] スペースのメニューを変更するため、次の手順でソースコードを書き換えます。

1 [3Dビューポート] スペースのSidebarに配置されているタブ [ビュー] より、[3Dカーソル] パネルの [位置] の下にあるテキストボックスを [右クリック] します。
2 表示されたポップアップメニューから、[ソース編集] をクリックします。
3 [テキストエディター] スペースにソースコードが表示されます。また、[3Dカーソル] パネルの [位置] の下にあるテキストボックスを表示するためのソースコードに、カーソルが自動的に移動しています。
4 カーソルが示している行をコメントアウトします。
5 [テキストエディター] スペースのメニュー [テキスト] > [保存] を実行し、上書き保存します。
6 1-4節 で紹介した『Reload Scripts』機能を用いてアップデートすると、[3Dカーソル] パネルから、[位置] の下にあるテキストボックスがラベルを含めて消えます。

変更したUIを元に戻すためには、先ほどコメントアウトした行のコメントを外して保存し、『Reload Scripts』機能でアップデートすることで、元に戻すことができます。

ここまでの説明で、BlenderのUIがPythonで制御されていることを理解できたのではないでしょうか。 以降は、BlenderでUIを構築する方法をサンプルアドオンを用いながら説明します。

作成するアドオンの仕様

アドオンを作成する

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

import bpy
from bpy.props import (
    IntProperty,
    FloatProperty,
    FloatVectorProperty,
    EnumProperty,
    BoolProperty,
)


bl_info = {
    "name": "サンプル 2-7: BlenderのUIを制御するアドオン",
    "author": "ぬっち(Nutti)",
    "version": (3, 0),
    "blender": (2, 80, 0),
    "location": "3Dビューポート > Sidebar",
    "description": "BlenderのUIを制御するアドオン",
    "warning": "",
    "support": "TESTING",
    "doc_url": "",
    "tracker_url": "",
    "category": "User Interface"
}


class SAMPLE27_OT_Nop(bpy.types.Operator):

    bl_idname = "object.sample27_nop"
    bl_label = "NOP"
    bl_description = "何もしない"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        return {'FINISHED'}


class SAMPLE27_MT_NopMenu(bpy.types.Menu):

    bl_idname = "SAMPLE27_MT_NopMenu"
    bl_label = "NOP メニュー"
    bl_description = "何もしないオペレータを複数持つメニュー"

    def draw(self, context):
        layout = self.layout
        # メニュー項目の追加
        for i in range(3):
            layout.operator(SAMPLE27_OT_Nop.bl_idname, text=("項目 %d" % (i)))


# Sidebarのタブ [カスタムタブ] に、パネル [カスタムパネル] を追加
class SAMPLE27_PT_CustomPanel(bpy.types.Panel):

    bl_label = "カスタムパネル"         # パネルのヘッダに表示される文字列
    bl_space_type = 'VIEW_3D'           # パネルを登録するスペース
    bl_region_type = 'UI'               # パネルを登録するリージョン
    bl_category = "カスタムタブ"        # パネルを登録するタブ名
    bl_context = "objectmode"           # パネルを表示するコンテキスト

    # 本クラスの処理が実行可能かを判定する
    @classmethod
    def poll(cls, context):
        # オブジェクトが選択されているときのみメニューを表示させる
        for o in bpy.data.objects:
            if o.select_get():
                return True
        return False

    # ヘッダーのカスタマイズ
    def draw_header(self, context):
        layout = self.layout
        layout.label(text="", icon='PLUGIN')

    # メニューの描画処理
    def draw(self, context):
        layout = self.layout
        scene = context.scene

        # ボタンを追加
        layout.label(text="ボタン:")
        layout.operator(SAMPLE27_OT_Nop.bl_idname, text="ボタン1")
        layout.operator(SAMPLE27_OT_Nop.bl_idname, text="ボタン2", emboss=False)

        # セパレータを追加
        layout.separator()

        # ドロップダウンメニューを追加
        layout.label(text="ドロップダウンメニュー:")
        layout.menu(SAMPLE27_MT_NopMenu.bl_idname,
                    text="ドロップダウンメニュー")

        layout.separator()

        # テキストボックスを追加
        layout.label(text="テキストボックス:")
        layout.prop(scene, "sample27_prop_int", text="プロパティ 1")
        layout.prop(scene, "sample27_prop_float", text="プロパティ 2")
        layout.prop(scene, "sample27_prop_floatv", text="プロパティ 3")

        # ドロップダウンプロパティを追加
        layout.label(text="ドロップダウンプロパティ:")
        layout.prop(scene, "sample27_prop_enum", text="プロパティ 4")

        # チェックボックスを追加
        layout.label(text="チェックボックス:")
        layout.prop(scene, "sample27_prop_bool", text="プロパティ 5")

        layout.separator()

        # 一行配置(アライメントなし)
        layout.label(text="一行配置(アライメントなし):")
        row = layout.row(align=False)
        for i in range(3):
            row.operator(SAMPLE27_OT_Nop.bl_idname, text=("列 %d" % (i)))

        layout.separator()

        # 一行配置(アライメントあり)
        layout.label(text="一行配置(アライメントあり):")
        row = layout.row(align=True)
        for i in range(3):
            row.operator(SAMPLE27_OT_Nop.bl_idname, text=("列 %d" % (i)))

        layout.separator()

        # 一列配置(アライメントなし)
        layout.label(text="一列配置(アライメントなし):")
        column = layout.column(align=False)
        for i in range(3):
            column.operator(SAMPLE27_OT_Nop.bl_idname, text=("行 %d" % (i)))

        layout.separator()

        # 一列配置(アライメントあり)
        layout.label(text="一列配置(アライメントあり):")
        column = layout.column(align=True)
        for i in range(3):
            column.operator(SAMPLE27_OT_Nop.bl_idname, text=("行 %d" % (i)))

        layout.separator()

        # 複数列配置
        layout.label(text="複数列配置:")
        column = layout.column(align=True)
        row = column.row(align=True)
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1, 行 1")
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2, 行 1")
        row = column.row(align=True)
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1, 行 2")
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2, 行 2")

        layout.separator()

        # 領域分割
        layout.label(text="領域分割:")
        split = layout.split(factor=0.3)
        column = split.column(align=True)
        column.label(text="領域1:")
        column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1")
        column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2")
        split = split.split(factor=0.7)
        column = split.column()
        column.label(text="領域2:")
        column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1")
        column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2")
        split = split.split(factor=1.0)
        column = split.column(align=False)
        column.label(text="領域3:")
        column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1")
        column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2")

        layout.separator()

        # 横幅の自動拡大
        layout.label(text="横幅の自動拡大:")
        row = layout.row()
        row.alignment = 'EXPAND'
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1")
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2")

        layout.separator()

        # 左寄せ
        layout.label(text="左寄せ:")
        row = layout.row()
        row.alignment = 'LEFT'
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1")
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2")

        layout.separator()

        # 右寄せ
        layout.label(text="右寄せ:")
        row = layout.row()
        row.alignment = 'RIGHT'
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1")
        row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2")

        layout.separator()

        # グループ化
        layout.label(text="グループ化:")
        row = layout.row()
        box = row.box()
        box_row = box.row()
        box_column = box_row.column()
        box_column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1, 列 1")
        box_column.separator()
        box_column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2, 列 1")
        box_row.separator()
        box_column = box_row.column()
        box_column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1, 列 2")
        box_column.separator()
        box_column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2, 列 2")


# プロパティの初期化
def init_props():
    scene = bpy.types.Scene
    scene.sample27_prop_int = IntProperty(
        name="プロパティ 1",
        description="プロパティ(int)",
        default=100,
        min=0,
        max=255
    )
    scene.sample27_prop_float = FloatProperty(
        name="プロパティ 2",
        description="プロパティ(float)",
        default=0.75,
        min=0.0,
        max=1.0
    )
    scene.sample27_prop_floatv = FloatVectorProperty(
        name="プロパティ 3",
        description="プロパティ(float vector)",
        subtype='COLOR_GAMMA',
        default=(1.0, 1.0, 1.0),
        min=0.0,
        max=1.0
    )
    scene.sample27_prop_enum = EnumProperty(
        name="プロパティ 4",
        description="プロパティ(enum)",
        items=[
            ('ITEM_1', "項目 1", "項目 1"),
            ('ITEM_2', "項目 2", "項目 2"),
            ('ITEM_3', "項目 3", "項目 3")
        ],
        default='ITEM_1'
    )
    scene.sample27_prop_bool = BoolProperty(
        name="プロパティ 5",
        description="プロパティ(bool)",
        default=False
    )


# プロパティを削除
def clear_props():
    scene = bpy.types.Scene
    del scene.sample27_prop_int
    del scene.sample27_prop_float
    del scene.sample27_prop_floatv
    del scene.sample27_prop_enum
    del scene.sample27_prop_bool


classes = [
    SAMPLE27_OT_Nop,
    SAMPLE27_MT_NopMenu,
    SAMPLE27_PT_CustomPanel,
]


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


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


if __name__ == "__main__":
    register()

アドオンを使用する

アドオンを有効化する

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

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

さらに、[3Dビューポート] スペースのSidebarに、タブ [カスタムタブ] が追加されます。 タブは、[オブジェクトモード] 時、かつオブジェクトが1つ以上選択されているときのみ、表示されることに注意が必要です。

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

[3Dビューポート] スペースのSidebarのタブ [カスタムタブ] をクリックし、パネル [カスタムパネル] を表示すると、アドオンの仕様で説明したパネルが表示されます。

アドオンを無効化する

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

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

ソースコードの解説

プロパティを定義する

アドオンの機能を実行するときに、ユーザが指定するプロパティを定義します。 プロパティの定義は、プロパティクラスの変数を定義することで行います。

プロパティを追加する

サンプルアドオンでは、アドオン有効化時に register 関数から呼び出される init_props 関数で、プロパティを作成しています。 プロパティクラスの変数を bpy.types.Scene に追加することで、プロパティを追加できます。

# プロパティの初期化
def init_props():
    scene = bpy.types.Scene
    scene.sample27_prop_int = IntProperty(
        name="プロパティ 1",
        description="プロパティ(int)",
        default=100,
        min=0,
        max=255
    )
    scene.sample27_prop_float = FloatProperty(
        name="プロパティ 2",
        description="プロパティ(float)",
        default=0.75,
        min=0.0,
        max=1.0
    )
    scene.sample27_prop_floatv = FloatVectorProperty(
        name="プロパティ 3",
        description="プロパティ(float vector)",
        subtype='COLOR_GAMMA',
        default=(1.0, 1.0, 1.0),
        min=0.0,
        max=1.0
    )
    scene.sample27_prop_enum = EnumProperty(
        name="プロパティ 4",
        description="プロパティ(enum)",
        items=[
            ('ITEM_1', "項目 1", "項目 1"),
            ('ITEM_2', "項目 2", "項目 2"),
            ('ITEM_3', "項目 3", "項目 3")
        ],
        default='ITEM_1'
    )
    scene.sample27_prop_bool = BoolProperty(
        name="プロパティ 5",
        description="プロパティ(bool)",
        default=False
    )

プロパティを削除する

アドオンを無効化するときは、bpy.types.Scene に追加したプロパティを削除する必要があります。 プロパティを削除せずにアドオンを無効化すると、プロパティクラスのインスタンスがメモリに残ったままとなり、メモリを消費した状態のままとなってしまいます。

サンプルアドオンでは、unregister 関数から呼び出される clear_props 関数で、プロパティを削除しています。

# プロパティを削除
def clear_props():
    scene = bpy.types.Scene
    del scene.sample27_prop_int
    del scene.sample27_prop_float
    del scene.sample27_prop_floatv
    del scene.sample27_prop_enum
    del scene.sample27_prop_bool

Sidebarにパネルを追加する

本節のサンプルアドオンでは、Sidebarにタブを追加し、そのタブにパネルを追加しています。 サンプルアドオンのように、アドオンの機能をパネルとしてまとめることで、アドオンがどのような機能を持っているかが一目でわかるようになります。 また、普段よく使う機能をパネルにまとめ、独自のUIを構築してBlenderの利便性を高めるのもよいかもしれません。

Sidebarにパネルを追加するためには、bpy.types.Panel クラスを継承した パネルクラス を定義する必要があります。

# Sidebarのタブ [カスタムタブ] に、パネル [カスタムパネル] を追加
class SAMPLE27_PT_CustomPanel(bpy.types.Panel):

    bl_label = "カスタムパネル"         # パネルのヘッダに表示される文字列
    bl_space_type = 'VIEW_3D'           # パネルを登録するスペース
    bl_region_type = 'UI'               # パネルを登録するリージョン
    bl_category = "カスタムタブ"        # パネルを登録するタブ名
    bl_context = "objectmode"           # パネルを表示するコンテキスト

本節のサンプルアドオンでは、パネルクラスとして SAMPLE27_PT_CustomPanel クラスを作成しています。 パネルクラスのクラス名は、フォーマット XXX_PT_YYY に従う必要があります。 XXX は英大文字から始まる英字/数字/アンダースコア(_)から構成される文字列、YYY は英字/数字/アンダースコア(_)から構成される文字列です。

パネルクラス SAMPLE27_PT_CustomPanel には、4つのクラス変数が定義されています。

クラス変数 値の意味
bl_category str パネルを登録するタブ名
bl_space_type str パネルを登録するスペース
bl_region_type str パネルを登録するリージョン
bl_label str パネルのヘッダに表示される文字列

クラス bl_label には、パネルのヘッダに表示される文字列を指定します。 クラス変数 bl_space_type には、パネルを登録するスペースを指定します。 本節のサンプルアドオンでは、[3Dビューポート] スペースにパネルを登録することを考えて、'VIEW_3D' を指定しています。

クラス変数 bl_space_type には、Blenderのスペースに対応した、次のような値を指定できます。

設定値 スペース
'VIEW_3D' [3Dビューポート] スペース
'IMAGE_EDITOR' [画像エディター] スペース、[UVエディター] スペース
'NODE_EDITOR' [シェーダーエディター] スペース、[コンポジター] スペース、[テクスチャノードエディター] スペース
'SEQUENCE_EDITOR' [ビデオシーケンサー] スペース
'CLIP_EDITOR' [動画クリップエディター] スペース
'DOPESHEET_EDITOR' [ドープシート] スペース、[タイムライン] スペース
'GRAPH_EDITOR' [グラフエディター] スペース、[ドライバー] スペース
'NLA_EDITOR' [ノンリニアアニメーション] スペース
'TEXT_EDITOR' [テキストエディター] スペース
'CONSOLE' [Pythonコンソール] スペース
'INFO' [情報] スペース
'OUTLINER' [アウトライナー] スペース
'PROPERTIES' [プロパティ] スペース
'FILE_BROWSER' [ファイルブラウザー] スペース
'PREFERENCES' [プリファレンス] スペース

クラス変数 bl_region_type には、パネルを登録するリージョンを指定します。 本節のサンプルアドオンでは、Sidebarに登録するため、UI を指定しています。

クラス変数 bl_region_type には、ほかにも次のような値を設定可能です。

設定値 リージョン
'UI' Sidebar。[N] キーを押したときに、エリアの右側に表示される領域
'TOOLS' Toolbar。[T] キーを押したときに、エリアの左側に表示される領域
'TOOL_PROPS' オペレータプロパティ。オペレータを実行したときに、エリアの左側下部に表示される領域
'WINDOW' エリア中央の領域で、常に表示される領域
'HEADER' エリア上部または下部に表示されるバーで、常に表示される領域

SAMPLE27_PT_CustomPanel クラスには、poll メソッド、draw_header メソッド、draw メソッドが定義されています。 各メソッドで行う処理を、次に示します。 定義したメソッドの詳細については、のちほど説明します。

メソッド 処理
poll 本メソッドが定義されたクラスの処理について、実行可能か否かを判定するメソッド。
True を返すと実行可能と判断し、False を返すと実行不可能であると判断する。
実行不可能と判断されると、本メソッドが定義されたクラスで定義した、いかなる処理も実行されない
draw_header パネルのヘッダ部のUIを描画するメソッド
draw パネルのUIを描画するメソッド。本メソッドでは、ヘッダ部のUIを描画しない

パネルが表示される条件を設定する

本節のサンプルアドオンでは、[オブジェクトモード] 時、かつ少なくとも1つ以上のオブジェクトが選択されているときのみ、タブが表示される仕様です。 このように、特定の状況下でのみメニューやタブを表示したり、処理を実行できるようにしたりするためには、poll メソッドとクラス変数 bl_context を活用します。

クラス変数 bl_context は、パネルクラスのクラス変数として定義可能な変数で、指定したコンテキストのときのみ、パネルの描画処理を実行します。 bl_context には、次のような値を設定できます。

説明
"objectmode" [オブジェクトモード] 時のみ描画する
"mesh_edit" [編集モード] 時のみ描画する

本節のサンプルアドオンでは、[オブジェクトモード] 時のみパネルを描画するため、bl_context = "objectmode" としています。 また、poll メソッドでは、オブジェクトが選択されているときのみ、パネルクラスの処理が実行可能になるように定義しています。

# 本クラスの処理が実行可能かを判定する
@classmethod
def poll(cls, context):
    # オブジェクトが選択されているときのみメニューを表示させる
    for o in bpy.data.objects:
        if o.select_get():
            return True
    return False

poll メソッドはクラス単位で定義する処理となるため、クラスメソッドとして定義する必要があります。 このため、デコレータ @classmethod をつけてメソッドを定義する必要があります。

poll メソッドに渡されてくる引数は、次の通りです。

引数
cls パネルクラス
context bpy_types.Context

poll メソッド内では、bpy.data.objects から1つずつオブジェクトを参照し、選択されているオブジェクトが存在する場合は True を返しています。 1つもオブジェクトが選択されていない場合は、False を返します。 この処理によって、選択されているオブジェクトが1つ以上存在する場合に、タブが表示されるようになります。

パネルのヘッダUIを変更する

パネルのヘッダのUIを変更するためには、パネルクラスの draw_header メソッドを定義します。 draw メソッドでは、ヘッダのUIを変更できない点に注意が必要です。

# ヘッダーのカスタマイズ
def draw_header(self, context):
    layout = self.layout
    layout.label(text="", icon='PLUGIN')

draw_header メソッドの引数は、次に示す通りです。

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

draw_header メソッドでは、メニューのヘッダに表示される文字列の左に、アイコンを表示する処理を定義しています。

layout.label の引数を以下に示します。

引数 値の意味
text str ラベルに表示する文字列
icon str ラベルの隣に配置するアイコン

本節のサンプルアドオンでは、文字列を表示せずにアイコンのみを表示するため、引数 text に空の文字列、引数 icon にプラグインのアイコンID 'PLUGIN' を指定しています。

UIパーツ

Blenderは、ボタンやリストボックスなどのさまざまなUIパーツを提供しています。 これらのUIパーツは、アドオンからも利用できます。

ラベル

ラベル は、ユーザが編集できないテキストボックスで、主にテキストを表示するために使用します。 layout.label メソッドを利用することで、ラベルを配置できます。 引数については、draw_header メソッドで説明済みのため、そちらを参照してください。

ボタン

ボタンlayout.operator メソッドで追加でき、次の引数を指定します。

引数 値の意味
第1引数 str オペレータクラスの bl_idname
text str ボタンに表示する文字列(指定しない場合は、第1引数に指定した bl_idname に対応するオペレータクラスに定義されている、bl_label の値が表示される
icon str ボタンに表示するアイコン
emboss bool False の場合、文字列の周りの装飾が消える

ボタンを押すと、layout.operator メソッドの第1引数に指定したオペレータクラスの bl_idname を持つ、オペレータクラスの処理が実行されます。 本節のサンプルアドオンでは、次の処理により2種類のボタン(標準のボタンと文字列の周りの装飾が消えたボタン)を追加しています。

# ボタンを追加
layout.label(text="ボタン:")
layout.operator(SAMPLE27_OT_Nop.bl_idname, text="ボタン1")
layout.operator(SAMPLE27_OT_Nop.bl_idname, text="ボタン2", emboss=False)

セパレータ

layout.separator メソッドを呼ぶことで、上下のスペースを空けることができます(セパレータ と呼びます)。 メニューに対して layout.separator メソッドを実行したときの動作については、2-1節 を参考にしてください。

# セパレータを追加
layout.separator()

ドロップダウンメニュー

layout.menu メソッドを利用することで、ドロップダウンメニュー を追加できます。

引数 値の意味
第1引数 str メニュークラスの bl_idname
text str ボタンに表示する文字列(指定しない場合は、第1引数に指定した bl_idname に対応するメニュークラスに定義されている、bl_label の値が表示される

メニュー名は、第1引数に指定したメニュークラスのクラス変数 bl_label がデフォルトになりますが、text 引数に文字列を指定することで変更できます。

# ドロップダウンメニューを追加
layout.label(text="ドロップダウンメニュー:")
layout.menu(SAMPLE27_MT_NopMenu.bl_idname,
            text="ドロップダウンメニュー")

テキストボックス

ユーザが値を変更可能な テキストボックス を配置するためには、layout.prop メソッドを使います。 layout.prop メソッドの引数を次に示します。

引数 意味
第1引数 bpy.types.Scene プロパティクラスの変数を持つオブジェクト
第2引数 str プロパティクラスの変数名
text str 表示する文字列(指定しない場合は、プロパティクラスの変数を定義したときに引数に指定した、name の値が表示される)

サンプルアドオンでは、bpy.types.Scene にプロパティクラスの変数を登録したため、context.scene を第1引数に指定します。 第2引数には、bpy.types.Scene に登録したプロパティクラスの変数名を文字列で指定します。

# テキストボックスを追加
layout.label(text="テキストボックス:")
layout.prop(scene, "sample27_prop_int", text="プロパティ 1")
layout.prop(scene, "sample27_prop_float", text="プロパティ 2")
layout.prop(scene, "sample27_prop_floatv", text="プロパティ 3")

ドロップダウンプロパティ

ドロップダウンプロパティ は、登録された項目の中からユーザが値を設定できるUIパーツです。 self.layout.prop メソッドの第2引数に、bpy.props.EnumProperty クラスの変数を指定することで、ドロップダウンプロパティを作成できます。

# ドロップダウンプロパティを追加
layout.label(text="ドロップダウンプロパティ:")
layout.prop(scene, "sample27_prop_enum", text="プロパティ 4")

チェックボックス

チェックボックス は、ユーザがON/OFFを切りかえることができるUIパーツです。 self.layout.prop メソッドの第2引数に、bpy.props.BoolProperty の変数を指定することで、チェックボックスを作成できます。

# チェックボックスを追加
layout.label(text="チェックボックス:")
layout.prop(scene, "sample27_prop_bool", text="プロパティ 5")

整列

一行配置(アライメントなし)

layout.operator メソッドを用いると、横幅が100%のボタンが配置されます。 このため、単純に layout.operator メソッドを複数回実行すると、実行した回数分だけ縦方向にボタンが配置されてしまいます。

ボタンを横に並べるためには、layout.row メソッドを使って行成分を取得し、取得した行成分に対して operator メソッドを使ってボタンを配置する必要があります。 本節のサンプルアドオンでは、次のようにして3つのボタンを一行に並べています。

# 一行配置(アライメントなし)
layout.label(text="一行配置(アライメントなし):")
row = layout.row(align=False)
for i in range(3):
    row.operator(SAMPLE27_OT_Nop.bl_idname, text=("列 %d" % (i)))

なお、operator メソッドの代わりに label メソッド、prop メソッドや menu メソッドを使うことによって、ラベルやボタンなどを一行に並べて配置できます。

一行配置(アライメントあり)

layout.row メソッドの引数に align=True を指定すると、ボタンとボタンの間に隙間がなくなるように配置されます。

# 一行配置(アライメントあり)
layout.label(text="一行配置(アライメントあり):")
row = layout.row(align=True)
for i in range(3):
    row.operator(SAMPLE27_OT_Nop.bl_idname, text=("列 %d" % (i)))

一列配置(アライメントなし)

layout.operator メソッドを複数回実行することで、ボタンを一列に配置できますが、隙間が広くて気に入らない人もいると思います。 隙間を縮めた状態でボタンを縦に並べるためには、layout.column メソッドを使って列成分を取得し、取得した列成分に対して operator メソッドを使ってボタンを配置します。

本節のサンプルアドオンでは、次のようにして3つのボタンを一列に並べています。

# 一列配置(アライメントなし)
layout.label(text="一列配置(アライメントなし):")
column = layout.column(align=False)
for i in range(3):
    column.operator(SAMPLE27_OT_Nop.bl_idname, text=("行 %d" % (i)))

なお、operator メソッドの代わりに label メソッド、prop メソッドや menu メソッドを使うことによって、ラベルやボタンなどを一列に並べて配置できます。

一列配置(アライメントあり)

layout.column メソッドの引数に align=True を指定すると、ボタンとボタンの間に隙間がなくなるように配置されます。

# 一列配置(アライメントあり)
layout.label(text="一列配置(アライメントあり):")
column = layout.column(align=True)
for i in range(3):
    column.operator(SAMPLE27_OT_Nop.bl_idname, text=("行 %d" % (i)))

複数行、複数列配置

layout.column メソッドや layout.row メソッドで取得した行成分や列成分に対して、さらに行成分や列成分を取得することで、より複雑なボタンの配置を実現できます。

本節のサンプルアドオンでは、次のようにして2行2列にボタンを配置しています。

# 複数列配置
layout.label(text="複数列配置:")
column = layout.column(align=True)
row = column.row(align=True)
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1, 行 1")
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2, 行 1")
row = column.row(align=True)
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1, 行 2")
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2, 行 2")

領域分割

layout.row メソッドを用いて行成分を取得することで、一行にボタンを配置できますが、ボタンはすべて等幅になっていました。 ボタンの横幅を変えたいときは、layout.split メソッドを用いて領域を分割します。

layout.split メソッドの引数 factor に値を指定することで、領域の横幅を決めることができます。 引数 factor の値はfloat型で指定し、1.0 で横幅100%、0.0 で横幅0%となります。 例えば、Sidebarの横幅に対して70%の横幅を持つ領域に分割する場合は、layout.split(factor=0.7) とします。

本節のサンプルアドオンでは、次のような処理で領域を3分割しています。

# 領域分割
layout.label(text="領域分割:")
split = layout.split(factor=0.3)
column = split.column(align=True)
column.label(text="領域1:")
column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1")
column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2")
split = split.split(factor=0.7)
column = split.column()
column.label(text="領域2:")
column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1")
column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2")
split = split.split(factor=1.0)
column = split.column(align=False)
column.label(text="領域3:")
column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1")
column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2")

分割後の各領域では、縦並びにボタンを2つ表示しています。

layout.split メソッドによって分割した領域の変数 split に対して、split.split メソッドを実行することで、2つ以上の領域に分割できます。 なお、split.split メソッドに指定する引数 factor については、注意が必要です。 最初の領域分割のために layout.split メソッドを実行するときは、引数に指定した factor がSidebarの横幅に対する割合を示しますが、2回目の領域分割で split.split メソッドを実行したときは、layout.split メソッドで分割した残りの領域、つまりサンプルアドオンではSidebarの横幅70%の領域に対する割合を指定します。 同様に3回目の領域分割では、2回目に分割した残りの領域に対する割合を指定します。 したがって、Sidebarに対する横幅はそれぞれ、領域1で30%、領域2で70%×0.7=49%、領域3で70%×0.3=21%となります。

明示的な横幅最大化

layout.operator メソッドを使ってボタンを配置すると、ボタンの横幅が自動的に領域全体へ拡大されますが、ボタンの横幅を明示的に領域全体に最大化する方法もあります。

サンプルアドオンでは、次のように row.alignmentEXPAND を設定し、明示的にボタンの横幅を最大化しています。

# 横幅の自動拡大
layout.label(text="横幅の自動拡大:")
row = layout.row()
row.alignment = 'EXPAND'
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1")
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2")

左寄せ

ボタンの横幅を最大化せず、右や左に寄せて配置することもできます。

サンプルアドオンでは、次のように row.alignmentLEFT を指定し、ボタンを左寄せ配置しています。

# 左寄せ
layout.label(text="左寄せ:")
row = layout.row()
row.alignment = 'LEFT'
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1")
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2")

右寄せ

row.alignmentRIGHT を指定することで、右寄せ配置も可能です。

# 右寄せ
layout.label(text="右寄せ:")
row = layout.row()
row.alignment = 'RIGHT'
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 1")
row.operator(SAMPLE27_OT_Nop.bl_idname, text="列 2")

グループ化

複数のUIパーツをグループ化することもできます。 layout.box メソッドの戻り値に対して、operator メソッドや menu メソッドなどを呼び出すことで、作成したUIがグループ化されます。

サンプルアドオンでは、次のソースコードにより、4つのボタンをグループ化しています。 グループ内のUIは、通常のUIと同様の方法でUIを構築できます。

# グループ化
layout.label(text="グループ化:")
row = layout.row()
box = row.box()
box_row = box.row()
box_column = box_row.column()
box_column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1, 列 1")
box_column.separator()
box_column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2, 列 1")
box_row.separator()
box_column = box_row.column()
box_column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 1, 列 2")
box_column.separator()
box_column.operator(SAMPLE27_OT_Nop.bl_idname, text="行 2, 列 2")

まとめ

本節では、BlenderのUIをPythonから制御できることを確認しました。 また、[3Dビューポート] スペースのSidebarへパネルを追加する方法について説明し、UIパーツの配置方法や整列方法も説明しました。

BlenderのUIを変更することは難しそうに思えますが、Pythonさえ理解できていれば、なんとかなりそうだと思えてきたのではないでしょうか。 最後に、本節で紹介したUIパーツの配置や整理に使用するAPIをまとめます。

UI API
ラベル layout.label
ボタン layout.operator
セパレータ layout.separator
ドロップダウンメニュー layout.menu
テキストボックス layout.prop
ドロップダウンプロパティ layout.propbpy.props.EnumProperty
チェックボックス layout.propbpy.props.BoolProperty
行成分取得(アライメントなし) layout.row
layout.row(align=False)
行成分取得(アライメントあり) layout.row(align=True)
列成分取得(アライメントなし) layout.column
layout.column(align=False)
列成分取得(アライメントあり) layout.column(align=True)
領域分割 layout.split
明示的な横幅最大化 row.alignment = 'EXPAND'
左寄せ row.alignment = 'LEFT'
右寄せ row.alignment = 'RIGHT'
グループ化 layout.box

ポイント