Maya UIにひとてま

[Maya Advent Calendar 2019](https://qiita.com/advent-calendar/2019/maya)

今年もアドカレ一筆。

今年はちゃんとオンタイムです。

今回は今までの自分の趣向を変えてのUIネタでございます。見栄えがいいからね。

私、ツール作るときは遊び心を大事にしてまして、

何か新しいツール作るときは、常に新しい要素とかイースターエッグとかを仕込みます。

特にUIを作っていると、使ってて楽しそうなのを作りたくなる次第です。
<br>
<br>
<br>
<br>

というわけで、今回のサンプル。
![coordUI_00.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153835/98ad4e1f-fa01-6ec1-23e0-c20fd84a5353.gif)
今回のネタを使うとキャラメイクっぽいことができるようなUIが作れます。
<br>
<br>

# そもそも

Mayaが用意しているUI系のコマンド(本記事では以下Maya UIと呼称)ってなんか痒い所に手が届かないんですよね。

たとえば、今回のサンプルで言えば、画像表示に使う <font color="Orange">picture</font> コマンド。

これ、ソース画像のサイズをそのままで表示します。

ソース画像のサイズをそろえていれば何の問題もないですが、

できれば、サイズが違った場合に、フレキシブルにスケーリングして使いたいです。

また、サムネ表示として小さく表示したいときもあることでしょう。

つまり、ソースに依存せずに表示のコントロールをコードで行いたいのです。

その他のUI系のコマンドでもちょっとコントロールしたいことができない、

まさしく「痒い所に手が届かない」むずむず感があります。
<br>

といいつつも、Maya UIにもいいところはあります。

「コマンドを叩けば細かいこと指定しなくてもなんかうまい感じで出てくれる」というところです。

作り慣れていない人からすれば、Maya UIでもめんどくさいんですが、

ちょっとしたボタンだけのUIとかを作るときとかは、PyQt, PySideよりも接しやすいと思います。
(※もちろんPyQt, PySideのが楽という人もいます。)
<br>

というわけで、本記事の趣旨。

**「Maya UIの気安さをいいとこどりしつつ、ちょっと凝ったUIを作る」**

やることは簡単。

Maya UIをベースにして、そこに必要なPySideのwidgetを埋め込みます。
<br>
<br>

# Maya UIにPySideを埋め込む

じゃあ、Maya UIにPySideを埋め込むにはどうしたらいいのか。

埋め込むといっても、単純に親子関係を作ってあげるだけでいいのです。

つまり、PySideのwidgetに対してMaya UIを親に指定してあげます。

```python:
QWidget.setParent(parentWidget)
```

簡単ですね。

ですが、ここで問題。parentWidgetに入れる型は<font color="Green">PySide.QtGui.QWidget</font>じゃないといけません。

```python:
QWidget.setParent(mayaUI)
```

としても、<font color="Red">駄目</font>なわけです。

そこで、Maya UIを一旦PySideに変換します。
<br>

MayaのUIをPySideとして取得する方法!!
https://kiwamiden.com/how-to-get-mayas-ui-as-pyside
　

あったよ！**たっくん大先生**の執筆した記事が！

でかした！

というわけで解決です。

Maya2017以降ではPySide2になっているのでちょちょいと書き換えます。

ほぼほぼ転載みたいで**大先生**には忍びないけど…

```python:
from PySide2 import QtGui
from PySide2 import QtWidgets
try:
    import shiboken2
except ImportError:
    from PySide2 import shiboken2
import maya.OpenMayaUI as omUI

# ---------------------------------------------------------
# mayaUI to pyside(thanks to takkyun)
# ---------------------------------------------------------
# @param <str>name    : maya UI name
# @param <type>toType : pyside type
# @return <obj> : pyside obj
def mayaToPySide(name, toType):

    ptr = omUI.MQtUtil.findControl(name)
    if not ptr:
        ptr = omUI.MQtUtil.findLayout(name)    
    if not ptr:
        ptr = omUI.MQtUtil.findMenuItem(name)
    if not ptr:
        return None
 
    return shiboken2.wrapInstance(long(ptr), toType)
```

これを…こうじゃ

```python:
QWidget.setParent(mayaToPySide(mayaUI), QtWidgets.QWidget)
```

これで親子関係が作れる = PySideを埋め込めます。
<br>
<br>

# サンプルを作ってみる

まずは、入れ物となるMaya UIを作ります。

さながら3分クッキングばりに作ったものがこちらになります。

```python:
import maya.cmds as cmds
class toolGUI(object):
    windowName = 'coordWindow'
    windowTitle = 'Coordinate window'
    tabName = ['Coordinate']
    icName = 'coord'
    defWidth = 510
    defHeight = 530

    # ---------------------------------------------------------
    # init
    # ---------------------------------------------------------
    # @param None
    # @return None
    def __init__(self):
        if cmds.window(self.windowName, exists=True):
            cmds.deleteUI(self.windowName)

    # ---------------------------------------------------------
    # UI : Tab layout
    # ---------------------------------------------------------
    # @param None
    # @return <uiObj>coordTabcLayout : tab layout
    def tab_coord(self):
        coordTabcLayout = cmds.columnLayout(adj=1, p=self.alltabLayout)
        # thumbnail UI

        return coordTabcLayout

    # ---------------------------------------------------------
    # UI : show window
    # ---------------------------------------------------------
    # @param None
    # @return None
    def show(self):
        windowlayout = cmds.window(self.windowName, title=self.windowTitle,
                                   iconName=self.icName, menuBar=True)
        # window layout
        allformLayout = cmds.formLayout()
        self.alltabLayout = cmds.tabLayout(p=allformLayout)
        # build coord Tab
        coordTabcLayout = self.tab_coord()
        # set tab
        cmds.tabLayout(self.alltabLayout, e=True,
                        tabLabel=((coordTabcLayout, self.tabName[0])))
        # all form
        cmds.formLayout(allformLayout, e=True, af=[self.alltabLayout, 'top', 0])
        cmds.formLayout(allformLayout, e=True, af=[self.alltabLayout, 'left', 0])
        cmds.formLayout(allformLayout, e=True, af=[self.alltabLayout, 'right', 0])
        cmds.formLayout(allformLayout, e=True, af=[self.alltabLayout, 'bottom', 0])
        cmds.setParent('..')
        cmds.showWindow()

        cmds.window(self.windowName, e=True, w=self.defWidth, h=self.defHeight)
```

現状こちらを

```python:
gui = toolGUI()
gui.show()
```

これで実行すると、
![coordUI_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153835/6ee72190-c345-f845-da3d-0a119fc0c554.png)
こんな感じ。

次は、先ほどのコード内の **# thumbnail UI** のとこに画像表示用のUIを配置します。

PySideで画像を表示するときは<font color="Green">QtWidgets.QGraphicsView</font>を使用します。

画像を表示するだけなら別の方法もあるのですが、表示をコントロールしやすくするためです。

QGraphicsViewでものを描画するには、<font color="Green">QtWidgets.QGraphicsScene</font>を指定してその中に描画する対象を入れます。

今回は画像を描画するので<font color="Green">QtGui.QPixmap</font>を使用します。

MayaでいうとQGraphicsViewがproject、QGraphicsSceneがsceneデータ、QPixmapがテクスチャといった感じでしょうか。

実際は違うと思いますが、まぁ大枠の包含関係はそんな感じです。

というわけで、

```
QGraphicsView作成
 ↓ 
QGraphicsScene作成
 ↓ 
QPixmap作成
 ↓
QPixmapをQGraphicsSceneにセット
 ↓
QGraphicsSceneをQGraphicsViewにセット
```

とやります。

QPixmapは作成時に画像のパスを設定してあげることでその画像のQPixmapオブジェクトが作成されます。

```python:
QtGui.QPixmap('C:/img/test.png')
```

後は、QGraphicsViewの親をMayaのレイアウトUIにしてあげればおっけーです。

このとき注意したいのが、Mayaのレイアウトに存在するアジャスト系フラグが効かないことです。

そのため、そのままレイアウトにQGraphicsViewを放り込むと
![coordUI_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153835/8788ac2d-6c93-c7c1-d1d7-63a88812fe98.png)
こんな感じのただの棒のビューしか表示されません。

わかりにくいですが、Coordinateの文字の下の水色の線がビューです。

なので、任意にレイアウトのサイズを指定してあげます。

このときに一緒にQGraphicsViewのサイズも指定します。

以上をやることで、画像が表示できるようになります。

実際にファンクションにすると下のような感じ。

```python:
    # ---------------------------------------------------------
    # UI : coord frame layout
    # ---------------------------------------------------------
    # @param <uiObj>parent : parent UI
    # @return <uiObj>coordformLayout : form layout
    def frame_coord(self, parent):
        coordformLayout = cmds.formLayout(p=parent)
        # thumbnail row
        self.coordThumbnailLayout = cmds.columnLayout(adj=True, p=coordformLayout)
        thumbLayoutPyside = mayaToPySide(self.coordThumbnailLayout, QtWidgets.QWidget)
        self.coordGView = QtWidgets.QGraphicsView()
        self.coordGView.setParent(thumbLayoutPyside)
        self.coordThumbScene = QtWidgets.QGraphicsScene()
        self.coordThumbScene.clear()
        self.coordThumbScene.addPixmap(QtGui.QPixmap('C:/img/test.png'))
        self.coordGView.setScene(self.coordThumbScene)
        cmds.setParent(coordformLayout)
        # size 
        self.coordGView.resize(self.thumbWidth, self.thumbHeight)
        cmds.columnLayout(self.coordThumbnailLayout, e=True, w=self.thumbWidth, h=self.thumbHeight)

        # coord frame formLayout
        cmds.formLayout(coordformLayout, e=True, ap=[self.coordThumbnailLayout, 'top', 0, 0])
        cmds.formLayout(coordformLayout, e=True, af=[self.coordThumbnailLayout, 'left', 0])
        cmds.formLayout(coordformLayout, e=True, af=[self.coordThumbnailLayout, 'right', 0])
        cmds.setParent('..')
```

PySideを入れたレイアウトをさらに親レイアウトに入れてます。

PySide用のレイアウトを作っておくことで、Maya UIのコマンドでコントロールしやすくするためです。

親レイアウトは、<font color="Orange">optionMenu</font>など別のUIを作ったときにこの親レイアウトにいれることでレイアウトしやすくしています。

後は先ほど作ったMaya UIの下地にこのファンクションを組み込んで、

**\# thumbnail UI**の部分に、<font color="Green">self.frame_coord(coordTabcLayout)</font>と記述してあげるとサンプルが表示されます。

デン☆
![coordUI_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153835/f37f489a-fc91-ade7-55ce-ea082b67c5b7.png)
<br>
<br>

# キャラメイクっぽいのを作ってみる

さて、ここまできたら後少しです。

冒頭のサンプルでやっていることはpng透過した画像を重ねて表示しているだけです。

つまり、QPixmapを必要な分だけ作ってQGraphicsSceneに登録しておきます。

後必要なのは、入力用のUIと入力したときに画像を変える機構です。

入力用のUIは<font color="Orange">optionMenu</font>を追加しているだけなので割愛。

<font color="Orange">optionMenu</font>を作ってうまいこと既存のレイアウト内に配置してください。

次に、画像を変える機構です。

QPixmapで画像のオブジェクトを作ったら、

<font color="Green">pixmap.size().width()</font>と<font color="Green">pixmap.size().height()</font>で縦横のサイズがとれます。

こいつを使ってうまいことフレキシブルに同じ大きさになるようにします。

今回は画像自体を小さくするのではなく、

QGraphicsViewをスケールすることでサイズをコントロールしてみました。

```python:
QGraphicsView.scale(scaleVal, scaleVal)
```

あとは画像の差し替えです。

差し替えるためには、QPixmapを差し替えます。

QGraphicsSceneには、QPixmapをセットしたときにQGraphicsPixmapItemが登録されています。

先ほど、QPixmapはMayaに例えるとテクスチャと言ったわけですが、

Mayaのビューでテクスチャだけでは表示できません。

テクスチャを貼るオブジェクトが必要です。

そのテクスチャを貼るためのオブジェクトがQGraphicsPixmapItemです。

つまり、具体的にはQGraphicsPixmapItemに貼られているQPixmapを差し替えればいいわけです。

QGraphicsSceneからQGraphicsItemを取得して、その中のQGraphicsPixmapItemだけをピックアップします。

```python:
    # ---------------------------------------------------------
    # thumbnail : get imageItem from scene
    # ---------------------------------------------------------
    # @param <QtWidgets.QGraphicsScene>scene : scene object
    # @return <QtWidgets.QGraphicsPixmapItem/List>bgimgList : image object
    def getImgItemFromScn(self, scene):
        bgimgList = []
        for item in scene.items():
            if type(item) == QtWidgets.QGraphicsPixmapItem:
                bgimgList.append(item)
        return bgimgList
```

ファンクションにするとこんな感じ。

QPixmapをセットするにはこんな感じ。

```python:
QGraphicsPixmapItem.setPixmap(pixmap)
```

各部位の画像をこれで重ねつつ変更するわけですが、

ここで注意しないといけないのが重ね合わせる順番です。

QGraphicsSceneからQGraphicsItemを取得したときには、表示されているものが上から順番に取得されます。

つまり、今回のサンプルでいうと、頭、体、脚、後髪といった順番で表示しているQGraphicsPixmapItemが取得されます。

後はこれの取得順を考慮してpixmapを差し替えてあげれば問題ありません。

今回は、画像側で表示位置合わせを行っているため、画像の表示位置はそのままです。

コード側で調整するときは、別途位置調整を加えます。

以上が、画像を変える機構です。

このファンクションを<font color="Orange">optionMenu</font>が切り替わったときに叩くようにします。

何気にめんどくさい画像作成をすっ飛ばして…
<br>
<br>
<br>
<br>

そして、できあがったのがこちら。
![coordUI_01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153835/9afc3dce-8add-bbf2-4799-0444f2203d22.gif)
カワイイヤッター
<br>

<font color="Green">QtWidgets.QGraphicsView</font>を継承してクラスをごにょごにょすることで拡縮移動もできるようにしてみました。

QGraphicsView使ったのはこのためだったんですねーなるほど。

拡縮移動は紙幅(記事幅?)が尽きそうなのと、PySide話になるのでまた別の機会に。
<br>
<br>

# カワイイヤッター

これはあくまで2Dでしたけど、3Dのビューワーにできればもっとすんばらしいですね。

ここまでやるなら全部PySideでやれば…げふんげふん

いや、PySideのUIモジュールを既存のMaya UIに利用できたりするからね！

つかえるつかえる
<br>
<br>

埋め込む方法は他にもっといいやり方があると思うんですが、とりあえず自分はこんな感じでやってみました。

Maya UIに一手間加えたいときの一助になればと。
<br>
<br>

ちなみに、正直コード書くのよりサンプル用に画像作るのが大変でした。

この記事の7割はサンプルコード用の画像作成でできています。
<br>

ということでここまで(:3ﾉｼ　)ﾍ
<br>
<br>

明日は[yamahigashi@github](https://qiita.com/yamahigashi@github)さんによる「decorator を使用したメインメニューへのコマンドの登録」のお話です。

![UCL_logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153835/006b6424-d43c-ef77-8d7d-edd0cd9dfbac.png)

*　※免責事項※
本記事内で公開しているコードの安全性について、当方は一切の保証を与えておりません。
これらのコードを使用したことによって引き起こる損害に対し、当方は一切責任を負うものではありません。
自己責任でご使用ください。*