my funeral week

少しでも日々の生活に変化を。

SoftimageのUIをPythonから

今回もSoftimageの話。
中でもPythonでのUI制作についての備忘録です。

最初に言いたいのは、Softimage(以下XSI)において、Pythonを使うのはちょっと面倒です。

XSIではバージョン2010以降からPythonに標準対応となりましたが、それ以下のバージョンでは別途手順を踏まないとPythonが利用できませんでした。
そのせいか、利用者が少なくてネット上にリファレンスになりそうな情報も少ないです。
頼りの公式リファレンスもVBやJsをベースに説明されている印象?があり、わざわざPythonを使うメリットがないように私は感じました

私はJsやVBよりもPythonの方が馴染みがあるので、無理やりにでもPythonを利用していましたけれど、もしVBかJsの知識をお持ちなのでしたらそちらを参考にされた方が良いです。

もっとも・・・XSIのScriptingを今更勉強しようなんて人はいないと思いますけど!

前置きはこれくらいにして、本編にいきます。



簡単な例

まずは、最低限のものを制作します。

f:id:garysfirearms108:20190113110737j:plain

構成物は以下の通り

  • テキストフィールド
  • ボタン


コード例

xsi = Application

#Define CustomProperty
customProperty = XSIFactory.CreateObject("CustomProperty")

customProperty.Name = "MyCustomProperty"

#Define TextField
ui_textField = "MyTextField"

customProperty.AddParameter2(ui_textField, 8)

#Define Layout
layout = customProperty.PPGLayout

layout.AddGroup("MyGroup")

layout.AddItem(ui_textField, "Label")

layout.EndGroup()

#Define Button
layout.AddButton("CommandName", "Button")

#Show window
xsi.InspectObj(customProperty)


解説

XSIにおけるUIの作成は、恐らく以下の流れがセオリーじゃないかと思う。

  1. カスタムプロパティを用意
  2. パラメータ(ウィンドウの内容物)をカスタムプロパティに追加
  3. レイアウトを定義
  4. レイアウトにプロパティを配置
  5. InspectObjにてウィンドウを表示

前述のコードもこの流れにのっとって記述している。

では、コード各部を解説。


Define CustomProperty

customProperty = XSIFactory.CreateObject("CustomProperty")

customProperty.Name = "MyCustomProperty"

カスタムプロパティを作成しています。
UIを作成するには、カスタムプロパティが必要です。
このオブジェクトに色々なパラメータを付与して、最終的にウィンドウとして表示させます。
この XSIFactory.CreateObject("CustomProperty") 以外にも作成方法がありますが、違いがわかりません。

作成したカスタムプロパティには、念のために分かりやすい名前を付けるようにしています。

カスタムプロパティそのものについては、各自リファレンスを参照してください。

download.autodesk.com

作成したカスタムプロパティは、シーンに追加されているものだと思うのですが・・・、
SceneRoot以下に存在しているのかどうか、確認したことはないです。というか確認できたことがないです・・・。

  • シーンに存在しているけど不可視?
  • ウィンドウを閉じた瞬間にシーンから削除されている?
  • メモリには存在している?

多分、このうちのどれかが該当するのだと思いますが、詳しい人いませんか?


Define TextField

ui_textField = "MyTextField"

customProperty.AddParameter2(ui_textField, 8)

ウィンドウ内の内容物を作成しています。
この例ではテキストフィールドしか作成していません。それ以外のパーツの作成に関しては後述します。

XSIでは、テキストフィールドをはじめとする、ユーザーが値を自由に編集できるもの はカスタムプロパティのパラメータとして扱われるようです。

そして、パラメータは必ず 名前付けが必要 です。
XSI上ではScriptNameと呼ばれるようで、この名前を参照にパラメータの値にアクセスするようです。

AddParameter2 にて、カスタムプロパティにパラメータを追加しています。
その際にはsiVariantTypeを指定する必要があります。XSIにおける、データの型のことです。

download.autodesk.com

リファレンスを見る限り、siVariantType自体は 定数 として定義されているようです。
テキストフィールドは文字列を扱うので、siString、つまり「8」が該当します。
指定したsiVariantTypeによって、扱われる型が変化し、UIの種類もそれに対応したものが表示されます。


Define Layout

layout = customProperty.PPGLayout

layout.AddGroup("MyGroup")

layout.AddItem(ui_textField, "Label")

layout.EndGroup()

UIの見た目を決めるにはレイアウトを指定する必要があります。
レイアウトの指定は、カスタムプロパティのメンバの PPGLayout を編集することで行います。

レイアウトの種類。色々あると思いますが、私は凝ったUIをXSIで作ったことがないので・・・この辺りの知識はございません。すみません。
AddGroup を使えばそれらしい形には仕上がるので、ここから調べてみてください。

AddGroupは文字通り、ウィンドウ内のパーツをまとめることができます。

例えば・・・トランスフォーム値の設定するパラメータ群とそれ以外でグループを分けたりする等、利用することも多いと思います。

