めもてう

忘れっぽいTIPs、HowToのメモ帳です。

【Blender 2.8 アドオン開発】006 UI - Panel編 -

前回、Propertyクラスを使うと UI(プロパティポップアップ)から機能(Operator)への入力が出来ることを説明しました。

ただ、プロパティポップアップから入力を行う場合、値が更新されるたびに機能の処理(execute)が実行されます。
もちろん逐次変化を確認できるので便利ではありますが、重い処理など値を入力したら一度だけ処理を実行したい場合もあります。

また、機能の実行を毎回Pythonコンソールや、サーチメニューから実行するのは不便です。

そこで今回は、サイドバーへ UI を追加し、そこからオペレーターを実行することを目標とし、Panel クラスの基本を説明していきたいとおもいます。

サンプルスクリプト

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」を押すと出てきます。

f:id:Hobbyist:20190611030300p:plain

今回、登録方法(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です。

公式リファレンス - Panel#bl_space_type

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は右側に出てくるサイドバーのことを指しています。

公式リファレンス - Panel#bl_region_type

bl_category

今回のVIEW_3DUIという組み合わせの場合、このbl_categoryに設定した文字列はサイドバーのタブに使用されます。

すでに登録されているカテゴリーを設定すると、そのカテゴリータブへ追加されます。
ためしに、bl_category = 'Item'として、Item カテゴリータブへ追加されることを確認してみてください。

今回は「Tutorial」を設定しているのでサイドバーへ新しく Tutorial タブ(Tutorial カテゴリー)が追加されました。

bl_label

このbl_labelに設定した文字列は Panel のタイトルとして使用されます。
bl_labelbl_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 内にオペレーターを実行するボタンが追加されます。

サンプルスクリプト

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」ボタンが追加されたかと思います。

f:id:Hobbyist:20190613151857p:plain

ボタンを押すと Blender ウィンドウの下部、または、Info エリアに「Hello」と表示されるので確認してください。

f:id:Hobbyist:20190613151854p:plain

f:id:Hobbyist:20190613151850p:plain

オペレーターについてはこれまでのエントリーで説明していますので特に説明はしませんが、プロパティについて補足をします。

今回、オペレーター内に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が使用されます。

他にもいくつか引数があるのですが、説明がズレるので割愛します。

公式リファレンス - UILayout#operator

指定したオペレーターへ値を渡す

UI からオペレーターを実行できるようになりましたが、実行するオペレーターへ値を渡したい場合どうするか、サンプルスクリプト2でいえばTUTORIAL_OT_SayCommentcommnetプロパティーへ文字列を渡したい場合です。

その場合、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 と密接に関わってくるのでこの仕組みは非常に重要なものになります。

サンプルスクリプト

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()

以上が今回のエントリーの最終形サンプルになります。

まず、注目してもらいたいのが、registerunregisterメソッドです。

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)について詳しく説明できればとおもいます。

ここで記載されているソースコードを使用する場合は自己責任でお願いします。

プライバシーポリシー (Privacy policy)