MayaのcmdsコマンドのコントロールをQtに組み込む

mayaのgradientコントロールをQtで扱うにはどうするのか試行錯誤した記録です

![2025-01-17_10h37_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125058/abd329a7-a260-685d-7143-d3a75275b2e4.png)
自作の疑似シミュレーションツールを作成した際に、nHairのいくつかのgradientアトリビュートを操作するUIが必要となりました

いろいろと検索したところ、cmdsのコントロールをQtのwidgetに変換するといいようです
### 基本配置
```python
from maya import OpenMayaUI, cmds
from PySide2 import QtWidgets, QtCore
import shiboken2

class MainUI(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(MainUI, self).__init__(parent)
        
        self.setWindowTitle('gradientTest')

        layout = QtWidgets.QVBoxLayout(self)

        # gradientコントロールを作成
        self.grad_ctrl = cmds.gradientControlNoAttr()
        # ポインタを取得しQtWidgetに変換
        ptr = OpenMayaUI.MQtUtil.findControl(self.grad_ctrl)
        self.grad_wdg = shiboken2.wrapInstance(int(ptr), QtWidgets.QWidget)
        # デフォルトの設定を文字列でひとまとめに設定
        cmds.gradientControlNoAttr(
            self.grad_ctrl, e=True, asString='0.1,0.25,1, 0.9,0.75,1')

        layout.addWidget(self.grad_wdg)

def main():
    mayaWindow = shiboken2.wrapInstance(
        int(OpenMayaUI.MQtUtil.mainWindow()),QtWidgets.QWidget)
    existsWindow = mayaWindow.findChildren(QtWidgets.QWidget,'gradTestWindow')
    for w in existsWindow:
        w.close()
    
    ui = MainUI(mayaWindow)
    ui.setObjectName('gradTestWindow')
    ui.setWindowFlags(QtCore.Qt.Window|QtCore.Qt.WindowCloseButtonHint)
    ui.show()
```
![2025-01-17_10h50_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125058/1be12efd-51a7-bfe6-ebbc-51287c574b98.png)
まずはgradientControlNoAttrをcmdsコマンドで作成します
```python
self.grad_ctrl = cmds.gradientControlNoAttr()
```
コントロールのポインタを取得し、Qtのwidgetに変換します
```python
ptr = OpenMayaUI.MQtUtil.findControl(self.grad_ctrl)
self.grad_wdg = shiboken2.wrapInstance(int(ptr), QtWidgets.QWidget)
```

ここでいつもおまじない的に記述していたこの部分の意味がようやく理解できてきました
```python
mayaWindow = shiboken2.wrapInstance(int(OpenMayaUI.MQtUtil.mainWindow()),QtWidgets.QWidget)
```
mayaのメインウインドウのポインタを取得し、Qtのwidgetに変換していたのですね

```python
cmds.gradientControlNoAttr(self.grad_ctrl, e=True, asString='0.1,0.25,1, 0.9,0.75,1')
```
gradientControlNoAttrは全ての制御点の位置、値、補完法を文字列として参照、編集することができます
書式は以下のようになります

'位置 , 値 , 補完法 , 位置 , 値 , 補完法 ...'

ここでは初期設定として編集点二つとそれぞれの数値を設定しています


### 制御UIの追加
コントロールの制御をするwidgetを追加します
```python
self.selpos = QtWidgets.QDoubleSpinBox()
self.selval = QtWidgets.QDoubleSpinBox()
self.selint = QtWidgets.QComboBox()
inters = ['None', 'Linear', 'Smooth', 'Spline']
for it in inters:
    self.selint.addItem(it)
self.selint.setCurrentIndex(1)

layout.addWidget(self.grad_wdg)
layout.addWidget(self.selpos)
layout.addWidget(self.selval)
layout.addWidget(self.selint)
```
![2025-01-17_18h33_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125058/3f4c9b6f-f280-1965-d364-cc3a9606a0ad.png)

### コントロールの変更をUIに反映
gradientコントロールが変更されたときにUIの数値を変更する関数を設定します
```python
self.grad_ctrl = cmds.gradientControlNoAttr(cc=self.keyChanged)
```
関数の定義がこちら
```python
def keyChanged(self, values):
    # 編集点のindex、値、補完法を取得
    key = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, currentKey=True)
    val = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, currentKeyCurveValue=True)
    intr = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, currentKeyInterpValue=True)
    # 文字列で渡されたデータから編集点の位置を取得
    val_list = values.split(',')
    pos = float(val_list[key*3+1])

    self.selpos.setValue(pos)
    self.selval.setValue(val)
    self.selint.setCurrentIndex(intr)
```
コントローラで選択された編集点のindex、値、補完方法はqフラグで取得できますが、位置だけはできません
ccフラグで呼び出した関数にコントローラの位置を含む値の一覧の文字列が自動で渡されるので、それを配列に分解し編集点のindexを元に位置を取得します
取得した位置、値、補完方法をUIに反映します

### UIの変更をコントロールに反映

UIの変更をコントローラに反映させる関数を作成します