使い方は、グループを定義する AddGroupとEndGroupの間にAddItem にてパーツを追加します。

で、AddItem は、第一引数がパーツのScriptName、第二引数がUIに表示されるラベル となります。
日本語版のXSIを利用していると、ラベルに指定する文字によっては勝手に翻訳されて表示されることもあるので注意。「Position」と指定したラベルが、実際には「位置」と表示されたり等。

このグループ機能はMayaにも今後追加されるといいと思う。つか、もう代用品があんのかな?


Define Button

layout.AddButton("CommandName", "Button")

ボタンの作成を行います。
ボタンはレイアウトを指定しなくとも問題ないです。自動的に縦方向に配置されます。
パラメータをレイアウトに追加した時のように、AddButtonの 第一引数はScriptNameで、第二引数はラベル です。

このコード例では、ボタンを押したときのコマンドを何も追加していませんが、 ボタンのScriptNameは、追加するコマンド名(メソッド名)と関連付ける必要があります。

例えば、ボタンを押した際に GetParameterValues()というメソッドを実行したい ならば・・・ ボタンの ScriptNameをGetParameterValues にし、メソッド名をGetParameterValues_OnClicked()にする必要があるということです。

MayaのUIのようにボタンにメソッド名を渡すのではなく、ボタン自身が名前を元にメソッドを探す仕組み(?)になっているようです。


Show Window

xsi.InspectObj(customProperty)

最後にウィンドウの表示をします。
InspectObj() が、それにあたる箇所なのですが、これは文字通り シーン内のオブジェクトをインスペクトするコマンド です。シーン内のオブジェクトに実行すると、そのオブジェクトのプロパティ一覧が表示されます。

UIを表示するというよりは 「作成したカスタムプロパティをインスペクトする」 という感覚みたいです。この辺もMayaとは異なりますね。


簡単な例の解説は以上です。

ここからは、個人的によく利用するUIの種類について解説します。



パラメータとリストコントロール

よく利用するパラメータと項目を羅列するリストUI。
さらに、実際にスクリプトからUIのパラメータのアクセスについても。

f:id:garysfirearms108:20190113112252j:plain

構成物


コード例

from win32com.client import constants as c

xsi = Application

customProperty = XSIFactory.CreateObject("CustomProperty")

customProperty.Name = "MyCustomProperty"


# Define Parameters
ui_CheckBox = "MyCheckBox"
ui_FloatField = "MyFloatField"
ui_IntField = "MyIntField"
ui_TextField = "MyTextField"

customProperty.AddParameter2(ui_CheckBox, c.siBool, True)
customProperty.AddParameter2(ui_FloatField, c.siFloat, 1.25)
customProperty.AddParameter2(ui_IntField, c.siInt4, 20)
customProperty.AddParameter2(ui_TextField, c.siString, "something text")


# Define Layout
layout = customProperty.PPGLayout

layout.AddGroup("Parameters")

layout.AddItem(ui_CheckBox, "CheckBox")
layout.AddItem(ui_FloatField, "Float")
layout.AddItem(ui_IntField, "Integer")
layout.AddItem(ui_TextField, "Text")

layout.EndGroup()


# Define ListItems
enumArray = ["ValueA", 1, "ValueB", 2, "ValueC", 3, "ValueD", 4]

# Define Parameters
ui_ComboBox = "MyComboBox"
ui_RadioButton = "MyRadioButton"
ui_ScrollList = "MyScrollList"

customProperty.AddParameter2(ui_ComboBox, c.siInt4)
customProperty.AddParameter2(ui_RadioButton, c.siInt4)
customProperty.AddParameter2(ui_ScrollList, c.siInt4)

# Define Layout
layout.AddGroup("ListItems")

layout.AddEnumControl(ui_ComboBox, enumArray, "ComboBox", "Combo")
layout.AddEnumControl(ui_RadioButton, enumArray, "RadioButton", "Radio")
layout.AddEnumControl(ui_ScrollList, enumArray, "ScrollList", "ListBox")

layout.EndGroup()

# Define Button
layout.AddButton("GetParameterValues", "Get")

# Show Window
xsi.InspectObj(customProperty)


解説

最初の一行目に win32のインポートが追加されています が、これを追加することで XSI側の定数を利用することが可能 になり、siVariantTypeを数値でなく、直接指定できるようになります。

チェックボックスからテキストフィールドまでのパラメータ部分の解説は、siVariantTypeを変更しているだけなので、詳しくは省きます。 一点だけ、AddParameter2の第三引数は、そのパラメータのデフォルト値となります。

customProperty.AddParameter2(ui_CheckBox, c.siBool, True)
customProperty.AddParameter2(ui_FloatField, c.siFloat, 1.25)
customProperty.AddParameter2(ui_IntField, c.siInt4, 20)
customProperty.AddParameter2(ui_TextField, c.siString, "something text")


リストコントロールの作成

リストコントロールは大体EnumControlを利用します。

