【Blender 2.8 アドオン開発】006 UI - Panel編 -
前回、Propertyクラスを使うと UI(プロパティポップアップ)から機能(Operator)への入力が出来ることを説明しました。
ただ、プロパティポップアップから入力を行う場合、値が更新されるたびに機能の処理(execute)が実行されます。
もちろん逐次変化を確認できるので便利ではありますが、重い処理など値を入力したら一度だけ処理を実行したい場合もあります。
また、機能の実行を毎回Pythonコンソールや、サーチメニューから実行するのは不便です。
そこで今回は、サイドバーへ UI を追加し、そこからオペレーターを実行することを目標とし、Panel クラスの基本を説明していきたいとおもいます。
サンプルスクリプト1
import bpy # # TUTORIAL_PT_SamplePanel # class TUTORIAL_PT_SamplePanel(bpy.types.Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "Tutorial" bl_label = "PanelTitle" #--- draw ---# def draw(self, context): layout = self.layout layout.label(text="Hello") # # register classs # classs = [ TUTORIAL_PT_SamplePanel ] # # register # def register(): for c in classs: bpy.utils.register_class(c) # # unregister # def unregister(): for c in classs: bpy.utils.register_class(c) # # script entry # if __name__ == "__main__": register()
以上が、Panel クラスの最小スクリプトです。このスクリプトを実行すると、View3D スペースのサイドバーへ Panel が追加されます。
サイドバーが表示されていなければ、キーボードの「N」を押すと出てきます。
今回、登録方法(register()
unregister()
)が少し変わっています。
グローバルにリストを持ち各関数内で再帰的に登録、登録解除を行っています。これは複数のクラスを扱う際の定石、定型となっているので覚えておきましょう。
では、Panel クラスを見ていきます。
Panel
サイドバーへ UI を追加する場合、まずは Panel クラスを定義してやる必要があります。
Panelクラスは Operatorクラスと同様にbpy.types.Panel
クラスを継承し、サブクラスとして定義します。
命名規則
第004回でクラスの命名規則にふれましたが、Panel クラスも同じです。
ですが、
class SamplePanel(bpy.types.Panel): bl_idname = "TUTORIAL_PT_SamplePanel"
と、いうようにbl_idname
を宣言して、命名規則にのっとった id を設定してやることでクラス名は自由にできます。
bl_idname
を省略した場合はデフォルトでクラス名が設定されるので、上記のサンプルスクリプトは、
class TUTORIAL_PT_SamplePanel(bpy.types.Panel): bl_idname = "TUTORIAL_PT_SamplePanel"
と同義です。
bl_space_type
Panel が所属するスペースを指定します。
今回は 3D Viewport を指すView_3D
を設定します。
指定できるものには
EMPTY | VIEW_3D | IMAGE_EDITOR |
NODE_EDITOR | SEQUENCE_EDITOR | CLIP_EDITOR |
DOPESHEET_EDITOR | GRAPH_EDITOR | NLA_EDITOR |
TEXT_EDITOR | CONSOLE | INFO |
TOPBAR | STATUSBAR | OUTLINER |
PROPERTIES | FILE_BROWSER | PREFERENCES |
があり、デフォルトはEMPTY
です。
bl_region_type
Panel が所属する region(領域)を指定します。
この指定には注意が必要で、先に指定したbl_space_type
に依存していて、スペースに存在しない領域を指定したり、存在はしていても指定できない領域を指定した場合エラーとなります。
今回の例でいうと、3D View スペースに TOOLS 領域は存在していますが、指定は出来ないので、bl_region = "TOOLS"
とした場合はエラーとなります。
どの組み合わせがダメなのかは調べきれませんでしたので、エラーがでたら「あぁダメなんだ~」ぐらいの感覚でやってます。
指定できるものには
WINDOW | HEADER | CHANNELS |
TEMPORARY | UI | TOOLS |
TOOL_PROPS | PREVIEW | HUD |
NAVIGATION_BAR | EXECUTE | FOOTER |
TOOL_HEADER |
があり、デフォルトはWINDOW
です。今回指定したUI
は右側に出てくるサイドバーのことを指しています。
bl_category
今回のVIEW_3D
UI
という組み合わせの場合、このbl_category
に設定した文字列はサイドバーのタブに使用されます。
すでに登録されているカテゴリーを設定すると、そのカテゴリータブへ追加されます。
ためしに、bl_category = 'Item'
として、Item カテゴリータブへ追加されることを確認してみてください。
今回は「Tutorial」を設定しているのでサイドバーへ新しく Tutorial タブ(Tutorial カテゴリー)が追加されました。
bl_label
このbl_label
に設定した文字列は Panel のタイトルとして使用されます。
bl_label
はbl_category
とは違い、すでに表示されている Panel と重複しても追加とはならず、同じ名前の別 Panel として表示されます。
draw メソッド
Panel クラスのキモとなるメソッドで、描画のたびに呼び出されます。
draw
メソッドは引数に自身のレシーバーとコンテキストを取り、戻り値はありません。
このメソッドの役割は自身が保持しているレイアウトクラス(bpy.types.UILayout
)へ UI レイアウトを定義することです。
レイアウトクラスへレイアウトを定義する場合、基本的にメソッドを呼び出して、パラメーターを指定することで半自動的に UI が追加されます。
上記のサンプルでは、自身が保持しているレイアウトクラスを取得し、ラベルを設定しています。
#--- draw ---# def draw(self, context): layout = self.layout layout.label(text="Hello")
このレイアウトの構築はクセがあります。
今回の目標である「オペレーターの実行」を考えた時に、まず思いつくのは、ボタンを配置してそのボタンが押されたらオペレーターを実行するという構造だとおもいます。
ということは、レイアウトとしてはボタンを配置しないといけません。
ですが、このレイアウトクラスにボタンを配置する、例えばUILayout.button()
の様なメソッドはありません。
その代わり、UILayout.operator()
というメソッドがあります。
そうです、このUILayout.operator()
メソッドにオペレーターを指定してやれば Panel 内にオペレーターを実行するボタンが追加されます。
サンプルスクリプト2
import bpy from bpy.props import * # # TUTORIAL_OT_SayComment # class TUTORIAL_OT_SayComment(bpy.types.Operator): bl_idname = "tutorial.saycomment" bl_label = "Say Comment" bl_options = {'REGISTER', 'UNDO'} #--- properties ---# comment: StringProperty(default = "Hello", options = {'HIDDEN'}) #--- execute ---# def execute(self, context): self.report({'INFO'}, self.comment) return {'FINISHED'} # # TUTORIAL_PT_SamplePanel # class TUTORIAL_PT_SamplePanel(bpy.types.Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "Tutrial" bl_label = "PanelTitle" #--- draw ---# def draw(self, context): layout = self.layout layout.operator(TUTORIAL_OT_SayComment.bl_idname, text = "Say") # # register classs # classs = [ TUTORIAL_PT_SamplePanel, TUTORIAL_OT_SayComment ] # # register # def register(): for c in classs: bpy.utils.register_class(c) # # unregister() # def unregister(): for c in classs: bpy.utils.register_class(c) # # script entry # if __name__ == "__main__": register()
このサンプルスクリプト2を実行すると、「Tutorial」カテゴリー内の「PanelTitle」パネル内に「Say」ボタンが追加されたかと思います。
ボタンを押すと Blender ウィンドウの下部、または、Info エリアに「Hello」と表示されるので確認してください。
オペレーターについてはこれまでのエントリーで説明していますので特に説明はしませんが、プロパティについて補足をします。
今回、オペレーター内にcomment
というStringProperty
を宣言しています。
このプロパティは、文字列を扱うプロパティになっています。
引数のdefault
はいいとして、options
に{'HIDDEN'}
を指定しています。
これは、オペレーターを実行した時にプロパティポップアップへ自動で UI が追加されるのを防ぐ役割を持っています。
試しに、comment: StringProperty(default = "Hello")
として、「Run Script」して「Say」ボタンを押してみてください。
テキスト入力 UI を持ったプロパティポップアップが表示されるとおもいます。
プロパティポップアップは実行されたオペレーターにプロパティーが無い場合表示されません。
今回、唯一のプロパティーである comment プロパティーが HIDDEN なのでプロパティポップアップはプロパティーが無いものと判断します。
この、options = {'HIDDEN'}
はあくまでプロパティポップアップから見ての話なので、オペレーターをスクリプトで呼び出す際の引数としての役割はそのままです。
bpy.ops.tutorial.saycomment(comment = "Hello")
ではこのサンプルスクリプト2を拡張しながら解説をしていきたいとおもいます。
UILayout.operator メソッド
UILayout.operator
メソッドはオペレーターを実行するボタン UI を追加します。
引数は最低でも、実行するオペレーターの idname を文字列で指定してやる必要があります。
今回は、自作のオペレーターを使用するのでTUTORIAL_OT_SayComment.bl_idname
と、idname を指定しています。
既存の、すでに登録されているオペレーターを指定したい場合は、bpy.ops.object.select_all.idname_py()
というように API から取得できます。オペレーターは関数呼び出しではないことに注意してください。
ちなみにこのオペレーターは、表示しているオブジェクトの全選択・非選択を切り替えます。
次にtext
引数はボタンに表示する文字列を指定します。
この引数を設定しない場合は、指定したオペレーターのbl_label
が使用されます。
他にもいくつか引数があるのですが、説明がズレるので割愛します。
指定したオペレーターへ値を渡す
UI からオペレーターを実行できるようになりましたが、実行するオペレーターへ値を渡したい場合どうするか、サンプルスクリプト2でいえばTUTORIAL_OT_SayComment
のcommnet
プロパティーへ文字列を渡したい場合です。
その場合、UILayout.operator
メソッドの戻り値を使用することで実現できます。
# In TUTORIAL_PT_SamplePanel #--- draw ---# def draw(self, context): layout = self.layout op_prop = layout.operator(TUTORIAL_OT_SayComment.bl_idname, text = "Say") op_prop.comment = "Hello Panel"
UILayout.operator
メソッドは戻り値にbpy.types.OperatorProperties
クラスを返します。
このクラスはズバリ、オペレーターのプロパティーへの入力を行うためのクラスです。
注意点として、目当てのプロッパティーへのアクセスは.(ピリオド)
でメンバ変数のようにアクセスします。
補足ですが、UILayout.operator
メソッドでオペレーターを指定した後にプロパティーを設定するという記述に違和感を感じるかもしれません。僕も当初そうでした。
それは「オペレータを実行した後に値を設定している」様に見えるからです。
ですが、それは勘違いです。
draw
メソッドはあくまで描画要求がきた時に UI を構築するだけのメソッドです。
上記のスクリプトはオペレーターを実行するボタンを追加だけした後に、そのボタンに指定されたオペレーターのパラメーター(プロパティー)を更新しているといえます。
そして、ユーザーがボタンを押して初めてオペレーターが実行されます。
描画より前にボタンが押されるということはないので、ボタンからオペレーターを実行する前に必ずプロパティーが更新されているといえます。
値を UI から入力しよう
今回は文字列ですが、オペレーターへ設定する値を UI から設定する方法を説明していきます。
まず入力に関する UI といえば、テキストフィールドだったりトグルボタン、チェックボックスなど色々思いつくとおもいます。
先に書いた通り、レイアウトの追加は半自動で行われるため UI の種類を指定することは基本的にはできません。
では、どうするかというと、UILayout.prop
メソッドを使います。
このメソッドはプロパティーを保持するインスタンスとプロパティーの名前を文字列で指定すると、プロパティーの型にあった UI をレイアウトに追加してくれます。
サンプルスクリプト2でいえば、受け付けたい値の型は文字列(StringProperty
)ですので、UI から入力された文字列(値)を保持するプロパティーを設定すれば、勝手にテキスト入力フィールドが Panel 内に追加されます。
ただ、
def draw(self, context): layout = self.layout layout.prop(TUTORIAL_OT_SayComment, "comment")
この記述ではエラーが出ます、TUTORIAL_OT_SayComment
はあくまでクラスの型で、インスタンスではないからです。
そこで、draw
の引数self
に注目します。この引数は自身のインスタンスを受け取るので、Panel クラスにプロパティークラスを宣言すれば、
# In TUTORIAL_PT_SamplePanel comment_val: StringProperty(default="") def draw(self, context): layout = self.layout layout.prop(self, "comment_val")
いい感じに見えます。
ですが、大きな問題が1つあります。
それは、Panel クラスはプロパティクラスを宣言できないということです。
ですので、self
を使った方法はエラーが出ます。
そこで、出てくるのがカスタムプロパティーという仕組みです。
カスタムプロパティー
Python の言語仕様の話になるのですが、Python は定義したクラスやインスタンスを外部から拡張できるというブッ飛んだ仕様になっています。
この外部から拡張できることを利用して、既存の Blender API クラスへプロパティクラスを追加することをカスタムプロパティーと呼びます。
何度も書きますが、プロパティークラスは Blender において UI と密接に関わってくるのでこの仕組みは非常に重要なものになります。
サンプルスクリプト3
import bpy from bpy.props import * # # TUTORIAL_OT_SayComment # class TUTORIAL_OT_SayComment(bpy.types.Operator): bl_idname = "tutorial.saycomment" bl_label = "Say Comment" bl_options = {'REGISTER', 'UNDO'} #--- properties ---# comment: StringProperty(default = "Hello", options = {'HIDDEN'}) #--- execute ---# def execute(self, context): self.report({'INFO'}, self.comment) return {'FINISHED'} # # TUTORIAL_PT_SamplePanel # class TUTORIAL_PT_SamplePanel(bpy.types.Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "Tutrial" bl_label = "PanelTitle" #--- draw ---# def draw(self, context): layout = self.layout layout.prop(context.scene, "tutorial_comment") op_prop = layout.operator(TUTORIAL_OT_SayComment.bl_idname, text = "Say") op_prop.comment = context.scene.tutorial_comment # # register classs # classs = [ TUTORIAL_PT_SamplePanel, TUTORIAL_OT_SayComment ] # # register # def register(): for c in classs: bpy.utils.register_class(c) bpy.types.Scene.tutorial_comment = StringProperty(default = "") # # unregister() # def unregister(): for c in classs: bpy.utils.register_class(c) del bpy.types.Scene.tutorial_comment # # script entry # if __name__ == "__main__": register()
以上が今回のエントリーの最終形サンプルになります。
まず、注目してもらいたいのが、register
unregister
メソッドです。
register メソッド
各クラスを登録した後にbpy.types.Scene.tutorial_comment = StringProperty(default = "")
としています。
この行がカスタムプロパティーの登録作業になります。
クラスの型を拡張することにより、bpy.types.Scene
クラスのインスタンスは全てtutorial_comment
という変数(プロパティ)を持つようになります。
そして、拡張した変数(プロパティ)へStringProperty
クラスのインスタンスを代入することで拡張した変数(プロパティー)はStringProperty
型として扱えます。
注意点として、クラス内に宣言するときとは違い:(コロン)
ではなく、=(イコール)
による代入であるところです。
unregister メソッド
このメソッドでは各クラスの登録解除をした後にこの行del bpy.types.Scene.tutorial_comment
で、保持しているインスタンスを削除しています。
削除をしなくてもメモリリークはしませんが、アドオン開発などでカクスタムプロパティーを使用する場合はアドオン固有の値の保持が主な目的だとおもいますので、マナーとして削除をしましょう。
カスタムプロパティーを UI として使う
今回はbpy.types.Scene
クラスを拡張したので、現在のシーンももちろんtutorial_comment
を持っています。
「現在の」といえばdraw
メソッドの引数で渡ってくるcontext
が使えます。
そして、コンテキストが保持しているのはインスタンスですので、layout.prop(context.scene, "tutorial_comment")
としてやれば無事、UI が追加されます。
最後に、入力された文字列でオペレーターを更新してop_prop.comment = context.scene.tutorial_comment
完了です。
テキスト入力 UI の左側の名前が要らない場合は
layout.prop(context.scene, "tutorial_comment", text = "")
と、text
引数に空文字を設定すれば消えます。
「Run Script」して入力した文字列がインフォログとして表示されれば成功です。
まとめ
少し長くなってしまいましたが、Panel クラスとオペレーターの関係、UI とプロパティークラス(カスタムプロパティー)の関係はアドオン開発で必ず必要になってきます。
今回のサンプルでいえば、オペレーターのプロパティーを消して、
# In TUTORIAL_OT_SayComment def execute(self, context): self.report({'INFO'}, context.tutorial_comment) return {'FINISHED'}
# In TUTORIAL_PT_SamplePanel def draw(self, context): layout = self.layout layout.prop(context.scene, "tutorial_comment") layout.operator(TUTORIAL_OT_SayComment.bl_idname, text = "Say")
と、いうようにオペレーター内で直接カスタムプロパティーから値を取得することもできます。
もちろん、その場合オペレーターの関数呼び出し時の引数はなくなりますが、そこは実装の仕様でよろしくやってください。
今回は仕組みに中心に説明しました。ですので構築したレイアウトは箇条書きの様になっています。UILayout
クラスは表示方法をある程度指定できるようになっています。
次回は UI レイアウト(bpy.types.UILayout)について詳しく説明できればとおもいます。
ここで記載されているソースコードを使用する場合は自己責任でお願いします。