Maya PySide2 / PySide チュートリアル 初級編 ④ – .uiとPythonのコーディングを組み合わせてみる

python,qt,PySide,PySide2,Tutorial

このチュートリアルでは PySide で.uiとPythonのコーディングと組み合わせる方法を学びます
Mayaのシーン上にあるメッシュをアウトライナーのような感じでリストで表示
表示したメッシュをWidget上で選択できる
選択したメッシュにVertexColorを設定する
という内容を作ってみます
完成イメージは下の画像のようなイメージです
20220429_01
実はSignal&SlotをQtDeisgner上で行うこともできるのですが今回はPython上で行います

QtDeisgnerでレイアウト

リスト表示させることができるWidgetが既にPySideでは用意されており、Item Widgets項目にあるList Widgetと名前の通りのWidgetです
20220429_02
一つ上の項目のItem Views項目にあるList Viewというものもありますが今回はこちらは使用しません
Viewではなく、Widgetを使用しましょう

20220429_04
List WidgetをWindowの左側に配置します

20220429_03
次はCheck BoxをApplyの上に配置します

20220429_05
配置すると上の画像のようにSpinBoxがつぶれてしまうかもしれませんがこれはsizePolicyの問題なのでProperty EditorでsizePolicy>Horizontal Polocy>Preferred, sizePolicy>Vertical Polocy>Preferredに設定してください
下の画像のようになっていたらうまく設定できています
この辺りは実際に起動した際とQtDeisgner上で若干見た目が変わることがあるので自分の意図した見た目になっているのであれば問題ないかと思います
20220429_07

ClassObjectName
QCheckBoxcheckBox_IsSelect
QListWidgetlistWidget

それぞれの名前を上の表のように設定できたら.uiのレイアウトが完成です

コード

# !/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys

import maya.cmds as cmds
import maya.OpenMayaUI as OpenMayaUI

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
    from PySide2.QtUiTools import QUiLoader
    import shiboken2 as shiboken
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *
    from PySide.QtUiTools import QUiLoader
    import shiboken


ptr = OpenMayaUI.MQtUtil.mainWindow()
mayaMainWindow = shiboken.wrapInstance(long(ptr), QMainWindow)


class vtxMainWindow(QMainWindow):
    __currentPath = os.path.dirname(__file__)
    __uiFilePath = os.path.join(__currentPath, "ui", "vtxMain.ui")
    __geometries = []

    def __init__(self, parent=None):
        super(vtxMainWindow, self).__init__(parent)
        loader = QUiLoader()
        uiWidget = loader.load(self.__uiFilePath)
        self.setCentralWidget(uiWidget)
        self.resize(400, 270)
        self.setWindowTitle(".ui MainWindow")
        self.installEventFilter(self)
        self.centralWidget().listWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.__initSignalSlot()

    def __initSignalSlot(self):
        self.centralWidget().button_Apply.clicked.connect(self.setVertexColor)
        self.centralWidget().listWidget.itemClicked.connect(self.selectItem)
        self.centralWidget().checkBox_IsSelect.stateChanged.connect(self.selectItem)

    def setVertexColor(self):
        current = cmds.ls(sl=True)
        meshList = []
        if self.centralWidget().checkBox_IsSelect.isChecked():
            for item in self.centralWidget().listWidget.selectedItems():
                meshList.append(item.text())
        else:
            meshList = cmds.ls(sl=True)

        for mesh in meshList:
            cmds.select(mesh)
            cmds.polyColorPerVertex(
                r=self.centralWidget().doubleSpinBox_R.value(),
                g=self.centralWidget().doubleSpinBox_G.value(),
                b=self.centralWidget().doubleSpinBox_B.value(),
                a=self.centralWidget().doubleSpinBox_A.value()
            )
            cmds.setAttr("%s.displayColors" % mesh, 1)

        cmds.select(current)

    def selectItem(self):
        if self.centralWidget().checkBox_IsSelect.isChecked():
            cmds.select(cl=True)
            for item in self.centralWidget().listWidget.selectedItems():
                cmds.select(item.text(), add=True)

    def eventFilter(self, object, event):
        if event.type() == QEvent.WindowActivate \
                or event.type() == QEvent.FocusOut:
            self.widgetReload()
            return True
        return False

    def widgetReload(self):
        self.centralWidget().listWidget.clear()
        for count, geometry in enumerate(self.geometries):
            self.centralWidget().listWidget.insertItem(count, geometry)

    @property
    def geometries(self):
        self.__geometries = []
        meshlist = cmds.ls(type="mesh")
        for mesh in meshlist:
            self.__geometries.append(cmds.listRelatives(mesh, fullPath=True, parent=True, type="transform")[0])
        return self.__geometries


def main():
    app = QApplication.instance()
    mainWin = vtxMainWindow(parent=mayaMainWindow)
    mainWin.show()
    sys.exit()
    app.exec_()


ひとつ前のパートから5種類の関数が増えました

関数説明
__initSignalSlotSignals&Slotsを行う関数
selectItemlistWidgetで選択したMeshをMaya上で選択状態にする関数
eventFilterクラスvtxMainWindowで起きたイベントの受け取りを行う関数
widgetReloadlistWidgetのQListWidgetItemをリロードする
geometriesMayaシーン内のmeshをlistするプロパティの定義

class vtxMainWindow(QMainWindow):
    __currentPath = os.path.dirname(__file__)
    __uiFilePath = os.path.join(__currentPath, "ui", "vtxMain.ui")
    __geometries = []

    @property
    def geometries(self):
        self.__geometries = []
        meshlist = cmds.ls(type="mesh")
        for mesh in meshlist:
            self.__geometries.append(cmds.listRelatives(mesh, fullPath=True, parent=True, type="transform")[0])
        return self.__geometries

