スクリプト練習に最適!
カスタムファイルブラウザで仕事効率化!

この記事はMaya Advent Calendar 2021の24日目の記事です。

おはこんばんちわ、最近子育て筋トレメソッドを考案中の山本ほっさんです。

今回はAutodesk Mayaにおけるスクリプティング初心者向けの内容で、Pythonを使った便利なファイルブラウザの作成方法についてご紹介します。

これからMayaスクリプティングをガシガシ習得していきたい人のファーストステップとしては最適だと思いますので、ぜひ参考にしていただきたいと思います!

見直そう、ファイルブラウザ

そもそもなんでファイルブラウザ?
すでにMaya準拠のものがあるし、別段自分で作る必要なんかなくない??

ご自身の業務を振り返って考えてみると、いったいどれくらいのファイルを一日に開いたり閉じたりしているでしょうか。
特に配下にスタッフを抱えている方だと、膨大なファイル数をopen/closeしていると思います。

その度に…

  • 案件やスタッフ、データごとに違うフォルダをあちこち探す
  • どのファイルまでチェックしたか忘れないようにメモ…。
  • 開く?インポート?リファレンス?場合によって開き方は様々…その度にダイアログを切り替えて…切り替えたら最初に見てるディレクトリが違う!
  • 急にMayaのファイルブラウザが 激重になった…多分VPNの接続を切ったのでネットワークディレクトリに繋がらなくなったからかも。VPNをもう一度繋ぎ直して…面倒くさい!

…と言う具合に、実は割と細かな無駄な時間を浪費してたりして。
あまりに普段のことなので、意外と気づかないものです。


Toolの設計

まずは設計から。
カスタムブラウザでは、上記に上がったような問題はせめて解決しておきたいですね。

どういう操作が理想かイメージする

効率化のためのToolなので、複雑な操作はなく基本的にワンクリックで開きたファイルをサクサク開ける感じがいいですよね。

  • UIはどんな感じ?

    file

    最初に、操作をイメージしてこんな感じでToolのUIを落書きでしてみるといいと思います。
    操作イメージはこんな感じ。

  • 任意のフォルダをパスかファイルダイアログで指定
  • 指定されたフォルダ内のMayaファイルがボタンで一覧表示されている
  • ボタンを押すとファイルが開く
  • 一度開いたファイルは、ボタンの色が変わって分かるようにする

と、こんな感じでしょうか。

pythonコーディングの全体をイメージする

  • UIはClassを使って、オブジェクトにまとめてしまおう。

    フォルダのルートを指定して、取得したファイルリストや、「どこまで開いたか」の情報など、Tool内に一時的に記憶させておきたい情報がいくつかあります。
    この場合、ToolのUIはClassを使ってオブジェクトとしてまとめてしまったほうが、情報の取り回しが楽になります。

  • 本処理部分はUIのClassからは分けておきたい

    ただし、すべての処理をClass内に入れ込んでしまうと、Tool部分のソースコードが長くなってしまったり、追加処理や仕様変更のたびに修正が入ってメンテナンスが大変に。
    リリース後の運用を考えると、本処理部分はClassの外に出しておくのが無難です。
    今回の本処理部分でいうと、「ファイルを開く」に当たります。


実装

では、設計が完了したので、ガシガシ作っていきます。

まずは簡単なUIを作ってみよう

まずは、

  • フォルダのフルパスを指定する
  • 指定されたフォルダ直下のファイルがボタンで羅列される
  • 羅列されたボタンをクリックするとファイルが開く
    というシンプルなところから始めてみましょう。

ざっと処理を書くとこんな感じ。

# coding: utf-8
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import division

from maya import cmds
from maya.common.ui import LayoutManager
import os,functools

#ファイルを開く本処理
def openFile(filePath,*args,**kwargs):
    #拡張子を判別
    type = 'mayaAscii'
    if filePath.split('.')[-1] == 'mb':
        type = 'mayaBinary'
    #ファイルを開く
    cmds.file(filePath,o = True,f = True,type = type)