```python
def valChanged(self):
    # UIからの入力をコントロールに反映させる
    key = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, currentKey=True)
    pos = self.selpos.value()
    val = self.selval.value()
    intr = self.selint.currentIndex()

    # コントロールの設定を文字列で取得したのち、対応部分を書き換える
    attr_string = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, asString=True)

    attr_list = attr_string.split(',')
    attr_list[key*3] = str(val)
    attr_list[key*3+1] = str(pos)
    attr_list[key*3+2] = str(intr)

    # 文字列に戻して設定
    attr_string = ','.join(attr_list)
    cmds.gradientControlNoAttr(self.grad_ctrl, e=True ,asString=attr_string)
```

先ほどと同様に編集点の位置は単独フラグで設定できないので、asStrigで文字列として設定するようにします

そのため一旦qフラグで取得した設定文字列を分解してUIからの数値で書き直した後に文字列に連結し直しています

### 完成版

```python
from maya import OpenMayaUI, cmds
from PySide2 import QtWidgets, QtCore
import shiboken2

class MainUI(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(MainUI, self).__init__(parent)
        
        self.setWindowTitle('gradientTest')

        layout = QtWidgets.QVBoxLayout(self)

        # gradientコントロールを作成
        self.grad_ctrl = cmds.gradientControlNoAttr(cc=self.keyChanged)
        # ポインタを取得しQtWidgetに変換
        ptr = OpenMayaUI.MQtUtil.findControl(self.grad_ctrl)
        self.grad_wdg = shiboken2.wrapInstance(int(ptr), QtWidgets.QWidget)
        # デフォルトの設定を文字列でひとまとめに設定
        cmds.gradientControlNoAttr(
            self.grad_ctrl, e=True, asString='0.1,0.25,1, 0.9,0.75,1')

        grid_layout = QtWidgets.QGridLayout(self)
        # コントローラの数値を入力するUI
        label_pos = QtWidgets.QLabel('selectedPos')
        label_val = QtWidgets.QLabel('selectedVal')
        label_int = QtWidgets.QLabel('interpolate')
        self.selpos = QtWidgets.QDoubleSpinBox()
        self.selval = QtWidgets.QDoubleSpinBox()
        self.selint = QtWidgets.QComboBox()
        inters = ['None', 'Linear', 'Smooth', 'Spline']
        for it in inters:
            self.selint.addItem(it)
        self.selint.setCurrentIndex(1)

        grid_layout.addWidget(label_pos, 0, 0)
        grid_layout.addWidget(self.selpos, 0, 1)
        grid_layout.addWidget(label_val, 1, 0)
        grid_layout.addWidget(self.selval, 1, 1)
        grid_layout.addWidget(label_int, 2, 0)
        grid_layout.addWidget(self.selint)

        layout.addWidget(self.grad_wdg)
        layout.addLayout(grid_layout)

        self.selpos.valueChanged.connect(self.valChanged)
        self.selval.valueChanged.connect(self.valChanged)
        self.selint.currentIndexChanged.connect(self.valChanged)

    def keyChanged(self, values):
        # 編集点のindex、値、補完法を取得
        key = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, currentKey=True)
        val = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, currentKeyCurveValue=True)
        intr = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, currentKeyInterpValue=True)
        # 文字列で渡されたデータから編集点の位置を取得
        val_list = values.split(',')
        pos = float(val_list[key*3+1])

        self.selpos.setValue(pos)
        self.selval.setValue(val)
        self.selint.setCurrentIndex(intr)

    def valChanged(self):
        # UIからの入力をコントロールに反映させる
        key = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, currentKey=True)
        pos = self.selpos.value()
        val = self.selval.value()
        intr = self.selint.currentIndex()

        # コントロールの設定を文字列で取得したのち、対応部分を書き換える
        attr_string = cmds.gradientControlNoAttr(self.grad_ctrl, q=True, asString=True)

        attr_list = attr_string.split(',')
        attr_list[key*3] = str(val)
        attr_list[key*3+1] = str(pos)
        attr_list[key*3+2] = str(intr)

        # 文字列に戻して設定
        attr_string = ','.join(attr_list)
        cmds.gradientControlNoAttr(self.grad_ctrl, e=True ,asString=attr_string)

def main():
    mayaWindow = shiboken2.wrapInstance(
        int(OpenMayaUI.MQtUtil.mainWindow()),QtWidgets.QWidget)
    existsWindow = mayaWindow.findChildren(QtWidgets.QWidget,'gradTestWindow')
    for w in existsWindow:
        w.close()
    
    ui = MainUI(mayaWindow)
    ui.setObjectName('gradTestWindow')
    ui.setWindowFlags(QtCore.Qt.Window|QtCore.Qt.WindowCloseButtonHint)
    ui.show()
```

pymelを使ってgradientContolを設定する方法もあり、その方がすっきり記述できるのですが、最近のmayaでpymelが使えなくなっている問題がありましたのでcmdsで記述しました．

より良い方法がありましたら是非お教えいただけると助かります．