まずはdef geometries(self):から解説していきます
def geometries(self):はシーン内にあるmeshのTransfromを__geometriesにappendし、listを作成しています
試しに呼び出してみるとちゃんとMayaシーン内にあるMeshをリストしてくれていることが確認できます
20220429_08

    def widgetReload(self):
        self.centralWidget().listWidget.clear()
        for count, geometry in enumerate(self.geometries):
            self.centralWidget().listWidget.insertItem(count, geometry)

def widgetReload(self):は呼び出されたタイミングで現在設定されているWidgetItemをすべて削除し、
プロパティgeometriesを呼び出し、再度QListWidgetにinsertItemで項目を追加しています

QListWidgetのサンプル

20220429_09

import sys
from PySide2.QtWidgets import QListWidget, QApplication

class ListWidget(QListWidget):
    def __init__(self):
        QListWidget.__init__(self)
        self.insertItem(1, "Orange")
        self.insertItem(2, "Blue")
        self.insertItem(3, "White")
        self.insertItem(4, "Green")

app = QApplication.instance()
lw = ListWidget()
lw.show()
sys.exit()
app.exec_()

QListWidgetのもっとも小さなサンプルです
insertItemとインサートしたいrow、stringを指定することで項目を追加することができます

Qtのイベント処理

Qtにはアプリケーション内や外部の活動の結果として発生した、例えばWindowがアクティブ、クローズ、フォーカスなどイベント自体を処理するのではなく、配信されたイベントの種類に基づいて、その特定の種類のイベントのイベントハンドラを呼び出し、イベントが受け入れられたか無視されたかに基づいて応答を送信させることができます

イベント通知の受け取りは通常、仮想関数を呼び出すことで例えば、QPaintEventpaintEvent()を呼び出すことによって受け取りすることができます

時には、あるオブジェクトが他のオブジェクトに配信されるイベントを見たり、場合によっては傍受したりする必要があるのですが今回はclass vtxMainWindow(QMainWindow):で発声されるイベントを元にself.widgetReload()を呼び出しています
イベントのタイミングはQEvent.WindowActivateQEvent.FocusOutのタイミングにしています
eventFilterを有効にするにはinstallEventFilterの呼び出しが必要なためself.installEventFilter(self)で呼び出しています

class vtxMainWindow(QMainWindow):

    def __init__(self, parent=None):
        self.installEventFilter(self)

    def eventFilter(self, object, event):
        if event.type() == QEvent.WindowActivate \
                or event.type() == QEvent.FocusOut:
            self.widgetReload()
            return True
        return False

20220429_10
今回のようなツールでは常に監視し、ListWidget上に表示されたmeshが最新である必要はないため、GUIを使用する際に最新の状態になっていればよいとしています
もちろんscriptJobやほかのイベントを使用すれば常に最新と同期させることもできるのでこの辺りはケースバイケースで設定してみてください


    def __init__(self, parent=None):
        self.centralWidget().listWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)

    def selectItem(self):
        if self.centralWidget().checkBox_IsSelect.isChecked():
            cmds.select(cl=True)
            for item in self.centralWidget().listWidget.selectedItems():
                cmds.select(item.text(), add=True)

20220429_11
このコードではcheckBox_IsSelectが有効になっている場合、Maya上の選択と同期し、無効になっている場合はMaya上の選択は変わらず何もしません
複数選択も行いたいので.setSelectionMode(QAbstractItemView.ExtendedSelection)selectionModeを変更しています

    def setVertexColor(self):
        current = cmds.ls(sl=True)
        meshList = []
        if self.centralWidget().checkBox_IsSelect.isChecked():
            for item in self.centralWidget().listWidget.selectedItems():
                meshList.append(item.text())
        else:
            meshList = cmds.ls(sl=True)

        for mesh in meshList:
            cmds.select(mesh)
            cmds.polyColorPerVertex(
                r=self.centralWidget().doubleSpinBox_R.value(),
                g=self.centralWidget().doubleSpinBox_G.value(),
                b=self.centralWidget().doubleSpinBox_B.value(),
                a=self.centralWidget().doubleSpinBox_A.value()
            )
            cmds.setAttr("%s.displayColors" % mesh, 1)

        cmds.select(current)

setVertexColorもcheckBox_IsSelectが有効化無効化で処理が変わるように変更しています
有効の場合はlistWidgetで選択されたメッシュに対してvertexColorの設定
無効の場合はMaya上で選択中のメッシュに対してvertexColorの設定

class vtxMainWindow(QMainWindow):

    def __init__(self, parent=None):
        self.__initSignalSlot()

    def __initSignalSlot(self):
        self.centralWidget().button_Apply.clicked.connect(self.setVertexColor)
        self.centralWidget().listWidget.itemClicked.connect(self.selectItem)
        self.centralWidget().checkBox_IsSelect.stateChanged.connect(self.selectItem)

__initSignalSlotでは、signals&Slotsを行っています
button_Applyでは前回のパートでクリックした際にバーテックスカラーの設定が行われるsetVertexColor
listWidgetでitemClickedした際にselectItem
checkBox_IsSelectではチェックボックスがオンオフの際にselectItemを呼び出しています

20220429_12


Maya PySide2 / PySide チュートリアルのこのパートでは、.uiとUIのコーディングと組み合わせる方法を学びました

次はMaya PySide2 / PySide チュートリアル 初級編 ⑤ – データフォーマットを扱ってみる
前はMaya PySide2 / PySide チュートリアル 初級編 ③ – GUIからMayaのコマンドを実行、BuddyやTab orderの設定方法