リスト内のアイテム
enumArray = ["ValueA", 1, "ValueB", 2, "ValueC", 3, "ValueD", 4]

XSIでリストコントロールを作成するには、まず、リスト内のアイテムの配列が必要 です。
これも列挙型をPythonで扱えないせいなのでしょうか、なんだか気持ち悪い配列を用意します。

項目名の後に、それぞれ数値を割り振って疑似的な列挙型を作ります
Pythonが動的型付けを採用しているとは言え、非常に気持ち悪く感じます。


カスタムプロパティへの追加
customProperty.AddParameter2(ui_ComboBox, c.siInt4)
customProperty.AddParameter2(ui_RadioButton, c.siInt4)
customProperty.AddParameter2(ui_ScrollList, c.siInt4)

また、リストコントロール自体もパラメータとして扱われますので、AddParameter2にてカスタムプロパティへ追加します。


レイアウトへの追加
layout.AddEnumControl(ui_ComboBox, enumArray, "ComboBox", "Combo")
layout.AddEnumControl(ui_RadioButton, enumArray, "RadioButton", "Radio")
layout.AddEnumControl(ui_ScrollList, enumArray, "ScrollList", "ListBox")

AddEnumControl を使います。
第一引数がScriptName、第二引数が作成したリスト内アイテム、第三引数がラベル、第四引数が siPPGControlType です。
こちらもsiVariantTypeと同様に型指定を行う定数ですが、こちらは文字列での指定が可能です。定数ですので、siXXXXみたいなことも出来ると思いますが、長くなるし、siVariantTypeのように劇的に可読性が上がるようなメリットもないので私は文字列で指定していました。

さて、UIの作成についてはこれくらいにして・・・
これらのパラメータへスクリプトからアクセスの仕方について少し触れます。


UIへのアクセス

先ほどのコード例の最後に追加した ボタンを押すと、パラメータ値を取得 という内容で進めます。

UIに追加されたパラメータへのアクセスは、全てPPGというオブジェクトから行います。

def GetParameterValues_OnClicked():
    
    print PPG.MyCheckBox.Value
    print PPG.MyFloatField.Value
    print PPG.MyIntField.Value
    print PPG.MyTextField.Value
    
    print PPG.MyComboBox.Value
    print PPG.MyRadioButton.Value
    print PPG.MyScrollList.Value

こんな感じで、PPG.パラメータ名.Valueでアクセスします。


コマンドを実装する

・・・では、この GetParameterValues_OnClicked をボタンに認識させたいのですが、これがPythonだと厄介な手順を踏む必要があります。

XSIのボタンにコマンドを認識させる為には、コマンド自体をPPGLayout.Logicに渡す必要があります
また、渡す際には、コマンド内で利用するメソッドやオブジェクトを全て文字列に変換する必要があります

Jsなんかではオブジェクト自体を .toString() で簡単に変換できますが、デフォのPythonにはそんな機能がないので、コードそのものをクォーテーションで囲って文字列にしておく等の処理が必要です。

私はそもそも UIを生成するスクリプトと実際の処理を行うコードを分ける派 の人間ですので、以下のような処理でコードの文字列変換を行っています。

先ほどのパラメータとリストコントロールのコード例から、追記します。

# Define Button
layout.AddButton("GetParameterValues", "Get")

#---------------------------------------
layout.Language = "Python"

logic = open("FilePath").read()

layout.Logic = logic
#---------------------------------------

# Show Window
xsi.InspectObj(customProperty)

点線で囲んだ部分が追記箇所です。

まずはLanguageに、利用する言語を指定します。

次に問題のコードの文字列変換を行う部分ですが、処理を行うコードを別スクリプトに分けているので、open()スクリプトファイルのパスを渡すことで、文字列として読み込んでいます。

スマートではないかも知れませんが、これが一番話が早いかなと思います。

スクリプト自体をXSIがプラグインとして認識できるような形で書けば、もっと方法があるのかもしれませんけど・・・。



コマンドパスを取得する

最後に、これは私の環境のせいかもしれませんが、XSIを挟むとPythonからスクリプト自体のパスを認識できません・・・。

それを解決する為に、以下のような手法を利用していました。

def getMainFile():
    
    import os
    
    selfFilePath = xsi.GetCommandByScriptingName("ScriptName").OriginPath
    
    mainFilePath = os.path.join(selfFilePath, "MainLogicFileName")
    
    if not os.path.exists(mainFilePath):
        
        return None
    
    return open(mainFilePath).read()

一旦、GetCommandByScriptingNameでXSI側で認識されている自身のスクリプトコマンドにアクセス することで、自分のパスを取得し、それと同階層にあるメイン処理のスクリプトを取得するという内容です。

もっとよい方法があれば、ぜひ教えてください。


非常に長くなりましたが、今回は以上です。
もう使わないだろうって思った知識を頭の外に放り出したかっただけです。
当時、間違って覚えてた知識もそのままですけど、いいでしょう。なんせSoftimageことXSIはもう死んだのですから。