#Tool本体
class customFileBrowser():
    #クラスアトリビュートを定義
    def __init__(self,*args,**kwargs):
        #UIのな名前をきめちゃう。
        self.win = 'CustomFileBrouserUI'
        #デフォルトのボタンの色(まだ開いてない・開いたあと)
        self.buttonColorList = [[0.2,0.3,0.35],[0.0,0.5,1.0]]        
        #ファイルリストを取得するRootのディレクトリのパスを格納
        self.rootPath = ''
        #ファイルパスのリスト(あとで指定フォルダ直下のファイルパスをリストアップする)
        self.fileList = []

        #ファイルを開くボタンコントロールのリストを格納
        self.fileButtonList = []
        #一度開いたボタンを記憶
        self.openedFileButtonList = []

    #UIをアップデートする(ボタンをファイルリストに合わせて追加)
    def updateUi(self,*args,**kwargs):

        #元々格納されているボタンの情報をいったんクリアする
        for btn in self.fileButtonList:
            cmds.deleteUI(btn)
        self.fileButtonList[:] = []
        self.openedFileButtonList[:] = []

        #ファイルリストを取得
        if not os.path.exists(self.rootPath):
            #rootディレクトリが存在しなかった場合
            self.fileList = []
        else:
            self.fileList = os.listdir(self.rootPath)

        #ボタンを追加する
        indx = 0
        for fileName in self.fileList:
            #Mayaファイルかどうか
            if not fileName.split('.')[-1] in ['ma','mb']:continue

            self.fileButtonList.append(cmds.button(l = fileName,h = 20,
                c = functools.partial(self.openFile,fileName,indx),
                p = self.btnLayout))
            indx +=1
        #ボタンの色を更新        
        self.updateButtonColor()

    #ボタンの色を更新する
    def updateButtonColor(self,*args,**kwargs):
        for btn in self.fileButtonList:
            if btn in self.openedFileButtonList:
                #既に開いた
                cmds.button(btn,e = True,bgc = self.buttonColorList[1])
            else:
                #まだ開いてない
                cmds.button(btn,e = True,bgc = self.buttonColorList[0])

    #rootディレクトリのパスを設定
    def setPath(self,*args,**kwargs):
        #セットされたrootディレクトリのパスから「\」を「/」に置換して、正しく評価されるようにする。
        self.rootPath = args[0].replace('\\','/')
        #セットされたフォルダパスが正しいかどうか
        if not os.path.exists(self.rootPath):
            self.rootPath = ''
        cmds.textField(self.dirPathFiled,e = True,tx = self.rootPath)
        self.updateUi()

    def setPathByDialog(self,*args,**kwargs):
        dirList = cmds.fileDialog2(fm = 3,cap = 'Please set root dir path',okc = 'Set')
        if not dirList:return
        self.setPath(dirList[0])

    #ファイルを開く前処理(Classアトリビュートからファイルパスを生成して、本処理へ渡す)
    def openFile(self,filename,btnIndx,*args,**kwargs):
        #ファイルパスを生成
        filePath = '{}/{}'.format(self.rootPath,filename)
        #ファイルをひたく(本処理へ)
        openFile(filePath)
        self.openedFileButtonList.append(self.fileButtonList[btnIndx])

        #ボタンの色を更新        
        self.updateButtonColor()

    # 指定フォルダ以下のファイル
    def  ui(self):
        #もしもすでにそのUIがあったら、あらかじめ消しておく
        if cmds.window(self.win,q=True,ex=True):
            cmds.deleteUI(self.win)
        cmds.window(self.win)
        with LayoutManager(cmds.formLayout()) as fl:
            #rootディレクトリを指定するUIを追加
            with LayoutManager(cmds.rowLayout(nc = 3,adj = 2)) as row:
                cmds.text(l = 'directory:')
                self.dirPathFiled = cmds.textField(cc = self.setPath)
                cmds.iconTextButton(st = 'iconOnly',image1 = 'browseFolder.png',
                    c = self.setPathByDialog)
            #ファイルリストを表示するスクロールリストを追加
            self.btnLayout = cmds.scrollLayout(cr = True)

        #UI要素の位置調整
        cmds.formLayout(fl,e = True,af = ((row,'top',0),(row,'left',0),(row,'right',0),
            (self.btnLayout,'bottom',0),(self.btnLayout,'left',0),
            (self.btnLayout,'right',0)),ac = (self.btnLayout,'top',0,row))

        cmds.showWindow(self.win)

def showWindow():
    tool = customFileBrowser()
    tool.ui()

showWindow()

上記をスクリプトエディタにコピペし、実行するとToolが起動します。
で、実行した結果がこちら。

file

上部のフォルダアイコンボタンを押すと、ダイアログが出るので…

file

ここから任意のフォルダを指定すると…
file

こんな感じに、ボタンが羅列されます。

もちろん、エクスプローラーからファイルパスをコピーしてペーストしても…

file

「\」が「/」に自動的に置換されて格納されて、正しく同様にボタンが羅列されます。
file

ボタンを押して、ファイルを開いてみると…

file

シーンが開き、ボタンの色が変わりました!
もちろん別のシーンを開いても…
file

この通り!

非常にシンプルなToolですが、日々チェック業務などで追われている方には非常に有効なToolではないでしょうか。


かいつまんで解説

ここからは、上記でさらりとご紹介したソースコードについて、いくつかポイントとなる個所を解説いたします。

LayoutManager ってなに!?

これは、Maya AdventCalendar2019の以下記事
https://qiita.com/lie_871221/items/1b6ef8faba06e5180cf9
@lie_871221よりご紹介頂いたMayaコマンドを使ったGUIの書き方です。
通常の書き方では、以下のように…

from maya import cmds
win = cmds.window()
cmds.columnLayout(adj = True)
cmds.button()
cmds.rowLayout(nc = 2)
cmds.text(l = 'hogehoge')
cmds.button()
cmds.setParent('..')
cmds.setParent('..')

cmds.showWindow(win)

…というように、レイアウトの親子構造が非常に見にくくなります。
LayoutManagerを使うことで…

from maya import cmds
from maya.common.ui import LayoutManager
win = cmds.window()
with LayoutManager(cmds.columnLayout(adj = True)):
    cmds.button()
    with LayoutManager(cmds.rowLayout(nc = 2)):
        cmds.text(l = 'hogehoge')
        cmds.button()

cmds.showWindow(win)

このようにインデントでレイアウトを分けて書くことができ、大変便利になります。
この書き方は、ヘルプドキュメントにも明確に書いておいてほしいものですね…!!

どうやって動的にボタンを追加している?

それは、以下のソースのところで実行しています。

    #UIをアップデートする(ボタンをファイルリストに合わせて追加)
    def updateUi(self,*args,**kwargs):

        #元々格納されているボタンの情報をいったんクリアする
        for btn in self.fileButtonList:
            cmds.deleteUI(btn)
        self.fileButtonList[:] = []
        self.openedFileButtonList[:] = []

        #ファイルリストを取得
        if not os.path.exists(self.rootPath):
            #rootディレクトリが存在しなかった場合
            self.fileList = []
        else:
            self.fileList = os.listdir(self.rootPath)

        #ボタンを追加する
        indx = 0
        for fileName in self.fileList:
            #Mayaファイルかどうか
            if not fileName.split('.')[-1] in ['ma','mb']:continue

            self.fileButtonList.append(cmds.button(l = fileName,h = 20,
                c = functools.partial(self.openFile,fileName,indx),
                p = self.btnLayout))
            indx +=1
        #ボタンの色を更新        
        self.updateButtonColor()

まず、あらかじめToolのUI上に、ボタンを格納するためのcolumnLayoutを用意しておきます。
「self.btnLayout」がそれにあたります。
追加されたボタンの名前は「self.fileButtonList」に登録されているので、前に登録されていたボタンをすべて削除した後、このレイアウトに対してボタンを動的にfor文で追加しているわけです。

functools.partial ってなに!?

通常、コントロールから実行させたいコマンドは、mel文ではプロシージャかプロシージャやコマンドを実行する文字列を指定しますが、maya.cmdsでも実行文字列以外に、関数をそのまま指定することが可能です。
実行させる関数の引数に変数を当てておくことで、実行結果を動的に制御させることが可能になるわけですが、これを可能とするために無名関数、すなわちlambda式を使用することが多々あります。
ですが、今回のようにfor文でlambda式を書いてしまうと、毎回せっかく生成した無名関数を上書きしてしまって、最終的にはどのボタンを押しても最後に生成されたボタンと同じ結果しか実行されなくなってしまいます。
試しに、上記のソースコードで

            self.fileButtonList.append(cmds.button(l = fileName,h = 20,
                c = functools.partial(self.openFile,fileName,indx),
                p = self.btnLayout))

のところを

            self.fileButtonList.append(cmds.button(l = fileName,h = 20,
                c = lambda *args:self.openFile(fileName,indx),
                p = self.btnLayout))

…に変えて実行してみると…。。

file

このように、どのボタンを押しても最後のボタンのシーンが開くようになり、かつ、ボタンの色を変えようとした際にindexErrorが出ます。
これは、ボタンを登録した後で、登録されたボタンのインデックスを示すための変数indxがインデントしているからです。

        for fileName in self.fileList:
            #Mayaファイルかどうか
            if not fileName.split('.')[-1] in ['ma','mb']:continue

            self.fileButtonList.append(cmds.button(l = fileName,h = 20,
                c = lambda *args:self.openFile(fileName,indx),
                p = self.btnLayout))
            indx +=1
        #ボタンの色を更新  

ここのindex +=1のところがそれです。
すなわち、想定した変数が正しく関数にわたっていないのです。
これを解決するのに、functools.partialが使えます。
functools.partialを使って、関数と変数を指定しておけば、lambda式のように関数や変数を上書きされることなく、動的にボタンを生成することが可能です。
lambda式はUIの動的生成ではなく、固定の場合に有効です。


まとめ

いかがでしたでしょうか!
シンプルなToolでも割と奥が深かく、習熟にはもってこいではなかったでしょうか。
また、このようなシンプルなToolでも業務に使用する分には非常に効果的で、実用性が高かったと思います。

実務に使用するレベルのToolをなるべくシンプルな機能に落とし込んで作ってみるというのも、スクリプト習熟のための練習には非常によい教材になりえると思います。
是非、身の回りの諸問題を見直して、よりより作業環境を整えてみてください。

OMAKE

OMAKEと書いてオーメイク…ではないですよ、オマケですよ~?

さて、ここまで作ったシンプルなToolですが、やっぱり使ってみるともう少しカユイところに手を伸ばしたくなってくるものです。

  • Toolを開きなおすと、毎回情報がリセットされてしまうので、情報を永続的に記憶できないか…
  • Rootディレクトリ、毎回セットするのが面倒。直近のパスを「よく使うリスト」みたいに登録できないものか…
  • mbもmaも一緒くたに表示されてしまう…フィルタリングできないか…
  • ボタンの色を一つだけ戻したい…
  • fbxもインポートさせたい…
  • 子階層へもアクセスしたい…
  • プロジェクトセットできないもんか…

…とかとか!
今後、これらの追加要望について、そのアプローチを紹介する形で、今回ご紹介した「カスタムファイルブラウザ」をアップデートしていってみましょう。

ひとまず、今回はこの辺で!
サヨナラ!サヨナラ!サヨナラ!!

Author: yamamoto tomohito

COYOTE 3DCG STUDIO モーション・リグ・パイプライン系TA。 他にやる人がいないのでチームリーダーです。三時のヒロイン…じゃない、三児の父。パパじゃないぞ。おとうさんだ!